Prevent duplicate tenant memberships on import
All checks were successful
Build and Push Docker Images / build-backend (push) Successful in 37s
Build and Push Docker Images / build-frontend (push) Successful in 1m18s
Build and Push Docker Images / build-website (push) Successful in 17s
Build and Push Docker Images / build-docs (push) Successful in 16s

This commit is contained in:
2026-07-20 19:06:01 +02:00
parent 720c70845e
commit 37d08ee984
4 changed files with 119 additions and 4 deletions

View File

@@ -660,10 +660,16 @@ export default async function adminRoutes(server: FastifyInstance) {
})
.from(authUserRoles),
]);
const uniqueMembershipRows = Array.from(
new Map(membershipRows.map((membership) => [
`${membership.tenant_id}:${membership.user_id}`,
membership,
])).values()
);
const users = userRows.map((user) => {
const profiles = profileRows.filter((profile) => profile.user_id === user.id);
const memberships = membershipRows.filter((membership) => membership.user_id === user.id);
const memberships = uniqueMembershipRows.filter((membership) => membership.user_id === user.id);
const roleAssignments = roleAssignmentRows.filter((assignment) => assignment.user_id === user.id);
const preferredProfile = profiles.find((profile) => profile.active) || profiles[0];
const fallbackName = deriveNameFromEmail(user.email);
@@ -683,7 +689,7 @@ export default async function adminRoutes(server: FastifyInstance) {
const tenantsWithCounts = tenantRows.map((tenant) => ({
...tenant,
user_count: membershipRows.filter((membership) => membership.tenant_id === tenant.id).length,
user_count: uniqueMembershipRows.filter((membership) => membership.tenant_id === tenant.id).length,
}));
return {
@@ -691,7 +697,7 @@ export default async function adminRoutes(server: FastifyInstance) {
tenants: tenantsWithCounts,
roles: roleRows,
unassignedProfiles: profileRows.filter((profile) => !profile.user_id),
memberships: membershipRows,
memberships: uniqueMembershipRows,
roleAssignments: roleAssignmentRows,
};
} catch (err) {

View File

@@ -69,7 +69,9 @@ export default async function meRoutes(server: FastifyInstance) {
.innerJoin(tenants, eq(authTenantUsers.tenant_id, tenants.id))
.where(eq(authTenantUsers.user_id, userId))
const tenantList = tenantRows ?? []
const tenantList = Array.from(
new Map((tenantRows ?? []).map((tenant) => [tenant.id, tenant])).values()
)
// ----------------------------------------------------
// 3) ACTIVE TENANT

View File

@@ -772,6 +772,37 @@ const insertRows = async (client: any, table: string, rows: Record<string, any>[
return inserted
}
const insertAuthTenantUserRows = async (client: any, rows: Record<string, any>[], metadata: TableMetadata) => {
if (!rows.length) return 0
let inserted = 0
const availableColumns = new Set(metadata.columns.filter((column) => !metadata.generatedColumns.has(column)))
for (const row of rows) {
if (!row.tenant_id || !row.user_id) continue
const existing = await client.query(
`select 1 from ${quoteIdent("auth_tenant_users")} where ${quoteIdent("tenant_id")} = $1 and ${quoteIdent("user_id")} = $2 limit 1`,
[row.tenant_id, row.user_id]
)
if (existing.rows.length) continue
const rowColumns = Object.keys(row).filter((column) => availableColumns.has(column))
if (!rowColumns.length) continue
const placeholders = rowColumns.map((_, index) => `$${index + 1}`).join(", ")
const values = rowColumns.map((column) => prepareColumnValue(row[column], metadata.jsonColumns.has(column)))
await client.query(
`insert into ${quoteIdent("auth_tenant_users")} (${rowColumns.map(quoteIdent).join(", ")}) values (${placeholders})`,
values
)
inserted += 1
}
return inserted
}
const insertIdentityRowsWithRemap = async (
client: any,
table: string,
@@ -882,6 +913,7 @@ export const importTenantFullExport = async (
...Object.keys(exportData.tables).filter((table) => !importOrder.includes(table)).sort(),
].filter((table, index, all) => all.indexOf(table) === index)
const specialTables = [
columnsByTable.has("auth_tenant_users") ? "auth_tenant_users" : null,
columnsByTable.has("bankaccounts") ? "bankaccounts" : null,
columnsByTable.has("bankstatements") ? "bankstatements" : null,
columnsByTable.has("letterheads") ? "letterheads" : null,
@@ -905,6 +937,14 @@ export const importTenantFullExport = async (
await reportProgress("Dateien vorbereitet")
const importedTables: { table: string; rows: number }[] = []
const authTenantUsersMetadata = columnsByTable.get("auth_tenant_users")
if (authTenantUsersMetadata) {
const count = await insertAuthTenantUserRows(client, exportData.tables.auth_tenant_users || [], authTenantUsersMetadata)
importedTables.push({ table: "auth_tenant_users", rows: count })
progressDone += 1
await reportProgress("Tenant-Zuordnungen importiert")
}
const bankaccountMetadata = columnsByTable.get("bankaccounts")
if (bankaccountMetadata) {
const result = await insertIdentityRowsWithRemap(client, "bankaccounts", exportData.tables.bankaccounts || [], bankaccountMetadata)