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:
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user