authService.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532
  1. import { userApi, User, RegisterRequest as ApiRegisterRequest, LoginResponse as ApiLoginResponse, TokenVerifyResponse } from './userApi';
  2. // 用户信息接口(兼容现有代码)
  3. export interface UserInfo {
  4. id: string;
  5. nickname: string;
  6. phone?: string;
  7. email?: string;
  8. avatar?: string;
  9. registrationDate?: string;
  10. apikey?: string; // 添加API密钥字段
  11. realName?: string; // 真实姓名
  12. isVerified?: string; // 认证状态
  13. verifiedAt?: string; // 认证时间
  14. }
  15. // 登录响应接口(兼容现有代码)
  16. export interface LoginResponse {
  17. code: number;
  18. message: string;
  19. data: {
  20. token: string;
  21. refreshToken?: string;
  22. expiresIn?: number;
  23. user: UserInfo;
  24. };
  25. }
  26. // 注册请求接口
  27. export interface RegisterRequest {
  28. username: string;
  29. password: string;
  30. email?: string;
  31. phone?: string;
  32. nickname?: string;
  33. schoolEmail?: string; // 学校邮箱(可选)
  34. sms_code?: string;
  35. email_code?: string;
  36. }
  37. // 注册响应接口
  38. export interface RegisterResponse {
  39. code: number;
  40. message: string;
  41. data: {
  42. userId: string;
  43. user?: UserInfo;
  44. };
  45. }
  46. // 转换后端用户数据为前端格式
  47. function convertUserToUserInfo(user: User): UserInfo {
  48. return {
  49. id: user.id,
  50. nickname: user.nickname,
  51. phone: user.phone || undefined,
  52. email: user.email || undefined,
  53. avatar: user.avatar || undefined,
  54. registrationDate: user.registration_date,
  55. apikey: user.apikey || undefined,
  56. realName: user.real_name || undefined,
  57. isVerified: user.is_verified || 'unverified',
  58. verifiedAt: user.verified_at || undefined,
  59. };
  60. }
  61. // Token 存储键名
  62. const TOKEN_KEY = 'aigc_space_token';
  63. const REFRESH_TOKEN_KEY = 'aigc_space_refresh_token';
  64. const USER_INFO_KEY = 'aigc_space_user_info';
  65. const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8010';
  66. // 认证状态变化事件名
  67. const AUTH_CHANGE_EVENT = 'auth_state_change';
  68. /**
  69. * 鉴权服务类
  70. * 负责管理用户认证、token 存储和 API 请求鉴权
  71. */
  72. class AuthService {
  73. private token: string | null = null;
  74. private refreshToken: string | null = null;
  75. private userInfo: UserInfo | null = null;
  76. constructor() {
  77. // 从 localStorage 恢复 token 和用户信息
  78. this.loadFromStorage();
  79. }
  80. /**
  81. * 订阅认证状态变化
  82. * @param callback 状态变化时的回调函数
  83. * @returns 取消订阅的函数
  84. */
  85. subscribe(callback: () => void): () => void {
  86. const handler = (e: Event) => {
  87. // console.log('[AuthService] event handler triggered');
  88. callback();
  89. };
  90. window.addEventListener(AUTH_CHANGE_EVENT, handler);
  91. console.log('[AuthService] subscribed to', AUTH_CHANGE_EVENT);
  92. return () => {
  93. window.removeEventListener(AUTH_CHANGE_EVENT, handler);
  94. // console.log('[AuthService] unsubscribed from', AUTH_CHANGE_EVENT);
  95. };
  96. }
  97. /**
  98. * 通知所有订阅者状态已变化
  99. */
  100. private notifyListeners(): void {
  101. console.log('[AuthService] notifyListeners called');
  102. // 触发自定义事件,用于跨组件通信
  103. if (typeof window !== 'undefined') {
  104. const event = new CustomEvent(AUTH_CHANGE_EVENT);
  105. console.log('[AuthService] dispatching event:', AUTH_CHANGE_EVENT);
  106. window.dispatchEvent(event);
  107. }
  108. }
  109. /**
  110. * 从 localStorage 加载 token 和用户信息
  111. */
  112. private loadFromStorage(): void {
  113. if (typeof window !== 'undefined') {
  114. const token = localStorage.getItem(TOKEN_KEY);
  115. const refreshToken = localStorage.getItem(REFRESH_TOKEN_KEY);
  116. // 确保 token 不是空字符串
  117. this.token = token && token.trim() !== '' ? token : null;
  118. this.refreshToken = refreshToken && refreshToken.trim() !== '' ? refreshToken : null;
  119. const userInfoStr = localStorage.getItem(USER_INFO_KEY);
  120. if (userInfoStr) {
  121. try {
  122. this.userInfo = JSON.parse(userInfoStr);
  123. } catch (e) {
  124. console.error('Failed to parse user info from storage:', e);
  125. this.userInfo = null;
  126. }
  127. }
  128. }
  129. }
  130. /**
  131. * 保存 token 和用户信息到 localStorage
  132. */
  133. private saveToStorage(token: string, userInfo: UserInfo, refreshToken?: string): void {
  134. if (typeof window !== 'undefined') {
  135. localStorage.setItem(TOKEN_KEY, token);
  136. if (refreshToken) {
  137. localStorage.setItem(REFRESH_TOKEN_KEY, refreshToken);
  138. }
  139. localStorage.setItem(USER_INFO_KEY, JSON.stringify(userInfo));
  140. }
  141. this.token = token;
  142. this.refreshToken = refreshToken || null;
  143. this.userInfo = userInfo;
  144. // 通知订阅者状态已变化
  145. this.notifyListeners();
  146. }
  147. /**
  148. * 清除存储的 token 和用户信息
  149. */
  150. private clearStorage(): void {
  151. console.log('[AuthService] clearStorage called');
  152. if (typeof window !== 'undefined') {
  153. localStorage.removeItem(TOKEN_KEY);
  154. localStorage.removeItem(REFRESH_TOKEN_KEY);
  155. localStorage.removeItem(USER_INFO_KEY);
  156. console.log('[AuthService] localStorage cleared');
  157. }
  158. this.token = null;
  159. this.refreshToken = null;
  160. this.userInfo = null;
  161. console.log('[AuthService] instance state cleared, notifying listeners');
  162. // 通知订阅者状态已变化
  163. this.notifyListeners();
  164. }
  165. /**
  166. * SSO一键登录(通过学校网站token)
  167. * @param ssoToken 学校网站提供的SSO token
  168. * @returns Promise<LoginResponse>
  169. */
  170. async ssoLogin(ssoToken: string): Promise<LoginResponse> {
  171. try {
  172. const response = await fetch(`${API_BASE_URL}/api/auth/verify`, {
  173. method: 'GET',
  174. headers: {
  175. 'Content-Type': 'application/json',
  176. Authorization: `Bearer ${ssoToken}`,
  177. },
  178. });
  179. if (!response.ok) {
  180. return {
  181. code: response.status,
  182. message: 'SSO登录失败,票据已失效或无效',
  183. data: {
  184. token: '',
  185. user: {
  186. id: '',
  187. nickname: '',
  188. },
  189. },
  190. };
  191. }
  192. const verifyResult = (await response.json()) as TokenVerifyResponse;
  193. const userInfo = convertUserToUserInfo(verifyResult.user);
  194. this.saveToStorage(ssoToken, userInfo);
  195. return {
  196. code: 200,
  197. message: 'SSO登录成功',
  198. data: {
  199. token: ssoToken,
  200. user: userInfo,
  201. },
  202. };
  203. } catch (error) {
  204. return {
  205. code: 500,
  206. message: error instanceof Error ? error.message : 'SSO登录失败',
  207. data: {
  208. token: '',
  209. user: {
  210. id: '',
  211. nickname: '',
  212. },
  213. },
  214. };
  215. }
  216. }
  217. /**
  218. * 查询后端 SSO 配置(含是否启用、CAS 登录地址等)
  219. */
  220. async getSsoConfig(): Promise<{ sso_enabled: boolean; cas_login_url: string } | null> {
  221. try {
  222. const res = await fetch(`${API_BASE_URL}/api/sso/config`);
  223. if (!res.ok) return null;
  224. return await res.json();
  225. } catch {
  226. return null;
  227. }
  228. }
  229. /**
  230. * 查询后端 SSO 是否启用
  231. */
  232. async isSsoEnabled(): Promise<boolean> {
  233. const cfg = await this.getSsoConfig();
  234. if (cfg === null) return false; // 请求失败时默认不启用SSO,跳普通登录页
  235. return cfg.sso_enabled === true;
  236. }
  237. /**
  238. * 登录
  239. * @param username 用户名或手机号
  240. * @param password 密码
  241. * @param keyId 会话密钥ID
  242. * @returns Promise<LoginResponse>
  243. */
  244. async login(username: string, password: string, keyId: string | null = null): Promise<LoginResponse> {
  245. try {
  246. const apiResponse = await userApi.login(username, password, keyId);
  247. const userInfo = convertUserToUserInfo(apiResponse.user);
  248. // 保存 token 和用户信息
  249. this.saveToStorage(apiResponse.access_token, userInfo);
  250. return {
  251. code: 200,
  252. message: '登录成功',
  253. data: {
  254. token: apiResponse.access_token,
  255. user: userInfo,
  256. }
  257. };
  258. } catch (error) {
  259. return {
  260. code: 401,
  261. message: error instanceof Error ? error.message : '登录失败',
  262. data: {
  263. token: '',
  264. user: {
  265. id: '',
  266. nickname: '',
  267. }
  268. }
  269. };
  270. }
  271. }
  272. /**
  273. * 注册
  274. * @param registerData 注册信息
  275. * @param keyId 会话密钥ID
  276. * @returns Promise<RegisterResponse>
  277. */
  278. async register(registerData: RegisterRequest, keyId: string | null = null): Promise<RegisterResponse> {
  279. try {
  280. const apiRegisterData: ApiRegisterRequest = {
  281. username: registerData.username,
  282. password: registerData.password,
  283. nickname: registerData.nickname || registerData.username,
  284. email: registerData.email || registerData.schoolEmail,
  285. phone: registerData.phone,
  286. sms_code: registerData.sms_code,
  287. email_code: registerData.email_code,
  288. };
  289. const user = await userApi.register(apiRegisterData, keyId);
  290. const userInfo = convertUserToUserInfo(user);
  291. return {
  292. code: 200,
  293. message: '注册成功',
  294. data: {
  295. userId: user.id,
  296. user: userInfo,
  297. }
  298. };
  299. } catch (error) {
  300. return {
  301. code: 400,
  302. message: error instanceof Error ? error.message : '注册失败',
  303. data: {
  304. userId: '',
  305. }
  306. };
  307. }
  308. }
  309. /**
  310. * 登出
  311. * @param callApi 是否调用后端API(默认true)
  312. */
  313. async logout(callApi: boolean = true): Promise<void> {
  314. console.log('[AuthService] logout called');
  315. // 尝试调用后端登出API
  316. if (callApi && this.token) {
  317. try {
  318. await userApi.logout();
  319. console.log('[AuthService] backend logout successful');
  320. } catch (error) {
  321. console.warn('[AuthService] backend logout failed:', error);
  322. // 即使后端调用失败,也继续清理本地状态
  323. }
  324. }
  325. this.clearStorage();
  326. }
  327. /**
  328. * 本地登出后跳转CAS统一认证注销
  329. */
  330. async logoutAndRedirectToCas(): Promise<void> {
  331. await this.logout(true);
  332. if (typeof window !== 'undefined') {
  333. const cfg = await this.getSsoConfig();
  334. if (cfg?.sso_enabled) {
  335. const casLogoutUrl = cfg.cas_login_url.replace(/\/login$/, '/logout');
  336. window.location.href = casLogoutUrl;
  337. } else {
  338. window.location.href = '/login';
  339. }
  340. }
  341. }
  342. /**
  343. * 验证Token是否有效
  344. * @returns Promise<{valid: boolean, user?: UserInfo}>
  345. */
  346. async verifyToken(): Promise<{ valid: boolean; user?: UserInfo }> {
  347. if (!this.token) {
  348. return { valid: false };
  349. }
  350. try {
  351. const response = await userApi.verifyToken();
  352. if (response.valid && response.user) {
  353. const userInfo = convertUserToUserInfo(response.user);
  354. // 更新本地存储的用户信息
  355. this.userInfo = userInfo;
  356. if (typeof window !== 'undefined') {
  357. localStorage.setItem(USER_INFO_KEY, JSON.stringify(userInfo));
  358. }
  359. return { valid: true, user: userInfo };
  360. }
  361. return { valid: false };
  362. } catch (error) {
  363. console.error('[AuthService] Token verification failed:', error);
  364. return { valid: false };
  365. }
  366. }
  367. /**
  368. * 刷新Token并延长有效期
  369. * @returns Promise<boolean> 是否刷新成功
  370. */
  371. async refreshTokenAndExtend(): Promise<boolean> {
  372. if (!this.token) {
  373. return false;
  374. }
  375. try {
  376. const response = await userApi.refreshToken();
  377. const userInfo = convertUserToUserInfo(response.user);
  378. // 保存新的Token和用户信息
  379. this.saveToStorage(response.access_token, userInfo);
  380. console.log('[AuthService] Token refreshed successfully');
  381. return true;
  382. } catch (error) {
  383. console.error('[AuthService] Token refresh failed:', error);
  384. return false;
  385. }
  386. }
  387. /**
  388. * 检查并刷新Token
  389. * 如果Token有效则刷新延长有效期,如果无效则清除登录状态
  390. * @returns Promise<boolean> Token是否有效
  391. */
  392. async checkAndRefreshToken(): Promise<boolean> {
  393. const verifyResult = await this.verifyToken();
  394. if (!verifyResult.valid) {
  395. // Token无效,清除登录状态(不调用后端API,因为Token已经无效)
  396. await this.logout(false);
  397. return false;
  398. }
  399. // Token有效,刷新延长有效期
  400. const refreshed = await this.refreshTokenAndExtend();
  401. if (!refreshed) {
  402. // 刷新失败但验证成功,保持当前状态
  403. console.warn('[AuthService] Token valid but refresh failed');
  404. }
  405. return true;
  406. }
  407. /**
  408. * 获取当前 token
  409. * @returns token 字符串或 null
  410. */
  411. getToken(): string | null {
  412. return this.token;
  413. }
  414. /**
  415. * 获取刷新 token
  416. * @returns refreshToken 字符串或 null
  417. */
  418. getRefreshToken(): string | null {
  419. return this.refreshToken;
  420. }
  421. /**
  422. * 获取当前用户信息
  423. * @returns 用户信息对象或 null
  424. */
  425. getUserInfo(): UserInfo | null {
  426. return this.userInfo;
  427. }
  428. /**
  429. * 检查是否已登录
  430. * @returns 是否已登录
  431. */
  432. isAuthenticated(): boolean {
  433. return this.token !== null && this.userInfo !== null;
  434. }
  435. /**
  436. * 刷新 token
  437. * @returns Promise<string> 新的 token
  438. */
  439. async refreshAccessToken(): Promise<string> {
  440. if (!this.refreshToken) {
  441. throw new Error('No refresh token available');
  442. }
  443. // 模拟网络延迟
  444. await new Promise(resolve => setTimeout(resolve, 300));
  445. // 模拟刷新 token API 调用
  446. const newToken = `mock_token_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
  447. if (typeof window !== 'undefined') {
  448. localStorage.setItem(TOKEN_KEY, newToken);
  449. }
  450. this.token = newToken;
  451. return newToken;
  452. }
  453. /**
  454. * 设置 token(用于外部设置,如从其他来源获取 token)
  455. * @param token token 字符串
  456. * @param userInfo 用户信息
  457. */
  458. setToken(token: string, userInfo: UserInfo): void {
  459. this.saveToStorage(token, userInfo);
  460. }
  461. /**
  462. * 获取 Authorization header 值
  463. * @returns Authorization header 字符串,格式:'Bearer {token}'
  464. */
  465. getAuthHeader(): string | null {
  466. if (!this.token || this.token.trim() === '') {
  467. return null;
  468. }
  469. return `Bearer ${this.token}`;
  470. }
  471. }
  472. // 导出单例
  473. export const authService = new AuthService();
  474. // 导出类型
  475. export type { UserInfo, LoginResponse, RegisterRequest, RegisterResponse };