Compare commits

..
10 changed files with 1400 additions and 2462 deletions
+1 -1
View File
@@ -26,5 +26,5 @@ keywords:
- web-ui - web-ui
- agent - agent
license: Apache-2.0 license: Apache-2.0
version: 1.2.5-rc.2 version: 1.2.5-rc.7
date-released: "2026-02-01" date-released: "2026-02-01"
+121 -241
View File
@@ -1,289 +1,169 @@
// import {NextResponse} from "next/server"; import {NextResponse} from "next/server";
// import {isUuidv4} from "@/utils/verify-uuid"; import {isUuidv4} from "@/utils/verify-uuid";
// import {v4 as uuidv4} from "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 * as drizzleDb from "@/db"; import * as drizzleDb from "@/db";
import { db } from "@/db"; import {db} from "@/db";
import { isUuidv4 } from "@/utils/verify-uuid"; import {Backup} from "@/db/schema/07_database";
import uploadTempFileToProviders, { createDecryptionStream } from "@/features/api/upload/helpers/common"; import {and, eq} from "drizzle-orm";
import { getFileExtension, saveStreamToTempFile } from "@/features/api/upload/helpers/file"; import {withUpdatedAt} from "@/db/utils";
import { Backup } from "@/db/schema/07_database"; import {decryptedDump, getFileExtension} from "./helpers";
import { withUpdatedAt } from "@/db/utils"; import {sendNotificationsBackupRestore} from "@/features/notifications/helpers";
import { eventEmitter } from "@/features/shared/event"; import {storeBackupFiles} from "@/features/storages/helpers";
import { sendNotificationsBackupRestore } from "@/features/notifications/helpers"; import {eventEmitter} from "@/features/shared/event";
import {Readable} from "node:stream";
export async function POST(request: Request, { params }: { params: Promise<{ agentId: string }> }) { export async function POST(
request: Request,
{params}: { params: Promise<{ agentId: string }> }
) {
try { try {
const agentId = (await params).agentId; const contentType = request.headers.get("Content-Type");
const headers = request.headers;
const generatedId = headers.get("x-generated-id"); if (!contentType || !contentType.includes("multipart/form-data")) {
const status = headers.get("x-status"); return NextResponse.json(
const encryptedAesKeyHex = headers.get("x-aes-key"); {error: "Unsupported or missing Content-Type"},
const ivHex = headers.get("x-iv"); {status: 400}
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 (!isUuidv4(generatedId)) { eventEmitter.emit('modification', {update: true});
return NextResponse.json({ error: "generatedId is not a valid UUID" }, { status: 400 });
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({ const agent = await db.query.agent.findFirst({
where: eq(drizzleDb.schemas.agent.id, agentId), where: eq(drizzleDb.schemas.agent.id, agentId),
}); });
if (!agent) { 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({ const database = await db.query.database.findFirst({
where: eq(drizzleDb.schemas.database.agentDatabaseId, generatedId), where: eq(drizzleDb.schemas.database.agentDatabaseId, generatedId),
with: { project: true, storagePolicies: true }, with: {
project: true,
alertPolicies: true,
storagePolicies: true
}
}); });
if (!database) { 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; let backup: Backup | null | undefined = null;
if (method === "automatic") { if (method === "automatic") {
[backup] = await db [backup] = await db
.insert(drizzleDb.schemas.backup) .insert(drizzleDb.schemas.backup)
.values({ status: "ongoing", databaseId: database.id }) .values({
status: 'ongoing',
databaseId: database.id,
})
.returning(); .returning();
if (!backup) {
return NextResponse.json(
{error: "Unable to create an automatic backup"},
{status: 500}
);
}
} else { } else {
backup = await db.query.backup.findFirst({ backup = await db.query.backup.findFirst({
where: and( where: and(
eq(drizzleDb.schemas.backup.status, 'ongoing'),
eq(drizzleDb.schemas.backup.databaseId, database.id), 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) { const status = formData.get("status") as string | null;
return NextResponse.json({ error: "Backup not found" }, { status: 404 });
}
if (status === "success") { if (status === "success") {
const file = formData.get("file") as File | null;
const decipher = createDecryptionStream(encryptedAesKeyHex, ivHex); const extension = formData.get("extension") as string | null;
const fileExt = extension || getFileExtension(database.dbms); if (!aesKeyHex || !ivHex) {
const fileName = `${uuidv4()}${fileExt}`; return NextResponse.json({error: "Missing fields"}, {status: 400});
// const nodeStream = webStreamToNode(request.body as any);
// const tmpPath = await new Promise<string | null>((resolve, reject) => {
// pipeline(nodeStream, decipher, async (err) => {
// if (err) return reject(err);
// try {
// const path = await saveStreamToTempFile(decipher, fileName);
// resolve(path);
// } catch (e) {
// reject(e);
// }
// });
// });
const tmpPath = await saveStreamToTempFile(decipher, fileName);
if (!tmpPath) {
return NextResponse.json({ error: "Unable to save temporary backup file" }, { status: 500 });
} }
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 { } else {
await db await db
.update(drizzleDb.schemas.backup) .update(drizzleDb.schemas.backup)
.set(withUpdatedAt({ status: "failed" })) .set(withUpdatedAt({status: 'failed'}))
.where(eq(drizzleDb.schemas.backup.id, backup.id)); .where(eq(drizzleDb.schemas.backup.id, backup.id));
eventEmitter.emit("modification", { update: true }); eventEmitter.emit('modification', {update: true});
await sendNotificationsBackupRestore(database, "error_backup"); 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) { } catch (error) {
console.error("Upload error:", err); console.error("Error in POST handler:", error);
return NextResponse.json({ error: "Upload failed", detail: err.message }, { status: 500 }); return NextResponse.json(
{error: "Internal server error"},
{status: 500}
);
} }
} }
function webStreamToNode(stream: ReadableStream<Uint8Array>) {
return Readable.fromWeb(stream);
}
+3 -3
View File
@@ -84,8 +84,8 @@ RUN chown -R nextjs:nodejs /app/private
RUN chown -R nextjs:nodejs /app/public RUN chown -R nextjs:nodejs /app/public
COPY --from=builder /app/next.config.ts ./ COPY --from=builder /app/next.config.ts ./
#COPY --from=builder /app/server ./server COPY --from=builder /app/server ./server
#COPY --from=builder /app/src ./src COPY --from=builder /app/src ./src
COPY --from=builder /app/portabase.config.ts ./ COPY --from=builder /app/portabase.config.ts ./
COPY --from=builder /app/drizzle.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 --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
COPY --chown=nextjs:nodejs src/db ./src/db 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 USER root
+2 -2
View File
@@ -7,8 +7,8 @@ else
echo "[WARN] No TZ provided, using default container timezone" echo "[WARN] No TZ provided, using default container timezone"
fi fi
#pnpm run start pnpm run start
node server.js #node server.js
exec "$@" exec "$@"
-2
View File
@@ -41,13 +41,11 @@ const nextConfig: NextConfig = {
typescript: { typescript: {
ignoreBuildErrors: true, ignoreBuildErrors: true,
}, },
experimental: { experimental: {
serverActions: { serverActions: {
bodySizeLimit: "10gb", bodySizeLimit: "10gb",
}, },
proxyClientMaxBodySize: '10gb', proxyClientMaxBodySize: '10gb',
}, },
async headers() { async headers() {
return [ return [
+3 -3
View File
@@ -1,11 +1,11 @@
{ {
"name": "portabase", "name": "portabase",
"version": "1.2.5-rc.2", "version": "1.2.5-rc.7",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev --turbopack -p 8887", "dev": "NODE_ENV=development tsx watch server/server.ts",
"build": "next build --experimental-build-mode compile", "build": "next build --experimental-build-mode compile",
"start": "next start", "start": "NODE_ENV=production tsx server/server.ts",
"lint": "next lint", "lint": "next lint",
"email": "email dev --dir ./src/components/emails", "email": "email dev --dir ./src/components/emails",
+1246 -2199
View File
File diff suppressed because it is too large Load Diff
+19 -9
View File
@@ -1,6 +1,7 @@
import next from "next"; import next from "next";
import express from "express"; import express from "express";
import {mountApi} from "./api"; import {mountApi} from "./api";
import http from "http";
const port = Number(process.env.PORT) || 8887; const port = Number(process.env.PORT) || 8887;
const dev = process.env.NODE_ENV !== "production"; const dev = process.env.NODE_ENV !== "production";
@@ -17,11 +18,17 @@ async function start() {
await nextApp.prepare(); await nextApp.prepare();
const server = express(); const app = express();
mountApi(server); app.use((req, res, next) => {
req.setTimeout(0);
res.setTimeout(0);
next();
});
server.use((req, res, next) => { mountApi(app);
app.use((req, res, next) => {
if (req.path.startsWith('/services/v1')) return next(); if (req.path.startsWith('/services/v1')) return next();
handle(req, res).catch((err) => { handle(req, res).catch((err) => {
console.error('Next.js App Router error:', err); console.error('Next.js App Router error:', err);
@@ -29,13 +36,16 @@ async function start() {
}); });
}); });
const server = http.createServer(app);
server.setTimeout(0);
server.headersTimeout = 0;
server.requestTimeout = 0;
server.keepAliveTimeout = 0;
server.listen(port, "0.0.0.0", () => { server.listen(port, "0.0.0.0", () => {
console.log( console.log(`NEXT APP → http://localhost:${port}`);
`NEXT APP → http://localhost:${port}` console.log(`API → http://localhost:${port}/services/v1`);
);
console.log(
`API → http://localhost:${port}/services/v1`
);
}); });
} }
+2 -2
View File
@@ -1,6 +1,6 @@
import express, {Router} from "express"; import express, { Router } from "express";
import uploadRouter from "./upload"; import uploadRouter from "./upload";
import {loggingMiddleware} from "@/features/api/middleware"; import { loggingMiddleware } from "@/features/api/middleware";
const router: Router = express.Router(); const router: Router = express.Router();
+3
View File
@@ -88,6 +88,9 @@ router.post("/:agentId", async (req: Request, res: Response) => {
const tmpPath = await saveStreamToTempFile(decryptedStream, fileName); const tmpPath = await saveStreamToTempFile(decryptedStream, fileName);
if (!tmpPath) { if (!tmpPath) {
return res.status(500).json({ return res.status(500).json({
error: "Unable to save tmp backup file", error: "Unable to save tmp backup file",