feat(db): add Better-Auth schema tables (GRO-118)

Add user, session, account, and verification tables required by
Better-Auth's Drizzle adapter. Add nullable userId FK on staff to
link business identity to auth identity. Fix test fixtures and
factory to include the new column.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Paperclip
2026-03-27 20:23:58 +00:00
parent 317fe57703
commit 7bc4285cdc
6 changed files with 107 additions and 0 deletions
@@ -15,6 +15,7 @@ import type { StaffRow } from "../middleware/rbac.js";
const MANAGER: StaffRow = {
id: "staff-manager-id",
oidcSub: "oidc-manager-sub",
userId: null,
role: "manager",
name: "Manager McManager",
email: "manager@example.com",
+1
View File
@@ -7,6 +7,7 @@ import type { AppEnv, StaffRow } from "../middleware/rbac.js";
const MANAGER: StaffRow = {
id: "staff-manager-id",
oidcSub: "oidc-manager-sub",
userId: null,
role: "manager",
name: "Manager McManager",
email: "manager@example.com",
+1
View File
@@ -8,6 +8,7 @@ import type { AppEnv, StaffRow } from "../middleware/rbac.js";
const MANAGER: StaffRow = {
id: "staff-manager-id",
oidcSub: "oidc-manager-sub",
userId: null,
role: "manager",
name: "Manager McManager",
email: "manager@example.com",
@@ -0,0 +1,49 @@
-- Better-Auth required tables for session-based authentication
CREATE TABLE "user" (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
email_verified BOOLEAN NOT NULL DEFAULT false,
image TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE "session" (
id TEXT PRIMARY KEY,
expires_at TIMESTAMPTZ NOT NULL,
token TEXT NOT NULL UNIQUE,
ip_address TEXT,
user_agent TEXT,
user_id TEXT NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE "account" (
id TEXT PRIMARY KEY,
account_id TEXT NOT NULL,
provider_id TEXT NOT NULL,
user_id TEXT NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
access_token TEXT,
refresh_token TEXT,
id_token TEXT,
access_token_expires_at TIMESTAMPTZ,
refresh_token_expires_at TIMESTAMPTZ,
scope TEXT,
password TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE "verification" (
id TEXT PRIMARY KEY,
identifier TEXT NOT NULL,
value TEXT NOT NULL,
expires_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Link staff records to auth identity
ALTER TABLE staff ADD COLUMN user_id TEXT REFERENCES "user"(id) ON DELETE SET NULL;
+1
View File
@@ -50,6 +50,7 @@ export function buildStaff(overrides: Partial<StaffRow> = {}): StaffRow {
name: `Staff Member ${id}`,
email: `${id}@groombook.test`,
oidcSub: `oidc-${id}`,
userId: null,
role: "groomer",
active: true,
icalToken: null,
+54
View File
@@ -48,6 +48,58 @@ export const clientStatusEnum = pgEnum("client_status", [
"disabled",
]);
// ─── Better-Auth Tables ──────────────────────────────────────────────────────
export const user = pgTable("user", {
id: text("id").primaryKey(),
name: text("name").notNull(),
email: text("email").notNull().unique(),
emailVerified: boolean("email_verified").notNull().default(false),
image: text("image"),
createdAt: timestamp("created_at").notNull().defaultNow(),
updatedAt: timestamp("updated_at").notNull().defaultNow(),
});
export const session = pgTable("session", {
id: text("id").primaryKey(),
expiresAt: timestamp("expires_at").notNull(),
token: text("token").notNull().unique(),
ipAddress: text("ip_address"),
userAgent: text("user_agent"),
userId: text("user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
createdAt: timestamp("created_at").notNull().defaultNow(),
updatedAt: timestamp("updated_at").notNull().defaultNow(),
});
export const account = pgTable("account", {
id: text("id").primaryKey(),
accountId: text("account_id").notNull(),
providerId: text("provider_id").notNull(),
userId: text("user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
accessToken: text("access_token"),
refreshToken: text("refresh_token"),
idToken: text("id_token"),
accessTokenExpiresAt: timestamp("access_token_expires_at"),
refreshTokenExpiresAt: timestamp("refresh_token_expires_at"),
scope: text("scope"),
password: text("password"),
createdAt: timestamp("created_at").notNull().defaultNow(),
updatedAt: timestamp("updated_at").notNull().defaultNow(),
});
export const verification = pgTable("verification", {
id: text("id").primaryKey(),
identifier: text("identifier").notNull(),
value: text("value").notNull(),
expiresAt: timestamp("expires_at").notNull(),
createdAt: timestamp("created_at").notNull().defaultNow(),
updatedAt: timestamp("updated_at").notNull().defaultNow(),
});
// ─── Tables ───────────────────────────────────────────────────────────────────
export const clients = pgTable("clients", {
@@ -104,6 +156,8 @@ export const staff = pgTable("staff", {
email: text("email").notNull().unique(),
// oidcSub links to the Authentik OIDC subject claim
oidcSub: text("oidc_sub").unique(),
// Better-Auth user ID — links staff business record to auth identity
userId: text("user_id").references(() => user.id, { onDelete: "set null" }),
role: staffRoleEnum("role").notNull().default("groomer"),
active: boolean("active").notNull().default(true),
// Token for iCal calendar feed subscription (no auth required)