qinglong/back/api/log.ts
copilot-swe-agent[bot] 07fcb09cc6 Add log isolation and admin-only access for system/login logs
Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>
2025-11-11 16:36:12 +00:00

294 lines
8.5 KiB
TypeScript

import { celebrate, Joi } from 'celebrate';
import { NextFunction, Request, Response, Router } from 'express';
import { Container } from 'typedi';
import { Logger } from 'winston';
import config from '../config';
import {
getFileContentByName,
readDirs,
removeAnsi,
rmPath,
IFile,
} from '../config/util';
import LogService from '../services/log';
import CronService from '../services/cron';
import { UserRole } from '../data/user';
import { Crontab } from '../data/cron';
const route = Router();
const blacklist = ['.tmp'];
export default (app: Router) => {
app.use('/logs', route);
route.get('/', async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const result = await readDirs(config.logPath, config.logPath, blacklist);
// Filter logs based on user permissions
if (req.user?.role !== UserRole.admin && req.user?.userId) {
const cronService = Container.get(CronService);
const { data: userCrons } = await cronService.crontabs({
searchValue: '',
page: '0',
size: '0',
sorter: '',
filters: '',
queryString: '',
userId: req.user.userId,
});
// Build a set of log paths that the user has access to
const allowedLogPaths = new Set(
userCrons
.filter((cron: Crontab) => cron.log_name && cron.log_name !== '/dev/null')
.map((cron: Crontab) => cron.log_name)
);
// Filter the result to only include logs the user owns
const filteredResult = (result as IFile[]).filter((item: IFile) =>
item.type === 'directory' && (allowedLogPaths.has(item.title) || allowedLogPaths.has(`${item.title}/`))
);
res.send({
code: 200,
data: filteredResult,
});
return;
}
res.send({
code: 200,
data: result,
});
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
});
route.get(
'/detail',
async (req: Request, res: Response, next: NextFunction) => {
try {
const logService = Container.get(LogService);
const finalPath = logService.checkFilePath(
(req.query.path as string) || '',
(req.query.file as string) || '',
);
if (!finalPath || blacklist.includes(req.query.path as string)) {
return res.send({
code: 403,
message: '暂无权限',
});
}
// Check if user has permission to view this log
if (req.user?.role !== UserRole.admin && req.user?.userId) {
const cronService = Container.get(CronService);
const { data: userCrons } = await cronService.crontabs({
searchValue: '',
page: '0',
size: '0',
sorter: '',
filters: '',
queryString: '',
userId: req.user.userId,
});
const logPath = (req.query.path as string) || '';
const hasAccess = userCrons.some((cron: Crontab) =>
cron.log_name &&
cron.log_name !== '/dev/null' &&
(logPath.startsWith(cron.log_name) || cron.log_name.startsWith(logPath))
);
if (!hasAccess) {
return res.send({
code: 403,
message: '暂无权限',
});
}
}
const content = await getFileContentByName(finalPath);
res.send({ code: 200, data: removeAnsi(content) });
} catch (e) {
return next(e);
}
},
);
route.get(
'/:file',
async (req: Request, res: Response, next: NextFunction) => {
try {
const logService = Container.get(LogService);
const finalPath = logService.checkFilePath(
(req.query.path as string) || '',
(req.params.file as string) || '',
);
if (!finalPath || blacklist.includes(req.query.path as string)) {
return res.send({
code: 403,
message: '暂无权限',
});
}
// Check if user has permission to view this log
if (req.user?.role !== UserRole.admin && req.user?.userId) {
const cronService = Container.get(CronService);
const { data: userCrons } = await cronService.crontabs({
searchValue: '',
page: '0',
size: '0',
sorter: '',
filters: '',
queryString: '',
userId: req.user.userId,
});
const logPath = (req.query.path as string) || '';
const hasAccess = userCrons.some((cron: Crontab) =>
cron.log_name &&
cron.log_name !== '/dev/null' &&
(logPath.startsWith(cron.log_name) || cron.log_name.startsWith(logPath))
);
if (!hasAccess) {
return res.send({
code: 403,
message: '暂无权限',
});
}
}
const content = await getFileContentByName(finalPath);
res.send({ code: 200, data: content });
} catch (e) {
return next(e);
}
},
);
route.delete(
'/',
celebrate({
body: Joi.object({
filename: Joi.string().required(),
path: Joi.string().allow(''),
type: Joi.string().optional(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
try {
let { filename, path } = req.body as {
filename: string;
path: string;
};
const logService = Container.get(LogService);
const finalPath = logService.checkFilePath(path, filename);
if (!finalPath || blacklist.includes(path)) {
return res.send({
code: 403,
message: '暂无权限',
});
}
// Check if user has permission to delete this log
if (req.user?.role !== UserRole.admin && req.user?.userId) {
const cronService = Container.get(CronService);
const { data: userCrons } = await cronService.crontabs({
searchValue: '',
page: '0',
size: '0',
sorter: '',
filters: '',
queryString: '',
userId: req.user.userId,
});
const hasAccess = userCrons.some((cron: Crontab) =>
cron.log_name &&
cron.log_name !== '/dev/null' &&
(path.startsWith(cron.log_name) || cron.log_name.startsWith(path))
);
if (!hasAccess) {
return res.send({
code: 403,
message: '暂无权限',
});
}
}
await rmPath(finalPath);
res.send({ code: 200 });
} catch (e) {
return next(e);
}
},
);
route.post(
'/download',
celebrate({
body: Joi.object({
filename: Joi.string().required(),
path: Joi.string().allow(''),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
try {
let { filename, path } = req.body as {
filename: string;
path: string;
};
const logService = Container.get(LogService);
const filePath = logService.checkFilePath(path, filename);
if (!filePath) {
return res.send({
code: 403,
message: '暂无权限',
});
}
// Check if user has permission to download this log
if (req.user?.role !== UserRole.admin && req.user?.userId) {
const cronService = Container.get(CronService);
const { data: userCrons } = await cronService.crontabs({
searchValue: '',
page: '0',
size: '0',
sorter: '',
filters: '',
queryString: '',
userId: req.user.userId,
});
const hasAccess = userCrons.some((cron: Crontab) =>
cron.log_name &&
cron.log_name !== '/dev/null' &&
(path.startsWith(cron.log_name) || cron.log_name.startsWith(path))
);
if (!hasAccess) {
return res.send({
code: 403,
message: '暂无权限',
});
}
}
return res.download(filePath, filename, (err) => {
if (err) {
return next(err);
}
});
} catch (e) {
return next(e);
}
},
);
};