|
|
|
|
|
|
|
|
|
|
|
|
|
|
class InputValidator { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
validateUsername(username) { |
|
|
if (!username || typeof username !== 'string') { |
|
|
throw new Error('用户名必须是非空字符串') |
|
|
} |
|
|
|
|
|
const trimmed = username.trim() |
|
|
|
|
|
|
|
|
if (trimmed.length < 3 || trimmed.length > 64) { |
|
|
throw new Error('用户名长度必须在3-64个字符之间') |
|
|
} |
|
|
|
|
|
|
|
|
const usernameRegex = /^[a-zA-Z0-9_-]+$/ |
|
|
if (!usernameRegex.test(trimmed)) { |
|
|
throw new Error('用户名只能包含字母、数字、下划线和连字符') |
|
|
} |
|
|
|
|
|
|
|
|
if (trimmed.startsWith('-') || trimmed.endsWith('-')) { |
|
|
throw new Error('用户名不能以连字符开头或结尾') |
|
|
} |
|
|
|
|
|
return trimmed |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
validateEmail(email) { |
|
|
if (!email || typeof email !== 'string') { |
|
|
throw new Error('电子邮件必须是非空字符串') |
|
|
} |
|
|
|
|
|
const trimmed = email.trim().toLowerCase() |
|
|
|
|
|
|
|
|
const emailRegex = |
|
|
/^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/ |
|
|
if (!emailRegex.test(trimmed)) { |
|
|
throw new Error('电子邮件格式无效') |
|
|
} |
|
|
|
|
|
|
|
|
if (trimmed.length > 254) { |
|
|
throw new Error('电子邮件地址过长') |
|
|
} |
|
|
|
|
|
return trimmed |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
validatePassword(password) { |
|
|
if (!password || typeof password !== 'string') { |
|
|
throw new Error('密码必须是非空字符串') |
|
|
} |
|
|
|
|
|
|
|
|
if (password.length < 8) { |
|
|
throw new Error('密码至少需要8个字符') |
|
|
} |
|
|
|
|
|
|
|
|
if (password.length > 128) { |
|
|
throw new Error('密码不能超过128个字符') |
|
|
} |
|
|
|
|
|
return true |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
validateRole(role) { |
|
|
const validRoles = ['admin', 'user', 'viewer'] |
|
|
|
|
|
if (!role || typeof role !== 'string') { |
|
|
throw new Error('角色必须是非空字符串') |
|
|
} |
|
|
|
|
|
const trimmed = role.trim().toLowerCase() |
|
|
|
|
|
if (!validRoles.includes(trimmed)) { |
|
|
throw new Error(`角色必须是以下之一: ${validRoles.join(', ')}`) |
|
|
} |
|
|
|
|
|
return trimmed |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
validateWebhookUrl(url) { |
|
|
if (!url || typeof url !== 'string') { |
|
|
throw new Error('Webhook URL必须是非空字符串') |
|
|
} |
|
|
|
|
|
const trimmed = url.trim() |
|
|
|
|
|
|
|
|
try { |
|
|
const urlObj = new URL(trimmed) |
|
|
|
|
|
|
|
|
if (!['http:', 'https:'].includes(urlObj.protocol)) { |
|
|
throw new Error('Webhook URL必须使用HTTP或HTTPS协议') |
|
|
} |
|
|
|
|
|
|
|
|
const hostname = urlObj.hostname.toLowerCase() |
|
|
const dangerousHosts = [ |
|
|
'localhost', |
|
|
'127.0.0.1', |
|
|
'0.0.0.0', |
|
|
'::1', |
|
|
'169.254.169.254', |
|
|
'metadata.google.internal' |
|
|
] |
|
|
|
|
|
if (dangerousHosts.includes(hostname)) { |
|
|
throw new Error('Webhook URL不能指向内部服务') |
|
|
} |
|
|
|
|
|
|
|
|
const ipRegex = /^(\d{1,3}\.){3}\d{1,3}$/ |
|
|
if (ipRegex.test(hostname)) { |
|
|
const parts = hostname.split('.').map(Number) |
|
|
|
|
|
|
|
|
if ( |
|
|
parts[0] === 10 || |
|
|
(parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) || |
|
|
(parts[0] === 192 && parts[1] === 168) |
|
|
) { |
|
|
throw new Error('Webhook URL不能指向私有IP地址') |
|
|
} |
|
|
} |
|
|
|
|
|
return trimmed |
|
|
} catch (error) { |
|
|
if (error.message.includes('Webhook URL')) { |
|
|
throw error |
|
|
} |
|
|
throw new Error('Webhook URL格式无效') |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
validateDisplayName(displayName) { |
|
|
if (!displayName || typeof displayName !== 'string') { |
|
|
throw new Error('显示名称必须是非空字符串') |
|
|
} |
|
|
|
|
|
const trimmed = displayName.trim() |
|
|
|
|
|
|
|
|
if (trimmed.length < 1 || trimmed.length > 100) { |
|
|
throw new Error('显示名称长度必须在1-100个字符之间') |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const controlCharRegex = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/ |
|
|
if (controlCharRegex.test(trimmed)) { |
|
|
throw new Error('显示名称不能包含控制字符') |
|
|
} |
|
|
|
|
|
return trimmed |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
sanitizeHtml(input) { |
|
|
if (!input || typeof input !== 'string') { |
|
|
return '' |
|
|
} |
|
|
|
|
|
return input |
|
|
.replace(/&/g, '&') |
|
|
.replace(/</g, '<') |
|
|
.replace(/>/g, '>') |
|
|
.replace(/"/g, '"') |
|
|
.replace(/'/g, ''') |
|
|
.replace(/\//g, '/') |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
validateApiKeyName(name) { |
|
|
if (!name || typeof name !== 'string') { |
|
|
throw new Error('API Key名称必须是非空字符串') |
|
|
} |
|
|
|
|
|
const trimmed = name.trim() |
|
|
|
|
|
|
|
|
if (trimmed.length < 1 || trimmed.length > 100) { |
|
|
throw new Error('API Key名称长度必须在1-100个字符之间') |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const controlCharRegex = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/ |
|
|
if (controlCharRegex.test(trimmed)) { |
|
|
throw new Error('API Key名称不能包含控制字符') |
|
|
} |
|
|
|
|
|
return trimmed |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
validatePagination(page, limit) { |
|
|
const pageNum = parseInt(page, 10) || 1 |
|
|
const limitNum = parseInt(limit, 10) || 20 |
|
|
|
|
|
if (pageNum < 1) { |
|
|
throw new Error('页码必须大于0') |
|
|
} |
|
|
|
|
|
if (limitNum < 1 || limitNum > 100) { |
|
|
throw new Error('每页数量必须在1-100之间') |
|
|
} |
|
|
|
|
|
return { |
|
|
page: pageNum, |
|
|
limit: limitNum |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
validateUuid(uuid) { |
|
|
if (!uuid || typeof uuid !== 'string') { |
|
|
throw new Error('UUID必须是非空字符串') |
|
|
} |
|
|
|
|
|
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i |
|
|
if (!uuidRegex.test(uuid)) { |
|
|
throw new Error('UUID格式无效') |
|
|
} |
|
|
|
|
|
return uuid.toLowerCase() |
|
|
} |
|
|
} |
|
|
|
|
|
module.exports = new InputValidator() |
|
|
|