chore: seminarhof customer project

This commit is contained in:
e2e
2026-07-11 08:06:17 +02:00
commit d34b9cd674
188 changed files with 20779 additions and 0 deletions

10
db/sqlite/0001-init.sql Normal file
View File

@@ -0,0 +1,10 @@
CREATE TABLE IF NOT EXISTS kanban_card (
card_id TEXT PRIMARY KEY,
title TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'todo',
position INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_kanban_card_status_position
ON kanban_card (status, position);

View File

@@ -0,0 +1,218 @@
-- Drop kanban template scaffold.
DROP TABLE IF EXISTS kanban_card;
-- Seminarhof Drawehn core schema.
-- Multi-tenant from day one (venue_id on every domain row).
-- All money stored as integer cents (euro_cents) to avoid float drift.
--
-- Enum-valued columns carry a CHECK (col IN (...)) so the DB is the single
-- source of truth for each enum. The pikku CLI introspects these into
-- string-literal unions (db.types.ts + enums.gen.ts).
-- ─────────────────────────────────────────────────────────────────────
-- Tenancy
CREATE TABLE IF NOT EXISTS venue (
venue_id TEXT PRIMARY KEY,
name TEXT NOT NULL,
slug TEXT NOT NULL UNIQUE,
locale_default TEXT NOT NULL DEFAULT 'de',
meal_window_breakfast TEXT NOT NULL DEFAULT '07:00-10:00',
meal_window_lunch TEXT NOT NULL DEFAULT '12:00-14:00',
meal_window_dinner TEXT NOT NULL DEFAULT '18:00-20:00',
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
-- ─────────────────────────────────────────────────────────────────────
-- Identity
CREATE TABLE IF NOT EXISTS app_user (
user_id TEXT PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
locale TEXT NOT NULL DEFAULT 'de',
role TEXT NOT NULL CHECK (role IN ('admin', 'client', 'owner')),
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS client (
client_id TEXT PRIMARY KEY,
venue_id TEXT NOT NULL REFERENCES venue(venue_id),
name TEXT,
is_stammgruppe INTEGER NOT NULL DEFAULT 0,
billing_address TEXT,
contact_email TEXT,
contact_phone TEXT,
website TEXT,
notes TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS client_member (
client_id TEXT NOT NULL REFERENCES client(client_id),
user_id TEXT NOT NULL REFERENCES app_user(user_id),
role TEXT NOT NULL DEFAULT 'organiser' CHECK (role IN ('organiser', 'colleague')),
PRIMARY KEY (client_id, user_id)
);
-- ─────────────────────────────────────────────────────────────────────
-- Physical inventory
CREATE TABLE IF NOT EXISTS bathroom (
bathroom_id TEXT PRIMARY KEY,
venue_id TEXT NOT NULL REFERENCES venue(venue_id),
label TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS room (
room_id TEXT PRIMARY KEY,
venue_id TEXT NOT NULL REFERENCES venue(venue_id),
number INTEGER NOT NULL,
beds INTEGER NOT NULL,
room_type TEXT NOT NULL CHECK (room_type IN ('MBZ', 'DZ', 'EZ_only')),
building TEXT NOT NULL CHECK (building IN ('gaestehaus', 'haupthaus')),
floor TEXT NOT NULL CHECK (floor IN ('eg', 'og')),
bathroom_id TEXT REFERENCES bathroom(bathroom_id),
wheelchair INTEGER NOT NULL DEFAULT 0,
ground_floor INTEGER NOT NULL DEFAULT 0,
quiet INTEGER NOT NULL DEFAULT 0,
shared_bath INTEGER NOT NULL DEFAULT 0,
dogs_allowed INTEGER NOT NULL DEFAULT 0,
double_bed_140 INTEGER NOT NULL DEFAULT 0,
allergy_friendly INTEGER NOT NULL DEFAULT 0,
surcharge_eur_cents INTEGER NOT NULL DEFAULT 0,
UNIQUE (venue_id, number)
);
CREATE TABLE IF NOT EXISTS seminar_room (
seminar_room_id TEXT PRIMARY KEY,
venue_id TEXT NOT NULL REFERENCES venue(venue_id),
name TEXT NOT NULL
);
-- ─────────────────────────────────────────────────────────────────────
-- Bookings
CREATE TABLE IF NOT EXISTS booking (
booking_id TEXT PRIMARY KEY,
venue_id TEXT NOT NULL REFERENCES venue(venue_id),
client_id TEXT NOT NULL REFERENCES client(client_id),
event_name TEXT NOT NULL,
start_date TEXT,
end_date TEXT,
status TEXT NOT NULL CHECK (status IN ('enquiry', 'reserved', 'confirmed', 'ended', 'cancelled')),
half_house INTEGER NOT NULL DEFAULT 0,
online_ad INTEGER NOT NULL DEFAULT 0,
cover_image_url TEXT,
short_description TEXT,
organiser_website TEXT,
expected_persons INTEGER,
meal_time_breakfast TEXT,
meal_time_lunch TEXT,
meal_time_dinner TEXT,
daily_plan TEXT,
arrival_time TEXT,
departure_time TEXT,
payment_method TEXT NOT NULL DEFAULT 'transfer' CHECK (payment_method IN ('cash', 'transfer')),
agb_year INTEGER,
agb_accepted_at TEXT,
privacy_accepted_at TEXT,
notes TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_booking_venue_dates
ON booking (venue_id, start_date, end_date);
CREATE INDEX IF NOT EXISTS idx_booking_client
ON booking (client_id);
CREATE INDEX IF NOT EXISTS idx_booking_status
ON booking (venue_id, status);
CREATE TABLE IF NOT EXISTS booking_extra (
booking_extra_id TEXT PRIMARY KEY,
booking_id TEXT NOT NULL REFERENCES booking(booking_id),
kind TEXT NOT NULL CHECK (kind IN ('cake', 'second_seminar_room', 'early_arrival', 'bedlinen', 'massage_bench', 'sauna')),
qty INTEGER NOT NULL DEFAULT 1,
on_dates TEXT, -- JSON array of dates for cake-day picker
unit_price_cents INTEGER NOT NULL DEFAULT 0,
note TEXT
);
-- ─────────────────────────────────────────────────────────────────────
-- Participants
CREATE TABLE IF NOT EXISTS participant (
participant_id TEXT PRIMARY KEY,
booking_id TEXT NOT NULL REFERENCES booking(booking_id),
name TEXT NOT NULL,
email TEXT,
age INTEGER,
kind TEXT NOT NULL DEFAULT 'overnight' CHECK (kind IN ('overnight', 'day_guest')),
dietary_tag TEXT CHECK (dietary_tag IN ('omnivore', 'vegetarian', 'vegan', 'gluten_free', 'lactose_free', 'pescatarian', 'unknown')),
free_text TEXT,
self_form_token TEXT UNIQUE,
self_form_submitted_at TEXT,
notes TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_participant_booking
ON participant (booking_id);
CREATE TABLE IF NOT EXISTS participant_allergy (
participant_id TEXT NOT NULL REFERENCES participant(participant_id),
allergen TEXT NOT NULL CHECK (allergen IN ('nuts', 'soy', 'gluten_strict', 'dairy', 'egg', 'fish', 'celery', 'mustard', 'sesame', 'shellfish', 'other')),
severity TEXT NOT NULL DEFAULT 'standard' CHECK (severity IN ('standard', 'mild', 'moderate', 'severe', 'separate_prep')),
note TEXT,
PRIMARY KEY (participant_id, allergen)
);
CREATE TABLE IF NOT EXISTS room_assignment (
booking_id TEXT NOT NULL REFERENCES booking(booking_id),
participant_id TEXT NOT NULL REFERENCES participant(participant_id),
room_id TEXT NOT NULL REFERENCES room(room_id),
PRIMARY KEY (booking_id, participant_id)
);
CREATE INDEX IF NOT EXISTS idx_room_assignment_room
ON room_assignment (room_id);
-- ─────────────────────────────────────────────────────────────────────
-- Invoicing
CREATE TABLE IF NOT EXISTS invoice (
invoice_id TEXT PRIMARY KEY,
booking_id TEXT NOT NULL REFERENCES booking(booking_id),
kind TEXT NOT NULL CHECK (kind IN ('deposit', 'final')),
invoice_number TEXT NOT NULL UNIQUE,
issued_on TEXT NOT NULL,
due_on TEXT,
paid_on TEXT,
amount_cents INTEGER NOT NULL,
status TEXT NOT NULL DEFAULT 'outstanding' CHECK (status IN ('outstanding', 'paid', 'cancelled')),
pdf_url TEXT,
datev_ref TEXT
);
CREATE INDEX IF NOT EXISTS idx_invoice_booking
ON invoice (booking_id);
-- ─────────────────────────────────────────────────────────────────────
-- Audit
CREATE TABLE IF NOT EXISTS audit_log (
audit_id TEXT PRIMARY KEY,
venue_id TEXT NOT NULL REFERENCES venue(venue_id),
user_id TEXT REFERENCES app_user(user_id),
entity TEXT NOT NULL,
entity_id TEXT NOT NULL,
field TEXT,
before_value TEXT,
after_value TEXT,
action TEXT NOT NULL CHECK (action IN ('create', 'update', 'delete')),
at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_audit_entity
ON audit_log (entity, entity_id);

View File

@@ -0,0 +1,12 @@
-- Auth sessions: opaque cookie token → user_id mapping.
-- We avoid JWT to keep the dependency footprint small; revocation is just DELETE.
CREATE TABLE session (
session_id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES app_user(user_id) ON DELETE CASCADE,
expires_at TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX session_user_idx ON session(user_id);
CREATE INDEX session_expires_idx ON session(expires_at);

View File

@@ -0,0 +1,4 @@
-- Password support for app_user. Format stored: pbkdf2:<iterations>:<salt_b64>:<hash_b64>
-- Existing rows have NULL password_hash and cannot log in until one is set.
ALTER TABLE app_user ADD COLUMN password_hash TEXT;

View File

@@ -0,0 +1,4 @@
-- Rename short_description -> description (the field is multi-sentence prose, not a
-- "short" blurb) and add a separate one-line event_outline captured on the enquiry form.
ALTER TABLE booking RENAME COLUMN short_description TO description;
ALTER TABLE booking ADD COLUMN event_outline TEXT;

View File

@@ -0,0 +1,17 @@
-- Booking lifecycle milestones.
--
-- The status enum is reshaped to a lean "bucket": enquiry | reserved |
-- confirmed | ended | cancelled. The contract + deposit progression that
-- runs inside `reserved` (over ~1 year) is tracked as the columns below plus the
-- deposit row on the invoice table — NOT as extra statuses. A daily cron reads
-- these columns; nothing here runs a long-lived workflow.
ALTER TABLE booking ADD COLUMN contract_sent_at TEXT; -- ISO; day-0 anchor + R1 idempotency guard
ALTER TABLE booking ADD COLUMN contract_response TEXT CHECK (contract_response IN ('approved', 'declined')); -- null = pending
ALTER TABLE booking ADD COLUMN contract_response_at TEXT; -- ISO
ALTER TABLE booking ADD COLUMN deposit_reminder_sent_at TEXT; -- ISO; R2 idempotency guard
ALTER TABLE booking ADD COLUMN flagged_at TEXT; -- ISO; set when admin review is needed
ALTER TABLE booking ADD COLUMN flag_reason TEXT CHECK (flag_reason IN ('deposit_overdue', 'short_window', 'contract_declined'));
-- Collapse the two retired statuses onto the new bucket (for any non-reset DB).
UPDATE booking SET status = 'reserved' WHERE status = 'form_received';
UPDATE booking SET status = 'confirmed' WHERE status = 'deposit_paid';

View File

@@ -0,0 +1,6 @@
-- Status transition timestamps. Set by applyBookingTransition when each
-- bucket is entered for the first time. No backfill — existing rows get NULL.
ALTER TABLE booking ADD COLUMN reserved_at TEXT;
ALTER TABLE booking ADD COLUMN confirmed_at TEXT;
ALTER TABLE booking ADD COLUMN ended_at TEXT;
ALTER TABLE booking ADD COLUMN cancelled_at TEXT;

View File

@@ -0,0 +1,64 @@
-- Public enquiries get their own table and lifecycle, separate from bookings.
-- A public submission lands here as `pending`. An admin then either:
-- approve → status=approved, a booking is created (copy) referencing this
-- enquiry via booking.enquiry_id (admin picks start/end + a client)
-- waitlist → waitlisted_at set (requires ≥1 date option); stays pending so it
-- can still be approved or declined later
-- declined → status=declined
--
-- `waitlisted_at` is a parallel timestamp on a pending row, NOT a status — a
-- waitlisted enquiry is still pending and can be approved or declined.
--
-- Dates live in `enquiry_date_option` (1-to-N, priority-ordered), NOT on the
-- enquiry row — a visitor can propose several date ranges in priority order
-- (E6.1). The enquiry has no single start/end of its own; reads that need a
-- "preferred" date pick the lowest-priority option.
CREATE TABLE IF NOT EXISTS enquiry (
enquiry_id TEXT PRIMARY KEY,
venue_id TEXT NOT NULL REFERENCES venue(venue_id),
-- Contact details from the public form. No client row exists yet — one is
-- selected or created on approval.
contact_name TEXT,
contact_email TEXT NOT NULL,
contact_phone TEXT,
website TEXT,
-- Event details.
event_name TEXT NOT NULL,
event_outline TEXT,
description TEXT,
expected_persons INTEGER,
half_house INTEGER NOT NULL DEFAULT 0,
notes TEXT,
privacy_accepted_at TEXT,
-- Lifecycle.
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'approved', 'declined')), -- 'waitlisted' is a derived UI state (pending + waitlisted_at), not a status
waitlisted_at TEXT,
approved_at TEXT,
declined_at TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_enquiry_venue_status
ON enquiry (venue_id, status);
-- Prioritized date options for an enquiry (E6.1). `priority` is 0-based; 0 is
-- the visitor's first choice. Optional: an enquiry may have zero options.
CREATE TABLE IF NOT EXISTS enquiry_date_option (
option_id TEXT PRIMARY KEY,
enquiry_id TEXT NOT NULL REFERENCES enquiry(enquiry_id) ON DELETE CASCADE,
start_date TEXT NOT NULL,
end_date TEXT NOT NULL,
priority INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_enquiry_date_option_enquiry
ON enquiry_date_option (enquiry_id, priority);
-- A booking may originate from an approved enquiry. NULL for admin-created
-- bookings. UNIQUE so an enquiry maps to at most one booking (SQLite allows
-- many NULLs in a unique index).
ALTER TABLE booking ADD COLUMN enquiry_id TEXT REFERENCES enquiry(enquiry_id);
CREATE UNIQUE INDEX IF NOT EXISTS idx_booking_enquiry
ON booking (enquiry_id);

View File

@@ -0,0 +1,94 @@
-- Migrate from custom app_user/session to Better Auth.
-- Drop tables with FKs to app_user, drop app_user, recreate with better-auth schema.
-- Drop tables that reference app_user
DROP TABLE IF EXISTS client_member;
DROP TABLE IF EXISTS session;
DROP TABLE IF EXISTS audit_log;
DROP TABLE IF EXISTS app_user;
-- Better Auth core tables
CREATE TABLE IF NOT EXISTS "user" (
"id" TEXT NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL,
"email" TEXT NOT NULL UNIQUE,
"email_verified" INTEGER NOT NULL,
"image" TEXT,
"created_at" TEXT NOT NULL,
"updated_at" TEXT NOT NULL,
"role" TEXT NOT NULL DEFAULT 'client' CHECK ("role" IN ('admin', 'client', 'owner')),
"locale" TEXT NOT NULL DEFAULT 'de'
);
CREATE TABLE IF NOT EXISTS "session" (
"id" TEXT NOT NULL PRIMARY KEY,
"expires_at" TEXT NOT NULL,
"token" TEXT NOT NULL UNIQUE,
"created_at" TEXT NOT NULL,
"updated_at" TEXT NOT NULL,
"ip_address" TEXT,
"user_agent" TEXT,
"user_id" TEXT NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS "account" (
"id" TEXT NOT NULL 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" TEXT,
"refresh_token_expires_at" TEXT,
"scope" TEXT,
"password" TEXT,
"created_at" TEXT NOT NULL,
"updated_at" TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS "verification" (
"id" TEXT NOT NULL PRIMARY KEY,
"identifier" TEXT NOT NULL,
"value" TEXT NOT NULL,
"expires_at" TEXT NOT NULL,
"created_at" TEXT NOT NULL,
"updated_at" TEXT NOT NULL
);
-- Booking-client cookie sessions (kept separate from better-auth sessions)
CREATE TABLE IF NOT EXISTS booking_session (
session_id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE,
expires_at TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
-- Recreate client_member referencing user(id)
CREATE TABLE IF NOT EXISTS client_member (
client_id TEXT NOT NULL REFERENCES client(client_id),
user_id TEXT NOT NULL REFERENCES "user" ("id"),
role TEXT NOT NULL DEFAULT 'organiser' CHECK (role IN ('organiser', 'colleague')),
PRIMARY KEY (client_id, user_id)
);
-- Recreate audit_log referencing user(id)
CREATE TABLE IF NOT EXISTS audit_log (
audit_id TEXT PRIMARY KEY,
venue_id TEXT NOT NULL REFERENCES venue(venue_id),
user_id TEXT REFERENCES "user" ("id"),
entity TEXT NOT NULL,
entity_id TEXT NOT NULL,
field TEXT,
before_value TEXT,
after_value TEXT,
action TEXT NOT NULL CHECK (action IN ('create', 'update', 'delete')),
at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS session_user_id_idx ON "session" ("user_id");
CREATE INDEX IF NOT EXISTS account_user_id_idx ON "account" ("user_id");
CREATE INDEX IF NOT EXISTS verification_identifier_idx ON "verification" ("identifier");
CREATE INDEX IF NOT EXISTS booking_session_user_idx ON booking_session (user_id);
CREATE INDEX IF NOT EXISTS booking_session_expires_idx ON booking_session (expires_at);
CREATE INDEX IF NOT EXISTS audit_log_entity_idx ON audit_log (entity, entity_id);

View File

@@ -0,0 +1,28 @@
-- ============================================================
-- Fabric audit event sink — the table Pikku Fabric writes AuditEvent
-- records to. This is the framework-level event stream (distinct from any
-- domain-level row-change history). Columns mirror the AuditEvent shape
-- (camelCase → snake_case for the CamelCasePlugin kysely).
-- ============================================================
CREATE TABLE audit (
event_id TEXT PRIMARY KEY,
type TEXT NOT NULL,
source TEXT,
outcome TEXT,
occurred_at TEXT NOT NULL DEFAULT (datetime('now')),
function_id TEXT,
wire_type TEXT,
wire_id TEXT,
trace_id TEXT,
transaction_id TEXT,
query_id TEXT,
actor TEXT,
input TEXT,
metadata TEXT
);
CREATE INDEX idx_audit_type ON audit(type);
CREATE INDEX idx_audit_occurred ON audit(occurred_at);
CREATE INDEX idx_audit_function ON audit(function_id);
CREATE INDEX idx_audit_trace ON audit(trace_id);

View File

@@ -0,0 +1,32 @@
-- Tear out the hand-rolled `audit_log` domain-history table and realign the
-- framework `audit` sink to the canonical pikku AuditEvent shape (audit_id PK,
-- actor_user_id, data JSON). Domain row-change history now flows through the
-- pikku audit service — KyselyAuditService locally, a platform queue sink in
-- prod — persisted here and read back by get-booking-audit-log via metadata →
-- data. The old `audit` table had a drifted shape and was never written to.
DROP TABLE IF EXISTS audit_log;
DROP TABLE IF EXISTS audit;
CREATE TABLE audit (
audit_id TEXT NOT NULL PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
occurred_at TEXT NOT NULL DEFAULT (datetime('now')),
type TEXT NOT NULL,
source TEXT NOT NULL DEFAULT 'auto',
outcome TEXT,
function_id TEXT,
wire_type TEXT,
trace_id TEXT,
transaction_id TEXT,
query_id TEXT,
actor_user_id TEXT,
actor_org_id TEXT,
tables TEXT, -- JSON array of table names touched
changed_cols TEXT, -- JSON array of changed column names
event TEXT, -- custom event label
old TEXT, -- JSON: previous values
data TEXT -- JSON: new values / event payload
);
CREATE INDEX IF NOT EXISTS idx_audit_occurred_at ON audit (occurred_at);
CREATE INDEX IF NOT EXISTS idx_audit_actor ON audit (actor_user_id) WHERE actor_user_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_audit_function ON audit (function_id) WHERE function_id IS NOT NULL;

View File

@@ -0,0 +1,11 @@
-- =============================================================================
-- BETTER AUTH ACTOR PLUGIN — user-flow actors
-- Adds the `actor` boolean the Better Auth `actor()` plugin declares so its
-- schema is covered by an explicit migration and the db drift check passes.
-- Actor rows are synthetic users signed in by pikkuUserFlow via
-- POST /api/auth/sign-in/actor with the server-held USER_FLOW_ACTOR_SECRET;
-- the flag rides into the pikku core session so audits/analytics can address
-- synthetic traffic. A non-actor user can never be signed in this way.
-- =============================================================================
ALTER TABLE "user" ADD COLUMN "actor" integer NOT NULL DEFAULT 0;

17
db/sqlite/0013-admin.sql Normal file
View File

@@ -0,0 +1,17 @@
-- =============================================================================
-- BETTER AUTH ADMIN PLUGIN — user administration
-- Adds the columns the Better Auth `admin()` plugin declares so its schema is
-- covered by an explicit migration and the db drift check passes. The plugin
-- exposes /api/auth/admin/* (listUsers, setRole, ban, impersonateUser, …);
-- `impersonated_by` marks a session created via "view as user" so it can be
-- reverted with admin.stopImpersonating. All fields are optional — a user with
-- `role` = 'admin' is an administrator; everyone else stays a normal user.
-- `role` already exists on the user table (0009-better-auth.sql), so only the
-- admin-specific columns are added here.
-- =============================================================================
ALTER TABLE "user" ADD COLUMN "banned" integer DEFAULT 0;
ALTER TABLE "user" ADD COLUMN "ban_reason" text;
ALTER TABLE "user" ADD COLUMN "ban_expires" date;
ALTER TABLE "session" ADD COLUMN "impersonated_by" text;

13
db/sqlite/0014-fabric.sql Normal file
View File

@@ -0,0 +1,13 @@
-- =============================================================================
-- BETTER AUTH FABRIC PLUGIN — Fabric operator admin sessions
-- Adds the `fabric` boolean the Better Auth `fabric()` plugin declares so its
-- schema is covered by an explicit migration and the db drift check passes.
-- Fabric rows are synthetic operator users minted by POST /api/auth/sign-in/fabric
-- after verifying a short-lived RS256 token the Fabric control plane signed
-- (checked against FABRIC_AUTH_PUBLIC_KEY). They are created with role 'admin' so
-- the console Users tab can list/impersonate real end-users WITHOUT the operator
-- being one of them; these rows are filtered out of any end-user listing. A real
-- user can never be signed in this way.
-- =============================================================================
ALTER TABLE "user" ADD COLUMN "fabric" integer NOT NULL DEFAULT 0;