user-manager change

This commit is contained in:
xion 2025-04-03 20:08:40 +08:00
parent 8fafe74fa3
commit d97053a443
12 changed files with 297 additions and 22 deletions

View File

@ -91,6 +91,7 @@ app
}) })
.define(async (ctx) => { .define(async (ctx) => {
const id = ctx.query.id; const id = ctx.query.id;
const deleteFile = !!ctx.query.deleteFile; // 是否删除文件, 默认不删除
if (!id) { if (!id) {
throw new CustomError('id is required'); throw new CustomError('id is required');
} }
@ -106,7 +107,7 @@ app
throw new CustomError('app is published'); throw new CustomError('app is published');
} }
const files = app.data.files || []; const files = app.data.files || [];
if (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 app.destroy({
@ -217,13 +218,25 @@ app
}) })
.define(async (ctx) => { .define(async (ctx) => {
const tokenUser = ctx.state.tokenUser; const tokenUser = ctx.state.tokenUser;
const { id, username } = ctx.query.data; const { id, username, appKey, version } = ctx.query.data;
if (!id) { if (!id && !appKey) {
throw new CustomError('id is required'); throw new CustomError('id or appKey is required');
} }
const uid = await getUidByUsername(app, ctx, username); const uid = await getUidByUsername(app, ctx, username);
const appList = await AppListModel.findByPk(id); let appList: AppListModel | null = null;
if (id) {
appList = await AppListModel.findByPk(id);
if (appList?.uid !== uid) {
throw new CustomError('no permission');
}
}
if (!appList && appKey) {
if (!version) {
throw new CustomError('version is required');
}
appList = await AppListModel.findOne({ where: { key: appKey, version, uid } });
}
if (!appList) { if (!appList) {
throw new CustomError('app not found'); throw new CustomError('app not found');
} }
@ -265,7 +278,6 @@ app
}) })
.addTo(app); .addTo(app);
app app
.route({ .route({
path: 'app', path: 'app',

View File

@ -1,6 +1,7 @@
import { AppModel, AppListModel } from './module/index.ts'; import { AppModel, AppListModel } from './module/index.ts';
import { app } from '@/app.ts'; import { app } from '@/app.ts';
import { setExpire } from './revoke.ts'; import { setExpire } from './revoke.ts';
import { deleteFileByPrefix } from '../file/index.ts';
app app
.route({ .route({
@ -107,6 +108,7 @@ app
.define(async (ctx) => { .define(async (ctx) => {
const tokenUser = ctx.state.tokenUser; const tokenUser = ctx.state.tokenUser;
const id = ctx.query.id; const id = ctx.query.id;
const deleteFile = !!ctx.query.deleteFile; // 是否删除文件, 默认不删除
if (!id) { if (!id) {
ctx.throw(500, 'id is required'); ctx.throw(500, 'id is required');
} }
@ -120,6 +122,10 @@ app
const list = await AppListModel.findAll({ where: { key: am.key, uid: tokenUser.id } }); const list = await AppListModel.findAll({ where: { key: am.key, uid: tokenUser.id } });
await am.destroy({ force: true }); await am.destroy({ force: true });
await Promise.all(list.map((item) => item.destroy({ force: true }))); await Promise.all(list.map((item) => item.destroy({ force: true })));
if (deleteFile) {
const username = tokenUser.username;
await deleteFileByPrefix(`${username}/${am.key}`);
}
ctx.body = 'success'; ctx.body = 'success';
return ctx; return ctx;
}) })

View File

@ -5,6 +5,7 @@ import { oss } from '@/app.ts';
import { ConfigOssService } from '@kevisual/oss/services'; import { ConfigOssService } from '@kevisual/oss/services';
import { User } from '@/models/user.ts'; import { User } from '@/models/user.ts';
import { defaultKeys } from './models/default-keys.ts'; import { defaultKeys } from './models/default-keys.ts';
app app
.route({ .route({
path: 'config', path: 'config',
@ -20,7 +21,7 @@ app
const user = new User(); const user = new User();
user.setTokenUser(tokenUser); user.setTokenUser(tokenUser);
const isAdmin = await user.hasUser('admin'); const isAdmin = await user.hasUser('admin');
const usersConfig = ['upload.json', 'workspace.json', 'ai.json']; const usersConfig = ['upload.json', 'workspace.json', 'ai.json', 'user.json'];
const adminConfig = ['vip.json']; const adminConfig = ['vip.json'];
const configs = [...usersConfig, ...(isAdmin ? adminConfig : [])]; const configs = [...usersConfig, ...(isAdmin ? adminConfig : [])];
if (!configs.includes(configKey)) { if (!configs.includes(configKey)) {
@ -34,6 +35,8 @@ app
uid: tokenUser.id, uid: tokenUser.id,
}, },
defaults: { defaults: {
title: defaultConfig?.key,
description: defaultConfig?.description || '',
key: configKey, key: configKey,
uid: tokenUser.id, uid: tokenUser.id,
data: defaultConfig?.data, data: defaultConfig?.data,

View File

@ -1,18 +1,27 @@
export const defaultKeys = [ export const defaultKeys = [
{ {
key: 'upload.json', key: 'upload.json',
description: '上传配置',
data: { key: 'upload', version: '1.0.0' }, data: { key: 'upload', version: '1.0.0' },
}, },
{ {
key: 'workspace.json', key: 'workspace.json',
description: '工作空间配置',
data: { key: 'workspace', version: '1.0.0' }, data: { key: 'workspace', version: '1.0.0' },
}, },
{ {
key: 'ai.json', key: 'ai.json',
description: 'AI配置',
data: { key: 'ai', version: '1.0.0' }, data: { key: 'ai', version: '1.0.0' },
}, },
{ {
key: 'vip.json', key: 'vip.json',
description: 'VIP配置',
data: { key: 'vip', version: '1.0.0' }, data: { key: 'vip', version: '1.0.0' },
}, },
{
key: 'user.json',
description: '用户配置',
data: { key: 'user', version: '1.0.0', redirectURL: '/root/center/' },
},
]; ];

View File

@ -1,5 +1,6 @@
import { minioClient } from '@/app.ts'; import dayjs from 'dayjs';
import { bucketName } from '@/modules/minio.ts'; import { minioClient } from '../../../modules/minio.ts';
import { bucketName } from '../../../modules/minio.ts';
import { CopyDestinationOptions, CopySourceOptions } from 'minio'; import { CopyDestinationOptions, CopySourceOptions } from 'minio';
type MinioListOpt = { type MinioListOpt = {
prefix: string; prefix: string;
@ -95,7 +96,17 @@ export const deleteFiles = async (prefixs: string[]): Promise<any> => {
return false; return false;
} }
}; };
export const deleteFileByPrefix = async (prefix: string): Promise<any> => {
try {
const allFiles = await getMinioList<true>({ prefix, recursive: true });
const files = allFiles.filter((item) => item.name.startsWith(prefix));
await deleteFiles(files.map((item) => item.name));
return true;
} catch (e) {
console.error('delete File Error not handle', e);
return false;
}
};
type GetMinioListAndSetToAppListOpts = { type GetMinioListAndSetToAppListOpts = {
username: string; username: string;
appKey: string; appKey: string;
@ -148,3 +159,61 @@ export const updateFileStat = async (
}; };
} }
}; };
/**
* A的文件移动到用户B
* @param usernameA
* @param usernameB
* @param clearOldUser A的文件
*/
export const mvUserAToUserB = async (usernameA: string, usernameB: string, clearOldUser = false) => {
const oldPrefix = `${usernameA}/`;
const newPrefix = `${usernameB}/`;
const listSource = await getMinioList<true>({ prefix: oldPrefix, recursive: true });
for (const item of listSource) {
const source = new CopySourceOptions({ Bucket: bucketName, Object: item.name });
const stat = await getFileStat(item.name);
const newName = item.name.slice(oldPrefix.length);
const metadata = stat?.userMetadata;
const destination = new CopyDestinationOptions({
Bucket: bucketName,
Object: `${newPrefix}${newName}`,
UserMetadata: metadata,
MetadataDirective: 'COPY',
});
await minioClient.copyObject(source, destination);
}
if (clearOldUser) {
const files = await getMinioList<true>({ prefix: oldPrefix, recursive: true });
for (const file of files) {
await minioClient.removeObject(bucketName, file.name);
}
}
};
export const backupUserA = async (usernameA: string, id: string, backName?: string) => {
const today = backName || dayjs().format('YYYY-MM-DD-HH-mm');
const backupAllPrefix = `private/backup/${id}/`;
const backupPrefix = `private/backup/${id}/${today}`;
const backupList = await getMinioList<false>({ prefix: backupAllPrefix });
const backupListSort = backupList.sort((a, b) => -a.prefix.localeCompare(b.prefix));
if (backupListSort.length > 2) {
const deleteBackup = backupListSort.slice(2);
for (const item of deleteBackup) {
const files = await getMinioList<true>({ prefix: item.prefix, recursive: true });
for (const file of files) {
await minioClient.removeObject(bucketName, file.name);
}
}
}
await mvUserAToUserB(usernameA, backupPrefix, false);
};
/**
*
* @param username
*/
export const deleteUser = async (username: string) => {
const list = await getMinioList<true>({ prefix: `${username}/`, recursive: true });
for (const item of list) {
await minioClient.removeObject(bucketName, item.name);
}
};

View File

@ -0,0 +1,149 @@
import { app } 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';
export const checkUsername = (username: string) => {
if (username.length > 30) {
throw new CustomError(400, 'Username cannot be too long');
}
if (!/^[a-zA-Z0-9_@]+$/.test(username)) {
throw new CustomError(400, 'Username cannot contain special characters');
}
if (username.includes(' ')) {
throw new CustomError(400, 'Username cannot contain spaces');
}
};
export const checkUsernameShort = (username: string) => {
if (username.length < 3) {
throw new CustomError(400, 'Username cannot be too short');
}
};
app
.route({
path: 'user',
key: 'changeName',
middleware: ['auth-admin'],
})
.define(async (ctx) => {
const { id, newName } = ctx.query.data || {};
const user = await User.findByPk(id);
if (!user) {
ctx.throw(404, 'User not found');
}
const oldName = user.username;
checkUsername(newName);
const findUserByUsername = await User.findOne({ where: { username: newName } });
if (findUserByUsername) {
ctx.throw(400, 'Username already exists');
}
user.username = newName;
try {
await user.save();
// 迁移文件数据
await backupUserA(oldName, user.id); // 备份文件数据
await mvUserAToUserB(oldName, newName, true); // 迁移文件数据
} catch (error) {
console.error('迁移文件数据失败', error);
ctx.throw(500, 'Failed to change username');
}
ctx.body = user;
})
.addTo(app);
app
.route({
path: 'user',
key: 'checkUserExist',
middleware: ['auth'],
})
.define(async (ctx) => {
const { username } = ctx.query.data || {};
if (!username) {
ctx.throw(400, 'Username is required');
}
checkUsername(username);
const user = await User.findOne({ where: { username } });
ctx.body = {
id: user?.id,
username: user?.username,
};
})
.addTo(app);
app
.route({
path: 'user',
key: 'resetPassword',
middleware: ['auth-admin'],
})
.define(async (ctx) => {
const { id, password } = ctx.query.data || {};
const user = await User.findByPk(id);
if (!user) {
ctx.throw(404, 'User not found');
}
let pwd = password || nanoid(6);
user.createPassword(pwd);
await user.save();
ctx.body = {
id: user.id,
username: user.username,
password: !password ? pwd : undefined,
};
})
.addTo(app);
app
.route({
path: 'user',
key: 'createNewUser',
middleware: ['auth-admin'],
})
.define(async (ctx) => {
const { username, password, description } = ctx.query.data || {};
if (!username) {
ctx.throw(400, 'Username is required');
}
checkUsername(username);
const findUserByUsername = await User.findOne({ where: { username } });
if (findUserByUsername) {
ctx.throw(400, 'Username already exists');
}
let pwd = password || nanoid(6);
const user = await User.createUser(username, pwd, description);
ctx.body = {
id: user.id,
username: user.username,
description: user.description,
password: pwd,
};
})
.addTo(app);
app
.route({
path: 'user',
key: 'deleteUser',
middleware: ['auth-admin'],
})
.define(async (ctx) => {
const { id } = ctx.query.data || {};
const user = await User.findByPk(id);
if (!user) {
ctx.throw(404, 'User not found');
}
await user.destroy();
backupUserA(user.username, user.id);
deleteUser(user.username);
// TODO: EXPIRE 删除token
ctx.body = {
id: user.id,
username: user.username,
message: 'User deleted successfully',
};
})
.addTo(app);

View File

@ -10,3 +10,5 @@ import './init.ts'
import './web-login.ts' import './web-login.ts'
import './org-user/list.ts' import './org-user/list.ts'
import './admin/user.ts';

View File

@ -1,6 +1,8 @@
import { app } from '@/app.ts'; import { app } 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 { nanoid } from 'nanoid';
app app
.route({ .route({
@ -18,7 +20,6 @@ app
}) })
.addTo(app); .addTo(app);
app app
.route({ .route({
path: 'user', path: 'user',
@ -28,9 +29,12 @@ app
.define(async (ctx) => { .define(async (ctx) => {
const tokenUser = ctx.state.tokenUser; const tokenUser = ctx.state.tokenUser;
const { id, username, password, description } = ctx.query.data || {}; const { id, username, password, description } = ctx.query.data || {};
if (!id) {
throw new CustomError(400, 'id is required');
}
const user = await User.findByPk(id); const user = await User.findByPk(id);
if (user.id !== tokenUser.id) { if (user.id !== tokenUser.id) {
throw new CustomError(401, 'Permission denied'); throw new CustomError(403, 'Permission denied');
} }
if (!user) { if (!user) {
@ -59,21 +63,26 @@ app
.route({ .route({
path: 'user', path: 'user',
key: 'add', key: 'add',
middleware: ['auth'], middleware: ['auth-admin'],
}) })
.define(async (ctx) => { .define(async (ctx) => {
const tokenUser = ctx.state.tokenUser;
const { username, password, description } = ctx.query.data || {}; const { username, password, description } = ctx.query.data || {};
if (!username) { if (!username) {
throw new CustomError(400, 'username is required'); throw new CustomError(400, 'username is required');
} }
const user = await User.createUser(username, password, description); checkUsername(username);
const token = await user.createToken(); const findUserByUsername = await User.findOne({ where: { username } });
if (findUserByUsername) {
throw new CustomError(400, 'username already exists');
}
const pwd = password || nanoid(6);
const user = await User.createUser(username, pwd, description);
ctx.body = { ctx.body = {
id: user.id, id: user.id,
username: user.username, username: user.username,
description: user.description, description: user.description,
needChangePassword: user.needChangePassword, needChangePassword: user.needChangePassword,
token, password: pwd,
}; };
}); })
.addTo(app);

View File

@ -209,13 +209,13 @@ app
const { id } = tokenUser; const { id } = tokenUser;
const user = await User.findByPk(id); const user = await User.findByPk(id);
if (!user) { if (!user) {
ctx.throw(500, 'user not found'); ctx.throw(404, 'user not found');
} }
user.setTokenUser(tokenUser); user.setTokenUser(tokenUser);
if (username) { if (username) {
user.username = username; user.username = username;
} }
if (password) { if (password && user.type !== 'org') {
user.createPassword(password); user.createPassword(password);
} }
if (description) { if (description) {

15
src/scripts/mv-minio.ts Normal file
View File

@ -0,0 +1,15 @@
process.env.NODE_ENV = 'development';
// import { mvUserAToUserB, backupUserA } from '../routes/file/module/get-minio-list.ts';
// mvUserAToUserB('demo', 'demo2');
// backupUserA('demo', '123', '2025-04-02-16-00');
// backupUserA('demo', '123', '2025-04-02-16-01');
// backupUserA('demo', '123', '2025-04-02-16-02');
// backupUserA('demo', '123', '2025-04-02-16-03');
// backupUserA('demo', '123', '2025-04-02-16-04');
// backupUserA('demo', '123', '2025-04-02-16-05');
// backupUserA('demo', '123', '2025-04-02-16-06');
// backupUserA('demo', '123', '2025-04-02-16-07');
// backupUserA('demo', '123', '2025-04-02-16-08');

View File

@ -5,6 +5,7 @@ export const getContentType = (filePath: string) => {
const contentType = { const contentType = {
'.html': 'text/html; charset=utf-8', '.html': 'text/html; charset=utf-8',
'.js': 'text/javascript; charset=utf-8', '.js': 'text/javascript; charset=utf-8',
'.mjs': 'text/javascript; charset=utf-8',
'.css': 'text/css; charset=utf-8', '.css': 'text/css; charset=utf-8',
'.txt': 'text/plain; charset=utf-8', '.txt': 'text/plain; charset=utf-8',
'.json': 'application/json; charset=utf-8', '.json': 'application/json; charset=utf-8',

@ -1 +1 @@
Subproject commit 0a72db77719e8f99a4027566bc86217c85c1bab3 Subproject commit ad0d2e717f0cd409530735ab7143c94d910f939e