Merge remote-tracking branch 'origin/dev' into dev
All checks were successful
Build and Push Docker Images / build-backend (push) Successful in 50s
Build and Push Docker Images / build-frontend (push) Successful in 25s
Build and Push Docker Images / build-website (push) Successful in 22s
Build and Push Docker Images / build-central-services-api (push) Successful in 22s
Build and Push Docker Images / build-docs (push) Successful in 22s
Build and Push Docker Images / build-central-services-admin (push) Successful in 23s
All checks were successful
Build and Push Docker Images / build-backend (push) Successful in 50s
Build and Push Docker Images / build-frontend (push) Successful in 25s
Build and Push Docker Images / build-website (push) Successful in 22s
Build and Push Docker Images / build-central-services-api (push) Successful in 22s
Build and Push Docker Images / build-docs (push) Successful in 22s
Build and Push Docker Images / build-central-services-admin (push) Successful in 23s
This commit is contained in:
@@ -1,7 +1,11 @@
|
||||
import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
|
||||
import { and, eq, inArray, isNull, sql } from "drizzle-orm";
|
||||
import multipart from "@fastify/multipart";
|
||||
import { GetObjectCommand } from "@aws-sdk/client-s3";
|
||||
import { DeleteObjectCommand, GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { createReadStream, createWriteStream, openAsBlob } from "node:fs";
|
||||
import { stat, unlink } from "node:fs/promises";
|
||||
import { pipeline } from "node:stream/promises";
|
||||
|
||||
import {
|
||||
authTenantUsers,
|
||||
@@ -463,8 +467,9 @@ export default async function adminRoutes(server: FastifyInstance) {
|
||||
const startTenantImportJob = async (
|
||||
jobId: string,
|
||||
currentUser: { id: string; email: string },
|
||||
source: { archiveBuffer: Buffer } | { exportData: TenantFullExport }
|
||||
source: { archiveStoragePath: string } | { exportData: TenantFullExport }
|
||||
) => {
|
||||
let temporaryArchivePath: string | null = null;
|
||||
try {
|
||||
await server.db
|
||||
.update(tenantExportJobs)
|
||||
@@ -487,9 +492,21 @@ export default async function adminRoutes(server: FastifyInstance) {
|
||||
.where(eq(tenantExportJobs.id, jobId));
|
||||
};
|
||||
|
||||
const result = "archiveBuffer" in source
|
||||
? await importTenantFullExportArchive(server, source.archiveBuffer, { onProgress })
|
||||
: await importTenantFullExport(server, source.exportData, { onProgress });
|
||||
let result: Awaited<ReturnType<typeof importTenantFullExport>>;
|
||||
if ("archiveStoragePath" in source) {
|
||||
const object = await s3.send(new GetObjectCommand({
|
||||
Bucket: secrets.S3_BUCKET,
|
||||
Key: source.archiveStoragePath,
|
||||
}));
|
||||
if (!object.Body) throw new Error("Importarchiv konnte nicht aus dem Speicher gelesen werden");
|
||||
|
||||
temporaryArchivePath = `${process.env.TMPDIR || "/tmp"}/fedeo-tenant-import-${randomUUID()}.zip`;
|
||||
await pipeline(object.Body as NodeJS.ReadableStream, createWriteStream(temporaryArchivePath, { flags: "wx" }));
|
||||
const archive = await openAsBlob(temporaryArchivePath);
|
||||
result = await importTenantFullExportArchive(server, archive, { onProgress });
|
||||
} else {
|
||||
result = await importTenantFullExport(server, source.exportData, { onProgress });
|
||||
}
|
||||
const access = await completeImportedTenantAccess(currentUser, result);
|
||||
const importResult = { success: true, ...access, ...result };
|
||||
|
||||
@@ -517,6 +534,20 @@ export default async function adminRoutes(server: FastifyInstance) {
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(tenantExportJobs.id, jobId));
|
||||
} finally {
|
||||
if (temporaryArchivePath) {
|
||||
await unlink(temporaryArchivePath).catch((cleanupError) => {
|
||||
console.error("ERROR cleanup temporary tenant import archive:", cleanupError);
|
||||
});
|
||||
}
|
||||
if ("archiveStoragePath" in source) {
|
||||
await s3.send(new DeleteObjectCommand({
|
||||
Bucket: secrets.S3_BUCKET,
|
||||
Key: source.archiveStoragePath,
|
||||
})).catch((cleanupError) => {
|
||||
console.error("ERROR cleanup tenant import archive:", cleanupError);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1425,12 +1456,14 @@ export default async function adminRoutes(server: FastifyInstance) {
|
||||
// POST /admin/tenant-imports
|
||||
// -------------------------------------------------------------
|
||||
server.post("/admin/tenant-imports", { bodyLimit: 1024 * 1024 * 1024 }, async (req, reply) => {
|
||||
let archiveStoragePath: string | null = null;
|
||||
let temporaryArchivePath: string | null = null;
|
||||
try {
|
||||
const currentUser = await requireAdmin(req, reply);
|
||||
if (!currentUser) return;
|
||||
|
||||
const isMultipart = req.headers["content-type"]?.includes("multipart/form-data");
|
||||
let source!: { archiveBuffer: Buffer } | { exportData: TenantFullExport };
|
||||
let source!: { archiveStoragePath: string } | { exportData: TenantFullExport };
|
||||
let filename = "tenant-export.json";
|
||||
let fileSize: number | null = null;
|
||||
let targetTenantId: number | null = null;
|
||||
@@ -1439,9 +1472,7 @@ export default async function adminRoutes(server: FastifyInstance) {
|
||||
const data: any = await req.file();
|
||||
if (!data?.file) return reply.code(400).send({ error: "export file required" });
|
||||
|
||||
const archiveBuffer = await data.toBuffer();
|
||||
filename = data.filename || "tenant-export.fedeo-export.zip";
|
||||
fileSize = archiveBuffer.length;
|
||||
targetTenantId = Number(data.fields?.targetTenantId?.value || 0) || null;
|
||||
if (targetTenantId) {
|
||||
return reply.code(409).send({
|
||||
@@ -1449,7 +1480,21 @@ export default async function adminRoutes(server: FastifyInstance) {
|
||||
});
|
||||
}
|
||||
|
||||
source = { archiveBuffer };
|
||||
archiveStoragePath = `tenant-imports/${randomUUID()}-${filename}`;
|
||||
temporaryArchivePath = `${process.env.TMPDIR || "/tmp"}/fedeo-tenant-import-${randomUUID()}.zip`;
|
||||
await pipeline(data.file, createWriteStream(temporaryArchivePath, { flags: "wx" }));
|
||||
const temporaryArchive = await stat(temporaryArchivePath);
|
||||
await s3.send(new PutObjectCommand({
|
||||
Bucket: secrets.S3_BUCKET,
|
||||
Key: archiveStoragePath,
|
||||
Body: createReadStream(temporaryArchivePath),
|
||||
ContentLength: temporaryArchive.size,
|
||||
ContentType: data.mimetype || "application/zip",
|
||||
}));
|
||||
fileSize = temporaryArchive.size;
|
||||
await unlink(temporaryArchivePath);
|
||||
temporaryArchivePath = null;
|
||||
source = { archiveStoragePath };
|
||||
} else {
|
||||
const body = req.body as TenantFullExport | { exportData?: TenantFullExport; targetTenantId?: number };
|
||||
const exportData = "format" in body ? body : body.exportData;
|
||||
@@ -1493,6 +1538,19 @@ export default async function adminRoutes(server: FastifyInstance) {
|
||||
});
|
||||
} catch (err: any) {
|
||||
console.error("ERROR /admin/tenant-imports:", err);
|
||||
if (temporaryArchivePath) {
|
||||
await unlink(temporaryArchivePath).catch((cleanupError) => {
|
||||
console.error("ERROR cleanup temporary tenant import archive:", cleanupError);
|
||||
});
|
||||
}
|
||||
if (archiveStoragePath) {
|
||||
await s3.send(new DeleteObjectCommand({
|
||||
Bucket: secrets.S3_BUCKET,
|
||||
Key: archiveStoragePath,
|
||||
})).catch((cleanupError) => {
|
||||
console.error("ERROR cleanup tenant import archive:", cleanupError);
|
||||
});
|
||||
}
|
||||
const message = err?.message || "Internal Server Error";
|
||||
const statusCode = message.includes("Tenant mit dieser ID existiert bereits") ? 409 : 500;
|
||||
return reply.code(statusCode).send({ error: message });
|
||||
|
||||
@@ -8,6 +8,7 @@ import { decrypt, encrypt } from "../utils/crypt"
|
||||
import { DE_BANK_CODE_TO_NAME } from "../utils/deBankCodes"
|
||||
import { DE_BANK_CODE_TO_BIC } from "../utils/deBankBics"
|
||||
import { centralServicesClient } from "../modules/push-server.client"
|
||||
import { findBankInstitutionByBic } from "../modules/banking-institution"
|
||||
|
||||
import {
|
||||
bankrequisitions,
|
||||
@@ -997,7 +998,10 @@ export default async function bankingRoutes(server: FastifyInstance) {
|
||||
|
||||
if (!tenantId) return reply.code(401).send({ error: "Unauthorized" })
|
||||
|
||||
const redirect = new URL("/settings/banking", secrets.API_BASE_URL).toString()
|
||||
// API_BASE_URL kann einen Reverse-Proxy-Pfad wie `/backend` enthalten.
|
||||
// Der OAuth-Rücksprung muss aber auf die Frontend-Route zeigen und darf
|
||||
// diesen Backend-Pfad nicht erneut enthalten.
|
||||
const redirect = new URL("/settings/banking", new URL(secrets.API_BASE_URL).origin).toString()
|
||||
let data: any
|
||||
if (useCentralBanking) {
|
||||
data = await centralServicesClient.createBankingRequisition({ institutionId: institutionid, redirect, userLanguage: "de" })
|
||||
@@ -1044,7 +1048,7 @@ export default async function bankingRoutes(server: FastifyInstance) {
|
||||
))
|
||||
}
|
||||
|
||||
const bank = data.find((i: any) => i.bic.toLowerCase() === bic.toLowerCase())
|
||||
const bank = findBankInstitutionByBic(data, bic)
|
||||
|
||||
if (!bank) return reply.code(404).send("Bank not found")
|
||||
|
||||
|
||||
@@ -1129,9 +1129,17 @@ export default async function resourceRoutes(server: FastifyInstance) {
|
||||
return reply.code(404).send({ error: "Resource not found" })
|
||||
}
|
||||
|
||||
let data: Record<string, any> = { ...body, updated_at: new Date().toISOString(), updated_by: userId }
|
||||
//@ts-ignore
|
||||
delete data.updatedBy; delete data.updatedAt;
|
||||
let data: Record<string, any> = { ...body }
|
||||
delete data.updatedAt
|
||||
delete data.updated_at
|
||||
delete data.updatedBy
|
||||
delete data.updated_by
|
||||
|
||||
const updatedAt = new Date()
|
||||
if (Object.prototype.hasOwnProperty.call(table, "updatedAt")) data.updatedAt = updatedAt
|
||||
if (Object.prototype.hasOwnProperty.call(table, "updated_at")) data.updated_at = updatedAt
|
||||
if (Object.prototype.hasOwnProperty.call(table, "updatedBy")) data.updatedBy = userId
|
||||
if (Object.prototype.hasOwnProperty.call(table, "updated_by")) data.updated_by = userId
|
||||
|
||||
if (resource === "filetags") {
|
||||
delete data.isSystemUsed
|
||||
@@ -1144,9 +1152,11 @@ export default async function resourceRoutes(server: FastifyInstance) {
|
||||
if (portalCustomerId) {
|
||||
data = {
|
||||
...sanitizePortalCustomerUpdate(data),
|
||||
updated_at: data.updated_at,
|
||||
updated_by: data.updated_by,
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(table, "updatedAt")) data.updatedAt = updatedAt
|
||||
if (Object.prototype.hasOwnProperty.call(table, "updated_at")) data.updated_at = updatedAt
|
||||
if (Object.prototype.hasOwnProperty.call(table, "updatedBy")) data.updatedBy = userId
|
||||
if (Object.prototype.hasOwnProperty.call(table, "updated_by")) data.updated_by = userId
|
||||
}
|
||||
|
||||
if (resource === "members") {
|
||||
@@ -1162,9 +1172,11 @@ export default async function resourceRoutes(server: FastifyInstance) {
|
||||
if (prepared.error) return reply.code(400).send({ error: prepared.error })
|
||||
data = {
|
||||
...prepared.data,
|
||||
updated_at: data.updated_at,
|
||||
updated_by: data.updated_by,
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(table, "updatedAt")) data.updatedAt = updatedAt
|
||||
if (Object.prototype.hasOwnProperty.call(table, "updated_at")) data.updated_at = updatedAt
|
||||
if (Object.prototype.hasOwnProperty.call(table, "updatedBy")) data.updatedBy = userId
|
||||
if (Object.prototype.hasOwnProperty.call(table, "updated_by")) data.updated_by = userId
|
||||
}
|
||||
|
||||
if (resource === "costcentres") {
|
||||
|
||||
Reference in New Issue
Block a user