06e536fb87
Deploy to route.crispygoat.com / deploy (push) Successful in 3m46s
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.
110 lines
4.8 KiB
SQL
110 lines
4.8 KiB
SQL
-- ============================================================================
|
|
-- 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());
|