From 922b0c421f02bf6b63a0d9a52ca5b8af38c591cf Mon Sep 17 00:00:00 2001 From: xion Date: Thu, 19 Jun 2025 19:20:27 +0800 Subject: [PATCH] update for token --- package.json | 28 ++-- src/core-models.ts | 3 +- src/lib.ts | 3 + src/middleware/auth-manual.ts | 19 ++- src/models/user-secret.ts | 247 ++++++++++++++++++++++++++++++++++ src/models/user.ts | 11 +- src/oauth/auth.ts | 16 ++- src/oauth/oauth.ts | 63 ++++++++- 8 files changed, 364 insertions(+), 26 deletions(-) create mode 100644 src/models/user-secret.ts diff --git a/package.json b/package.json index e47ec29..7f062b8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kevisual/code-center-module", - "version": "0.0.20", + "version": "0.0.23", "description": "", "main": "dist/system.mjs", "module": "dist/system.mjs", @@ -27,41 +27,42 @@ }, "dependencies": { "@kevisual/auth": "1.0.5", - "@kevisual/router": "^0.0.21", - "@kevisual/use-config": "^1.0.17", + "@kevisual/context": "^0.0.3", + "@kevisual/router": "^0.0.22", + "@kevisual/use-config": "^1.0.19", "ioredis": "^5.6.1", "nanoid": "^5.1.5", - "pg": "^8.16.0", + "pg": "^8.16.1", "sequelize": "^6.37.7", "socket.io": "^4.8.1", - "zod": "^3.25.28" + "zod": "^3.25.67" }, "devDependencies": { "@kevisual/types": "^0.0.10", "@rollup/plugin-alias": "^5.1.1", - "@rollup/plugin-commonjs": "^28.0.3", + "@rollup/plugin-commonjs": "^28.0.6", "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^16.0.1", "@rollup/plugin-replace": "^6.0.2", - "@rollup/plugin-typescript": "^12.1.2", + "@rollup/plugin-typescript": "^12.1.3", "@types/archiver": "^6.0.3", "@types/crypto-js": "^4.2.2", "@types/formidable": "^3.4.5", - "@types/jsonwebtoken": "^9.0.9", + "@types/jsonwebtoken": "^9.0.10", "@types/lodash-es": "^4.17.12", - "@types/node": "^22.15.21", - "@types/react": "^19.1.5", + "@types/node": "^24.0.3", + "@types/react": "^19.1.8", "@types/uuid": "^10.0.0", "concurrently": "^9.1.2", "cross-env": "^7.0.3", "nodemon": "^3.1.10", "rimraf": "^6.0.1", - "rollup": "^4.41.1", + "rollup": "^4.44.0", "rollup-plugin-copy": "^3.5.0", "rollup-plugin-dts": "^6.2.1", "rollup-plugin-esbuild": "^6.2.1", "tape": "^5.9.0", - "tsx": "^4.19.4", + "tsx": "^4.20.3", "typescript": "^5.8.3" }, "exports": { @@ -80,6 +81,9 @@ "./oauth": { "import": "./dist/oauth.mjs", "types": "./dist/oauth.d.ts" + }, + "./src/*": { + "import": "./src/*" } } } \ No newline at end of file diff --git a/src/core-models.ts b/src/core-models.ts index 58ae4ec..bbfe9e3 100644 --- a/src/core-models.ts +++ b/src/core-models.ts @@ -3,9 +3,10 @@ */ import { UserServices, User, UserInit, UserModel } from './models/user.ts'; import { Org, OrgInit, OrgModel } from './models/org.ts'; +import { UserSecret, UserSecretInit } from './models/user-secret.ts'; import { addAuth } from './middleware/auth.ts'; import { checkAuth, getLoginUser } from './middleware/auth-manual.ts'; -export { User, Org, UserServices, UserInit, OrgInit, UserModel, OrgModel }; +export { User, Org, UserServices, UserInit, OrgInit, UserModel, OrgModel, UserSecret, UserSecretInit }; /** * 可以不需要user成功, 有则赋值,交给下一个中间件 diff --git a/src/lib.ts b/src/lib.ts index 0641570..b0ed018 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -4,11 +4,13 @@ import { app } from './app.ts'; import { UserServices, UserInit, UserModel, User } from './models/user.ts'; import { Org, OrgInit, OrgModel } from './models/org.ts'; +import { UserSecret, UserSecretInit } from './models/user-secret.ts'; import { useContextKey } from '@kevisual/use-config/context'; import { Sequelize } from 'sequelize'; import { Redis } from 'ioredis'; export { User, UserServices, UserInit, UserModel }; export { Org, OrgInit, OrgModel }; +export { UserSecret, UserSecretInit }; export const redis = useContextKey('redis'); export const sequelize = useContextKey('sequelize'); @@ -16,4 +18,5 @@ export { app }; export const init = () => { OrgInit(); UserInit(); + UserSecretInit(); }; diff --git a/src/middleware/auth-manual.ts b/src/middleware/auth-manual.ts index 9c3c54c..9681d0c 100644 --- a/src/middleware/auth-manual.ts +++ b/src/middleware/auth-manual.ts @@ -4,19 +4,23 @@ import cookie from 'cookie'; export const error = (msg: string, code = 500) => { return JSON.stringify({ code, message: msg }); }; +type CheckAuthOptions = { + check401?: boolean; // 是否返回权限信息 +}; /** * 手动验证token,如果token不存在,则返回401 * @param req * @param res * @returns */ -export const checkAuth = async (req: http.IncomingMessage, res: http.ServerResponse) => { +export const checkAuth = async (req: http.IncomingMessage, res: http.ServerResponse, opts?: CheckAuthOptions) => { let token = (req.headers?.['authorization'] as string) || (req.headers?.['Authorization'] as string) || ''; const url = new URL(req.url || '', 'http://localhost'); + const check401 = opts?.check401 ?? true; // 是否返回401错误 const resNoPermission = () => { res.statusCode = 401; res.end(error('Invalid authorization')); - return { tokenUser: null, token: null }; + return { tokenUser: null, token: null, hasToken: false }; }; if (!token) { token = url.searchParams.get('token') || ''; @@ -25,22 +29,24 @@ export const checkAuth = async (req: http.IncomingMessage, res: http.ServerRespo const parsedCookies = cookie.parse(req.headers.cookie || ''); token = parsedCookies.token || ''; } - if (!token) { + if (!token && check401) { return resNoPermission(); } if (token) { token = token.replace('Bearer ', ''); } let tokenUser; + const hasToken = !!token; // 是否有token存在 + try { tokenUser = await User.verifyToken(token); } catch (e) { console.log('checkAuth error', e); res.statusCode = 401; res.end(error('Invalid token')); - return { tokenUser: null, token: null }; + return { tokenUser: null, token: null, hasToken: false }; } - return { tokenUser, token }; + return { tokenUser, token, hasToken }; }; /** @@ -62,6 +68,9 @@ export const getLoginUser = async (req: http.IncomingMessage) => { if (token) { token = token.replace('Bearer ', ''); } + if (!token) { + return null; + } let tokenUser; try { tokenUser = await User.verifyToken(token); diff --git a/src/models/user-secret.ts b/src/models/user-secret.ts new file mode 100644 index 0000000..8a8903f --- /dev/null +++ b/src/models/user-secret.ts @@ -0,0 +1,247 @@ +import { DataTypes, Model, Sequelize } from 'sequelize'; + +import { useContextKey } from '@kevisual/use-config/context'; +import { Redis } from 'ioredis'; +import { SyncOpts, User } from './user.ts'; +import { oauth } from '../oauth/auth.ts'; +import { OauthUser } from '@/oauth/oauth.ts'; +export const redis = useContextKey('redis'); + +const UserSecretStatus = ['active', 'inactive', 'expired'] as const; +export class UserSecret extends Model { + static oauth = oauth; + declare id: string; + declare token: string; + declare userId: string; + declare orgId: string; + declare title: string; + declare description: string; + declare status: (typeof UserSecretStatus)[number]; + declare expiredTime: Date; + declare data: any; + /** + * 验证token + * @param token + * @returns + */ + static async verifyToken(token: string) { + if (!oauth.isSecretKey(token)) { + return await oauth.verifyToken(token); + } + // const secretToken = await oauth.verifyToken(token); + // if (secretToken) { + // return secretToken; + // } + const userSecret = await UserSecret.findOne({ + where: { token }, + }); + if (!userSecret) { + return null; // 如果没有找到对应的用户密钥,则返回null + } + if (userSecret.isExpired()) { + return null; // 如果用户密钥已过期,则返回null + } + if (userSecret.status !== 'active') { + return null; // 如果用户密钥状态不是active,则返回null + } + if (!userSecret.token) { + return null; // 如果用户密钥没有token,则返回null + } + // 如果用户密钥未过期,则返回用户信息 + const oauthUser = await userSecret.getOauthUser(); + if (!oauthUser) { + return null; // 如果没有找到对应的oauth用户,则返回null + } + // await oauth.saveSecretKey(oauthUser, userSecret.token); + // 存储到oauth中的token store中 + return oauthUser; + } + /** + * owner 组织用户的 oauthUser + * @returns + */ + async getOauthUser() { + const user = await User.findOne({ + where: { id: this.userId }, + attributes: ['id', 'username', 'type', 'owner'], + }); + let org: User = null; + if (!user) { + return null; // 如果没有找到对应的用户,则返回null + } + const expiredTime = this.expiredTime ? new Date(this.expiredTime).getTime() : null; + const oauthUser: Partial = { + id: user.id, + username: user.username, + type: 'user', + oauthExpand: { + expiredTime: expiredTime, + }, + }; + if (this.orgId) { + org = await User.findOne({ + where: { id: this.orgId }, + attributes: ['id', 'username', 'type', 'owner'], + }); + if (org) { + oauthUser.id = org.id; + oauthUser.username = org.username; + oauthUser.type = 'org'; + oauthUser.uid = user.id; + } else { + console.warn(`getOauthUser: org not found for orgId ${this.orgId}`); + } + } + + return oauth.getOauthUser(oauthUser); + } + isExpired() { + if (!this.expiredTime) { + return false; // 没有设置过期时间 + } + const now = Date.now(); + const expiredTime = new Date(this.expiredTime); + return now > expiredTime.getTime(); // 如果当前时间大于过期时间,则认为已过期 + } + async createNewToken() { + if (this.token) { + await oauth.delToken(this.token); + } + const token = await UserSecret.createToken(); + this.token = token; + await this.save(); + return token; + } + static async createToken() { + let token = oauth.generateSecretKey(); + // 确保生成的token是唯一的 + while (await UserSecret.findOne({ where: { token } })) { + token = oauth.generateSecretKey(); + } + return token; + } + static async createSecret(tokenUser: { id: string; uid?: string }, expireDay = 365) { + const expireTime = expireDay * 24 * 60 * 60 * 1000; // 转换为毫秒 + const token = await UserSecret.createToken(); + let userId = tokenUser.id; + let orgId: string = null; + if (tokenUser.uid) { + userId = tokenUser.uid; + orgId = tokenUser.id; // 如果是组织用户,则uid是组织ID + } + const userSecret = await UserSecret.create({ + userId, + orgId, + token, + expiredTime: new Date(Date.now() + expireTime), + }); + + return userSecret; + } + async getPermission(opts: { id: string; uid?: string }) { + const { id, uid } = opts; + let userId: string = id; + let hasPermission = false; + let isUser = false; + let isAdmin: boolean = null; + if (uid) { + userId = uid; + } + if (!id) { + return { + hasPermission, + isUser, + isAdmin, + }; + } + if (this.userId === userId) { + hasPermission = true; + isUser = true; + } + + if (hasPermission) { + return { + hasPermission, + isUser, + isAdmin, + }; + } + if (this.orgId) { + const orgUser = await User.findByPk(this.orgId); + if (orgUser && orgUser.owner === userId) { + isAdmin = true; + hasPermission = true; + } + } + return { + hasPermission, + isUser, + isAdmin, + }; + } +} +/** + * 组织模型,在sequelize之后初始化 + */ +export const UserSecretInit = async (newSequelize?: any, tableName?: string, sync?: SyncOpts) => { + const sequelize = useContextKey('sequelize'); + UserSecret.init( + { + id: { + type: DataTypes.UUID, + primaryKey: true, + defaultValue: DataTypes.UUIDV4, + }, + description: { + type: DataTypes.TEXT, + allowNull: true, + }, + status: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: 'active', + comment: '状态', + }, + title: { + type: DataTypes.TEXT, + allowNull: true, + }, + expiredTime: { + type: DataTypes.DATE, + allowNull: true, + }, + token: { + type: DataTypes.STRING, + allowNull: false, + comment: '用户密钥', + defaultValue: '', + }, + userId: { + type: DataTypes.UUID, + allowNull: true, + }, + data: { + type: DataTypes.JSONB, + allowNull: true, + defaultValue: {}, + }, + orgId: { + type: DataTypes.UUID, + allowNull: true, + comment: '组织ID', + }, + }, + { + sequelize: newSequelize || sequelize, + modelName: tableName || 'cf_user_secret', + }, + ); + if (sync) { + await UserSecret.sync({ alter: true, logging: false, ...sync }).catch((e) => { + console.error('UserSecret sync', e); + }); + return UserSecret; + } + return UserSecret; +}; +export const UserSecretModel = useContextKey('UserSecretModel', () => UserSecret); diff --git a/src/models/user.ts b/src/models/user.ts index 8cdc765..4a091cb 100644 --- a/src/models/user.ts +++ b/src/models/user.ts @@ -9,7 +9,7 @@ import { oauth } from '../oauth/auth.ts'; import { cryptPwd } from '../oauth/salt.ts'; import { OauthUser } from '../oauth/oauth.ts'; export const redis = useContextKey('redis'); - +import { UserSecret } from './user-secret.ts'; type UserData = { orgs?: string[]; wxUnionId?: string; @@ -42,6 +42,7 @@ export class User extends Model { setTokenUser(tokenUser: any) { this.tokenUser = tokenUser; } + /** * uid 是用于 orgId 的用户id, 如果uid存在,则表示是用户是组织,其中uid为真实用户 * @param uid @@ -68,7 +69,7 @@ export class User extends Model { * @returns */ static async verifyToken(token: string) { - return await oauth.verifyToken(token); + return await UserSecret.verifyToken(token); } /** * 刷新token @@ -80,7 +81,7 @@ export class User extends Model { return { accessToken: token.accessToken, refreshToken: token.refreshToken, token: token.accessToken }; } static async getOauthUser(token: string) { - return await oauth.verifyToken(token); + return await UserSecret.verifyToken(token); } /** * 清理用户的token,需要重新登陆 @@ -97,7 +98,7 @@ export class User extends Model { * @returns */ static async getUserByToken(token: string) { - const oauthUser = await oauth.verifyToken(token); + const oauthUser = await UserSecret.verifyToken(token); if (!oauthUser) { throw new CustomError('Token is invalid. get UserByToken'); } @@ -230,7 +231,7 @@ export class User extends Model { } export type SyncOpts = { alter?: boolean; - logging?: boolean; + logging?: any; force?: boolean; }; export const UserInit = async (newSequelize?: any, tableName?: string, sync?: SyncOpts) => { diff --git a/src/oauth/auth.ts b/src/oauth/auth.ts index 2351910..c99e699 100644 --- a/src/oauth/auth.ts +++ b/src/oauth/auth.ts @@ -2,5 +2,17 @@ import { OAuth, RedisTokenStore } from './oauth.ts'; import { useContextKey } from '@kevisual/use-config/context'; import { Redis } from 'ioredis'; -export const redis = useContextKey('redis'); -export const oauth = useContextKey('oauth', () => new OAuth(new RedisTokenStore(redis))); +export const oauth = useContextKey('oauth', () => { + const redis = useContextKey('redis'); + const store = new RedisTokenStore(redis); + // redis是promise + if (redis instanceof Promise) { + redis.then((r) => { + store.setRedis(r); + }); + } else if (redis) { + store.setRedis(redis); + } + const oauth = new OAuth(store); + return oauth; +}); diff --git a/src/oauth/oauth.ts b/src/oauth/oauth.ts index bc3dea1..8ea9e27 100644 --- a/src/oauth/oauth.ts +++ b/src/oauth/oauth.ts @@ -76,10 +76,13 @@ interface Store { export class RedisTokenStore implements Store { redis: Redis; private prefix: string = 'oauth:'; - constructor(redis: Redis, prefix?: string) { + constructor(redis?: Redis, prefix?: string) { this.redis = redis; this.prefix = prefix || this.prefix; } + async setRedis(redis: Redis) { + this.redis = redis; + } async set(key: string, value: string, ttl?: number) { await this.redis.set(this.prefix + key, value, 'EX', ttl); } @@ -183,9 +186,21 @@ export class OAuth { constructor(store: Store) { this.store = store; } + generateSecretKey(sk = true) { + if (sk) { + return 'sk_' + randomId64(); + } + return 'st_' + randomId32(); + } /** * 生成token * @param user + * @param user.id 访问者id + * @param user.uid 如果是org,这个是真实用户id,id是orgId + * @param user.userId 真实用户id + * @param user.orgId 组织id,可选 + * @param user.username + * @param user.type * @returns */ async generateToken( @@ -216,6 +231,37 @@ export class OAuth { return { accessToken, refreshToken }; } + async saveSecretKey(oauthUser: T, secretKey: string, opts?: StoreSetOpts) { + // 生成一个secretKey + // 设置到store中 + oauthUser.oauthExpand = { + ...oauthUser.oauthExpand, + accessToken: secretKey, + description: 'secretKey', + createTime: new Date().getTime(), // 创建时间 + }; + await this.store.setToken( + { accessToken: secretKey, refreshToken: '', value: oauthUser }, + { + ...opts, + hasRefreshToken: false, + }, + ); + return secretKey; + } + getOauthUser({ id, uid, username, type }: Partial): OauthUser { + const oauthUser: OauthUser = { + id, + username, + uid, + userId: uid || id, // 必存在,真实用户id + type: type as 'user' | 'org', + }; + if (uid) { + oauthUser.orgId = id; + } + return oauthUser; + } /** * 验证token,如果token不存在,返回null * @param token @@ -225,6 +271,21 @@ export class OAuth { const res = await this.store.getObject(token); return res; } + /** + * 验证token是否是accessToken, sk 开头的为secretKey,没有refreshToken + * @param token + * @returns + */ + isSecretKey(token: string) { + if (!token) { + return false; + } + // 如果是sk_开头,则是secretKey + if (token.startsWith('sk_')) { + return true; + } + return false; + } /** * 刷新token * @param refreshToken