mirror of
https://github.com/whyour/qinglong.git
synced 2025-11-08 15:06:08 +08:00
Merge 2ff7a186e7 into 73f8f3c5fa
This commit is contained in:
commit
e35d5eb52d
112
MULTI_USER_GUIDE.md
Normal file
112
MULTI_USER_GUIDE.md
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
# 多用户管理功能说明 (Multi-User Management Guide)
|
||||
|
||||
## 功能概述 (Overview)
|
||||
|
||||
青龙面板现已支持多用户管理和数据隔离功能。管理员可以创建多个用户账号,每个用户只能看到和操作自己的数据。
|
||||
|
||||
Qinglong now supports multi-user management with data isolation. Administrators can create multiple user accounts, and each user can only see and operate their own data.
|
||||
|
||||
## 用户角色 (User Roles)
|
||||
|
||||
### 管理员 (Admin)
|
||||
- 可以访问所有用户的数据
|
||||
- 可以创建、编辑、删除用户
|
||||
- 可以管理系统设置
|
||||
- Can access all users' data
|
||||
- Can create, edit, and delete users
|
||||
- Can manage system settings
|
||||
|
||||
### 普通用户 (Regular User)
|
||||
- 只能访问自己创建的数据
|
||||
- 可以管理自己的定时任务、环境变量、订阅和依赖
|
||||
- 无法访问其他用户的数据
|
||||
- Can only access their own data
|
||||
- Can manage their own cron jobs, environment variables, subscriptions, and dependencies
|
||||
- Cannot access other users' data
|
||||
|
||||
## API 使用 (API Usage)
|
||||
|
||||
### 用户管理接口 (User Management Endpoints)
|
||||
|
||||
所有用户管理接口需要管理员权限。
|
||||
All user management endpoints require admin privileges.
|
||||
|
||||
#### 获取用户列表 (Get User List)
|
||||
```
|
||||
GET /api/user-management?searchValue=keyword
|
||||
```
|
||||
|
||||
#### 创建用户 (Create User)
|
||||
```
|
||||
POST /api/user-management
|
||||
{
|
||||
"username": "user1",
|
||||
"password": "password123",
|
||||
"role": 1, // 0: admin, 1: user
|
||||
"status": 0 // 0: active, 1: disabled
|
||||
}
|
||||
```
|
||||
|
||||
#### 更新用户 (Update User)
|
||||
```
|
||||
PUT /api/user-management
|
||||
{
|
||||
"id": 1,
|
||||
"username": "user1",
|
||||
"password": "newpassword",
|
||||
"role": 1,
|
||||
"status": 0
|
||||
}
|
||||
```
|
||||
|
||||
#### 删除用户 (Delete Users)
|
||||
```
|
||||
DELETE /api/user-management
|
||||
[1, 2, 3] // User IDs to delete
|
||||
```
|
||||
|
||||
## 数据隔离 (Data Isolation)
|
||||
|
||||
### 定时任务 (Cron Jobs)
|
||||
- 每个用户创建的定时任务会自动关联到该用户
|
||||
- 用户只能查看、编辑、运行、删除自己的定时任务
|
||||
- 管理员可以查看所有用户的定时任务
|
||||
|
||||
### 环境变量 (Environment Variables)
|
||||
- 每个用户的环境变量相互隔离
|
||||
- 用户只能查看和修改自己的环境变量
|
||||
- 管理员可以查看所有环境变量
|
||||
|
||||
### 订阅和依赖 (Subscriptions and Dependencies)
|
||||
- 用户数据完全隔离
|
||||
- Only accessible by the owning user and admins
|
||||
|
||||
## 密码安全 (Password Security)
|
||||
|
||||
- 所有密码使用 bcrypt 加密存储
|
||||
- 密码长度最少为 6 位
|
||||
- 建议使用强密码
|
||||
- All passwords are hashed with bcrypt
|
||||
- Minimum password length is 6 characters
|
||||
- Strong passwords are recommended
|
||||
|
||||
## 向后兼容 (Backward Compatibility)
|
||||
|
||||
- 原有的单用户系统管理员账号继续有效
|
||||
- 已存在的数据可以被所有用户访问(遗留数据)
|
||||
- 新创建的数据会自动关联到创建者
|
||||
- The original system admin account remains valid
|
||||
- Existing data is accessible by all users (legacy data)
|
||||
- Newly created data is automatically associated with the creator
|
||||
|
||||
## 注意事项 (Notes)
|
||||
|
||||
1. **首次使用**:首次使用多用户功能时,建议先创建一个管理员账号作为备份
|
||||
2. **密码管理**:请妥善保管用户密码,忘记密码需要管理员重置
|
||||
3. **数据迁移**:如需将现有数据分配给特定用户,请联系管理员手动更新数据库
|
||||
4. **权限控制**:删除用户不会删除该用户的数据,数据会变为遗留数据
|
||||
|
||||
1. **First Use**: When first using multi-user functionality, it's recommended to create an admin account as a backup
|
||||
2. **Password Management**: Please keep user passwords safe; forgotten passwords need admin reset
|
||||
3. **Data Migration**: To assign existing data to specific users, contact admin for manual database update
|
||||
4. **Permission Control**: Deleting a user doesn't delete their data; the data becomes legacy data
|
||||
|
|
@ -145,7 +145,10 @@ export default (app: Router) => {
|
|||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const cronService = Container.get(CronService);
|
||||
const data = await cronService.crontabs(req.query as any);
|
||||
const data = await cronService.crontabs({
|
||||
...req.query as any,
|
||||
userId: req.user?.userId
|
||||
});
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
|
|
@ -177,7 +180,10 @@ export default (app: Router) => {
|
|||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const cronService = Container.get(CronService);
|
||||
const data = await cronService.create(req.body);
|
||||
const data = await cronService.create({
|
||||
...req.body,
|
||||
userId: req.user?.userId
|
||||
});
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
|
|
@ -194,10 +200,10 @@ export default (app: Router) => {
|
|||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const cronService = Container.get(CronService);
|
||||
const data = await cronService.run(req.body);
|
||||
const data = await cronService.run(req.body, req.user?.userId);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
} catch (e: any) {
|
||||
return res.send({ code: 400, message: e.message });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
@ -211,10 +217,10 @@ export default (app: Router) => {
|
|||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const cronService = Container.get(CronService);
|
||||
const data = await cronService.stop(req.body);
|
||||
const data = await cronService.stop(req.body, req.user?.userId);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
} catch (e: any) {
|
||||
return res.send({ code: 400, message: e.message });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
@ -234,10 +240,11 @@ export default (app: Router) => {
|
|||
const data = await cronService.removeLabels(
|
||||
req.body.ids,
|
||||
req.body.labels,
|
||||
req.user?.userId,
|
||||
);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
} catch (e: any) {
|
||||
return res.send({ code: 400, message: e.message });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
@ -254,10 +261,10 @@ export default (app: Router) => {
|
|||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const cronService = Container.get(CronService);
|
||||
const data = await cronService.addLabels(req.body.ids, req.body.labels);
|
||||
const data = await cronService.addLabels(req.body.ids, req.body.labels, req.user?.userId);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
} catch (e: any) {
|
||||
return res.send({ code: 400, message: e.message });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
@ -271,10 +278,10 @@ export default (app: Router) => {
|
|||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const cronService = Container.get(CronService);
|
||||
const data = await cronService.disabled(req.body);
|
||||
const data = await cronService.disabled(req.body, req.user?.userId);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
} catch (e: any) {
|
||||
return res.send({ code: 400, message: e.message });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
@ -288,10 +295,10 @@ export default (app: Router) => {
|
|||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const cronService = Container.get(CronService);
|
||||
const data = await cronService.enabled(req.body);
|
||||
const data = await cronService.enabled(req.body, req.user?.userId);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
} catch (e: any) {
|
||||
return res.send({ code: 400, message: e.message });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
@ -344,10 +351,10 @@ export default (app: Router) => {
|
|||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const cronService = Container.get(CronService);
|
||||
const data = await cronService.remove(req.body);
|
||||
const data = await cronService.remove(req.body, req.user?.userId);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
} catch (e: any) {
|
||||
return res.send({ code: 400, message: e.message });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
@ -361,10 +368,10 @@ export default (app: Router) => {
|
|||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const cronService = Container.get(CronService);
|
||||
const data = await cronService.pin(req.body);
|
||||
const data = await cronService.pin(req.body, req.user?.userId);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
} catch (e: any) {
|
||||
return res.send({ code: 400, message: e.message });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
@ -378,10 +385,10 @@ export default (app: Router) => {
|
|||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const cronService = Container.get(CronService);
|
||||
const data = await cronService.unPin(req.body);
|
||||
const data = await cronService.unPin(req.body, req.user?.userId);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
} catch (e: any) {
|
||||
return res.send({ code: 400, message: e.message });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ export default (app: Router) => {
|
|||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const envService = Container.get(EnvService);
|
||||
const data = await envService.envs(req.query.searchValue as string);
|
||||
const data = await envService.envs(req.query.searchValue as string, {}, req.user?.userId);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
logger.error('🔥 error: %o', e);
|
||||
|
|
@ -54,7 +54,7 @@ export default (app: Router) => {
|
|||
if (!req.body?.length) {
|
||||
return res.send({ code: 400, message: '参数不正确' });
|
||||
}
|
||||
const data = await envService.create(req.body);
|
||||
const data = await envService.create(req.body, req.user?.userId);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
|
|
@ -93,10 +93,10 @@ export default (app: Router) => {
|
|||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const envService = Container.get(EnvService);
|
||||
const data = await envService.remove(req.body);
|
||||
const data = await envService.remove(req.body, req.user?.userId);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
} catch (e: any) {
|
||||
return res.send({ code: 400, message: e.message });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
@ -132,10 +132,10 @@ export default (app: Router) => {
|
|||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const envService = Container.get(EnvService);
|
||||
const data = await envService.disabled(req.body);
|
||||
const data = await envService.disabled(req.body, req.user?.userId);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
} catch (e: any) {
|
||||
return res.send({ code: 400, message: e.message });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
@ -149,10 +149,10 @@ export default (app: Router) => {
|
|||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const envService = Container.get(EnvService);
|
||||
const data = await envService.enabled(req.body);
|
||||
const data = await envService.enabled(req.body, req.user?.userId);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
} catch (e: any) {
|
||||
return res.send({ code: 400, message: e.message });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
@ -169,10 +169,10 @@ export default (app: Router) => {
|
|||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const envService = Container.get(EnvService);
|
||||
const data = await envService.updateNames(req.body);
|
||||
const data = await envService.updateNames(req.body, req.user?.userId);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
} catch (e: any) {
|
||||
return res.send({ code: 400, message: e.message });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import system from './system';
|
|||
import subscription from './subscription';
|
||||
import update from './update';
|
||||
import health from './health';
|
||||
import userManagement from './userManagement';
|
||||
|
||||
export default () => {
|
||||
const app = Router();
|
||||
|
|
@ -26,6 +27,7 @@ export default () => {
|
|||
subscription(app);
|
||||
update(app);
|
||||
health(app);
|
||||
userManagement(app);
|
||||
|
||||
return app;
|
||||
};
|
||||
|
|
|
|||
114
back/api/userManagement.ts
Normal file
114
back/api/userManagement.ts
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { Container } from 'typedi';
|
||||
import { celebrate, Joi } from 'celebrate';
|
||||
import UserManagementService from '../services/userManagement';
|
||||
import { UserRole } from '../data/user';
|
||||
|
||||
const route = Router();
|
||||
|
||||
// Middleware to check if user is admin
|
||||
const requireAdmin = (req: Request, res: Response, next: NextFunction) => {
|
||||
if (req.user && req.user.role === UserRole.admin) {
|
||||
return next();
|
||||
}
|
||||
return res.status(403).send({ code: 403, message: '需要管理员权限' });
|
||||
};
|
||||
|
||||
export default (app: Router) => {
|
||||
app.use('/user-management', route);
|
||||
|
||||
// List all users (admin only)
|
||||
route.get(
|
||||
'/',
|
||||
requireAdmin,
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const userManagementService = Container.get(UserManagementService);
|
||||
const data = await userManagementService.list(req.query.searchValue as string);
|
||||
res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Get a specific user (admin only)
|
||||
route.get(
|
||||
'/:id',
|
||||
requireAdmin,
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const userManagementService = Container.get(UserManagementService);
|
||||
const data = await userManagementService.get(Number(req.params.id));
|
||||
res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Create a new user (admin only)
|
||||
route.post(
|
||||
'/',
|
||||
requireAdmin,
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
username: Joi.string().required(),
|
||||
password: Joi.string().required(),
|
||||
role: Joi.number().valid(UserRole.admin, UserRole.user).default(UserRole.user),
|
||||
status: Joi.number().optional(),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const userManagementService = Container.get(UserManagementService);
|
||||
const data = await userManagementService.create(req.body);
|
||||
res.send({ code: 200, data, message: '创建用户成功' });
|
||||
} catch (e: any) {
|
||||
return res.send({ code: 400, message: e.message });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Update a user (admin only)
|
||||
route.put(
|
||||
'/',
|
||||
requireAdmin,
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
id: Joi.number().required(),
|
||||
username: Joi.string().required(),
|
||||
password: Joi.string().required(),
|
||||
role: Joi.number().valid(UserRole.admin, UserRole.user),
|
||||
status: Joi.number().optional(),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const userManagementService = Container.get(UserManagementService);
|
||||
const data = await userManagementService.update(req.body);
|
||||
res.send({ code: 200, data, message: '更新用户成功' });
|
||||
} catch (e: any) {
|
||||
return res.send({ code: 400, message: e.message });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Delete users (admin only)
|
||||
route.delete(
|
||||
'/',
|
||||
requireAdmin,
|
||||
celebrate({
|
||||
body: Joi.array().items(Joi.number()).required(),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const userManagementService = Container.get(UserManagementService);
|
||||
const count = await userManagementService.delete(req.body);
|
||||
res.send({ code: 200, data: count, message: '删除用户成功' });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
|
|
@ -21,6 +21,7 @@ export class Crontab {
|
|||
extra_schedules?: Array<{ schedule: string }>;
|
||||
task_before?: string;
|
||||
task_after?: string;
|
||||
userId?: number;
|
||||
|
||||
constructor(options: Crontab) {
|
||||
this.name = options.name;
|
||||
|
|
@ -45,6 +46,7 @@ export class Crontab {
|
|||
this.extra_schedules = options.extra_schedules;
|
||||
this.task_before = options.task_before;
|
||||
this.task_after = options.task_after;
|
||||
this.userId = options.userId;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -84,4 +86,5 @@ export const CrontabModel = sequelize.define<CronInstance>('Crontab', {
|
|||
extra_schedules: DataTypes.JSON,
|
||||
task_before: DataTypes.STRING,
|
||||
task_after: DataTypes.STRING,
|
||||
userId: { type: DataTypes.NUMBER, allowNull: true },
|
||||
});
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ export class Dependence {
|
|||
name: string;
|
||||
log?: string[];
|
||||
remark?: string;
|
||||
userId?: number;
|
||||
|
||||
constructor(options: Dependence) {
|
||||
this.id = options.id;
|
||||
|
|
@ -21,6 +22,7 @@ export class Dependence {
|
|||
this.name = options.name.trim();
|
||||
this.log = options.log || [];
|
||||
this.remark = options.remark || '';
|
||||
this.userId = options.userId;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -59,5 +61,6 @@ export const DependenceModel = sequelize.define<DependenceInstance>(
|
|||
status: DataTypes.NUMBER,
|
||||
log: DataTypes.JSON,
|
||||
remark: DataTypes.STRING,
|
||||
userId: { type: DataTypes.NUMBER, allowNull: true },
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ export class Env {
|
|||
position?: number;
|
||||
name?: string;
|
||||
remarks?: string;
|
||||
userId?: number;
|
||||
|
||||
constructor(options: Env) {
|
||||
this.value = options.value;
|
||||
|
|
@ -21,6 +22,7 @@ export class Env {
|
|||
this.position = options.position;
|
||||
this.name = options.name;
|
||||
this.remarks = options.remarks || '';
|
||||
this.userId = options.userId;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -42,4 +44,5 @@ export const EnvModel = sequelize.define<EnvInstance>('Env', {
|
|||
position: DataTypes.NUMBER,
|
||||
name: { type: DataTypes.STRING, unique: 'compositeIndex' },
|
||||
remarks: DataTypes.STRING,
|
||||
userId: { type: DataTypes.NUMBER, allowNull: true },
|
||||
});
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ export class Subscription {
|
|||
proxy?: string;
|
||||
autoAddCron?: 1 | 0;
|
||||
autoDelCron?: 1 | 0;
|
||||
userId?: number;
|
||||
|
||||
constructor(options: Subscription) {
|
||||
this.id = options.id;
|
||||
|
|
@ -60,6 +61,7 @@ export class Subscription {
|
|||
this.proxy = options.proxy;
|
||||
this.autoAddCron = options.autoAddCron ? 1 : 0;
|
||||
this.autoDelCron = options.autoDelCron ? 1 : 0;
|
||||
this.userId = options.userId;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -111,5 +113,6 @@ export const SubscriptionModel = sequelize.define<SubscriptionInstance>(
|
|||
proxy: { type: DataTypes.STRING, allowNull: true },
|
||||
autoAddCron: { type: DataTypes.NUMBER, allowNull: true },
|
||||
autoDelCron: { type: DataTypes.NUMBER, allowNull: true },
|
||||
userId: { type: DataTypes.NUMBER, allowNull: true },
|
||||
},
|
||||
);
|
||||
|
|
|
|||
56
back/data/user.ts
Normal file
56
back/data/user.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import { sequelize } from '.';
|
||||
import { DataTypes, Model } from 'sequelize';
|
||||
|
||||
export class User {
|
||||
id?: number;
|
||||
username: string;
|
||||
password: string;
|
||||
role: UserRole;
|
||||
status: UserStatus;
|
||||
createdAt?: Date;
|
||||
updatedAt?: Date;
|
||||
|
||||
constructor(options: User) {
|
||||
this.id = options.id;
|
||||
this.username = options.username;
|
||||
this.password = options.password;
|
||||
this.role = options.role || UserRole.user;
|
||||
this.status =
|
||||
typeof options.status === 'number' && UserStatus[options.status]
|
||||
? options.status
|
||||
: UserStatus.active;
|
||||
this.createdAt = options.createdAt;
|
||||
this.updatedAt = options.updatedAt;
|
||||
}
|
||||
}
|
||||
|
||||
export enum UserRole {
|
||||
'admin' = 0,
|
||||
'user' = 1,
|
||||
}
|
||||
|
||||
export enum UserStatus {
|
||||
'active' = 0,
|
||||
'disabled' = 1,
|
||||
}
|
||||
|
||||
export interface UserInstance extends Model<User, User>, User {}
|
||||
export const UserModel = sequelize.define<UserInstance>('User', {
|
||||
username: {
|
||||
type: DataTypes.STRING,
|
||||
unique: true,
|
||||
allowNull: false,
|
||||
},
|
||||
password: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
role: {
|
||||
type: DataTypes.NUMBER,
|
||||
defaultValue: UserRole.user,
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.NUMBER,
|
||||
defaultValue: UserStatus.active,
|
||||
},
|
||||
});
|
||||
|
|
@ -42,6 +42,18 @@ export default ({ app }: { app: Application }) => {
|
|||
return next();
|
||||
});
|
||||
|
||||
// Extract userId and role from JWT
|
||||
app.use((req: Request, res, next) => {
|
||||
if (req.auth) {
|
||||
const payload = req.auth as any;
|
||||
req.user = {
|
||||
userId: payload.userId,
|
||||
role: payload.role,
|
||||
};
|
||||
}
|
||||
return next();
|
||||
});
|
||||
|
||||
app.use(async (req: Request, res, next) => {
|
||||
if (!['/open/', '/api/'].some((x) => req.path.startsWith(x))) {
|
||||
return next();
|
||||
|
|
|
|||
|
|
@ -29,6 +29,30 @@ import { ScheduleType } from '../interface/schedule';
|
|||
export default class CronService {
|
||||
constructor(@Inject('logger') private logger: winston.Logger) {}
|
||||
|
||||
private addUserIdFilter(query: any, userId?: number) {
|
||||
if (userId !== undefined) {
|
||||
query.userId = userId;
|
||||
}
|
||||
return query;
|
||||
}
|
||||
|
||||
private async checkOwnership(ids: number[], userId?: number): Promise<void> {
|
||||
if (userId === undefined) {
|
||||
// Admin can access all crons
|
||||
return;
|
||||
}
|
||||
const crons = await CrontabModel.findAll({
|
||||
where: { id: ids },
|
||||
attributes: ['id', 'userId'],
|
||||
});
|
||||
const unauthorized = crons.filter(
|
||||
(cron) => cron.userId !== undefined && cron.userId !== userId
|
||||
);
|
||||
if (unauthorized.length > 0) {
|
||||
throw new Error('无权限操作该定时任务');
|
||||
}
|
||||
}
|
||||
|
||||
private isNodeCron(cron: Crontab) {
|
||||
const { schedule, extra_schedules } = cron;
|
||||
if (Number(schedule?.split(/ +/).length) > 5 || extra_schedules?.length) {
|
||||
|
|
@ -156,21 +180,26 @@ export default class CronService {
|
|||
}
|
||||
}
|
||||
|
||||
public async remove(ids: number[]) {
|
||||
public async remove(ids: number[], userId?: number) {
|
||||
await this.checkOwnership(ids, userId);
|
||||
await CrontabModel.destroy({ where: { id: ids } });
|
||||
await cronClient.delCron(ids.map(String));
|
||||
await this.setCrontab();
|
||||
}
|
||||
|
||||
public async pin(ids: number[]) {
|
||||
public async pin(ids: number[], userId?: number) {
|
||||
await this.checkOwnership(ids, userId);
|
||||
await CrontabModel.update({ isPinned: 1 }, { where: { id: ids } });
|
||||
}
|
||||
|
||||
public async unPin(ids: number[]) {
|
||||
public async unPin(ids: number[], userId?: number) {
|
||||
await this.checkOwnership(ids, userId);
|
||||
await CrontabModel.update({ isPinned: 0 }, { where: { id: ids } });
|
||||
}
|
||||
|
||||
public async addLabels(ids: string[], labels: string[]) {
|
||||
public async addLabels(ids: string[], labels: string[], userId?: number) {
|
||||
const numIds = ids.map(Number);
|
||||
await this.checkOwnership(numIds, userId);
|
||||
const docs = await CrontabModel.findAll({ where: { id: ids } });
|
||||
for (const doc of docs) {
|
||||
await CrontabModel.update(
|
||||
|
|
@ -182,7 +211,9 @@ export default class CronService {
|
|||
}
|
||||
}
|
||||
|
||||
public async removeLabels(ids: string[], labels: string[]) {
|
||||
public async removeLabels(ids: string[], labels: string[], userId?: number) {
|
||||
const numIds = ids.map(Number);
|
||||
await this.checkOwnership(numIds, userId);
|
||||
const docs = await CrontabModel.findAll({ where: { id: ids } });
|
||||
for (const doc of docs) {
|
||||
await CrontabModel.update(
|
||||
|
|
@ -377,6 +408,7 @@ export default class CronService {
|
|||
sorter: string;
|
||||
filters: string;
|
||||
queryString: string;
|
||||
userId?: number;
|
||||
}): Promise<{ data: Crontab[]; total: number }> {
|
||||
const searchText = params?.searchValue;
|
||||
const page = Number(params?.page || '0');
|
||||
|
|
@ -397,6 +429,7 @@ export default class CronService {
|
|||
this.formatSearchText(query, searchText);
|
||||
this.formatFilterQuery(query, filterQuery);
|
||||
this.formatViewSort(order, viewQuery);
|
||||
this.addUserIdFilter(query, params?.userId);
|
||||
|
||||
if (sorterQuery) {
|
||||
const { field, type } = sorterQuery;
|
||||
|
|
@ -429,7 +462,8 @@ export default class CronService {
|
|||
return doc.get({ plain: true });
|
||||
}
|
||||
|
||||
public async run(ids: number[]) {
|
||||
public async run(ids: number[], userId?: number) {
|
||||
await this.checkOwnership(ids, userId);
|
||||
await CrontabModel.update(
|
||||
{ status: CrontabStatus.queued },
|
||||
{ where: { id: ids } },
|
||||
|
|
@ -439,7 +473,8 @@ export default class CronService {
|
|||
});
|
||||
}
|
||||
|
||||
public async stop(ids: number[]) {
|
||||
public async stop(ids: number[], userId?: number) {
|
||||
await this.checkOwnership(ids, userId);
|
||||
const docs = await CrontabModel.findAll({ where: { id: ids } });
|
||||
for (const doc of docs) {
|
||||
if (doc.pid) {
|
||||
|
|
@ -533,13 +568,15 @@ export default class CronService {
|
|||
});
|
||||
}
|
||||
|
||||
public async disabled(ids: number[]) {
|
||||
public async disabled(ids: number[], userId?: number) {
|
||||
await this.checkOwnership(ids, userId);
|
||||
await CrontabModel.update({ isDisabled: 1 }, { where: { id: ids } });
|
||||
await cronClient.delCron(ids.map(String));
|
||||
await this.setCrontab();
|
||||
}
|
||||
|
||||
public async enabled(ids: number[]) {
|
||||
public async enabled(ids: number[], userId?: number) {
|
||||
await this.checkOwnership(ids, userId);
|
||||
await CrontabModel.update({ isDisabled: 0 }, { where: { id: ids } });
|
||||
const docs = await CrontabModel.findAll({ where: { id: ids } });
|
||||
const sixCron = docs
|
||||
|
|
|
|||
|
|
@ -19,8 +19,32 @@ import { writeFileWithLock } from '../shared/utils';
|
|||
export default class EnvService {
|
||||
constructor(@Inject('logger') private logger: winston.Logger) {}
|
||||
|
||||
public async create(payloads: Env[]): Promise<Env[]> {
|
||||
const envs = await this.envs();
|
||||
private addUserIdFilter(query: any, userId?: number) {
|
||||
if (userId !== undefined) {
|
||||
query.userId = userId;
|
||||
}
|
||||
return query;
|
||||
}
|
||||
|
||||
private async checkOwnership(ids: number[], userId?: number): Promise<void> {
|
||||
if (userId === undefined) {
|
||||
// Admin can access all envs
|
||||
return;
|
||||
}
|
||||
const envs = await EnvModel.findAll({
|
||||
where: { id: ids },
|
||||
attributes: ['id', 'userId'],
|
||||
});
|
||||
const unauthorized = envs.filter(
|
||||
(env) => env.userId !== undefined && env.userId !== userId
|
||||
);
|
||||
if (unauthorized.length > 0) {
|
||||
throw new Error('无权限操作该环境变量');
|
||||
}
|
||||
}
|
||||
|
||||
public async create(payloads: Env[], userId?: number): Promise<Env[]> {
|
||||
const envs = await this.envs('', {}, userId);
|
||||
let position = initPosition;
|
||||
if (
|
||||
envs &&
|
||||
|
|
@ -31,7 +55,7 @@ export default class EnvService {
|
|||
}
|
||||
const tabs = payloads.map((x) => {
|
||||
position = position - stepPosition;
|
||||
const tab = new Env({ ...x, position });
|
||||
const tab = new Env({ ...x, position, userId });
|
||||
return tab;
|
||||
});
|
||||
const docs = await this.insert(tabs);
|
||||
|
|
@ -62,7 +86,8 @@ export default class EnvService {
|
|||
return await this.getDb({ id: payload.id });
|
||||
}
|
||||
|
||||
public async remove(ids: number[]) {
|
||||
public async remove(ids: number[], userId?: number) {
|
||||
await this.checkOwnership(ids, userId);
|
||||
await EnvModel.destroy({ where: { id: ids } });
|
||||
await this.set_envs();
|
||||
}
|
||||
|
|
@ -119,8 +144,9 @@ export default class EnvService {
|
|||
return parseFloat(position.toPrecision(16));
|
||||
}
|
||||
|
||||
public async envs(searchText: string = '', query: any = {}): Promise<Env[]> {
|
||||
public async envs(searchText: string = '', query: any = {}, userId?: number): Promise<Env[]> {
|
||||
let condition = { ...query };
|
||||
this.addUserIdFilter(condition, userId);
|
||||
if (searchText) {
|
||||
const encodeText = encodeURI(searchText);
|
||||
const reg = {
|
||||
|
|
@ -172,7 +198,8 @@ export default class EnvService {
|
|||
return doc.get({ plain: true });
|
||||
}
|
||||
|
||||
public async disabled(ids: number[]) {
|
||||
public async disabled(ids: number[], userId?: number) {
|
||||
await this.checkOwnership(ids, userId);
|
||||
await EnvModel.update(
|
||||
{ status: EnvStatus.disabled },
|
||||
{ where: { id: ids } },
|
||||
|
|
@ -180,12 +207,14 @@ export default class EnvService {
|
|||
await this.set_envs();
|
||||
}
|
||||
|
||||
public async enabled(ids: number[]) {
|
||||
public async enabled(ids: number[], userId?: number) {
|
||||
await this.checkOwnership(ids, userId);
|
||||
await EnvModel.update({ status: EnvStatus.normal }, { where: { id: ids } });
|
||||
await this.set_envs();
|
||||
}
|
||||
|
||||
public async updateNames({ ids, name }: { ids: number[]; name: string }) {
|
||||
public async updateNames({ ids, name }: { ids: number[]; name: string }, userId?: number) {
|
||||
await this.checkOwnership(ids, userId);
|
||||
await EnvModel.update({ name }, { where: { id: ids } });
|
||||
await this.set_envs();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,12 +24,17 @@ import uniq from 'lodash/uniq';
|
|||
import pickBy from 'lodash/pickBy';
|
||||
import isNil from 'lodash/isNil';
|
||||
import { shareStore } from '../shared/store';
|
||||
import UserManagementService from './userManagement';
|
||||
import { UserRole } from '../data/user';
|
||||
|
||||
@Service()
|
||||
export default class UserService {
|
||||
@Inject((type) => NotificationService)
|
||||
private notificationService!: NotificationService;
|
||||
|
||||
@Inject((type) => UserManagementService)
|
||||
private userManagementService!: UserManagementService;
|
||||
|
||||
constructor(
|
||||
@Inject('logger') private logger: winston.Logger,
|
||||
private scheduleService: ScheduleService,
|
||||
|
|
@ -93,27 +98,57 @@ export default class UserService {
|
|||
const { country, province, city, isp } = ipAddress;
|
||||
address = uniq([country, province, city, isp]).filter(Boolean).join(' ');
|
||||
}
|
||||
if (username === cUsername && password === cPassword) {
|
||||
|
||||
// Check if this is a regular user (not admin) trying to login
|
||||
let authenticatedUser = null;
|
||||
let userId: number | undefined = undefined;
|
||||
let userRole = UserRole.admin;
|
||||
|
||||
// First check if it's the system admin
|
||||
const isSystemAdmin = username === cUsername && password === cPassword;
|
||||
|
||||
if (!isSystemAdmin) {
|
||||
// Try to authenticate as a regular user
|
||||
try {
|
||||
authenticatedUser = await this.userManagementService.authenticate(username, password);
|
||||
if (authenticatedUser) {
|
||||
userId = authenticatedUser.id;
|
||||
userRole = authenticatedUser.role;
|
||||
}
|
||||
} catch (e: any) {
|
||||
// User disabled or other error
|
||||
return { code: 400, message: e.message };
|
||||
}
|
||||
}
|
||||
|
||||
if (isSystemAdmin || authenticatedUser) {
|
||||
const data = createRandomString(50, 100);
|
||||
const expiration = twoFactorActivated ? '60d' : '20d';
|
||||
let token = jwt.sign({ data }, config.jwt.secret, {
|
||||
const expiration = (isSystemAdmin && twoFactorActivated) ? '60d' : '20d';
|
||||
let token = jwt.sign(
|
||||
{ data, userId, role: userRole },
|
||||
config.jwt.secret,
|
||||
{
|
||||
expiresIn: config.jwt.expiresIn || expiration,
|
||||
algorithm: 'HS384',
|
||||
});
|
||||
|
||||
await this.updateAuthInfo(content, {
|
||||
token,
|
||||
tokens: {
|
||||
...tokens,
|
||||
[req.platform]: token,
|
||||
},
|
||||
lastlogon: timestamp,
|
||||
retries: 0,
|
||||
lastip: ip,
|
||||
lastaddr: address,
|
||||
platform: req.platform,
|
||||
isTwoFactorChecking: false,
|
||||
});
|
||||
// Only update authInfo for system admin
|
||||
if (isSystemAdmin) {
|
||||
await this.updateAuthInfo(content, {
|
||||
token,
|
||||
tokens: {
|
||||
...tokens,
|
||||
[req.platform]: token,
|
||||
},
|
||||
lastlogon: timestamp,
|
||||
retries: 0,
|
||||
lastip: ip,
|
||||
lastaddr: address,
|
||||
platform: req.platform,
|
||||
isTwoFactorChecking: false,
|
||||
});
|
||||
}
|
||||
|
||||
this.notificationService.notify(
|
||||
'登录通知',
|
||||
`你于${dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')}在 ${address} ${
|
||||
|
|
|
|||
125
back/services/userManagement.ts
Normal file
125
back/services/userManagement.ts
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
import { Service, Inject } from 'typedi';
|
||||
import winston from 'winston';
|
||||
import { User, UserModel, UserRole, UserStatus } from '../data/user';
|
||||
import { Op } from 'sequelize';
|
||||
import bcrypt from 'bcrypt';
|
||||
|
||||
@Service()
|
||||
export default class UserManagementService {
|
||||
constructor(@Inject('logger') private logger: winston.Logger) {}
|
||||
|
||||
private async hashPassword(password: string): Promise<string> {
|
||||
const saltRounds = 10;
|
||||
return bcrypt.hash(password, saltRounds);
|
||||
}
|
||||
|
||||
private async verifyPassword(password: string, hash: string): Promise<boolean> {
|
||||
return bcrypt.compare(password, hash);
|
||||
}
|
||||
|
||||
public async list(searchText?: string): Promise<User[]> {
|
||||
let query: any = {};
|
||||
if (searchText) {
|
||||
query = {
|
||||
username: { [Op.like]: `%${searchText}%` },
|
||||
};
|
||||
}
|
||||
const docs = await UserModel.findAll({ where: query });
|
||||
return docs.map((x) => x.get({ plain: true }));
|
||||
}
|
||||
|
||||
public async get(id: number): Promise<User> {
|
||||
const doc = await UserModel.findByPk(id);
|
||||
if (!doc) {
|
||||
throw new Error('用户不存在');
|
||||
}
|
||||
return doc.get({ plain: true });
|
||||
}
|
||||
|
||||
public async getByUsername(username: string): Promise<User | null> {
|
||||
const doc = await UserModel.findOne({ where: { username } });
|
||||
if (!doc) {
|
||||
return null;
|
||||
}
|
||||
return doc.get({ plain: true });
|
||||
}
|
||||
|
||||
public async create(payload: User): Promise<User> {
|
||||
const existingUser = await this.getByUsername(payload.username);
|
||||
if (existingUser) {
|
||||
throw new Error('用户名已存在');
|
||||
}
|
||||
|
||||
if (payload.password.length < 6) {
|
||||
throw new Error('密码长度至少为6位');
|
||||
}
|
||||
|
||||
// Hash the password before storing
|
||||
const hashedPassword = await this.hashPassword(payload.password);
|
||||
const userWithHashedPassword = { ...payload, password: hashedPassword };
|
||||
|
||||
const doc = await UserModel.create(userWithHashedPassword);
|
||||
return doc.get({ plain: true });
|
||||
}
|
||||
|
||||
public async update(payload: User): Promise<User> {
|
||||
if (!payload.id) {
|
||||
throw new Error('缺少用户ID');
|
||||
}
|
||||
|
||||
const existingUser = await this.get(payload.id);
|
||||
if (!existingUser) {
|
||||
throw new Error('用户不存在');
|
||||
}
|
||||
|
||||
if (payload.password && payload.password.length < 6) {
|
||||
throw new Error('密码长度至少为6位');
|
||||
}
|
||||
|
||||
// Check if username is being changed and if new username already exists
|
||||
if (payload.username !== existingUser.username) {
|
||||
const userWithSameUsername = await this.getByUsername(payload.username);
|
||||
if (userWithSameUsername && userWithSameUsername.id !== payload.id) {
|
||||
throw new Error('用户名已存在');
|
||||
}
|
||||
}
|
||||
|
||||
// Hash the password if it's being updated
|
||||
const updatePayload = { ...payload };
|
||||
if (payload.password) {
|
||||
updatePayload.password = await this.hashPassword(payload.password);
|
||||
}
|
||||
|
||||
const [, [updated]] = await UserModel.update(updatePayload, {
|
||||
where: { id: payload.id },
|
||||
returning: true,
|
||||
});
|
||||
return updated.get({ plain: true });
|
||||
}
|
||||
|
||||
public async delete(ids: number[]): Promise<number> {
|
||||
const count = await UserModel.destroy({ where: { id: ids } });
|
||||
return count;
|
||||
}
|
||||
|
||||
public async authenticate(
|
||||
username: string,
|
||||
password: string,
|
||||
): Promise<User | null> {
|
||||
const user = await this.getByUsername(username);
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isPasswordValid = await this.verifyPassword(password, user.password);
|
||||
if (!isPasswordValid) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (user.status === UserStatus.disabled) {
|
||||
throw new Error('用户已被禁用');
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
}
|
||||
5
back/types/express.d.ts
vendored
5
back/types/express.d.ts
vendored
|
|
@ -6,6 +6,11 @@ declare global {
|
|||
namespace Express {
|
||||
interface Request {
|
||||
platform: 'desktop' | 'mobile';
|
||||
user?: {
|
||||
userId?: number;
|
||||
role?: number;
|
||||
};
|
||||
auth?: any;
|
||||
}
|
||||
}
|
||||
}
|
||||
42
package.json
42
package.json
|
|
@ -54,12 +54,16 @@
|
|||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@bufbuild/protobuf": "^2.10.0",
|
||||
"@grpc/grpc-js": "^1.14.0",
|
||||
"@grpc/proto-loader": "^0.8.0",
|
||||
"@keyv/sqlite": "^4.0.1",
|
||||
"@otplib/preset-default": "^12.0.1",
|
||||
"bcrypt": "^6.0.0",
|
||||
"body-parser": "^1.20.3",
|
||||
"celebrate": "^15.0.3",
|
||||
"chokidar": "^4.0.1",
|
||||
"compression": "^1.7.4",
|
||||
"cors": "^2.8.5",
|
||||
"cron-parser": "^4.9.0",
|
||||
"cross-spawn": "^7.0.6",
|
||||
|
|
@ -69,51 +73,49 @@
|
|||
"express-jwt": "^8.4.1",
|
||||
"express-rate-limit": "^7.4.1",
|
||||
"express-urlrewrite": "^2.0.3",
|
||||
"undici": "^7.9.0",
|
||||
"helmet": "^8.1.0",
|
||||
"hpagent": "^1.2.0",
|
||||
"http-proxy-middleware": "^3.0.3",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"ip2region": "2.3.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"keyv": "^5.2.3",
|
||||
"lodash": "^4.17.21",
|
||||
"multer": "1.4.5-lts.1",
|
||||
"node-schedule": "^2.1.0",
|
||||
"nodemailer": "^6.9.16",
|
||||
"p-queue-cjs": "7.3.4",
|
||||
"@bufbuild/protobuf": "^2.10.0",
|
||||
"proper-lockfile": "^4.1.2",
|
||||
"ps-tree": "^1.2.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"request-ip": "3.3.0",
|
||||
"sequelize": "^6.37.5",
|
||||
"sockjs": "^0.3.24",
|
||||
"sqlite3": "git+https://github.com/whyour/node-sqlite3.git#v1.0.3",
|
||||
"toad-scheduler": "^3.0.1",
|
||||
"typedi": "^0.10.0",
|
||||
"undici": "^7.9.0",
|
||||
"uuid": "^11.0.3",
|
||||
"winston": "^3.17.0",
|
||||
"winston-daily-rotate-file": "^5.0.0",
|
||||
"request-ip": "3.3.0",
|
||||
"ip2region": "2.3.0",
|
||||
"keyv": "^5.2.3",
|
||||
"@keyv/sqlite": "^4.0.1",
|
||||
"proper-lockfile": "^4.1.2",
|
||||
"compression": "^1.7.4",
|
||||
"helmet": "^8.1.0"
|
||||
"winston-daily-rotate-file": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"moment": "2.30.1",
|
||||
"@ant-design/icons": "^5.0.1",
|
||||
"@ant-design/pro-layout": "6.38.22",
|
||||
"@codemirror/view": "^6.34.1",
|
||||
"@codemirror/state": "^6.4.1",
|
||||
"@codemirror/view": "^6.34.1",
|
||||
"@monaco-editor/react": "4.2.1",
|
||||
"@react-hook/resize-observer": "^2.0.2",
|
||||
"react-router-dom": "6.26.1",
|
||||
"@types/bcrypt": "^6.0.0",
|
||||
"@types/body-parser": "^1.19.2",
|
||||
"@types/compression": "^1.7.2",
|
||||
"@types/cors": "^2.8.12",
|
||||
"@types/cross-spawn": "^6.0.2",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/express-jwt": "^6.0.4",
|
||||
"@types/file-saver": "2.0.2",
|
||||
"@types/helmet": "^4.0.0",
|
||||
"@types/js-yaml": "^4.0.5",
|
||||
"@types/jsonwebtoken": "^8.5.8",
|
||||
"@types/lodash": "^4.14.185",
|
||||
|
|
@ -121,17 +123,17 @@
|
|||
"@types/node": "^17.0.21",
|
||||
"@types/node-schedule": "^1.3.2",
|
||||
"@types/nodemailer": "^6.4.4",
|
||||
"@types/proper-lockfile": "^4.1.4",
|
||||
"@types/ps-tree": "^1.1.6",
|
||||
"@types/qrcode.react": "^1.0.2",
|
||||
"@types/react": "^18.0.20",
|
||||
"@types/react-copy-to-clipboard": "^5.0.4",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@types/request-ip": "0.0.41",
|
||||
"@types/serve-handler": "^6.1.1",
|
||||
"@types/sockjs": "^0.3.33",
|
||||
"@types/sockjs-client": "^1.5.1",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@types/request-ip": "0.0.41",
|
||||
"@types/proper-lockfile": "^4.1.4",
|
||||
"@types/ps-tree": "^1.1.6",
|
||||
"@uiw/codemirror-extensions-langs": "^4.21.9",
|
||||
"@uiw/react-codemirror": "^4.21.9",
|
||||
"@umijs/max": "^4.4.4",
|
||||
|
|
@ -143,9 +145,9 @@
|
|||
"axios": "^1.4.0",
|
||||
"compression-webpack-plugin": "9.2.0",
|
||||
"concurrently": "^7.0.0",
|
||||
"react-hotkeys-hook": "^4.6.1",
|
||||
"file-saver": "2.0.2",
|
||||
"lint-staged": "^13.0.3",
|
||||
"moment": "2.30.1",
|
||||
"monaco-editor": "0.33.0",
|
||||
"nodemon": "^3.0.1",
|
||||
"prettier": "^2.5.1",
|
||||
|
|
@ -161,7 +163,9 @@
|
|||
"react-dnd": "^16.0.1",
|
||||
"react-dnd-html5-backend": "^16.0.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react-hotkeys-hook": "^4.6.1",
|
||||
"react-intl-universal": "^2.12.0",
|
||||
"react-router-dom": "6.26.1",
|
||||
"react-split-pane": "^0.1.92",
|
||||
"sockjs-client": "^1.6.0",
|
||||
"ts-node": "^10.9.2",
|
||||
|
|
@ -169,8 +173,6 @@
|
|||
"tslib": "^2.4.0",
|
||||
"typescript": "5.2.2",
|
||||
"vh-check": "^2.0.5",
|
||||
"virtualizedtableforantd4": "1.3.0",
|
||||
"@types/compression": "^1.7.2",
|
||||
"@types/helmet": "^4.0.0"
|
||||
"virtualizedtableforantd4": "1.3.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,5 @@
|
|||
lockfileVersion: '6.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
overrides:
|
||||
sqlite3: git+https://github.com/whyour/node-sqlite3.git#v1.0.3
|
||||
|
||||
|
|
@ -23,6 +19,9 @@ dependencies:
|
|||
'@otplib/preset-default':
|
||||
specifier: ^12.0.1
|
||||
version: 12.0.1
|
||||
bcrypt:
|
||||
specifier: ^6.0.0
|
||||
version: 6.0.0
|
||||
body-parser:
|
||||
specifier: ^1.20.3
|
||||
version: 1.20.3
|
||||
|
|
@ -160,6 +159,9 @@ devDependencies:
|
|||
'@react-hook/resize-observer':
|
||||
specifier: ^2.0.2
|
||||
version: 2.0.2(react@18.3.1)
|
||||
'@types/bcrypt':
|
||||
specifier: ^6.0.0
|
||||
version: 6.0.0
|
||||
'@types/body-parser':
|
||||
specifier: ^1.19.2
|
||||
version: 1.19.5
|
||||
|
|
@ -3786,6 +3788,12 @@ packages:
|
|||
'@babel/types': 7.26.0
|
||||
dev: true
|
||||
|
||||
/@types/bcrypt@6.0.0:
|
||||
resolution: {integrity: sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==}
|
||||
dependencies:
|
||||
'@types/node': 17.0.45
|
||||
dev: true
|
||||
|
||||
/@types/body-parser@1.19.5:
|
||||
resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==}
|
||||
dependencies:
|
||||
|
|
@ -5747,6 +5755,15 @@ packages:
|
|||
/base64-js@1.5.1:
|
||||
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
||||
|
||||
/bcrypt@6.0.0:
|
||||
resolution: {integrity: sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==}
|
||||
engines: {node: '>= 18'}
|
||||
requiresBuild: true
|
||||
dependencies:
|
||||
node-addon-api: 8.5.0
|
||||
node-gyp-build: 4.8.4
|
||||
dev: false
|
||||
|
||||
/before@0.0.1:
|
||||
resolution: {integrity: sha512-1J5SWbkoVJH9DTALN8igB4p+nPKZzPrJ/HomqBDLpfUvDXCdjdBmBUcH5McZfur0lftVssVU6BZug5NYh87zTw==}
|
||||
dev: true
|
||||
|
|
@ -10212,6 +10229,11 @@ packages:
|
|||
resolution: {integrity: sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==}
|
||||
dev: false
|
||||
|
||||
/node-addon-api@8.5.0:
|
||||
resolution: {integrity: sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==}
|
||||
engines: {node: ^18 || ^20 || >= 21}
|
||||
dev: false
|
||||
|
||||
/node-domexception@1.0.0:
|
||||
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
|
||||
engines: {node: '>=10.5.0'}
|
||||
|
|
@ -10238,6 +10260,11 @@ packages:
|
|||
formdata-polyfill: 4.0.10
|
||||
dev: true
|
||||
|
||||
/node-gyp-build@4.8.4:
|
||||
resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==}
|
||||
hasBin: true
|
||||
dev: false
|
||||
|
||||
/node-gyp@8.4.1:
|
||||
resolution: {integrity: sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==}
|
||||
engines: {node: '>= 10.12.0'}
|
||||
|
|
@ -15213,3 +15240,7 @@ packages:
|
|||
- encoding
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user