324 lines
8.4 KiB
TypeScript
324 lines
8.4 KiB
TypeScript
import { DataTypes, Model, Sequelize } from 'sequelize';
|
||
|
||
import { useContextKey } from '@kevisual/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;
|
||
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;
|
||
/**
|
||
* 微信开放平台的某一个应用的openid
|
||
*/
|
||
wxOpenid?: string;
|
||
/**
|
||
* 微信开放平台的unionid:主要
|
||
*/
|
||
wxUnionid?: string;
|
||
/**
|
||
* 微信公众号的openid:次要
|
||
*/
|
||
wxmpOpenid?: string;
|
||
|
||
}
|
||
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: Data;
|
||
/**
|
||
* 验证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) {
|
||
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 },
|
||
});
|
||
if (!userSecret) {
|
||
return null; // 如果没有找到对应的用户密钥,则返回null
|
||
}
|
||
if (userSecret.isExpired()) {
|
||
return null; // 如果用户密钥已过期,则返回null
|
||
}
|
||
if (userSecret.status !== 'active') {
|
||
return null; // 如果用户密钥状态不是active,则返回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(opts?: { wx?: boolean }) {
|
||
const user = await User.findOne({
|
||
where: { id: this.userId },
|
||
attributes: ['id', 'username', 'type', 'owner', 'data'],
|
||
});
|
||
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(); // 如果当前时间大于过期时间,则认为已过期
|
||
}
|
||
/**
|
||
* 检查是否过期,如果过期则更新状态为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);
|
||
}
|
||
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;
|
||
}
|
||
/**
|
||
* 根据 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;
|
||
if (tokenUser.uid) {
|
||
userId = tokenUser.uid;
|
||
orgId = tokenUser.id; // 如果是组织用户,则uid是组织ID
|
||
}
|
||
const userSecret = await UserSecret.create({
|
||
userId,
|
||
orgId,
|
||
token,
|
||
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;
|
||
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);
|