- Add QR code login flow with scanner and confirmation screens - Implement swipe-to-reply in chat with SwipeableMessageBubble - Replace FlatList with FlashList for chat performance optimization - Add Schedule stack navigator with CourseDetail screen support - Configure deep linking for QR code login (carrotbbs://qrcode/login/:sessionId) - Update branding from 胡萝卜 to 萝卜社区 - Display dynamic app version in settings
495 lines
14 KiB
TypeScript
495 lines
14 KiB
TypeScript
/**
|
||
* 认证服务
|
||
* 处理用户登录、注册、登出等认证功能
|
||
*/
|
||
|
||
import { api } from './api';
|
||
import { User } from '../types';
|
||
|
||
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.token) {
|
||
await api.setToken(response.data.token);
|
||
}
|
||
const refreshToken = response.data.refresh_token || response.data.refreshToken;
|
||
if (refreshToken) {
|
||
await api.setRefreshToken(refreshToken);
|
||
}
|
||
|
||
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.token) {
|
||
await api.setToken(response.data.token);
|
||
}
|
||
const refreshToken = response.data.refresh_token || response.data.refreshToken;
|
||
if (refreshToken) {
|
||
await api.setRefreshToken(refreshToken);
|
||
}
|
||
|
||
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) {
|
||
console.error('[AuthService] fetchCurrentUserFromAPI 失败:', error);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
// 刷新Token
|
||
async refreshToken(): Promise<RefreshTokenResponse | null> {
|
||
try {
|
||
const response = await api.post<RefreshTokenResponse>('/auth/refresh');
|
||
|
||
if (response.data.token) {
|
||
await api.setToken(response.data.token);
|
||
}
|
||
if (response.data.refreshToken) {
|
||
await api.setRefreshToken(response.data.refreshToken);
|
||
}
|
||
|
||
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 };
|
||
}
|
||
}
|
||
}
|
||
|
||
// ── 二维码登录相关接口 ──
|
||
|
||
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();
|