From fe86bfe6b27378f03c53a4c5debefd336dc2fea8 Mon Sep 17 00:00:00 2001 From: egecankomur Date: Tue, 12 May 2026 17:18:19 +0300 Subject: [PATCH] fix: resolve auth/tenant loop and serialization errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - middleware: remove auth-path→/dashboard redirect; stale session cookies caused dashboard→onboarding→sign-in→dashboard infinite loop - dashboard layout: check getCurrentUser first, redirect to /sign-in directly instead of going through /onboarding - getActiveContext: use admin client (users.listMemberships) for tenant resolution instead of session-dependent getUserTeams() - requireTenant: validate membership before trusting stored tenantId; clear stale cookie and re-resolve if user is not a member - sunum page: JSON.parse/stringify property rows before passing to Client Component (Appwrite SDK objects have non-plain prototypes) --- src/app/(dashboard)/layout.tsx | 5 ++++- src/app/sunum/[token]/page.tsx | 2 +- src/lib/appwrite/active-context.ts | 21 ++++++++++++++++----- src/lib/appwrite/tenant-guard.ts | 11 ++++++++--- src/middleware.ts | 8 +++++--- 5 files changed, 34 insertions(+), 13 deletions(-) diff --git a/src/app/(dashboard)/layout.tsx b/src/app/(dashboard)/layout.tsx index 35e1f84..b73d7d1 100644 --- a/src/app/(dashboard)/layout.tsx +++ b/src/app/(dashboard)/layout.tsx @@ -3,7 +3,7 @@ import { Query } from "node-appwrite"; import { getActiveContext } from "@/lib/appwrite/active-context"; import { getLogoUrl } from "@/lib/appwrite/storage"; -import { createAdminClient, createSessionClient } from "@/lib/appwrite/server"; +import { createAdminClient, createSessionClient, getCurrentUser } from "@/lib/appwrite/server"; import { DATABASE_ID, TABLES } from "@/lib/appwrite/schema"; import type { ThemePrefs } from "@/lib/appwrite/theme-prefs-actions"; import { DashboardShell } from "./dashboard-shell"; @@ -13,6 +13,9 @@ export default async function DashboardLayout({ }: { children: React.ReactNode; }) { + const sessionUser = await getCurrentUser(); + if (!sessionUser) redirect("/sign-in"); + const ctx = await getActiveContext(); if (!ctx) redirect("/onboarding"); diff --git a/src/app/sunum/[token]/page.tsx b/src/app/sunum/[token]/page.tsx index 4b304c2..eab0ad6 100644 --- a/src/app/sunum/[token]/page.tsx +++ b/src/app/sunum/[token]/page.tsx @@ -59,7 +59,7 @@ export default async function SunumPage({ params }: Props) { for (const pid of propertyIds) { try { const row = await tablesDB.getRow(DATABASE_ID, TABLES.properties, pid); - properties.push(row as unknown as Property); + properties.push(JSON.parse(JSON.stringify(row)) as Property); } catch { // deleted } diff --git a/src/lib/appwrite/active-context.ts b/src/lib/appwrite/active-context.ts index b06926a..156a08d 100644 --- a/src/lib/appwrite/active-context.ts +++ b/src/lib/appwrite/active-context.ts @@ -6,7 +6,7 @@ import { Query } 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 ActiveContext = { user: { id: string; name: string; email: string }; @@ -30,7 +30,7 @@ export async function getActiveContext(): Promise { const user = await getCurrentUser(); if (!user) return null; - const { teams: adminTeams, tablesDB } = createAdminClient(); + const { teams: adminTeams, users: adminUsers, tablesDB } = createAdminClient(); let tenantId = await getActiveTenantId(); @@ -52,9 +52,20 @@ export async function getActiveContext(): Promise { } if (!tenantId) { - const userTeams = await getUserTeams(); - tenantId = userTeams?.teams[0]?.$id ?? null; - // Persist so the next request skips this resolution path. + try { + const memberships = await adminUsers.listMemberships(user.$id); + if (memberships.total > 0) { + const teamIds = memberships.memberships.map((m) => m.teamId); + const settings = await tablesDB.listRows({ + databaseId: DATABASE_ID, + tableId: TABLES.tenantSettings, + queries: [Query.equal("tenantId", teamIds), Query.limit(1)], + }); + tenantId = (settings.rows[0] as unknown as { tenantId: string })?.tenantId ?? teamIds[0] ?? null; + } + } catch { + // admin client failure is a server config issue — fall through to null + } if (tenantId) await setActiveTenantCookie(tenantId); } diff --git a/src/lib/appwrite/tenant-guard.ts b/src/lib/appwrite/tenant-guard.ts index 01c3a20..fc0a992 100644 --- a/src/lib/appwrite/tenant-guard.ts +++ b/src/lib/appwrite/tenant-guard.ts @@ -93,12 +93,17 @@ export async function requireTenant(): Promise { const { tablesDB, teams } = createAdminClient(); - // If we have a tenantId from cookie/prefs, verify the team still exists. + // Validate stored tenantId: team must exist AND user must be a member. if (tenantId) { try { - await teams.get(tenantId); + const memberships = await teams.listMemberships(tenantId); + const isMember = memberships.memberships.some((m) => m.userId === user.$id); + if (!isMember) { + try { (await cookies()).delete(ACTIVE_TENANT_COOKIE); } catch { /* ignore */ } + tenantId = null; + } } catch { - // Team was deleted — clear stale pointer and fall through to resolution. + // Team deleted or inaccessible — clear stale pointer. try { (await cookies()).delete(ACTIVE_TENANT_COOKIE); } catch { /* ignore */ } tenantId = null; } diff --git a/src/middleware.ts b/src/middleware.ts index 6484198..2fd7340 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -43,9 +43,11 @@ export function middleware(request: NextRequest) { return NextResponse.redirect(url); } - if (isAuthPath && session) { - return NextResponse.redirect(new URL("/dashboard", request.url)); - } + // NOTE: We intentionally do NOT redirect auth paths to /dashboard here. + // The sign-in/sign-up pages handle that server-side via getCurrentUser(). + // Doing it here causes an infinite loop when the session cookie is stale + // (cookie exists → middleware bounces to /dashboard → layout gets null user + // → bounces to /onboarding → bounces to /sign-in → middleware loops again). return NextResponse.next(); }