import { nanoid, customAlphabet } from 'nanoid'; import { CustomError } from '@kevisual/router'; import { useContextKey } from '@kevisual/context'; import { Redis } from 'ioredis'; import { oauth } from '../oauth/auth.ts'; import { cryptPwd } from '../oauth/salt.ts'; import { OauthUser } from '../oauth/oauth.ts'; import { db } from '../../modules/db.ts'; import { Org } from './org.ts'; import { cfUser, cfOrgs, cfUserSecrets } from '../../db/drizzle/schema.ts'; import { eq, sql, InferSelectModel, InferInsertModel } from 'drizzle-orm'; // 类型定义 export type UserData = { orgs?: string[]; wxUnionId?: string; phone?: string; canChangeUsername?: boolean; }; export enum UserTypes { user = 'user', org = 'org', visitor = 'visitor', } export type UserSelect = InferSelectModel; export type UserInsert = InferInsertModel; export type OrgSelect = InferSelectModel; const usersTable = cfUser; const orgsTable = cfOrgs; const userSecretsTable = cfUserSecrets; export const redis = useContextKey('redis'); /** * 用户模型,使用 Drizzle ORM */ export class User { static oauth = oauth; id: string; username: string; nickname: string; password: string; salt: string; needChangePassword: boolean; description: string; data: UserData; type: string; owner: string; orgId: string; email: string; avatar: string; tokenUser: any; constructor(data: UserSelect) { Object.assign(this, data); } setTokenUser(tokenUser: any) { this.tokenUser = tokenUser; } /** * uid 是用于 orgId 的用户id, 如果uid存在,则表示是用户是组织,其中uid为真实用户 * @param uid * @returns */ async createToken(uid?: string, loginType?: 'default' | 'plugin' | 'month' | 'season' | 'year' | 'week', expand: any = {}) { const { id, username, type } = this; const oauthUser: OauthUser = { id, username, uid, userId: uid || id, // 必存在,真实用户id type: type as 'user' | 'org', }; if (uid) { oauthUser.orgId = id; } const token = await oauth.generateToken(oauthUser, { type: loginType, hasRefreshToken: true, ...expand }); return { accessToken: token.accessToken, refreshToken: token.refreshToken, token: token.accessToken, refreshTokenExpiresIn: token.refreshTokenExpiresIn, accessTokenExpiresIn: token.accessTokenExpiresIn, }; } /** * 验证token * @param token * @returns */ static async verifyToken(token: string) { const { UserSecret } = await import('./user-secret.ts'); return await UserSecret.verifyToken(token); } /** * 刷新token * @param refreshToken * @returns */ static async refreshToken(refreshToken: string) { const token = await oauth.refreshToken(refreshToken); return { accessToken: token.accessToken, refreshToken: token.refreshToken, token: token.accessToken }; } static async getOauthUser(token: string) { const { UserSecret } = await import('./user-secret.ts'); return await UserSecret.verifyToken(token); } /** * 清理用户的token,需要重新登陆 * @param userid * @param orgid * @returns */ static async clearUserToken(userid: string, type: 'org' | 'user' = 'user') { return await oauth.expireUserTokens(userid, type); } /** * 获取用户信息, 并设置tokenUser * @param token * @returns */ static async getUserByToken(token: string) { const { UserSecret } = await import('./user-secret.ts'); const oauthUser = await UserSecret.verifyToken(token); if (!oauthUser) { throw new CustomError('Token is invalid. get UserByToken'); } const userId = oauthUser?.uid || oauthUser.id; const user = await User.findByPk(userId); if (!user) { throw new CustomError('User not found'); } user.setTokenUser(oauthUser); return user; } /** * 判断是否在用户列表中, 需要预先设置 tokenUser * orgs has set curentUser * @param username * @param includeMe * @returns */ async hasUser(username: string, includeMe = true) { const orgs = await this.getOrgs(); const me = this.username; const allUsers = [...orgs]; if (includeMe) { allUsers.push(me); } return allUsers.includes(username); } /** * 根据主键查找用户 */ static async findByPk(id: string): Promise { const users = await db.select().from(usersTable).where(eq(usersTable.id, id)).limit(1); return users.length > 0 ? new User(users[0]) : null; } /** * 根据微信 UnionId 查找用户 */ static async findByUnionId(unionId: string): Promise { const users = await db .select() .from(usersTable) .where(sql`${usersTable.data}->>'wxUnionId' = ${unionId}`) .limit(1); return users.length > 0 ? new User(users[0]) : null; } /** * 根据条件查找一个用户 */ static async findOne(where: { username?: string; id?: string; email?: string }): Promise { let query = db.select().from(usersTable); if (where.username) { query = query.where(eq(usersTable.username, where.username)) as any; } else if (where.id) { query = query.where(eq(usersTable.id, where.id)) as any; } else if (where.email) { query = query.where(eq(usersTable.email, where.email)) as any; } const users = await query.limit(1); return users.length > 0 ? new User(users[0]) : null; } static findByunionid(){ } static async createUser(username: string, password?: string, description?: string) { const user = await User.findOne({ username }); if (user) { throw new CustomError('User already exists'); } const salt = nanoid(6); let needChangePassword = !password; password = password || '123456'; const cPassword = cryptPwd(password, salt); const insertData: any = { username, password: cPassword, salt, }; // 只在需要时才设置非默认值 if (needChangePassword) { insertData.needChangePassword = true; } if (description !== undefined && description !== null) { insertData.description = description; } try { const inserted = await db.insert(usersTable).values(insertData).returning(); return new User(inserted[0]); } catch (e) { console.log(e) throw e } } static async createOrg(username: string, owner: string, description?: string) { const user = await User.findOne({ username }); if (user) { throw new CustomError('User already exists'); } const me = await User.findByPk(owner); if (!me) { throw new CustomError('Owner not found'); } if (me.type !== 'user') { throw new CustomError('Owner type is not user'); } const org = await Org.create({ username, description, users: [{ uid: owner, role: 'owner' }] }); const insertData: any = { username, password: '', type: 'org', owner, orgId: org.id, }; if (description !== undefined && description !== null) { insertData.description = description; } const inserted = await db.insert(usersTable).values(insertData).returning(); // owner add await redis.del(`user:${me.id}:orgs`); return new User(inserted[0]); } async createPassword(password: string) { const salt = this.salt; const cPassword = cryptPwd(password, salt); this.password = cPassword; await db.update(usersTable).set({ password: cPassword }).where(eq(usersTable.id, this.id)); return cPassword; } checkPassword(password: string) { const salt = this.salt; const cPassword = cryptPwd(password, salt); return this.password === cPassword; } /** * 更新用户 */ async update(data: Partial) { await db.update(usersTable).set(data).where(eq(usersTable.id, this.id)); Object.assign(this, data); } /** * 保存用户 */ async save() { await db.update(usersTable).set({ username: this.username, nickname: this.nickname, password: this.password, email: this.email, avatar: this.avatar, salt: this.salt, description: this.description, type: this.type, owner: this.owner, orgId: this.orgId, needChangePassword: this.needChangePassword, data: this.data, updatedAt: new Date().toISOString(), }).where(eq(usersTable.id, this.id)); } /** * 获取用户信息, 需要先设置 tokenUser 或者设置 uid * @param uid 如果存在,则表示是组织,其中uid为真实用户 * @returns */ async getInfo(uid?: string) { const orgs = await this.getOrgs(); const info: Record = { id: this.id, username: this.username, nickname: this.nickname, description: this.description, needChangePassword: this.needChangePassword, type: this.type, avatar: this.avatar, orgs, }; if (this.data?.canChangeUsername) { info.canChangeUsername = this.data.canChangeUsername; } const tokenUser = this.tokenUser; if (uid) { info.uid = uid; } else if (tokenUser.uid) { info.uid = tokenUser.uid; } return info; } /** * 获取用户组织 * @returns */ async getOrgs() { let id = this.id; if (this.type === 'org') { if (this.tokenUser && this.tokenUser.uid) { id = this.tokenUser.uid; } else { throw new CustomError(400, 'Permission denied'); } } const cache = await redis.get(`user:${id}:orgs`); if (cache) { return JSON.parse(cache) as string[]; } // 使用 Drizzle 的 SQL 查询来检查 JSONB 数组 const orgs = await db .select() .from(orgsTable) .where(sql`${orgsTable.users} @> ${JSON.stringify([{ uid: id }])}::jsonb`) .orderBy(sql`${orgsTable.updatedAt} DESC`); const orgNames = orgs.map((org) => org.username); if (orgNames.length > 0) { await redis.set(`user:${id}:orgs`, JSON.stringify(orgNames), 'EX', 60 * 60); // 1 hour } return orgNames; } async expireOrgs() { await redis.del(`user:${this.id}:orgs`); } static async getUserNameById(id: string) { const redisName = await redis.get(`user:id:${id}:name`); if (redisName) { return redisName; } const user = await User.findByPk(id); if (user?.username) { await redis.set(`user:id:${id}:name`, user.username, 'EX', 60 * 60); // 1 hour } return user?.username; } /** * 查找所有符合条件的用户 */ static async findAll(options: { where?: any; attributes?: string[] }) { let query = db.select().from(usersTable); if (options.where?.id?.in) { query = query.where(sql`${usersTable.id} = ANY(${options.where.id.in})`) as any; } const users = await query; return users.map(u => new User(u)); } } const letter = 'abcdefghijklmnopqrstuvwxyz'; const custom = customAlphabet(letter, 6); export const initializeUser = async (pwd = custom()) => { const w = await User.findOne({ username: 'root' }); if (!w) { const root = await User.createUser('root', pwd, '系统管理员'); const org = await User.createOrg('admin', root.id, '管理员'); console.info(' new Users name', root.username, org.username); console.info('new Users root password', pwd); console.info('new Users id', root.id, org.id); const demo = await createDemoUser(); return { code: 200, data: { root, org, pwd: pwd, demo }, }; } else { return { code: 500, message: 'Users has been created', }; } }; export const createDemoUser = async (username = 'demo', pwd = custom()) => { const u = await User.findOne({ username }); if (!u) { const user = await User.createUser(username, pwd, 'demo'); console.info('new Users name', user.username, pwd); return { code: 200, data: { user, pwd: pwd }, }; } else { console.info('Users has been created', u.username); return { code: 500, message: 'Users has been created', }; } }; export class UserServices extends User { static async loginByPhone(phone: string) { let user = await User.findOne({ username: phone }); let isNew = false; if (!user) { user = await User.createUser(phone, phone.slice(-6)); isNew = true; } const token = await user.createToken(null, 'season'); return { ...token, isNew }; } static initializeUser = initializeUser; static createDemoUser = createDemoUser; } export const UserModel = useContextKey('UserModel', () => UserServices);