KI-AGENT: Statusanzeige für Tenant-Import ergänzen
All checks were successful
Build and Push Docker Images / build-backend (push) Successful in 45s
Build and Push Docker Images / build-frontend (push) Successful in 1m21s
Build and Push Docker Images / build-central-services-api (push) Successful in 22s
Build and Push Docker Images / build-website (push) Successful in 23s
Build and Push Docker Images / build-central-services-admin (push) Successful in 22s
Build and Push Docker Images / build-docs (push) Successful in 21s
All checks were successful
Build and Push Docker Images / build-backend (push) Successful in 45s
Build and Push Docker Images / build-frontend (push) Successful in 1m21s
Build and Push Docker Images / build-central-services-api (push) Successful in 22s
Build and Push Docker Images / build-website (push) Successful in 23s
Build and Push Docker Images / build-central-services-admin (push) Successful in 22s
Build and Push Docker Images / build-docs (push) Successful in 21s
This commit is contained in:
@@ -439,6 +439,66 @@ export default async function adminRoutes(server: FastifyInstance) {
|
||||
}
|
||||
};
|
||||
|
||||
const startTenantImportJob = async (
|
||||
jobId: string,
|
||||
currentUser: { id: string; email: string },
|
||||
source: { archiveBuffer: Buffer } | { exportData: TenantFullExport }
|
||||
) => {
|
||||
try {
|
||||
await server.db
|
||||
.update(tenantExportJobs)
|
||||
.set({
|
||||
status: "running",
|
||||
statusMessage: "Mandantenimport wird vorbereitet",
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(tenantExportJobs.id, jobId));
|
||||
|
||||
const onProgress = async ({ done, total, message }: { done: number; total: number; message?: string }) => {
|
||||
await server.db
|
||||
.update(tenantExportJobs)
|
||||
.set({
|
||||
filesDone: done,
|
||||
filesTotal: total,
|
||||
statusMessage: message || "Mandant wird importiert",
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(tenantExportJobs.id, jobId));
|
||||
};
|
||||
|
||||
const result = "archiveBuffer" in source
|
||||
? await importTenantFullExportArchive(server, source.archiveBuffer, { onProgress })
|
||||
: await importTenantFullExport(server, source.exportData, { onProgress });
|
||||
const access = await completeImportedTenantAccess(currentUser, result);
|
||||
const importResult = { success: true, ...access, ...result };
|
||||
|
||||
await server.db
|
||||
.update(tenantExportJobs)
|
||||
.set({
|
||||
tenantId: result.tenantId,
|
||||
status: "completed",
|
||||
statusMessage: "Mandantenimport abgeschlossen",
|
||||
importResult,
|
||||
error: null,
|
||||
completedAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(tenantExportJobs.id, jobId));
|
||||
} catch (err: any) {
|
||||
console.error("ERROR tenant import job:", err);
|
||||
await server.db
|
||||
.update(tenantExportJobs)
|
||||
.set({
|
||||
status: "failed",
|
||||
statusMessage: "Mandantenimport fehlgeschlagen",
|
||||
error: err?.message || String(err),
|
||||
completedAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(tenantExportJobs.id, jobId));
|
||||
}
|
||||
};
|
||||
|
||||
const createPreparedTenantExport = async (tenantId: number, currentUserId: string) => {
|
||||
const [tenant] = await server.db
|
||||
.select({
|
||||
@@ -1347,7 +1407,9 @@ export default async function adminRoutes(server: FastifyInstance) {
|
||||
if (!currentUser) return;
|
||||
|
||||
const isMultipart = req.headers["content-type"]?.includes("multipart/form-data");
|
||||
let result;
|
||||
let source!: { archiveBuffer: Buffer } | { exportData: TenantFullExport };
|
||||
let filename = "tenant-export.json";
|
||||
let fileSize: number | null = null;
|
||||
let targetTenantId: number | null = null;
|
||||
|
||||
if (isMultipart) {
|
||||
@@ -1355,6 +1417,8 @@ export default async function adminRoutes(server: FastifyInstance) {
|
||||
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({
|
||||
@@ -1362,7 +1426,7 @@ export default async function adminRoutes(server: FastifyInstance) {
|
||||
});
|
||||
}
|
||||
|
||||
result = await importTenantFullExportArchive(server, archiveBuffer);
|
||||
source = { archiveBuffer };
|
||||
} else {
|
||||
const body = req.body as TenantFullExport | { exportData?: TenantFullExport; targetTenantId?: number };
|
||||
const exportData = "format" in body ? body : body.exportData;
|
||||
@@ -1378,17 +1442,32 @@ export default async function adminRoutes(server: FastifyInstance) {
|
||||
});
|
||||
}
|
||||
|
||||
result = await importTenantFullExport(server, exportData);
|
||||
source = { exportData };
|
||||
}
|
||||
|
||||
const { matrixProvisioned, matrixProvisioningError } = await completeImportedTenantAccess(currentUser, result);
|
||||
const [job] = await server.db
|
||||
.insert(tenantExportJobs)
|
||||
.values({
|
||||
tenantId: null,
|
||||
createdBy: currentUser.id,
|
||||
operation: "import",
|
||||
status: "queued",
|
||||
statusMessage: "Upload abgeschlossen, Import wird gestartet",
|
||||
filename,
|
||||
fileSize,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.returning();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
matrixProvisioned,
|
||||
matrixProvisioningError,
|
||||
...result,
|
||||
};
|
||||
void startTenantImportJob(job.id, currentUser, source);
|
||||
|
||||
return reply.code(202).send({
|
||||
importId: job.id,
|
||||
status: job.status,
|
||||
statusMessage: job.statusMessage,
|
||||
filename: job.filename,
|
||||
statusUrl: `/api/admin/tenant-imports/${job.id}`,
|
||||
});
|
||||
} catch (err: any) {
|
||||
console.error("ERROR /admin/tenant-imports:", err);
|
||||
const message = err?.message || "Internal Server Error";
|
||||
@@ -1397,6 +1476,48 @@ export default async function adminRoutes(server: FastifyInstance) {
|
||||
}
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// GET /admin/tenant-imports/:import_id
|
||||
// -------------------------------------------------------------
|
||||
server.get("/admin/tenant-imports/:import_id", async (req, reply) => {
|
||||
try {
|
||||
const currentUser = await requireAdmin(req, reply);
|
||||
if (!currentUser) return;
|
||||
|
||||
const { import_id } = req.params as { import_id: string };
|
||||
const [job] = await server.db
|
||||
.select()
|
||||
.from(tenantExportJobs)
|
||||
.where(and(
|
||||
eq(tenantExportJobs.id, import_id),
|
||||
eq(tenantExportJobs.operation, "import")
|
||||
))
|
||||
.limit(1);
|
||||
|
||||
if (!job) return reply.code(404).send({ error: "Import nicht gefunden" });
|
||||
|
||||
return {
|
||||
importId: job.id,
|
||||
tenantId: job.tenantId,
|
||||
operation: job.operation,
|
||||
status: job.status,
|
||||
statusMessage: job.statusMessage,
|
||||
filename: job.filename,
|
||||
fileSize: job.fileSize,
|
||||
filesDone: job.filesDone,
|
||||
filesTotal: job.filesTotal,
|
||||
error: job.error,
|
||||
result: job.importResult,
|
||||
createdAt: job.createdAt,
|
||||
updatedAt: job.updatedAt,
|
||||
completedAt: job.completedAt,
|
||||
};
|
||||
} catch (err) {
|
||||
console.error("ERROR /admin/tenant-imports/:import_id:", err);
|
||||
return reply.code(500).send({ error: "Internal Server Error" });
|
||||
}
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// PUT /admin/users/:user_id/access
|
||||
// -------------------------------------------------------------
|
||||
|
||||
@@ -565,16 +565,21 @@ const readZipTextEntry = async (entriesByName: Map<string, any>, name: string) =
|
||||
const restoreArchiveFiles = async (
|
||||
entriesByName: Map<string, any>,
|
||||
exportData: TenantFullExport,
|
||||
manifest: TenantArchiveManifest
|
||||
manifest: TenantArchiveManifest,
|
||||
onProgress?: (progress: { done: number; total: number; message?: string }) => Promise<void> | void
|
||||
) => {
|
||||
let restored = 0
|
||||
let skipped = 0
|
||||
const filesByPath = new Map((manifest.files || []).map((file) => [file.path, file]))
|
||||
const filesTotal = exportData.files?.length || 0
|
||||
let filesDone = 0
|
||||
|
||||
for (const fileRow of exportData.files || []) {
|
||||
const originalPath = fileRow.path
|
||||
if (!originalPath) {
|
||||
skipped += 1
|
||||
filesDone += 1
|
||||
await onProgress?.({ done: filesDone, total: filesTotal, message: "Archivdateien werden wiederhergestellt" })
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -582,12 +587,16 @@ const restoreArchiveFiles = async (
|
||||
const archivePath = manifestFile?.archivePath
|
||||
if (!archivePath || manifestFile?.missing) {
|
||||
skipped += 1
|
||||
filesDone += 1
|
||||
await onProgress?.({ done: filesDone, total: filesTotal, message: "Archivdateien werden wiederhergestellt" })
|
||||
continue
|
||||
}
|
||||
|
||||
const entry = entriesByName.get(archivePath)
|
||||
if (!entry) {
|
||||
skipped += 1
|
||||
filesDone += 1
|
||||
await onProgress?.({ done: filesDone, total: filesTotal, message: "Archivdateien werden wiederhergestellt" })
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -600,6 +609,8 @@ const restoreArchiveFiles = async (
|
||||
ContentLength: content.length,
|
||||
}))
|
||||
restored += 1
|
||||
filesDone += 1
|
||||
await onProgress?.({ done: filesDone, total: filesTotal, message: "Archivdateien werden wiederhergestellt" })
|
||||
}
|
||||
|
||||
return { restored, skipped }
|
||||
@@ -1008,8 +1019,8 @@ export const importTenantFullExportArchive = async (
|
||||
})),
|
||||
}
|
||||
|
||||
const result = await importTenantFullExport(server, rawExportData)
|
||||
const files = await restoreArchiveFiles(entriesByName, rawExportData, manifest)
|
||||
const result = await importTenantFullExport(server, rawExportData, options)
|
||||
const files = await restoreArchiveFiles(entriesByName, rawExportData, manifest, options.onProgress)
|
||||
|
||||
return {
|
||||
...result,
|
||||
|
||||
Reference in New Issue
Block a user