import { DataTypes, Model, Op, Sequelize } from 'sequelize'; import { nanoid, customAlphabet } from 'nanoid'; import { CustomError } from '@kevisual/router'; import { Org } from './org.ts'; import { useContextKey } from '@kevisual/use-config/context'; import { Redis } from 'ioredis'; import { oauth } from '../oauth/auth.ts'; import { cryptPwd } from '../oauth/salt.ts'; import { OauthUser } from '../oauth/oauth.ts'; export const redis = useContextKey('redis'); import { UserSecret } from './user-secret.ts'; type UserData = { orgs?: string[]; wxUnionId?: string; phone?: string; }; export enum UserTypes { 'user' = 'user', 'org' = 'org', 'visitor' = 'visitor', } /** * 用户模型,在sequelize和Org之后初始化 */ export class User extends Model { static oauth = oauth; declare id: string; declare username: string; declare nickname: string; // 昵称 declare password: string; declare salt: string; declare needChangePassword: boolean; declare description: string; declare data: UserData; declare type: string; // user | org | visitor declare owner: string; declare orgId: string; declare email: string; declare avatar: string; tokenUser: any; 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 }; } /** * 验证token * @param token * @returns */ static async verifyToken(token: string) { 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) { 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 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); 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 createUser(username: string, password?: string, description?: string) { const user = await User.findOne({ where: { username } }); if (user) { throw new CustomError('User already exists'); } const salt = nanoid(6); let needChangePassword = !password; password = password || '123456'; const cPassword = cryptPwd(password, salt); return await User.create({ username, password: cPassword, description, salt, needChangePassword }); } static async createOrg(username: string, owner: string, description?: string) { const user = await User.findOne({ where: { 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 newUser = await User.create({ username, password: '', description, type: 'org', owner, orgId: org.id }); // owner add await redis.del(`user:${me.id}:orgs`); return newUser; } async createPassword(password: string) { const salt = this.salt; const cPassword = cryptPwd(password, salt); this.password = cPassword; await this.update({ password: cPassword }); return cPassword; } checkPassword(password: string) { const salt = this.salt; const cPassword = cryptPwd(password, salt); return this.password === cPassword; } /** * 获取用户信息, 需要先设置 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, }; 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[]; } const orgs = await Org.findAll({ order: [['updatedAt', 'DESC']], where: { users: { [Op.contains]: [ { uid: id, }, ], }, }, }); 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`); } } export type SyncOpts = { alter?: boolean; logging?: any; force?: boolean; }; export const UserInit = async (newSequelize?: any, tableName?: string, sync?: SyncOpts) => { const sequelize = useContextKey('sequelize'); User.init( { id: { type: DataTypes.UUID, primaryKey: true, defaultValue: DataTypes.UUIDV4, }, username: { type: DataTypes.STRING, allowNull: false, unique: true, // 用户名或者手机号 // 创建后避免修改的字段,当注册用户后,用户名注册则默认不能用手机号 }, nickname: { type: DataTypes.TEXT, allowNull: true, }, password: { type: DataTypes.STRING, allowNull: true, }, email: { type: DataTypes.STRING, allowNull: true, }, avatar: { type: DataTypes.TEXT, allowNull: true, }, salt: { type: DataTypes.STRING, allowNull: true, }, description: { type: DataTypes.TEXT, }, type: { type: DataTypes.STRING, defaultValue: 'user', }, owner: { type: DataTypes.UUID, }, orgId: { type: DataTypes.UUID, }, needChangePassword: { type: DataTypes.BOOLEAN, defaultValue: false, }, data: { type: DataTypes.JSONB, defaultValue: {}, }, }, { sequelize: newSequelize || sequelize, tableName: tableName || 'cf_user', // codeflow user paranoid: true, }, ); if (sync) { await User.sync({ alter: true, logging: true, ...sync }) .then((res) => { initializeUser(); }) .catch((err) => { console.error('Sync User error', err); }); return User; } return User; }; const letter = 'abcdefghijklmnopqrstuvwxyz'; const custom = customAlphabet(letter, 6); export const initializeUser = async (pwd = custom()) => { const w = await User.findOne({ where: { username: 'root' }, logging: false }); 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({ where: { username }, logging: false }); 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', }; } }; // initializeUser(); export class UserServices extends User { static async loginByPhone(phone: string) { let user = await User.findOne({ where: { 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);