From 5da472d6ff7d763e5d4ef5e298b07b9fb95d9938 Mon Sep 17 00:00:00 2001 From: ginnoir Date: Wed, 6 May 2026 02:37:41 -0500 Subject: [PATCH] Add Authentik OIDC integration (task 06) - next-auth@beta + @auth/drizzle-adapter wired up with database sessions - src/lib/auth.ts: OIDC provider, authorized callback, signIn household-attach, getCurrentUser() - src/middleware.ts: protects all routes except /login, /s/*, /api/auth/* - src/app/api/auth/[...nextauth]/route.ts: mounts Auth.js handlers - src/app/login/page.tsx: single SSO sign-in button (server action) - Schema: users extended (name/emailVerified/image), accounts/sessions/verificationTokens added - drizzle/0001_auth_tables.sql: migration for schema changes - deploy/authentik/README.md: manual bootstrap steps for Authentik - src/lib/db.ts: pass schema to drizzle for relational query builder Co-Authored-By: Claude Sonnet 4.6 --- STATUS.md | 3 +- deploy/authentik/README.md | 91 +++++++++++++++++++++++++ drizzle/0001_auth_tables.sql | 35 ++++++++++ package.json | 2 + pnpm-lock.yaml | 85 +++++++++++++++++++++++ src/app/api/auth/[...nextauth]/route.ts | 3 + src/app/login/page.tsx | 22 ++++++ src/lib/auth.ts | 88 ++++++++++++++++++++++++ src/lib/db.ts | 3 +- src/middleware.ts | 8 +++ src/modules/_core/schema.ts | 56 ++++++++++++++- 11 files changed, 392 insertions(+), 4 deletions(-) create mode 100644 deploy/authentik/README.md create mode 100644 drizzle/0001_auth_tables.sql create mode 100644 src/app/api/auth/[...nextauth]/route.ts create mode 100644 src/app/login/page.tsx create mode 100644 src/lib/auth.ts create mode 100644 src/middleware.ts diff --git a/STATUS.md b/STATUS.md index 46a7452..4564da6 100644 --- a/STATUS.md +++ b/STATUS.md @@ -10,10 +10,11 @@ Living progress tracker. Update at the end of each task. The canonical brief is - **04 — Module loader & registry**. `src/modules/_core/module.ts` types (`ModuleManifest`, `EntityTypeRegistration`, `DashboardWidget`, etc.), `registry.ts` singleton with `registerModule`/`getRegistry`/`getEntityType`/`getWidget`, barrel `_core/index.ts`. Stub manifests for `calendar`, `lists`, `notes`. `src/modules/index.ts` loader. Root layout imports loader; `AppNav` reads registry for nav links. `/debug/registry` dumps full registry JSON (dev only). Uses zod v4 + built-in `z.toJSONSchema()`. `tsc --noEmit`, `pnpm build`, `pnpm lint` all clean. - **05 — Compose + Caddy**. `Dockerfile` (3-stage: deps/builder/runner, pnpm fetch + offline install, non-root `nextjs` user, `output: standalone`), `deploy/compose.yaml` (famapp + famapp-db + full Authentik stack on `famapp_net`), `deploy/Caddyfile.snippet`, `.env.production.example`. `docker build -t famapp .` succeeds (~311 MB); `docker compose -f deploy/compose.yaml config` validates clean. Added `.dockerignore` and `public/.gitkeep`. +- **06 — Authentik OIDC**. `next-auth@beta` + `@auth/drizzle-adapter` wired up. `src/lib/auth.ts` configures OIDC provider (Authentik), database sessions, `authorized` callback guarding all routes except `/login`, `/s/*`, `/api/auth/*`. `src/middleware.ts` exports `auth` as middleware. `src/app/api/auth/[...nextauth]/route.ts` mounts the handlers. `src/app/login/page.tsx` has a single "Sign in with SSO" server action. `getCurrentUser()` available for server components/actions. Schema extended: `users` → added `name`/`emailVerified`/`image`; new `accounts`, `sessions`, `verificationTokens` tables; migration `0001_auth_tables.sql` generated. `deploy/authentik/README.md` documents the manual Authentik bootstrap. `tsc --noEmit` and `pnpm lint` pass clean. ## Next up -- **06 — Authentik OIDC** ([brief](docs/tasks/06-authentik-oidc.md)). +- **07 — Household seed** ([brief](docs/tasks/07-household-seed.md)). ## Phase 1 remaining diff --git a/deploy/authentik/README.md b/deploy/authentik/README.md new file mode 100644 index 0000000..2369ec0 --- /dev/null +++ b/deploy/authentik/README.md @@ -0,0 +1,91 @@ +# Authentik — manual bootstrap + +Run these steps once after the first `docker compose up -d` in the `deploy/` directory. + +--- + +## 1. Set the admin password + +Visit `https://auth.ginnoir.com/if/flow/initial-setup/` and set the **akadmin** password. + +--- + +## 2. Create the OIDC provider + +1. Log in to the Authentik Admin UI at `https://auth.ginnoir.com/if/admin/`. +2. Go to **Applications → Providers → Create**. +3. Choose **OAuth2/OpenID Provider**. +4. Configure: + - **Name:** `famapp` + - **Authorization flow:** `default-provider-authorization-explicit-consent` + - **Client type:** `Confidential` + - **Client ID:** (auto-generated — copy this) + - **Client Secret:** (auto-generated — copy this) + - **Redirect URIs:** `https://fam.ginnoir.com/api/auth/callback/authentik` + - **Signing Key:** `authentik Self-signed Certificate` + - **Token validity:** 24 hours (or your preference) +5. Save and note the **Issuer URL** shown on the provider detail page. + +The issuer URL will look like: + +``` +https://auth.ginnoir.com/application/o/famapp/ +``` + +Set this (and the client ID/secret) in famapp's `.env` / production secrets: + +```env +AUTH_OIDC_ISSUER=https://auth.ginnoir.com/application/o/famapp/ +AUTH_OIDC_CLIENT_ID= +AUTH_OIDC_CLIENT_SECRET= +``` + +--- + +## 3. Create the Application + +1. Go to **Applications → Applications → Create**. +2. Configure: + - **Name:** `famapp` + - **Slug:** `famapp` + - **Provider:** select the `famapp` provider created above + - **Launch URL:** `https://fam.ginnoir.com` +3. Save. + +--- + +## 4. Create user accounts + +1. Go to **Directory → Users → Create**. +2. Create accounts for Matt and wife. Recommended fields: + - **Username / Email:** use real email addresses (famapp uses email as the identity key) + - **Name:** display name shown in the app +3. Optionally invite them via email to set their own passwords. + +--- + +## 5. Set up passkeys (optional but recommended) + +Each user can enroll a passkey from their Authentik profile: + +1. Sign in as the user at `https://auth.ginnoir.com`. +2. Go to **Settings → MFA Devices → Add → WebAuthn Device**. +3. Follow the browser prompt to register a Touch ID / Face ID / hardware key. + +--- + +## 6. Generate AUTH_SECRET + +Run this locally and put the output in your `.env` / production secrets: + +```sh +openssl rand -base64 32 +``` + +--- + +## Notes + +- The Authentik image in `compose.yaml` is currently pinned to `latest`. Before production, pin to a specific version tag (e.g. `ghcr.io/goauthentik/server:2024.12.3`). +- famapp uses **database sessions** (Auth.js). Sessions are stored in the `sessions` table and expire according to Auth.js defaults (30 days). +- The forward-auth / Outpost wiring for other services (Sonarr, Radarr, etc.) is a separate future task. diff --git a/drizzle/0001_auth_tables.sql b/drizzle/0001_auth_tables.sql new file mode 100644 index 0000000..b28eadd --- /dev/null +++ b/drizzle/0001_auth_tables.sql @@ -0,0 +1,35 @@ +ALTER TABLE "users" RENAME COLUMN "display_name" TO "name";--> statement-breakpoint +ALTER TABLE "users" ALTER COLUMN "name" TYPE text USING "name"::text;--> statement-breakpoint +ALTER TABLE "users" ALTER COLUMN "name" DROP NOT NULL;--> statement-breakpoint +ALTER TABLE "users" ADD COLUMN "email_verified" timestamp with time zone;--> statement-breakpoint +ALTER TABLE "users" ADD COLUMN "image" text;--> statement-breakpoint +CREATE TABLE "accounts" ( + "user_id" uuid NOT NULL, + "type" text NOT NULL, + "provider" text NOT NULL, + "provider_account_id" text NOT NULL, + "refresh_token" text, + "access_token" text, + "expires_at" integer, + "token_type" text, + "scope" text, + "id_token" text, + "session_state" text, + CONSTRAINT "accounts_provider_provider_account_id_pk" PRIMARY KEY("provider","provider_account_id") +); +--> statement-breakpoint +CREATE TABLE "sessions" ( + "session_token" text PRIMARY KEY NOT NULL, + "user_id" uuid NOT NULL, + "expires" timestamp with time zone NOT NULL +); +--> statement-breakpoint +CREATE TABLE "verification_tokens" ( + "identifier" text NOT NULL, + "token" text NOT NULL, + "expires" timestamp with time zone NOT NULL, + CONSTRAINT "verification_tokens_identifier_token_pk" PRIMARY KEY("identifier","token") +); +--> statement-breakpoint +ALTER TABLE "accounts" ADD CONSTRAINT "accounts_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action; diff --git a/package.json b/package.json index cd9156c..62625ac 100644 --- a/package.json +++ b/package.json @@ -37,12 +37,14 @@ "typescript-eslint": "^8.15.0" }, "dependencies": { + "@auth/drizzle-adapter": "^1.11.2", "@base-ui/react": "^1.4.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "drizzle-orm": "^0.45.2", "lucide-react": "^1.14.0", "next": "^15.5.15", + "next-auth": "5.0.0-beta.31", "postgres": "^3.4.9", "react": "^19.2.5", "react-dom": "^19.2.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d347444..472e28a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@auth/drizzle-adapter': + specifier: ^1.11.2 + version: 1.11.2 '@base-ui/react': specifier: ^1.4.1 version: 1.4.1(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) @@ -26,6 +29,9 @@ importers: next: specifier: ^15.5.15 version: 15.5.15(@babel/core@7.29.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + next-auth: + specifier: 5.0.0-beta.31 + version: 5.0.0-beta.31(next@15.5.15(@babel/core@7.29.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5) postgres: specifier: ^3.4.9 version: 3.4.9 @@ -100,6 +106,23 @@ packages: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} + '@auth/core@0.41.2': + resolution: {integrity: sha512-Hx5MNBxN2fJTbJKGUKAA0wca43D0Akl3TvufY54Gn8lop7F+34vU1zA1pn0vQfIoVuLIrpfc2nkyjwIaPJMW7w==} + peerDependencies: + '@simplewebauthn/browser': ^9.0.1 + '@simplewebauthn/server': ^9.0.2 + nodemailer: ^7.0.7 + peerDependenciesMeta: + '@simplewebauthn/browser': + optional: true + '@simplewebauthn/server': + optional: true + nodemailer: + optional: true + + '@auth/drizzle-adapter@1.11.2': + resolution: {integrity: sha512-VOuj7REI8jfJjpSbsYwDM/Zrn55T6lS9Yc+29V+EXMcel8eqG7x+7LodNfd1WHjakfkLYi+qsbYUHB3E6aDA4w==} + '@babel/code-frame@7.29.0': resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} engines: {node: '>=6.9.0'} @@ -1141,6 +1164,9 @@ packages: '@open-draft/until@2.1.0': resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + '@panva/hkdf@1.2.1': + resolution: {integrity: sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==} + '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} @@ -2872,6 +2898,22 @@ packages: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} + next-auth@5.0.0-beta.31: + resolution: {integrity: sha512-1OBgCKPzo+S7UWWMp3xgvGvIJ0OpV7B3vR4ZDRqD9a4Ch+OT6dakLXG9ivhtmIWVa71nTSXattOHyCg8sNi8/Q==} + peerDependencies: + '@simplewebauthn/browser': ^9.0.1 + '@simplewebauthn/server': ^9.0.2 + next: ^14.0.0-0 || ^15.0.0 || ^16.0.0 + nodemailer: ^7.0.7 + react: ^18.2.0 || ^19.0.0 + peerDependenciesMeta: + '@simplewebauthn/browser': + optional: true + '@simplewebauthn/server': + optional: true + nodemailer: + optional: true + next@15.5.15: resolution: {integrity: sha512-VSqCrJwtLVGwAVE0Sb/yikrQfkwkZW9p+lL/J4+xe+G3ZA+QnWPqgcfH1tDUEuk9y+pthzzVFp4L/U8JerMfMQ==} engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} @@ -2917,6 +2959,9 @@ packages: resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} engines: {node: '>=18'} + oauth4webapi@3.8.6: + resolution: {integrity: sha512-iwemM91xz8nryHti2yTmg5fhyEMVOkOXwHNqbvcATjyajb5oQxCQzrNOA6uElRHuMhQQTKUyFKV9y/CNyg25BQ==} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -3074,6 +3119,14 @@ packages: resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==} engines: {node: '>=20'} + preact-render-to-string@6.5.11: + resolution: {integrity: sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==} + peerDependencies: + preact: '>=10' + + preact@10.24.3: + resolution: {integrity: sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==} + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -3640,6 +3693,22 @@ snapshots: '@alloc/quick-lru@5.2.0': {} + '@auth/core@0.41.2': + dependencies: + '@panva/hkdf': 1.2.1 + jose: 6.2.3 + oauth4webapi: 3.8.6 + preact: 10.24.3 + preact-render-to-string: 6.5.11(preact@10.24.3) + + '@auth/drizzle-adapter@1.11.2': + dependencies: + '@auth/core': 0.41.2 + transitivePeerDependencies: + - '@simplewebauthn/browser' + - '@simplewebauthn/server' + - nodemailer + '@babel/code-frame@7.29.0': dependencies: '@babel/helper-validator-identifier': 7.28.5 @@ -4449,6 +4518,8 @@ snapshots: '@open-draft/until@2.1.0': {} + '@panva/hkdf@1.2.1': {} + '@rtsao/scc@1.1.0': {} '@sec-ant/readable-stream@0.4.1': {} @@ -6220,6 +6291,12 @@ snapshots: negotiator@1.0.0: {} + next-auth@5.0.0-beta.31(next@15.5.15(@babel/core@7.29.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5): + dependencies: + '@auth/core': 0.41.2 + next: 15.5.15(@babel/core@7.29.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + react: 19.2.5 + next@15.5.15(@babel/core@7.29.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5): dependencies: '@next/env': 15.5.15 @@ -6269,6 +6346,8 @@ snapshots: path-key: 4.0.0 unicorn-magic: 0.3.0 + oauth4webapi@3.8.6: {} + object-assign@4.1.1: {} object-inspect@1.13.4: {} @@ -6435,6 +6514,12 @@ snapshots: powershell-utils@0.1.0: {} + preact-render-to-string@6.5.11(preact@10.24.3): + dependencies: + preact: 10.24.3 + + preact@10.24.3: {} + prelude-ls@1.2.1: {} prettier@3.8.3: {} diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..c55a45e --- /dev/null +++ b/src/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,3 @@ +import { handlers } from "@/lib/auth"; + +export const { GET, POST } = handlers; diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx new file mode 100644 index 0000000..3fed4ce --- /dev/null +++ b/src/app/login/page.tsx @@ -0,0 +1,22 @@ +import { signIn } from "@/lib/auth"; +import { Button } from "@/components/ui/button"; + +export default function LoginPage() { + return ( +
+
+

famapp

+
{ + "use server"; + await signIn("authentik", { redirectTo: "/" }); + }} + > + +
+
+
+ ); +} diff --git a/src/lib/auth.ts b/src/lib/auth.ts new file mode 100644 index 0000000..215a2a7 --- /dev/null +++ b/src/lib/auth.ts @@ -0,0 +1,88 @@ +import { DrizzleAdapter } from "@auth/drizzle-adapter"; +import NextAuth, { type DefaultSession } from "next-auth"; +import { db } from "@/lib/db"; +import { + accounts, + households, + householdMembers, + sessions, + users, + verificationTokens, +} from "@/modules/_core/schema"; +import { eq } from "drizzle-orm"; + +declare module "next-auth" { + interface Session { + user: { id: string } & DefaultSession["user"]; + } +} + +export const { handlers, auth, signIn, signOut } = NextAuth({ + adapter: DrizzleAdapter(db, { + usersTable: users, + accountsTable: accounts, + sessionsTable: sessions, + verificationTokensTable: verificationTokens, + }), + session: { strategy: "database" }, + providers: [ + { + id: "authentik", + name: "Authentik", + type: "oidc", + issuer: process.env.AUTH_OIDC_ISSUER, + clientId: process.env.AUTH_OIDC_CLIENT_ID, + clientSecret: process.env.AUTH_OIDC_CLIENT_SECRET, + }, + ], + pages: { + signIn: "/login", + }, + 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) + .where(eq(householdMembers.userId, user.id)) + .limit(1); + if (existing.length === 0) { + const [household] = await db.select().from(households).limit(1); + if (household) { + await db + .insert(householdMembers) + .values({ householdId: household.id, userId: user.id }) + .onConflictDoNothing(); + } + } + return true; + }, + session({ session, user }) { + session.user.id = user.id; + return session; + }, + authorized({ auth: session, request: { nextUrl } }) { + const isLoggedIn = !!session?.user; + const { pathname } = nextUrl; + if ( + pathname.startsWith("/s/") || + pathname.startsWith("/api/auth/") || + pathname === "/login" + ) { + return true; + } + return isLoggedIn; + }, + }, +}); + +export async function getCurrentUser() { + const session = await auth(); + if (!session?.user?.id) return null; + const user = await db.query.users.findFirst({ + where: (u, { eq }) => eq(u.id, session.user.id), + }); + return user ?? null; +} diff --git a/src/lib/db.ts b/src/lib/db.ts index ad2bf8b..379ab39 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -1,6 +1,7 @@ import { drizzle } from "drizzle-orm/postgres-js"; import postgres from "postgres"; +import * as schema from "@/modules/_core/schema"; const client = postgres(process.env["DATABASE_URL"]!); -export const db = drizzle(client); +export const db = drizzle(client, { schema }); diff --git a/src/middleware.ts b/src/middleware.ts new file mode 100644 index 0000000..0e5fb03 --- /dev/null +++ b/src/middleware.ts @@ -0,0 +1,8 @@ +export { auth as middleware } from "@/lib/auth"; + +export const config = { + matcher: [ + // Skip Next.js internals and static files + "/((?!_next/static|_next/image|favicon.ico).*)", + ], +}; diff --git a/src/modules/_core/schema.ts b/src/modules/_core/schema.ts index ae5915b..5e9e8b9 100644 --- a/src/modules/_core/schema.ts +++ b/src/modules/_core/schema.ts @@ -1,14 +1,66 @@ -import { pgEnum, pgTable, primaryKey, timestamp, uuid, varchar } from "drizzle-orm/pg-core"; +import type { AdapterAccountType } from "@auth/core/adapters"; +import { + integer, + pgEnum, + pgTable, + primaryKey, + text, + timestamp, + uuid, + varchar, +} from "drizzle-orm/pg-core"; export const roleEnum = pgEnum("household_member_role", ["owner", "member"]); +// Auth.js-compatible users table. "name" and "image" are populated from OIDC claims. export const users = pgTable("users", { id: uuid("id").primaryKey().defaultRandom(), + name: text("name"), email: varchar("email", { length: 255 }).notNull().unique(), - displayName: varchar("display_name", { length: 255 }).notNull(), + emailVerified: timestamp("email_verified", { withTimezone: true }), + image: text("image"), createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), }); +// Auth.js adapter tables +export const accounts = pgTable( + "accounts", + { + userId: uuid("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + type: text("type").$type().notNull(), + provider: text("provider").notNull(), + providerAccountId: text("provider_account_id").notNull(), + refresh_token: text("refresh_token"), + access_token: text("access_token"), + expires_at: integer("expires_at"), + token_type: text("token_type"), + scope: text("scope"), + id_token: text("id_token"), + session_state: text("session_state"), + }, + (t) => [primaryKey({ columns: [t.provider, t.providerAccountId] })], +); + +export const sessions = pgTable("sessions", { + sessionToken: text("session_token").primaryKey(), + userId: uuid("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + expires: timestamp("expires", { withTimezone: true }).notNull(), +}); + +export const verificationTokens = pgTable( + "verification_tokens", + { + identifier: text("identifier").notNull(), + token: text("token").notNull(), + expires: timestamp("expires", { withTimezone: true }).notNull(), + }, + (t) => [primaryKey({ columns: [t.identifier, t.token] })], +); + export const households = pgTable("households", { id: uuid("id").primaryKey().defaultRandom(), name: varchar("name", { length: 255 }).notNull(),