This commit is contained in:
2025-12-04 14:22:04 +08:00
parent 2a55f2d3ef
commit 9e458f4a77
17 changed files with 449 additions and 143 deletions

View File

@@ -8,7 +8,14 @@ import { OauthUser } from '../oauth/oauth.ts';
export const redis = useContextKey<Redis>('redis');
const UserSecretStatus = ['active', 'inactive', 'expired'] as const;
const randomString = (length: number) => {
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
let result = '';
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
};
type Data = {
[key: string]: any;
/**
@@ -45,10 +52,12 @@ export class UserSecret extends Model {
if (!oauth.isSecretKey(token)) {
return await oauth.verifyToken(token);
}
// const secretToken = await oauth.verifyToken(token);
// if (secretToken) {
// return secretToken;
// }
const secretToken = await oauth.verifyToken(token);
if (secretToken) {
console.log('verifyToken: verified as normal token');
return secretToken;
}
console.log('verifyToken: try to verify as secret key');
const userSecret = await UserSecret.findOne({
where: { token },
});
@@ -66,7 +75,7 @@ export class UserSecret extends Model {
if (!oauthUser) {
return null; // 如果没有找到对应的oauth用户则返回null
}
// await oauth.saveSecretKey(oauthUser, userSecret.token);
await oauth.saveSecretKey(oauthUser, userSecret.token);
// 存储到oauth中的token store中
return oauthUser;
}
@@ -74,10 +83,10 @@ export class UserSecret extends Model {
* owner 组织用户的 oauthUser
* @returns
*/
async getOauthUser() {
async getOauthUser(opts?: { wx?: boolean }) {
const user = await User.findOne({
where: { id: this.userId },
attributes: ['id', 'username', 'type', 'owner'],
attributes: ['id', 'username', 'type', 'owner', 'data'],
});
let org: User = null;
if (!user) {
@@ -117,6 +126,44 @@ export class UserSecret extends Model {
const expiredTime = new Date(this.expiredTime);
return now > expiredTime.getTime(); // 如果当前时间大于过期时间,则认为已过期
}
/**
* 检查是否过期如果过期则更新状态为expired
*
* @returns
*/
async checkOnUse() {
if (!this.expiredTime) {
return {
code: 200
}
}
try {
const now = Date.now();
const expiredTime = new Date(this.expiredTime);
const isExpired = now > expiredTime.getTime(); // 如果当前时间大于过期时间,则认为已过期
if (isExpired) {
this.status = 'active';
const expireTime = UserSecret.getExpiredTime();
this.expiredTime = expireTime;
await this.save()
}
if (this.status !== 'active') {
this.status = 'active';
await this.save()
}
return {
code: 200
};
}
catch (e) {
console.error('checkExpiredAndUpdate error', this.id, this.title);
return {
code: 500,
message: 'checkExpiredAndUpdate error'
}
}
}
async createNewToken() {
if (this.token) {
await oauth.delToken(this.token);
@@ -134,8 +181,21 @@ export class UserSecret extends Model {
}
return token;
}
static async createSecret(tokenUser: { id: string; uid?: string }, expireDay = 365) {
const expireTime = expireDay * 24 * 60 * 60 * 1000; // 转换为毫秒
/**
* 根据 unionid 生成redis的key
* `wxmp:unionid:token:${unionid}`
* @param unionid
* @returns
*/
static wxRedisKey(unionid: string) {
return `wxmp:unionid:token:${unionid}`;
}
static getExpiredTime(expireDays?: number) {
const defaultExpireDays = expireDays || 365;
const expireTime = defaultExpireDays * 24 * 60 * 60 * 1000;
return new Date(Date.now() + expireTime)
}
static async createSecret(tokenUser: { id: string; uid?: string, title?: string }, expireDays = 365) {
const token = await UserSecret.createToken();
let userId = tokenUser.id;
let orgId: string = null;
@@ -147,11 +207,13 @@ export class UserSecret extends Model {
userId,
orgId,
token,
expiredTime: new Date(Date.now() + expireTime),
title: tokenUser.title || randomString(6),
expiredTime: UserSecret.getExpiredTime(expireDays),
});
return userSecret;
}
async getPermission(opts: { id: string; uid?: string }) {
const { id, uid } = opts;
let userId: string = id;

View File

@@ -2,6 +2,7 @@ import { User, UserInit, UserServices } from '../auth/models/index.ts';
import { UserSecretInit, UserSecret } from '../auth/models/index.ts';
import { OrgInit } from '../auth/models/index.ts';
export { User, UserInit, UserServices, UserSecret };
import { useContextKey } from '@kevisual/context';
const init = async () => {
await OrgInit(null, null, {
alter: true,
@@ -21,5 +22,7 @@ const init = async () => {
}).catch((e) => {
console.error('UserSecret sync', e);
});
console.log('Models synced');
useContextKey('models-synced', true);
};
init();

View File

@@ -68,16 +68,7 @@ export class WxServices {
},
});
// @ts-ignore
if (type === 'open' && user && user.data.wxOpenid !== token.openid) {
user.data = {
...user.data,
// @ts-ignore
wxOpenid: token.openid,
};
user = await user.update({ data: user.data });
console.log('mp-user login openid update=============', token.openid, token.unionid);
// @ts-ignore
} else if (type === 'mp' && user && user.data.wxmpOpenid !== token.openid) {
if (type === 'mp' && user && user.data.wxmpOpenid !== token.openid) {
user.data = {
...user.data,
// @ts-ignore
@@ -94,7 +85,7 @@ export class WxServices {
canChangeUsername: true,
};
user.data = data;
if ((type = 'mp')) {
if (type === 'mp') {
// @ts-ignore
data.wxmpOpenid = token.openid;
} else {

View File

@@ -1,7 +1,7 @@
import { Op } from 'sequelize';
import { User, UserSecret } from '@/models/user.ts';
import { app } from '@/app.ts';
import { redis } from '@/app.ts';
app
.route({
path: 'secret',
@@ -10,7 +10,7 @@ app
})
.define(async (ctx) => {
const tokenUser = ctx.state.tokenUser;
const { page = 1, pageSize = 100, search, sort = 'DESC', orgId } = ctx.query;
const { page = 1, pageSize = 100, search, sort = 'DESC', orgId, showToken = false } = ctx.query;
const searchWhere: Record<string, any> = search
? {
[Op.or]: [{ title: { [Op.like]: `%${search}%` } }, { description: { [Op.like]: `%${search}%` } }],
@@ -18,7 +18,10 @@ app
: {};
if (orgId) {
searchWhere.orgId = orgId;
} else {
searchWhere.orgId = null;
}
const excludeFields = showToken ? [] : ['token'];
const { rows: secrets, count } = await UserSecret.findAndCountAll({
where: {
userId: tokenUser.userId,
@@ -27,7 +30,7 @@ app
offset: (page - 1) * pageSize,
limit: pageSize,
attributes: {
exclude: ['token'], // Exclude sensitive token field
exclude: excludeFields, // Exclude sensitive token field
},
order: [['updatedAt', sort]],
});
@@ -166,3 +169,52 @@ app
ctx.body = secret;
})
.addTo(app);
app.route({
path: 'secret',
key: 'wxnotify',
description: '为了微信去缓存需要的数据, unionid是公众号下的用户的unionid',
}).define(async (ctx) => {
const { openid, unionid } = ctx.query;
if (!openid && !unionid) {
// ctx.throw(400, '需要提供 openid 或者 unionid 参数');
ctx.throw(400, '需要提供 unionid 参数');
}
// 最少20为的openid
if (unionid.length < 20) {
ctx.throw(400, 'unionid 是必填的');
}
const redisKey = UserSecret.wxRedisKey(unionid);
const token = await redis.get(redisKey);
if (token) {
ctx.body = 'success'
return;
}
const user = await User.findOne({
where: {
data: {
wxUnionId: unionid
}
}
})
if (!user) {
ctx.throw(404, '请关注公众号《人生可视化助手》后再操作');
return
}
let secretKey = await UserSecret.findOne({
where: {
userId: user.id,
title: 'wxmp-notify-token'
}
});
if (!secretKey) {
secretKey = await UserSecret.createSecret({ id: user.id, title: 'wxmp-notify-token' });
}
const check = await secretKey.checkOnUse();
if (check.code !== 200) {
ctx.throw(check.code, check.message);
}
await redis.set(redisKey, secretKey.token, 'EX', 30 * 24 * 60 * 60); // 30天过期
ctx.body = 'success'
}).addTo(app);

33
src/test/common.ts Normal file
View File

@@ -0,0 +1,33 @@
import { app } from '@/app.ts';
import '@/route.ts';
import { useConfig, useContextKey } from '@kevisual/context';
import { Query } from '@kevisual/query';
import util from 'node:util';
export {
app,
useContextKey
}
export const config = useConfig();
export const token = config.TOKEN || '';
export const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
export const showRes = (res, ...args) => {
if (res.code === 200) {
if (args.length === 0) {
console.log(res.code, util.inspect(res.body, { depth: 6, colors: true }));
return;
}
console.log(res.code, ...args);
} else {
console.error(res.code, res.message, ...args);
}
}
export const exit = (code = 0) => {
process.exit(code);
}
export const query = new Query({
url: 'https://kevisual.cn/api/router'
})

45
src/test/secret-key.ts Normal file
View File

@@ -0,0 +1,45 @@
import { app, token, showRes, sleep, useContextKey, exit, query } from './common.ts'
// await sleep(4000)
// const token2 = 'sk_6m3gjpkpny2ma9r96ei3bzck3kpg7b7g4oajghw7gmqoqk0vlh3swgxy85e0wnpt'
// await useContextKey('models-synced');
// const res = await app.call({
// path: 'secret',
// key: 'list',
// payload: {
// token,
// showToken: true
// }
// })
// showRes(res)
// const userRes = await app.call({
// path: 'user',
// key: 'me',
// payload: {
// token: token2,
// }
// })
// showRes(userRes)
// const openid = 'omcvy7AHC6bAA0QM4x9_bE0fGD1g'
// const res = await app.call({
// path: 'secret',
// key: 'wxnotify',
// payload: {
// openid
// }
// });
// showRes(res)
const res = await query.post({
path: 'secret',
key: 'wxnotify',
payload: {
openid: 'omcvy7M5CBAIB8TWDw6gNDHeHGeE'
}
})
showRes(res)
exit(0);