update for token
This commit is contained in:
parent
83f65e1554
commit
922b0c421f
28
package.json
28
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/*"
|
||||
}
|
||||
}
|
||||
}
|
@ -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成功, 有则赋值,交给下一个中间件
|
||||
|
@ -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>('redis');
|
||||
export const sequelize = useContextKey<Sequelize>('sequelize');
|
||||
@ -16,4 +18,5 @@ export { app };
|
||||
export const init = () => {
|
||||
OrgInit();
|
||||
UserInit();
|
||||
UserSecretInit();
|
||||
};
|
||||
|
@ -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);
|
||||
|
247
src/models/user-secret.ts
Normal file
247
src/models/user-secret.ts
Normal file
@ -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>('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<OauthUser> = {
|
||||
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>('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);
|
@ -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>('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) => {
|
||||
|
@ -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>('redis');
|
||||
export const oauth = useContextKey('oauth', () => new OAuth(new RedisTokenStore(redis)));
|
||||
export const oauth = useContextKey('oauth', () => {
|
||||
const redis = useContextKey<Redis>('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;
|
||||
});
|
||||
|
@ -76,10 +76,13 @@ interface Store<T> {
|
||||
export class RedisTokenStore implements Store<OauthUser> {
|
||||
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<T extends OauthUser> {
|
||||
constructor(store: Store<T>) {
|
||||
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<T extends OauthUser> {
|
||||
|
||||
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<T>): 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<T extends OauthUser> {
|
||||
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
|
||||
|
Loading…
x
Reference in New Issue
Block a user