fix(api): fail-safe прод-старт, транзакции и санитизация install-скриптов
- прод-режим отказывается стартовать без AUTH_REQUIRED и реальных секретов (opt-out через EVOFW_ALLOW_UNSAFE) - CORS: whitelist через CORS_ORIGINS вместо origin:true; CSP для раздаваемого SPA - транзакции для setAgentPolicySets, reorderPolicyRules, replaceResolvedForRule, replaceIpListEntries - install-скрипты: Zod-валидация имени ссылки, экранирование $ и контрольных символов в RouterOS-рендере - constant-time сравнение enroll-seed - опциональное шифрование токена EvoBGP в БД (EVOFW_SECRET_KEY, AES-256-GCM) и маскирование per-list api_token в ответах - graceful shutdown (SIGTERM/SIGINT) + тесты
This commit is contained in:
@@ -60,17 +60,19 @@ export function countEntriesByListIds(
|
||||
}
|
||||
|
||||
export function replaceIpListEntries(db: Db, listId: string, cidrs: string[]) {
|
||||
db.delete(ipListEntries).where(eq(ipListEntries.listId, listId)).run()
|
||||
if (cidrs.length === 0) return
|
||||
const now = new Date().toISOString()
|
||||
db.insert(ipListEntries)
|
||||
.values(
|
||||
cidrs.map((cidr) => ({
|
||||
id: crypto.randomUUID(),
|
||||
listId,
|
||||
cidr,
|
||||
createdAt: now,
|
||||
})),
|
||||
)
|
||||
.run()
|
||||
db.transaction((tx) => {
|
||||
tx.delete(ipListEntries).where(eq(ipListEntries.listId, listId)).run()
|
||||
if (cidrs.length === 0) return
|
||||
const now = new Date().toISOString()
|
||||
tx.insert(ipListEntries)
|
||||
.values(
|
||||
cidrs.map((cidr) => ({
|
||||
id: crypto.randomUUID(),
|
||||
listId,
|
||||
cidr,
|
||||
createdAt: now,
|
||||
})),
|
||||
)
|
||||
.run()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -170,11 +170,13 @@ export function setAgentPolicySets(db: Db, agentId: string, setIds: string[]) {
|
||||
}
|
||||
}
|
||||
|
||||
db.delete(agentPolicySets).where(eq(agentPolicySets.agentId, agentId)).run()
|
||||
setIds.forEach((setId, i) => {
|
||||
db.insert(agentPolicySets)
|
||||
.values({ agentId, setId, sort: i * 10 })
|
||||
.run()
|
||||
db.transaction((tx) => {
|
||||
tx.delete(agentPolicySets).where(eq(agentPolicySets.agentId, agentId)).run()
|
||||
setIds.forEach((setId, i) => {
|
||||
tx.insert(agentPolicySets)
|
||||
.values({ agentId, setId, sort: i * 10 })
|
||||
.run()
|
||||
})
|
||||
})
|
||||
bumpAgentGeneration(db, agentId)
|
||||
}
|
||||
@@ -271,20 +273,22 @@ export function reorderPolicyRules(
|
||||
throw new Error('ordered_ids must list every rule in the set exactly once')
|
||||
}
|
||||
// Temporary priorities to avoid UNIQUE collisions
|
||||
orderedIds.forEach((id, i) => {
|
||||
db.update(policyRules)
|
||||
.set({ priority: 9000 + i, updatedAt: new Date().toISOString() })
|
||||
.where(eq(policyRules.id, id))
|
||||
.run()
|
||||
})
|
||||
orderedIds.forEach((id, i) => {
|
||||
db.update(policyRules)
|
||||
.set({
|
||||
priority: (i + 1) * 10,
|
||||
updatedAt: new Date().toISOString(),
|
||||
})
|
||||
.where(eq(policyRules.id, id))
|
||||
.run()
|
||||
db.transaction((tx) => {
|
||||
orderedIds.forEach((id, i) => {
|
||||
tx.update(policyRules)
|
||||
.set({ priority: 9000 + i, updatedAt: new Date().toISOString() })
|
||||
.where(eq(policyRules.id, id))
|
||||
.run()
|
||||
})
|
||||
orderedIds.forEach((id, i) => {
|
||||
tx.update(policyRules)
|
||||
.set({
|
||||
priority: (i + 1) * 10,
|
||||
updatedAt: new Date().toISOString(),
|
||||
})
|
||||
.where(eq(policyRules.id, id))
|
||||
.run()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -316,21 +320,23 @@ export function listResolvedForRule(db: Db, ruleId: string) {
|
||||
}
|
||||
|
||||
export function replaceResolvedForRule(db: Db, ruleId: string, cidrs: string[]) {
|
||||
db.delete(policyRuleResolved)
|
||||
.where(eq(policyRuleResolved.ruleId, ruleId))
|
||||
.run()
|
||||
if (cidrs.length === 0) return
|
||||
const now = new Date().toISOString()
|
||||
db.insert(policyRuleResolved)
|
||||
.values(
|
||||
cidrs.map((cidr) => ({
|
||||
id: crypto.randomUUID(),
|
||||
ruleId,
|
||||
cidr,
|
||||
resolvedAt: now,
|
||||
})),
|
||||
)
|
||||
.run()
|
||||
db.transaction((tx) => {
|
||||
tx.delete(policyRuleResolved)
|
||||
.where(eq(policyRuleResolved.ruleId, ruleId))
|
||||
.run()
|
||||
if (cidrs.length === 0) return
|
||||
const now = new Date().toISOString()
|
||||
tx.insert(policyRuleResolved)
|
||||
.values(
|
||||
cidrs.map((cidr) => ({
|
||||
id: crypto.randomUUID(),
|
||||
ruleId,
|
||||
cidr,
|
||||
resolvedAt: now,
|
||||
})),
|
||||
)
|
||||
.run()
|
||||
})
|
||||
}
|
||||
|
||||
export function listOverrides(db: Db, agentId: string) {
|
||||
|
||||
@@ -492,8 +492,22 @@ export const dashboardStatsSchema = z.object({
|
||||
lists_total: z.number().int(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Install-link client name. The name is embedded into generated bash /
|
||||
* RouterOS install scripts, so it is limited to safe printable characters.
|
||||
*/
|
||||
export const installLinkNameSchema = z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.max(64)
|
||||
.regex(
|
||||
/^[\p{L}\p{N}][\p{L}\p{N} .:_-]*$/u,
|
||||
'Имя может содержать буквы, цифры и символы . : _ - (без перевода строк и кавычек)',
|
||||
)
|
||||
|
||||
export const createInstallLinkBodySchema = z.object({
|
||||
name: z.string().min(1),
|
||||
name: installLinkNameSchema,
|
||||
platform: agentPlatformSchema.optional().default('linux'),
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user