Files
code-center/src/auth/models/user.ts

457 lines
12 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 { 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<typeof cfUser>;
export type UserInsert = InferInsertModel<typeof cfUser>;
export type OrgSelect = InferSelectModel<typeof cfOrgs>;
const usersTable = cfUser;
const orgsTable = cfOrgs;
const userSecretsTable = cfUserSecrets;
export const redis = useContextKey<Redis>('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<User | null> {
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<User | null> {
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<User | null> {
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<UserInsert>) {
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<string, any> = {
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);