323 lines
9.0 KiB
TypeScript
323 lines
9.0 KiB
TypeScript
import { useContextKey } from '@kevisual/context';
|
||
import { Redis } from 'ioredis';
|
||
import { User } from './user.ts';
|
||
import { oauth } 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 (!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);
|