2025-02-23 10:22:06 +08:00

289 lines
8.0 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 { useConfig } from '@kevisual/use-config';
import { sequelize } from '@/modules/sequelize.ts';
import { DataTypes, Model, Op } from 'sequelize';
import { createToken, checkToken } from '@kevisual/auth';
import { cryptPwd } from '@kevisual/auth';
import { customRandom, nanoid, customAlphabet } from 'nanoid';
import { CustomError } from '@kevisual/router';
import { Org } from './org.ts';
import { redis } from '@/app.ts';
const config = useConfig<{ tokenSecret: string }>();
type UserData = {
orgs?: string[];
};
export class User extends Model {
declare id: string;
declare username: string;
declare nickname: string; // 昵称
declare alias: 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 真实用户的id
* @param uid
* @returns
*/
async createToken(uid?: string, loginType?: 'default' | 'plugin' | 'month' | 'season' | 'year') {
const { id, username, type } = this;
let expireTime = 60 * 60 * 24 * 7; // 7 days
switch (loginType) {
case 'plugin':
expireTime = 60 * 60 * 24 * 30 * 12; // 365 days
break;
case 'month':
expireTime = 60 * 60 * 24 * 30; // 30 days
break;
case 'season':
expireTime = 60 * 60 * 24 * 30 * 3; // 90 days
break;
case 'year':
expireTime = 60 * 60 * 24 * 30 * 12; // 365 days
break;
}
const now = new Date().getTime();
const token = await createToken({ id, username, uid, type }, config.tokenSecret);
return { token, expireTime: now + expireTime };
}
static async verifyToken(token: string) {
const ct = await checkToken(token, config.tokenSecret);
const tokenUser = ct.payload;
return tokenUser;
}
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;
}
createPassword(password: string) {
const salt = this.salt;
const cPassword = cryptPwd(password, salt);
this.password = cPassword;
return cPassword;
}
checkPassword(password: string) {
const salt = this.salt;
const cPassword = cryptPwd(password, salt);
return this.password === cPassword;
}
async getInfo() {
const orgs = await this.getOrgs();
return {
id: this.id,
username: this.username,
nickname: this.nickname,
description: this.description,
needChangePassword: this.needChangePassword,
type: this.type,
avatar: this.avatar,
orgs,
};
}
async getOrgs() {
let id = this.id;
if (this.type === 'org') {
if (this.tokenUser && this.tokenUser.uid) {
id = this.tokenUser.uid;
} else {
console.log('getOrgs', 'no uid', this.id, this.username);
throw new CustomError('Permission denied');
}
}
const cache = await redis.get(`user:${id}:orgs`);
if (cache) {
return JSON.parse(cache);
}
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`);
}
}
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,
},
alias: {
type: DataTypes.TEXT,
allowNull: true, // 别名网络请求的别名需要唯一不能和username重复
},
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.JSON,
defaultValue: {},
},
},
{
sequelize,
tableName: 'cf_user', // codeflow user
paranoid: true,
},
);
User.sync({ alter: true, logging: false })
.then((res) => {
initializeUser();
})
.catch((err) => {
console.error('Sync User error', err);
});
const letter = 'abcdefghijklmnopqrstuvwxyz';
const custom = customAlphabet(letter, 6);
export const initializeUser = async (pwd = custom()) => {
const w = await User.findAndCountAll();
console.info('[User count]', w.count);
if (w.count < 1) {
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 w = await User.findAndCountAll({
logging: false,
});
console.info('[User count]', w.count);
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);
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;
}