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 = {
|
const MANAGER: StaffRow = {
|
||||||
id: "staff-manager-id",
|
id: "staff-manager-id",
|
||||||
oidcSub: "oidc-manager-sub",
|
oidcSub: "oidc-manager-sub",
|
||||||
|
userId: null,
|
||||||
role: "manager",
|
role: "manager",
|
||||||
name: "Manager McManager",
|
name: "Manager McManager",
|
||||||
email: "manager@example.com",
|
email: "manager@example.com",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import type { AppEnv, StaffRow } from "../middleware/rbac.js";
|
|||||||
const MANAGER: StaffRow = {
|
const MANAGER: StaffRow = {
|
||||||
id: "staff-manager-id",
|
id: "staff-manager-id",
|
||||||
oidcSub: "oidc-manager-sub",
|
oidcSub: "oidc-manager-sub",
|
||||||
|
userId: null,
|
||||||
role: "manager",
|
role: "manager",
|
||||||
name: "Manager McManager",
|
name: "Manager McManager",
|
||||||
email: "manager@example.com",
|
email: "manager@example.com",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import type { AppEnv, StaffRow } from "../middleware/rbac.js";
|
|||||||
const MANAGER: StaffRow = {
|
const MANAGER: StaffRow = {
|
||||||
id: "staff-manager-id",
|
id: "staff-manager-id",
|
||||||
oidcSub: "oidc-manager-sub",
|
oidcSub: "oidc-manager-sub",
|
||||||
|
userId: null,
|
||||||
role: "manager",
|
role: "manager",
|
||||||
name: "Manager McManager",
|
name: "Manager McManager",
|
||||||
email: "manager@example.com",
|
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}`,
|
name: `Staff Member ${id}`,
|
||||||
email: `${id}@groombook.test`,
|
email: `${id}@groombook.test`,
|
||||||
oidcSub: `oidc-${id}`,
|
oidcSub: `oidc-${id}`,
|
||||||
|
userId: null,
|
||||||
role: "groomer",
|
role: "groomer",
|
||||||
active: true,
|
active: true,
|
||||||
icalToken: null,
|
icalToken: null,
|
||||||
|
|||||||
@@ -48,6 +48,58 @@ export const clientStatusEnum = pgEnum("client_status", [
|
|||||||
"disabled",
|
"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 ───────────────────────────────────────────────────────────────────
|
// ─── Tables ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export const clients = pgTable("clients", {
|
export const clients = pgTable("clients", {
|
||||||
@@ -104,6 +156,8 @@ export const staff = pgTable("staff", {
|
|||||||
email: text("email").notNull().unique(),
|
email: text("email").notNull().unique(),
|
||||||
// oidcSub links to the Authentik OIDC subject claim
|
// oidcSub links to the Authentik OIDC subject claim
|
||||||
oidcSub: text("oidc_sub").unique(),
|
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"),
|
role: staffRoleEnum("role").notNull().default("groomer"),
|
||||||
active: boolean("active").notNull().default(true),
|
active: boolean("active").notNull().default(true),
|
||||||
// Token for iCal calendar feed subscription (no auth required)
|
// Token for iCal calendar feed subscription (no auth required)
|
||||||
|
|||||||
Reference in New Issue
Block a user