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:
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user