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 <noreply@anthropic.com>
This commit is contained in:
ginnoir
2026-05-06 02:37:41 -05:00
co-authored by Claude Sonnet 4.6
parent f59753404e
commit 5da472d6ff
11 changed files with 392 additions and 4 deletions
+54 -2
View File
@@ -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<AdapterAccountType>().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(),