feat: 优化架构
This commit is contained in:
576
src/services/auth/authService.ts
Normal file
576
src/services/auth/authService.ts
Normal file
@@ -0,0 +1,576 @@
|
||||
/**
|
||||
* 认证服务
|
||||
* 处理用户登录、注册、登出等认证功能
|
||||
*/
|
||||
|
||||
import { api } from '../core/api';
|
||||
import { User } from '@/types';
|
||||
import type { PrivacySettingsDTO, PrivacySettingsRequestDTO, DeletionStatusDTO } from '@/types/dto';
|
||||
|
||||
export function resolveAuthApiError(error: any, fallback = '操作失败,请稍后重试'): string {
|
||||
const code: number = error?.code ?? 0;
|
||||
const msg: string = String(error?.message ?? '').toLowerCase();
|
||||
|
||||
if (msg.includes('network request failed') || msg.includes('failed to fetch')) {
|
||||
return '网络连接失败,请检查网络后重试';
|
||||
}
|
||||
if (code === 429 || msg.includes('too frequently')) {
|
||||
return '验证码发送过于频繁,请稍后再试';
|
||||
}
|
||||
if (msg.includes('verification code expired')) {
|
||||
return '验证码已过期,请重新获取';
|
||||
}
|
||||
if (msg.includes('invalid verification code')) {
|
||||
return '验证码错误,请重新输入';
|
||||
}
|
||||
if (msg.includes('email already exists')) {
|
||||
return '该邮箱已被注册,请直接登录';
|
||||
}
|
||||
if (msg.includes('email not bound')) {
|
||||
return '当前账号未绑定邮箱,请先完成邮箱绑定验证';
|
||||
}
|
||||
if (msg.includes('user not found')) {
|
||||
return '该邮箱尚未注册';
|
||||
}
|
||||
if (msg.includes('email service unavailable')) {
|
||||
return '邮件服务暂不可用,请稍后再试';
|
||||
}
|
||||
if (msg.includes('invalid email')) {
|
||||
return '邮箱格式不正确';
|
||||
}
|
||||
if (msg.includes('invalid username or password')) {
|
||||
return '账号或密码错误';
|
||||
}
|
||||
if (typeof error?.message === 'string' && error.message.trim().length > 0) {
|
||||
return error.message;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
// 登录请求参数
|
||||
export interface LoginRequest {
|
||||
username?: string;
|
||||
account?: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
// 注册请求参数
|
||||
export interface RegisterRequest {
|
||||
username: string;
|
||||
password: string;
|
||||
nickname: string;
|
||||
email: string;
|
||||
verification_code: string;
|
||||
phone?: string;
|
||||
}
|
||||
|
||||
export interface SendEmailCodeRequest {
|
||||
email: string;
|
||||
}
|
||||
|
||||
export interface ResetPasswordRequest {
|
||||
email: string;
|
||||
verification_code: string;
|
||||
new_password: string;
|
||||
}
|
||||
|
||||
export interface VerifyCurrentUserEmailRequest {
|
||||
email: string;
|
||||
verification_code: string;
|
||||
}
|
||||
|
||||
// 认证响应
|
||||
export interface AuthResponse {
|
||||
token: string;
|
||||
refreshToken?: string;
|
||||
refresh_token?: string; // 兼容后端的snake_case
|
||||
user: User;
|
||||
}
|
||||
|
||||
// 刷新Token响应
|
||||
export interface RefreshTokenResponse {
|
||||
token: string;
|
||||
refreshToken?: string;
|
||||
refresh_token?: string; // 兼容后端的snake_case
|
||||
}
|
||||
|
||||
// 用户信息响应 - API直接返回User对象,不需要包装
|
||||
export type UserResponse = User;
|
||||
|
||||
// 更新用户信息请求
|
||||
export interface UpdateUserRequest {
|
||||
nickname?: string;
|
||||
avatar?: string;
|
||||
bio?: string;
|
||||
website?: string;
|
||||
location?: string;
|
||||
phone?: string;
|
||||
email?: string;
|
||||
}
|
||||
|
||||
export interface BlockedUserListResponse {
|
||||
list: User[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
total_pages: number;
|
||||
}
|
||||
|
||||
export interface BlockStatusResponse {
|
||||
is_blocked: boolean;
|
||||
}
|
||||
|
||||
// 认证服务类
|
||||
// 职责:纯 HTTP 层 + Token 管理,不涉及 SQLite
|
||||
// SQLite 的读写编排由各领域 Manager/store 统一负责
|
||||
class AuthService {
|
||||
// ── 内部辅助:从服务器获取用户 ──
|
||||
private async refreshCurrentUserFromServer(): Promise<User | null> {
|
||||
try {
|
||||
const response = await api.get<UserResponse>('/users/me');
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('[AuthService] 获取当前用户失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async refreshUserByIdFromServer(userId: string): Promise<User | null> {
|
||||
try {
|
||||
const response = await api.get<UserResponse>(`/users/${userId}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('[AuthService] 获取用户信息失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ── 公开 API:仅做 HTTP 请求 + Token 存储,不碰 SQLite ──
|
||||
|
||||
// 用户登录(只管 API + Token,DB 操作交给 authStore)
|
||||
async login(data: LoginRequest): Promise<AuthResponse> {
|
||||
const response = await api.post<AuthResponse>('/auth/login', data);
|
||||
|
||||
if (!response.data) {
|
||||
throw new Error('登录响应数据为空');
|
||||
}
|
||||
if (response.data.token) {
|
||||
await api.setToken(response.data.token);
|
||||
}
|
||||
const refreshToken = response.data.refresh_token || response.data.refreshToken;
|
||||
if (refreshToken) {
|
||||
await api.setRefreshToken(refreshToken);
|
||||
}
|
||||
api.resetAuthState();
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// 用户注册(只管 API + Token,DB 操作交给 authStore)
|
||||
async register(data: RegisterRequest): Promise<AuthResponse> {
|
||||
const response = await api.post<AuthResponse>('/auth/register', data);
|
||||
|
||||
if (!response.data) {
|
||||
throw new Error('注册响应数据为空');
|
||||
}
|
||||
if (response.data.token) {
|
||||
await api.setToken(response.data.token);
|
||||
}
|
||||
const refreshToken = response.data.refresh_token || response.data.refreshToken;
|
||||
if (refreshToken) {
|
||||
await api.setRefreshToken(refreshToken);
|
||||
}
|
||||
api.resetAuthState();
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// 发送注册验证码
|
||||
async sendRegisterCode(email: string): Promise<boolean> {
|
||||
const response = await api.post<{ success: boolean }>('/auth/register/send-code', { email } as SendEmailCodeRequest);
|
||||
return response.code === 0;
|
||||
}
|
||||
|
||||
// 发送找回密码验证码
|
||||
async sendPasswordResetCode(email: string): Promise<boolean> {
|
||||
const response = await api.post<{ success: boolean }>('/auth/password/send-code', { email } as SendEmailCodeRequest);
|
||||
return response.code === 0;
|
||||
}
|
||||
|
||||
// 找回密码重置
|
||||
async resetPassword(data: ResetPasswordRequest): Promise<boolean> {
|
||||
const response = await api.post<{ success: boolean }>('/auth/password/reset', data);
|
||||
return response.code === 0;
|
||||
}
|
||||
|
||||
// 已登录用户发送邮箱验证验证码
|
||||
async sendCurrentUserEmailVerifyCode(email: string): Promise<boolean> {
|
||||
const response = await api.post<{ success: boolean }>('/users/me/email/send-code', { email } as SendEmailCodeRequest);
|
||||
return response.code === 0;
|
||||
}
|
||||
|
||||
// 已登录用户提交邮箱验证码
|
||||
async verifyCurrentUserEmail(data: VerifyCurrentUserEmailRequest): Promise<boolean> {
|
||||
const response = await api.post<{ success: boolean }>('/users/me/email/verify', data);
|
||||
return response.code === 0;
|
||||
}
|
||||
|
||||
// 用户登出(只管 API + Token,DB/缓存清理交给 authStore)
|
||||
async logout(): Promise<void> {
|
||||
try {
|
||||
await api.post('/auth/logout');
|
||||
} catch (error) {
|
||||
console.error('[AuthService] 登出 API 失败:', error);
|
||||
} finally {
|
||||
await api.clearToken();
|
||||
}
|
||||
}
|
||||
|
||||
// 纯 API 获取当前用户(无任何 DB 依赖,供冷启动校验 Token 时使用)
|
||||
async fetchCurrentUserFromAPI(): Promise<User | null> {
|
||||
try {
|
||||
const response = await api.get<UserResponse>('/users/me');
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
// 401 错误(未登录/登录过期)是预期情况,不需要打印错误日志
|
||||
const isAuthError = error?.code === 401 ||
|
||||
String(error?.message ?? '').includes('登录') ||
|
||||
String(error?.message ?? '').includes('token') ||
|
||||
String(error?.message ?? '').includes('unauthorized');
|
||||
|
||||
if (!isAuthError) {
|
||||
console.error('[AuthService] fetchCurrentUserFromAPI 失败:', error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新Token
|
||||
async refreshToken(): Promise<RefreshTokenResponse | null> {
|
||||
try {
|
||||
const refreshToken = await api.getRefreshToken();
|
||||
if (!refreshToken) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const response = await api.post<RefreshTokenResponse>('/auth/refresh', {
|
||||
refresh_token: refreshToken,
|
||||
});
|
||||
|
||||
if (!response.data) {
|
||||
return null;
|
||||
}
|
||||
if (response.data.token) {
|
||||
await api.setToken(response.data.token);
|
||||
}
|
||||
const newRefreshToken = response.data.refresh_token || response.data.refreshToken;
|
||||
if (newRefreshToken) {
|
||||
await api.setRefreshToken(newRefreshToken);
|
||||
}
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('刷新Token失败:', error);
|
||||
await api.clearToken();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前用户信息(兼容旧调用,始终走服务端)
|
||||
async getCurrentUser(): Promise<User | null> {
|
||||
return this.refreshCurrentUserFromServer();
|
||||
}
|
||||
|
||||
// 强制从服务器获取当前用户信息(别名,保留兼容)
|
||||
async fetchCurrentUserFresh(): Promise<User | null> {
|
||||
return this.fetchCurrentUserFromAPI();
|
||||
}
|
||||
|
||||
async fetchUserByIdFromAPI(userId: string): Promise<User | null> {
|
||||
return this.refreshUserByIdFromServer(userId);
|
||||
}
|
||||
|
||||
// 获取用户信息(根据用户ID)
|
||||
async getUserById(userId: string, forceRefresh = false): Promise<User | null> {
|
||||
void forceRefresh; // 兼容旧签名:缓存由 userManager 统一编排
|
||||
return this.refreshUserByIdFromServer(userId);
|
||||
}
|
||||
|
||||
// 更新用户信息
|
||||
async updateUser(data: UpdateUserRequest): Promise<User | null> {
|
||||
try {
|
||||
const response = await api.put<UserResponse>('/users/me', data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('更新用户信息失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 修改密码
|
||||
async sendChangePasswordCode(): Promise<boolean> {
|
||||
const response = await api.post<{ success: boolean }>('/users/change-password/send-code');
|
||||
return response.code === 0;
|
||||
}
|
||||
|
||||
// 修改密码(需要邮箱验证码)
|
||||
async changePassword(oldPassword: string, newPassword: string, verificationCode: string): Promise<boolean> {
|
||||
try {
|
||||
const response = await api.post('/users/change-password', {
|
||||
old_password: oldPassword,
|
||||
new_password: newPassword,
|
||||
verification_code: verificationCode,
|
||||
});
|
||||
return response.code === 0;
|
||||
} catch (error) {
|
||||
console.error('修改密码失败:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查用户名是否可用
|
||||
async checkUsernameAvailable(username: string): Promise<boolean> {
|
||||
try {
|
||||
const response = await api.get<{ available: boolean }>('/auth/check-username', {
|
||||
username,
|
||||
});
|
||||
return response.data.available;
|
||||
} catch (error) {
|
||||
console.error('检查用户名失败:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 关注用户
|
||||
async followUser(userId: string): Promise<boolean> {
|
||||
try {
|
||||
const response = await api.post(`/users/${userId}/follow`);
|
||||
return response.code === 0;
|
||||
} catch (error) {
|
||||
console.error('关注用户失败:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 取消关注用户
|
||||
async unfollowUser(userId: string): Promise<boolean> {
|
||||
try {
|
||||
const response = await api.delete(`/users/${userId}/follow`);
|
||||
return response.code === 0;
|
||||
} catch (error) {
|
||||
console.error('取消关注用户失败:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取用户关注列表
|
||||
async getFollowingList(userId: string, page = 1, pageSize = 20): Promise<User[]> {
|
||||
try {
|
||||
const response = await api.get<{ list: User[] }>(`/users/${userId}/following`, {
|
||||
page,
|
||||
page_size: pageSize,
|
||||
});
|
||||
return response.data.list;
|
||||
} catch (error) {
|
||||
console.error('获取关注列表失败:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// 获取用户粉丝列表
|
||||
async getFollowersList(userId: string, page = 1, pageSize = 20): Promise<User[]> {
|
||||
try {
|
||||
const response = await api.get<{ list: User[] }>(`/users/${userId}/followers`, {
|
||||
page,
|
||||
page_size: pageSize,
|
||||
});
|
||||
return response.data.list;
|
||||
} catch (error) {
|
||||
console.error('获取粉丝列表失败:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// 拉黑用户
|
||||
async blockUser(userId: string): Promise<boolean> {
|
||||
try {
|
||||
const response = await api.post(`/users/${userId}/block`);
|
||||
return response.code === 0;
|
||||
} catch (error) {
|
||||
console.error('拉黑用户失败:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 取消拉黑
|
||||
async unblockUser(userId: string): Promise<boolean> {
|
||||
try {
|
||||
const response = await api.delete(`/users/${userId}/block`);
|
||||
return response.code === 0;
|
||||
} catch (error) {
|
||||
console.error('取消拉黑失败:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取黑名单
|
||||
async getBlockedUsers(page = 1, pageSize = 20): Promise<BlockedUserListResponse> {
|
||||
try {
|
||||
const response = await api.get<BlockedUserListResponse>('/users/blocks', {
|
||||
page,
|
||||
page_size: pageSize,
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('获取黑名单失败:', error);
|
||||
return {
|
||||
list: [],
|
||||
total: 0,
|
||||
page,
|
||||
page_size: pageSize,
|
||||
total_pages: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 获取拉黑状态
|
||||
async getBlockStatus(userId: string): Promise<boolean> {
|
||||
try {
|
||||
const response = await api.get<BlockStatusResponse>(`/users/${userId}/block-status`);
|
||||
return !!response.data.is_blocked;
|
||||
} catch (error) {
|
||||
console.error('获取拉黑状态失败:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索用户
|
||||
async searchUsers(keyword: string, page = 1, pageSize = 20): Promise<{ list: User[]; total: number }> {
|
||||
try {
|
||||
const response = await api.get<{ list: User[]; total: number }>('/users/search', {
|
||||
keyword,
|
||||
page,
|
||||
page_size: pageSize,
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('搜索用户失败:', error);
|
||||
return { list: [], total: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
// 隐私设置
|
||||
async getPrivacySettings(): Promise<PrivacySettingsDTO | null> {
|
||||
try {
|
||||
const response = await api.get<PrivacySettingsDTO>('/users/me/privacy');
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('获取隐私设置失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async updatePrivacySettings(settings: PrivacySettingsRequestDTO): Promise<boolean> {
|
||||
try {
|
||||
await api.put('/users/me/privacy', settings);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('更新隐私设置失败:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 账号注销
|
||||
async requestAccountDeletion(password: string): Promise<DeletionStatusDTO | null> {
|
||||
try {
|
||||
const response = await api.post<DeletionStatusDTO>('/users/me/deactivate', {
|
||||
password,
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('申请注销失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async cancelAccountDeletion(): Promise<boolean> {
|
||||
try {
|
||||
await api.delete('/users/me/deactivate');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('取消注销失败:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async getDeletionStatus(): Promise<DeletionStatusDTO | null> {
|
||||
try {
|
||||
const response = await api.get<DeletionStatusDTO>('/users/me/deletion-status');
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('获取注销状态失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 二维码登录相关接口 ──
|
||||
|
||||
export interface QRCodeSession {
|
||||
session_id: string;
|
||||
qrcode_url: string;
|
||||
expires_in: number;
|
||||
expires_at: number;
|
||||
}
|
||||
|
||||
export interface ScanResponse {
|
||||
user: {
|
||||
id: string;
|
||||
nickname: string;
|
||||
avatar: string;
|
||||
};
|
||||
}
|
||||
|
||||
// 二维码登录 API(扩展 AuthService)
|
||||
export const qrcodeApi = {
|
||||
/**
|
||||
* 获取二维码
|
||||
*/
|
||||
getQRCode: async (): Promise<QRCodeSession> => {
|
||||
const response = await api.get<QRCodeSession>('/auth/qrcode');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 扫描二维码
|
||||
*/
|
||||
scan: async (sessionId: string): Promise<ScanResponse> => {
|
||||
const response = await api.post<ScanResponse>('/auth/qrcode/scan', {
|
||||
session_id: sessionId,
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 确认登录
|
||||
*/
|
||||
confirm: async (sessionId: string): Promise<{ success: boolean }> => {
|
||||
const response = await api.post<{ success: boolean }>('/auth/qrcode/confirm', {
|
||||
session_id: sessionId,
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 取消登录
|
||||
*/
|
||||
cancel: async (sessionId: string): Promise<{ success: boolean }> => {
|
||||
const response = await api.post<{ success: boolean }>('/auth/qrcode/cancel', {
|
||||
session_id: sessionId,
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
// 导出认证服务实例
|
||||
export const authService = new AuthService();
|
||||
18
src/services/auth/index.ts
Normal file
18
src/services/auth/index.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export { authService, resolveAuthApiError, qrcodeApi } from './authService';
|
||||
export type {
|
||||
LoginRequest,
|
||||
RegisterRequest,
|
||||
SendEmailCodeRequest,
|
||||
ResetPasswordRequest,
|
||||
VerifyCurrentUserEmailRequest,
|
||||
AuthResponse,
|
||||
RefreshTokenResponse,
|
||||
UpdateUserRequest,
|
||||
BlockedUserListResponse,
|
||||
BlockStatusResponse,
|
||||
QRCodeSession,
|
||||
ScanResponse,
|
||||
} from './authService';
|
||||
|
||||
export { verificationService } from './verificationService';
|
||||
export type { SubmitVerificationResponse } from './verificationService';
|
||||
68
src/services/auth/verificationService.ts
Normal file
68
src/services/auth/verificationService.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* 身份认证服务
|
||||
* 处理用户身份认证申请、状态查询等功能
|
||||
*/
|
||||
|
||||
import { api } from '../core/api';
|
||||
import {
|
||||
VerificationStatusDTO,
|
||||
VerificationRecordDTO,
|
||||
SubmitVerificationRequest,
|
||||
PaginatedData,
|
||||
} from '@/types/dto';
|
||||
|
||||
// 提交认证响应
|
||||
export interface SubmitVerificationResponse {
|
||||
id: string;
|
||||
user_id: string;
|
||||
identity: string;
|
||||
status: string;
|
||||
real_name: string;
|
||||
student_id?: string;
|
||||
employee_id?: string;
|
||||
department?: string;
|
||||
proof_materials: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
class VerificationService {
|
||||
// 获取当前用户的认证状态
|
||||
async getVerificationStatus(): Promise<VerificationStatusDTO | null> {
|
||||
try {
|
||||
const response = await api.get<VerificationStatusDTO>('/verification/status');
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('[VerificationService] 获取认证状态失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 提交认证申请
|
||||
async submitVerification(data: SubmitVerificationRequest): Promise<SubmitVerificationResponse | null> {
|
||||
try {
|
||||
const response = await api.post<SubmitVerificationResponse>('/verification/submit', data);
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
console.error('[VerificationService] 提交认证申请失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取认证记录列表
|
||||
async getVerificationRecords(page = 1, pageSize = 10): Promise<PaginatedData<VerificationRecordDTO> | null> {
|
||||
try {
|
||||
const response = await api.get<PaginatedData<VerificationRecordDTO>>('/verification/records', {
|
||||
page,
|
||||
page_size: pageSize,
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('[VerificationService] 获取认证记录失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 导出服务实例
|
||||
export const verificationService = new VerificationService();
|
||||
Reference in New Issue
Block a user