de91f2505a
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
137 lines
4.9 KiB
TypeScript
137 lines
4.9 KiB
TypeScript
import "server-only";
|
||
import { headers } from "next/headers";
|
||
import { notFound, redirect } from "next/navigation";
|
||
import { cache } from "react";
|
||
import type { SessionUser } from "@/features/auth/types";
|
||
import { hasHouseholdRole, pickActiveHousehold } from "@/features/households/roles";
|
||
import { listMemberships, resolveMembership, type Membership } from "@/features/households/service";
|
||
import type { HouseholdRole, HouseholdSummary } from "@/features/households/types";
|
||
import { readActiveHouseholdId } from "@/lib/active-household";
|
||
import { getAuth } from "@/lib/auth";
|
||
import { visibleEmail } from "@/shared/account-email";
|
||
import type { SystemRole } from "@/shared/schemas/common";
|
||
|
||
/**
|
||
* Data Access Layer — her korumalı sayfa, layout, Server Action ve Route Handler oturumu
|
||
* BURADAN okur. proxy.ts yalnızca iyimser (cookie var mı?) kontrol yapar; güvenlik bu katmandadır.
|
||
*/
|
||
|
||
type RawUser = {
|
||
id: string;
|
||
name: string;
|
||
email: string;
|
||
username?: string | null;
|
||
role?: string | null;
|
||
banned?: boolean | null;
|
||
image?: string | null;
|
||
};
|
||
|
||
export function toSessionUser(user: RawUser): SessionUser {
|
||
return {
|
||
id: user.id,
|
||
name: user.name,
|
||
username: user.username ?? "",
|
||
email: visibleEmail(user.email),
|
||
role: user.role === "admin" ? "admin" : "user",
|
||
image: user.image ?? undefined,
|
||
};
|
||
}
|
||
|
||
/** İstek başına bir kez çalışır (React cache). Oturum yoksa veya kullanıcı askıdaysa null. */
|
||
export const getCurrentUser = cache(async (): Promise<SessionUser | null> => {
|
||
// headers() önce çağrılır: Cache Components prerender'ı burada durur, auth hiç oluşturulmaz.
|
||
const requestHeaders = await headers();
|
||
const session = await getAuth().api.getSession({ headers: requestHeaders });
|
||
if (!session || session.user.banned) return null;
|
||
return toSessionUser(session.user);
|
||
});
|
||
|
||
/** Sayfa/layout: oturum yoksa /login'e yönlendirir. */
|
||
export async function requireSession(): Promise<SessionUser> {
|
||
const user = await getCurrentUser();
|
||
if (!user) redirect("/login");
|
||
return user;
|
||
}
|
||
|
||
/** Sayfa/layout: rol yoksa 404 (sayfanın varlığı bile gösterilmez). */
|
||
export async function requireRole(role: SystemRole): Promise<SessionUser> {
|
||
const user = await requireSession();
|
||
if (user.role !== role) notFound();
|
||
return user;
|
||
}
|
||
|
||
/**
|
||
* Server Action / Route Handler: yönlendirme yapmaz, yetkisizse null döner
|
||
* (çağıran `fail(...)` veya 401/403 döndürür).
|
||
*/
|
||
export async function getActionUser(role?: SystemRole): Promise<SessionUser | null> {
|
||
const user = await getCurrentUser();
|
||
if (!user) return null;
|
||
if (role && user.role !== role) return null;
|
||
return user;
|
||
}
|
||
|
||
// --- Hane kapsamı (veri izolasyonu) ---
|
||
|
||
export type HouseholdContext = {
|
||
user: SessionUser;
|
||
/** Aktif hane; kullanıcı hiçbir haneye üye değilse null. */
|
||
household: HouseholdSummary | null;
|
||
memberships: HouseholdSummary[];
|
||
};
|
||
|
||
/**
|
||
* Oturum + kullanıcının haneleri + aktif hane (istek başına bir kez). Cookie'deki hane
|
||
* kullanıcının üyesi olduğu bir hane değilse yok sayılır ve ilk haneye düşülür.
|
||
* Sistem admini için ayrıcalık YOKTUR: yalnızca üyesi olduğu haneler görünür.
|
||
*/
|
||
export const getHouseholdContext = cache(async (): Promise<HouseholdContext | null> => {
|
||
const user = await getCurrentUser();
|
||
if (!user) return null;
|
||
const [memberships, preferred] = await Promise.all([
|
||
listMemberships(user.id),
|
||
readActiveHouseholdId(),
|
||
]);
|
||
const household = pickActiveHousehold(memberships, preferred);
|
||
return { user, household, memberships };
|
||
});
|
||
|
||
export type ActiveHousehold = HouseholdContext & { household: HouseholdSummary };
|
||
|
||
/**
|
||
* Sayfa: aktif hanede en az `minRole` yetkisi. Oturum yoksa /login, hanesi yoksa /hane-yok,
|
||
* yetkisi yetmiyorsa 404.
|
||
*/
|
||
export async function requireActiveHousehold(
|
||
minRole: HouseholdRole = "viewer",
|
||
): Promise<ActiveHousehold> {
|
||
const ctx = await getHouseholdContext();
|
||
if (!ctx) redirect("/login");
|
||
if (!ctx.household) redirect("/hane-yok");
|
||
if (!hasHouseholdRole(ctx.household.role, minRole)) notFound();
|
||
return ctx as ActiveHousehold;
|
||
}
|
||
|
||
/** Server Action: aktif hanede en az `minRole` yetkisi; yoksa null (yönlendirme yapmaz). */
|
||
export async function getHouseholdActionContext(
|
||
minRole: HouseholdRole = "viewer",
|
||
): Promise<ActiveHousehold | null> {
|
||
const ctx = await getHouseholdContext();
|
||
if (!ctx?.household || !hasHouseholdRole(ctx.household.role, minRole)) return null;
|
||
return ctx as ActiveHousehold;
|
||
}
|
||
|
||
/**
|
||
* Belirli bir hane için üyelik kontrolü (ör. URL'den veya istek gövdesinden gelen householdId).
|
||
* Üye değilse veya yetkisi yetmiyorsa null — çağıran 404/403 döner.
|
||
*/
|
||
export async function requireHouseholdMember(
|
||
householdId: string,
|
||
minRole: HouseholdRole = "viewer",
|
||
): Promise<{ user: SessionUser; membership: Membership } | null> {
|
||
const user = await getCurrentUser();
|
||
if (!user) return null;
|
||
const membership = await resolveMembership(user.id, householdId, minRole);
|
||
return membership ? { user, membership } : null;
|
||
}
|