Compare commits

...

2 Commits

Author SHA1 Message Date
c7ba7a9cc5 KI-AGENT: Importiere Export in bestehenden Zielmandanten
All checks were successful
Build and Push Docker Images / build-backend (push) Successful in 17s
Build and Push Docker Images / build-frontend (push) Successful in 49s
Build and Push Docker Images / build-docs (push) Successful in 11s
2026-05-18 21:49:12 +02:00
1c68e6b724 KI-AGENT: Aktiviere importierten Mandanten für Zieladmin 2026-05-18 21:47:28 +02:00
4 changed files with 109 additions and 5 deletions

View File

@@ -246,6 +246,7 @@ export default async function adminRoutes(server: FastifyInstance) {
const [currentUser] = await server.db const [currentUser] = await server.db
.select({ .select({
id: authUsers.id, id: authUsers.id,
email: authUsers.email,
is_admin: authUsers.is_admin, is_admin: authUsers.is_admin,
}) })
.from(authUsers) .from(authUsers)
@@ -969,8 +970,47 @@ export default async function adminRoutes(server: FastifyInstance) {
const currentUser = await requireAdmin(req, reply); const currentUser = await requireAdmin(req, reply);
if (!currentUser) return; if (!currentUser) return;
const exportData = req.body as TenantFullExport; const body = req.body as TenantFullExport | { exportData?: TenantFullExport; targetTenantId?: number };
const result = await importTenantFullExport(server, exportData); const exportData = "format" in body ? body : body.exportData;
const targetTenantId = "format" in body ? null : Number(body.targetTenantId || 0) || null;
if (!exportData) {
return reply.code(400).send({ error: "exportData required" });
}
const result = await importTenantFullExport(server, exportData, { targetTenantId });
const fallbackName = deriveNameFromEmail(currentUser.email);
await server.db
.insert(authTenantUsers)
.values({
tenant_id: result.tenantId,
user_id: currentUser.id,
created_by: currentUser.id,
})
.onConflictDoNothing();
const [existingAdminProfile] = await server.db
.select({ id: authProfiles.id })
.from(authProfiles)
.where(and(
eq(authProfiles.tenant_id, result.tenantId),
eq(authProfiles.user_id, currentUser.id)
))
.limit(1);
if (!existingAdminProfile) {
await server.db
.insert(authProfiles)
.values({
tenant_id: result.tenantId,
user_id: currentUser.id,
first_name: fallbackName.first_name,
last_name: fallbackName.last_name,
email: currentUser.email,
active: true,
});
}
return { return {
success: true, success: true,

View File

@@ -34,6 +34,10 @@ type ImportResult = {
files: { restored: number; skipped: number } files: { restored: number; skipped: number }
} }
type ImportOptions = {
targetTenantId?: number | null
}
const quoteIdent = (value: string) => `"${value.replace(/"/g, '""')}"` const quoteIdent = (value: string) => `"${value.replace(/"/g, '""')}"`
const tableColumns = async (client: any) => { const tableColumns = async (client: any) => {
@@ -214,6 +218,54 @@ const restoreFiles = async (exportData: TenantFullExport) => {
return { restored, skipped } return { restored, skipped }
} }
const remapTenantScopedExport = (
exportData: TenantFullExport,
targetTenantId?: number | null
): TenantFullExport => {
if (!targetTenantId || targetTenantId === exportData.tenantId) return exportData
const sourceTenantId = exportData.tenantId
const sourcePathPrefix = `${sourceTenantId}/`
const targetPathPrefix = `${targetTenantId}/`
const tables: TableRows = {}
for (const [table, rows] of Object.entries(exportData.tables || {})) {
tables[table] = rows.map((row) => {
const nextRow = { ...row }
if (table === "tenants" && nextRow.id === sourceTenantId) {
nextRow.id = targetTenantId
}
if (nextRow.tenant === sourceTenantId) {
nextRow.tenant = targetTenantId
}
if (nextRow.tenant_id === sourceTenantId) {
nextRow.tenant_id = targetTenantId
}
if (table === "files" && typeof nextRow.path === "string" && nextRow.path.startsWith(sourcePathPrefix)) {
nextRow.path = `${targetPathPrefix}${nextRow.path.slice(sourcePathPrefix.length)}`
}
return nextRow
})
}
return {
...exportData,
tenantId: targetTenantId,
tables,
files: (exportData.files || []).map((file) => ({
...file,
path: file.path?.startsWith(sourcePathPrefix)
? `${targetPathPrefix}${file.path.slice(sourcePathPrefix.length)}`
: file.path,
})),
}
}
const prepareColumnValue = (value: any, isJsonColumn: boolean) => { const prepareColumnValue = (value: any, isJsonColumn: boolean) => {
if (!isJsonColumn || value === null || typeof value === "undefined") return value if (!isJsonColumn || value === null || typeof value === "undefined") return value
if (typeof value === "string") return value if (typeof value === "string") return value
@@ -263,11 +315,16 @@ const refreshSequences = async (client: any, columnsByTable: Map<string, TableMe
} }
} }
export const importTenantFullExport = async (server: FastifyInstance, exportData: TenantFullExport): Promise<ImportResult> => { export const importTenantFullExport = async (
if (exportData?.format !== "fedeo.tenant-full-export" || exportData.version !== 1) { server: FastifyInstance,
rawExportData: TenantFullExport,
options: ImportOptions = {}
): Promise<ImportResult> => {
if (rawExportData?.format !== "fedeo.tenant-full-export" || rawExportData.version !== 1) {
throw new Error("Ungültiges FEDEO Mandantenexport-Format") throw new Error("Ungültiges FEDEO Mandantenexport-Format")
} }
const exportData = remapTenantScopedExport(rawExportData, options.targetTenantId)
const client = await pool.connect() const client = await pool.connect()
const importOrder = [ const importOrder = [
"tenants", "tenants",

View File

@@ -140,7 +140,10 @@ const importTenantExport = async (event: Event) => {
try { try {
const rawContent = await file.text() const rawContent = await file.text()
const exportData = JSON.parse(rawContent) const exportData = JSON.parse(rawContent)
const result = await admin.importTenant(exportData) const result = await admin.importTenant({
exportData,
targetTenantId: tenantForm.value?.id || tenantId,
})
const rowCount = (result.tables || []).reduce((sum, table) => sum + table.rows, 0) const rowCount = (result.tables || []).reduce((sum, table) => sum + table.rows, 0)
lastImportResult.value = { lastImportResult.value = {
@@ -152,6 +155,8 @@ const importTenantExport = async (event: Event) => {
} }
await fetchTenant() await fetchTenant()
await auth.fetchMe()
await auth.switchTenant(String(result.tenantId))
toast.add({ toast.add({
title: "Mandantenimport abgeschlossen", title: "Mandantenimport abgeschlossen",

View File

@@ -108,6 +108,7 @@ const importTenantExport = async (event: Event) => {
const rowCount = (result.tables || []).reduce((sum, table) => sum + table.rows, 0) const rowCount = (result.tables || []).reduce((sum, table) => sum + table.rows, 0)
await fetchTenants() await fetchTenants()
await auth.fetchMe()
toast.add({ toast.add({
title: "Mandantenimport abgeschlossen", title: "Mandantenimport abgeschlossen",
@@ -116,6 +117,7 @@ const importTenantExport = async (event: Event) => {
}) })
if (result.tenantId) { if (result.tenantId) {
await auth.switchTenant(String(result.tenantId))
await router.push(`/administration/tenants/${result.tenantId}`) await router.push(`/administration/tenants/${result.tenantId}`)
} }
} catch (err: any) { } catch (err: any) {