Files
code-center/src/auth/models/user-secret.ts

328 lines
9.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useContextKey } from '@kevisual/context';
import { Redis } from 'ioredis';
import { User } from './user.ts';
import { oauth, jwksManager } from '../oauth/auth.ts';
import { OauthUser } from '../oauth/oauth.ts';
import { db } from '../../modules/db.ts';
import { cfUserSecrets, cfUser } from '../../db/drizzle/schema.ts';
import { eq, InferSelectModel, InferInsertModel } from 'drizzle-orm';
const userSecretsTable = cfUserSecrets;
const usersTable = cfUser;
export type UserSecretData = {
[key: string]: any;
wxOpenid?: string;
wxUnionid?: string;
wxmpOpenid?: string;
};
export type UserSecretSelect = InferSelectModel<typeof cfUserSecrets>;
export type UserSecretInsert = InferInsertModel<typeof cfUserSecrets>;
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;
};
export class UserSecret {
static oauth = oauth;
id: string;
token: string;
userId: string;
orgId: string;
title: string;
description: string;
status: (typeof UserSecretStatus)[number];
expiredTime: Date;
data: UserSecretData;
constructor(data: UserSecretSelect) {
Object.assign(this, data);
}
/**
* 验证token
* @param token
* @returns
*/
static async verifyToken(token: string) {
if (token?.includes?.('.')) {
// 先尝试作为jwt token验证如果验证成功则直接返回用户信息
console.log('[jwksManager] 验证token');
return await jwksManager.verify(token);
}
if (!oauth.isSecretKey(token)) {
return await oauth.verifyToken(token);
}
const secretToken = await oauth.verifyToken(token);
if (secretToken) {
return secretToken;
}
console.log('verifyToken: try to verify as secret key');
const userSecrets = await db.select().from(userSecretsTable).where(eq(userSecretsTable.token, token)).limit(1);
if (userSecrets.length === 0) {
return null; // 如果没有找到对应的用户密钥则返回null
}
const userSecret = new UserSecret(userSecrets[0]);
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;
}
/**
* 根据主键查找
*/
static async findByPk(id: string): Promise<UserSecret | null> {
const secrets = await db.select().from(userSecretsTable).where(eq(userSecretsTable.id, id)).limit(1);
return secrets.length > 0 ? new UserSecret(secrets[0]) : null;
}
/**
* 根据条件查找一个
*/
static async findOne(where: { token?: string; id?: string }): Promise<UserSecret | null> {
let query = db.select().from(userSecretsTable);
if (where.token) {
query = query.where(eq(userSecretsTable.token, where.token)) as any;
} else if (where.id) {
query = query.where(eq(userSecretsTable.id, where.id)) as any;
}
const secrets = await query.limit(1);
return secrets.length > 0 ? new UserSecret(secrets[0]) : null;
}
/**
* owner 组织用户的 oauthUser
* @returns
*/
async getOauthUser(opts?: { wx?: boolean }) {
const users = await db.select({
id: usersTable.id,
username: usersTable.username,
type: usersTable.type,
owner: usersTable.owner,
data: usersTable.data,
}).from(usersTable).where(eq(usersTable.id, this.userId)).limit(1);
let org: any = null;
if (users.length === 0) {
return null; // 如果没有找到对应的用户则返回null
}
const user = users[0];
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) {
const orgUsers = await db.select({
id: usersTable.id,
username: usersTable.username,
type: usersTable.type,
owner: usersTable.owner,
}).from(usersTable).where(eq(usersTable.id, this.orgId)).limit(1);
if (orgUsers.length > 0) {
org = orgUsers[0];
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 save() {
await db.update(userSecretsTable).set({
token: this.token,
userId: this.userId,
orgId: this.orgId,
title: this.title,
description: this.description,
status: this.status,
expiredTime: this.expiredTime ? this.expiredTime.toISOString() : null,
data: this.data,
updatedAt: new Date().toISOString(),
}).where(eq(userSecretsTable.id, this.id));
}
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({ 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 = null;
if (tokenUser.uid) {
userId = tokenUser.uid;
orgId = tokenUser.id;
}
const insertData: Partial<typeof userSecretsTable.$inferInsert> = {
userId,
token,
title: tokenUser.title || randomString(6),
expiredTime: UserSecret.getExpiredTime(expireDays).toISOString(),
};
if (orgId !== null && orgId !== undefined) {
insertData.orgId = orgId;
}
const inserted = await db.insert(userSecretsTable).values(insertData).returning();
return new UserSecret(inserted[0]);
}
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 orgUsers = await db.select().from(usersTable).where(eq(usersTable.id, this.orgId)).limit(1);
if (orgUsers.length > 0 && orgUsers[0].owner === userId) {
isAdmin = true;
hasPermission = true;
}
}
return {
hasPermission,
isUser,
isAdmin,
};
}
}
export const UserSecretModel = useContextKey('UserSecretModel', () => UserSecret);