| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532 |
- import { userApi, User, RegisterRequest as ApiRegisterRequest, LoginResponse as ApiLoginResponse, TokenVerifyResponse } from './userApi';
- // 用户信息接口(兼容现有代码)
- export interface UserInfo {
- id: string;
- nickname: string;
- phone?: string;
- email?: string;
- avatar?: string;
- registrationDate?: string;
- apikey?: string; // 添加API密钥字段
- realName?: string; // 真实姓名
- isVerified?: string; // 认证状态
- verifiedAt?: string; // 认证时间
- }
- // 登录响应接口(兼容现有代码)
- export interface LoginResponse {
- code: number;
- message: string;
- data: {
- token: string;
- refreshToken?: string;
- expiresIn?: number;
- user: UserInfo;
- };
- }
- // 注册请求接口
- export interface RegisterRequest {
- username: string;
- password: string;
- email?: string;
- phone?: string;
- nickname?: string;
- schoolEmail?: string; // 学校邮箱(可选)
- sms_code?: string;
- email_code?: string;
- }
- // 注册响应接口
- export interface RegisterResponse {
- code: number;
- message: string;
- data: {
- userId: string;
- user?: UserInfo;
- };
- }
- // 转换后端用户数据为前端格式
- function convertUserToUserInfo(user: User): UserInfo {
- return {
- id: user.id,
- nickname: user.nickname,
- phone: user.phone || undefined,
- email: user.email || undefined,
- avatar: user.avatar || undefined,
- registrationDate: user.registration_date,
- apikey: user.apikey || undefined,
- realName: user.real_name || undefined,
- isVerified: user.is_verified || 'unverified',
- verifiedAt: user.verified_at || undefined,
- };
- }
- // Token 存储键名
- const TOKEN_KEY = 'aigc_space_token';
- const REFRESH_TOKEN_KEY = 'aigc_space_refresh_token';
- const USER_INFO_KEY = 'aigc_space_user_info';
- const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8010';
- // 认证状态变化事件名
- const AUTH_CHANGE_EVENT = 'auth_state_change';
- /**
- * 鉴权服务类
- * 负责管理用户认证、token 存储和 API 请求鉴权
- */
- class AuthService {
- private token: string | null = null;
- private refreshToken: string | null = null;
- private userInfo: UserInfo | null = null;
- constructor() {
- // 从 localStorage 恢复 token 和用户信息
- this.loadFromStorage();
- }
- /**
- * 订阅认证状态变化
- * @param callback 状态变化时的回调函数
- * @returns 取消订阅的函数
- */
- subscribe(callback: () => void): () => void {
- const handler = (e: Event) => {
- // console.log('[AuthService] event handler triggered');
- callback();
- };
- window.addEventListener(AUTH_CHANGE_EVENT, handler);
- console.log('[AuthService] subscribed to', AUTH_CHANGE_EVENT);
- return () => {
- window.removeEventListener(AUTH_CHANGE_EVENT, handler);
- // console.log('[AuthService] unsubscribed from', AUTH_CHANGE_EVENT);
- };
- }
- /**
- * 通知所有订阅者状态已变化
- */
- private notifyListeners(): void {
- console.log('[AuthService] notifyListeners called');
- // 触发自定义事件,用于跨组件通信
- if (typeof window !== 'undefined') {
- const event = new CustomEvent(AUTH_CHANGE_EVENT);
- console.log('[AuthService] dispatching event:', AUTH_CHANGE_EVENT);
- window.dispatchEvent(event);
- }
- }
- /**
- * 从 localStorage 加载 token 和用户信息
- */
- private loadFromStorage(): void {
- if (typeof window !== 'undefined') {
- const token = localStorage.getItem(TOKEN_KEY);
- const refreshToken = localStorage.getItem(REFRESH_TOKEN_KEY);
-
- // 确保 token 不是空字符串
- this.token = token && token.trim() !== '' ? token : null;
- this.refreshToken = refreshToken && refreshToken.trim() !== '' ? refreshToken : null;
-
- const userInfoStr = localStorage.getItem(USER_INFO_KEY);
- if (userInfoStr) {
- try {
- this.userInfo = JSON.parse(userInfoStr);
- } catch (e) {
- console.error('Failed to parse user info from storage:', e);
- this.userInfo = null;
- }
- }
- }
- }
- /**
- * 保存 token 和用户信息到 localStorage
- */
- private saveToStorage(token: string, userInfo: UserInfo, refreshToken?: string): void {
- if (typeof window !== 'undefined') {
- localStorage.setItem(TOKEN_KEY, token);
- if (refreshToken) {
- localStorage.setItem(REFRESH_TOKEN_KEY, refreshToken);
- }
- localStorage.setItem(USER_INFO_KEY, JSON.stringify(userInfo));
- }
- this.token = token;
- this.refreshToken = refreshToken || null;
- this.userInfo = userInfo;
- // 通知订阅者状态已变化
- this.notifyListeners();
- }
- /**
- * 清除存储的 token 和用户信息
- */
- private clearStorage(): void {
- console.log('[AuthService] clearStorage called');
- if (typeof window !== 'undefined') {
- localStorage.removeItem(TOKEN_KEY);
- localStorage.removeItem(REFRESH_TOKEN_KEY);
- localStorage.removeItem(USER_INFO_KEY);
- console.log('[AuthService] localStorage cleared');
- }
- this.token = null;
- this.refreshToken = null;
- this.userInfo = null;
- console.log('[AuthService] instance state cleared, notifying listeners');
- // 通知订阅者状态已变化
- this.notifyListeners();
- }
- /**
- * SSO一键登录(通过学校网站token)
- * @param ssoToken 学校网站提供的SSO token
- * @returns Promise<LoginResponse>
- */
- async ssoLogin(ssoToken: string): Promise<LoginResponse> {
- try {
- const response = await fetch(`${API_BASE_URL}/api/auth/verify`, {
- method: 'GET',
- headers: {
- 'Content-Type': 'application/json',
- Authorization: `Bearer ${ssoToken}`,
- },
- });
- if (!response.ok) {
- return {
- code: response.status,
- message: 'SSO登录失败,票据已失效或无效',
- data: {
- token: '',
- user: {
- id: '',
- nickname: '',
- },
- },
- };
- }
- const verifyResult = (await response.json()) as TokenVerifyResponse;
- const userInfo = convertUserToUserInfo(verifyResult.user);
- this.saveToStorage(ssoToken, userInfo);
- return {
- code: 200,
- message: 'SSO登录成功',
- data: {
- token: ssoToken,
- user: userInfo,
- },
- };
- } catch (error) {
- return {
- code: 500,
- message: error instanceof Error ? error.message : 'SSO登录失败',
- data: {
- token: '',
- user: {
- id: '',
- nickname: '',
- },
- },
- };
- }
- }
- /**
- * 查询后端 SSO 配置(含是否启用、CAS 登录地址等)
- */
- async getSsoConfig(): Promise<{ sso_enabled: boolean; cas_login_url: string } | null> {
- try {
- const res = await fetch(`${API_BASE_URL}/api/sso/config`);
- if (!res.ok) return null;
- return await res.json();
- } catch {
- return null;
- }
- }
- /**
- * 查询后端 SSO 是否启用
- */
- async isSsoEnabled(): Promise<boolean> {
- const cfg = await this.getSsoConfig();
- if (cfg === null) return false; // 请求失败时默认不启用SSO,跳普通登录页
- return cfg.sso_enabled === true;
- }
- /**
- * 登录
- * @param username 用户名或手机号
- * @param password 密码
- * @param keyId 会话密钥ID
- * @returns Promise<LoginResponse>
- */
- async login(username: string, password: string, keyId: string | null = null): Promise<LoginResponse> {
- try {
- const apiResponse = await userApi.login(username, password, keyId);
-
- const userInfo = convertUserToUserInfo(apiResponse.user);
-
- // 保存 token 和用户信息
- this.saveToStorage(apiResponse.access_token, userInfo);
- return {
- code: 200,
- message: '登录成功',
- data: {
- token: apiResponse.access_token,
- user: userInfo,
- }
- };
- } catch (error) {
- return {
- code: 401,
- message: error instanceof Error ? error.message : '登录失败',
- data: {
- token: '',
- user: {
- id: '',
- nickname: '',
- }
- }
- };
- }
- }
- /**
- * 注册
- * @param registerData 注册信息
- * @param keyId 会话密钥ID
- * @returns Promise<RegisterResponse>
- */
- async register(registerData: RegisterRequest, keyId: string | null = null): Promise<RegisterResponse> {
- try {
- const apiRegisterData: ApiRegisterRequest = {
- username: registerData.username,
- password: registerData.password,
- nickname: registerData.nickname || registerData.username,
- email: registerData.email || registerData.schoolEmail,
- phone: registerData.phone,
- sms_code: registerData.sms_code,
- email_code: registerData.email_code,
- };
- const user = await userApi.register(apiRegisterData, keyId);
- const userInfo = convertUserToUserInfo(user);
- return {
- code: 200,
- message: '注册成功',
- data: {
- userId: user.id,
- user: userInfo,
- }
- };
- } catch (error) {
- return {
- code: 400,
- message: error instanceof Error ? error.message : '注册失败',
- data: {
- userId: '',
- }
- };
- }
- }
- /**
- * 登出
- * @param callApi 是否调用后端API(默认true)
- */
- async logout(callApi: boolean = true): Promise<void> {
- console.log('[AuthService] logout called');
-
- // 尝试调用后端登出API
- if (callApi && this.token) {
- try {
- await userApi.logout();
- console.log('[AuthService] backend logout successful');
- } catch (error) {
- console.warn('[AuthService] backend logout failed:', error);
- // 即使后端调用失败,也继续清理本地状态
- }
- }
-
- this.clearStorage();
- }
- /**
- * 本地登出后跳转CAS统一认证注销
- */
- async logoutAndRedirectToCas(): Promise<void> {
- await this.logout(true);
- if (typeof window !== 'undefined') {
- const cfg = await this.getSsoConfig();
- if (cfg?.sso_enabled) {
- const casLogoutUrl = cfg.cas_login_url.replace(/\/login$/, '/logout');
- window.location.href = casLogoutUrl;
- } else {
- window.location.href = '/login';
- }
- }
- }
- /**
- * 验证Token是否有效
- * @returns Promise<{valid: boolean, user?: UserInfo}>
- */
- async verifyToken(): Promise<{ valid: boolean; user?: UserInfo }> {
- if (!this.token) {
- return { valid: false };
- }
- try {
- const response = await userApi.verifyToken();
- if (response.valid && response.user) {
- const userInfo = convertUserToUserInfo(response.user);
- // 更新本地存储的用户信息
- this.userInfo = userInfo;
- if (typeof window !== 'undefined') {
- localStorage.setItem(USER_INFO_KEY, JSON.stringify(userInfo));
- }
- return { valid: true, user: userInfo };
- }
- return { valid: false };
- } catch (error) {
- console.error('[AuthService] Token verification failed:', error);
- return { valid: false };
- }
- }
- /**
- * 刷新Token并延长有效期
- * @returns Promise<boolean> 是否刷新成功
- */
- async refreshTokenAndExtend(): Promise<boolean> {
- if (!this.token) {
- return false;
- }
- try {
- const response = await userApi.refreshToken();
- const userInfo = convertUserToUserInfo(response.user);
-
- // 保存新的Token和用户信息
- this.saveToStorage(response.access_token, userInfo);
- console.log('[AuthService] Token refreshed successfully');
- return true;
- } catch (error) {
- console.error('[AuthService] Token refresh failed:', error);
- return false;
- }
- }
- /**
- * 检查并刷新Token
- * 如果Token有效则刷新延长有效期,如果无效则清除登录状态
- * @returns Promise<boolean> Token是否有效
- */
- async checkAndRefreshToken(): Promise<boolean> {
- const verifyResult = await this.verifyToken();
-
- if (!verifyResult.valid) {
- // Token无效,清除登录状态(不调用后端API,因为Token已经无效)
- await this.logout(false);
- return false;
- }
- // Token有效,刷新延长有效期
- const refreshed = await this.refreshTokenAndExtend();
- if (!refreshed) {
- // 刷新失败但验证成功,保持当前状态
- console.warn('[AuthService] Token valid but refresh failed');
- }
-
- return true;
- }
- /**
- * 获取当前 token
- * @returns token 字符串或 null
- */
- getToken(): string | null {
- return this.token;
- }
- /**
- * 获取刷新 token
- * @returns refreshToken 字符串或 null
- */
- getRefreshToken(): string | null {
- return this.refreshToken;
- }
- /**
- * 获取当前用户信息
- * @returns 用户信息对象或 null
- */
- getUserInfo(): UserInfo | null {
- return this.userInfo;
- }
- /**
- * 检查是否已登录
- * @returns 是否已登录
- */
- isAuthenticated(): boolean {
- return this.token !== null && this.userInfo !== null;
- }
- /**
- * 刷新 token
- * @returns Promise<string> 新的 token
- */
- async refreshAccessToken(): Promise<string> {
- if (!this.refreshToken) {
- throw new Error('No refresh token available');
- }
- // 模拟网络延迟
- await new Promise(resolve => setTimeout(resolve, 300));
- // 模拟刷新 token API 调用
- const newToken = `mock_token_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
-
- if (typeof window !== 'undefined') {
- localStorage.setItem(TOKEN_KEY, newToken);
- }
- this.token = newToken;
- return newToken;
- }
- /**
- * 设置 token(用于外部设置,如从其他来源获取 token)
- * @param token token 字符串
- * @param userInfo 用户信息
- */
- setToken(token: string, userInfo: UserInfo): void {
- this.saveToStorage(token, userInfo);
- }
- /**
- * 获取 Authorization header 值
- * @returns Authorization header 字符串,格式:'Bearer {token}'
- */
- getAuthHeader(): string | null {
- if (!this.token || this.token.trim() === '') {
- return null;
- }
- return `Bearer ${this.token}`;
- }
- }
- // 导出单例
- export const authService = new AuthService();
- // 导出类型
- export type { UserInfo, LoginResponse, RegisterRequest, RegisterResponse };
|