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:
2026-02-07 01:26:16 +08:00
parent d62a75842f
commit 7dfa96d165
40 changed files with 1066 additions and 1171 deletions

View File

@@ -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) => {
const appList = await AppModel.findAll({
where: {
user: userA,
},
});
const appList = await db.select().from(schema.kvApp).where(eq(schema.kvApp.user, userA));
for (const app of appList) {
app.user = userB;
await app.save();
await db.update(schema.kvApp)
.set({ user: userB, updatedAt: new Date().toISOString() })
.where(eq(schema.kvApp.id, app.id));
}
};

View File

@@ -1,6 +1,8 @@
import { app } from '@/app.ts';
import { AppModel } from '../module/app.ts';
import { AppDomainModel } from '../module/app-domain.ts';
import { app, db, schema } from '@/app.ts';
import { App, AppData } from '../module/app-drizzle.ts';
import { AppDomain, AppDomainHelper } from '../module/app-domain-drizzle.ts';
import { eq, and } from 'drizzle-orm';
import { randomUUID } from 'crypto';
app
.route({
@@ -9,17 +11,17 @@ app
})
.define(async (ctx) => {
const { domain } = ctx.query.data;
// const query = {
// }
const domainInfo = await AppDomainModel.findOne({ where: { domain } });
const domainInfos = await db.select().from(schema.kvAppDomain).where(eq(schema.kvAppDomain.domain, domain)).limit(1);
const domainInfo = domainInfos[0];
if (!domainInfo || !domainInfo.appId) {
ctx.throw(404, 'app not found');
}
const app = await AppModel.findByPk(domainInfo.appId);
if (!app) {
const apps = await db.select().from(schema.kvApp).where(eq(schema.kvApp.id, domainInfo.appId)).limit(1);
const appFound = apps[0];
if (!appFound) {
ctx.throw(404, 'app not found');
}
ctx.body = app;
ctx.body = appFound;
return ctx;
})
.addTo(app);
@@ -37,7 +39,8 @@ app
if (!domain || !appId) {
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;
return ctx;
})
@@ -59,12 +62,17 @@ app
if (!status) {
ctx.throw(400, 'status is required');
}
let domainInfo: AppDomainModel | null = null;
let domainInfo: AppDomain | undefined;
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) {
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) {
ctx.throw(404, 'domain not found');
@@ -72,19 +80,23 @@ app
if (domainInfo.uid !== uid) {
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');
}
const updateData: any = {};
if (status) {
domainInfo.status = status;
updateData.status = status;
}
if (appId) {
domainInfo.appId = appId;
updateData.appId = appId;
}
await domainInfo.save({ fields: ['status', 'appId'] });
ctx.body = domainInfo;
updateData.updatedAt = new Date().toISOString();
const updateResult = await db.update(schema.kvAppDomain)
.set(updateData)
.where(eq(schema.kvAppDomain.id, domainInfo.id))
.returning();
const updatedDomain = updateResult[0];
ctx.body = updatedDomain;
return ctx;
})
.addTo(app);

View File

@@ -1,7 +1,9 @@
import { app } from '@/app.ts';
import { AppDomainModel } from '../module/app-domain.ts';
import { AppModel } from '../module/app.ts';
import { app, db, schema } from '@/app.ts';
import { AppDomain, AppDomainHelper } from '../module/app-domain-drizzle.ts';
import { App } from '../module/app-drizzle.ts';
import { CustomError } from '@kevisual/router';
import { eq, or } from 'drizzle-orm';
import { randomUUID } from 'crypto';
app
.route({
@@ -11,10 +13,12 @@ app
})
.define(async (ctx) => {
const { page = 1, pageSize = 999 } = ctx.query.data || {};
const { count, rows } = await AppDomainModel.findAndCountAll({
offset: (page - 1) * pageSize,
limit: pageSize,
});
const offset = (page - 1) * pageSize;
const rows = await db.select().from(schema.kvAppDomain)
.limit(pageSize)
.offset(offset);
const countResult = await db.select().from(schema.kvAppDomain);
const count = countResult.length;
ctx.body = { count, list: rows, pagination: { page, pageSize } };
return ctx;
})
@@ -31,11 +35,10 @@ app
if (!domain) {
ctx.throw(400, 'domain is required');
}
let domainInfo: AppDomainModel;
let domainInfo: AppDomain | undefined;
if (id) {
domainInfo = await AppDomainModel.findByPk(id);
} else {
domainInfo = await AppDomainModel.create({ domain });
const domains = await db.select().from(schema.kvAppDomain).where(eq(schema.kvAppDomain.id, id)).limit(1);
domainInfo = domains[0];
}
const checkAppId = async () => {
const isUUID = (id: string) => {
@@ -45,7 +48,8 @@ app
if (!isUUID(rest.appId)) {
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) {
ctx.throw(400, 'appId is not exist');
}
@@ -53,24 +57,31 @@ app
};
try {
if (!domainInfo) {
domainInfo = await AppDomainModel.create({ domain, data: {}, ...rest });
await checkAppId();
const newDomains = await db.insert(schema.kvAppDomain).values({ id: randomUUID(), domain, data: {}, ...rest }).returning();
domainInfo = newDomains[0];
} else {
if (rest.status && domainInfo.status !== rest.status) {
await domainInfo.clearCache();
await AppDomainHelper.clearCache(domainInfo.domain!);
}
await checkAppId();
await domainInfo.update({
domain,
data: {
...domainInfo.data,
...data,
},
...rest,
});
const domainData = domainInfo.data as any;
const updateResult = await db.update(schema.kvAppDomain)
.set({
domain,
data: {
...domainData,
...data,
},
...rest,
updatedAt: new Date().toISOString()
})
.where(eq(schema.kvAppDomain.id, domainInfo.id))
.returning();
domainInfo = updateResult[0];
}
ctx.body = domainInfo;
} catch (error) {
} catch (error: any) {
if (error.code) {
ctx.throw(error.code, error.message);
}
@@ -94,9 +105,9 @@ app
ctx.throw(400, 'id or domain is required');
}
if (id) {
await AppDomainModel.destroy({ where: { id }, force: true });
await db.delete(schema.kvAppDomain).where(eq(schema.kvAppDomain.id, id));
} 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' };
@@ -115,7 +126,8 @@ app
if (!id && !domain) {
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) {
ctx.throw(404, 'domain not found');
}

View File

@@ -1,12 +1,14 @@
import { App, CustomError } from '@kevisual/router';
import { AppModel, AppListModel } from './module/index.ts';
import { app, redis } from '@/app.ts';
import { App as AppType, AppList, AppData, AppHelper } from './module/app-drizzle.ts';
import { app, redis, db, schema } from '@/app.ts';
import { uniqBy } from 'es-toolkit';
import { getUidByUsername, prefixFix } from './util.ts';
import { deleteFiles, getMinioListAndSetToAppList } from '../file/index.ts';
import { setExpire } from './revoke.ts';
import { User } from '@/models/user.ts';
import { callDetectAppVersion } from './export.ts';
import { eq, and, desc } from 'drizzle-orm';
import { randomUUID } from 'crypto';
app
.route({
path: 'app',
@@ -20,14 +22,13 @@ app
if (!data.key) {
throw new CustomError('key is required');
}
const list = await AppListModel.findAll({
order: [['updatedAt', 'DESC']],
where: {
uid: tokenUser.id,
key: data.key,
},
logging: false,
});
const list = await db.select()
.from(schema.kvAppList)
.where(and(
eq(schema.kvAppList.uid, tokenUser.id),
eq(schema.kvAppList.key, data.key)
))
.orderBy(desc(schema.kvAppList.updatedAt));
ctx.body = list.map((item) => prefixFix(item, tokenUser.username));
return ctx;
})
@@ -48,33 +49,35 @@ app
if (!id && (!key || !version)) {
throw new CustomError('id is required');
}
let appListModel: AppListModel;
let appListModel: AppList | undefined;
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) {
appListModel = await AppListModel.findOne({
where: {
key,
version,
uid: tokenUser.id,
},
});
const apps = await db.select().from(schema.kvAppList).where(and(
eq(schema.kvAppList.key, key),
eq(schema.kvAppList.version, version),
eq(schema.kvAppList.uid, tokenUser.id)
)).limit(1);
appListModel = apps[0];
}
if (!appListModel && create) {
appListModel = await AppListModel.create({
const newApps = await db.insert(schema.kvAppList).values({
id: randomUUID(),
key,
version,
uid: tokenUser.id,
data: {},
});
const appModel = await AppModel.findOne({
where: {
key,
uid: tokenUser.id,
},
});
}).returning();
appListModel = newApps[0];
const appModels = await db.select().from(schema.kvApp).where(and(
eq(schema.kvApp.key, key),
eq(schema.kvApp.uid, tokenUser.id)
)).limit(1);
const appModel = appModels[0];
if (!appModel) {
await AppModel.create({
await db.insert(schema.kvApp).values({
id: randomUUID(),
key,
uid: tokenUser.id,
user: tokenUser.username,
@@ -88,13 +91,12 @@ app
if (res.code !== 200) {
ctx.throw(res.message || 'detect version list error');
}
appListModel = await AppListModel.findOne({
where: {
key,
version,
uid: tokenUser.id,
},
});
const apps2 = await db.select().from(schema.kvAppList).where(and(
eq(schema.kvAppList.key, key),
eq(schema.kvAppList.version, version),
eq(schema.kvAppList.uid, tokenUser.id)
)).limit(1);
appListModel = apps2[0];
}
if (!appListModel) {
ctx.throw('app not found');
@@ -115,10 +117,16 @@ app
const tokenUser = ctx.state.tokenUser;
const { data, id, ...rest } = ctx.query.data;
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) {
const newData = { ...app.data, ...data };
const newApp = await app.update({ data: newData, ...rest });
const appData = app.data as AppData;
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;
setExpire(newApp.id, 'test');
} else {
@@ -130,8 +138,8 @@ app
if (!rest.key) {
throw new CustomError('key is required');
}
const app = await AppListModel.create({ data, ...rest, uid: tokenUser.id });
ctx.body = app;
const newApps = await db.insert(schema.kvAppList).values({ id: randomUUID(), data, ...rest, uid: tokenUser.id }).returning();
ctx.body = newApps[0];
return ctx;
})
.addTo(app);
@@ -149,24 +157,28 @@ app
if (!id) {
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) {
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) {
throw new CustomError('app not found');
}
if (am.version === app.version) {
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) {
await deleteFiles(files.map((item) => item.path));
}
await app.destroy({
force: true,
});
await db.delete(schema.kvAppList).where(eq(schema.kvAppList.id, id));
ctx.body = 'success';
return ctx;
})
@@ -205,11 +217,16 @@ app
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;
if (!am) {
appIsNew = true;
am = await AppModel.create({
const newAms = await db.insert(schema.kvApp).values({
id: randomUUID(),
user: userPrefix,
key: appKey,
uid,
@@ -220,24 +237,40 @@ app
data: {
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) {
app = await AppListModel.create({
const newApps = await db.insert(schema.kvAppList).values({
id: randomUUID(),
key: appKey,
version,
uid: uid,
data: {
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 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) {
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');
ctx.body = prefixFix(res, userPrefix);
@@ -263,9 +296,10 @@ app
}
const uid = await getUidByUsername(app, ctx, username);
let appList: AppListModel | null = null;
let appList: AppList | undefined = undefined;
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) {
ctx.throw('no permission');
}
@@ -274,7 +308,12 @@ app
if (!version) {
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) {
ctx.throw('app 未发现');
@@ -287,18 +326,27 @@ app
if (res.code !== 200) {
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) {
ctx.throw('app 未发现');
}
const files = appList.data.files || [];
const am = await AppModel.findOne({ where: { key: appList.key, uid: uid } });
const appListData = appList.data as AppData;
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) {
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);
ctx.body = {
key: appList.key,
@@ -317,11 +365,16 @@ app
})
.define(async (ctx) => {
const { user, key, id } = ctx.query.data;
let app;
let app: AppType | undefined;
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) {
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 {
throw new CustomError('user or key is required');
}
@@ -364,16 +417,23 @@ app
throw new CustomError('appKey and version are required');
}
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) {
appList = await AppListModel.create({
const newAppLists = await db.insert(schema.kvAppList).values({
id: randomUUID(),
key: appKey,
version,
uid,
data: {
files: [],
},
});
}).returning();
appList = newAppLists[0];
}
const checkUsername = username || tokenUser.username;
const files = await getMinioListAndSetToAppList({ username: checkUsername, appKey, version });
@@ -383,7 +443,8 @@ app
path: item.name,
};
});
let appListFiles = appList.data?.files || [];
const appListData = appList.data as AppData;
let appListFiles = appListData?.files || [];
const needAddFiles = newFiles.map((item) => {
const findFile = appListFiles.find((appListFile) => appListFile.name === item.name);
if (findFile && findFile.name === item.name) {
@@ -391,11 +452,20 @@ app
}
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');
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) {
am = await AppModel.create({
const newAms = await db.insert(schema.kvApp).values({
id: randomUUID(),
title: appKey,
key: appKey,
version: version || '0.0.1',
@@ -403,11 +473,19 @@ app
uid,
data: { files: needAddFiles },
proxy: appKey.includes('center') ? false : true,
});
}).returning();
am = newAms[0];
} 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) {
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);
}
}

View 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);
}
}
}

View File

@@ -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,
},
);

View 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;
}
}

View File

@@ -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);
// });

View File

@@ -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);
// });

View File

@@ -1,2 +1,3 @@
export * from './app-list.ts';
export * from './app.ts';
// Drizzle 模型(推荐使用)
export * from './app-domain-drizzle.ts'
export * from './app-drizzle.ts'

View File

@@ -1,6 +1,6 @@
import { app } from '@/app.ts';
import { AppModel } from '../module/index.ts';
import { app, db, schema } from '@/app.ts';
import { ConfigPermission } from '@kevisual/permission';
import { eq, desc, asc } from 'drizzle-orm';
// curl http://localhost:4005/api/router?path=app&key=public-list
// TODO:
@@ -11,23 +11,20 @@ app
})
.define(async (ctx) => {
const { username = 'root', status = 'running', page = 1, pageSize = 100, order = 'DESC' } = ctx.query.data || {};
const { rows, count } = await AppModel.findAndCountAll({
where: {
status,
user: username,
},
attributes: {
exclude: [],
},
order: [['updatedAt', order]],
limit: pageSize,
offset: (page - 1) * pageSize,
distinct: true,
logging: false,
});
const offset = (page - 1) * pageSize;
const apps = await db.select().from(schema.kvApp)
.where(eq(schema.kvApp.user, username))
.orderBy(order === 'DESC' ? desc(schema.kvApp.updatedAt) : asc(schema.kvApp.updatedAt))
.limit(pageSize)
.offset(offset);
// Note: Drizzle doesn't have a direct equivalent to findAndCountAll
// We need to do a separate count query
const countResult = await db.select({ count: schema.kvApp.id }).from(schema.kvApp)
.where(eq(schema.kvApp.user, username));
const count = countResult.length;
ctx.body = {
list: rows.map((item) => {
return ConfigPermission.getDataPublicPermission(item.toJSON());
list: apps.map((item) => {
return ConfigPermission.getDataPublicPermission(item);
}),
pagination: {
total: count,

View File

@@ -1,9 +1,7 @@
import { app } from '@/app.ts';
import { AppModel } from '../module/index.ts';
import { AppListModel } from '../module/index.ts';
import { app, db, schema } from '@/app.ts';
import { randomUUID } from 'crypto';
import { oss } from '@/app.ts';
import { User } from '@/models/user.ts';
import { permission } from 'process';
import { customAlphabet } from 'nanoid';
import dayjs from 'dayjs';
@@ -65,7 +63,8 @@ app
path: urlPath,
},
];
const appModel = await AppModel.create({
const appModels = await db.insert(schema.kvApp).values({
id: randomUUID(),
title,
description,
version,
@@ -80,15 +79,18 @@ app
},
files: files,
},
});
const appVersionModel = await AppListModel.create({
}).returning();
const appModel = appModels[0];
const appVersionModels = await db.insert(schema.kvAppList).values({
id: randomUUID(),
data: {
files: files,
},
version: appModel.version,
key: appModel.key,
uid: appModel.uid,
});
}).returning();
const appVersionModel = appVersionModels[0];
ctx.body = {
url: `/${username}/${key}/`,

View File

@@ -1,7 +1,8 @@
import { AppModel, AppListModel } from './module/index.ts';
import { app } from '@/app.ts';
import { App, AppList, AppData, AppHelper } from './module/app-drizzle.ts';
import { app, db, schema } from '@/app.ts';
import { setExpire } from './revoke.ts';
import { deleteFileByPrefix } from '../file/index.ts';
import { eq, and, desc } from 'drizzle-orm';
app
.route({
@@ -12,15 +13,24 @@ app
})
.define(async (ctx) => {
const tokenUser = ctx.state.tokenUser;
const list = await AppModel.findAll({
order: [['updatedAt', 'DESC']],
where: {
uid: tokenUser.id,
},
attributes: {
exclude: ['data'],
},
});
const list = await db.select({
id: schema.kvApp.id,
title: schema.kvApp.title,
description: schema.kvApp.description,
version: schema.kvApp.version,
key: schema.kvApp.key,
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;
return ctx;
})
@@ -40,14 +50,18 @@ app
if (!id && !key) {
ctx.throw(500, 'id is required');
}
let am: AppModel;
let am: App | undefined;
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) {
ctx.throw(500, 'app not found');
}
} 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) {
ctx.throw(500, 'app not found');
}
@@ -71,21 +85,27 @@ app
const { data, id, user, ...rest } = ctx.query.data;
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) {
const newData = { ...app.data, ...data };
const appData = app.data as AppData;
const newData = { ...appData, ...data };
if (app.user !== tokenUser.username) {
rest.user = tokenUser.username;
let files = newData?.files || [];
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;
}
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;
if (app.status !== 'running' || data?.share || rest?.status) {
setExpire(newApp.key, app.user);
setExpire(newApp.key!, app.user!);
}
} else {
ctx.throw(500, 'app not found');
@@ -95,17 +115,19 @@ app
if (!rest.key) {
ctx.throw(500, 'key is required');
}
const findApp = await AppModel.findOne({ where: { key: rest.key, uid: tokenUser.id } });
if (findApp) {
const findApps = await db.select().from(schema.kvApp)
.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');
}
const app = await AppModel.create({
const newApps = await db.insert(schema.kvApp).values({
data: { files: [] },
...rest,
uid: tokenUser.id,
user: tokenUser.username,
});
ctx.body = app;
}).returning();
ctx.body = newApps[0];
return ctx;
})
.addTo(app);
@@ -124,16 +146,18 @@ app
if (!id) {
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) {
ctx.throw(500, 'app not found');
}
if (am.uid !== tokenUser.id) {
ctx.throw(500, 'app not found');
}
const list = await AppListModel.findAll({ where: { key: am.key, uid: tokenUser.id } });
await am.destroy({ force: true });
await Promise.all(list.map((item) => item.destroy({ force: true })));
const list = await db.select().from(schema.kvAppList)
.where(and(eq(schema.kvAppList.key, am.key!), eq(schema.kvAppList.uid, tokenUser.id)));
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) {
const username = tokenUser.username;
await deleteFileByPrefix(`${username}/${am.key}`);
@@ -154,13 +178,13 @@ app
if (!id) {
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) {
ctx.throw(500, 'app not found');
}
const amJson = am.toJSON();
ctx.body = {
...amJson,
...am,
proxy: true,
};
})

View File

@@ -1 +1 @@
import './list.ts';
// import './list.ts';

View File

@@ -1,107 +1,107 @@
import { Op } from 'sequelize';
import { app } from '@/app.ts';
import { FileSyncModel } from './model.ts';
app
.route({
path: 'file-listener',
key: 'list',
middleware: ['auth'],
description: '获取用户的某一个文件夹下的所有的列表的数据',
})
.define(async (ctx) => {
const tokenUser = ctx.state.tokenUser;
const username = tokenUser.username;
const { page = 1, pageSize = 20, sort = 'DESC' } = ctx.query;
let { prefix } = ctx.query;
if (prefix) {
if (typeof prefix !== 'string') {
ctx.throw(400, 'prefix must be a string');
}
if (prefix.startsWith('/')) {
prefix = prefix.slice(1); // Remove leading slash if present
}
if (!prefix.startsWith(username + '/')) {
ctx.throw(400, 'prefix must start with the your username:', username);
}
}
const searchWhere = prefix
? {
[Op.or]: [{ name: { [Op.like]: `${prefix}%` } }],
}
: {};
// import { Op } from 'sequelize';
// import { app } from '@/app.ts';
// import { FileSyncModel } from './model.ts';
// app
// .route({
// path: 'file-listener',
// key: 'list',
// middleware: ['auth'],
// description: '获取用户的某一个文件夹下的所有的列表的数据',
// })
// .define(async (ctx) => {
// const tokenUser = ctx.state.tokenUser;
// const username = tokenUser.username;
// const { page = 1, pageSize = 20, sort = 'DESC' } = ctx.query;
// let { prefix } = ctx.query;
// if (prefix) {
// if (typeof prefix !== 'string') {
// ctx.throw(400, 'prefix must be a string');
// }
// if (prefix.startsWith('/')) {
// prefix = prefix.slice(1); // Remove leading slash if present
// }
// if (!prefix.startsWith(username + '/')) {
// ctx.throw(400, 'prefix must start with the your username:', username);
// }
// }
// const searchWhere = prefix
// ? {
// [Op.or]: [{ name: { [Op.like]: `${prefix}%` } }],
// }
// : {};
const { rows: files, count } = await FileSyncModel.findAndCountAll({
where: {
...searchWhere,
},
offset: (page - 1) * pageSize,
limit: pageSize,
order: [['updatedAt', sort]],
});
const getPublicFiles = (files: FileSyncModel[]) => {
return files.map((file) => {
const value = file.toJSON();
const stat = value.stat || {};
delete stat.password;
return {
...value,
stat: stat,
};
});
};
// const { rows: files, count } = await FileSyncModel.findAndCountAll({
// where: {
// ...searchWhere,
// },
// offset: (page - 1) * pageSize,
// limit: pageSize,
// order: [['updatedAt', sort]],
// });
// const getPublicFiles = (files: FileSyncModel[]) => {
// return files.map((file) => {
// const value = file.toJSON();
// const stat = value.stat || {};
// delete stat.password;
// return {
// ...value,
// stat: stat,
// };
// });
// };
ctx.body = {
list: getPublicFiles(files),
pagination: {
page,
current: page,
pageSize,
total: count,
},
};
})
.addTo(app);
// ctx.body = {
// list: getPublicFiles(files),
// pagination: {
// page,
// current: page,
// pageSize,
// total: count,
// },
// };
// })
// .addTo(app);
app
.route({
path: 'file-listener',
key: 'get',
middleware: ['auth'],
})
.define(async (ctx) => {
const tokenUser = ctx.state.tokenUser;
const username = tokenUser.username;
const { id, name, hash } = ctx.query.data || {};
// app
// .route({
// path: 'file-listener',
// key: 'get',
// middleware: ['auth'],
// })
// .define(async (ctx) => {
// const tokenUser = ctx.state.tokenUser;
// const username = tokenUser.username;
// const { id, name, hash } = ctx.query.data || {};
if (!id && !name && !hash) {
ctx.throw(400, 'id, name or hash is required');
}
let fileSync: FileSyncModel | null = null;
if (id) {
fileSync = await FileSyncModel.findByPk(id);
}
if (name && !fileSync) {
fileSync = await FileSyncModel.findOne({
where: {
name,
hash,
},
});
}
if (!fileSync && hash) {
fileSync = await FileSyncModel.findOne({
where: {
name: {
[Op.like]: `${username}/%`,
},
hash,
},
});
}
// if (!id && !name && !hash) {
// ctx.throw(400, 'id, name or hash is required');
// }
// let fileSync: FileSyncModel | null = null;
// if (id) {
// fileSync = await FileSyncModel.findByPk(id);
// }
// if (name && !fileSync) {
// fileSync = await FileSyncModel.findOne({
// where: {
// name,
// hash,
// },
// });
// }
// if (!fileSync && hash) {
// fileSync = await FileSyncModel.findOne({
// where: {
// name: {
// [Op.like]: `${username}/%`,
// },
// hash,
// },
// });
// }
if (!fileSync || !fileSync.name.startsWith(`${username}/`)) {
ctx.throw(404, 'NotFoundFile');
}
ctx.body = fileSync;
})
.addTo(app);
// if (!fileSync || !fileSync.name.startsWith(`${username}/`)) {
// ctx.throw(404, 'NotFoundFile');
// }
// ctx.body = fileSync;
// })
// .addTo(app);

View File

@@ -1,3 +1,3 @@
import { FileSyncModel } from '@kevisual/file-listener/src/file-sync/model.ts';
import type { FileSyncModelType } from '@kevisual/file-listener/src/file-sync/model.ts';
export { FileSyncModel, FileSyncModelType };
// import { FileSyncModel } from '@kevisual/file-listener/src/file-sync/model.ts';
// import type { FileSyncModelType } from '@kevisual/file-listener/src/file-sync/model.ts';
// export { FileSyncModel, FileSyncModelType };

View File

@@ -8,8 +8,6 @@ import './micro-app/index.ts';
import './config/index.ts';
// import './file-listener/index.ts';
import './mark/index.ts';
import './light-code/index.ts';

View File

@@ -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 { manager } from './manager-app.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
// 把对应的应用安装到系统的apps目录下并解压然后把配置项写入数据库配置
// key 是应用的唯一标识和package.json中的key一致绑定关系
@@ -26,17 +27,17 @@ app
if (data.username && username === 'admin') {
username = data.username;
}
let microApp: AppListModel;
let microApp: AppList | undefined;
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) {
microApp = await AppListModel.findOne({
where: {
key: postAppKey,
version: postVersion,
},
});
const apps = await db.select().from(schema.kvAppList).where(and(
eq(schema.kvAppList.key, postAppKey),
eq(schema.kvAppList.version, postVersion)
)).limit(1);
microApp = apps[0];
}
if (!microApp) {

View File

@@ -5,7 +5,6 @@ import path from 'path';
const assistantAppsConfig = path.join(process.cwd(), 'assistant-apps-config.json');
const isExist = fileIsExist(assistantAppsConfig);
export const existDenpend = [
'sequelize', // commonjs
'pg', // commonjs
'@kevisual/router', // 共享模块
'ioredis', // commonjs

View File

@@ -1 +0,0 @@
export * from '../old-apps/container/type.ts'

View File

@@ -1,9 +1,10 @@
import { app } from '@/app.ts';
import { app, db, schema } from '@/app.ts';
import { User } from '@/models/user.ts';
import { nanoid } from 'nanoid';
import { CustomError } from '@kevisual/router';
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';
export const checkUsername = (username: string) => {
@@ -43,7 +44,7 @@ export const toChangeName = async (opts: { id: number; newName: string; admin?:
data.canChangeUsername = false;
user.data = data;
try {
await user.save({ fields: ['username', 'data'] });
await user.save();
// 迁移文件数据
await backupUserA(oldName, user.id); // 备份文件数据
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';
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) {
console.error('迁移文件数据失败', error);
ctx.throw(500, 'Failed to change username');
@@ -93,7 +102,7 @@ app
ctx.throw(400, 'Username is required');
}
checkUsername(username);
const user = await User.findOne({ where: { username } });
const user = await User.findOne({ username });
ctx.body = {
id: user?.id,
@@ -138,7 +147,7 @@ app
ctx.throw(400, 'Username is required');
}
checkUsername(username);
const findUserByUsername = await User.findOne({ where: { username } });
const findUserByUsername = await User.findOne({ username });
if (findUserByUsername) {
ctx.throw(400, 'Username already exists');
}
@@ -165,7 +174,7 @@ app
if (!user) {
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);
deleteUser(user.username);
// TODO: EXPIRE 删除token

View File

@@ -1,8 +1,9 @@
import { app } from '@/app.ts';
import { app, db, schema } from '@/app.ts';
import { User } from '@/models/user.ts';
import { CustomError } from '@kevisual/router';
import { checkUsername } from './admin/user.ts';
import { nanoid } from 'nanoid';
import { sql } from 'drizzle-orm';
app
.route({
@@ -11,11 +12,15 @@ app
middleware: ['auth'],
})
.define(async (ctx) => {
const users = await User.findAll({
attributes: ['id', 'username', 'description', 'needChangePassword'],
order: [['updatedAt', 'DESC']],
logging: false,
});
const users = await db
.select({
id: schema.cfUser.id,
username: schema.cfUser.username,
description: schema.cfUser.description,
needChangePassword: schema.cfUser.needChangePassword,
})
.from(schema.cfUser)
.orderBy(sql`${schema.cfUser.updatedAt} DESC`);
ctx.body = users;
})
.addTo(app);
@@ -71,7 +76,7 @@ app
throw new CustomError(400, 'username is required');
}
checkUsername(username);
const findUserByUsername = await User.findOne({ where: { username } });
const findUserByUsername = await User.findOne({ username });
if (findUserByUsername) {
throw new CustomError(400, 'username already exists');
}

View File

@@ -39,7 +39,7 @@ app
if (userId) {
user = await User.findByPk(userId);
} else if (username) {
user = await User.findOne({ where: { username } });
user = await User.findOne({ username });
}
if (!user) {
ctx.throw('用户不存在');

View File

@@ -1,7 +1,7 @@
import { app } from '@/app.ts';
import { app, db, schema } from '@/app.ts';
import { Org } from '@/models/org.ts';
import { User } from '@/models/user.ts';
import { Op } from 'sequelize';
import { sql, eq } from 'drizzle-orm';
app
.route({
@@ -11,19 +11,11 @@ app
})
.define(async (ctx) => {
const tokenUser = ctx.state.tokenUser;
const list = await Org.findAll({
order: [['updatedAt', 'DESC']],
where: {
users: {
[Op.contains]: [
{
uid: tokenUser.id,
},
],
},
},
logging: false,
});
const list = await db
.select()
.from(schema.cfOrgs)
.where(sql`${schema.cfOrgs.users} @> ${JSON.stringify([{ uid: tokenUser.id }])}::jsonb`)
.orderBy(sql`${schema.cfOrgs.updatedAt} DESC`);
ctx.body = list;
return ctx;
@@ -49,14 +41,16 @@ app
ctx.throw('org not found');
}
org.description = description;
await org.save();
const user = await User.findOne({ where: { username } });
user.description = description;
await user.save();
await db.update(schema.cfOrgs).set({ description }).where(eq(schema.cfOrgs.id, org.id));
const user = await User.findOne({ username });
if (user) {
user.description = description;
await user.save();
}
ctx.body = {
id: user.id,
username: user.username,
description: user.description,
id: user?.id,
username: user?.username,
description: user?.description,
};
return;
}
@@ -100,11 +94,11 @@ app
if (owner.uid !== tokenUser.id) {
ctx.throw('Permission denied');
}
await org.destroy({ force: true });
const orgUser = await User.findOne({
where: { username },
});
await orgUser.destroy({ force: true });
await db.delete(schema.cfOrgs).where(eq(schema.cfOrgs.id, org.id));
const orgUser = await User.findOne({ username });
if (orgUser) {
await db.delete(schema.cfUser).where(eq(schema.cfUser.id, orgUser.id));
}
ctx.body = 'success';
})
.addTo(app);
@@ -160,12 +154,7 @@ app
};
return;
}
const usernameUser = await User.findOne({
where: { username },
attributes: {
exclude: ['password', 'salt'],
},
});
const usernameUser = await User.findOne({ username });
if (!usernameUser) {
ctx.body = {

View File

@@ -1,7 +1,8 @@
import { Op } from 'sequelize';
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 { eq, and, or, like, isNull, sql } from 'drizzle-orm';
app
.route({
path: 'secret',
@@ -11,29 +12,63 @@ app
.define(async (ctx) => {
const tokenUser = ctx.state.tokenUser;
const { page = 1, pageSize = 100, search, sort = 'DESC', orgId, showToken = false } = ctx.query;
const searchWhere: Record<string, any> = search
? {
[Op.or]: [{ title: { [Op.like]: `%${search}%` } }, { description: { [Op.like]: `%${search}%` } }],
}
: {};
if (orgId) {
searchWhere.orgId = orgId;
} else {
searchWhere.orgId = null;
let conditions = [eq(schema.cfUserSecrets.userId, tokenUser.userId)];
if (search) {
conditions.push(
or(
like(schema.cfUserSecrets.title, `%${search}%`),
like(schema.cfUserSecrets.description, `%${search}%`)
)
);
}
const excludeFields = showToken ? [] : ['token'];
const { rows: secrets, count } = await UserSecret.findAndCountAll({
where: {
userId: tokenUser.userId,
...searchWhere,
},
offset: (page - 1) * pageSize,
limit: pageSize,
attributes: {
exclude: excludeFields, // Exclude sensitive token field
},
order: [['updatedAt', sort]],
});
if (orgId) {
conditions.push(eq(schema.cfUserSecrets.orgId, orgId));
} else {
conditions.push(isNull(schema.cfUserSecrets.orgId));
}
const selectFields = showToken ? {
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,
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 = {
list: secrets,
@@ -69,12 +104,14 @@ app
ctx.throw(403, 'No permission');
}
} else if (title) {
secret = await UserSecret.findOne({
where: {
userId: tokenUser.userId,
title,
},
});
const secrets = await db
.select()
.from(schema.cfUserSecrets)
.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) {
secret = await UserSecret.createSecret({
@@ -83,10 +120,12 @@ app
});
isNew = true;
}
if (secret) {
secret = await secret.update({
...rest,
});
if (secret && Object.keys(rest).length > 0) {
await db
.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;
@@ -112,12 +151,14 @@ app
secret = await UserSecret.findByPk(id);
}
if (!secret && title) {
secret = await UserSecret.findOne({
where: {
userId: tokenUser.userId,
title,
},
});
const secrets = await db
.select()
.from(schema.cfUserSecrets)
.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) {
ctx.throw(404, 'Secret not found');
}
@@ -130,7 +171,7 @@ app
ctx.throw(403, 'No permission');
}
await secret.destroy();
await db.delete(schema.cfUserSecrets).where(eq(schema.cfUserSecrets.id, secret.id));
ctx.body = secret;
})
.addTo(app);
@@ -154,12 +195,14 @@ app
if (id) {
secret = await UserSecret.findByPk(id);
} else if (title) {
secret = await UserSecret.findOne({
where: {
userId: tokenUser.userId,
title,
},
});
const secrets = await db
.select()
.from(schema.cfUserSecrets)
.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) {
@@ -194,23 +237,22 @@ app.route({
ctx.body = 'success'
return;
}
const user = await User.findOne({
where: {
data: {
wxUnionId: unionid
}
}
})
const users = await db
.select()
.from(schema.cfUser)
.where(sql`${schema.cfUser.data}->>'wxUnionId' = ${unionid}`)
.limit(1);
const user = users.length > 0 ? new User(users[0]) : null;
if (!user) {
ctx.throw(404, '请关注公众号《人生可视化助手》后再操作');
return
}
let secretKey = await UserSecret.findOne({
where: {
userId: user.id,
title: 'wxmp-notify-token'
}
});
const secretKeys = await db
.select()
.from(schema.cfUserSecrets)
.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) {
secretKey = await UserSecret.createSecret({ id: user.id, title: 'wxmp-notify-token' });
}

View File

@@ -39,18 +39,13 @@ app
if (avatar) {
updateData.avatar = avatar;
}
await user.update(
{
...updateData,
data: {
...user.data,
...data,
},
await user.update({
...updateData,
data: {
...user.data,
...data,
},
{
fields: ['nickname', 'avatar', 'data'],
},
);
});
user.setTokenUser(tokenUser);
ctx.body = await user.getInfo();
})