user-manager change
This commit is contained in:
parent
8fafe74fa3
commit
d97053a443
@ -91,6 +91,7 @@ app
|
||||
})
|
||||
.define(async (ctx) => {
|
||||
const id = ctx.query.id;
|
||||
const deleteFile = !!ctx.query.deleteFile; // 是否删除文件, 默认不删除
|
||||
if (!id) {
|
||||
throw new CustomError('id is required');
|
||||
}
|
||||
@ -106,7 +107,7 @@ app
|
||||
throw new CustomError('app is published');
|
||||
}
|
||||
const files = app.data.files || [];
|
||||
if (files.length > 0) {
|
||||
if (deleteFile && files.length > 0) {
|
||||
await deleteFiles(files.map((item) => item.path));
|
||||
}
|
||||
await app.destroy({
|
||||
@ -217,13 +218,25 @@ app
|
||||
})
|
||||
.define(async (ctx) => {
|
||||
const tokenUser = ctx.state.tokenUser;
|
||||
const { id, username } = ctx.query.data;
|
||||
if (!id) {
|
||||
throw new CustomError('id is required');
|
||||
const { id, username, appKey, version } = ctx.query.data;
|
||||
if (!id && !appKey) {
|
||||
throw new CustomError('id or appKey is required');
|
||||
}
|
||||
|
||||
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) {
|
||||
throw new CustomError('app not found');
|
||||
}
|
||||
@ -265,7 +278,6 @@ app
|
||||
})
|
||||
.addTo(app);
|
||||
|
||||
|
||||
app
|
||||
.route({
|
||||
path: 'app',
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { AppModel, AppListModel } from './module/index.ts';
|
||||
import { app } from '@/app.ts';
|
||||
import { setExpire } from './revoke.ts';
|
||||
import { deleteFileByPrefix } from '../file/index.ts';
|
||||
|
||||
app
|
||||
.route({
|
||||
@ -107,6 +108,7 @@ app
|
||||
.define(async (ctx) => {
|
||||
const tokenUser = ctx.state.tokenUser;
|
||||
const id = ctx.query.id;
|
||||
const deleteFile = !!ctx.query.deleteFile; // 是否删除文件, 默认不删除
|
||||
if (!id) {
|
||||
ctx.throw(500, 'id is required');
|
||||
}
|
||||
@ -120,6 +122,10 @@ app
|
||||
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 })));
|
||||
if (deleteFile) {
|
||||
const username = tokenUser.username;
|
||||
await deleteFileByPrefix(`${username}/${am.key}`);
|
||||
}
|
||||
ctx.body = 'success';
|
||||
return ctx;
|
||||
})
|
||||
|
@ -5,6 +5,7 @@ import { oss } from '@/app.ts';
|
||||
import { ConfigOssService } from '@kevisual/oss/services';
|
||||
import { User } from '@/models/user.ts';
|
||||
import { defaultKeys } from './models/default-keys.ts';
|
||||
|
||||
app
|
||||
.route({
|
||||
path: 'config',
|
||||
@ -20,7 +21,7 @@ app
|
||||
const user = new User();
|
||||
user.setTokenUser(tokenUser);
|
||||
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 configs = [...usersConfig, ...(isAdmin ? adminConfig : [])];
|
||||
if (!configs.includes(configKey)) {
|
||||
@ -34,6 +35,8 @@ app
|
||||
uid: tokenUser.id,
|
||||
},
|
||||
defaults: {
|
||||
title: defaultConfig?.key,
|
||||
description: defaultConfig?.description || '',
|
||||
key: configKey,
|
||||
uid: tokenUser.id,
|
||||
data: defaultConfig?.data,
|
||||
|
@ -1,18 +1,27 @@
|
||||
export const defaultKeys = [
|
||||
{
|
||||
key: 'upload.json',
|
||||
description: '上传配置',
|
||||
data: { key: 'upload', version: '1.0.0' },
|
||||
},
|
||||
{
|
||||
key: 'workspace.json',
|
||||
description: '工作空间配置',
|
||||
data: { key: 'workspace', version: '1.0.0' },
|
||||
},
|
||||
{
|
||||
key: 'ai.json',
|
||||
description: 'AI配置',
|
||||
data: { key: 'ai', version: '1.0.0' },
|
||||
},
|
||||
{
|
||||
key: 'vip.json',
|
||||
description: 'VIP配置',
|
||||
data: { key: 'vip', version: '1.0.0' },
|
||||
},
|
||||
{
|
||||
key: 'user.json',
|
||||
description: '用户配置',
|
||||
data: { key: 'user', version: '1.0.0', redirectURL: '/root/center/' },
|
||||
},
|
||||
];
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { minioClient } from '@/app.ts';
|
||||
import { bucketName } from '@/modules/minio.ts';
|
||||
import dayjs from 'dayjs';
|
||||
import { minioClient } from '../../../modules/minio.ts';
|
||||
import { bucketName } from '../../../modules/minio.ts';
|
||||
import { CopyDestinationOptions, CopySourceOptions } from 'minio';
|
||||
type MinioListOpt = {
|
||||
prefix: string;
|
||||
@ -95,7 +96,17 @@ export const deleteFiles = async (prefixs: string[]): Promise<any> => {
|
||||
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 = {
|
||||
username: 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);
|
||||
}
|
||||
};
|
||||
|
149
src/routes/user/admin/user.ts
Normal file
149
src/routes/user/admin/user.ts
Normal 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);
|
@ -9,4 +9,6 @@ import './init.ts'
|
||||
|
||||
import './web-login.ts'
|
||||
|
||||
import './org-user/list.ts'
|
||||
import './org-user/list.ts'
|
||||
|
||||
import './admin/user.ts';
|
@ -1,6 +1,8 @@
|
||||
import { app } from '@/app.ts';
|
||||
import { User } from '@/models/user.ts';
|
||||
import { CustomError } from '@kevisual/router';
|
||||
import { checkUsername } from './admin/user.ts';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
app
|
||||
.route({
|
||||
@ -18,7 +20,6 @@ app
|
||||
})
|
||||
.addTo(app);
|
||||
|
||||
|
||||
app
|
||||
.route({
|
||||
path: 'user',
|
||||
@ -28,9 +29,12 @@ app
|
||||
.define(async (ctx) => {
|
||||
const tokenUser = ctx.state.tokenUser;
|
||||
const { id, username, password, description } = ctx.query.data || {};
|
||||
if (!id) {
|
||||
throw new CustomError(400, 'id is required');
|
||||
}
|
||||
const user = await User.findByPk(id);
|
||||
if (user.id !== tokenUser.id) {
|
||||
throw new CustomError(401, 'Permission denied');
|
||||
throw new CustomError(403, 'Permission denied');
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
@ -59,21 +63,26 @@ app
|
||||
.route({
|
||||
path: 'user',
|
||||
key: 'add',
|
||||
middleware: ['auth'],
|
||||
middleware: ['auth-admin'],
|
||||
})
|
||||
.define(async (ctx) => {
|
||||
const tokenUser = ctx.state.tokenUser;
|
||||
const { username, password, description } = ctx.query.data || {};
|
||||
if (!username) {
|
||||
throw new CustomError(400, 'username is required');
|
||||
}
|
||||
const user = await User.createUser(username, password, description);
|
||||
const token = await user.createToken();
|
||||
checkUsername(username);
|
||||
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 = {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
description: user.description,
|
||||
needChangePassword: user.needChangePassword,
|
||||
token,
|
||||
password: pwd,
|
||||
};
|
||||
});
|
||||
})
|
||||
.addTo(app);
|
||||
|
@ -209,13 +209,13 @@ app
|
||||
const { id } = tokenUser;
|
||||
const user = await User.findByPk(id);
|
||||
if (!user) {
|
||||
ctx.throw(500, 'user not found');
|
||||
ctx.throw(404, 'user not found');
|
||||
}
|
||||
user.setTokenUser(tokenUser);
|
||||
if (username) {
|
||||
user.username = username;
|
||||
}
|
||||
if (password) {
|
||||
if (password && user.type !== 'org') {
|
||||
user.createPassword(password);
|
||||
}
|
||||
if (description) {
|
||||
|
15
src/scripts/mv-minio.ts
Normal file
15
src/scripts/mv-minio.ts
Normal 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');
|
@ -5,6 +5,7 @@ export const getContentType = (filePath: string) => {
|
||||
const contentType = {
|
||||
'.html': 'text/html; charset=utf-8',
|
||||
'.js': 'text/javascript; charset=utf-8',
|
||||
'.mjs': 'text/javascript; charset=utf-8',
|
||||
'.css': 'text/css; charset=utf-8',
|
||||
'.txt': 'text/plain; charset=utf-8',
|
||||
'.json': 'application/json; charset=utf-8',
|
||||
|
@ -1 +1 @@
|
||||
Subproject commit 0a72db77719e8f99a4027566bc86217c85c1bab3
|
||||
Subproject commit ad0d2e717f0cd409530735ab7143c94d910f939e
|
Loading…
x
Reference in New Issue
Block a user