From 06e536fb87622f7d5d129eca8f60ed2f8c632e25 Mon Sep 17 00:00:00 2001 From: route-commerce Date: Wed, 1 Jul 2026 13:58:18 -0600 Subject: [PATCH] fix(water-log): add missing tables for admin PIN portal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The water-log admin actions (saveWaterAdminSettings, verifyWaterAdminPin, regenerateAdminPin) and /api/water-admin-auth route were all wired up against the Drizzle schema in db/schema/water-log.ts, but the three underlying tables never had a migration applied: - water_admin_settings - water_admin_sessions - water_audit_log Hitting any of these surfaces threw 'relation does not exist' from pg, which /api/water-admin-auth surfaced as a 500 'Server error' to the field. Adding /admin/water-log/settings → save also failed silently with a server error. This migration creates the three tables with columns matching the Drizzle schema (PKs, defaults, FKs to brands + admin_users) and wires up RLS + tenant_isolation policies in the same shape as 0001_init.sql. The deploy workflow (scripts/migrate.js) will pick this file up automatically on the next push to main. --- db/migrations/0004_water_log_admin.sql | 109 +++++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 db/migrations/0004_water_log_admin.sql diff --git a/db/migrations/0004_water_log_admin.sql b/db/migrations/0004_water_log_admin.sql new file mode 100644 index 0000000..4b6e2ad --- /dev/null +++ b/db/migrations/0004_water_log_admin.sql @@ -0,0 +1,109 @@ +-- ============================================================================ +-- 0004_water_log_admin.sql +-- +-- Adds the three water-log tables that the Drizzle schema +-- (db/schema/water-log.ts) declares but 0001_init.sql never created. +-- The corresponding actions live in src/actions/water-log/* and were +-- stubbed as "Water log is not configured in the SaaS rebuild" before +-- this migration; calling them raised "relation does not exist" and +-- /api/water-admin-auth returned a 500 "Server error" to the field. +-- +-- water_admin_settings — per-brand admin PIN + portal config +-- water_admin_sessions — /water/admin sign-in sessions +-- water_audit_log — who changed what, when +-- +-- All three are brand-scoped with RLS, matching the 0001 pattern +-- (`brand_id = current_brand_id() OR is_platform_admin()`). +-- ============================================================================ + +-- ============================================================================ +-- water_admin_settings +-- ============================================================================ +CREATE TABLE IF NOT EXISTS water_admin_settings ( + brand_id UUID PRIMARY KEY REFERENCES brands(id) ON DELETE CASCADE, + -- Hashed admin PIN, format: scrypt$N$r$p$salt_b64$hash_b64 + -- (see src/lib/water-log-pin.ts). Nullable so a brand can exist + -- without an admin PIN until one is generated. + pin_hash TEXT, + enabled BOOLEAN NOT NULL DEFAULT true, + session_duration_hours INTEGER NOT NULL DEFAULT 4, + can_edit_entries BOOLEAN NOT NULL DEFAULT true, + can_delete_entries BOOLEAN NOT NULL DEFAULT true, + can_export_csv BOOLEAN NOT NULL DEFAULT true, + alert_phone TEXT, + alerts_enabled BOOLEAN NOT NULL DEFAULT false, + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_by UUID REFERENCES admin_users(id) +); + +-- ============================================================================ +-- water_admin_sessions +-- ============================================================================ +CREATE TABLE IF NOT EXISTS water_admin_sessions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + brand_id UUID NOT NULL REFERENCES brands(id) ON DELETE CASCADE, + admin_user_id UUID NOT NULL REFERENCES admin_users(id) ON DELETE CASCADE, + -- Snapshot of the PIN hash used to sign in, so a subsequent PIN + -- rotation invalidates the session without us having to walk every row. + pin_hash_used TEXT NOT NULL, + expires_at TIMESTAMPTZ NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE INDEX IF NOT EXISTS water_admin_sessions_admin_idx + ON water_admin_sessions (admin_user_id, expires_at); + +-- ============================================================================ +-- water_audit_log +-- ============================================================================ +CREATE TABLE IF NOT EXISTS water_audit_log ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + brand_id UUID NOT NULL REFERENCES brands(id) ON DELETE CASCADE, + actor_id UUID REFERENCES admin_users(id), + actor_label TEXT NOT NULL, + action TEXT NOT NULL, + entity_type TEXT NOT NULL, + entity_id UUID, + details JSONB, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE INDEX IF NOT EXISTS water_audit_log_brand_recent_idx + ON water_audit_log (brand_id, created_at); + +-- ============================================================================ +-- RLS +-- ============================================================================ +ALTER TABLE water_admin_settings ENABLE ROW LEVEL SECURITY; +ALTER TABLE water_admin_settings FORCE ROW LEVEL SECURITY; + +ALTER TABLE water_admin_sessions ENABLE ROW LEVEL SECURITY; +ALTER TABLE water_admin_sessions FORCE ROW LEVEL SECURITY; + +ALTER TABLE water_audit_log ENABLE ROW LEVEL SECURITY; +ALTER TABLE water_audit_log FORCE ROW LEVEL SECURITY; + +-- ============================================================================ +-- Policies +-- ============================================================================ + +-- water_admin_settings: PK is brand_id, so a single per-brand row. +DROP POLICY IF EXISTS tenant_isolation ON water_admin_settings; +CREATE POLICY tenant_isolation ON water_admin_settings + FOR ALL + USING (brand_id = current_brand_id() OR is_platform_admin()) + WITH CHECK (brand_id = current_brand_id() OR is_platform_admin()); + +-- water_admin_sessions: keyed off brand_id directly. +DROP POLICY IF EXISTS tenant_isolation ON water_admin_sessions; +CREATE POLICY tenant_isolation ON water_admin_sessions + FOR ALL + USING (brand_id = current_brand_id() OR is_platform_admin()) + WITH CHECK (brand_id = current_brand_id() OR is_platform_admin()); + +-- water_audit_log: keyed off brand_id directly. +DROP POLICY IF EXISTS tenant_isolation ON water_audit_log; +CREATE POLICY tenant_isolation ON water_audit_log + FOR ALL + USING (brand_id = current_brand_id() OR is_platform_admin()) + WITH CHECK (brand_id = current_brand_id() OR is_platform_admin());