From c418ea01e280899175d227a8256ce3792271cdcc Mon Sep 17 00:00:00 2001 From: Charles GTE Date: Sat, 11 Jul 2026 09:59:06 +0200 Subject: [PATCH] fix: log job when storage file missing for restoration --- app/api/agent/[agentId]/status/route.ts | 16 +-- src/features/agents/types/index.ts | 13 +++ .../utils/status/restoration.helpers.ts | 37 ++++++ .../agents/utils/status/status.helpers.ts | 110 ++---------------- .../utils/status/storage-channels.helpers.ts | 64 ++++++++++ .../status/storage-encryption.helpers.ts | 39 +++++++ 6 files changed, 164 insertions(+), 115 deletions(-) create mode 100644 src/features/agents/types/index.ts create mode 100644 src/features/agents/utils/status/restoration.helpers.ts rename app/api/agent/[agentId]/status/helpers.ts => src/features/agents/utils/status/status.helpers.ts (72%) create mode 100644 src/features/agents/utils/status/storage-channels.helpers.ts create mode 100644 src/features/agents/utils/status/storage-encryption.helpers.ts diff --git a/app/api/agent/[agentId]/status/route.ts b/app/api/agent/[agentId]/status/route.ts index e5362d03..573409fe 100644 --- a/app/api/agent/[agentId]/status/route.ts +++ b/app/api/agent/[agentId]/status/route.ts @@ -1,8 +1,8 @@ import { NextResponse } from "next/server"; -import { handleDatabases } from "./helpers"; +import { handleDatabases } from "@/features/agents/utils/status/status.helpers"; +import { Body } from "@/features/agents/types"; import * as drizzleDb from "@/db"; import { db } from "@/db"; -import { EDbmsSchema } from "@/db/schema/types"; import { and, eq } from "drizzle-orm"; import { withUpdatedAt } from "@/db/utils"; import { logger } from "@/lib/logger"; @@ -10,18 +10,6 @@ import { isUUID } from "@/utils/text"; const log = logger.child({ module: "api/agent/status/route" }); -export type databaseAgent = { - name: string; - dbms: EDbmsSchema; - generatedId: string; - pingStatus: boolean; -}; - -export type Body = { - version: string; - databases: databaseAgent[]; -}; - export async function POST( request: Request, { params }: { params: Promise<{ agentId: string }> }, diff --git a/src/features/agents/types/index.ts b/src/features/agents/types/index.ts new file mode 100644 index 00000000..6dbde703 --- /dev/null +++ b/src/features/agents/types/index.ts @@ -0,0 +1,13 @@ +import {EDbmsSchema} from "@/db/schema/types"; + +export type databaseAgent = { + name: string; + dbms: EDbmsSchema; + generatedId: string; + pingStatus: boolean; +}; + +export type Body = { + version: string; + databases: databaseAgent[]; +}; diff --git a/src/features/agents/utils/status/restoration.helpers.ts b/src/features/agents/utils/status/restoration.helpers.ts new file mode 100644 index 00000000..0a05b553 --- /dev/null +++ b/src/features/agents/utils/status/restoration.helpers.ts @@ -0,0 +1,37 @@ +import * as drizzleDb from "@/db"; +import {db as dbClient} from "@/db"; +import {eq} from "drizzle-orm"; +import {logger} from "@/lib/logger"; +import {sendNotificationsBackupRestore} from "@/features/notifications/utils/notifications.helpers"; + +const log = logger.child({module: "api/agent/status/restoration"}); + +export async function handleFailedRestoration(restorationId: string, databaseId: string, reason: string): Promise { + try { + await dbClient + .insert(drizzleDb.schemas.jobLog) + .values({ + backupId: null, + restorationId, + loggedAt: new Date(), + entryType: "log", + level: "error", + message: reason, + command: null, + output: null, + exitCode: null, + durationMs: null, + }); + + const databaseWithPolicies = await dbClient.query.database.findFirst({ + where: eq(drizzleDb.schemas.database.id, databaseId), + with: {alertPolicies: true}, + }); + + if (databaseWithPolicies) { + await sendNotificationsBackupRestore(databaseWithPolicies, "error_restore"); + } + } catch (err) { + log.error({error: err, name: "handleFailedRestoration"}, "Failed to record restoration failure job log / notification"); + } +} diff --git a/app/api/agent/[agentId]/status/helpers.ts b/src/features/agents/utils/status/status.helpers.ts similarity index 72% rename from app/api/agent/[agentId]/status/helpers.ts rename to src/features/agents/utils/status/status.helpers.ts index d7478050..440c771c 100644 --- a/app/api/agent/[agentId]/status/helpers.ts +++ b/src/features/agents/utils/status/status.helpers.ts @@ -1,9 +1,9 @@ import {NextResponse} from "next/server"; -import {Body} from "./route"; +import {Body} from "@/features/agents/types"; import {Agent} from "@/db/schema/08_agent"; import {DatabaseWith} from "@/db/schema/07_database"; import * as drizzleDb from "@/db"; -import {db, db as dbClient} from "@/db"; +import {db as dbClient} from "@/db"; import {and, eq, inArray, desc, sql} from "drizzle-orm"; import {dbmsEnumSchema, EDbmsSchema} from "@/db/schema/types"; import {withUpdatedAt} from "@/db/utils"; @@ -13,7 +13,9 @@ import {isUUID} from "@/utils/text"; import {StorageInput} from "@/features/storages/types"; import {dispatchStorage} from "@/features/storages/utils/storages.dispatch"; import {getMasterServerKeyContent} from "@/features/agents/actions/keys.action"; -import {encryptStorages, isAgentVersionAtLeast, MIN_AGENT_VERSION_STORAGE_ENC} from "@/utils/status-crypto"; +import {getDatabaseStorageChannels, PingDatabaseStorageChannels} from "./storage-channels.helpers"; +import {applyStorageEncryption} from "./storage-encryption.helpers"; +import {handleFailedRestoration} from "./restoration.helpers"; const log = logger.child({module: "api/agent/status/helpers"}); @@ -202,6 +204,9 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat const errorMessage = "Failed to get backup URL"; log.error({error: errorMessage, name: "handleDatabases"}, "Restoration failed"); + + await handleFailedRestoration(restoration.id, databaseUpdated.id, errorMessage); + continue; } } catch (err) { @@ -210,6 +215,7 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat .update(drizzleDb.schemas.restoration) .set(withUpdatedAt({status: "failed"})) .where(eq(drizzleDb.schemas.restoration.id, restoration.id)); + await handleFailedRestoration(restoration.id, databaseUpdated.id, "Restoration crashed unexpectedly"); continue; } @@ -226,101 +232,3 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat } return databasesResponse; } - - -type PingDatabaseStorageChannels = { - id: string; - config: any - provider: string -} - -async function getDatabaseStorageChannels(databaseId: string): Promise { - - const database = await db.query.database.findFirst({ - where: eq(drizzleDb.schemas.database.id, databaseId), - with: { - project: true, - retentionPolicy: true, - alertPolicies: true, - storagePolicies: true - } - }); - - if (!database) { - return [] - } - - const settings = await db.query.setting.findFirst({ - where: eq(drizzleDb.schemas.setting.name, "system"), - with: {storageChannel: true}, - }); - - const defaultStorageChannel: PingDatabaseStorageChannels[] = settings?.storageChannel - ? [{ - id: settings.storageChannel.id, - provider: settings.storageChannel.provider, - config: settings.storageChannel.config, - }] - : []; - - - const enabledDatabaseStorageChannels = await Promise.all( - (database.storagePolicies ?? []) - .filter(p => p.enabled) - .map(async policy => { - const storageChannel = await db.query.storageChannel.findFirst({ - where: eq(drizzleDb.schemas.storageChannel.id, policy.storageChannelId), - }); - - if (!storageChannel) return null; - - return { - id: storageChannel.id, - config: storageChannel.config, - provider: storageChannel.provider, - } as PingDatabaseStorageChannels; - }) - ); - - const filteredChannels: PingDatabaseStorageChannels[] = enabledDatabaseStorageChannels.filter( - (c): c is PingDatabaseStorageChannels => c !== null - ); - - return filteredChannels.length > 0 ? filteredChannels : defaultStorageChannel; -} - -function applyStorageEncryption( - entry: Record, - version: string | undefined, - masterKey: Buffer | null, - agentId: string, -): void { - if (!masterKey) return; - if (!Array.isArray(entry.storages) || entry.storages.length === 0) return; - if (!isAgentVersionAtLeast(version, MIN_AGENT_VERSION_STORAGE_ENC)) { - log.warn( - { - name: "applyStorageEncryption", - agentId, - agentVersion: version ?? "unknown", - requiredVersion: MIN_AGENT_VERSION_STORAGE_ENC, - }, - `\n============================================================\n` + - ` ⚠️ OUTDATED AGENT — STORAGE CREDENTIALS SENT UNENCRYPTED\n` + - ` Agent ${agentId} reports v${version ?? "unknown"} (< required v${MIN_AGENT_VERSION_STORAGE_ENC}).\n` + - ` Update this agent to v${MIN_AGENT_VERSION_STORAGE_ENC}+ to encrypt storage credentials in transit.\n` + - `============================================================`, - ); - return; - } - - try { - const ciphertext = encryptStorages(entry.storages, masterKey); - entry.storages_ciphertext = ciphertext; - entry.storages_encrypted = true; - entry.storages = []; - } catch (err) { - log.error({error: err, name: "applyStorageEncryption"}, "Storage encryption failed; sending plaintext"); - } -} - diff --git a/src/features/agents/utils/status/storage-channels.helpers.ts b/src/features/agents/utils/status/storage-channels.helpers.ts new file mode 100644 index 00000000..c30bd373 --- /dev/null +++ b/src/features/agents/utils/status/storage-channels.helpers.ts @@ -0,0 +1,64 @@ +import * as drizzleDb from "@/db"; +import {db} from "@/db"; +import {eq} from "drizzle-orm"; + +export type PingDatabaseStorageChannels = { + id: string; + config: any + provider: string +} + +export async function getDatabaseStorageChannels(databaseId: string): Promise { + + const database = await db.query.database.findFirst({ + where: eq(drizzleDb.schemas.database.id, databaseId), + with: { + project: true, + retentionPolicy: true, + alertPolicies: true, + storagePolicies: true + } + }); + + if (!database) { + return [] + } + + const settings = await db.query.setting.findFirst({ + where: eq(drizzleDb.schemas.setting.name, "system"), + with: {storageChannel: true}, + }); + + const defaultStorageChannel: PingDatabaseStorageChannels[] = settings?.storageChannel + ? [{ + id: settings.storageChannel.id, + provider: settings.storageChannel.provider, + config: settings.storageChannel.config, + }] + : []; + + + const enabledDatabaseStorageChannels = await Promise.all( + (database.storagePolicies ?? []) + .filter(p => p.enabled) + .map(async policy => { + const storageChannel = await db.query.storageChannel.findFirst({ + where: eq(drizzleDb.schemas.storageChannel.id, policy.storageChannelId), + }); + + if (!storageChannel) return null; + + return { + id: storageChannel.id, + config: storageChannel.config, + provider: storageChannel.provider, + } as PingDatabaseStorageChannels; + }) + ); + + const filteredChannels: PingDatabaseStorageChannels[] = enabledDatabaseStorageChannels.filter( + (c): c is PingDatabaseStorageChannels => c !== null + ); + + return filteredChannels.length > 0 ? filteredChannels : defaultStorageChannel; +} diff --git a/src/features/agents/utils/status/storage-encryption.helpers.ts b/src/features/agents/utils/status/storage-encryption.helpers.ts new file mode 100644 index 00000000..62475af0 --- /dev/null +++ b/src/features/agents/utils/status/storage-encryption.helpers.ts @@ -0,0 +1,39 @@ +import {logger} from "@/lib/logger"; +import {encryptStorages, isAgentVersionAtLeast, MIN_AGENT_VERSION_STORAGE_ENC} from "@/utils/status-crypto"; + +const log = logger.child({module: "api/agent/status/storage-encryption"}); + +export function applyStorageEncryption( + entry: Record, + version: string | undefined, + masterKey: Buffer | null, + agentId: string, +): void { + if (!masterKey) return; + if (!Array.isArray(entry.storages) || entry.storages.length === 0) return; + if (!isAgentVersionAtLeast(version, MIN_AGENT_VERSION_STORAGE_ENC)) { + log.warn( + { + name: "applyStorageEncryption", + agentId, + agentVersion: version ?? "unknown", + requiredVersion: MIN_AGENT_VERSION_STORAGE_ENC, + }, + `\n============================================================\n` + + ` ⚠️ OUTDATED AGENT — STORAGE CREDENTIALS SENT UNENCRYPTED\n` + + ` Agent ${agentId} reports v${version ?? "unknown"} (< required v${MIN_AGENT_VERSION_STORAGE_ENC}).\n` + + ` Update this agent to v${MIN_AGENT_VERSION_STORAGE_ENC}+ to encrypt storage credentials in transit.\n` + + `============================================================`, + ); + return; + } + + try { + const ciphertext = encryptStorages(entry.storages, masterKey); + entry.storages_ciphertext = ciphertext; + entry.storages_encrypted = true; + entry.storages = []; + } catch (err) { + log.error({error: err, name: "applyStorageEncryption"}, "Storage encryption failed; sending plaintext"); + } +}