2025-05-30 21:36:12 +08:00

435 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<T = any>(data: any, opts?: DataOpts) {
try {
return this.query.post<T>({ 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<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 data
* @returns
*/
async loginByCode(data: { phone: string; code: string }) {
const res = await this.post<QueryLoginResult>({ 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<QueryLoginResult>({ 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<Result>({ key: 'logout', data: { tokens } });
}
/**
* 检查用户名的组,这个用户是否存在
* @param username
* @returns
*/
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;
},
},
);
}
/**
* 检查登录状态
* @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 };
}
}