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
This commit is contained in:
egecankomur
2026-05-12 14:19:24 +03:00
parent 04e11c3fed
commit 84be9ec5e3
+40 -10
View File
@@ -1,12 +1,12 @@
import "server-only"; import "server-only";
import { cookies } from "next/headers"; import { cookies } from "next/headers";
import { Query } from "node-appwrite"; import { ID, Permission, Query, Role } from "node-appwrite";
import { createAdminClient, getCurrentUser } from "./server"; import { createAdminClient, getCurrentUser } from "./server";
import { DATABASE_ID, TABLES, type TenantSettings } from "./schema"; import { DATABASE_ID, TABLES, type TenantSettings } from "./schema";
import { ACTIVE_TENANT_COOKIE } from "./tenant-types"; import { ACTIVE_TENANT_COOKIE } from "./tenant-types";
import { getActiveTenantId, getUserTeams } from "./tenant"; import { getActiveTenantId } from "./tenant";
export type TenantRole = "owner" | "admin" | "member"; export type TenantRole = "owner" | "admin" | "member";
@@ -30,17 +30,49 @@ async function resolveFirstValidTenantId(userId: string): Promise<string | null>
const memberships = await users.listMemberships(userId); const memberships = await users.listMemberships(userId);
if (memberships.total === 0) return null; if (memberships.total === 0) return null;
const teamIds = memberships.memberships.map((m) => m.teamId); 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({ const settings = await tablesDB.listRows({
databaseId: DATABASE_ID, databaseId: DATABASE_ID,
tableId: TABLES.tenantSettings, tableId: TABLES.tenantSettings,
queries: [Query.equal("tenantId", teamIds), Query.limit(1)], 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 { } catch {
return null; return null;
} }
} }
async function ensureSettings(
tablesDB: ReturnType<typeof createAdminClient>["tablesDB"],
tenantId: string,
userId: string,
teamName: string,
): Promise<TenantSettings> {
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) { async function setTenantCookie(tenantId: string) {
try { try {
(await cookies()).set(ACTIVE_TENANT_COOKIE, tenantId, { (await cookies()).set(ACTIVE_TENANT_COOKIE, tenantId, {
@@ -79,7 +111,10 @@ export async function requireTenant(): Promise<TenantContext> {
if (!tenantId) throw new Error("NO_TENANT"); 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); const membership = memberships.memberships.find((m) => m.userId === user.$id);
if (!membership) throw new Error("NOT_A_MEMBER"); if (!membership) throw new Error("NOT_A_MEMBER");
@@ -87,12 +122,7 @@ export async function requireTenant(): Promise<TenantContext> {
let settings: TenantSettings | null = null; let settings: TenantSettings | null = null;
try { try {
const result = await tablesDB.listRows({ settings = await ensureSettings(tablesDB, tenantId, user.$id, team.name);
databaseId: DATABASE_ID,
tableId: TABLES.tenantSettings,
queries: [Query.equal("tenantId", tenantId), Query.limit(1)],
});
settings = (result.rows[0] as unknown as TenantSettings) ?? null;
} catch { } catch {
settings = null; settings = null;
} }