435 lines
12 KiB
TypeScript
435 lines
12 KiB
TypeScript
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 };
|
||
}
|
||
}
|