Refactor app management to use Drizzle ORM
- Replaced Sequelize models with Drizzle ORM for app and app list management. - Updated routes in app-manager to utilize new database queries. - Removed obsolete Sequelize model files for app, app list, and app domain. - Introduced new helper functions for app and app domain management. - Enhanced user app management with improved file handling and user migration. - Adjusted public API routes to align with new database structure. - Implemented caching mechanisms for domain management.
This commit is contained in:
@@ -1,42 +0,0 @@
|
||||
import { pgTable, serial, text, varchar, uuid, boolean, jsonb, timestamp } from "drizzle-orm/pg-core";
|
||||
import { InferSelectModel, InferInsertModel } from "drizzle-orm";
|
||||
|
||||
export const users = pgTable('cf_user', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
username: text('username').notNull().unique(),
|
||||
nickname: text('nickname'),
|
||||
password: text('password'),
|
||||
email: text('email'),
|
||||
avatar: text('avatar'),
|
||||
salt: text('salt'),
|
||||
description: text('description'),
|
||||
type: text('type').notNull().default('user'),
|
||||
owner: uuid('owner'),
|
||||
orgId: uuid('orgId'),
|
||||
needChangePassword: boolean('needChangePassword').notNull().default(false),
|
||||
data: jsonb('data').notNull().default({}),
|
||||
createdAt: timestamp('createdAt').notNull().defaultNow(),
|
||||
updatedAt: timestamp('updatedAt').notNull().defaultNow(),
|
||||
deletedAt: timestamp('deletedAt'),
|
||||
});
|
||||
|
||||
// 类型推断
|
||||
export type User = InferSelectModel<typeof users>;
|
||||
export type NewUser = InferInsertModel<typeof users>;
|
||||
|
||||
// 用户数据类型
|
||||
export type UserData = {
|
||||
orgs?: string[];
|
||||
wxUnionId?: string;
|
||||
phone?: string;
|
||||
};
|
||||
|
||||
// 用户类型枚举
|
||||
export enum UserTypes {
|
||||
user = 'user',
|
||||
org = 'org',
|
||||
visitor = 'visitor',
|
||||
}
|
||||
// export class User {
|
||||
|
||||
// }
|
||||
@@ -1,3 +1,3 @@
|
||||
export { User, UserInit, UserServices, UserModel } from './user.ts';
|
||||
export { UserSecretInit, UserSecret } from './user-secret.ts';
|
||||
export { OrgInit, Org } from './org.ts';
|
||||
export { User, UserServices, UserModel, initializeUser, createDemoUser } from './user.ts';
|
||||
export { UserSecret, UserSecretModel } from './user-secret.ts';
|
||||
export { Org, OrgModel, OrgRole } from './org.ts';
|
||||
@@ -1,20 +1,35 @@
|
||||
import { DataTypes, Model, Op, Sequelize } from 'sequelize';
|
||||
import { useContextKey } from '@kevisual/context';
|
||||
import { SyncOpts, User } from './user.ts';
|
||||
import { User } from './user.ts';
|
||||
import { db } from '../../modules/db.ts';
|
||||
import { cfOrgs, cfUser } from '../../db/drizzle/schema.ts';
|
||||
import { eq, inArray, sql, InferSelectModel, InferInsertModel } from 'drizzle-orm';
|
||||
|
||||
const orgsTable = cfOrgs;
|
||||
const usersTable = cfUser;
|
||||
|
||||
type AddUserOpts = {
|
||||
role: string;
|
||||
};
|
||||
export enum OrgRole {
|
||||
admin = 'admin',
|
||||
member = 'member',
|
||||
owner = 'owner',
|
||||
}
|
||||
export class Org extends Model {
|
||||
declare id: string;
|
||||
declare username: string;
|
||||
declare description: string;
|
||||
declare users: { role: string; uid: string }[];
|
||||
|
||||
export type OrgUser = {
|
||||
role: string;
|
||||
uid: string;
|
||||
};
|
||||
|
||||
export type OrgSelect = InferSelectModel<typeof cfOrgs>;
|
||||
export type OrgInsert = InferInsertModel<typeof cfOrgs>;
|
||||
|
||||
export class Org {
|
||||
id: string;
|
||||
username: string;
|
||||
description: string;
|
||||
users: OrgUser[];
|
||||
|
||||
constructor(data: OrgSelect) {
|
||||
Object.assign(this, data);
|
||||
}
|
||||
/**
|
||||
* operateId 是真实操作者的id
|
||||
* @param user
|
||||
@@ -67,8 +82,8 @@ export class Org extends Model {
|
||||
} else {
|
||||
users.push({ role: opts?.role || 'member', uid: user.id });
|
||||
}
|
||||
await Org.update({ users }, { where: { id: this.id } });
|
||||
|
||||
await db.update(orgsTable).set({ users }).where(eq(orgsTable.id, this.id));
|
||||
this.users = users;
|
||||
}
|
||||
/**
|
||||
* operateId 是真实操作者的id
|
||||
@@ -89,7 +104,8 @@ export class Org extends Model {
|
||||
}
|
||||
await user.expireOrgs();
|
||||
const users = this.users.filter((u) => u.uid !== user.id || u.role === 'owner');
|
||||
await Org.update({ users }, { where: { id: this.id } });
|
||||
await db.update(orgsTable).set({ users }).where(eq(orgsTable.id, this.id));
|
||||
this.users = users;
|
||||
}
|
||||
/**
|
||||
* operateId 是真实操作者的id
|
||||
@@ -112,13 +128,7 @@ export class Org extends Model {
|
||||
}
|
||||
}
|
||||
}
|
||||
const _users = await User.findAll({
|
||||
where: {
|
||||
id: {
|
||||
[Op.in]: usersIds,
|
||||
},
|
||||
},
|
||||
});
|
||||
const _users = await db.select().from(usersTable).where(inArray(usersTable.id, usersIds));
|
||||
|
||||
const users = _users.map((u) => {
|
||||
const role = orgUser.find((r) => r.uid === u.id)?.role;
|
||||
@@ -139,46 +149,45 @@ export class Org extends Model {
|
||||
const user = this.users.find((u) => u.uid === userId && u.role === role);
|
||||
return !!user;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 组织模型,在sequelize之后初始化
|
||||
*/
|
||||
export const OrgInit = async (newSequelize?: any, tableName?: string, sync?: SyncOpts) => {
|
||||
const sequelize = useContextKey<Sequelize>('sequelize');
|
||||
Org.init(
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
primaryKey: true,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
},
|
||||
username: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
},
|
||||
description: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
},
|
||||
users: {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: true,
|
||||
defaultValue: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
sequelize: newSequelize || sequelize,
|
||||
modelName: tableName || 'cf_org',
|
||||
paranoid: true,
|
||||
},
|
||||
);
|
||||
if (sync) {
|
||||
await Org.sync({ alter: true, logging: false, ...sync }).catch((e) => {
|
||||
console.error('Org sync', e);
|
||||
});
|
||||
return Org;
|
||||
|
||||
/**
|
||||
* 根据主键查找
|
||||
*/
|
||||
static async findByPk(id: string): Promise<Org | null> {
|
||||
const orgs = await db.select().from(orgsTable).where(eq(orgsTable.id, id)).limit(1);
|
||||
return orgs.length > 0 ? new Org(orgs[0]) : null;
|
||||
}
|
||||
return Org;
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据条件查找一个
|
||||
*/
|
||||
static async findOne(where: { username?: string; id?: string }): Promise<Org | null> {
|
||||
let query = db.select().from(orgsTable);
|
||||
|
||||
if (where.username) {
|
||||
query = query.where(eq(orgsTable.username, where.username)) as any;
|
||||
} else if (where.id) {
|
||||
query = query.where(eq(orgsTable.id, where.id)) as any;
|
||||
}
|
||||
|
||||
const orgs = await query.limit(1);
|
||||
return orgs.length > 0 ? new Org(orgs[0]) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建组织
|
||||
*/
|
||||
static async create(data: { username: string; description?: string; users: OrgUser[] }): Promise<Org> {
|
||||
const inserted = await db.insert(orgsTable).values(data).returning();
|
||||
return new Org(inserted[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新组织
|
||||
*/
|
||||
static async update(data: Partial<OrgInsert>, where: { id: string }) {
|
||||
await db.update(orgsTable).set(data).where(eq(orgsTable.id, where.id));
|
||||
}
|
||||
}
|
||||
|
||||
export const OrgModel = useContextKey('OrgModel', () => Org);
|
||||
|
||||
@@ -1,10 +1,25 @@
|
||||
import { DataTypes, Model, Sequelize } from 'sequelize';
|
||||
|
||||
import { useContextKey } from '@kevisual/context';
|
||||
import { Redis } from 'ioredis';
|
||||
import { SyncOpts, User } from './user.ts';
|
||||
import { User } from './user.ts';
|
||||
import { oauth } from '../oauth/auth.ts';
|
||||
import { OauthUser } from '../oauth/oauth.ts';
|
||||
import { db } from '../../modules/db.ts';
|
||||
import { cfUserSecrets, cfUser } from '../../db/drizzle/schema.ts';
|
||||
import { eq, InferSelectModel, InferInsertModel } from 'drizzle-orm';
|
||||
|
||||
const userSecretsTable = cfUserSecrets;
|
||||
const usersTable = cfUser;
|
||||
|
||||
export type UserSecretData = {
|
||||
[key: string]: any;
|
||||
wxOpenid?: string;
|
||||
wxUnionid?: string;
|
||||
wxmpOpenid?: string;
|
||||
};
|
||||
|
||||
export type UserSecretSelect = InferSelectModel<typeof cfUserSecrets>;
|
||||
export type UserSecretInsert = InferInsertModel<typeof cfUserSecrets>;
|
||||
|
||||
export const redis = useContextKey<Redis>('redis');
|
||||
|
||||
const UserSecretStatus = ['active', 'inactive', 'expired'] as const;
|
||||
@@ -16,33 +31,22 @@ const randomString = (length: number) => {
|
||||
}
|
||||
return result;
|
||||
};
|
||||
type Data = {
|
||||
[key: string]: any;
|
||||
/**
|
||||
* 微信开放平台的某一个应用的openid
|
||||
*/
|
||||
wxOpenid?: string;
|
||||
/**
|
||||
* 微信开放平台的unionid:主要
|
||||
*/
|
||||
wxUnionid?: string;
|
||||
/**
|
||||
* 微信公众号的openid:次要
|
||||
*/
|
||||
wxmpOpenid?: string;
|
||||
|
||||
}
|
||||
export class UserSecret extends Model {
|
||||
export class UserSecret {
|
||||
static oauth = oauth;
|
||||
declare id: string;
|
||||
declare token: string;
|
||||
declare userId: string;
|
||||
declare orgId: string;
|
||||
declare title: string;
|
||||
declare description: string;
|
||||
declare status: (typeof UserSecretStatus)[number];
|
||||
declare expiredTime: Date;
|
||||
declare data: Data;
|
||||
id: string;
|
||||
token: string;
|
||||
userId: string;
|
||||
orgId: string;
|
||||
title: string;
|
||||
description: string;
|
||||
status: (typeof UserSecretStatus)[number];
|
||||
expiredTime: Date;
|
||||
data: UserSecretData;
|
||||
|
||||
constructor(data: UserSecretSelect) {
|
||||
Object.assign(this, data);
|
||||
}
|
||||
/**
|
||||
* 验证token
|
||||
* @param token
|
||||
@@ -57,12 +61,13 @@ export class UserSecret extends Model {
|
||||
return secretToken;
|
||||
}
|
||||
console.log('verifyToken: try to verify as secret key');
|
||||
const userSecret = await UserSecret.findOne({
|
||||
where: { token },
|
||||
});
|
||||
if (!userSecret) {
|
||||
const userSecrets = await db.select().from(userSecretsTable).where(eq(userSecretsTable.token, token)).limit(1);
|
||||
|
||||
if (userSecrets.length === 0) {
|
||||
return null; // 如果没有找到对应的用户密钥,则返回null
|
||||
}
|
||||
|
||||
const userSecret = new UserSecret(userSecrets[0]);
|
||||
if (userSecret.isExpired()) {
|
||||
return null; // 如果用户密钥已过期,则返回null
|
||||
}
|
||||
@@ -78,19 +83,49 @@ export class UserSecret extends Model {
|
||||
// 存储到oauth中的token store中
|
||||
return oauthUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据主键查找
|
||||
*/
|
||||
static async findByPk(id: string): Promise<UserSecret | null> {
|
||||
const secrets = await db.select().from(userSecretsTable).where(eq(userSecretsTable.id, id)).limit(1);
|
||||
return secrets.length > 0 ? new UserSecret(secrets[0]) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据条件查找一个
|
||||
*/
|
||||
static async findOne(where: { token?: string; id?: string }): Promise<UserSecret | null> {
|
||||
let query = db.select().from(userSecretsTable);
|
||||
|
||||
if (where.token) {
|
||||
query = query.where(eq(userSecretsTable.token, where.token)) as any;
|
||||
} else if (where.id) {
|
||||
query = query.where(eq(userSecretsTable.id, where.id)) as any;
|
||||
}
|
||||
|
||||
const secrets = await query.limit(1);
|
||||
return secrets.length > 0 ? new UserSecret(secrets[0]) : null;
|
||||
}
|
||||
/**
|
||||
* owner 组织用户的 oauthUser
|
||||
* @returns
|
||||
*/
|
||||
async getOauthUser(opts?: { wx?: boolean }) {
|
||||
const user = await User.findOne({
|
||||
where: { id: this.userId },
|
||||
attributes: ['id', 'username', 'type', 'owner', 'data'],
|
||||
});
|
||||
let org: User = null;
|
||||
if (!user) {
|
||||
const users = await db.select({
|
||||
id: usersTable.id,
|
||||
username: usersTable.username,
|
||||
type: usersTable.type,
|
||||
owner: usersTable.owner,
|
||||
data: usersTable.data,
|
||||
}).from(usersTable).where(eq(usersTable.id, this.userId)).limit(1);
|
||||
|
||||
let org: any = null;
|
||||
if (users.length === 0) {
|
||||
return null; // 如果没有找到对应的用户,则返回null
|
||||
}
|
||||
|
||||
const user = users[0];
|
||||
const expiredTime = this.expiredTime ? new Date(this.expiredTime).getTime() : null;
|
||||
const oauthUser: Partial<OauthUser> = {
|
||||
id: user.id,
|
||||
@@ -101,11 +136,15 @@ export class UserSecret extends Model {
|
||||
},
|
||||
};
|
||||
if (this.orgId) {
|
||||
org = await User.findOne({
|
||||
where: { id: this.orgId },
|
||||
attributes: ['id', 'username', 'type', 'owner'],
|
||||
});
|
||||
if (org) {
|
||||
const orgUsers = await db.select({
|
||||
id: usersTable.id,
|
||||
username: usersTable.username,
|
||||
type: usersTable.type,
|
||||
owner: usersTable.owner,
|
||||
}).from(usersTable).where(eq(usersTable.id, this.orgId)).limit(1);
|
||||
|
||||
if (orgUsers.length > 0) {
|
||||
org = orgUsers[0];
|
||||
oauthUser.id = org.id;
|
||||
oauthUser.username = org.username;
|
||||
oauthUser.type = 'org';
|
||||
@@ -125,6 +164,7 @@ export class UserSecret extends Model {
|
||||
const expiredTime = new Date(this.expiredTime);
|
||||
return now > expiredTime.getTime(); // 如果当前时间大于过期时间,则认为已过期
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否过期,如果过期则更新状态为expired
|
||||
*
|
||||
@@ -137,7 +177,6 @@ export class UserSecret extends Model {
|
||||
}
|
||||
}
|
||||
try {
|
||||
|
||||
const now = Date.now();
|
||||
const expiredTime = new Date(this.expiredTime);
|
||||
const isExpired = now > expiredTime.getTime(); // 如果当前时间大于过期时间,则认为已过期
|
||||
@@ -145,11 +184,11 @@ export class UserSecret extends Model {
|
||||
this.status = 'active';
|
||||
const expireTime = UserSecret.getExpiredTime();
|
||||
this.expiredTime = expireTime;
|
||||
await this.save()
|
||||
await this.save();
|
||||
}
|
||||
if (this.status !== 'active') {
|
||||
this.status = 'active';
|
||||
await this.save()
|
||||
await this.save();
|
||||
}
|
||||
return {
|
||||
code: 200
|
||||
@@ -163,6 +202,20 @@ export class UserSecret extends Model {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async save() {
|
||||
await db.update(userSecretsTable).set({
|
||||
token: this.token,
|
||||
userId: this.userId,
|
||||
orgId: this.orgId,
|
||||
title: this.title,
|
||||
description: this.description,
|
||||
status: this.status,
|
||||
expiredTime: this.expiredTime ? this.expiredTime.toISOString() : null,
|
||||
data: this.data,
|
||||
updatedAt: new Date().toISOString(),
|
||||
}).where(eq(userSecretsTable.id, this.id));
|
||||
}
|
||||
async createNewToken() {
|
||||
if (this.token) {
|
||||
await oauth.delToken(this.token);
|
||||
@@ -172,14 +225,16 @@ export class UserSecret extends Model {
|
||||
await this.save();
|
||||
return token;
|
||||
}
|
||||
|
||||
static async createToken() {
|
||||
let token = oauth.generateSecretKey();
|
||||
// 确保生成的token是唯一的
|
||||
while (await UserSecret.findOne({ where: { token } })) {
|
||||
while (await UserSecret.findOne({ token })) {
|
||||
token = oauth.generateSecretKey();
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 unionid 生成redis的key
|
||||
* `wxmp:unionid:token:${unionid}`
|
||||
@@ -189,28 +244,30 @@ export class UserSecret extends Model {
|
||||
static wxRedisKey(unionid: string) {
|
||||
return `wxmp:unionid:token:${unionid}`;
|
||||
}
|
||||
|
||||
static getExpiredTime(expireDays?: number) {
|
||||
const defaultExpireDays = expireDays || 365;
|
||||
const expireTime = defaultExpireDays * 24 * 60 * 60 * 1000;
|
||||
return new Date(Date.now() + expireTime)
|
||||
return new Date(Date.now() + expireTime);
|
||||
}
|
||||
|
||||
static async createSecret(tokenUser: { id: string; uid?: string, title?: string }, expireDays = 365) {
|
||||
const token = await UserSecret.createToken();
|
||||
let userId = tokenUser.id;
|
||||
let orgId: string = null;
|
||||
let orgId: string | null = null;
|
||||
if (tokenUser.uid) {
|
||||
userId = tokenUser.uid;
|
||||
orgId = tokenUser.id; // 如果是组织用户,则uid是组织ID
|
||||
orgId = tokenUser.id;
|
||||
}
|
||||
const userSecret = await UserSecret.create({
|
||||
const inserted = await db.insert(userSecretsTable).values({
|
||||
userId,
|
||||
orgId,
|
||||
token,
|
||||
title: tokenUser.title || randomString(6),
|
||||
expiredTime: UserSecret.getExpiredTime(expireDays),
|
||||
});
|
||||
expiredTime: UserSecret.getExpiredTime(expireDays).toISOString(),
|
||||
}).returning();
|
||||
|
||||
return userSecret;
|
||||
return new UserSecret(inserted[0]);
|
||||
}
|
||||
|
||||
async getPermission(opts: { id: string; uid?: string }) {
|
||||
@@ -242,8 +299,8 @@ export class UserSecret extends Model {
|
||||
};
|
||||
}
|
||||
if (this.orgId) {
|
||||
const orgUser = await User.findByPk(this.orgId);
|
||||
if (orgUser && orgUser.owner === userId) {
|
||||
const orgUsers = await db.select().from(usersTable).where(eq(usersTable.id, this.orgId)).limit(1);
|
||||
if (orgUsers.length > 0 && orgUsers[0].owner === userId) {
|
||||
isAdmin = true;
|
||||
hasPermission = true;
|
||||
}
|
||||
@@ -255,68 +312,5 @@ export class UserSecret extends Model {
|
||||
};
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 组织模型,在sequelize之后初始化
|
||||
*/
|
||||
export const UserSecretInit = async (newSequelize?: any, tableName?: string, sync?: SyncOpts) => {
|
||||
const sequelize = useContextKey<Sequelize>('sequelize');
|
||||
UserSecret.init(
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
primaryKey: true,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
},
|
||||
description: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
defaultValue: 'active',
|
||||
comment: '状态',
|
||||
},
|
||||
title: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
},
|
||||
expiredTime: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
},
|
||||
token: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
comment: '用户密钥',
|
||||
defaultValue: '',
|
||||
},
|
||||
userId: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: true,
|
||||
},
|
||||
data: {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: true,
|
||||
defaultValue: {},
|
||||
},
|
||||
orgId: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: true,
|
||||
comment: '组织ID',
|
||||
},
|
||||
},
|
||||
{
|
||||
sequelize: newSequelize || sequelize,
|
||||
modelName: tableName || 'cf_user_secret',
|
||||
},
|
||||
);
|
||||
if (sync) {
|
||||
await UserSecret.sync({ alter: true, logging: false, ...sync }).catch((e) => {
|
||||
console.error('UserSecret sync', e);
|
||||
});
|
||||
return UserSecret;
|
||||
}
|
||||
return UserSecret;
|
||||
};
|
||||
|
||||
export const UserSecretModel = useContextKey('UserSecretModel', () => UserSecret);
|
||||
|
||||
@@ -1,45 +1,62 @@
|
||||
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/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 = {
|
||||
import { db } from '../../modules/db.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',
|
||||
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');
|
||||
|
||||
/**
|
||||
* 用户模型,在sequelize和Org之后初始化
|
||||
* 用户模型,使用 Drizzle ORM
|
||||
*/
|
||||
export class User extends Model {
|
||||
export class User {
|
||||
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;
|
||||
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;
|
||||
}
|
||||
@@ -76,6 +93,7 @@ export class User extends Model {
|
||||
* @returns
|
||||
*/
|
||||
static async verifyToken(token: string) {
|
||||
const { UserSecret } = await import('./user-secret.ts');
|
||||
return await UserSecret.verifyToken(token);
|
||||
}
|
||||
/**
|
||||
@@ -88,6 +106,7 @@ export class User extends Model {
|
||||
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);
|
||||
}
|
||||
/**
|
||||
@@ -105,12 +124,16 @@ export class User extends Model {
|
||||
* @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;
|
||||
}
|
||||
@@ -130,8 +153,33 @@ export class User extends Model {
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据条件查找一个用户
|
||||
*/
|
||||
static async findOne(where: { username?: string; id?: 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;
|
||||
}
|
||||
|
||||
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({ where: { username } });
|
||||
const user = await User.findOne({ username });
|
||||
if (user) {
|
||||
throw new CustomError('User already exists');
|
||||
}
|
||||
@@ -139,10 +187,20 @@ export class User extends Model {
|
||||
let needChangePassword = !password;
|
||||
password = password || '123456';
|
||||
const cPassword = cryptPwd(password, salt);
|
||||
return await User.create({ username, password: cPassword, description, salt, needChangePassword });
|
||||
|
||||
const inserted = await db.insert(usersTable).values({
|
||||
username,
|
||||
password: cPassword,
|
||||
description,
|
||||
salt,
|
||||
needChangePassword,
|
||||
}).returning();
|
||||
|
||||
return new User(inserted[0]);
|
||||
}
|
||||
|
||||
static async createOrg(username: string, owner: string, description?: string) {
|
||||
const user = await User.findOne({ where: { username } });
|
||||
const user = await User.findOne({ username });
|
||||
if (user) {
|
||||
throw new CustomError('User already exists');
|
||||
}
|
||||
@@ -153,24 +211,64 @@ export class User extends Model {
|
||||
if (me.type !== 'user') {
|
||||
throw new CustomError('Owner type is not user');
|
||||
}
|
||||
|
||||
const { Org } = await import('./org.ts');
|
||||
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 });
|
||||
const inserted = await db.insert(usersTable).values({
|
||||
username,
|
||||
password: '',
|
||||
description,
|
||||
type: 'org',
|
||||
owner,
|
||||
orgId: org.id,
|
||||
}).returning();
|
||||
|
||||
// owner add
|
||||
await redis.del(`user:${me.id}:orgs`);
|
||||
return newUser;
|
||||
return new User(inserted[0]);
|
||||
}
|
||||
async createPassword(password: string) {
|
||||
const salt = this.salt;
|
||||
const cPassword = cryptPwd(password, salt);
|
||||
this.password = cPassword;
|
||||
await this.update({ 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为真实用户
|
||||
@@ -217,18 +315,14 @@ export class User extends Model {
|
||||
if (cache) {
|
||||
return JSON.parse(cache) as string[];
|
||||
}
|
||||
const orgs = await Org.findAll({
|
||||
order: [['updatedAt', 'DESC']],
|
||||
where: {
|
||||
users: {
|
||||
[Op.contains]: [
|
||||
{
|
||||
uid: id,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// 使用 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
|
||||
@@ -249,93 +343,27 @@ export class User extends Model {
|
||||
}
|
||||
return user?.username;
|
||||
}
|
||||
}
|
||||
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;
|
||||
/**
|
||||
* 查找所有符合条件的用户
|
||||
*/
|
||||
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));
|
||||
}
|
||||
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 });
|
||||
const w = await User.findOne({ username: 'root' });
|
||||
if (!w) {
|
||||
const root = await User.createUser('root', pwd, '系统管理员');
|
||||
const org = await User.createOrg('admin', root.id, '管理员');
|
||||
@@ -354,8 +382,9 @@ export const initializeUser = async (pwd = custom()) => {
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const createDemoUser = async (username = 'demo', pwd = custom()) => {
|
||||
const u = await User.findOne({ where: { username }, logging: false });
|
||||
const u = await User.findOne({ username });
|
||||
if (!u) {
|
||||
const user = await User.createUser(username, pwd, 'demo');
|
||||
console.info('new Users name', user.username, pwd);
|
||||
@@ -371,11 +400,10 @@ export const createDemoUser = async (username = 'demo', pwd = custom()) => {
|
||||
};
|
||||
}
|
||||
};
|
||||
// initializeUser();
|
||||
|
||||
export class UserServices extends User {
|
||||
static async loginByPhone(phone: string) {
|
||||
let user = await User.findOne({ where: { username: phone } });
|
||||
let user = await User.findOne({ username: phone });
|
||||
let isNew = false;
|
||||
if (!user) {
|
||||
user = await User.createUser(phone, phone.slice(-6));
|
||||
|
||||
Reference in New Issue
Block a user