fix: resolve auth/tenant loop and serialization errors
- 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)
This commit is contained in:
@@ -3,7 +3,7 @@ import { Query } from "node-appwrite";
|
|||||||
|
|
||||||
import { getActiveContext } from "@/lib/appwrite/active-context";
|
import { getActiveContext } from "@/lib/appwrite/active-context";
|
||||||
import { getLogoUrl } from "@/lib/appwrite/storage";
|
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 { DATABASE_ID, TABLES } from "@/lib/appwrite/schema";
|
||||||
import type { ThemePrefs } from "@/lib/appwrite/theme-prefs-actions";
|
import type { ThemePrefs } from "@/lib/appwrite/theme-prefs-actions";
|
||||||
import { DashboardShell } from "./dashboard-shell";
|
import { DashboardShell } from "./dashboard-shell";
|
||||||
@@ -13,6 +13,9 @@ export default async function DashboardLayout({
|
|||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
|
const sessionUser = await getCurrentUser();
|
||||||
|
if (!sessionUser) redirect("/sign-in");
|
||||||
|
|
||||||
const ctx = await getActiveContext();
|
const ctx = await getActiveContext();
|
||||||
if (!ctx) redirect("/onboarding");
|
if (!ctx) redirect("/onboarding");
|
||||||
|
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ export default async function SunumPage({ params }: Props) {
|
|||||||
for (const pid of propertyIds) {
|
for (const pid of propertyIds) {
|
||||||
try {
|
try {
|
||||||
const row = await tablesDB.getRow(DATABASE_ID, TABLES.properties, pid);
|
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 {
|
} catch {
|
||||||
// deleted
|
// deleted
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Query } 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 ActiveContext = {
|
export type ActiveContext = {
|
||||||
user: { id: string; name: string; email: string };
|
user: { id: string; name: string; email: string };
|
||||||
@@ -30,7 +30,7 @@ export async function getActiveContext(): Promise<ActiveContext | null> {
|
|||||||
const user = await getCurrentUser();
|
const user = await getCurrentUser();
|
||||||
if (!user) return null;
|
if (!user) return null;
|
||||||
|
|
||||||
const { teams: adminTeams, tablesDB } = createAdminClient();
|
const { teams: adminTeams, users: adminUsers, tablesDB } = createAdminClient();
|
||||||
|
|
||||||
let tenantId = await getActiveTenantId();
|
let tenantId = await getActiveTenantId();
|
||||||
|
|
||||||
@@ -52,9 +52,20 @@ export async function getActiveContext(): Promise<ActiveContext | null> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!tenantId) {
|
if (!tenantId) {
|
||||||
const userTeams = await getUserTeams();
|
try {
|
||||||
tenantId = userTeams?.teams[0]?.$id ?? null;
|
const memberships = await adminUsers.listMemberships(user.$id);
|
||||||
// Persist so the next request skips this resolution path.
|
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);
|
if (tenantId) await setActiveTenantCookie(tenantId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -93,12 +93,17 @@ export async function requireTenant(): Promise<TenantContext> {
|
|||||||
|
|
||||||
const { tablesDB, teams } = createAdminClient();
|
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) {
|
if (tenantId) {
|
||||||
try {
|
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 {
|
} 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 */ }
|
try { (await cookies()).delete(ACTIVE_TENANT_COOKIE); } catch { /* ignore */ }
|
||||||
tenantId = null;
|
tenantId = null;
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-3
@@ -43,9 +43,11 @@ export function middleware(request: NextRequest) {
|
|||||||
return NextResponse.redirect(url);
|
return NextResponse.redirect(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isAuthPath && session) {
|
// NOTE: We intentionally do NOT redirect auth paths to /dashboard here.
|
||||||
return NextResponse.redirect(new URL("/dashboard", request.url));
|
// 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();
|
return NextResponse.next();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user