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,6 +1,5 @@
|
|||||||
// @ts-check
|
// @ts-check
|
||||||
import { resolvePath } from '@kevisual/use-config';
|
import { resolvePath } from '@kevisual/use-config';
|
||||||
import { execSync } from 'node:child_process';
|
|
||||||
|
|
||||||
const entry = 'src/index.ts';
|
const entry = 'src/index.ts';
|
||||||
const naming = 'app';
|
const naming = 'app';
|
||||||
@@ -22,16 +21,4 @@ await Bun.build({
|
|||||||
|
|
||||||
// const cmd = `dts -i src/index.ts -o app.d.ts`;
|
// const cmd = `dts -i src/index.ts -o app.d.ts`;
|
||||||
// const cmd = `dts -i ${entry} -o ${naming}.d.ts`;
|
// const cmd = `dts -i ${entry} -o ${naming}.d.ts`;
|
||||||
// execSync(cmd, { stdio: 'inherit' });
|
// execSync(cmd, { stdio: 'inherit' });
|
||||||
|
|
||||||
await Bun.build({
|
|
||||||
target: 'node',
|
|
||||||
format: 'esm',
|
|
||||||
entrypoints: [resolvePath('./src/run.ts', { meta: import.meta })],
|
|
||||||
outdir: resolvePath('./dist', { meta: import.meta }),
|
|
||||||
naming: {
|
|
||||||
entry: `${'run'}.js`,
|
|
||||||
},
|
|
||||||
external,
|
|
||||||
env: 'KEVISUAL_*',
|
|
||||||
});
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { App } from '@kevisual/router';
|
import { App } from '@kevisual/router';
|
||||||
import * as redisLib from './modules/redis.ts';
|
import * as redisLib from './modules/redis.ts';
|
||||||
import * as sequelizeLib from './modules/sequelize.ts';
|
|
||||||
import { useContextKey } from '@kevisual/context';
|
import { useContextKey } from '@kevisual/context';
|
||||||
import { SimpleRouter } from '@kevisual/router/simple';
|
import { SimpleRouter } from '@kevisual/router/simple';
|
||||||
import { s3Client, oss as s3Oss } from './modules/s3.ts';
|
import { s3Client, oss as s3Oss } from './modules/s3.ts';
|
||||||
@@ -22,7 +21,6 @@ export const oss = useContextKey(
|
|||||||
export { s3Client }
|
export { s3Client }
|
||||||
export const redis = useContextKey('redis', () => redisLib.redis);
|
export const redis = useContextKey('redis', () => redisLib.redis);
|
||||||
export const subscriber = useContextKey('subscriber', () => redisLib.subscriber);
|
export const subscriber = useContextKey('subscriber', () => redisLib.subscriber);
|
||||||
export const sequelize = useContextKey('sequelize', () => sequelizeLib.sequelize);
|
|
||||||
export { db };
|
export { db };
|
||||||
const init = () => {
|
const init = () => {
|
||||||
return new App({
|
return new App({
|
||||||
|
|||||||
@@ -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 { User, UserServices, UserModel, initializeUser, createDemoUser } from './user.ts';
|
||||||
export { UserSecretInit, UserSecret } from './user-secret.ts';
|
export { UserSecret, UserSecretModel } from './user-secret.ts';
|
||||||
export { OrgInit, Org } from './org.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 { 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 {
|
export enum OrgRole {
|
||||||
admin = 'admin',
|
admin = 'admin',
|
||||||
member = 'member',
|
member = 'member',
|
||||||
owner = 'owner',
|
owner = 'owner',
|
||||||
}
|
}
|
||||||
export class Org extends Model {
|
|
||||||
declare id: string;
|
export type OrgUser = {
|
||||||
declare username: string;
|
role: string;
|
||||||
declare description: string;
|
uid: string;
|
||||||
declare users: { 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
|
* operateId 是真实操作者的id
|
||||||
* @param user
|
* @param user
|
||||||
@@ -67,8 +82,8 @@ export class Org extends Model {
|
|||||||
} else {
|
} else {
|
||||||
users.push({ role: opts?.role || 'member', uid: user.id });
|
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
|
* operateId 是真实操作者的id
|
||||||
@@ -89,7 +104,8 @@ export class Org extends Model {
|
|||||||
}
|
}
|
||||||
await user.expireOrgs();
|
await user.expireOrgs();
|
||||||
const users = this.users.filter((u) => u.uid !== user.id || u.role === 'owner');
|
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
|
* operateId 是真实操作者的id
|
||||||
@@ -112,13 +128,7 @@ export class Org extends Model {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const _users = await User.findAll({
|
const _users = await db.select().from(usersTable).where(inArray(usersTable.id, usersIds));
|
||||||
where: {
|
|
||||||
id: {
|
|
||||||
[Op.in]: usersIds,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const users = _users.map((u) => {
|
const users = _users.map((u) => {
|
||||||
const role = orgUser.find((r) => r.uid === u.id)?.role;
|
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);
|
const user = this.users.find((u) => u.uid === userId && u.role === role);
|
||||||
return !!user;
|
return !!user;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
/**
|
/**
|
||||||
* 组织模型,在sequelize之后初始化
|
* 根据主键查找
|
||||||
*/
|
*/
|
||||||
export const OrgInit = async (newSequelize?: any, tableName?: string, sync?: SyncOpts) => {
|
static async findByPk(id: string): Promise<Org | null> {
|
||||||
const sequelize = useContextKey<Sequelize>('sequelize');
|
const orgs = await db.select().from(orgsTable).where(eq(orgsTable.id, id)).limit(1);
|
||||||
Org.init(
|
return orgs.length > 0 ? new Org(orgs[0]) : null;
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
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);
|
export const OrgModel = useContextKey('OrgModel', () => Org);
|
||||||
|
|||||||
@@ -1,10 +1,25 @@
|
|||||||
import { DataTypes, Model, Sequelize } from 'sequelize';
|
|
||||||
|
|
||||||
import { useContextKey } from '@kevisual/context';
|
import { useContextKey } from '@kevisual/context';
|
||||||
import { Redis } from 'ioredis';
|
import { Redis } from 'ioredis';
|
||||||
import { SyncOpts, User } from './user.ts';
|
import { User } from './user.ts';
|
||||||
import { oauth } from '../oauth/auth.ts';
|
import { oauth } from '../oauth/auth.ts';
|
||||||
import { OauthUser } from '../oauth/oauth.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');
|
export const redis = useContextKey<Redis>('redis');
|
||||||
|
|
||||||
const UserSecretStatus = ['active', 'inactive', 'expired'] as const;
|
const UserSecretStatus = ['active', 'inactive', 'expired'] as const;
|
||||||
@@ -16,33 +31,22 @@ const randomString = (length: number) => {
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
type Data = {
|
|
||||||
[key: string]: any;
|
|
||||||
/**
|
|
||||||
* 微信开放平台的某一个应用的openid
|
|
||||||
*/
|
|
||||||
wxOpenid?: string;
|
|
||||||
/**
|
|
||||||
* 微信开放平台的unionid:主要
|
|
||||||
*/
|
|
||||||
wxUnionid?: string;
|
|
||||||
/**
|
|
||||||
* 微信公众号的openid:次要
|
|
||||||
*/
|
|
||||||
wxmpOpenid?: string;
|
|
||||||
|
|
||||||
}
|
export class UserSecret {
|
||||||
export class UserSecret extends Model {
|
|
||||||
static oauth = oauth;
|
static oauth = oauth;
|
||||||
declare id: string;
|
id: string;
|
||||||
declare token: string;
|
token: string;
|
||||||
declare userId: string;
|
userId: string;
|
||||||
declare orgId: string;
|
orgId: string;
|
||||||
declare title: string;
|
title: string;
|
||||||
declare description: string;
|
description: string;
|
||||||
declare status: (typeof UserSecretStatus)[number];
|
status: (typeof UserSecretStatus)[number];
|
||||||
declare expiredTime: Date;
|
expiredTime: Date;
|
||||||
declare data: Data;
|
data: UserSecretData;
|
||||||
|
|
||||||
|
constructor(data: UserSecretSelect) {
|
||||||
|
Object.assign(this, data);
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* 验证token
|
* 验证token
|
||||||
* @param token
|
* @param token
|
||||||
@@ -57,12 +61,13 @@ export class UserSecret extends Model {
|
|||||||
return secretToken;
|
return secretToken;
|
||||||
}
|
}
|
||||||
console.log('verifyToken: try to verify as secret key');
|
console.log('verifyToken: try to verify as secret key');
|
||||||
const userSecret = await UserSecret.findOne({
|
const userSecrets = await db.select().from(userSecretsTable).where(eq(userSecretsTable.token, token)).limit(1);
|
||||||
where: { token },
|
|
||||||
});
|
if (userSecrets.length === 0) {
|
||||||
if (!userSecret) {
|
|
||||||
return null; // 如果没有找到对应的用户密钥,则返回null
|
return null; // 如果没有找到对应的用户密钥,则返回null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const userSecret = new UserSecret(userSecrets[0]);
|
||||||
if (userSecret.isExpired()) {
|
if (userSecret.isExpired()) {
|
||||||
return null; // 如果用户密钥已过期,则返回null
|
return null; // 如果用户密钥已过期,则返回null
|
||||||
}
|
}
|
||||||
@@ -78,19 +83,49 @@ export class UserSecret extends Model {
|
|||||||
// 存储到oauth中的token store中
|
// 存储到oauth中的token store中
|
||||||
return oauthUser;
|
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
|
* owner 组织用户的 oauthUser
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
async getOauthUser(opts?: { wx?: boolean }) {
|
async getOauthUser(opts?: { wx?: boolean }) {
|
||||||
const user = await User.findOne({
|
const users = await db.select({
|
||||||
where: { id: this.userId },
|
id: usersTable.id,
|
||||||
attributes: ['id', 'username', 'type', 'owner', 'data'],
|
username: usersTable.username,
|
||||||
});
|
type: usersTable.type,
|
||||||
let org: User = null;
|
owner: usersTable.owner,
|
||||||
if (!user) {
|
data: usersTable.data,
|
||||||
|
}).from(usersTable).where(eq(usersTable.id, this.userId)).limit(1);
|
||||||
|
|
||||||
|
let org: any = null;
|
||||||
|
if (users.length === 0) {
|
||||||
return null; // 如果没有找到对应的用户,则返回null
|
return null; // 如果没有找到对应的用户,则返回null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const user = users[0];
|
||||||
const expiredTime = this.expiredTime ? new Date(this.expiredTime).getTime() : null;
|
const expiredTime = this.expiredTime ? new Date(this.expiredTime).getTime() : null;
|
||||||
const oauthUser: Partial<OauthUser> = {
|
const oauthUser: Partial<OauthUser> = {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
@@ -101,11 +136,15 @@ export class UserSecret extends Model {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
if (this.orgId) {
|
if (this.orgId) {
|
||||||
org = await User.findOne({
|
const orgUsers = await db.select({
|
||||||
where: { id: this.orgId },
|
id: usersTable.id,
|
||||||
attributes: ['id', 'username', 'type', 'owner'],
|
username: usersTable.username,
|
||||||
});
|
type: usersTable.type,
|
||||||
if (org) {
|
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.id = org.id;
|
||||||
oauthUser.username = org.username;
|
oauthUser.username = org.username;
|
||||||
oauthUser.type = 'org';
|
oauthUser.type = 'org';
|
||||||
@@ -125,6 +164,7 @@ export class UserSecret extends Model {
|
|||||||
const expiredTime = new Date(this.expiredTime);
|
const expiredTime = new Date(this.expiredTime);
|
||||||
return now > expiredTime.getTime(); // 如果当前时间大于过期时间,则认为已过期
|
return now > expiredTime.getTime(); // 如果当前时间大于过期时间,则认为已过期
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查是否过期,如果过期则更新状态为expired
|
* 检查是否过期,如果过期则更新状态为expired
|
||||||
*
|
*
|
||||||
@@ -137,7 +177,6 @@ export class UserSecret extends Model {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const expiredTime = new Date(this.expiredTime);
|
const expiredTime = new Date(this.expiredTime);
|
||||||
const isExpired = now > expiredTime.getTime(); // 如果当前时间大于过期时间,则认为已过期
|
const isExpired = now > expiredTime.getTime(); // 如果当前时间大于过期时间,则认为已过期
|
||||||
@@ -145,11 +184,11 @@ export class UserSecret extends Model {
|
|||||||
this.status = 'active';
|
this.status = 'active';
|
||||||
const expireTime = UserSecret.getExpiredTime();
|
const expireTime = UserSecret.getExpiredTime();
|
||||||
this.expiredTime = expireTime;
|
this.expiredTime = expireTime;
|
||||||
await this.save()
|
await this.save();
|
||||||
}
|
}
|
||||||
if (this.status !== 'active') {
|
if (this.status !== 'active') {
|
||||||
this.status = 'active';
|
this.status = 'active';
|
||||||
await this.save()
|
await this.save();
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
code: 200
|
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() {
|
async createNewToken() {
|
||||||
if (this.token) {
|
if (this.token) {
|
||||||
await oauth.delToken(this.token);
|
await oauth.delToken(this.token);
|
||||||
@@ -172,14 +225,16 @@ export class UserSecret extends Model {
|
|||||||
await this.save();
|
await this.save();
|
||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async createToken() {
|
static async createToken() {
|
||||||
let token = oauth.generateSecretKey();
|
let token = oauth.generateSecretKey();
|
||||||
// 确保生成的token是唯一的
|
// 确保生成的token是唯一的
|
||||||
while (await UserSecret.findOne({ where: { token } })) {
|
while (await UserSecret.findOne({ token })) {
|
||||||
token = oauth.generateSecretKey();
|
token = oauth.generateSecretKey();
|
||||||
}
|
}
|
||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据 unionid 生成redis的key
|
* 根据 unionid 生成redis的key
|
||||||
* `wxmp:unionid:token:${unionid}`
|
* `wxmp:unionid:token:${unionid}`
|
||||||
@@ -189,28 +244,30 @@ export class UserSecret extends Model {
|
|||||||
static wxRedisKey(unionid: string) {
|
static wxRedisKey(unionid: string) {
|
||||||
return `wxmp:unionid:token:${unionid}`;
|
return `wxmp:unionid:token:${unionid}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
static getExpiredTime(expireDays?: number) {
|
static getExpiredTime(expireDays?: number) {
|
||||||
const defaultExpireDays = expireDays || 365;
|
const defaultExpireDays = expireDays || 365;
|
||||||
const expireTime = defaultExpireDays * 24 * 60 * 60 * 1000;
|
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) {
|
static async createSecret(tokenUser: { id: string; uid?: string, title?: string }, expireDays = 365) {
|
||||||
const token = await UserSecret.createToken();
|
const token = await UserSecret.createToken();
|
||||||
let userId = tokenUser.id;
|
let userId = tokenUser.id;
|
||||||
let orgId: string = null;
|
let orgId: string | null = null;
|
||||||
if (tokenUser.uid) {
|
if (tokenUser.uid) {
|
||||||
userId = 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,
|
userId,
|
||||||
orgId,
|
orgId,
|
||||||
token,
|
token,
|
||||||
title: tokenUser.title || randomString(6),
|
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 }) {
|
async getPermission(opts: { id: string; uid?: string }) {
|
||||||
@@ -242,8 +299,8 @@ export class UserSecret extends Model {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (this.orgId) {
|
if (this.orgId) {
|
||||||
const orgUser = await User.findByPk(this.orgId);
|
const orgUsers = await db.select().from(usersTable).where(eq(usersTable.id, this.orgId)).limit(1);
|
||||||
if (orgUser && orgUser.owner === userId) {
|
if (orgUsers.length > 0 && orgUsers[0].owner === userId) {
|
||||||
isAdmin = true;
|
isAdmin = true;
|
||||||
hasPermission = 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);
|
export const UserSecretModel = useContextKey('UserSecretModel', () => UserSecret);
|
||||||
|
|||||||
@@ -1,45 +1,62 @@
|
|||||||
import { DataTypes, Model, Op, Sequelize } from 'sequelize';
|
|
||||||
import { nanoid, customAlphabet } from 'nanoid';
|
import { nanoid, customAlphabet } from 'nanoid';
|
||||||
import { CustomError } from '@kevisual/router';
|
import { CustomError } from '@kevisual/router';
|
||||||
import { Org } from './org.ts';
|
|
||||||
|
|
||||||
import { useContextKey } from '@kevisual/context';
|
import { useContextKey } from '@kevisual/context';
|
||||||
import { Redis } from 'ioredis';
|
import { Redis } from 'ioredis';
|
||||||
import { oauth } from '../oauth/auth.ts';
|
import { oauth } from '../oauth/auth.ts';
|
||||||
import { cryptPwd } from '../oauth/salt.ts';
|
import { cryptPwd } from '../oauth/salt.ts';
|
||||||
import { OauthUser } from '../oauth/oauth.ts';
|
import { OauthUser } from '../oauth/oauth.ts';
|
||||||
export const redis = useContextKey<Redis>('redis');
|
import { db } from '../../modules/db.ts';
|
||||||
import { UserSecret } from './user-secret.ts';
|
import { cfUser, cfOrgs, cfUserSecrets } from '../../db/drizzle/schema.ts';
|
||||||
type UserData = {
|
import { eq, sql, InferSelectModel, InferInsertModel } from 'drizzle-orm';
|
||||||
|
|
||||||
|
// 类型定义
|
||||||
|
export type UserData = {
|
||||||
orgs?: string[];
|
orgs?: string[];
|
||||||
wxUnionId?: string;
|
wxUnionId?: string;
|
||||||
phone?: string;
|
phone?: string;
|
||||||
canChangeUsername?: boolean;
|
canChangeUsername?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum UserTypes {
|
export enum UserTypes {
|
||||||
'user' = 'user',
|
user = 'user',
|
||||||
'org' = 'org',
|
org = 'org',
|
||||||
'visitor' = 'visitor',
|
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;
|
static oauth = oauth;
|
||||||
declare id: string;
|
id: string;
|
||||||
declare username: string;
|
username: string;
|
||||||
declare nickname: string; // 昵称
|
nickname: string;
|
||||||
declare password: string;
|
password: string;
|
||||||
declare salt: string;
|
salt: string;
|
||||||
declare needChangePassword: boolean;
|
needChangePassword: boolean;
|
||||||
declare description: string;
|
description: string;
|
||||||
declare data: UserData;
|
data: UserData;
|
||||||
declare type: string; // user | org | visitor
|
type: string;
|
||||||
declare owner: string;
|
owner: string;
|
||||||
declare orgId: string;
|
orgId: string;
|
||||||
declare email: string;
|
email: string;
|
||||||
declare avatar: string;
|
avatar: string;
|
||||||
tokenUser: any;
|
tokenUser: any;
|
||||||
|
|
||||||
|
constructor(data: UserSelect) {
|
||||||
|
Object.assign(this, data);
|
||||||
|
}
|
||||||
|
|
||||||
setTokenUser(tokenUser: any) {
|
setTokenUser(tokenUser: any) {
|
||||||
this.tokenUser = tokenUser;
|
this.tokenUser = tokenUser;
|
||||||
}
|
}
|
||||||
@@ -76,6 +93,7 @@ export class User extends Model {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
static async verifyToken(token: string) {
|
static async verifyToken(token: string) {
|
||||||
|
const { UserSecret } = await import('./user-secret.ts');
|
||||||
return await UserSecret.verifyToken(token);
|
return await UserSecret.verifyToken(token);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
@@ -88,6 +106,7 @@ export class User extends Model {
|
|||||||
return { accessToken: token.accessToken, refreshToken: token.refreshToken, token: token.accessToken };
|
return { accessToken: token.accessToken, refreshToken: token.refreshToken, token: token.accessToken };
|
||||||
}
|
}
|
||||||
static async getOauthUser(token: string) {
|
static async getOauthUser(token: string) {
|
||||||
|
const { UserSecret } = await import('./user-secret.ts');
|
||||||
return await UserSecret.verifyToken(token);
|
return await UserSecret.verifyToken(token);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
@@ -105,12 +124,16 @@ export class User extends Model {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
static async getUserByToken(token: string) {
|
static async getUserByToken(token: string) {
|
||||||
|
const { UserSecret } = await import('./user-secret.ts');
|
||||||
const oauthUser = await UserSecret.verifyToken(token);
|
const oauthUser = await UserSecret.verifyToken(token);
|
||||||
if (!oauthUser) {
|
if (!oauthUser) {
|
||||||
throw new CustomError('Token is invalid. get UserByToken');
|
throw new CustomError('Token is invalid. get UserByToken');
|
||||||
}
|
}
|
||||||
const userId = oauthUser?.uid || oauthUser.id;
|
const userId = oauthUser?.uid || oauthUser.id;
|
||||||
const user = await User.findByPk(userId);
|
const user = await User.findByPk(userId);
|
||||||
|
if (!user) {
|
||||||
|
throw new CustomError('User not found');
|
||||||
|
}
|
||||||
user.setTokenUser(oauthUser);
|
user.setTokenUser(oauthUser);
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
@@ -130,8 +153,33 @@ export class User extends Model {
|
|||||||
}
|
}
|
||||||
return allUsers.includes(username);
|
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) {
|
static async createUser(username: string, password?: string, description?: string) {
|
||||||
const user = await User.findOne({ where: { username } });
|
const user = await User.findOne({ username });
|
||||||
if (user) {
|
if (user) {
|
||||||
throw new CustomError('User already exists');
|
throw new CustomError('User already exists');
|
||||||
}
|
}
|
||||||
@@ -139,10 +187,20 @@ export class User extends Model {
|
|||||||
let needChangePassword = !password;
|
let needChangePassword = !password;
|
||||||
password = password || '123456';
|
password = password || '123456';
|
||||||
const cPassword = cryptPwd(password, salt);
|
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) {
|
static async createOrg(username: string, owner: string, description?: string) {
|
||||||
const user = await User.findOne({ where: { username } });
|
const user = await User.findOne({ username });
|
||||||
if (user) {
|
if (user) {
|
||||||
throw new CustomError('User already exists');
|
throw new CustomError('User already exists');
|
||||||
}
|
}
|
||||||
@@ -153,24 +211,64 @@ export class User extends Model {
|
|||||||
if (me.type !== 'user') {
|
if (me.type !== 'user') {
|
||||||
throw new CustomError('Owner type is not 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 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
|
// owner add
|
||||||
await redis.del(`user:${me.id}:orgs`);
|
await redis.del(`user:${me.id}:orgs`);
|
||||||
return newUser;
|
return new User(inserted[0]);
|
||||||
}
|
}
|
||||||
async createPassword(password: string) {
|
async createPassword(password: string) {
|
||||||
const salt = this.salt;
|
const salt = this.salt;
|
||||||
const cPassword = cryptPwd(password, salt);
|
const cPassword = cryptPwd(password, salt);
|
||||||
this.password = cPassword;
|
this.password = cPassword;
|
||||||
await this.update({ password: cPassword });
|
await db.update(usersTable).set({ password: cPassword }).where(eq(usersTable.id, this.id));
|
||||||
return cPassword;
|
return cPassword;
|
||||||
}
|
}
|
||||||
|
|
||||||
checkPassword(password: string) {
|
checkPassword(password: string) {
|
||||||
const salt = this.salt;
|
const salt = this.salt;
|
||||||
const cPassword = cryptPwd(password, salt);
|
const cPassword = cryptPwd(password, salt);
|
||||||
return this.password === cPassword;
|
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
|
* 获取用户信息, 需要先设置 tokenUser 或者设置 uid
|
||||||
* @param uid 如果存在,则表示是组织,其中uid为真实用户
|
* @param uid 如果存在,则表示是组织,其中uid为真实用户
|
||||||
@@ -217,18 +315,14 @@ export class User extends Model {
|
|||||||
if (cache) {
|
if (cache) {
|
||||||
return JSON.parse(cache) as string[];
|
return JSON.parse(cache) as string[];
|
||||||
}
|
}
|
||||||
const orgs = await Org.findAll({
|
|
||||||
order: [['updatedAt', 'DESC']],
|
// 使用 Drizzle 的 SQL 查询来检查 JSONB 数组
|
||||||
where: {
|
const orgs = await db
|
||||||
users: {
|
.select()
|
||||||
[Op.contains]: [
|
.from(orgsTable)
|
||||||
{
|
.where(sql`${orgsTable.users} @> ${JSON.stringify([{ uid: id }])}::jsonb`)
|
||||||
uid: id,
|
.orderBy(sql`${orgsTable.updatedAt} DESC`);
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const orgNames = orgs.map((org) => org.username);
|
const orgNames = orgs.map((org) => org.username);
|
||||||
if (orgNames.length > 0) {
|
if (orgNames.length > 0) {
|
||||||
await redis.set(`user:${id}:orgs`, JSON.stringify(orgNames), 'EX', 60 * 60); // 1 hour
|
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;
|
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,
|
*/
|
||||||
},
|
static async findAll(options: { where?: any; attributes?: string[] }) {
|
||||||
email: {
|
let query = db.select().from(usersTable);
|
||||||
type: DataTypes.STRING,
|
|
||||||
allowNull: true,
|
if (options.where?.id?.in) {
|
||||||
},
|
query = query.where(sql`${usersTable.id} = ANY(${options.where.id.in})`) as any;
|
||||||
avatar: {
|
}
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true,
|
const users = await query;
|
||||||
},
|
return users.map(u => new User(u));
|
||||||
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 letter = 'abcdefghijklmnopqrstuvwxyz';
|
||||||
const custom = customAlphabet(letter, 6);
|
const custom = customAlphabet(letter, 6);
|
||||||
|
|
||||||
export const initializeUser = async (pwd = custom()) => {
|
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) {
|
if (!w) {
|
||||||
const root = await User.createUser('root', pwd, '系统管理员');
|
const root = await User.createUser('root', pwd, '系统管理员');
|
||||||
const org = await User.createOrg('admin', root.id, '管理员');
|
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()) => {
|
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) {
|
if (!u) {
|
||||||
const user = await User.createUser(username, pwd, 'demo');
|
const user = await User.createUser(username, pwd, 'demo');
|
||||||
console.info('new Users name', user.username, pwd);
|
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 {
|
export class UserServices extends User {
|
||||||
static async loginByPhone(phone: string) {
|
static async loginByPhone(phone: string) {
|
||||||
let user = await User.findOne({ where: { username: phone } });
|
let user = await User.findOne({ username: phone });
|
||||||
let isNew = false;
|
let isNew = false;
|
||||||
if (!user) {
|
if (!user) {
|
||||||
user = await User.createUser(phone, phone.slice(-6));
|
user = await User.createUser(phone, phone.slice(-6));
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export const appsTrades = pgTable("apps_trades", {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
export const cfOrgs = pgTable("cf_orgs", {
|
export const cfOrgs = pgTable("cf_orgs", {
|
||||||
id: uuid().primaryKey().notNull(),
|
id: uuid().primaryKey().notNull().defaultRandom(),
|
||||||
username: varchar({ length: 255 }).notNull(),
|
username: varchar({ length: 255 }).notNull(),
|
||||||
users: jsonb().default([]),
|
users: jsonb().default([]),
|
||||||
createdAt: timestamp({ withTimezone: true, mode: 'string' }).notNull().defaultNow(),
|
createdAt: timestamp({ withTimezone: true, mode: 'string' }).notNull().defaultNow(),
|
||||||
@@ -81,7 +81,7 @@ export const cfRouterCode = pgTable("cf_router_code", {
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const cfUser = pgTable("cf_user", {
|
export const cfUser = pgTable("cf_user", {
|
||||||
id: uuid().primaryKey().notNull(),
|
id: uuid().primaryKey().notNull().defaultRandom(),
|
||||||
username: varchar({ length: 255 }).notNull(),
|
username: varchar({ length: 255 }).notNull(),
|
||||||
password: varchar({ length: 255 }),
|
password: varchar({ length: 255 }),
|
||||||
salt: varchar({ length: 255 }),
|
salt: varchar({ length: 255 }),
|
||||||
@@ -102,7 +102,7 @@ export const cfUser = pgTable("cf_user", {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
export const cfUserSecrets = pgTable("cf_user_secrets", {
|
export const cfUserSecrets = pgTable("cf_user_secrets", {
|
||||||
id: uuid().primaryKey().notNull(),
|
id: uuid().primaryKey().notNull().defaultRandom(),
|
||||||
description: text(),
|
description: text(),
|
||||||
status: varchar({ length: 255 }).default('active'),
|
status: varchar({ length: 255 }).default('active'),
|
||||||
title: text(),
|
title: text(),
|
||||||
|
|||||||
@@ -1,18 +1,3 @@
|
|||||||
import { User, UserInit, UserServices } from '../auth/models/index.ts';
|
import { User, UserServices } from '../auth/models/index.ts';
|
||||||
import { UserSecretInit, UserSecret } from '../auth/models/index.ts';
|
import { UserSecret } from '../auth/models/index.ts';
|
||||||
import { OrgInit } from '../auth/models/index.ts';
|
export { User, UserServices, UserSecret };
|
||||||
export { User, UserInit, UserServices, UserSecret };
|
|
||||||
import { useContextKey } from '@kevisual/context';
|
|
||||||
const init = async () => {
|
|
||||||
await OrgInit(null, null).catch((e) => {
|
|
||||||
console.error('Org sync', e);
|
|
||||||
});
|
|
||||||
await UserInit(null, null).catch((e) => {
|
|
||||||
console.error('User sync', e);
|
|
||||||
});
|
|
||||||
await UserSecretInit(null, null).catch((e) => {
|
|
||||||
console.error('UserSecret sync', e);
|
|
||||||
});
|
|
||||||
useContextKey('models-synced', true);
|
|
||||||
};
|
|
||||||
init();
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { sequelize } from './sequelize.ts';
|
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
import { Sequelize } from 'sequelize';
|
|
||||||
import { config } from './config.ts';
|
|
||||||
import { log } from './logger.ts';
|
|
||||||
export type PostgresConfig = {
|
|
||||||
postgres: {
|
|
||||||
username: string;
|
|
||||||
password: string;
|
|
||||||
host: string;
|
|
||||||
port: number;
|
|
||||||
database: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
if (!config.POSTGRES_PASSWORD || !config.POSTGRES_USER) {
|
|
||||||
log.error('postgres config is required password and user');
|
|
||||||
log.error('config', config);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
const postgresConfig = {
|
|
||||||
username: config.POSTGRES_USER,
|
|
||||||
password: config.POSTGRES_PASSWORD,
|
|
||||||
host: config.POSTGRES_HOST || 'localhost',
|
|
||||||
port: parseInt(config.POSTGRES_PORT || '5432'),
|
|
||||||
database: config.POSTGRES_DB || 'postgres',
|
|
||||||
};
|
|
||||||
// connect to db
|
|
||||||
export const sequelize = new Sequelize({
|
|
||||||
dialect: 'postgres',
|
|
||||||
...postgresConfig,
|
|
||||||
// logging: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
sequelize
|
|
||||||
.authenticate({ logging: false })
|
|
||||||
.then(() => {
|
|
||||||
log.info('Database connected');
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
log.error('Database connection failed', { err, config: postgresConfig });
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
import { AppModel } from '../module/index.ts';
|
import { db, schema } from '@/app.ts';
|
||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
|
||||||
export const mvAppFromUserAToUserB = async (userA: string, userB: string) => {
|
export const mvAppFromUserAToUserB = async (userA: string, userB: string) => {
|
||||||
const appList = await AppModel.findAll({
|
const appList = await db.select().from(schema.kvApp).where(eq(schema.kvApp.user, userA));
|
||||||
where: {
|
|
||||||
user: userA,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
for (const app of appList) {
|
for (const app of appList) {
|
||||||
app.user = userB;
|
await db.update(schema.kvApp)
|
||||||
await app.save();
|
.set({ user: userB, updatedAt: new Date().toISOString() })
|
||||||
|
.where(eq(schema.kvApp.id, app.id));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { app } from '@/app.ts';
|
import { app, db, schema } from '@/app.ts';
|
||||||
import { AppModel } from '../module/app.ts';
|
import { App, AppData } from '../module/app-drizzle.ts';
|
||||||
import { AppDomainModel } from '../module/app-domain.ts';
|
import { AppDomain, AppDomainHelper } from '../module/app-domain-drizzle.ts';
|
||||||
|
import { eq, and } from 'drizzle-orm';
|
||||||
|
import { randomUUID } from 'crypto';
|
||||||
|
|
||||||
app
|
app
|
||||||
.route({
|
.route({
|
||||||
@@ -9,17 +11,17 @@ app
|
|||||||
})
|
})
|
||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
const { domain } = ctx.query.data;
|
const { domain } = ctx.query.data;
|
||||||
// const query = {
|
const domainInfos = await db.select().from(schema.kvAppDomain).where(eq(schema.kvAppDomain.domain, domain)).limit(1);
|
||||||
// }
|
const domainInfo = domainInfos[0];
|
||||||
const domainInfo = await AppDomainModel.findOne({ where: { domain } });
|
|
||||||
if (!domainInfo || !domainInfo.appId) {
|
if (!domainInfo || !domainInfo.appId) {
|
||||||
ctx.throw(404, 'app not found');
|
ctx.throw(404, 'app not found');
|
||||||
}
|
}
|
||||||
const app = await AppModel.findByPk(domainInfo.appId);
|
const apps = await db.select().from(schema.kvApp).where(eq(schema.kvApp.id, domainInfo.appId)).limit(1);
|
||||||
if (!app) {
|
const appFound = apps[0];
|
||||||
|
if (!appFound) {
|
||||||
ctx.throw(404, 'app not found');
|
ctx.throw(404, 'app not found');
|
||||||
}
|
}
|
||||||
ctx.body = app;
|
ctx.body = appFound;
|
||||||
return ctx;
|
return ctx;
|
||||||
})
|
})
|
||||||
.addTo(app);
|
.addTo(app);
|
||||||
@@ -37,7 +39,8 @@ app
|
|||||||
if (!domain || !appId) {
|
if (!domain || !appId) {
|
||||||
ctx.throw(400, 'domain and appId are required');
|
ctx.throw(400, 'domain and appId are required');
|
||||||
}
|
}
|
||||||
const domainInfo = await AppDomainModel.create({ domain, appId, uid });
|
const newDomains = await db.insert(schema.kvAppDomain).values({ id: randomUUID(), domain, appId, uid }).returning();
|
||||||
|
const domainInfo = newDomains[0];
|
||||||
ctx.body = domainInfo;
|
ctx.body = domainInfo;
|
||||||
return ctx;
|
return ctx;
|
||||||
})
|
})
|
||||||
@@ -59,12 +62,17 @@ app
|
|||||||
if (!status) {
|
if (!status) {
|
||||||
ctx.throw(400, 'status is required');
|
ctx.throw(400, 'status is required');
|
||||||
}
|
}
|
||||||
let domainInfo: AppDomainModel | null = null;
|
let domainInfo: AppDomain | undefined;
|
||||||
if (id) {
|
if (id) {
|
||||||
domainInfo = await AppDomainModel.findByPk(id);
|
const domains = await db.select().from(schema.kvAppDomain).where(eq(schema.kvAppDomain.id, id)).limit(1);
|
||||||
|
domainInfo = domains[0];
|
||||||
}
|
}
|
||||||
if (!domainInfo && domain) {
|
if (!domainInfo && domain) {
|
||||||
domainInfo = await AppDomainModel.findOne({ where: { domain, appId } });
|
const domains = await db.select().from(schema.kvAppDomain).where(and(
|
||||||
|
eq(schema.kvAppDomain.domain, domain),
|
||||||
|
eq(schema.kvAppDomain.appId, appId)
|
||||||
|
)).limit(1);
|
||||||
|
domainInfo = domains[0];
|
||||||
}
|
}
|
||||||
if (!domainInfo) {
|
if (!domainInfo) {
|
||||||
ctx.throw(404, 'domain not found');
|
ctx.throw(404, 'domain not found');
|
||||||
@@ -72,19 +80,23 @@ app
|
|||||||
if (domainInfo.uid !== uid) {
|
if (domainInfo.uid !== uid) {
|
||||||
ctx.throw(403, 'domain must be owned by the user');
|
ctx.throw(403, 'domain must be owned by the user');
|
||||||
}
|
}
|
||||||
if (!domainInfo.checkCanUpdateStatus(status)) {
|
if (!AppDomainHelper.checkCanUpdateStatus(domainInfo.status!, status)) {
|
||||||
ctx.throw(400, 'domain status can not be updated');
|
ctx.throw(400, 'domain status can not be updated');
|
||||||
}
|
}
|
||||||
|
const updateData: any = {};
|
||||||
if (status) {
|
if (status) {
|
||||||
domainInfo.status = status;
|
updateData.status = status;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (appId) {
|
if (appId) {
|
||||||
domainInfo.appId = appId;
|
updateData.appId = appId;
|
||||||
}
|
}
|
||||||
await domainInfo.save({ fields: ['status', 'appId'] });
|
updateData.updatedAt = new Date().toISOString();
|
||||||
|
const updateResult = await db.update(schema.kvAppDomain)
|
||||||
ctx.body = domainInfo;
|
.set(updateData)
|
||||||
|
.where(eq(schema.kvAppDomain.id, domainInfo.id))
|
||||||
|
.returning();
|
||||||
|
const updatedDomain = updateResult[0];
|
||||||
|
ctx.body = updatedDomain;
|
||||||
return ctx;
|
return ctx;
|
||||||
})
|
})
|
||||||
.addTo(app);
|
.addTo(app);
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { app } from '@/app.ts';
|
import { app, db, schema } from '@/app.ts';
|
||||||
import { AppDomainModel } from '../module/app-domain.ts';
|
import { AppDomain, AppDomainHelper } from '../module/app-domain-drizzle.ts';
|
||||||
import { AppModel } from '../module/app.ts';
|
import { App } from '../module/app-drizzle.ts';
|
||||||
import { CustomError } from '@kevisual/router';
|
import { CustomError } from '@kevisual/router';
|
||||||
|
import { eq, or } from 'drizzle-orm';
|
||||||
|
import { randomUUID } from 'crypto';
|
||||||
|
|
||||||
app
|
app
|
||||||
.route({
|
.route({
|
||||||
@@ -11,10 +13,12 @@ app
|
|||||||
})
|
})
|
||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
const { page = 1, pageSize = 999 } = ctx.query.data || {};
|
const { page = 1, pageSize = 999 } = ctx.query.data || {};
|
||||||
const { count, rows } = await AppDomainModel.findAndCountAll({
|
const offset = (page - 1) * pageSize;
|
||||||
offset: (page - 1) * pageSize,
|
const rows = await db.select().from(schema.kvAppDomain)
|
||||||
limit: pageSize,
|
.limit(pageSize)
|
||||||
});
|
.offset(offset);
|
||||||
|
const countResult = await db.select().from(schema.kvAppDomain);
|
||||||
|
const count = countResult.length;
|
||||||
ctx.body = { count, list: rows, pagination: { page, pageSize } };
|
ctx.body = { count, list: rows, pagination: { page, pageSize } };
|
||||||
return ctx;
|
return ctx;
|
||||||
})
|
})
|
||||||
@@ -31,11 +35,10 @@ app
|
|||||||
if (!domain) {
|
if (!domain) {
|
||||||
ctx.throw(400, 'domain is required');
|
ctx.throw(400, 'domain is required');
|
||||||
}
|
}
|
||||||
let domainInfo: AppDomainModel;
|
let domainInfo: AppDomain | undefined;
|
||||||
if (id) {
|
if (id) {
|
||||||
domainInfo = await AppDomainModel.findByPk(id);
|
const domains = await db.select().from(schema.kvAppDomain).where(eq(schema.kvAppDomain.id, id)).limit(1);
|
||||||
} else {
|
domainInfo = domains[0];
|
||||||
domainInfo = await AppDomainModel.create({ domain });
|
|
||||||
}
|
}
|
||||||
const checkAppId = async () => {
|
const checkAppId = async () => {
|
||||||
const isUUID = (id: string) => {
|
const isUUID = (id: string) => {
|
||||||
@@ -45,7 +48,8 @@ app
|
|||||||
if (!isUUID(rest.appId)) {
|
if (!isUUID(rest.appId)) {
|
||||||
ctx.throw(400, 'appId is not valid');
|
ctx.throw(400, 'appId is not valid');
|
||||||
}
|
}
|
||||||
const appInfo = await AppModel.findByPk(rest.appId);
|
const apps = await db.select().from(schema.kvApp).where(eq(schema.kvApp.id, rest.appId)).limit(1);
|
||||||
|
const appInfo = apps[0];
|
||||||
if (!appInfo) {
|
if (!appInfo) {
|
||||||
ctx.throw(400, 'appId is not exist');
|
ctx.throw(400, 'appId is not exist');
|
||||||
}
|
}
|
||||||
@@ -53,24 +57,31 @@ app
|
|||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
if (!domainInfo) {
|
if (!domainInfo) {
|
||||||
domainInfo = await AppDomainModel.create({ domain, data: {}, ...rest });
|
|
||||||
await checkAppId();
|
await checkAppId();
|
||||||
|
const newDomains = await db.insert(schema.kvAppDomain).values({ id: randomUUID(), domain, data: {}, ...rest }).returning();
|
||||||
|
domainInfo = newDomains[0];
|
||||||
} else {
|
} else {
|
||||||
if (rest.status && domainInfo.status !== rest.status) {
|
if (rest.status && domainInfo.status !== rest.status) {
|
||||||
await domainInfo.clearCache();
|
await AppDomainHelper.clearCache(domainInfo.domain!);
|
||||||
}
|
}
|
||||||
await checkAppId();
|
await checkAppId();
|
||||||
await domainInfo.update({
|
const domainData = domainInfo.data as any;
|
||||||
domain,
|
const updateResult = await db.update(schema.kvAppDomain)
|
||||||
data: {
|
.set({
|
||||||
...domainInfo.data,
|
domain,
|
||||||
...data,
|
data: {
|
||||||
},
|
...domainData,
|
||||||
...rest,
|
...data,
|
||||||
});
|
},
|
||||||
|
...rest,
|
||||||
|
updatedAt: new Date().toISOString()
|
||||||
|
})
|
||||||
|
.where(eq(schema.kvAppDomain.id, domainInfo.id))
|
||||||
|
.returning();
|
||||||
|
domainInfo = updateResult[0];
|
||||||
}
|
}
|
||||||
ctx.body = domainInfo;
|
ctx.body = domainInfo;
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
if (error.code) {
|
if (error.code) {
|
||||||
ctx.throw(error.code, error.message);
|
ctx.throw(error.code, error.message);
|
||||||
}
|
}
|
||||||
@@ -94,9 +105,9 @@ app
|
|||||||
ctx.throw(400, 'id or domain is required');
|
ctx.throw(400, 'id or domain is required');
|
||||||
}
|
}
|
||||||
if (id) {
|
if (id) {
|
||||||
await AppDomainModel.destroy({ where: { id }, force: true });
|
await db.delete(schema.kvAppDomain).where(eq(schema.kvAppDomain.id, id));
|
||||||
} else {
|
} else {
|
||||||
await AppDomainModel.destroy({ where: { domain }, force: true });
|
await db.delete(schema.kvAppDomain).where(eq(schema.kvAppDomain.domain, domain));
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.body = { message: 'delete domain success' };
|
ctx.body = { message: 'delete domain success' };
|
||||||
@@ -115,7 +126,8 @@ app
|
|||||||
if (!id && !domain) {
|
if (!id && !domain) {
|
||||||
ctx.throw(400, 'id or domain is required');
|
ctx.throw(400, 'id or domain is required');
|
||||||
}
|
}
|
||||||
const domainInfo = await AppDomainModel.findOne({ where: { id } });
|
const domains = await db.select().from(schema.kvAppDomain).where(eq(schema.kvAppDomain.id, id)).limit(1);
|
||||||
|
const domainInfo = domains[0];
|
||||||
if (!domainInfo) {
|
if (!domainInfo) {
|
||||||
ctx.throw(404, 'domain not found');
|
ctx.throw(404, 'domain not found');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import { App, CustomError } from '@kevisual/router';
|
import { App, CustomError } from '@kevisual/router';
|
||||||
import { AppModel, AppListModel } from './module/index.ts';
|
import { App as AppType, AppList, AppData, AppHelper } from './module/app-drizzle.ts';
|
||||||
import { app, redis } from '@/app.ts';
|
import { app, redis, db, schema } from '@/app.ts';
|
||||||
import { uniqBy } from 'es-toolkit';
|
import { uniqBy } from 'es-toolkit';
|
||||||
import { getUidByUsername, prefixFix } from './util.ts';
|
import { getUidByUsername, prefixFix } from './util.ts';
|
||||||
import { deleteFiles, getMinioListAndSetToAppList } from '../file/index.ts';
|
import { deleteFiles, getMinioListAndSetToAppList } from '../file/index.ts';
|
||||||
import { setExpire } from './revoke.ts';
|
import { setExpire } from './revoke.ts';
|
||||||
import { User } from '@/models/user.ts';
|
import { User } from '@/models/user.ts';
|
||||||
import { callDetectAppVersion } from './export.ts';
|
import { callDetectAppVersion } from './export.ts';
|
||||||
|
import { eq, and, desc } from 'drizzle-orm';
|
||||||
|
import { randomUUID } from 'crypto';
|
||||||
app
|
app
|
||||||
.route({
|
.route({
|
||||||
path: 'app',
|
path: 'app',
|
||||||
@@ -20,14 +22,13 @@ app
|
|||||||
if (!data.key) {
|
if (!data.key) {
|
||||||
throw new CustomError('key is required');
|
throw new CustomError('key is required');
|
||||||
}
|
}
|
||||||
const list = await AppListModel.findAll({
|
const list = await db.select()
|
||||||
order: [['updatedAt', 'DESC']],
|
.from(schema.kvAppList)
|
||||||
where: {
|
.where(and(
|
||||||
uid: tokenUser.id,
|
eq(schema.kvAppList.uid, tokenUser.id),
|
||||||
key: data.key,
|
eq(schema.kvAppList.key, data.key)
|
||||||
},
|
))
|
||||||
logging: false,
|
.orderBy(desc(schema.kvAppList.updatedAt));
|
||||||
});
|
|
||||||
ctx.body = list.map((item) => prefixFix(item, tokenUser.username));
|
ctx.body = list.map((item) => prefixFix(item, tokenUser.username));
|
||||||
return ctx;
|
return ctx;
|
||||||
})
|
})
|
||||||
@@ -48,33 +49,35 @@ app
|
|||||||
if (!id && (!key || !version)) {
|
if (!id && (!key || !version)) {
|
||||||
throw new CustomError('id is required');
|
throw new CustomError('id is required');
|
||||||
}
|
}
|
||||||
let appListModel: AppListModel;
|
let appListModel: AppList | undefined;
|
||||||
if (id) {
|
if (id) {
|
||||||
appListModel = await AppListModel.findByPk(id);
|
const apps = await db.select().from(schema.kvAppList).where(eq(schema.kvAppList.id, id)).limit(1);
|
||||||
|
appListModel = apps[0];
|
||||||
} else if (key && version) {
|
} else if (key && version) {
|
||||||
appListModel = await AppListModel.findOne({
|
const apps = await db.select().from(schema.kvAppList).where(and(
|
||||||
where: {
|
eq(schema.kvAppList.key, key),
|
||||||
key,
|
eq(schema.kvAppList.version, version),
|
||||||
version,
|
eq(schema.kvAppList.uid, tokenUser.id)
|
||||||
uid: tokenUser.id,
|
)).limit(1);
|
||||||
},
|
appListModel = apps[0];
|
||||||
});
|
|
||||||
}
|
}
|
||||||
if (!appListModel && create) {
|
if (!appListModel && create) {
|
||||||
appListModel = await AppListModel.create({
|
const newApps = await db.insert(schema.kvAppList).values({
|
||||||
|
id: randomUUID(),
|
||||||
key,
|
key,
|
||||||
version,
|
version,
|
||||||
uid: tokenUser.id,
|
uid: tokenUser.id,
|
||||||
data: {},
|
data: {},
|
||||||
});
|
}).returning();
|
||||||
const appModel = await AppModel.findOne({
|
appListModel = newApps[0];
|
||||||
where: {
|
const appModels = await db.select().from(schema.kvApp).where(and(
|
||||||
key,
|
eq(schema.kvApp.key, key),
|
||||||
uid: tokenUser.id,
|
eq(schema.kvApp.uid, tokenUser.id)
|
||||||
},
|
)).limit(1);
|
||||||
});
|
const appModel = appModels[0];
|
||||||
if (!appModel) {
|
if (!appModel) {
|
||||||
await AppModel.create({
|
await db.insert(schema.kvApp).values({
|
||||||
|
id: randomUUID(),
|
||||||
key,
|
key,
|
||||||
uid: tokenUser.id,
|
uid: tokenUser.id,
|
||||||
user: tokenUser.username,
|
user: tokenUser.username,
|
||||||
@@ -88,13 +91,12 @@ app
|
|||||||
if (res.code !== 200) {
|
if (res.code !== 200) {
|
||||||
ctx.throw(res.message || 'detect version list error');
|
ctx.throw(res.message || 'detect version list error');
|
||||||
}
|
}
|
||||||
appListModel = await AppListModel.findOne({
|
const apps2 = await db.select().from(schema.kvAppList).where(and(
|
||||||
where: {
|
eq(schema.kvAppList.key, key),
|
||||||
key,
|
eq(schema.kvAppList.version, version),
|
||||||
version,
|
eq(schema.kvAppList.uid, tokenUser.id)
|
||||||
uid: tokenUser.id,
|
)).limit(1);
|
||||||
},
|
appListModel = apps2[0];
|
||||||
});
|
|
||||||
}
|
}
|
||||||
if (!appListModel) {
|
if (!appListModel) {
|
||||||
ctx.throw('app not found');
|
ctx.throw('app not found');
|
||||||
@@ -115,10 +117,16 @@ app
|
|||||||
const tokenUser = ctx.state.tokenUser;
|
const tokenUser = ctx.state.tokenUser;
|
||||||
const { data, id, ...rest } = ctx.query.data;
|
const { data, id, ...rest } = ctx.query.data;
|
||||||
if (id) {
|
if (id) {
|
||||||
const app = await AppListModel.findByPk(id);
|
const apps = await db.select().from(schema.kvAppList).where(eq(schema.kvAppList.id, id)).limit(1);
|
||||||
|
const app = apps[0];
|
||||||
if (app) {
|
if (app) {
|
||||||
const newData = { ...app.data, ...data };
|
const appData = app.data as AppData;
|
||||||
const newApp = await app.update({ data: newData, ...rest });
|
const newData = { ...appData, ...data };
|
||||||
|
const updateResult = await db.update(schema.kvAppList)
|
||||||
|
.set({ data: newData, ...rest, updatedAt: new Date().toISOString() })
|
||||||
|
.where(eq(schema.kvAppList.id, id))
|
||||||
|
.returning();
|
||||||
|
const newApp = updateResult[0];
|
||||||
ctx.body = newApp;
|
ctx.body = newApp;
|
||||||
setExpire(newApp.id, 'test');
|
setExpire(newApp.id, 'test');
|
||||||
} else {
|
} else {
|
||||||
@@ -130,8 +138,8 @@ app
|
|||||||
if (!rest.key) {
|
if (!rest.key) {
|
||||||
throw new CustomError('key is required');
|
throw new CustomError('key is required');
|
||||||
}
|
}
|
||||||
const app = await AppListModel.create({ data, ...rest, uid: tokenUser.id });
|
const newApps = await db.insert(schema.kvAppList).values({ id: randomUUID(), data, ...rest, uid: tokenUser.id }).returning();
|
||||||
ctx.body = app;
|
ctx.body = newApps[0];
|
||||||
return ctx;
|
return ctx;
|
||||||
})
|
})
|
||||||
.addTo(app);
|
.addTo(app);
|
||||||
@@ -149,24 +157,28 @@ app
|
|||||||
if (!id) {
|
if (!id) {
|
||||||
throw new CustomError('id is required');
|
throw new CustomError('id is required');
|
||||||
}
|
}
|
||||||
const app = await AppListModel.findByPk(id);
|
const apps = await db.select().from(schema.kvAppList).where(eq(schema.kvAppList.id, id)).limit(1);
|
||||||
|
const app = apps[0];
|
||||||
if (!app) {
|
if (!app) {
|
||||||
throw new CustomError('app not found');
|
throw new CustomError('app not found');
|
||||||
}
|
}
|
||||||
const am = await AppModel.findOne({ where: { key: app.key, uid: app.uid } });
|
const ams = await db.select().from(schema.kvApp).where(and(
|
||||||
|
eq(schema.kvApp.key, app.key),
|
||||||
|
eq(schema.kvApp.uid, app.uid)
|
||||||
|
)).limit(1);
|
||||||
|
const am = ams[0];
|
||||||
if (!am) {
|
if (!am) {
|
||||||
throw new CustomError('app not found');
|
throw new CustomError('app not found');
|
||||||
}
|
}
|
||||||
if (am.version === app.version) {
|
if (am.version === app.version) {
|
||||||
throw new CustomError('app is published');
|
throw new CustomError('app is published');
|
||||||
}
|
}
|
||||||
const files = app.data.files || [];
|
const appData = app.data as AppData;
|
||||||
|
const files = appData.files || [];
|
||||||
if (deleteFile && files.length > 0) {
|
if (deleteFile && files.length > 0) {
|
||||||
await deleteFiles(files.map((item) => item.path));
|
await deleteFiles(files.map((item) => item.path));
|
||||||
}
|
}
|
||||||
await app.destroy({
|
await db.delete(schema.kvAppList).where(eq(schema.kvAppList.id, id));
|
||||||
force: true,
|
|
||||||
});
|
|
||||||
ctx.body = 'success';
|
ctx.body = 'success';
|
||||||
return ctx;
|
return ctx;
|
||||||
})
|
})
|
||||||
@@ -205,11 +217,16 @@ app
|
|||||||
throw new CustomError('user not found');
|
throw new CustomError('user not found');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let am = await AppModel.findOne({ where: { key: appKey, uid } });
|
const ams = await db.select().from(schema.kvApp).where(and(
|
||||||
|
eq(schema.kvApp.key, appKey),
|
||||||
|
eq(schema.kvApp.uid, uid)
|
||||||
|
)).limit(1);
|
||||||
|
let am = ams[0];
|
||||||
let appIsNew = false;
|
let appIsNew = false;
|
||||||
if (!am) {
|
if (!am) {
|
||||||
appIsNew = true;
|
appIsNew = true;
|
||||||
am = await AppModel.create({
|
const newAms = await db.insert(schema.kvApp).values({
|
||||||
|
id: randomUUID(),
|
||||||
user: userPrefix,
|
user: userPrefix,
|
||||||
key: appKey,
|
key: appKey,
|
||||||
uid,
|
uid,
|
||||||
@@ -220,24 +237,40 @@ app
|
|||||||
data: {
|
data: {
|
||||||
files: files || [],
|
files: files || [],
|
||||||
},
|
},
|
||||||
});
|
}).returning();
|
||||||
|
am = newAms[0];
|
||||||
}
|
}
|
||||||
let app = await AppListModel.findOne({ where: { version: version, key: appKey, uid: uid } });
|
const apps = await db.select().from(schema.kvAppList).where(and(
|
||||||
|
eq(schema.kvAppList.version, version),
|
||||||
|
eq(schema.kvAppList.key, appKey),
|
||||||
|
eq(schema.kvAppList.uid, uid)
|
||||||
|
)).limit(1);
|
||||||
|
let app = apps[0];
|
||||||
if (!app) {
|
if (!app) {
|
||||||
app = await AppListModel.create({
|
const newApps = await db.insert(schema.kvAppList).values({
|
||||||
|
id: randomUUID(),
|
||||||
key: appKey,
|
key: appKey,
|
||||||
version,
|
version,
|
||||||
uid: uid,
|
uid: uid,
|
||||||
data: {
|
data: {
|
||||||
files: [],
|
files: [],
|
||||||
},
|
},
|
||||||
});
|
}).returning();
|
||||||
|
app = newApps[0];
|
||||||
}
|
}
|
||||||
const dataFiles = app.data.files || [];
|
const appData = app.data as AppData;
|
||||||
|
const dataFiles = appData.files || [];
|
||||||
const newFiles = uniqBy([...dataFiles, ...files], (item) => item.name);
|
const newFiles = uniqBy([...dataFiles, ...files], (item) => item.name);
|
||||||
const res = await app.update({ data: { ...app.data, files: newFiles } });
|
const updateResult = await db.update(schema.kvAppList)
|
||||||
|
.set({ data: { ...appData, files: newFiles }, updatedAt: new Date().toISOString() })
|
||||||
|
.where(eq(schema.kvAppList.id, app.id))
|
||||||
|
.returning();
|
||||||
|
const res = updateResult[0];
|
||||||
if (version === am.version && !appIsNew) {
|
if (version === am.version && !appIsNew) {
|
||||||
await am.update({ data: { ...am.data, files: newFiles } });
|
const amData = am.data as AppData;
|
||||||
|
await db.update(schema.kvApp)
|
||||||
|
.set({ data: { ...amData, files: newFiles }, updatedAt: new Date().toISOString() })
|
||||||
|
.where(eq(schema.kvApp.id, am.id));
|
||||||
}
|
}
|
||||||
setExpire(app.id, 'test');
|
setExpire(app.id, 'test');
|
||||||
ctx.body = prefixFix(res, userPrefix);
|
ctx.body = prefixFix(res, userPrefix);
|
||||||
@@ -263,9 +296,10 @@ app
|
|||||||
}
|
}
|
||||||
|
|
||||||
const uid = await getUidByUsername(app, ctx, username);
|
const uid = await getUidByUsername(app, ctx, username);
|
||||||
let appList: AppListModel | null = null;
|
let appList: AppList | undefined = undefined;
|
||||||
if (id) {
|
if (id) {
|
||||||
appList = await AppListModel.findByPk(id);
|
const appLists = await db.select().from(schema.kvAppList).where(eq(schema.kvAppList.id, id)).limit(1);
|
||||||
|
appList = appLists[0];
|
||||||
if (appList?.uid !== uid) {
|
if (appList?.uid !== uid) {
|
||||||
ctx.throw('no permission');
|
ctx.throw('no permission');
|
||||||
}
|
}
|
||||||
@@ -274,7 +308,12 @@ app
|
|||||||
if (!version) {
|
if (!version) {
|
||||||
ctx.throw('version is required');
|
ctx.throw('version is required');
|
||||||
}
|
}
|
||||||
appList = await AppListModel.findOne({ where: { key: appKey, version, uid } });
|
const appLists = await db.select().from(schema.kvAppList).where(and(
|
||||||
|
eq(schema.kvAppList.key, appKey),
|
||||||
|
eq(schema.kvAppList.version, version),
|
||||||
|
eq(schema.kvAppList.uid, uid)
|
||||||
|
)).limit(1);
|
||||||
|
appList = appLists[0];
|
||||||
}
|
}
|
||||||
if (!appList) {
|
if (!appList) {
|
||||||
ctx.throw('app 未发现');
|
ctx.throw('app 未发现');
|
||||||
@@ -287,18 +326,27 @@ app
|
|||||||
if (res.code !== 200) {
|
if (res.code !== 200) {
|
||||||
ctx.throw(res.message || '检测版本列表失败');
|
ctx.throw(res.message || '检测版本列表失败');
|
||||||
}
|
}
|
||||||
appList = await AppListModel.findByPk(appList.id);
|
const appLists2 = await db.select().from(schema.kvAppList).where(eq(schema.kvAppList.id, appList.id)).limit(1);
|
||||||
|
appList = appLists2[0];
|
||||||
}
|
}
|
||||||
if (!appList) {
|
if (!appList) {
|
||||||
ctx.throw('app 未发现');
|
ctx.throw('app 未发现');
|
||||||
}
|
}
|
||||||
|
|
||||||
const files = appList.data.files || [];
|
const appListData = appList.data as AppData;
|
||||||
const am = await AppModel.findOne({ where: { key: appList.key, uid: uid } });
|
const files = appListData.files || [];
|
||||||
|
const ams = await db.select().from(schema.kvApp).where(and(
|
||||||
|
eq(schema.kvApp.key, appList.key),
|
||||||
|
eq(schema.kvApp.uid, uid)
|
||||||
|
)).limit(1);
|
||||||
|
const am = ams[0];
|
||||||
if (!am) {
|
if (!am) {
|
||||||
ctx.throw('app 未发现');
|
ctx.throw('app 未发现');
|
||||||
}
|
}
|
||||||
await am.update({ data: { ...am.data, files }, version: appList.version });
|
const amData = am.data as AppData;
|
||||||
|
await db.update(schema.kvApp)
|
||||||
|
.set({ data: { ...amData, files }, version: appList.version, updatedAt: new Date().toISOString() })
|
||||||
|
.where(eq(schema.kvApp.id, am.id));
|
||||||
setExpire(appList.key, am.user);
|
setExpire(appList.key, am.user);
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
key: appList.key,
|
key: appList.key,
|
||||||
@@ -317,11 +365,16 @@ app
|
|||||||
})
|
})
|
||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
const { user, key, id } = ctx.query.data;
|
const { user, key, id } = ctx.query.data;
|
||||||
let app;
|
let app: AppType | undefined;
|
||||||
if (id) {
|
if (id) {
|
||||||
app = await AppModel.findByPk(id);
|
const apps = await db.select().from(schema.kvApp).where(eq(schema.kvApp.id, id)).limit(1);
|
||||||
|
app = apps[0];
|
||||||
} else if (user && key) {
|
} else if (user && key) {
|
||||||
app = await AppModel.findOne({ where: { user, key } });
|
const apps = await db.select().from(schema.kvApp).where(and(
|
||||||
|
eq(schema.kvApp.user, user),
|
||||||
|
eq(schema.kvApp.key, key)
|
||||||
|
)).limit(1);
|
||||||
|
app = apps[0];
|
||||||
} else {
|
} else {
|
||||||
throw new CustomError('user or key is required');
|
throw new CustomError('user or key is required');
|
||||||
}
|
}
|
||||||
@@ -364,16 +417,23 @@ app
|
|||||||
throw new CustomError('appKey and version are required');
|
throw new CustomError('appKey and version are required');
|
||||||
}
|
}
|
||||||
const uid = await getUidByUsername(app, ctx, username);
|
const uid = await getUidByUsername(app, ctx, username);
|
||||||
let appList = await AppListModel.findOne({ where: { key: appKey, version, uid } });
|
const appLists = await db.select().from(schema.kvAppList).where(and(
|
||||||
|
eq(schema.kvAppList.key, appKey),
|
||||||
|
eq(schema.kvAppList.version, version),
|
||||||
|
eq(schema.kvAppList.uid, uid)
|
||||||
|
)).limit(1);
|
||||||
|
let appList = appLists[0];
|
||||||
if (!appList) {
|
if (!appList) {
|
||||||
appList = await AppListModel.create({
|
const newAppLists = await db.insert(schema.kvAppList).values({
|
||||||
|
id: randomUUID(),
|
||||||
key: appKey,
|
key: appKey,
|
||||||
version,
|
version,
|
||||||
uid,
|
uid,
|
||||||
data: {
|
data: {
|
||||||
files: [],
|
files: [],
|
||||||
},
|
},
|
||||||
});
|
}).returning();
|
||||||
|
appList = newAppLists[0];
|
||||||
}
|
}
|
||||||
const checkUsername = username || tokenUser.username;
|
const checkUsername = username || tokenUser.username;
|
||||||
const files = await getMinioListAndSetToAppList({ username: checkUsername, appKey, version });
|
const files = await getMinioListAndSetToAppList({ username: checkUsername, appKey, version });
|
||||||
@@ -383,7 +443,8 @@ app
|
|||||||
path: item.name,
|
path: item.name,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
let appListFiles = appList.data?.files || [];
|
const appListData = appList.data as AppData;
|
||||||
|
let appListFiles = appListData?.files || [];
|
||||||
const needAddFiles = newFiles.map((item) => {
|
const needAddFiles = newFiles.map((item) => {
|
||||||
const findFile = appListFiles.find((appListFile) => appListFile.name === item.name);
|
const findFile = appListFiles.find((appListFile) => appListFile.name === item.name);
|
||||||
if (findFile && findFile.name === item.name) {
|
if (findFile && findFile.name === item.name) {
|
||||||
@@ -391,11 +452,20 @@ app
|
|||||||
}
|
}
|
||||||
return item;
|
return item;
|
||||||
});
|
});
|
||||||
await appList.update({ data: { files: needAddFiles } });
|
const updateResult = await db.update(schema.kvAppList)
|
||||||
|
.set({ data: { files: needAddFiles }, updatedAt: new Date().toISOString() })
|
||||||
|
.where(eq(schema.kvAppList.id, appList.id))
|
||||||
|
.returning();
|
||||||
|
appList = updateResult[0];
|
||||||
setExpire(appList.id, 'test');
|
setExpire(appList.id, 'test');
|
||||||
let am = await AppModel.findOne({ where: { key: appKey, uid } });
|
const ams = await db.select().from(schema.kvApp).where(and(
|
||||||
|
eq(schema.kvApp.key, appKey),
|
||||||
|
eq(schema.kvApp.uid, uid)
|
||||||
|
)).limit(1);
|
||||||
|
let am = ams[0];
|
||||||
if (!am) {
|
if (!am) {
|
||||||
am = await AppModel.create({
|
const newAms = await db.insert(schema.kvApp).values({
|
||||||
|
id: randomUUID(),
|
||||||
title: appKey,
|
title: appKey,
|
||||||
key: appKey,
|
key: appKey,
|
||||||
version: version || '0.0.1',
|
version: version || '0.0.1',
|
||||||
@@ -403,11 +473,19 @@ app
|
|||||||
uid,
|
uid,
|
||||||
data: { files: needAddFiles },
|
data: { files: needAddFiles },
|
||||||
proxy: appKey.includes('center') ? false : true,
|
proxy: appKey.includes('center') ? false : true,
|
||||||
});
|
}).returning();
|
||||||
|
am = newAms[0];
|
||||||
} else {
|
} else {
|
||||||
const appModel = await AppModel.findOne({ where: { key: appKey, version, uid } });
|
const appModels = await db.select().from(schema.kvApp).where(and(
|
||||||
|
eq(schema.kvApp.key, appKey),
|
||||||
|
eq(schema.kvApp.version, version),
|
||||||
|
eq(schema.kvApp.uid, uid)
|
||||||
|
)).limit(1);
|
||||||
|
const appModel = appModels[0];
|
||||||
if (appModel) {
|
if (appModel) {
|
||||||
await appModel.update({ data: { files: needAddFiles } });
|
await db.update(schema.kvApp)
|
||||||
|
.set({ data: { files: needAddFiles }, updatedAt: new Date().toISOString() })
|
||||||
|
.where(eq(schema.kvApp.id, appModel.id));
|
||||||
setExpire(appModel.key, appModel.user);
|
setExpire(appModel.key, appModel.user);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
46
src/routes/app-manager/module/app-domain-drizzle.ts
Normal file
46
src/routes/app-manager/module/app-domain-drizzle.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { InferSelectModel, InferInsertModel } from 'drizzle-orm';
|
||||||
|
import { kvAppDomain } from '@/db/drizzle/schema.ts';
|
||||||
|
import { redis } from '@/modules/redis.ts';
|
||||||
|
|
||||||
|
// 审核,通过,驳回
|
||||||
|
const appDomainStatus = ['audit', 'auditReject', 'auditPending', 'running', 'stop'] as const;
|
||||||
|
|
||||||
|
export type AppDomainStatus = (typeof appDomainStatus)[number];
|
||||||
|
|
||||||
|
// 类型定义
|
||||||
|
export type AppDomain = InferSelectModel<typeof kvAppDomain>;
|
||||||
|
export type NewAppDomain = InferInsertModel<typeof kvAppDomain>;
|
||||||
|
export type DomainList = AppDomain;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AppDomain 辅助函数
|
||||||
|
*/
|
||||||
|
export class AppDomainHelper {
|
||||||
|
/**
|
||||||
|
* 检查是否可以更新状态
|
||||||
|
*/
|
||||||
|
static checkCanUpdateStatus(currentStatus: string, newStatus: AppDomainStatus): boolean {
|
||||||
|
// 原本是运行中,可以改为停止,原本是停止,可以改为运行。
|
||||||
|
if (currentStatus === 'running' || currentStatus === 'stop') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// 原本是审核状态,不能修改。
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除域名缓存
|
||||||
|
*/
|
||||||
|
static async clearCache(domain: string): Promise<void> {
|
||||||
|
// 清除缓存
|
||||||
|
const cacheKey = `domain:${domain}`;
|
||||||
|
const checkHas = async () => {
|
||||||
|
const has = await redis.get(cacheKey);
|
||||||
|
return has;
|
||||||
|
};
|
||||||
|
const has = await checkHas();
|
||||||
|
if (has) {
|
||||||
|
await redis.set(cacheKey, '', 'EX', 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
import { sequelize } from '../../../modules/sequelize.ts';
|
|
||||||
import { DataTypes, Model } from 'sequelize';
|
|
||||||
export type DomainList = Partial<InstanceType<typeof AppDomainModel>>;
|
|
||||||
import { redis } from '../../../modules/redis.ts';
|
|
||||||
|
|
||||||
// 审核,通过,驳回
|
|
||||||
const appDomainStatus = ['audit', 'auditReject', 'auditPending', 'running', 'stop'] as const;
|
|
||||||
|
|
||||||
type AppDomainStatus = (typeof appDomainStatus)[number];
|
|
||||||
/**
|
|
||||||
* 应用域名管理
|
|
||||||
*/
|
|
||||||
export class AppDomainModel extends Model {
|
|
||||||
declare id: string;
|
|
||||||
declare domain: string;
|
|
||||||
declare appId: string;
|
|
||||||
// 状态,
|
|
||||||
declare status: AppDomainStatus;
|
|
||||||
declare uid: string;
|
|
||||||
declare data: Record<string, any>;
|
|
||||||
|
|
||||||
declare createdAt: Date;
|
|
||||||
declare updatedAt: Date;
|
|
||||||
|
|
||||||
checkCanUpdateStatus(newStatus: AppDomainStatus) {
|
|
||||||
// 原本是运行中,可以改为停止,原本是停止,可以改为运行。
|
|
||||||
if (this.status === 'running' || this.status === 'stop') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// 原本是审核状态,不能修改。
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
async clearCache() {
|
|
||||||
// 清除缓存
|
|
||||||
const cacheKey = `domain:${this.domain}`;
|
|
||||||
const checkHas = async () => {
|
|
||||||
const has = await redis.get(cacheKey);
|
|
||||||
return has;
|
|
||||||
};
|
|
||||||
const has = await checkHas();
|
|
||||||
if (has) {
|
|
||||||
await redis.set(cacheKey, '', 'EX', 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AppDomainModel.init(
|
|
||||||
{
|
|
||||||
id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
primaryKey: true,
|
|
||||||
defaultValue: DataTypes.UUIDV4,
|
|
||||||
comment: 'id',
|
|
||||||
},
|
|
||||||
domain: {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
allowNull: false,
|
|
||||||
unique: true,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
type: DataTypes.JSONB,
|
|
||||||
allowNull: true,
|
|
||||||
},
|
|
||||||
status: {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: 'running',
|
|
||||||
},
|
|
||||||
appId: {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
allowNull: true,
|
|
||||||
},
|
|
||||||
uid: {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
allowNull: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sequelize,
|
|
||||||
tableName: 'kv_app_domain',
|
|
||||||
paranoid: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
80
src/routes/app-manager/module/app-drizzle.ts
Normal file
80
src/routes/app-manager/module/app-drizzle.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import { InferSelectModel, InferInsertModel } from 'drizzle-orm';
|
||||||
|
import { kvApp, kvAppList } from '@/db/drizzle/schema.ts';
|
||||||
|
|
||||||
|
type AppPermissionType = 'public' | 'private' | 'protected';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 共享设置
|
||||||
|
* 1. 设置公共可以直接访问
|
||||||
|
* 2. 设置受保护需要登录后访问
|
||||||
|
* 3. 设置私有只有自己可以访问。\n
|
||||||
|
* 受保护可以设置密码,设置访问的用户名。切换共享状态后,需要重新设置密码和用户名。
|
||||||
|
*/
|
||||||
|
export interface AppData {
|
||||||
|
files: { name: string; path: string }[];
|
||||||
|
permission?: {
|
||||||
|
// 访问权限, 字段和minio的权限配置一致
|
||||||
|
share: AppPermissionType; // public, private(Only Self), protected(protected, 通过配置访问)
|
||||||
|
usernames?: string; // 受保护的访问用户名,多个用逗号分隔
|
||||||
|
password?: string; // 受保护的访问密码
|
||||||
|
'expiration-time'?: string; // 受保护的访问过期时间
|
||||||
|
};
|
||||||
|
// 运行环境,browser, node, 或者其他,是数组
|
||||||
|
runtime?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum AppStatus {
|
||||||
|
running = 'running',
|
||||||
|
stop = 'stop',
|
||||||
|
}
|
||||||
|
|
||||||
|
// 类型定义
|
||||||
|
export type App = InferSelectModel<typeof kvApp>;
|
||||||
|
export type NewApp = InferInsertModel<typeof kvApp>;
|
||||||
|
export type AppList = InferSelectModel<typeof kvAppList>;
|
||||||
|
export type NewAppList = InferInsertModel<typeof kvAppList>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* App 辅助函数
|
||||||
|
*/
|
||||||
|
export class AppHelper {
|
||||||
|
/**
|
||||||
|
* 移动应用到新用户
|
||||||
|
*/
|
||||||
|
static async getNewFiles(
|
||||||
|
files: { name: string; path: string }[] = [],
|
||||||
|
opts: { oldUser: string; newUser: string } = { oldUser: '', newUser: '' }
|
||||||
|
) {
|
||||||
|
const { oldUser, newUser } = opts;
|
||||||
|
const _ = files.map((item) => {
|
||||||
|
if (item.path.startsWith('http')) {
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
if (oldUser && item.path.startsWith(oldUser)) {
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
const paths = item.path.split('/');
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
path: newUser + '/' + paths.slice(1).join('/'),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return _;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取公开信息(删除敏感数据)
|
||||||
|
*/
|
||||||
|
static getPublic(app: App) {
|
||||||
|
const value = { ...app };
|
||||||
|
// 删除不需要的字段
|
||||||
|
const data = value.data as AppData;
|
||||||
|
if (data && data.permission) {
|
||||||
|
delete data.permission.usernames;
|
||||||
|
delete data.permission.password;
|
||||||
|
delete data.permission['expiration-time'];
|
||||||
|
}
|
||||||
|
value.data = data;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import { sequelize } from '../../../modules/sequelize.ts';
|
|
||||||
import { DataTypes, Model } from 'sequelize';
|
|
||||||
import { AppData } from './app.ts';
|
|
||||||
|
|
||||||
export type AppList = Partial<InstanceType<typeof AppListModel>>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* APP List 管理 历史版本管理
|
|
||||||
*/
|
|
||||||
export class AppListModel extends Model {
|
|
||||||
declare id: string;
|
|
||||||
declare data: AppData;
|
|
||||||
declare version: string;
|
|
||||||
declare key: string;
|
|
||||||
declare uid: string;
|
|
||||||
declare status: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
AppListModel.init(
|
|
||||||
{
|
|
||||||
id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
primaryKey: true,
|
|
||||||
defaultValue: DataTypes.UUIDV4,
|
|
||||||
comment: 'id',
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
type: DataTypes.JSON,
|
|
||||||
defaultValue: {},
|
|
||||||
},
|
|
||||||
version: {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
defaultValue: '',
|
|
||||||
},
|
|
||||||
key: {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
},
|
|
||||||
uid: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: true,
|
|
||||||
},
|
|
||||||
status: {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
defaultValue: 'running',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sequelize,
|
|
||||||
tableName: 'kv_app_list',
|
|
||||||
paranoid: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// AppListModel.sync({ alter: true, logging: false }).catch((e) => {
|
|
||||||
// console.error('AppListModel sync', e);
|
|
||||||
// });
|
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
import { sequelize } from '../../../modules/sequelize.ts';
|
|
||||||
import { DataTypes, Model } from 'sequelize';
|
|
||||||
|
|
||||||
type AppPermissionType = 'public' | 'private' | 'protected';
|
|
||||||
/**
|
|
||||||
* 共享设置
|
|
||||||
* 1. 设置公共可以直接访问
|
|
||||||
* 2. 设置受保护需要登录后访问
|
|
||||||
* 3. 设置私有只有自己可以访问。\n
|
|
||||||
* 受保护可以设置密码,设置访问的用户名。切换共享状态后,需要重新设置密码和用户名。
|
|
||||||
*/
|
|
||||||
export interface AppData {
|
|
||||||
files: { name: string; path: string }[];
|
|
||||||
permission?: {
|
|
||||||
// 访问权限, 字段和minio的权限配置一致
|
|
||||||
share: AppPermissionType; // public, private(Only Self), protected(protected, 通过配置访问)
|
|
||||||
usernames?: string; // 受保护的访问用户名,多个用逗号分隔
|
|
||||||
password?: string; // 受保护的访问密码
|
|
||||||
'expiration-time'?: string; // 受保护的访问过期时间
|
|
||||||
};
|
|
||||||
// 运行环境,browser, node, 或者其他,是数组
|
|
||||||
runtime?: string[];
|
|
||||||
}
|
|
||||||
export enum AppStatus {
|
|
||||||
running = 'running',
|
|
||||||
stop = 'stop',
|
|
||||||
}
|
|
||||||
export type App = Partial<InstanceType<typeof AppModel>>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* APP 管理
|
|
||||||
*/
|
|
||||||
export class AppModel extends Model {
|
|
||||||
declare id: string;
|
|
||||||
declare data: AppData;
|
|
||||||
declare title: string;
|
|
||||||
declare description: string;
|
|
||||||
declare version: string;
|
|
||||||
declare key: string;
|
|
||||||
declare uid: string;
|
|
||||||
declare pid: string;
|
|
||||||
// 是否是history路由代理模式。静态的直接转minio,而不需要缓存下来。
|
|
||||||
declare proxy: boolean;
|
|
||||||
declare user: string;
|
|
||||||
declare status: string;
|
|
||||||
static async moveToNewUser(oldUserName: string, newUserName: string) {
|
|
||||||
const appIds = await AppModel.findAll({
|
|
||||||
where: {
|
|
||||||
user: oldUserName,
|
|
||||||
},
|
|
||||||
attributes: ['id'],
|
|
||||||
});
|
|
||||||
for (const app of appIds) {
|
|
||||||
const appData = await AppModel.findByPk(app.id);
|
|
||||||
appData.user = newUserName;
|
|
||||||
const data = appData.data;
|
|
||||||
data.files = await AppModel.getNewFiles(data.files, {
|
|
||||||
oldUser: oldUserName,
|
|
||||||
newUser: newUserName,
|
|
||||||
});
|
|
||||||
appData.data = { ...data };
|
|
||||||
await appData.save({ fields: ['data', 'user'] });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
static async getNewFiles(files: { name: string; path: string }[] = [], opts: { oldUser: string; newUser: string } = { oldUser: '', newUser: '' }) {
|
|
||||||
const { oldUser, newUser } = opts;
|
|
||||||
const _ = files.map((item) => {
|
|
||||||
if (item.path.startsWith('http')) {
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
if (oldUser && item.path.startsWith(oldUser)) {
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
const paths = item.path.split('/');
|
|
||||||
return {
|
|
||||||
...item,
|
|
||||||
path: newUser + '/' + paths.slice(1).join('/'),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
return _;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getPublic() {
|
|
||||||
const value = this.toJSON();
|
|
||||||
// 删除不需要的字段
|
|
||||||
const data = value.data;
|
|
||||||
if (data && data.permission) {
|
|
||||||
delete data.permission.usernames;
|
|
||||||
delete data.permission.password;
|
|
||||||
delete data.permission['expiration-time'];
|
|
||||||
}
|
|
||||||
value.data = data;
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
AppModel.init(
|
|
||||||
{
|
|
||||||
id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
primaryKey: true,
|
|
||||||
defaultValue: DataTypes.UUIDV4,
|
|
||||||
comment: 'id',
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
defaultValue: '',
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
defaultValue: '',
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
type: DataTypes.JSONB,
|
|
||||||
defaultValue: {},
|
|
||||||
},
|
|
||||||
version: {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
defaultValue: '',
|
|
||||||
},
|
|
||||||
key: {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
// 和 uid 组合唯一
|
|
||||||
},
|
|
||||||
uid: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: true,
|
|
||||||
},
|
|
||||||
pid: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: true,
|
|
||||||
},
|
|
||||||
proxy: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
defaultValue: false,
|
|
||||||
},
|
|
||||||
user: {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
allowNull: true,
|
|
||||||
},
|
|
||||||
status: {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
defaultValue: 'running', // stop, running
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sequelize,
|
|
||||||
tableName: 'kv_app',
|
|
||||||
paranoid: true,
|
|
||||||
indexes: [
|
|
||||||
{
|
|
||||||
unique: true,
|
|
||||||
fields: ['key', 'uid'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// AppModel.sync({ alter: true, logging: false }).catch((e) => {
|
|
||||||
// console.error('AppModel sync', e);
|
|
||||||
// });
|
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
export * from './app-list.ts';
|
// Drizzle 模型(推荐使用)
|
||||||
export * from './app.ts';
|
export * from './app-domain-drizzle.ts'
|
||||||
|
export * from './app-drizzle.ts'
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { app } from '@/app.ts';
|
import { app, db, schema } from '@/app.ts';
|
||||||
import { AppModel } from '../module/index.ts';
|
|
||||||
import { ConfigPermission } from '@kevisual/permission';
|
import { ConfigPermission } from '@kevisual/permission';
|
||||||
|
import { eq, desc, asc } from 'drizzle-orm';
|
||||||
|
|
||||||
// curl http://localhost:4005/api/router?path=app&key=public-list
|
// curl http://localhost:4005/api/router?path=app&key=public-list
|
||||||
// TODO:
|
// TODO:
|
||||||
@@ -11,23 +11,20 @@ app
|
|||||||
})
|
})
|
||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
const { username = 'root', status = 'running', page = 1, pageSize = 100, order = 'DESC' } = ctx.query.data || {};
|
const { username = 'root', status = 'running', page = 1, pageSize = 100, order = 'DESC' } = ctx.query.data || {};
|
||||||
const { rows, count } = await AppModel.findAndCountAll({
|
const offset = (page - 1) * pageSize;
|
||||||
where: {
|
const apps = await db.select().from(schema.kvApp)
|
||||||
status,
|
.where(eq(schema.kvApp.user, username))
|
||||||
user: username,
|
.orderBy(order === 'DESC' ? desc(schema.kvApp.updatedAt) : asc(schema.kvApp.updatedAt))
|
||||||
},
|
.limit(pageSize)
|
||||||
attributes: {
|
.offset(offset);
|
||||||
exclude: [],
|
// Note: Drizzle doesn't have a direct equivalent to findAndCountAll
|
||||||
},
|
// We need to do a separate count query
|
||||||
order: [['updatedAt', order]],
|
const countResult = await db.select({ count: schema.kvApp.id }).from(schema.kvApp)
|
||||||
limit: pageSize,
|
.where(eq(schema.kvApp.user, username));
|
||||||
offset: (page - 1) * pageSize,
|
const count = countResult.length;
|
||||||
distinct: true,
|
|
||||||
logging: false,
|
|
||||||
});
|
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
list: rows.map((item) => {
|
list: apps.map((item) => {
|
||||||
return ConfigPermission.getDataPublicPermission(item.toJSON());
|
return ConfigPermission.getDataPublicPermission(item);
|
||||||
}),
|
}),
|
||||||
pagination: {
|
pagination: {
|
||||||
total: count,
|
total: count,
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import { app } from '@/app.ts';
|
import { app, db, schema } from '@/app.ts';
|
||||||
import { AppModel } from '../module/index.ts';
|
import { randomUUID } from 'crypto';
|
||||||
import { AppListModel } from '../module/index.ts';
|
|
||||||
import { oss } from '@/app.ts';
|
import { oss } from '@/app.ts';
|
||||||
import { User } from '@/models/user.ts';
|
import { User } from '@/models/user.ts';
|
||||||
import { permission } from 'process';
|
|
||||||
import { customAlphabet } from 'nanoid';
|
import { customAlphabet } from 'nanoid';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
@@ -65,7 +63,8 @@ app
|
|||||||
path: urlPath,
|
path: urlPath,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const appModel = await AppModel.create({
|
const appModels = await db.insert(schema.kvApp).values({
|
||||||
|
id: randomUUID(),
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
version,
|
version,
|
||||||
@@ -80,15 +79,18 @@ app
|
|||||||
},
|
},
|
||||||
files: files,
|
files: files,
|
||||||
},
|
},
|
||||||
});
|
}).returning();
|
||||||
const appVersionModel = await AppListModel.create({
|
const appModel = appModels[0];
|
||||||
|
const appVersionModels = await db.insert(schema.kvAppList).values({
|
||||||
|
id: randomUUID(),
|
||||||
data: {
|
data: {
|
||||||
files: files,
|
files: files,
|
||||||
},
|
},
|
||||||
version: appModel.version,
|
version: appModel.version,
|
||||||
key: appModel.key,
|
key: appModel.key,
|
||||||
uid: appModel.uid,
|
uid: appModel.uid,
|
||||||
});
|
}).returning();
|
||||||
|
const appVersionModel = appVersionModels[0];
|
||||||
|
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
url: `/${username}/${key}/`,
|
url: `/${username}/${key}/`,
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { AppModel, AppListModel } from './module/index.ts';
|
import { App, AppList, AppData, AppHelper } from './module/app-drizzle.ts';
|
||||||
import { app } from '@/app.ts';
|
import { app, db, schema } from '@/app.ts';
|
||||||
import { setExpire } from './revoke.ts';
|
import { setExpire } from './revoke.ts';
|
||||||
import { deleteFileByPrefix } from '../file/index.ts';
|
import { deleteFileByPrefix } from '../file/index.ts';
|
||||||
|
import { eq, and, desc } from 'drizzle-orm';
|
||||||
|
|
||||||
app
|
app
|
||||||
.route({
|
.route({
|
||||||
@@ -12,15 +13,24 @@ app
|
|||||||
})
|
})
|
||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
const tokenUser = ctx.state.tokenUser;
|
const tokenUser = ctx.state.tokenUser;
|
||||||
const list = await AppModel.findAll({
|
const list = await db.select({
|
||||||
order: [['updatedAt', 'DESC']],
|
id: schema.kvApp.id,
|
||||||
where: {
|
title: schema.kvApp.title,
|
||||||
uid: tokenUser.id,
|
description: schema.kvApp.description,
|
||||||
},
|
version: schema.kvApp.version,
|
||||||
attributes: {
|
key: schema.kvApp.key,
|
||||||
exclude: ['data'],
|
uid: schema.kvApp.uid,
|
||||||
},
|
pid: schema.kvApp.pid,
|
||||||
});
|
proxy: schema.kvApp.proxy,
|
||||||
|
user: schema.kvApp.user,
|
||||||
|
status: schema.kvApp.status,
|
||||||
|
createdAt: schema.kvApp.createdAt,
|
||||||
|
updatedAt: schema.kvApp.updatedAt,
|
||||||
|
deletedAt: schema.kvApp.deletedAt,
|
||||||
|
})
|
||||||
|
.from(schema.kvApp)
|
||||||
|
.where(eq(schema.kvApp.uid, tokenUser.id))
|
||||||
|
.orderBy(desc(schema.kvApp.updatedAt));
|
||||||
ctx.body = list;
|
ctx.body = list;
|
||||||
return ctx;
|
return ctx;
|
||||||
})
|
})
|
||||||
@@ -40,14 +50,18 @@ app
|
|||||||
if (!id && !key) {
|
if (!id && !key) {
|
||||||
ctx.throw(500, 'id is required');
|
ctx.throw(500, 'id is required');
|
||||||
}
|
}
|
||||||
let am: AppModel;
|
let am: App | undefined;
|
||||||
if (id) {
|
if (id) {
|
||||||
am = await AppModel.findByPk(id);
|
const apps = await db.select().from(schema.kvApp).where(eq(schema.kvApp.id, id)).limit(1);
|
||||||
|
am = apps[0];
|
||||||
if (!am) {
|
if (!am) {
|
||||||
ctx.throw(500, 'app not found');
|
ctx.throw(500, 'app not found');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
am = await AppModel.findOne({ where: { key, uid: tokenUser.id } });
|
const apps = await db.select().from(schema.kvApp)
|
||||||
|
.where(and(eq(schema.kvApp.key, key), eq(schema.kvApp.uid, tokenUser.id)))
|
||||||
|
.limit(1);
|
||||||
|
am = apps[0];
|
||||||
if (!am) {
|
if (!am) {
|
||||||
ctx.throw(500, 'app not found');
|
ctx.throw(500, 'app not found');
|
||||||
}
|
}
|
||||||
@@ -71,21 +85,27 @@ app
|
|||||||
|
|
||||||
const { data, id, user, ...rest } = ctx.query.data;
|
const { data, id, user, ...rest } = ctx.query.data;
|
||||||
if (id) {
|
if (id) {
|
||||||
const app = await AppModel.findByPk(id);
|
const apps = await db.select().from(schema.kvApp).where(eq(schema.kvApp.id, id)).limit(1);
|
||||||
|
const app = apps[0];
|
||||||
if (app) {
|
if (app) {
|
||||||
const newData = { ...app.data, ...data };
|
const appData = app.data as AppData;
|
||||||
|
const newData = { ...appData, ...data };
|
||||||
if (app.user !== tokenUser.username) {
|
if (app.user !== tokenUser.username) {
|
||||||
rest.user = tokenUser.username;
|
rest.user = tokenUser.username;
|
||||||
let files = newData?.files || [];
|
let files = newData?.files || [];
|
||||||
if (files.length > 0) {
|
if (files.length > 0) {
|
||||||
files = await AppModel.getNewFiles(files, { oldUser: app.user, newUser: tokenUser.username });
|
files = await AppHelper.getNewFiles(files, { oldUser: app.user!, newUser: tokenUser.username });
|
||||||
}
|
}
|
||||||
newData.files = files;
|
newData.files = files;
|
||||||
}
|
}
|
||||||
const newApp = await app.update({ data: newData, ...rest });
|
const updateResult = await db.update(schema.kvApp)
|
||||||
|
.set({ data: newData, ...rest, updatedAt: new Date().toISOString() })
|
||||||
|
.where(eq(schema.kvApp.id, id))
|
||||||
|
.returning();
|
||||||
|
const newApp = updateResult[0];
|
||||||
ctx.body = newApp;
|
ctx.body = newApp;
|
||||||
if (app.status !== 'running' || data?.share || rest?.status) {
|
if (app.status !== 'running' || data?.share || rest?.status) {
|
||||||
setExpire(newApp.key, app.user);
|
setExpire(newApp.key!, app.user!);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ctx.throw(500, 'app not found');
|
ctx.throw(500, 'app not found');
|
||||||
@@ -95,17 +115,19 @@ app
|
|||||||
if (!rest.key) {
|
if (!rest.key) {
|
||||||
ctx.throw(500, 'key is required');
|
ctx.throw(500, 'key is required');
|
||||||
}
|
}
|
||||||
const findApp = await AppModel.findOne({ where: { key: rest.key, uid: tokenUser.id } });
|
const findApps = await db.select().from(schema.kvApp)
|
||||||
if (findApp) {
|
.where(and(eq(schema.kvApp.key, rest.key), eq(schema.kvApp.uid, tokenUser.id)))
|
||||||
|
.limit(1);
|
||||||
|
if (findApps.length > 0) {
|
||||||
ctx.throw(500, 'key already exists');
|
ctx.throw(500, 'key already exists');
|
||||||
}
|
}
|
||||||
const app = await AppModel.create({
|
const newApps = await db.insert(schema.kvApp).values({
|
||||||
data: { files: [] },
|
data: { files: [] },
|
||||||
...rest,
|
...rest,
|
||||||
uid: tokenUser.id,
|
uid: tokenUser.id,
|
||||||
user: tokenUser.username,
|
user: tokenUser.username,
|
||||||
});
|
}).returning();
|
||||||
ctx.body = app;
|
ctx.body = newApps[0];
|
||||||
return ctx;
|
return ctx;
|
||||||
})
|
})
|
||||||
.addTo(app);
|
.addTo(app);
|
||||||
@@ -124,16 +146,18 @@ app
|
|||||||
if (!id) {
|
if (!id) {
|
||||||
ctx.throw(500, 'id is required');
|
ctx.throw(500, 'id is required');
|
||||||
}
|
}
|
||||||
const am = await AppModel.findByPk(id);
|
const apps = await db.select().from(schema.kvApp).where(eq(schema.kvApp.id, id)).limit(1);
|
||||||
|
const am = apps[0];
|
||||||
if (!am) {
|
if (!am) {
|
||||||
ctx.throw(500, 'app not found');
|
ctx.throw(500, 'app not found');
|
||||||
}
|
}
|
||||||
if (am.uid !== tokenUser.id) {
|
if (am.uid !== tokenUser.id) {
|
||||||
ctx.throw(500, 'app not found');
|
ctx.throw(500, 'app not found');
|
||||||
}
|
}
|
||||||
const list = await AppListModel.findAll({ where: { key: am.key, uid: tokenUser.id } });
|
const list = await db.select().from(schema.kvAppList)
|
||||||
await am.destroy({ force: true });
|
.where(and(eq(schema.kvAppList.key, am.key!), eq(schema.kvAppList.uid, tokenUser.id)));
|
||||||
await Promise.all(list.map((item) => item.destroy({ force: true })));
|
await db.delete(schema.kvApp).where(eq(schema.kvApp.id, id));
|
||||||
|
await Promise.all(list.map((item) => db.delete(schema.kvAppList).where(eq(schema.kvAppList.id, item.id))));
|
||||||
if (deleteFile) {
|
if (deleteFile) {
|
||||||
const username = tokenUser.username;
|
const username = tokenUser.username;
|
||||||
await deleteFileByPrefix(`${username}/${am.key}`);
|
await deleteFileByPrefix(`${username}/${am.key}`);
|
||||||
@@ -154,13 +178,13 @@ app
|
|||||||
if (!id) {
|
if (!id) {
|
||||||
ctx.throw(500, 'id is required');
|
ctx.throw(500, 'id is required');
|
||||||
}
|
}
|
||||||
const am = await AppListModel.findByPk(id);
|
const apps = await db.select().from(schema.kvAppList).where(eq(schema.kvAppList.id, id)).limit(1);
|
||||||
|
const am = apps[0];
|
||||||
if (!am) {
|
if (!am) {
|
||||||
ctx.throw(500, 'app not found');
|
ctx.throw(500, 'app not found');
|
||||||
}
|
}
|
||||||
const amJson = am.toJSON();
|
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
...amJson,
|
...am,
|
||||||
proxy: true,
|
proxy: true,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
import './list.ts';
|
// import './list.ts';
|
||||||
|
|||||||
@@ -1,107 +1,107 @@
|
|||||||
import { Op } from 'sequelize';
|
// import { Op } from 'sequelize';
|
||||||
import { app } from '@/app.ts';
|
// import { app } from '@/app.ts';
|
||||||
import { FileSyncModel } from './model.ts';
|
// import { FileSyncModel } from './model.ts';
|
||||||
app
|
// app
|
||||||
.route({
|
// .route({
|
||||||
path: 'file-listener',
|
// path: 'file-listener',
|
||||||
key: 'list',
|
// key: 'list',
|
||||||
middleware: ['auth'],
|
// middleware: ['auth'],
|
||||||
description: '获取用户的某一个文件夹下的所有的列表的数据',
|
// description: '获取用户的某一个文件夹下的所有的列表的数据',
|
||||||
})
|
// })
|
||||||
.define(async (ctx) => {
|
// .define(async (ctx) => {
|
||||||
const tokenUser = ctx.state.tokenUser;
|
// const tokenUser = ctx.state.tokenUser;
|
||||||
const username = tokenUser.username;
|
// const username = tokenUser.username;
|
||||||
const { page = 1, pageSize = 20, sort = 'DESC' } = ctx.query;
|
// const { page = 1, pageSize = 20, sort = 'DESC' } = ctx.query;
|
||||||
let { prefix } = ctx.query;
|
// let { prefix } = ctx.query;
|
||||||
if (prefix) {
|
// if (prefix) {
|
||||||
if (typeof prefix !== 'string') {
|
// if (typeof prefix !== 'string') {
|
||||||
ctx.throw(400, 'prefix must be a string');
|
// ctx.throw(400, 'prefix must be a string');
|
||||||
}
|
// }
|
||||||
if (prefix.startsWith('/')) {
|
// if (prefix.startsWith('/')) {
|
||||||
prefix = prefix.slice(1); // Remove leading slash if present
|
// prefix = prefix.slice(1); // Remove leading slash if present
|
||||||
}
|
// }
|
||||||
if (!prefix.startsWith(username + '/')) {
|
// if (!prefix.startsWith(username + '/')) {
|
||||||
ctx.throw(400, 'prefix must start with the your username:', username);
|
// ctx.throw(400, 'prefix must start with the your username:', username);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
const searchWhere = prefix
|
// const searchWhere = prefix
|
||||||
? {
|
// ? {
|
||||||
[Op.or]: [{ name: { [Op.like]: `${prefix}%` } }],
|
// [Op.or]: [{ name: { [Op.like]: `${prefix}%` } }],
|
||||||
}
|
// }
|
||||||
: {};
|
// : {};
|
||||||
|
|
||||||
const { rows: files, count } = await FileSyncModel.findAndCountAll({
|
// const { rows: files, count } = await FileSyncModel.findAndCountAll({
|
||||||
where: {
|
// where: {
|
||||||
...searchWhere,
|
// ...searchWhere,
|
||||||
},
|
// },
|
||||||
offset: (page - 1) * pageSize,
|
// offset: (page - 1) * pageSize,
|
||||||
limit: pageSize,
|
// limit: pageSize,
|
||||||
order: [['updatedAt', sort]],
|
// order: [['updatedAt', sort]],
|
||||||
});
|
// });
|
||||||
const getPublicFiles = (files: FileSyncModel[]) => {
|
// const getPublicFiles = (files: FileSyncModel[]) => {
|
||||||
return files.map((file) => {
|
// return files.map((file) => {
|
||||||
const value = file.toJSON();
|
// const value = file.toJSON();
|
||||||
const stat = value.stat || {};
|
// const stat = value.stat || {};
|
||||||
delete stat.password;
|
// delete stat.password;
|
||||||
return {
|
// return {
|
||||||
...value,
|
// ...value,
|
||||||
stat: stat,
|
// stat: stat,
|
||||||
};
|
// };
|
||||||
});
|
// });
|
||||||
};
|
// };
|
||||||
|
|
||||||
ctx.body = {
|
// ctx.body = {
|
||||||
list: getPublicFiles(files),
|
// list: getPublicFiles(files),
|
||||||
pagination: {
|
// pagination: {
|
||||||
page,
|
// page,
|
||||||
current: page,
|
// current: page,
|
||||||
pageSize,
|
// pageSize,
|
||||||
total: count,
|
// total: count,
|
||||||
},
|
// },
|
||||||
};
|
// };
|
||||||
})
|
// })
|
||||||
.addTo(app);
|
// .addTo(app);
|
||||||
|
|
||||||
app
|
// app
|
||||||
.route({
|
// .route({
|
||||||
path: 'file-listener',
|
// path: 'file-listener',
|
||||||
key: 'get',
|
// key: 'get',
|
||||||
middleware: ['auth'],
|
// middleware: ['auth'],
|
||||||
})
|
// })
|
||||||
.define(async (ctx) => {
|
// .define(async (ctx) => {
|
||||||
const tokenUser = ctx.state.tokenUser;
|
// const tokenUser = ctx.state.tokenUser;
|
||||||
const username = tokenUser.username;
|
// const username = tokenUser.username;
|
||||||
const { id, name, hash } = ctx.query.data || {};
|
// const { id, name, hash } = ctx.query.data || {};
|
||||||
|
|
||||||
if (!id && !name && !hash) {
|
// if (!id && !name && !hash) {
|
||||||
ctx.throw(400, 'id, name or hash is required');
|
// ctx.throw(400, 'id, name or hash is required');
|
||||||
}
|
// }
|
||||||
let fileSync: FileSyncModel | null = null;
|
// let fileSync: FileSyncModel | null = null;
|
||||||
if (id) {
|
// if (id) {
|
||||||
fileSync = await FileSyncModel.findByPk(id);
|
// fileSync = await FileSyncModel.findByPk(id);
|
||||||
}
|
// }
|
||||||
if (name && !fileSync) {
|
// if (name && !fileSync) {
|
||||||
fileSync = await FileSyncModel.findOne({
|
// fileSync = await FileSyncModel.findOne({
|
||||||
where: {
|
// where: {
|
||||||
name,
|
// name,
|
||||||
hash,
|
// hash,
|
||||||
},
|
// },
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
if (!fileSync && hash) {
|
// if (!fileSync && hash) {
|
||||||
fileSync = await FileSyncModel.findOne({
|
// fileSync = await FileSyncModel.findOne({
|
||||||
where: {
|
// where: {
|
||||||
name: {
|
// name: {
|
||||||
[Op.like]: `${username}/%`,
|
// [Op.like]: `${username}/%`,
|
||||||
},
|
// },
|
||||||
hash,
|
// hash,
|
||||||
},
|
// },
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
|
|
||||||
if (!fileSync || !fileSync.name.startsWith(`${username}/`)) {
|
// if (!fileSync || !fileSync.name.startsWith(`${username}/`)) {
|
||||||
ctx.throw(404, 'NotFoundFile');
|
// ctx.throw(404, 'NotFoundFile');
|
||||||
}
|
// }
|
||||||
ctx.body = fileSync;
|
// ctx.body = fileSync;
|
||||||
})
|
// })
|
||||||
.addTo(app);
|
// .addTo(app);
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
import { FileSyncModel } from '@kevisual/file-listener/src/file-sync/model.ts';
|
// import { FileSyncModel } from '@kevisual/file-listener/src/file-sync/model.ts';
|
||||||
import type { FileSyncModelType } from '@kevisual/file-listener/src/file-sync/model.ts';
|
// import type { FileSyncModelType } from '@kevisual/file-listener/src/file-sync/model.ts';
|
||||||
export { FileSyncModel, FileSyncModelType };
|
// export { FileSyncModel, FileSyncModelType };
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ import './micro-app/index.ts';
|
|||||||
|
|
||||||
import './config/index.ts';
|
import './config/index.ts';
|
||||||
|
|
||||||
// import './file-listener/index.ts';
|
|
||||||
|
|
||||||
import './mark/index.ts';
|
import './mark/index.ts';
|
||||||
|
|
||||||
import './light-code/index.ts';
|
import './light-code/index.ts';
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { app } from '@/app.ts';
|
import { app, db, schema } from '@/app.ts';
|
||||||
import { appPathCheck, installApp } from './module/install-app.ts';
|
import { appPathCheck, installApp } from './module/install-app.ts';
|
||||||
import { manager } from './manager-app.ts';
|
import { manager } from './manager-app.ts';
|
||||||
import { selfRestart } from '@/modules/self-restart.ts';
|
import { selfRestart } from '@/modules/self-restart.ts';
|
||||||
import { AppListModel } from '../app-manager/module/index.ts';
|
import { AppList } from '../app-manager/module/index.ts';
|
||||||
|
import { eq, and } from 'drizzle-orm';
|
||||||
// curl http://localhost:4002/api/router?path=micro-app&key=deploy
|
// curl http://localhost:4002/api/router?path=micro-app&key=deploy
|
||||||
// 把对应的应用安装到系统的apps目录下,并解压,然后把配置项写入数据库配置
|
// 把对应的应用安装到系统的apps目录下,并解压,然后把配置项写入数据库配置
|
||||||
// key 是应用的唯一标识,和package.json中的key一致,绑定关系
|
// key 是应用的唯一标识,和package.json中的key一致,绑定关系
|
||||||
@@ -26,17 +27,17 @@ app
|
|||||||
if (data.username && username === 'admin') {
|
if (data.username && username === 'admin') {
|
||||||
username = data.username;
|
username = data.username;
|
||||||
}
|
}
|
||||||
let microApp: AppListModel;
|
let microApp: AppList | undefined;
|
||||||
if (!microApp && id) {
|
if (!microApp && id) {
|
||||||
microApp = await AppListModel.findByPk(id);
|
const apps = await db.select().from(schema.kvAppList).where(eq(schema.kvAppList.id, id)).limit(1);
|
||||||
|
microApp = apps[0];
|
||||||
}
|
}
|
||||||
if (!microApp && postAppKey) {
|
if (!microApp && postAppKey) {
|
||||||
microApp = await AppListModel.findOne({
|
const apps = await db.select().from(schema.kvAppList).where(and(
|
||||||
where: {
|
eq(schema.kvAppList.key, postAppKey),
|
||||||
key: postAppKey,
|
eq(schema.kvAppList.version, postVersion)
|
||||||
version: postVersion,
|
)).limit(1);
|
||||||
},
|
microApp = apps[0];
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!microApp) {
|
if (!microApp) {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import path from 'path';
|
|||||||
const assistantAppsConfig = path.join(process.cwd(), 'assistant-apps-config.json');
|
const assistantAppsConfig = path.join(process.cwd(), 'assistant-apps-config.json');
|
||||||
const isExist = fileIsExist(assistantAppsConfig);
|
const isExist = fileIsExist(assistantAppsConfig);
|
||||||
export const existDenpend = [
|
export const existDenpend = [
|
||||||
'sequelize', // commonjs
|
|
||||||
'pg', // commonjs
|
'pg', // commonjs
|
||||||
'@kevisual/router', // 共享模块
|
'@kevisual/router', // 共享模块
|
||||||
'ioredis', // commonjs
|
'ioredis', // commonjs
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
export * from '../old-apps/container/type.ts'
|
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import { app } from '@/app.ts';
|
import { app, db, schema } from '@/app.ts';
|
||||||
import { User } from '@/models/user.ts';
|
import { User } from '@/models/user.ts';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
import { CustomError } from '@kevisual/router';
|
import { CustomError } from '@kevisual/router';
|
||||||
import { backupUserA, deleteUser, mvUserAToUserB } from '@/routes/file/index.ts';
|
import { backupUserA, deleteUser, mvUserAToUserB } from '@/routes/file/index.ts';
|
||||||
import { AppModel } from '@/routes/app-manager/index.ts';
|
import { AppHelper } from '@/routes/app-manager/module/index.ts';
|
||||||
|
import { eq } from 'drizzle-orm';
|
||||||
// import { mvAppFromUserAToUserB } from '@/routes/app-manager/admin/mv-user-app.ts';
|
// import { mvAppFromUserAToUserB } from '@/routes/app-manager/admin/mv-user-app.ts';
|
||||||
|
|
||||||
export const checkUsername = (username: string) => {
|
export const checkUsername = (username: string) => {
|
||||||
@@ -43,7 +44,7 @@ export const toChangeName = async (opts: { id: number; newName: string; admin?:
|
|||||||
data.canChangeUsername = false;
|
data.canChangeUsername = false;
|
||||||
user.data = data;
|
user.data = data;
|
||||||
try {
|
try {
|
||||||
await user.save({ fields: ['username', 'data'] });
|
await user.save();
|
||||||
// 迁移文件数据
|
// 迁移文件数据
|
||||||
await backupUserA(oldName, user.id); // 备份文件数据
|
await backupUserA(oldName, user.id); // 备份文件数据
|
||||||
await mvUserAToUserB(oldName, newName, true); // 迁移文件数据
|
await mvUserAToUserB(oldName, newName, true); // 迁移文件数据
|
||||||
@@ -53,7 +54,15 @@ export const toChangeName = async (opts: { id: number; newName: string; admin?:
|
|||||||
const type = user.type === 'org' ? 'org' : 'user';
|
const type = user.type === 'org' ? 'org' : 'user';
|
||||||
await User.clearUserToken(user.id, type); // 清除旧token
|
await User.clearUserToken(user.id, type); // 清除旧token
|
||||||
}
|
}
|
||||||
await AppModel.moveToNewUser(oldName, newName); // 更新用户数据
|
// 更新应用数据中的用户名
|
||||||
|
const apps = await db.select().from(schema.kvApp).where(eq(schema.kvApp.user, oldName));
|
||||||
|
for (const appItem of apps) {
|
||||||
|
const appData = appItem.data as any;
|
||||||
|
const newFiles = await AppHelper.getNewFiles(appData.files || [], { oldUser: oldName, newUser: newName });
|
||||||
|
await db.update(schema.kvApp)
|
||||||
|
.set({ user: newName, data: { ...appData, files: newFiles }, updatedAt: new Date().toISOString() })
|
||||||
|
.where(eq(schema.kvApp.id, appItem.id));
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('迁移文件数据失败', error);
|
console.error('迁移文件数据失败', error);
|
||||||
ctx.throw(500, 'Failed to change username');
|
ctx.throw(500, 'Failed to change username');
|
||||||
@@ -93,7 +102,7 @@ app
|
|||||||
ctx.throw(400, 'Username is required');
|
ctx.throw(400, 'Username is required');
|
||||||
}
|
}
|
||||||
checkUsername(username);
|
checkUsername(username);
|
||||||
const user = await User.findOne({ where: { username } });
|
const user = await User.findOne({ username });
|
||||||
|
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
id: user?.id,
|
id: user?.id,
|
||||||
@@ -138,7 +147,7 @@ app
|
|||||||
ctx.throw(400, 'Username is required');
|
ctx.throw(400, 'Username is required');
|
||||||
}
|
}
|
||||||
checkUsername(username);
|
checkUsername(username);
|
||||||
const findUserByUsername = await User.findOne({ where: { username } });
|
const findUserByUsername = await User.findOne({ username });
|
||||||
if (findUserByUsername) {
|
if (findUserByUsername) {
|
||||||
ctx.throw(400, 'Username already exists');
|
ctx.throw(400, 'Username already exists');
|
||||||
}
|
}
|
||||||
@@ -165,7 +174,7 @@ app
|
|||||||
if (!user) {
|
if (!user) {
|
||||||
ctx.throw(404, 'User not found');
|
ctx.throw(404, 'User not found');
|
||||||
}
|
}
|
||||||
await user.destroy();
|
await db.delete(schema.cfUser).where(eq(schema.cfUser.id, user.id));
|
||||||
backupUserA(user.username, user.id);
|
backupUserA(user.username, user.id);
|
||||||
deleteUser(user.username);
|
deleteUser(user.username);
|
||||||
// TODO: EXPIRE 删除token
|
// TODO: EXPIRE 删除token
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { app } from '@/app.ts';
|
import { app, db, schema } from '@/app.ts';
|
||||||
import { User } from '@/models/user.ts';
|
import { User } from '@/models/user.ts';
|
||||||
import { CustomError } from '@kevisual/router';
|
import { CustomError } from '@kevisual/router';
|
||||||
import { checkUsername } from './admin/user.ts';
|
import { checkUsername } from './admin/user.ts';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
|
import { sql } from 'drizzle-orm';
|
||||||
|
|
||||||
app
|
app
|
||||||
.route({
|
.route({
|
||||||
@@ -11,11 +12,15 @@ app
|
|||||||
middleware: ['auth'],
|
middleware: ['auth'],
|
||||||
})
|
})
|
||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
const users = await User.findAll({
|
const users = await db
|
||||||
attributes: ['id', 'username', 'description', 'needChangePassword'],
|
.select({
|
||||||
order: [['updatedAt', 'DESC']],
|
id: schema.cfUser.id,
|
||||||
logging: false,
|
username: schema.cfUser.username,
|
||||||
});
|
description: schema.cfUser.description,
|
||||||
|
needChangePassword: schema.cfUser.needChangePassword,
|
||||||
|
})
|
||||||
|
.from(schema.cfUser)
|
||||||
|
.orderBy(sql`${schema.cfUser.updatedAt} DESC`);
|
||||||
ctx.body = users;
|
ctx.body = users;
|
||||||
})
|
})
|
||||||
.addTo(app);
|
.addTo(app);
|
||||||
@@ -71,7 +76,7 @@ app
|
|||||||
throw new CustomError(400, 'username is required');
|
throw new CustomError(400, 'username is required');
|
||||||
}
|
}
|
||||||
checkUsername(username);
|
checkUsername(username);
|
||||||
const findUserByUsername = await User.findOne({ where: { username } });
|
const findUserByUsername = await User.findOne({ username });
|
||||||
if (findUserByUsername) {
|
if (findUserByUsername) {
|
||||||
throw new CustomError(400, 'username already exists');
|
throw new CustomError(400, 'username already exists');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ app
|
|||||||
if (userId) {
|
if (userId) {
|
||||||
user = await User.findByPk(userId);
|
user = await User.findByPk(userId);
|
||||||
} else if (username) {
|
} else if (username) {
|
||||||
user = await User.findOne({ where: { username } });
|
user = await User.findOne({ username });
|
||||||
}
|
}
|
||||||
if (!user) {
|
if (!user) {
|
||||||
ctx.throw('用户不存在');
|
ctx.throw('用户不存在');
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { app } from '@/app.ts';
|
import { app, db, schema } from '@/app.ts';
|
||||||
import { Org } from '@/models/org.ts';
|
import { Org } from '@/models/org.ts';
|
||||||
import { User } from '@/models/user.ts';
|
import { User } from '@/models/user.ts';
|
||||||
import { Op } from 'sequelize';
|
import { sql, eq } from 'drizzle-orm';
|
||||||
|
|
||||||
app
|
app
|
||||||
.route({
|
.route({
|
||||||
@@ -11,19 +11,11 @@ app
|
|||||||
})
|
})
|
||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
const tokenUser = ctx.state.tokenUser;
|
const tokenUser = ctx.state.tokenUser;
|
||||||
const list = await Org.findAll({
|
const list = await db
|
||||||
order: [['updatedAt', 'DESC']],
|
.select()
|
||||||
where: {
|
.from(schema.cfOrgs)
|
||||||
users: {
|
.where(sql`${schema.cfOrgs.users} @> ${JSON.stringify([{ uid: tokenUser.id }])}::jsonb`)
|
||||||
[Op.contains]: [
|
.orderBy(sql`${schema.cfOrgs.updatedAt} DESC`);
|
||||||
{
|
|
||||||
uid: tokenUser.id,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
logging: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
ctx.body = list;
|
ctx.body = list;
|
||||||
return ctx;
|
return ctx;
|
||||||
@@ -49,14 +41,16 @@ app
|
|||||||
ctx.throw('org not found');
|
ctx.throw('org not found');
|
||||||
}
|
}
|
||||||
org.description = description;
|
org.description = description;
|
||||||
await org.save();
|
await db.update(schema.cfOrgs).set({ description }).where(eq(schema.cfOrgs.id, org.id));
|
||||||
const user = await User.findOne({ where: { username } });
|
const user = await User.findOne({ username });
|
||||||
user.description = description;
|
if (user) {
|
||||||
await user.save();
|
user.description = description;
|
||||||
|
await user.save();
|
||||||
|
}
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
id: user.id,
|
id: user?.id,
|
||||||
username: user.username,
|
username: user?.username,
|
||||||
description: user.description,
|
description: user?.description,
|
||||||
};
|
};
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -100,11 +94,11 @@ app
|
|||||||
if (owner.uid !== tokenUser.id) {
|
if (owner.uid !== tokenUser.id) {
|
||||||
ctx.throw('Permission denied');
|
ctx.throw('Permission denied');
|
||||||
}
|
}
|
||||||
await org.destroy({ force: true });
|
await db.delete(schema.cfOrgs).where(eq(schema.cfOrgs.id, org.id));
|
||||||
const orgUser = await User.findOne({
|
const orgUser = await User.findOne({ username });
|
||||||
where: { username },
|
if (orgUser) {
|
||||||
});
|
await db.delete(schema.cfUser).where(eq(schema.cfUser.id, orgUser.id));
|
||||||
await orgUser.destroy({ force: true });
|
}
|
||||||
ctx.body = 'success';
|
ctx.body = 'success';
|
||||||
})
|
})
|
||||||
.addTo(app);
|
.addTo(app);
|
||||||
@@ -160,12 +154,7 @@ app
|
|||||||
};
|
};
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const usernameUser = await User.findOne({
|
const usernameUser = await User.findOne({ username });
|
||||||
where: { username },
|
|
||||||
attributes: {
|
|
||||||
exclude: ['password', 'salt'],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!usernameUser) {
|
if (!usernameUser) {
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Op } from 'sequelize';
|
|
||||||
import { User, UserSecret } from '@/models/user.ts';
|
import { User, UserSecret } from '@/models/user.ts';
|
||||||
import { app } from '@/app.ts';
|
import { app, db, schema } from '@/app.ts';
|
||||||
import { redis } from '@/app.ts';
|
import { redis } from '@/app.ts';
|
||||||
|
import { eq, and, or, like, isNull, sql } from 'drizzle-orm';
|
||||||
|
|
||||||
app
|
app
|
||||||
.route({
|
.route({
|
||||||
path: 'secret',
|
path: 'secret',
|
||||||
@@ -11,29 +12,63 @@ app
|
|||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
const tokenUser = ctx.state.tokenUser;
|
const tokenUser = ctx.state.tokenUser;
|
||||||
const { page = 1, pageSize = 100, search, sort = 'DESC', orgId, showToken = false } = ctx.query;
|
const { page = 1, pageSize = 100, search, sort = 'DESC', orgId, showToken = false } = ctx.query;
|
||||||
const searchWhere: Record<string, any> = search
|
|
||||||
? {
|
let conditions = [eq(schema.cfUserSecrets.userId, tokenUser.userId)];
|
||||||
[Op.or]: [{ title: { [Op.like]: `%${search}%` } }, { description: { [Op.like]: `%${search}%` } }],
|
|
||||||
}
|
if (search) {
|
||||||
: {};
|
conditions.push(
|
||||||
if (orgId) {
|
or(
|
||||||
searchWhere.orgId = orgId;
|
like(schema.cfUserSecrets.title, `%${search}%`),
|
||||||
} else {
|
like(schema.cfUserSecrets.description, `%${search}%`)
|
||||||
searchWhere.orgId = null;
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
const excludeFields = showToken ? [] : ['token'];
|
|
||||||
const { rows: secrets, count } = await UserSecret.findAndCountAll({
|
if (orgId) {
|
||||||
where: {
|
conditions.push(eq(schema.cfUserSecrets.orgId, orgId));
|
||||||
userId: tokenUser.userId,
|
} else {
|
||||||
...searchWhere,
|
conditions.push(isNull(schema.cfUserSecrets.orgId));
|
||||||
},
|
}
|
||||||
offset: (page - 1) * pageSize,
|
|
||||||
limit: pageSize,
|
const selectFields = showToken ? {
|
||||||
attributes: {
|
id: schema.cfUserSecrets.id,
|
||||||
exclude: excludeFields, // Exclude sensitive token field
|
userId: schema.cfUserSecrets.userId,
|
||||||
},
|
orgId: schema.cfUserSecrets.orgId,
|
||||||
order: [['updatedAt', sort]],
|
title: schema.cfUserSecrets.title,
|
||||||
});
|
description: schema.cfUserSecrets.description,
|
||||||
|
status: schema.cfUserSecrets.status,
|
||||||
|
expiredTime: schema.cfUserSecrets.expiredTime,
|
||||||
|
data: schema.cfUserSecrets.data,
|
||||||
|
token: schema.cfUserSecrets.token,
|
||||||
|
createdAt: schema.cfUserSecrets.createdAt,
|
||||||
|
updatedAt: schema.cfUserSecrets.updatedAt,
|
||||||
|
} : {
|
||||||
|
id: schema.cfUserSecrets.id,
|
||||||
|
userId: schema.cfUserSecrets.userId,
|
||||||
|
orgId: schema.cfUserSecrets.orgId,
|
||||||
|
title: schema.cfUserSecrets.title,
|
||||||
|
description: schema.cfUserSecrets.description,
|
||||||
|
status: schema.cfUserSecrets.status,
|
||||||
|
expiredTime: schema.cfUserSecrets.expiredTime,
|
||||||
|
data: schema.cfUserSecrets.data,
|
||||||
|
createdAt: schema.cfUserSecrets.createdAt,
|
||||||
|
updatedAt: schema.cfUserSecrets.updatedAt,
|
||||||
|
};
|
||||||
|
|
||||||
|
const secrets = await db
|
||||||
|
.select(selectFields)
|
||||||
|
.from(schema.cfUserSecrets)
|
||||||
|
.where(and(...conditions))
|
||||||
|
.orderBy(sort === 'DESC' ? sql`${schema.cfUserSecrets.updatedAt} DESC` : sql`${schema.cfUserSecrets.updatedAt} ASC`)
|
||||||
|
.limit(pageSize)
|
||||||
|
.offset((page - 1) * pageSize);
|
||||||
|
|
||||||
|
const countResult = await db
|
||||||
|
.select({ count: sql<number>`count(*)` })
|
||||||
|
.from(schema.cfUserSecrets)
|
||||||
|
.where(and(...conditions));
|
||||||
|
|
||||||
|
const count = Number(countResult[0]?.count || 0);
|
||||||
|
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
list: secrets,
|
list: secrets,
|
||||||
@@ -69,12 +104,14 @@ app
|
|||||||
ctx.throw(403, 'No permission');
|
ctx.throw(403, 'No permission');
|
||||||
}
|
}
|
||||||
} else if (title) {
|
} else if (title) {
|
||||||
secret = await UserSecret.findOne({
|
const secrets = await db
|
||||||
where: {
|
.select()
|
||||||
userId: tokenUser.userId,
|
.from(schema.cfUserSecrets)
|
||||||
title,
|
.where(and(eq(schema.cfUserSecrets.userId, tokenUser.userId), eq(schema.cfUserSecrets.title, title)))
|
||||||
},
|
.limit(1);
|
||||||
});
|
if (secrets.length > 0) {
|
||||||
|
secret = new UserSecret(secrets[0]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!secret) {
|
if (!secret) {
|
||||||
secret = await UserSecret.createSecret({
|
secret = await UserSecret.createSecret({
|
||||||
@@ -83,10 +120,12 @@ app
|
|||||||
});
|
});
|
||||||
isNew = true;
|
isNew = true;
|
||||||
}
|
}
|
||||||
if (secret) {
|
if (secret && Object.keys(rest).length > 0) {
|
||||||
secret = await secret.update({
|
await db
|
||||||
...rest,
|
.update(schema.cfUserSecrets)
|
||||||
});
|
.set({ ...rest, updatedAt: new Date().toISOString() })
|
||||||
|
.where(eq(schema.cfUserSecrets.id, secret.id));
|
||||||
|
secret = await UserSecret.findByPk(secret.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.body = secret;
|
ctx.body = secret;
|
||||||
@@ -112,12 +151,14 @@ app
|
|||||||
secret = await UserSecret.findByPk(id);
|
secret = await UserSecret.findByPk(id);
|
||||||
}
|
}
|
||||||
if (!secret && title) {
|
if (!secret && title) {
|
||||||
secret = await UserSecret.findOne({
|
const secrets = await db
|
||||||
where: {
|
.select()
|
||||||
userId: tokenUser.userId,
|
.from(schema.cfUserSecrets)
|
||||||
title,
|
.where(and(eq(schema.cfUserSecrets.userId, tokenUser.userId), eq(schema.cfUserSecrets.title, title)))
|
||||||
},
|
.limit(1);
|
||||||
});
|
if (secrets.length > 0) {
|
||||||
|
secret = new UserSecret(secrets[0]);
|
||||||
|
}
|
||||||
if (!secret) {
|
if (!secret) {
|
||||||
ctx.throw(404, 'Secret not found');
|
ctx.throw(404, 'Secret not found');
|
||||||
}
|
}
|
||||||
@@ -130,7 +171,7 @@ app
|
|||||||
ctx.throw(403, 'No permission');
|
ctx.throw(403, 'No permission');
|
||||||
}
|
}
|
||||||
|
|
||||||
await secret.destroy();
|
await db.delete(schema.cfUserSecrets).where(eq(schema.cfUserSecrets.id, secret.id));
|
||||||
ctx.body = secret;
|
ctx.body = secret;
|
||||||
})
|
})
|
||||||
.addTo(app);
|
.addTo(app);
|
||||||
@@ -154,12 +195,14 @@ app
|
|||||||
if (id) {
|
if (id) {
|
||||||
secret = await UserSecret.findByPk(id);
|
secret = await UserSecret.findByPk(id);
|
||||||
} else if (title) {
|
} else if (title) {
|
||||||
secret = await UserSecret.findOne({
|
const secrets = await db
|
||||||
where: {
|
.select()
|
||||||
userId: tokenUser.userId,
|
.from(schema.cfUserSecrets)
|
||||||
title,
|
.where(and(eq(schema.cfUserSecrets.userId, tokenUser.userId), eq(schema.cfUserSecrets.title, title)))
|
||||||
},
|
.limit(1);
|
||||||
});
|
if (secrets.length > 0) {
|
||||||
|
secret = new UserSecret(secrets[0]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!secret) {
|
if (!secret) {
|
||||||
@@ -194,23 +237,22 @@ app.route({
|
|||||||
ctx.body = 'success'
|
ctx.body = 'success'
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const user = await User.findOne({
|
const users = await db
|
||||||
where: {
|
.select()
|
||||||
data: {
|
.from(schema.cfUser)
|
||||||
wxUnionId: unionid
|
.where(sql`${schema.cfUser.data}->>'wxUnionId' = ${unionid}`)
|
||||||
}
|
.limit(1);
|
||||||
}
|
const user = users.length > 0 ? new User(users[0]) : null;
|
||||||
})
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
ctx.throw(404, '请关注公众号《人生可视化助手》后再操作');
|
ctx.throw(404, '请关注公众号《人生可视化助手》后再操作');
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let secretKey = await UserSecret.findOne({
|
const secretKeys = await db
|
||||||
where: {
|
.select()
|
||||||
userId: user.id,
|
.from(schema.cfUserSecrets)
|
||||||
title: 'wxmp-notify-token'
|
.where(and(eq(schema.cfUserSecrets.userId, user.id), eq(schema.cfUserSecrets.title, 'wxmp-notify-token')))
|
||||||
}
|
.limit(1);
|
||||||
});
|
let secretKey = secretKeys.length > 0 ? new UserSecret(secretKeys[0]) : null;
|
||||||
if (!secretKey) {
|
if (!secretKey) {
|
||||||
secretKey = await UserSecret.createSecret({ id: user.id, title: 'wxmp-notify-token' });
|
secretKey = await UserSecret.createSecret({ id: user.id, title: 'wxmp-notify-token' });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,18 +39,13 @@ app
|
|||||||
if (avatar) {
|
if (avatar) {
|
||||||
updateData.avatar = avatar;
|
updateData.avatar = avatar;
|
||||||
}
|
}
|
||||||
await user.update(
|
await user.update({
|
||||||
{
|
...updateData,
|
||||||
...updateData,
|
data: {
|
||||||
data: {
|
...user.data,
|
||||||
...user.data,
|
...data,
|
||||||
...data,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
});
|
||||||
fields: ['nickname', 'avatar', 'data'],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
user.setTokenUser(tokenUser);
|
user.setTokenUser(tokenUser);
|
||||||
ctx.body = await user.getInfo();
|
ctx.body = await user.getInfo();
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
import { program } from './program.ts';
|
|
||||||
|
|
||||||
//
|
|
||||||
import './scripts/change-user-pwd.ts';
|
|
||||||
import './scripts/list-app.ts';
|
|
||||||
|
|
||||||
//
|
|
||||||
|
|
||||||
program.parse(process.argv);
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { AppListModel } from '../routes/app-manager/module/index.ts';
|
import { db, schema } from '../app.ts';
|
||||||
|
|
||||||
const main = async () => {
|
const main = async () => {
|
||||||
const list = await AppListModel.findAll();
|
const list = await db.select().from(schema.kvAppList);
|
||||||
console.log(list.map((item) => item.key));
|
console.log(list.map((item) => item.key));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
import { ContainerData } from './routes/types.ts';
|
|
||||||
|
|
||||||
export { ContainerData };
|
|
||||||
Reference in New Issue
Block a user