2025-06-19 19:20:27 +08:00

371 lines
10 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 { 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>('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<string, any> = {
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>('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);