feat(households): hane rolleri, servis, DAL hane kapsamı ve denetim kaydı

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-05 02:35:01 +03:00
parent 291a117b4f
commit de91f2505a
10 changed files with 731 additions and 0 deletions
+68
View File
@@ -3,6 +3,10 @@ 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";
@@ -66,3 +70,67 @@ export async function getActionUser(role?: SystemRole): Promise<SessionUser | nu
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;
}