init query login
This commit is contained in:
		
							
								
								
									
										159
									
								
								src/login-cache.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										159
									
								
								src/login-cache.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,159 @@ | ||||
| import { MyCache } from '@kevisual/cache'; | ||||
|  | ||||
| export type CacheLoginUser = { | ||||
|   user?: any; | ||||
|   id?: string; | ||||
|   accessToken?: string; | ||||
|   refreshToken?: string; | ||||
| }; | ||||
| type CacheLogin = { | ||||
|   loginUsers: CacheLoginUser[]; | ||||
| } & CacheLoginUser; | ||||
|  | ||||
| export interface CacheStore<T = any> { | ||||
|   name: string; | ||||
|   cacheData: CacheLogin; | ||||
|   cache: T; | ||||
|   /** | ||||
|    * @update 获取缓存 | ||||
|    */ | ||||
|   get(key: string): Promise<any>; | ||||
|   /** | ||||
|    * @update 设置缓存 | ||||
|    */ | ||||
|   set(key: string, value: CacheLogin): Promise<CacheLogin>; | ||||
|   /** | ||||
|    * @update 删除缓存 | ||||
|    */ | ||||
|   del(): Promise<void>; | ||||
|   /** | ||||
|    * 设置当前用户 | ||||
|    */ | ||||
|   setLoginUser(user: CacheLoginUser): Promise<void>; | ||||
|   /** | ||||
|    * 获取当前用户 | ||||
|    */ | ||||
|   getCurrentUser(): Promise<CacheLoginUser>; | ||||
|   /** | ||||
|    * 获取当前用户列表 | ||||
|    */ | ||||
|   getCurrentUserList(): Promise<CacheLoginUser[]>; | ||||
|   /** | ||||
|    * 获取缓存的refreshToken | ||||
|    */ | ||||
|   getRefreshToken(): Promise<string>; | ||||
|   /** | ||||
|    * 获取缓存的accessToken | ||||
|    */ | ||||
|   getAccessToken(): Promise<string>; | ||||
|   /** | ||||
|    * 初始化 | ||||
|    */ | ||||
|   init(): Promise<void>; | ||||
|   /** | ||||
|    * 清除当前用户 | ||||
|    */ | ||||
|   clearCurrentUser(): Promise<void>; | ||||
|   /** | ||||
|    * 清除所有用户 | ||||
|    */ | ||||
|   clearAll(): Promise<void>; | ||||
| } | ||||
| export class LoginCacheStore implements CacheStore<MyCache<any>> { | ||||
|   cache: MyCache<any>; | ||||
|   name: string; | ||||
|   cacheData: CacheLogin; | ||||
|   constructor(name: string) { | ||||
|     this.cache = new MyCache(name); | ||||
|     this.cacheData = { | ||||
|       loginUsers: [], | ||||
|       user: undefined, | ||||
|       id: undefined, | ||||
|       accessToken: undefined, | ||||
|       refreshToken: undefined, | ||||
|     }; | ||||
|     this.name = name; | ||||
|   } | ||||
|   /** | ||||
|    * 设置缓存 | ||||
|    * @param key | ||||
|    * @param value | ||||
|    * @returns | ||||
|    */ | ||||
|   async set(key: string, value: CacheLogin) { | ||||
|     await this.cache.set(key, value); | ||||
|     return value; | ||||
|   } | ||||
|   /** | ||||
|    * 删除缓存 | ||||
|    */ | ||||
|   async del() { | ||||
|     await this.cache.del(); | ||||
|   } | ||||
|   get(key: string): Promise<CacheLogin> { | ||||
|     return this.cache.get(key); | ||||
|   } | ||||
|  | ||||
|   async init() { | ||||
|     this.cacheData = (await this.get(this.name)) || { | ||||
|       loginUsers: [], | ||||
|       user: null, | ||||
|       id: null, | ||||
|       accessToken: null, | ||||
|       refreshToken: null, | ||||
|     }; | ||||
|   } | ||||
|   /** | ||||
|    * 设置当前用户 | ||||
|    * @param user | ||||
|    */ | ||||
|   async setLoginUser(user: CacheLoginUser) { | ||||
|     const has = this.cacheData.loginUsers.find((u) => u.id === user.id); | ||||
|     if (has) { | ||||
|       this.cacheData.loginUsers = this.cacheData?.loginUsers?.filter((u) => u?.id && u.id !== user.id); | ||||
|     } | ||||
|     this.cacheData.loginUsers.push(user); | ||||
|     this.cacheData.user = user.user; | ||||
|     this.cacheData.id = user.id; | ||||
|     this.cacheData.accessToken = user.accessToken; | ||||
|     this.cacheData.refreshToken = user.refreshToken; | ||||
|     await this.set(this.name, this.cacheData); | ||||
|   } | ||||
|  | ||||
|   getCurrentUser(): Promise<CacheLoginUser> { | ||||
|     const cacheData = this.cacheData; | ||||
|     return Promise.resolve(cacheData.user); | ||||
|   } | ||||
|   getCurrentUserList(): Promise<CacheLoginUser[]> { | ||||
|     return Promise.resolve(this.cacheData.loginUsers.filter((u) => u?.id)); | ||||
|   } | ||||
|   getRefreshToken(): Promise<string> { | ||||
|     const cacheData = this.cacheData; | ||||
|     return Promise.resolve(cacheData.refreshToken || ''); | ||||
|   } | ||||
|   getAccessToken(): Promise<string> { | ||||
|     const cacheData = this.cacheData; | ||||
|     return Promise.resolve(cacheData.accessToken || ''); | ||||
|   } | ||||
|  | ||||
|   async clearCurrentUser() { | ||||
|     const user = await this.getCurrentUser(); | ||||
|     const has = this.cacheData.loginUsers.find((u) => u.id === user.id); | ||||
|     if (has) { | ||||
|       this.cacheData.loginUsers = this.cacheData?.loginUsers?.filter((u) => u?.id && u.id !== user.id); | ||||
|     } | ||||
|     this.cacheData.user = undefined; | ||||
|     this.cacheData.id = undefined; | ||||
|     this.cacheData.accessToken = undefined; | ||||
|     this.cacheData.refreshToken = undefined; | ||||
|     await this.set(this.name, this.cacheData); | ||||
|   } | ||||
|   async clearAll() { | ||||
|     this.cacheData.loginUsers = []; | ||||
|     this.cacheData.user = undefined; | ||||
|     this.cacheData.id = undefined; | ||||
|     this.cacheData.accessToken = undefined; | ||||
|     this.cacheData.refreshToken = undefined; | ||||
|     await this.set(this.name, this.cacheData); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										236
									
								
								src/query-login.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										236
									
								
								src/query-login.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,236 @@ | ||||
| import { Query } from '@kevisual/query'; | ||||
| import type { Result, DataOpts } from '@kevisual/query/query'; | ||||
| import { setBaseResponse } from '@kevisual/query/query'; | ||||
| import { LoginCacheStore, CacheStore } from './login-cache.ts'; | ||||
|  | ||||
| type QueryLoginOpts = { | ||||
|   query?: Query; | ||||
|   isBrowser?: boolean; | ||||
|   onLoad?: () => void; | ||||
|   storage?: Storage; | ||||
| }; | ||||
| export type QueryLoginData = { | ||||
|   username?: string; | ||||
|   password: string; | ||||
|   email?: string; | ||||
| }; | ||||
| export type QueryLoginResult = { | ||||
|   accessToken: string; | ||||
|   refreshToken: string; | ||||
| }; | ||||
|  | ||||
| export class QueryLogin { | ||||
|   query: Query; | ||||
|   cache: CacheStore; | ||||
|   isBrowser: boolean; | ||||
|   load?: boolean; | ||||
|   storage: Storage; | ||||
|   onLoad?: () => void; | ||||
|  | ||||
|   constructor(opts?: QueryLoginOpts) { | ||||
|     this.query = opts?.query || new Query(); | ||||
|     this.cache = new LoginCacheStore('login'); | ||||
|     this.isBrowser = opts?.isBrowser ?? true; | ||||
|     this.init(); | ||||
|     this.onLoad = opts?.onLoad; | ||||
|     this.storage = opts?.storage || localStorage; | ||||
|   } | ||||
|   setQuery(query: Query) { | ||||
|     this.query = query; | ||||
|   } | ||||
|   async init() { | ||||
|     await this.cache.init(); | ||||
|     this.load = true; | ||||
|     this.onLoad?.(); | ||||
|   } | ||||
|   async post<T = any>(data: any, opts?: DataOpts) { | ||||
|     return this.query.post<T>({ path: 'user', ...data }, opts); | ||||
|   } | ||||
|   /** | ||||
|    * 登录, | ||||
|    * @param data | ||||
|    * @returns | ||||
|    */ | ||||
|   async login(data: QueryLoginData) { | ||||
|     const res = await this.post<QueryLoginResult>({ key: 'login', ...data }); | ||||
|     if (res.code === 200) { | ||||
|       const { accessToken, refreshToken } = res?.data || {}; | ||||
|       this.storage.setItem('token', accessToken || ''); | ||||
|       await this.beforeSetLoginUser({ accessToken, refreshToken }); | ||||
|     } | ||||
|     return res; | ||||
|   } | ||||
|   /** | ||||
|    * 登陆成功,需要获取用户信息进行缓存 | ||||
|    * @param param0 | ||||
|    */ | ||||
|   async beforeSetLoginUser({ accessToken, refreshToken, check401 }: { accessToken?: string; refreshToken?: string; check401?: boolean }) { | ||||
|     if (accessToken && refreshToken) { | ||||
|       const resUser = await this.getMe(accessToken, check401); | ||||
|       if (resUser.code === 200) { | ||||
|         const user = resUser.data; | ||||
|         if (user) { | ||||
|           this.cache.setLoginUser({ | ||||
|             user, | ||||
|             id: user.id, | ||||
|             accessToken, | ||||
|             refreshToken, | ||||
|           }); | ||||
|         } else { | ||||
|           console.error('登录失败'); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   async queryRefreshToken(refreshToken?: string) { | ||||
|     const _refreshToken = refreshToken || this.cache.getRefreshToken(); | ||||
|     let data = { refreshToken: _refreshToken }; | ||||
|     if (!_refreshToken) { | ||||
|       await this.cache.clearCurrentUser(); | ||||
|       return { | ||||
|         code: 401, | ||||
|         message: '请先登录', | ||||
|         data: {} as any, | ||||
|       }; | ||||
|     } | ||||
|     return this.post( | ||||
|       { key: 'refreshToken', data }, | ||||
|       { | ||||
|         afterResponse: async (response, ctx) => { | ||||
|           setBaseResponse(response); | ||||
|           return response as any; | ||||
|         }, | ||||
|       }, | ||||
|     ); | ||||
|   } | ||||
|   /** | ||||
|    * 检查401错误,并刷新token, 如果refreshToken存在,则刷新token, 否则返回401 | ||||
|    * @param response | ||||
|    * @param ctx | ||||
|    * @param refetch | ||||
|    * @returns | ||||
|    */ | ||||
|   async afterCheck401ToRefreshToken(response: Result, ctx?: { req?: any; res?: any; fetch?: any }, refetch?: boolean) { | ||||
|     const that = this; | ||||
|     if (response?.code === 401) { | ||||
|       const hasRefreshToken = await that.cache.getRefreshToken(); | ||||
|       if (hasRefreshToken) { | ||||
|         const res = await that.queryRefreshToken(hasRefreshToken); | ||||
|         if (res.code === 200) { | ||||
|           const { accessToken, refreshToken } = res?.data || {}; | ||||
|           that.storage.setItem('token', accessToken || ''); | ||||
|           await that.beforeSetLoginUser({ accessToken, refreshToken, check401: false }); | ||||
|           if (refetch && ctx && ctx.req && ctx.req.url && ctx.fetch) { | ||||
|             await new Promise((resolve) => setTimeout(resolve, 1500)); | ||||
|             const url = ctx.req?.url; | ||||
|             const body = ctx.req?.body; | ||||
|             const headers = ctx.req?.headers; | ||||
|             const res = await ctx.fetch(url, { | ||||
|               method: 'POST', | ||||
|               body: body, | ||||
|               headers: { ...headers, Authorization: `Bearer ${accessToken}` }, | ||||
|             }); | ||||
|             setBaseResponse(res); | ||||
|             return res; | ||||
|           } | ||||
|         } else { | ||||
|           that.storage.removeItem('token'); | ||||
|           await that.cache.clearCurrentUser(); | ||||
|         } | ||||
|         return res; | ||||
|       } | ||||
|     } | ||||
|     return response as any; | ||||
|   } | ||||
|   /** | ||||
|    * 获取用户信息 | ||||
|    * @param token | ||||
|    * @returns | ||||
|    */ | ||||
|   async getMe(token?: string, check401: boolean = true) { | ||||
|     const _token = token || this.storage.getItem('token'); | ||||
|     const that = this; | ||||
|     return that.post( | ||||
|       { key: 'me' }, | ||||
|       { | ||||
|         beforeRequest: async (config) => { | ||||
|           if (config.headers) { | ||||
|             config.headers['Authorization'] = `Bearer ${_token}`; | ||||
|           } | ||||
|           return config; | ||||
|         }, | ||||
|         afterResponse: async (response, ctx) => { | ||||
|           if (response?.code === 401 && check401) { | ||||
|             return await that.afterCheck401ToRefreshToken(response, ctx); | ||||
|           } | ||||
|           return response as any; | ||||
|         }, | ||||
|       }, | ||||
|     ); | ||||
|   } | ||||
|   /** | ||||
|    * 请求更新,切换用户, 使用switchUser | ||||
|    * @param username | ||||
|    * @returns | ||||
|    */ | ||||
|   private async postSwitchUser(username: string) { | ||||
|     return this.post({ key: 'switchCheck', data: { username } }); | ||||
|   } | ||||
|   /** | ||||
|    * 切换用户 | ||||
|    * @param username | ||||
|    * @returns | ||||
|    */ | ||||
|   async switchUser(username: string) { | ||||
|     const localUserList = await this.cache.getCurrentUserList(); | ||||
|     const user = localUserList.find((userItem) => userItem.user.username === username); | ||||
|     if (user) { | ||||
|       this.storage.setItem('token', user.accessToken || ''); | ||||
|       await this.beforeSetLoginUser({ accessToken: user.accessToken, refreshToken: user.refreshToken }); | ||||
|       return { | ||||
|         code: 200, | ||||
|         data: { | ||||
|           accessToken: user.accessToken, | ||||
|           refreshToken: user.refreshToken, | ||||
|         }, | ||||
|         success: true, | ||||
|         message: '切换用户成功', | ||||
|       }; | ||||
|     } | ||||
|     const res = await this.postSwitchUser(username); | ||||
|  | ||||
|     if (res.code === 200) { | ||||
|       const { accessToken, refreshToken } = res?.data || {}; | ||||
|       this.storage.setItem('token', accessToken || ''); | ||||
|       await this.beforeSetLoginUser({ accessToken, refreshToken }); | ||||
|     } | ||||
|     return res; | ||||
|   } | ||||
|  | ||||
|   async logout() { | ||||
|     this.storage.removeItem('token'); | ||||
|     this.cache.del(); | ||||
|     return this.post<Result>({ key: 'logout' }); | ||||
|   } | ||||
|   async hasUser(username: string) { | ||||
|     const that = this; | ||||
|     return this.post<Result>( | ||||
|       { | ||||
|         path: 'org', | ||||
|         key: 'hasUser', | ||||
|         data: { | ||||
|           username, | ||||
|         }, | ||||
|       }, | ||||
|       { | ||||
|         afterResponse: async (response, ctx) => { | ||||
|           if (response?.code === 401) { | ||||
|             const res = await that.afterCheck401ToRefreshToken(response, ctx, true); | ||||
|             return res; | ||||
|           } | ||||
|           return response as any; | ||||
|         }, | ||||
|       }, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										22
									
								
								src/test/login.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/test/login.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| import { QueryLogin } from '../query-login.ts'; | ||||
| import { Query } from '@kevisual/query'; | ||||
| const query = new Query({ | ||||
|   url: 'https://kevisual.silkyai.cn/api/router', | ||||
| }); | ||||
| query.before(async (options) => { | ||||
|   console.log('before', options); | ||||
|   const token = localStorage.getItem('token'); | ||||
|   if (token) { | ||||
|     options.headers = { | ||||
|       ...options.headers, | ||||
|       Authorization: `Bearer ${token}`, | ||||
|     }; | ||||
|   } | ||||
|   return options; | ||||
| }); | ||||
| const queryLogin = new QueryLogin({ | ||||
|   query, | ||||
|   isBrowser: true, | ||||
| }); | ||||
| // @ts-ignore | ||||
| window.queryLogin = queryLogin; | ||||
		Reference in New Issue
	
	Block a user