fix(settings): проверять резервные копии и исключить секреты из журнала

This commit is contained in:
Denozordec committed 2026-10-03 14:36:12 +07:00
1 parent e5842b415b
commit ff8b04bddf
7 files changed
+78 -47

No files matched your search

+36 -38
View File
@@ -1,41 +1,39 @@
import { ApiClientError, requestBlob } from "@/shared/api/http-client"
const MAX_RESTORE_BYTES = 512 * 1024 * 1024
function parseFilename(contentDisposition: string | null, fallback: string): string {
if (!contentDisposition) return fallback
const utfMatch = /filename\*=UTF-8''([^;]+)/i.exec(contentDisposition)
if (utfMatch?.[1]) {
try {
return decodeURIComponent(utfMatch[1])
} catch {
return utfMatch[1]
}
}
const plainMatch = /filename="([^"]+)"/i.exec(contentDisposition)
if (plainMatch?.[1]) return plainMatch[1]
return fallback
import {z} from "zod"
import {ApiClientError,requestBlob,requestJson} from "@/shared/api/http-client"
const MAX_RESTORE_BYTES=512*1024*1024
const magic=[80,71,68,77,80]
const receiptSchema=z.object({ok:z.literal(true),eventError:z.string().min(1).optional(),listenerError:z.string().min(1).optional(),schedulerError:z.string().min(1).optional()}).strict()
export type SystemDatabaseRestoreReceipt=z.output<typeof receiptSchema>
type Options={signal?:AbortSignal}
const signal=(options?:Options)=>options?.signal?AbortSignal.any([options.signal,AbortSignal.timeout(600_000)]):AbortSignal.timeout(600_000)
function parseFilename(value:string|null):string{
let name="mikrotik-manager.dump"
if(value){
const utf=/filename\*=UTF-8''([^;]+)/i.exec(value)
const plain=/filename=(?:"([^"]+)"|([^;]+))/i.exec(value)
try{name=utf?.[1]?decodeURIComponent(utf[1]):(plain?.[1]??plain?.[2]??name).trim()}catch{throw new Error("Имя файла бэкапа не подтверждено")}
}
if(!name||/[\\/\r\n\x00]/.test(name)||name==="."||name==="..")throw new Error("Имя файла бэкапа не подтверждено")
return name
}
export async function downloadSystemDatabaseBackup(
baseUrl: string,
): Promise<{ blob: Blob; filename: string }> {
const res = await requestBlob(baseUrl, "/api/system/database/backup")
const blob = await res.blob()
const filename = parseFilename(res.headers.get("Content-Disposition"), "mikrotik-manager.db")
return { blob, filename }
async function confirmDump(blob:Blob):Promise<void>{
if(blob.size<magic.length)throw new Error("Файл не похож на резервную копию PostgreSQL (PGDMP)")
const head=new Uint8Array(await blob.slice(0,magic.length).arrayBuffer())
if(!magic.every((value,index)=>head[index]===value))throw new Error("Файл не похож на резервную копию PostgreSQL (PGDMP)")
}
export async function restoreSystemDatabaseBackup(baseUrl: string, file: File): Promise<void> {
if (file.size > MAX_RESTORE_BYTES) {
throw new ApiClientError(
`Файл больше ${MAX_RESTORE_BYTES / (1024 * 1024)} МБ — уменьшите бэкап или обратитесь к администратору`,
413,
)
}
await requestBlob(baseUrl, "/api/system/database/restore", {
method: "POST",
headers: { "Content-Type": "application/octet-stream" },
body: file,
})
export async function downloadSystemDatabaseBackup(baseUrl:string,options?:Options):Promise<{blob:Blob;filename:string}>{
const activeSignal=signal(options)
const response=await requestBlob(baseUrl,"/api/system/database/backup",{signal:activeSignal})
const blob=await response.blob()
await confirmDump(blob);activeSignal.throwIfAborted()
return {blob,filename:parseFilename(response.headers.get("Content-Disposition"))}
}
export async function restoreSystemDatabaseBackup(baseUrl:string,file:File,options?:Options):Promise<SystemDatabaseRestoreReceipt>{
if(file.size>MAX_RESTORE_BYTES)throw new ApiClientError("Файл больше 512 МБ — уменьшите бэкап или обратитесь к администратору",413)
const activeSignal=signal(options);activeSignal.throwIfAborted()
await confirmDump(file);activeSignal.throwIfAborted()
const raw=await requestJson<unknown>(baseUrl,"/api/system/database/restore",{method:"POST",headers:{"Content-Type":"application/octet-stream"},body:file,signal:activeSignal})
const receipt=receiptSchema.safeParse(raw)
if(!receipt.success)throw new Error("Восстановление базы не подтверждено сервером")
return receipt.data
}