import { Query, BaseQuery } from '@kevisual/query'; import type { Result, DataOpts } from '@kevisual/query/query'; import { setBaseResponse } from '@kevisual/query/query'; import { LoginCacheStore, CacheStore } from './login-cache.ts'; import { Cache } from './login-cache.ts'; export type QueryLoginOpts = { query?: Query; isBrowser?: boolean; onLoad?: () => void; storage?: Storage; cache: Cache; }; export type QueryLoginData = { username?: string; password: string; email?: string; }; export type QueryLoginResult = { accessToken: string; refreshToken: string; }; export class QueryLogin extends BaseQuery { /** * query login cache, 非实际操作, 一个cache的包裹模块 */ cacheStore: CacheStore; isBrowser: boolean; load?: boolean; storage: Storage; onLoad?: () => void; constructor(opts?: QueryLoginOpts) { super({ query: opts?.query || new Query(), }); this.cacheStore = new LoginCacheStore({ name: 'login', cache: opts?.cache! }); this.isBrowser = opts?.isBrowser ?? true; this.init(); this.onLoad = opts?.onLoad; this.storage = opts?.storage || localStorage; } setQuery(query: Query) { this.query = query; } private async init() { await this.cacheStore.init(); this.load = true; this.onLoad?.(); } async post(data: any, opts?: DataOpts) { try { return this.query.post({ path: 'user', ...data }, opts); } catch (error) { console.log('error', error); return { code: 400, } as any; } } /** * 登录, * @param data * @returns */ async login(data: QueryLoginData) { const res = await this.post({ 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 data * @returns */ async loginByCode(data: { phone: string; code: string }) { const res = await this.post({ path: 'sms', key: 'login', data }); if (res.code === 200) { const { accessToken, refreshToken } = res?.data || {}; this.storage.setItem('token', accessToken || ''); await this.beforeSetLoginUser({ accessToken, refreshToken }); } return res; } /** * 设置token * @param token */ async setLoginToken(token: { accessToken: string; refreshToken: string }) { const { accessToken, refreshToken } = token; this.storage.setItem('token', accessToken || ''); await this.beforeSetLoginUser({ accessToken, refreshToken }); } async loginByWechat(data: { code: string }) { const res = await this.post({ path: 'wx', key: 'open-login', code: data.code }); if (res.code === 200) { const { accessToken, refreshToken } = res?.data || {}; this.storage.setItem('token', accessToken || ''); await this.beforeSetLoginUser({ accessToken, refreshToken }); } return res; } /** * 检测微信登录,登陆成功后,调用onSuccess,否则调用onError * @param param0 */ async checkWechat({ onSuccess, onError }: { onSuccess?: (res: QueryLoginResult) => void; onError?: (res: any) => void }) { const url = new URL(window.location.href); const code = url.searchParams.get('code'); const state = url.searchParams.get('state'); if (code && state) { const res = await this.loginByWechat({ code }); if (res.code === 200) { onSuccess?.(res.data); } else { onError?.(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.cacheStore.setLoginUser({ user, id: user.id, accessToken, refreshToken, }); } else { console.error('登录失败'); } } } } /** * 刷新token * @param refreshToken * @returns */ async queryRefreshToken(refreshToken?: string) { const _refreshToken = refreshToken || this.cacheStore.getRefreshToken(); let data = { refreshToken: _refreshToken }; if (!_refreshToken) { await this.cacheStore.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 * 拦截请求,请使用run401Action, 不要直接使用 afterCheck401ToRefreshToken * @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.cacheStore.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.cacheStore.clearCurrentUser(); } return res; } } return response as any; } /** * 一个简单的401处理, 如果401,则刷新token, 如果refreshToken不存在,则返回401 * refetch 是否重新请求, 会有bug,无限循环,按需要使用 * TODO: * @param response * @param ctx * @param opts * @returns */ async run401Action( response: Result, ctx?: { req?: any; res?: any; fetch?: any }, opts?: { /** * 是否重新请求, 会有bug,无限循环,按需要使用 */ refetch?: boolean; /** * check之后的回调 */ afterCheck?: (res: Result) => any; /** * 401处理后, 还是401, 则回调 */ afterAlso401?: (res: Result) => any; }, ) { const that = this; const refetch = opts?.refetch ?? false; if (response?.code === 401) { if (that.query.stop === true) { return { code: 500, success: false, message: 'refresh token loading...' }; } that.query.stop = true; const res = await that.afterCheck401ToRefreshToken(response, ctx, refetch); that.query.stop = false; opts?.afterCheck?.(res); if (res.code === 401) { opts?.afterAlso401?.(res); } return res; } else { 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}`; } if (!_token) { return false; } return config; }, afterResponse: async (response, ctx) => { if (response?.code === 401 && check401 && !token) { return await that.afterCheck401ToRefreshToken(response, ctx); } return response as any; }, }, ); } /** * 检查本地用户,如果本地用户存在,则返回本地用户,否则返回null * @returns */ async checkLocalUser() { const user = await this.cacheStore.getCurrentUser(); if (user) { return user; } return null; } /** * 检查本地token是否存在,简单的判断是否已经属于登陆状态 * @returns */ async checkLocalToken() { const token = this.storage.getItem('token'); return !!token; } /** * 检查本地用户列表 * @returns */ async getToken() { const token = this.storage.getItem('token'); return token || ''; } async beforeRequest(opts: any = {}) { const token = this.storage.getItem('token'); if (token) { opts.headers = { ...opts.headers, Authorization: `Bearer ${token}` }; } return opts; } /** * 请求更新,切换用户, 使用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.cacheStore.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; } /** * 退出登陆,去掉token, 并删除缓存 * @returns */ async logout() { this.storage.removeItem('token'); const users = await this.cacheStore.getCurrentUserList(); const tokens = users .map((user) => { return user?.accessToken; }) .filter(Boolean); this.cacheStore.delValue(); return this.post({ key: 'logout', data: { tokens } }); } /** * 检查用户名的组,这个用户是否存在 * @param username * @returns */ async hasUser(username: string) { const that = this; return this.post( { 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; }, }, ); } /** * 检查登录状态 * @param token * @returns */ async checkLoginStatus(token: string) { const res = await this.post({ path: 'user', key: 'checkLoginStatus', loginToken: token, }); if (res.code === 200) { const accessToken = res.data?.accessToken; this.storage.setItem('token', accessToken || ''); await this.beforeSetLoginUser({ accessToken, refreshToken: res.data?.refreshToken }); return res; } return false; } /** * 使用web登录,创建url地址, 需要MD5和jsonwebtoken */ loginWithWeb(baseURL: string, { MD5, jsonwebtoken }: { MD5: any; jsonwebtoken: any }) { const randomId = Math.random().toString(36).substring(2, 15); const timestamp = Date.now(); const tokenSecret = 'xiao' + randomId; const sign = MD5(`${tokenSecret}${timestamp}`).toString(); const token = jsonwebtoken.sign({ randomId, timestamp, sign }, tokenSecret, { // 10分钟过期 expiresIn: 60 * 10, // 10分钟 }); const url = `${baseURL}/api/router?path=user&key=webLogin&p&loginToken=${token}&sign=${sign}&randomId=${randomId}`; return { url, token, tokenSecret }; } }