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

592 lines
16 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, jwksManager } 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 { UserSecret } from './user-secret.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;
cnbId?: string;
};
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;
// 常量定义
const JWKS_TOKEN_EXPIRY = 2 * 3600; // 2 hours in seconds
export const redis = useContextKey<Redis>('redis');
type TokenOptions = {
expire?: number; // 过期时间,单位秒
ip?: string; // 用户ID默认为当前用户ID
browser?: string; // 浏览器信息
host?: string; // 主机信息
wx?: any;
loginWith?: string; // 登录方式,如 'cli', 'web', 'plugin' 等
hasRefreshToken?: boolean; // 是否需要 refresh token默认为 false
}
/**
* 用户模型,使用 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) {
if (data) {
Object.assign(this, data);
}
}
setTokenUser(tokenUser: any) {
this.tokenUser = tokenUser;
}
/**
* uid 是用于 orgId 的用户id, 如果uid存在则表示是用户是组织其中uid为真实用户
* @param uid
* @returns
*/
/**
* 创建JWKS token的通用方法
*/
static async createJwksTokenResponse(user: { id: string; username: string }, opts: { expire?: number, hasRefreshToken?: boolean } = {}) {
const expiresIn = opts?.expire ?? JWKS_TOKEN_EXPIRY;
const hasRefreshToken = opts?.hasRefreshToken ?? true;
const accessToken = await jwksManager.sign({
sub: 'user:' + user.id,
name: user.username,
exp: Math.floor(Date.now() / 1000) + expiresIn,
});
if (hasRefreshToken) {
await oauth.setJwksToken(accessToken, { id: user.id, expire: expiresIn });
}
const token = {
accessToken,
refreshToken: accessToken,
token: accessToken,
refreshTokenExpiresIn: expiresIn,
accessTokenExpiresIn: expiresIn,
};
return {
type: 'jwks',
...token,
};
}
async createToken(uid?: string, loginType?: 'default' | 'plugin' | 'month' | 'season' | 'year' | 'week' | 'jwks', opts: TokenOptions = {}) {
const { id, username, type } = this;
const hasRefreshToken = opts.hasRefreshToken ?? true;
const oauthUser: OauthUser = {
id,
username,
uid,
userId: uid || id, // 必存在真实用户id
type: type as 'user' | 'org',
};
if (uid) {
oauthUser.orgId = id;
}
if (loginType === 'jwks') {
return await User.createJwksTokenResponse(this, opts);
}
const token = await oauth.generateToken(oauthUser, { type: loginType, hasRefreshToken, ...opts });
return {
type: 'default',
...token,
};
}
/**
* 验证token
* @param token
* @returns
*/
static async verifyToken(token: string) {
return await UserSecret.verifyToken(token);
}
static async checkJwksValid(token: string) {
const verified = await User.verifyToken(token);
let isValid = false;
if (verified) {
isValid = true;
}
const jwksToken = await oauth.getJwksToken(token);
if (!isValid && !jwksToken) {
throw new CustomError('Invalid refresh token');
}
}
/**
* 刷新token
* @param refreshToken
* @returns
*/
static async refreshToken(opts: { refreshToken?: string, accessToken?: string }) {
const { refreshToken, accessToken } = opts;
let jwsRefreshToken = accessToken || refreshToken;
if (oauth.getTokenType(jwsRefreshToken) === 'jwks') {
// 可能是 jwks token
await User.checkJwksValid(jwsRefreshToken);
const decoded = await jwksManager.decode(jwsRefreshToken);
return await User.createJwksTokenResponse({
id: decoded.sub.replace('user:', ''),
username: decoded.name
});
}
if (!refreshToken && !accessToken) {
throw new CustomError('Refresh Token or Access Token 必须提供一个');
}
if (accessToken) {
try {
const token = await User.refreshTokenByAccessToken(accessToken);
return token;
} catch (e) {
// access token 无效,继续使用 refresh token 刷新
}
}
const token = await User.refreshTokenByRefreshToken(refreshToken);
return {
type: 'default',
...token,
};
}
static async refreshTokenByAccessToken(accessToken: string) {
const accessUser = await User.verifyToken(accessToken);
if (!accessUser) {
throw new CustomError('Invalid access token');
}
const refreshToken = accessUser.oauthExpand?.refreshToken;
if (refreshToken) {
return await User.refreshTokenByRefreshToken(refreshToken);
} else {
await User.oauth.delToken(accessToken);
const token = await User.oauth.generateToken(accessUser, {
...accessUser.oauthExpand,
hasRefreshToken: true,
});
return {
type: 'default',
...token,
};
}
}
static async refreshTokenByRefreshToken(refreshToken: string) {
const token = await oauth.refreshToken(refreshToken);
return {
type: 'default',
...token
};
}
/**
* 重置token立即过期token
* @param token
* @returns
*/
static async resetToken(refreshToken: string, expand?: Record<string, any>) {
if (oauth.getTokenType(refreshToken) === 'jwks') {
// 可能是 jwks token
await User.checkJwksValid(refreshToken);
const decoded = await jwksManager.decode(refreshToken);
return await User.createJwksTokenResponse({
id: decoded.sub.replace('user:', ''),
username: decoded.name
});
}
return await oauth.resetToken(refreshToken, expand);
}
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);
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;
}
/**
* 根据 CNB ID 查找用户
* @param cnbId
* @returns
*/
static async findByCnbId(cnbId: string): Promise<User | null> {
const users = await db
.select()
.from(usersTable)
.where(sql`${usersTable.data}->>'cnbId' = ${cnbId}`)
.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 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('Permission denied', { code: 400 });
}
}
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);