diff --git a/app/api/agent/[agentId]/backup/route.ts b/app/api/agent/[agentId]/backup/route.ts index 07c51664..ef9cb150 100644 --- a/app/api/agent/[agentId]/backup/route.ts +++ b/app/api/agent/[agentId]/backup/route.ts @@ -1,286 +1,169 @@ -// import {NextResponse} from "next/server"; -// import {isUuidv4} from "@/utils/verify-uuid"; -// import {v4 as uuidv4} from "uuid"; -// import * as drizzleDb from "@/db"; -// import {db} from "@/db"; -// import {Backup} from "@/db/schema/07_database"; -// import {and, eq} from "drizzle-orm"; -// import {withUpdatedAt} from "@/db/utils"; -// import {decryptedDump, getFileExtension} from "./helpers"; -// import {sendNotificationsBackupRestore} from "@/features/notifications/helpers"; -// import {storeBackupFiles} from "@/features/storages/helpers"; -// import {eventEmitter} from "@/features/shared/event"; -// -// export async function POST( -// request: Request, -// {params}: { params: Promise<{ agentId: string }> } -// ) { -// try { -// const contentType = request.headers.get("Content-Type"); -// -// if (!contentType || !contentType.includes("multipart/form-data")) { -// return NextResponse.json( -// {error: "Unsupported or missing Content-Type"}, -// {status: 400} -// ); -// } -// -// eventEmitter.emit('modification', {update: true}); -// -// const agentId = (await params).agentId; -// const formData = await request.formData(); -// const aesKeyHex = formData.get("aes_key") as string; -// const ivHex = formData.get("iv") as string; -// const generatedId = formData.get("generatedId") as string | null; -// const method = formData.get("method") as string | null; -// -// -// if (!generatedId || !isUuidv4(generatedId)) { -// return NextResponse.json( -// {error: "generatedId is not a valid UUID"}, -// {status: 400} -// ); -// } -// -// const agent = await db.query.agent.findFirst({ -// where: eq(drizzleDb.schemas.agent.id, agentId), -// }); -// -// if (!agent) { -// return NextResponse.json( -// {error: "Agent not found"}, -// {status: 404} -// ); -// } -// -// const database = await db.query.database.findFirst({ -// where: eq(drizzleDb.schemas.database.agentDatabaseId, generatedId), -// with: { -// project: true, -// alertPolicies: true, -// storagePolicies: true -// } -// }); -// -// if (!database) { -// return NextResponse.json( -// {error: "Database associated with generatedId not found"}, -// {status: 404} -// ); -// } -// -// let backup: Backup | null | undefined = null; -// -// if (method === "automatic") { -// [backup] = await db -// .insert(drizzleDb.schemas.backup) -// .values({ -// status: 'ongoing', -// databaseId: database.id, -// }) -// .returning(); -// -// -// if (!backup) { -// return NextResponse.json( -// {error: "Unable to create an automatic backup"}, -// {status: 500} -// ); -// } -// } else { -// backup = await db.query.backup.findFirst({ -// where: and( -// eq(drizzleDb.schemas.backup.status, 'ongoing'), -// eq(drizzleDb.schemas.backup.databaseId, database.id), -// ), -// }); -// -// -// if (!backup) { -// return NextResponse.json( -// {error: "Unable to find the corresponding backup"}, -// {status: 404} -// ); -// } -// } -// -// const status = formData.get("status") as string | null; -// -// if (status === "success") { -// const file = formData.get("file") as File | null; -// const extension = formData.get("extension") as string | null; -// if (!aesKeyHex || !ivHex) { -// return NextResponse.json({error: "Missing fields"}, {status: 400}); -// } -// -// -// if (!file) { -// return NextResponse.json( -// {error: "File is required for successful backup"}, -// {status: 400} -// ); -// } -// const fileSizeBytes = file.size; -// // const fileExtension = '.' + (file.name.split('.').pop()?.toLowerCase() || ''); -// const fileExtension = extension ? extension : getFileExtension(database.dbms) -// const decryptedFile = await decryptedDump(file, aesKeyHex, ivHex, fileExtension); -// const uuid = uuidv4(); -// const fileName = `${uuid}${fileExtension}`; -// const buffer = Buffer.from(await decryptedFile.arrayBuffer()); -// -// -// const storageResults = await storeBackupFiles(backup, database, buffer, fileName) -// eventEmitter.emit('modification', {update: true}); -// -// await sendNotificationsBackupRestore(database, "success_backup"); -// -// return NextResponse.json( -// { -// message: "Backup successfully uploaded", -// }, -// {status: 200} -// ); -// } else { -// -// await db -// .update(drizzleDb.schemas.backup) -// .set(withUpdatedAt({status: 'failed'})) -// .where(eq(drizzleDb.schemas.backup.id, backup.id)); -// -// eventEmitter.emit('modification', {update: true}); -// -// await sendNotificationsBackupRestore(database, "error_backup"); -// -// -// return NextResponse.json( -// { -// message: "Backup successfully updated with status failed", -// }, -// {status: 200} -// ); -// } -// } catch (error) { -// console.error("Error in POST handler:", error); -// return NextResponse.json( -// {error: "Internal server error"}, -// {status: 500} -// ); -// } -// } - - -import { NextResponse } from "next/server"; -import { v4 as uuidv4 } from "uuid"; -import { and, eq } from "drizzle-orm"; +import {NextResponse} from "next/server"; +import {isUuidv4} from "@/utils/verify-uuid"; +import {v4 as uuidv4} from "uuid"; import * as drizzleDb from "@/db"; -import { db } from "@/db"; -import { isUuidv4 } from "@/utils/verify-uuid"; -import uploadTempFileToProviders, { createDecryptionStream } from "@/features/api/upload/helpers/common"; -import { getFileExtension, saveStreamToTempFile } from "@/features/api/upload/helpers/file"; -import { Backup } from "@/db/schema/07_database"; -import { withUpdatedAt } from "@/db/utils"; -import { eventEmitter } from "@/features/shared/event"; -import { sendNotificationsBackupRestore } from "@/features/notifications/helpers"; -import {Readable} from "node:stream"; +import {db} from "@/db"; +import {Backup} from "@/db/schema/07_database"; +import {and, eq} from "drizzle-orm"; +import {withUpdatedAt} from "@/db/utils"; +import {decryptedDump, getFileExtension} from "./helpers"; +import {sendNotificationsBackupRestore} from "@/features/notifications/helpers"; +import {storeBackupFiles} from "@/features/storages/helpers"; +import {eventEmitter} from "@/features/shared/event"; -export async function POST(request: Request, { params }: { params: Promise<{ agentId: string }> }) { +export async function POST( + request: Request, + {params}: { params: Promise<{ agentId: string }> } +) { try { - console.log("POST"); - console.log("data"); - const agentId = (await params).agentId; - const headers = request.headers; + const contentType = request.headers.get("Content-Type"); - console.log("headers", headers); - console.log("agentId", agentId); - - const generatedId = headers.get("x-generated-id"); - const status = headers.get("x-status"); - const encryptedAesKeyHex = headers.get("x-aes-key"); - const ivHex = headers.get("x-iv"); - const method = headers.get("x-method") ?? "manual"; - const extension = headers.get("x-extension"); - - if (!agentId || !generatedId || !status || !encryptedAesKeyHex || !ivHex) { - return NextResponse.json({ error: "Missing required headers/params" }, { status: 400 }); + if (!contentType || !contentType.includes("multipart/form-data")) { + return NextResponse.json( + {error: "Unsupported or missing Content-Type"}, + {status: 400} + ); } - if (!isUuidv4(generatedId)) { - return NextResponse.json({ error: "generatedId is not a valid UUID" }, { status: 400 }); + eventEmitter.emit('modification', {update: true}); + + const agentId = (await params).agentId; + const formData = await request.formData(); + const aesKeyHex = formData.get("aes_key") as string; + const ivHex = formData.get("iv") as string; + const generatedId = formData.get("generatedId") as string | null; + const method = formData.get("method") as string | null; + + + if (!generatedId || !isUuidv4(generatedId)) { + return NextResponse.json( + {error: "generatedId is not a valid UUID"}, + {status: 400} + ); } const agent = await db.query.agent.findFirst({ where: eq(drizzleDb.schemas.agent.id, agentId), }); + if (!agent) { - return NextResponse.json({ error: "Agent not found" }, { status: 404 }); + return NextResponse.json( + {error: "Agent not found"}, + {status: 404} + ); } const database = await db.query.database.findFirst({ where: eq(drizzleDb.schemas.database.agentDatabaseId, generatedId), - with: { project: true, storagePolicies: true }, + with: { + project: true, + alertPolicies: true, + storagePolicies: true + } }); + if (!database) { - return NextResponse.json({ error: "Database not found" }, { status: 404 }); + return NextResponse.json( + {error: "Database associated with generatedId not found"}, + {status: 404} + ); } let backup: Backup | null | undefined = null; + if (method === "automatic") { [backup] = await db .insert(drizzleDb.schemas.backup) - .values({ status: "ongoing", databaseId: database.id }) + .values({ + status: 'ongoing', + databaseId: database.id, + }) .returning(); + + + if (!backup) { + return NextResponse.json( + {error: "Unable to create an automatic backup"}, + {status: 500} + ); + } } else { backup = await db.query.backup.findFirst({ where: and( + eq(drizzleDb.schemas.backup.status, 'ongoing'), eq(drizzleDb.schemas.backup.databaseId, database.id), - eq(drizzleDb.schemas.backup.status, "ongoing") ), }); + + + if (!backup) { + return NextResponse.json( + {error: "Unable to find the corresponding backup"}, + {status: 404} + ); + } } - if (!backup) { - return NextResponse.json({ error: "Backup not found" }, { status: 404 }); - } + const status = formData.get("status") as string | null; if (status === "success") { - - const decipher = createDecryptionStream(encryptedAesKeyHex, ivHex); - const fileExt = extension || getFileExtension(database.dbms); - const fileName = `${uuidv4()}${fileExt}`; - - - const nodeStream = webStreamToNode(request.body!); - - const tmpPath = await saveStreamToTempFile( - nodeStream.pipe(decipher), - fileName - ); - - if (!tmpPath) { - return NextResponse.json({ error: "Unable to save temporary backup file" }, { status: 500 }); + const file = formData.get("file") as File | null; + const extension = formData.get("extension") as string | null; + if (!aesKeyHex || !ivHex) { + return NextResponse.json({error: "Missing fields"}, {status: 400}); } - uploadTempFileToProviders(backup, database, tmpPath, fileName); - return NextResponse.json({ success: true }); + if (!file) { + return NextResponse.json( + {error: "File is required for successful backup"}, + {status: 400} + ); + } + const fileSizeBytes = file.size; + // const fileExtension = '.' + (file.name.split('.').pop()?.toLowerCase() || ''); + const fileExtension = extension ? extension : getFileExtension(database.dbms) + const decryptedFile = await decryptedDump(file, aesKeyHex, ivHex, fileExtension); + const uuid = uuidv4(); + const fileName = `${uuid}${fileExtension}`; + const buffer = Buffer.from(await decryptedFile.arrayBuffer()); + + + const storageResults = await storeBackupFiles(backup, database, buffer, fileName) + eventEmitter.emit('modification', {update: true}); + + await sendNotificationsBackupRestore(database, "success_backup"); + + return NextResponse.json( + { + message: "Backup successfully uploaded", + }, + {status: 200} + ); } else { + await db .update(drizzleDb.schemas.backup) - .set(withUpdatedAt({ status: "failed" })) + .set(withUpdatedAt({status: 'failed'})) .where(eq(drizzleDb.schemas.backup.id, backup.id)); - eventEmitter.emit("modification", { update: true }); + eventEmitter.emit('modification', {update: true}); + await sendNotificationsBackupRestore(database, "error_backup"); - return NextResponse.json({ message: "Backup updated with status failed" }, { status: 200 }); + + return NextResponse.json( + { + message: "Backup successfully updated with status failed", + }, + {status: 200} + ); } - } catch (err: any) { - console.error("Upload error:", err); - return NextResponse.json({ error: "Upload failed", detail: err.message }, { status: 500 }); + } catch (error) { + console.error("Error in POST handler:", error); + return NextResponse.json( + {error: "Internal server error"}, + {status: 500} + ); } } - - -function webStreamToNode(stream: ReadableStream) { - return Readable.fromWeb(stream); -} \ No newline at end of file diff --git a/docker/dockerfile/Dockerfile b/docker/dockerfile/Dockerfile index 56b00d8b..6ac85701 100644 --- a/docker/dockerfile/Dockerfile +++ b/docker/dockerfile/Dockerfile @@ -84,8 +84,8 @@ RUN chown -R nextjs:nodejs /app/private RUN chown -R nextjs:nodejs /app/public COPY --from=builder /app/next.config.ts ./ -#COPY --from=builder /app/server ./server -#COPY --from=builder /app/src ./src +COPY --from=builder /app/server ./server +COPY --from=builder /app/src ./src COPY --from=builder /app/portabase.config.ts ./ COPY --from=builder /app/drizzle.config.ts ./ @@ -93,7 +93,7 @@ COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static COPY --chown=nextjs:nodejs src/db ./src/db -#COPY --from=deps /app/node_modules ./node_modules +COPY --from=deps /app/node_modules ./node_modules USER root diff --git a/docker/entrypoints/app-prod-entrypoint.sh b/docker/entrypoints/app-prod-entrypoint.sh index 63597f52..901588b8 100644 --- a/docker/entrypoints/app-prod-entrypoint.sh +++ b/docker/entrypoints/app-prod-entrypoint.sh @@ -7,8 +7,8 @@ else echo "[WARN] No TZ provided, using default container timezone" fi -#pnpm run start -node server.js +pnpm run start +#node server.js exec "$@" diff --git a/next.config.ts b/next.config.ts index 7adb66d8..ffb58319 100644 --- a/next.config.ts +++ b/next.config.ts @@ -41,13 +41,11 @@ const nextConfig: NextConfig = { typescript: { ignoreBuildErrors: true, }, - experimental: { serverActions: { bodySizeLimit: "10gb", }, proxyClientMaxBodySize: '10gb', - }, async headers() { return [ diff --git a/package.json b/package.json index bfccc802..8f1966b9 100644 --- a/package.json +++ b/package.json @@ -3,9 +3,9 @@ "version": "1.2.5-rc.3", "private": true, "scripts": { - "dev": "next dev --turbopack -p 8887", + "dev": "NODE_ENV=development tsx watch server/server.ts", "build": "next build --experimental-build-mode compile", - "start": "next start", + "start": "NODE_ENV=production node server/server.js", "lint": "next lint", "email": "email dev --dir ./src/components/emails", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 35152525..0aec1760 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -303,6 +303,9 @@ importers: baseline-browser-mapping: specifier: ^2.9.19 version: 2.9.19 + concurrently: + specifier: ^9.2.1 + version: 9.2.1 drizzle-kit: specifier: ^0.31.1 version: 0.31.8 @@ -3894,6 +3897,11 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + concurrently@9.2.1: + resolution: {integrity: sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==} + engines: {node: '>=18'} + hasBin: true + confbox@0.2.2: resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==} @@ -6192,6 +6200,9 @@ packages: run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + safe-array-concat@1.1.3: resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} engines: {node: '>=0.4'} @@ -6286,6 +6297,10 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + shell-quote@1.8.3: + resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} + engines: {node: '>= 0.4'} + side-channel-list@1.0.0: resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} engines: {node: '>= 0.4'} @@ -6602,6 +6617,10 @@ packages: resolution: {integrity: sha512-vxXDZg8/+p3gblxB6BhhG5yWVn1kGRlaL8O78UDXc3wRnPizB5g83dcvWV1jpDMIPnjZjOFuxlMmE82XJ4407w==} engines: {node: '>= 0.4'} + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + ts-api-utils@2.4.0: resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} engines: {node: '>=18.12'} @@ -11224,6 +11243,15 @@ snapshots: concat-map@0.0.1: {} + concurrently@9.2.1: + dependencies: + chalk: 4.1.2 + rxjs: 7.8.2 + shell-quote: 1.8.3 + supports-color: 8.1.1 + tree-kill: 1.2.2 + yargs: 17.7.2 + confbox@0.2.2: {} consola@3.4.2: {} @@ -13747,6 +13775,10 @@ snapshots: dependencies: queue-microtask: 1.2.3 + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + safe-array-concat@1.1.3: dependencies: call-bind: 1.0.8 @@ -13920,6 +13952,8 @@ snapshots: shebang-regex@3.0.0: {} + shell-quote@1.8.3: {} + side-channel-list@1.0.0: dependencies: es-errors: 1.3.0 @@ -14316,6 +14350,8 @@ snapshots: typedarray.prototype.slice: 1.0.5 which-typed-array: 1.1.19 + tree-kill@1.2.2: {} + ts-api-utils@2.4.0(typescript@5.9.3): dependencies: typescript: 5.9.3 diff --git a/src/features/api/router.ts b/src/features/api/router.ts index 035f381f..d2e09797 100644 --- a/src/features/api/router.ts +++ b/src/features/api/router.ts @@ -1,6 +1,6 @@ -import express, {Router} from "express"; +import express, { Router } from "express"; import uploadRouter from "./upload"; -import {loggingMiddleware} from "@/features/api/middleware"; +import { loggingMiddleware } from "@/features/api/middleware"; const router: Router = express.Router();