From 729c878d403ca079ab1addd8dd79c7815cd06d61 Mon Sep 17 00:00:00 2001 From: Nicolas Meienberger Date: Tue, 27 Jan 2026 21:42:06 +0100 Subject: [PATCH] fix: add organization for users that don't have one --- app/server/modules/lifecycle/migrations.ts | 3 +- .../00002-isolate-restic-passwords.ts | 3 +- .../00003-fix-missing-org-memberships.ts | 92 +++++++++++++++++++ 3 files changed, 95 insertions(+), 3 deletions(-) create mode 100644 app/server/modules/lifecycle/migrations/00003-fix-missing-org-memberships.ts diff --git a/app/server/modules/lifecycle/migrations.ts b/app/server/modules/lifecycle/migrations.ts index ddc333be..e34df7a8 100644 --- a/app/server/modules/lifecycle/migrations.ts +++ b/app/server/modules/lifecycle/migrations.ts @@ -1,6 +1,7 @@ import { logger } from "../../utils/logger"; import { v00001 } from "./migrations/00001-retag-snapshots"; import { v00002 } from "./migrations/00002-isolate-restic-passwords"; +import { v00003 } from "./migrations/00003-fix-missing-org-memberships"; import { sql } from "drizzle-orm"; import { eq } from "drizzle-orm"; import { appMetadataTable, usersTable } from "../../db/schema"; @@ -38,7 +39,7 @@ type MigrationEntity = { dependsOn?: string[]; }; -const registry: MigrationEntity[] = [v00001, v00002]; +const registry: MigrationEntity[] = [v00001, v00002, v00003]; export const runMigrations = async () => { const userCount = await db.select({ count: sql`count(*)` }).from(usersTable); diff --git a/app/server/modules/lifecycle/migrations/00002-isolate-restic-passwords.ts b/app/server/modules/lifecycle/migrations/00002-isolate-restic-passwords.ts index 52f70d07..461a9f5a 100644 --- a/app/server/modules/lifecycle/migrations/00002-isolate-restic-passwords.ts +++ b/app/server/modules/lifecycle/migrations/00002-isolate-restic-passwords.ts @@ -90,8 +90,7 @@ const execute = async () => { const legacyPassword = (await Bun.file(RESTIC_PASS_FILE).text()).trim(); if (!legacyPassword) { - logger.info("No legacy restic passfile found, skipping migration"); - return { success: true, errors: [] }; + throw new Error("Legacy restic passfile is empty"); } // Step 2: Assign restic passwords to all existing organizations diff --git a/app/server/modules/lifecycle/migrations/00003-fix-missing-org-memberships.ts b/app/server/modules/lifecycle/migrations/00003-fix-missing-org-memberships.ts new file mode 100644 index 00000000..1b4a6812 --- /dev/null +++ b/app/server/modules/lifecycle/migrations/00003-fix-missing-org-memberships.ts @@ -0,0 +1,92 @@ +import { eq } from "drizzle-orm"; +import { db } from "../../../db/db"; +import { organization, member } from "../../../db/schema"; +import { logger } from "../../../utils/logger"; +import { toMessage } from "~/server/utils/errors"; +import { cryptoUtils } from "~/server/utils/crypto"; +import { RESTIC_PASS_FILE } from "~/server/core/constants"; + +const execute = async () => { + const errors: Array<{ name: string; error: string }> = []; + + try { + const allUsers = await db.query.usersTable.findMany({ + with: { + members: true, + }, + }); + + const usersWithoutOrg = allUsers.filter((user) => user.members.length === 0); + + if (usersWithoutOrg.length === 0) { + logger.info("No users found without organization memberships"); + return { success: true, errors: [] }; + } + + logger.info(`Found ${usersWithoutOrg.length} user(s) without organization memberships`); + + const legacyPassword = (await Bun.file(RESTIC_PASS_FILE).text()).trim(); + if (!legacyPassword) { + throw new Error("Legacy restic passfile is empty"); + } + + for (const user of usersWithoutOrg) { + try { + await db.transaction(async (tx) => { + const orgId = `default-org-${user.id}`; + const slug = user.email.split("@")[0] + "-" + Math.random().toString(36).slice(-4); + + // Check if an organization with this ID already exists + const existingOrg = await tx.query.organization.findFirst({ + where: eq(organization.id, orgId), + }); + + if (!existingOrg) { + const metadata = { + resticPassword: await cryptoUtils.sealSecret(legacyPassword), + }; + + await tx.insert(organization).values({ + id: orgId, + name: `${user.name}'s Workspace`, + slug: slug, + createdAt: new Date(), + metadata, + }); + + logger.info(`Created organization '${user.name}'s Workspace' for user '${user.username}'`); + } + + await tx.insert(member).values({ + id: `default-mem-${user.id}`, + organizationId: orgId, + userId: user.id, + role: user.role === "admin" ? "owner" : "owner", + createdAt: new Date(), + }); + + logger.info(`Created member record for user '${user.username}' in organization '${orgId}'`); + }); + } catch (err) { + const errorMsg = toMessage(err); + logger.error(`Failed to fix user '${user.username}': ${errorMsg}`); + errors.push({ name: `user:${user.username}`, error: errorMsg }); + } + } + + const fixed = usersWithoutOrg.length - errors.length; + logger.info(`Fixed ${fixed} user(s) without organization memberships`); + + return { success: errors.length === 0, errors }; + } catch (err) { + const errorMsg = toMessage(err); + logger.error(`Migration failed: ${errorMsg}`); + return { success: false, errors: [{ name: "migration", error: errorMsg }] }; + } +}; + +export const v00003 = { + execute, + id: "00003-fix-missing-org-memberships", + type: "critical" as const, +};