From 84be9ec5e3bcbd99556a4d26afcbe74fe3e16b77 Mon Sep 17 00:00:00 2001 From: egecankomur Date: Tue, 12 May 2026 14:19:24 +0300 Subject: [PATCH] fix: auto-create tenant_settings when missing to prevent onboarding loop - ensureSettings() creates a minimal settings row if one is missing for a team the user is already a member of, instead of returning null and letting the onboarding redirect fire - resolveFirstValidTenantId() falls back to any membership when no team has settings yet, so new registrations without a completed onboarding still land on the correct tenant - teams.get() is now fetched alongside listMemberships in a single Promise.all so the team name is available for the fallback row --- src/lib/appwrite/tenant-guard.ts | 50 +++++++++++++++++++++++++------- 1 file changed, 40 insertions(+), 10 deletions(-) diff --git a/src/lib/appwrite/tenant-guard.ts b/src/lib/appwrite/tenant-guard.ts index 76aa367..01c3a20 100644 --- a/src/lib/appwrite/tenant-guard.ts +++ b/src/lib/appwrite/tenant-guard.ts @@ -1,12 +1,12 @@ import "server-only"; import { cookies } from "next/headers"; -import { Query } from "node-appwrite"; +import { ID, Permission, Query, Role } from "node-appwrite"; import { createAdminClient, getCurrentUser } from "./server"; import { DATABASE_ID, TABLES, type TenantSettings } from "./schema"; import { ACTIVE_TENANT_COOKIE } from "./tenant-types"; -import { getActiveTenantId, getUserTeams } from "./tenant"; +import { getActiveTenantId } from "./tenant"; export type TenantRole = "owner" | "admin" | "member"; @@ -30,17 +30,49 @@ async function resolveFirstValidTenantId(userId: string): Promise const memberships = await users.listMemberships(userId); if (memberships.total === 0) return null; const teamIds = memberships.memberships.map((m) => m.teamId); + + // Prefer teams that already have settings, but fall back to any membership. const settings = await tablesDB.listRows({ databaseId: DATABASE_ID, tableId: TABLES.tenantSettings, queries: [Query.equal("tenantId", teamIds), Query.limit(1)], }); - return (settings.rows[0] as unknown as { tenantId: string })?.tenantId ?? null; + const fromSettings = (settings.rows[0] as unknown as { tenantId: string })?.tenantId; + return fromSettings ?? teamIds[0] ?? null; } catch { return null; } } +async function ensureSettings( + tablesDB: ReturnType["tablesDB"], + tenantId: string, + userId: string, + teamName: string, +): Promise { + const result = await tablesDB.listRows({ + databaseId: DATABASE_ID, + tableId: TABLES.tenantSettings, + queries: [Query.equal("tenantId", tenantId), Query.limit(1)], + }); + if (result.rows.length > 0) return result.rows[0] as unknown as TenantSettings; + + // Settings row missing — create a minimal one so the user isn't looped back to onboarding. + const created = await tablesDB.createRow( + DATABASE_ID, + TABLES.tenantSettings, + ID.unique(), + { tenantId, officeName: teamName, createdBy: userId, defaultCurrency: "TRY" }, + [ + Permission.read(Role.team(tenantId)), + Permission.update(Role.team(tenantId, "owner")), + Permission.update(Role.team(tenantId, "admin")), + Permission.delete(Role.team(tenantId, "owner")), + ], + ); + return created as unknown as TenantSettings; +} + async function setTenantCookie(tenantId: string) { try { (await cookies()).set(ACTIVE_TENANT_COOKIE, tenantId, { @@ -79,7 +111,10 @@ export async function requireTenant(): Promise { if (!tenantId) throw new Error("NO_TENANT"); - const memberships = await teams.listMemberships(tenantId); + const [team, memberships] = await Promise.all([ + teams.get(tenantId), + teams.listMemberships(tenantId), + ]); const membership = memberships.memberships.find((m) => m.userId === user.$id); if (!membership) throw new Error("NOT_A_MEMBER"); @@ -87,12 +122,7 @@ export async function requireTenant(): Promise { let settings: TenantSettings | null = null; try { - const result = await tablesDB.listRows({ - databaseId: DATABASE_ID, - tableId: TABLES.tenantSettings, - queries: [Query.equal("tenantId", tenantId), Query.limit(1)], - }); - settings = (result.rows[0] as unknown as TenantSettings) ?? null; + settings = await ensureSettings(tablesDB, tenantId, user.$id, team.name); } catch { settings = null; }