Restrict tenant imports to new tenants
This commit is contained in:
@@ -11,8 +11,6 @@ import { pool } from "../../db"
|
||||
import { s3 } from "./s3"
|
||||
import { secrets } from "./secrets"
|
||||
import { decrypt, encrypt } from "./crypt"
|
||||
import { addTenantExportGlobalResources } from "./tenantExportGlobalResources"
|
||||
import { restoreImportedTenantNumberRanges } from "./tenantImportNumberRanges"
|
||||
|
||||
type TableRows = Record<string, Record<string, any>[]>
|
||||
type TableMetadata = {
|
||||
@@ -46,7 +44,6 @@ type ImportResult = {
|
||||
}
|
||||
|
||||
type ImportOptions = {
|
||||
targetTenantId?: number | null
|
||||
onProgress?: (progress: { done: number; total: number; message?: string }) => Promise<void> | void
|
||||
}
|
||||
|
||||
@@ -84,6 +81,10 @@ const ENTITY_BANKACCOUNT_PLAIN_FIELDS = {
|
||||
bankName: "__plainBankName",
|
||||
}
|
||||
|
||||
// Diese globalen Stammdaten werden installationsweit per SQL-Migration gepflegt
|
||||
// und duerfen auch aus aelteren Tenant-Archiven nicht importiert werden.
|
||||
const GLOBAL_MIGRATION_TABLES = new Set(["accounts", "units", "citys", "countrys"])
|
||||
|
||||
const quoteIdent = (value: string) => `"${value.replace(/"/g, '""')}"`
|
||||
const matrixServerName = () =>
|
||||
process.env.MATRIX_SERVER_NAME ||
|
||||
@@ -301,14 +302,6 @@ export const buildTenantFullExport = async (
|
||||
if (!tenantRows.length) throw new Error("Tenant nicht gefunden")
|
||||
|
||||
addRows(tables, "tenants", tenantRows)
|
||||
await addTenantExportGlobalResources(
|
||||
tables,
|
||||
new Set(columnsByTable.keys()),
|
||||
tenantRows[0],
|
||||
(table, whereSql, params) => loadRows(client, table, whereSql, params),
|
||||
addRows
|
||||
)
|
||||
|
||||
for (const [table, metadata] of columnsByTable.entries()) {
|
||||
if (table === "tenants") continue
|
||||
const { columns } = metadata
|
||||
@@ -612,58 +605,6 @@ const restoreArchiveFiles = async (
|
||||
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)}`
|
||||
}
|
||||
|
||||
if (table === "letterheads" && 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 encryptEntityBankAccountRowsForImport = (exportData: TenantFullExport) => {
|
||||
const rows = exportData.tables.entitybankaccounts || []
|
||||
|
||||
@@ -712,12 +653,6 @@ const prepareCommunicationRoomsForImport = (exportData: TenantFullExport) => {
|
||||
}
|
||||
}
|
||||
|
||||
export const prepareTenantFullExportRowsForImport = (exportData: TenantFullExport) => {
|
||||
encryptEntityBankAccountRowsForImport(exportData)
|
||||
prepareCommunicationRoomsForImport(exportData)
|
||||
return exportData
|
||||
}
|
||||
|
||||
const cleanupImportedCommunicationRooms = async (client: any, exportData: TenantFullExport) => {
|
||||
const rows = exportData.tables.communication_rooms || []
|
||||
if (!rows.length) return 0
|
||||
@@ -905,8 +840,9 @@ export const importTenantFullExport = async (
|
||||
throw new Error("Ungültiges FEDEO Mandantenexport-Format")
|
||||
}
|
||||
|
||||
const exportData = remapTenantScopedExport(rawExportData, options.targetTenantId)
|
||||
prepareTenantFullExportRowsForImport(exportData)
|
||||
const exportData = rawExportData
|
||||
encryptEntityBankAccountRowsForImport(exportData)
|
||||
prepareCommunicationRoomsForImport(exportData)
|
||||
const client = await pool.connect()
|
||||
const importOrder = [
|
||||
"tenants",
|
||||
@@ -921,11 +857,21 @@ export const importTenantFullExport = async (
|
||||
]
|
||||
|
||||
try {
|
||||
const existingTenant = await client.query(
|
||||
`select 1 from ${quoteIdent("tenants")} where ${quoteIdent("id")} = $1 limit 1`,
|
||||
[exportData.tenantId]
|
||||
)
|
||||
if (existingTenant.rows.length) {
|
||||
throw new Error("Ein Tenant mit dieser ID existiert bereits. Tenant-Exporte können nur als neuer Tenant importiert werden.")
|
||||
}
|
||||
|
||||
const columnsByTable = await tableColumns(client)
|
||||
const tableNames = [
|
||||
...importOrder,
|
||||
...Object.keys(exportData.tables).filter((table) => !importOrder.includes(table)).sort(),
|
||||
].filter((table, index, all) => all.indexOf(table) === index)
|
||||
].filter((table, index, all) =>
|
||||
all.indexOf(table) === index && !GLOBAL_MIGRATION_TABLES.has(table)
|
||||
)
|
||||
const specialTables = [
|
||||
columnsByTable.has("auth_tenant_users") ? "auth_tenant_users" : null,
|
||||
columnsByTable.has("bankaccounts") ? "bankaccounts" : null,
|
||||
@@ -1005,8 +951,6 @@ export const importTenantFullExport = async (
|
||||
progressDone += 1
|
||||
await reportProgress("Kommunikationsräume bereinigt")
|
||||
|
||||
await restoreImportedTenantNumberRanges(client, exportData)
|
||||
|
||||
await refreshSequences(client, columnsByTable)
|
||||
progressDone = progressTotal
|
||||
await reportProgress("Import abgeschlossen")
|
||||
@@ -1030,42 +974,6 @@ export const importTenantFullExportArchive = async (
|
||||
archiveBuffer: Buffer,
|
||||
options: ImportOptions = {}
|
||||
): Promise<ImportResult> => {
|
||||
const rawExportData = await readTenantFullExportArchive(archiveBuffer)
|
||||
const exportData = remapTenantScopedExport(rawExportData, options.targetTenantId)
|
||||
const reader = new ZipReader(new BlobReader(new Blob([archiveBuffer])))
|
||||
|
||||
try {
|
||||
const entries = await reader.getEntries()
|
||||
const entriesByName = new Map(entries.map((entry: any) => [entry.filename, entry]))
|
||||
const manifest = JSON.parse(await readZipTextEntry(entriesByName, "manifest.json")) as TenantArchiveManifest
|
||||
const sourceTenantId = rawExportData.tenantId
|
||||
const targetTenantId = exportData.tenantId
|
||||
const sourcePrefix = `${sourceTenantId}/`
|
||||
const targetPrefix = `${targetTenantId}/`
|
||||
const remappedManifest: TenantArchiveManifest = {
|
||||
...manifest,
|
||||
tenantId: targetTenantId,
|
||||
files: (manifest.files || []).map((file) => ({
|
||||
...file,
|
||||
path: file.path?.startsWith(sourcePrefix)
|
||||
? `${targetPrefix}${file.path.slice(sourcePrefix.length)}`
|
||||
: file.path,
|
||||
})),
|
||||
}
|
||||
|
||||
const result = await importTenantFullExport(server, exportData, { targetTenantId: null })
|
||||
const files = await restoreArchiveFiles(entriesByName, exportData, remappedManifest)
|
||||
|
||||
return {
|
||||
...result,
|
||||
files,
|
||||
}
|
||||
} finally {
|
||||
await reader.close()
|
||||
}
|
||||
}
|
||||
|
||||
export const readTenantFullExportArchive = async (archiveBuffer: Buffer): Promise<TenantFullExport> => {
|
||||
const reader = new ZipReader(new BlobReader(new Blob([archiveBuffer])))
|
||||
|
||||
try {
|
||||
@@ -1082,7 +990,7 @@ export const readTenantFullExportArchive = async (archiveBuffer: Buffer): Promis
|
||||
tables[table.name] = JSON.parse(await readZipTextEntry(entriesByName, table.path))
|
||||
}
|
||||
|
||||
return {
|
||||
const rawExportData: TenantFullExport = {
|
||||
format: "fedeo.tenant-full-export",
|
||||
version: 1,
|
||||
exportedAt: manifest.exportedAt,
|
||||
@@ -1100,57 +1008,14 @@ export const readTenantFullExportArchive = async (archiveBuffer: Buffer): Promis
|
||||
})),
|
||||
}
|
||||
|
||||
const result = await importTenantFullExport(server, rawExportData)
|
||||
const files = await restoreArchiveFiles(entriesByName, rawExportData, manifest)
|
||||
|
||||
return {
|
||||
...result,
|
||||
files,
|
||||
}
|
||||
} finally {
|
||||
await reader.close()
|
||||
}
|
||||
}
|
||||
|
||||
export const restoreTenantMergeArchiveFiles = async (
|
||||
archiveBuffer: Buffer,
|
||||
rawExportData: TenantFullExport,
|
||||
targetTenantId: number,
|
||||
selectedFileRefs: ReadonlySet<string>
|
||||
) => {
|
||||
const reader = new ZipReader(new BlobReader(new Blob([archiveBuffer])))
|
||||
try {
|
||||
const entries = await reader.getEntries()
|
||||
const entriesByName = new Map(entries.map((entry: any) => [entry.filename, entry]))
|
||||
const manifest = JSON.parse(await readZipTextEntry(entriesByName, "manifest.json")) as TenantArchiveManifest
|
||||
const exportData = remapTenantScopedExport(rawExportData, targetTenantId)
|
||||
const sourcePrefix = `${rawExportData.tenantId}/`
|
||||
const targetPrefix = `${targetTenantId}/`
|
||||
const filteredFiles = (manifest.files || [])
|
||||
.filter((file) => selectedFileRefs.has(String(file.id)))
|
||||
.map((file) => ({
|
||||
...file,
|
||||
path: file.path?.startsWith(sourcePrefix)
|
||||
? `${targetPrefix}${file.path.slice(sourcePrefix.length)}`
|
||||
: file.path,
|
||||
}))
|
||||
const filteredManifest: TenantArchiveManifest = {
|
||||
...manifest,
|
||||
tenantId: targetTenantId,
|
||||
files: filteredFiles,
|
||||
}
|
||||
const filteredExportData: TenantFullExport = {
|
||||
...exportData,
|
||||
files: exportData.files.filter((file) => selectedFileRefs.has(String(file.id))),
|
||||
}
|
||||
|
||||
return await restoreArchiveFiles(entriesByName, filteredExportData, filteredManifest)
|
||||
} finally {
|
||||
await reader.close()
|
||||
}
|
||||
}
|
||||
|
||||
export const restoreTenantMergeInlineFiles = async (
|
||||
rawExportData: TenantFullExport,
|
||||
targetTenantId: number,
|
||||
selectedFileRefs: ReadonlySet<string>
|
||||
) => {
|
||||
const exportData = remapTenantScopedExport(rawExportData, targetTenantId)
|
||||
return await restoreFiles({
|
||||
...exportData,
|
||||
files: exportData.files.filter((file) => selectedFileRefs.has(String(file.id))),
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user