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
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:
67
backend/scripts/dedupe-auth-tenant-users.ts
Normal file
67
backend/scripts/dedupe-auth-tenant-users.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import "dotenv/config"
|
||||||
|
import { drizzle } from "drizzle-orm/node-postgres"
|
||||||
|
import { sql } from "drizzle-orm"
|
||||||
|
import { Pool } from "pg"
|
||||||
|
|
||||||
|
import { loadSecrets, secrets } from "../src/utils/secrets"
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
let connectionString = process.env.DATABASE_URL
|
||||||
|
|
||||||
|
if (!connectionString) {
|
||||||
|
await loadSecrets()
|
||||||
|
connectionString = secrets.DATABASE_URL
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!connectionString) {
|
||||||
|
throw new Error("DATABASE_URL not configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
const pool = new Pool({
|
||||||
|
connectionString,
|
||||||
|
max: 1,
|
||||||
|
connectionTimeoutMillis: 10000,
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
const db = drizzle(pool)
|
||||||
|
|
||||||
|
const duplicates = await db.execute(sql`
|
||||||
|
SELECT tenant_id, user_id, count(*)::int AS count
|
||||||
|
FROM auth_tenant_users
|
||||||
|
GROUP BY tenant_id, user_id
|
||||||
|
HAVING count(*) > 1
|
||||||
|
ORDER BY count(*) DESC;
|
||||||
|
`)
|
||||||
|
|
||||||
|
console.log(`Gefundene doppelte Tenant-Zuordnungen: ${duplicates.rows.length}`)
|
||||||
|
for (const row of duplicates.rows as Record<string, any>[]) {
|
||||||
|
console.log(JSON.stringify(row))
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleted = await db.execute(sql`
|
||||||
|
DELETE FROM auth_tenant_users duplicate
|
||||||
|
USING auth_tenant_users keep
|
||||||
|
WHERE duplicate.tenant_id = keep.tenant_id
|
||||||
|
AND duplicate.user_id = keep.user_id
|
||||||
|
AND duplicate.ctid > keep.ctid;
|
||||||
|
`)
|
||||||
|
|
||||||
|
console.log(`Entfernte doppelte Zeilen: ${deleted.rowCount || 0}`)
|
||||||
|
|
||||||
|
await db.execute(sql`
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS auth_tenant_users_tenant_user_unique
|
||||||
|
ON auth_tenant_users (tenant_id, user_id);
|
||||||
|
`)
|
||||||
|
|
||||||
|
console.log("Unique Index auth_tenant_users_tenant_user_unique ist vorhanden")
|
||||||
|
} finally {
|
||||||
|
await pool.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run().catch((err) => {
|
||||||
|
console.error("Dedupe auth_tenant_users failed")
|
||||||
|
console.error(err)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
@@ -660,10 +660,16 @@ export default async function adminRoutes(server: FastifyInstance) {
|
|||||||
})
|
})
|
||||||
.from(authUserRoles),
|
.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 users = userRows.map((user) => {
|
||||||
const profiles = profileRows.filter((profile) => profile.user_id === user.id);
|
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 roleAssignments = roleAssignmentRows.filter((assignment) => assignment.user_id === user.id);
|
||||||
const preferredProfile = profiles.find((profile) => profile.active) || profiles[0];
|
const preferredProfile = profiles.find((profile) => profile.active) || profiles[0];
|
||||||
const fallbackName = deriveNameFromEmail(user.email);
|
const fallbackName = deriveNameFromEmail(user.email);
|
||||||
@@ -683,7 +689,7 @@ export default async function adminRoutes(server: FastifyInstance) {
|
|||||||
|
|
||||||
const tenantsWithCounts = tenantRows.map((tenant) => ({
|
const tenantsWithCounts = tenantRows.map((tenant) => ({
|
||||||
...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 {
|
return {
|
||||||
@@ -691,7 +697,7 @@ export default async function adminRoutes(server: FastifyInstance) {
|
|||||||
tenants: tenantsWithCounts,
|
tenants: tenantsWithCounts,
|
||||||
roles: roleRows,
|
roles: roleRows,
|
||||||
unassignedProfiles: profileRows.filter((profile) => !profile.user_id),
|
unassignedProfiles: profileRows.filter((profile) => !profile.user_id),
|
||||||
memberships: membershipRows,
|
memberships: uniqueMembershipRows,
|
||||||
roleAssignments: roleAssignmentRows,
|
roleAssignments: roleAssignmentRows,
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -69,7 +69,9 @@ export default async function meRoutes(server: FastifyInstance) {
|
|||||||
.innerJoin(tenants, eq(authTenantUsers.tenant_id, tenants.id))
|
.innerJoin(tenants, eq(authTenantUsers.tenant_id, tenants.id))
|
||||||
.where(eq(authTenantUsers.user_id, userId))
|
.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
|
// 3) ACTIVE TENANT
|
||||||
|
|||||||
@@ -772,6 +772,37 @@ const insertRows = async (client: any, table: string, rows: Record<string, any>[
|
|||||||
return inserted
|
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 (
|
const insertIdentityRowsWithRemap = async (
|
||||||
client: any,
|
client: any,
|
||||||
table: string,
|
table: string,
|
||||||
@@ -882,6 +913,7 @@ export const importTenantFullExport = async (
|
|||||||
...Object.keys(exportData.tables).filter((table) => !importOrder.includes(table)).sort(),
|
...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)
|
||||||
const specialTables = [
|
const specialTables = [
|
||||||
|
columnsByTable.has("auth_tenant_users") ? "auth_tenant_users" : null,
|
||||||
columnsByTable.has("bankaccounts") ? "bankaccounts" : null,
|
columnsByTable.has("bankaccounts") ? "bankaccounts" : null,
|
||||||
columnsByTable.has("bankstatements") ? "bankstatements" : null,
|
columnsByTable.has("bankstatements") ? "bankstatements" : null,
|
||||||
columnsByTable.has("letterheads") ? "letterheads" : null,
|
columnsByTable.has("letterheads") ? "letterheads" : null,
|
||||||
@@ -905,6 +937,14 @@ export const importTenantFullExport = async (
|
|||||||
await reportProgress("Dateien vorbereitet")
|
await reportProgress("Dateien vorbereitet")
|
||||||
|
|
||||||
const importedTables: { table: string; rows: number }[] = []
|
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")
|
const bankaccountMetadata = columnsByTable.get("bankaccounts")
|
||||||
if (bankaccountMetadata) {
|
if (bankaccountMetadata) {
|
||||||
const result = await insertIdentityRowsWithRemap(client, "bankaccounts", exportData.tables.bankaccounts || [], bankaccountMetadata)
|
const result = await insertIdentityRowsWithRemap(client, "bankaccounts", exportData.tables.bankaccounts || [], bankaccountMetadata)
|
||||||
|
|||||||
Reference in New Issue
Block a user