Files
butcem/src/lib/dal.ts
T

137 lines
4.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
}