mirror of
https://github.com/whyour/qinglong.git
synced 2026-10-18 18:33:06 +08:00
Reject invalid extra_schedules with HTTP 400 before create or update reaches the task service. Preserve omitted, null and empty extras, supported cron syntax, and additional metadata. Localize missing rules, invalid string and array types, and non-object extra entries through the existing per-request translation middleware. Cover POST/PUT rejection without mutations, dynamic English/Chinese switching, and stable error paths. Validation: 9 related regression tests passed on current develop; backend build passed. The API validation fix also passed full regression and isolated develop/debian-dev acceptance. Refs #3099
103 lines
3.2 KiB
TypeScript
103 lines
3.2 KiB
TypeScript
import { Joi } from 'celebrate';
|
|
import { isValidCronSchedule } from '../shared/cronSchedule';
|
|
import { ScheduleType } from '../interface/schedule';
|
|
import path from 'path';
|
|
import config from '../config';
|
|
|
|
const validateSchedule = (value: string, helpers: any) => {
|
|
if (
|
|
value.startsWith(ScheduleType.ONCE) ||
|
|
value.startsWith(ScheduleType.BOOT)
|
|
) {
|
|
return value;
|
|
}
|
|
|
|
if (isValidCronSchedule(value)) return value;
|
|
return helpers.error('any.invalid');
|
|
};
|
|
|
|
export const scheduleSchema = Joi.string()
|
|
.required()
|
|
.custom(validateSchedule)
|
|
.messages({
|
|
'any.required': '定时规则不能为空',
|
|
'any.invalid': '无效的定时规则',
|
|
'string.empty': '定时规则不能为空',
|
|
'string.base': '定时规则必须是字符串',
|
|
});
|
|
|
|
export const commonCronSchema = {
|
|
name: Joi.string().optional(),
|
|
command: Joi.string().required(),
|
|
schedule: scheduleSchema,
|
|
labels: Joi.array().optional().allow(null),
|
|
sub_id: Joi.number().optional().allow(null),
|
|
extra_schedules: Joi.array()
|
|
.items(
|
|
Joi.object({
|
|
schedule: Joi.string()
|
|
.required()
|
|
.custom((value, helpers) =>
|
|
isValidCronSchedule(value) ? value : helpers.error('any.invalid'),
|
|
)
|
|
.messages({
|
|
'any.required': '定时规则不能为空',
|
|
'any.invalid': '无效的定时规则',
|
|
'string.empty': '定时规则不能为空',
|
|
'string.base': '定时规则必须是字符串',
|
|
}),
|
|
})
|
|
.unknown(true)
|
|
.messages({ 'object.base': '额外定时规则必须是对象' }),
|
|
)
|
|
.optional()
|
|
.allow(null)
|
|
.messages({ 'array.base': '额外定时规则必须是数组' }),
|
|
task_before: Joi.string().optional().allow('').allow(null),
|
|
task_after: Joi.string().optional().allow('').allow(null),
|
|
log_name: Joi.string()
|
|
.optional()
|
|
.allow('')
|
|
.allow(null)
|
|
.custom((value, helpers) => {
|
|
if (!value) return value;
|
|
|
|
// Check if it's an absolute path
|
|
if (value.startsWith('/')) {
|
|
// Allow /dev/null as special case
|
|
if (value === '/dev/null') {
|
|
return value;
|
|
}
|
|
|
|
// For other absolute paths, ensure they are within the safe log directory
|
|
const normalizedValue = path.normalize(value);
|
|
const normalizedLogPath = path.normalize(config.logPath);
|
|
|
|
if (!normalizedValue.startsWith(normalizedLogPath)) {
|
|
return helpers.error('string.unsafePath');
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
if (
|
|
!/^(?!.*(?:^|\/)\.{1,2}(?:\/|$))(?:\/)?(?:[\w\p{Script=Han}.-]+\/)*[\w\p{Script=Han}.-]+\/?$/u.test(
|
|
value,
|
|
)
|
|
) {
|
|
return helpers.error('string.pattern.base');
|
|
}
|
|
if (value.length > 100) {
|
|
return helpers.error('string.max');
|
|
}
|
|
return value;
|
|
})
|
|
.messages({
|
|
'string.pattern.base': '日志名称只能包含中文、字母、数字、下划线、连字符、点和路径分隔符',
|
|
'string.max': '日志名称不能超过100个字符',
|
|
'string.unsafePath': '绝对路径必须在日志目录内或使用 /dev/null',
|
|
}),
|
|
allow_multiple_instances: Joi.number().optional().valid(0, 1).allow(null),
|
|
work_dir: Joi.string().optional().allow('').allow(null),
|
|
};
|