Add household seeding & session (task 07)
- scripts/seed.ts: idempotent pnpm db:seed inserts "Home" household if none exists (powered by tsx)
- signIn callback: first member of household gets owner role, subsequent users get member
- src/lib/session.ts: getCurrentSession() returns { user, household, role }, throws if unauthenticated or unmembered
- /settings/household: member list for all roles; rename form gated to owner only (server action also enforces)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
5da472d6ff
commit
6710c8b231
@@ -0,0 +1,22 @@
|
||||
"use server";
|
||||
|
||||
import { eq } from "drizzle-orm";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { db } from "@/lib/db";
|
||||
import { getCurrentSession } from "@/lib/session";
|
||||
import { households } from "@/modules/_core/schema";
|
||||
|
||||
export async function renameHousehold(formData: FormData) {
|
||||
const { household, role } = await getCurrentSession();
|
||||
if (role !== "owner") throw new Error("Forbidden");
|
||||
|
||||
const name = formData.get("name");
|
||||
if (typeof name !== "string" || !name.trim()) throw new Error("Invalid name");
|
||||
|
||||
await db
|
||||
.update(households)
|
||||
.set({ name: name.trim() })
|
||||
.where(eq(households.id, household.id));
|
||||
|
||||
revalidatePath("/settings/household");
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { eq } from "drizzle-orm";
|
||||
import { db } from "@/lib/db";
|
||||
import { getCurrentSession } from "@/lib/session";
|
||||
import { householdMembers, users } from "@/modules/_core/schema";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { renameHousehold } from "./actions";
|
||||
|
||||
export default async function HouseholdSettingsPage() {
|
||||
const { household, role } = await getCurrentSession();
|
||||
|
||||
const members = await db
|
||||
.select({ user: users, role: householdMembers.role })
|
||||
.from(householdMembers)
|
||||
.innerJoin(users, eq(householdMembers.userId, users.id))
|
||||
.where(eq(householdMembers.householdId, household.id));
|
||||
|
||||
return (
|
||||
<div className="container max-w-2xl py-8 space-y-6">
|
||||
<h1 className="text-2xl font-semibold">Household Settings</h1>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Members</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="space-y-2">
|
||||
{members.map(({ user, role: memberRole }) => (
|
||||
<li key={user.id} className="flex items-center justify-between">
|
||||
<span>{user.name ?? user.email}</span>
|
||||
<span className="text-sm text-muted-foreground capitalize">
|
||||
{memberRole}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{role === "owner" && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Rename Household</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form action={renameHousehold} className="flex gap-2">
|
||||
<Input name="name" defaultValue={household.name} required />
|
||||
<Button type="submit">Save</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+7
-3
@@ -41,8 +41,6 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
|
||||
callbacks: {
|
||||
async signIn({ user }) {
|
||||
if (!user.id) return true;
|
||||
// Attach user to the single household if not already a member.
|
||||
// (Household is seeded by task 07; this is a no-op until then.)
|
||||
const existing = await db
|
||||
.select()
|
||||
.from(householdMembers)
|
||||
@@ -51,9 +49,15 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
|
||||
if (existing.length === 0) {
|
||||
const [household] = await db.select().from(households).limit(1);
|
||||
if (household) {
|
||||
const [anyMember] = await db
|
||||
.select()
|
||||
.from(householdMembers)
|
||||
.where(eq(householdMembers.householdId, household.id))
|
||||
.limit(1);
|
||||
const role = anyMember ? "member" : "owner";
|
||||
await db
|
||||
.insert(householdMembers)
|
||||
.values({ householdId: household.id, userId: user.id })
|
||||
.values({ householdId: household.id, userId: user.id, role })
|
||||
.onConflictDoNothing();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { eq } from "drizzle-orm";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { db } from "@/lib/db";
|
||||
import { householdMembers, households, users } from "@/modules/_core/schema";
|
||||
|
||||
export interface CurrentSession {
|
||||
user: typeof users.$inferSelect;
|
||||
household: typeof households.$inferSelect;
|
||||
role: "owner" | "member";
|
||||
}
|
||||
|
||||
export async function getCurrentSession(): Promise<CurrentSession> {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) throw new Error("Not authenticated");
|
||||
|
||||
const [row] = await db
|
||||
.select({
|
||||
user: users,
|
||||
household: households,
|
||||
role: householdMembers.role,
|
||||
})
|
||||
.from(householdMembers)
|
||||
.innerJoin(users, eq(householdMembers.userId, users.id))
|
||||
.innerJoin(households, eq(householdMembers.householdId, households.id))
|
||||
.where(eq(householdMembers.userId, session.user.id))
|
||||
.limit(1);
|
||||
|
||||
if (!row) throw new Error("No household membership");
|
||||
|
||||
return row;
|
||||
}
|
||||
Reference in New Issue
Block a user