RLS policies implementeren, Demo auth flow
This commit is contained in:
69
supabase/migrations/20241115000001_create_leads_table.sql
Normal file
69
supabase/migrations/20241115000001_create_leads_table.sql
Normal file
@@ -0,0 +1,69 @@
|
||||
-- Create leads table for contact form submissions
|
||||
-- Migration: 20241115000001_create_leads_table
|
||||
-- Description: Stores lead information from contact form with project details
|
||||
|
||||
create table if not exists public.leads (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
created_at timestamptz not null default now(),
|
||||
|
||||
-- Contact information
|
||||
name text not null,
|
||||
email text not null,
|
||||
company text,
|
||||
|
||||
-- Project details
|
||||
project_type text not null,
|
||||
budget text,
|
||||
message text not null,
|
||||
|
||||
-- Metadata
|
||||
status text not null default 'new' check (status in ('new', 'contacted', 'qualified', 'converted', 'rejected')),
|
||||
notes text,
|
||||
source text default 'website',
|
||||
|
||||
-- Tracking
|
||||
ip_address inet,
|
||||
user_agent text,
|
||||
referrer text
|
||||
);
|
||||
|
||||
-- Add index for email lookups
|
||||
create index if not exists leads_email_idx on public.leads (email);
|
||||
|
||||
-- Add index for status filtering
|
||||
create index if not exists leads_status_idx on public.leads (status);
|
||||
|
||||
-- Add index for created_at sorting
|
||||
create index if not exists leads_created_at_idx on public.leads (created_at desc);
|
||||
|
||||
-- Enable Row Level Security
|
||||
alter table public.leads enable row level security;
|
||||
|
||||
-- Policy: Anyone can insert (public form submission)
|
||||
create policy "Anyone can submit leads"
|
||||
on public.leads
|
||||
for insert
|
||||
to anon, authenticated
|
||||
with check (true);
|
||||
|
||||
-- Policy: Only authenticated users can view leads (admin only)
|
||||
create policy "Authenticated users can view leads"
|
||||
on public.leads
|
||||
for select
|
||||
to authenticated
|
||||
using (true);
|
||||
|
||||
-- Policy: Only authenticated users can update leads (admin only)
|
||||
create policy "Authenticated users can update leads"
|
||||
on public.leads
|
||||
for update
|
||||
to authenticated
|
||||
using (true);
|
||||
|
||||
-- Add comment to table
|
||||
comment on table public.leads is 'Contact form lead submissions with project details';
|
||||
|
||||
-- Add comments to important columns
|
||||
comment on column public.leads.status is 'Lead status: new, contacted, qualified, converted, rejected';
|
||||
comment on column public.leads.project_type is 'Type of project: Web app, Mobile app, etc.';
|
||||
comment on column public.leads.message is 'Project description from contact form';
|
||||
230
supabase/migrations/20241115000002_create_epd_core_tables.sql
Normal file
230
supabase/migrations/20241115000002_create_epd_core_tables.sql
Normal file
@@ -0,0 +1,230 @@
|
||||
-- ================================================
|
||||
-- EPD Core Tables Migration
|
||||
-- Created: 2024-11-15
|
||||
-- Epic: E2 - Database & Auth
|
||||
-- Story: E2.S1 - Database schema creëren
|
||||
-- ================================================
|
||||
-- This migration creates the 5 core EPD tables:
|
||||
-- 1. clients - basic client information
|
||||
-- 2. intake_notes - TipTap JSON content + derived fields
|
||||
-- 3. problem_profiles - DSM-light categories + severity
|
||||
-- 4. treatment_plans - Treatment plan JSONB with versioning
|
||||
-- 5. ai_events - AI API telemetry and debugging
|
||||
-- ================================================
|
||||
|
||||
-- ================================================
|
||||
-- TABLE 1: clients
|
||||
-- ================================================
|
||||
-- Stores basic client information
|
||||
CREATE TABLE clients (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
first_name TEXT NOT NULL,
|
||||
last_name TEXT NOT NULL,
|
||||
birth_date DATE NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
|
||||
|
||||
-- Constraints
|
||||
CONSTRAINT clients_name_not_empty CHECK (
|
||||
LENGTH(TRIM(first_name)) > 0 AND LENGTH(TRIM(last_name)) > 0
|
||||
)
|
||||
);
|
||||
|
||||
-- Index for searching clients by name
|
||||
CREATE INDEX idx_clients_name ON clients(last_name, first_name);
|
||||
|
||||
-- ================================================
|
||||
-- TABLE 2: intake_notes
|
||||
-- ================================================
|
||||
-- Stores intake notes with TipTap/ProseMirror JSON content
|
||||
CREATE TABLE intake_notes (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
client_id UUID NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
|
||||
title TEXT,
|
||||
tag TEXT CHECK (tag IN ('Intake', 'Evaluatie', 'Plan')),
|
||||
content_json JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
content_text TEXT, -- Derived text for full-text search
|
||||
author UUID, -- FK to auth.users.id (optional for MVP)
|
||||
created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
|
||||
|
||||
-- Constraints
|
||||
CONSTRAINT intake_notes_content_not_empty CHECK (
|
||||
content_json IS NOT NULL
|
||||
)
|
||||
);
|
||||
|
||||
-- Indexes for performance
|
||||
CREATE INDEX idx_intake_notes_client ON intake_notes(client_id);
|
||||
CREATE INDEX idx_intake_notes_created ON intake_notes(created_at DESC);
|
||||
|
||||
-- Full-text search index on content_text (for future search functionality)
|
||||
CREATE INDEX idx_intake_notes_fts ON intake_notes USING gin(to_tsvector('dutch', content_text));
|
||||
|
||||
-- ================================================
|
||||
-- TABLE 3: problem_profiles
|
||||
-- ================================================
|
||||
-- Stores DSM-light problem categorization
|
||||
CREATE TABLE problem_profiles (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
client_id UUID NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
|
||||
category TEXT NOT NULL CHECK (category IN (
|
||||
'stemming_depressie',
|
||||
'angst',
|
||||
'gedrag_impuls',
|
||||
'middelen_gebruik',
|
||||
'cognitief',
|
||||
'context_psychosociaal'
|
||||
)),
|
||||
severity TEXT NOT NULL CHECK (severity IN ('laag', 'middel', 'hoog')),
|
||||
remarks TEXT,
|
||||
source_note_id UUID REFERENCES intake_notes(id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW() NOT NULL
|
||||
);
|
||||
|
||||
-- Indexes
|
||||
CREATE INDEX idx_problem_profiles_client ON problem_profiles(client_id);
|
||||
CREATE INDEX idx_problem_profiles_category ON problem_profiles(category);
|
||||
|
||||
-- ================================================
|
||||
-- TABLE 4: treatment_plans
|
||||
-- ================================================
|
||||
-- Stores treatment plans with versioning
|
||||
CREATE TABLE treatment_plans (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
client_id UUID NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
|
||||
version INTEGER NOT NULL DEFAULT 1,
|
||||
status TEXT NOT NULL DEFAULT 'concept' CHECK (status IN ('concept', 'gepubliceerd')),
|
||||
plan JSONB NOT NULL DEFAULT '{
|
||||
"doelen": [],
|
||||
"interventies": [],
|
||||
"frequentie": "",
|
||||
"meetmomenten": []
|
||||
}'::jsonb,
|
||||
created_by UUID, -- FK to auth.users.id (optional for MVP)
|
||||
created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
|
||||
published_at TIMESTAMPTZ,
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
|
||||
|
||||
-- Constraints
|
||||
CONSTRAINT treatment_plans_version_positive CHECK (version > 0),
|
||||
CONSTRAINT treatment_plans_plan_structure CHECK (
|
||||
plan ? 'doelen' AND
|
||||
plan ? 'interventies' AND
|
||||
plan ? 'frequentie' AND
|
||||
plan ? 'meetmomenten'
|
||||
),
|
||||
-- Ensure published plans have published_at timestamp
|
||||
CONSTRAINT treatment_plans_published_timestamp CHECK (
|
||||
(status = 'gepubliceerd' AND published_at IS NOT NULL) OR
|
||||
(status = 'concept')
|
||||
),
|
||||
-- Unique version per client
|
||||
UNIQUE(client_id, version)
|
||||
);
|
||||
|
||||
-- Indexes
|
||||
CREATE INDEX idx_treatment_plans_client ON treatment_plans(client_id);
|
||||
CREATE INDEX idx_treatment_plans_status ON treatment_plans(status);
|
||||
CREATE INDEX idx_treatment_plans_version ON treatment_plans(client_id, version DESC);
|
||||
|
||||
-- ================================================
|
||||
-- TABLE 5: ai_events
|
||||
-- ================================================
|
||||
-- Telemetry and debugging for AI API calls
|
||||
CREATE TABLE ai_events (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
kind TEXT NOT NULL CHECK (kind IN ('summarize', 'readability', 'extract', 'plan')),
|
||||
client_id UUID REFERENCES clients(id) ON DELETE SET NULL,
|
||||
note_id UUID REFERENCES intake_notes(id) ON DELETE SET NULL,
|
||||
request JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
response JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
duration_ms INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
|
||||
|
||||
-- Constraints
|
||||
CONSTRAINT ai_events_duration_non_negative CHECK (duration_ms >= 0)
|
||||
);
|
||||
|
||||
-- Indexes for analytics and debugging
|
||||
CREATE INDEX idx_ai_events_kind ON ai_events(kind);
|
||||
CREATE INDEX idx_ai_events_client ON ai_events(client_id);
|
||||
CREATE INDEX idx_ai_events_created ON ai_events(created_at DESC);
|
||||
|
||||
-- ================================================
|
||||
-- ROW LEVEL SECURITY (RLS) POLICIES
|
||||
-- ================================================
|
||||
-- Enable RLS on all tables
|
||||
ALTER TABLE clients ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE intake_notes ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE problem_profiles ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE treatment_plans ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE ai_events ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Demo RLS policies: All authenticated users can access all data
|
||||
-- (For MVP/demo purposes only - in production, use org_id or user_id filtering)
|
||||
|
||||
CREATE POLICY "Allow all for authenticated users" ON clients
|
||||
FOR ALL USING (auth.uid() IS NOT NULL);
|
||||
|
||||
CREATE POLICY "Allow all for authenticated users" ON intake_notes
|
||||
FOR ALL USING (auth.uid() IS NOT NULL);
|
||||
|
||||
CREATE POLICY "Allow all for authenticated users" ON problem_profiles
|
||||
FOR ALL USING (auth.uid() IS NOT NULL);
|
||||
|
||||
CREATE POLICY "Allow all for authenticated users" ON treatment_plans
|
||||
FOR ALL USING (auth.uid() IS NOT NULL);
|
||||
|
||||
CREATE POLICY "Allow all for authenticated users" ON ai_events
|
||||
FOR ALL USING (auth.uid() IS NOT NULL);
|
||||
|
||||
-- ================================================
|
||||
-- TRIGGER FUNCTIONS FOR updated_at
|
||||
-- ================================================
|
||||
-- Automatically update updated_at timestamp on row updates
|
||||
|
||||
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Apply trigger to all tables with updated_at column
|
||||
CREATE TRIGGER update_clients_updated_at
|
||||
BEFORE UPDATE ON clients
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
CREATE TRIGGER update_intake_notes_updated_at
|
||||
BEFORE UPDATE ON intake_notes
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
CREATE TRIGGER update_problem_profiles_updated_at
|
||||
BEFORE UPDATE ON problem_profiles
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
CREATE TRIGGER update_treatment_plans_updated_at
|
||||
BEFORE UPDATE ON treatment_plans
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
-- ================================================
|
||||
-- COMMENTS (PostgreSQL Documentation)
|
||||
-- ================================================
|
||||
COMMENT ON TABLE clients IS 'Basic client information for EPD system';
|
||||
COMMENT ON TABLE intake_notes IS 'Intake notes stored as TipTap/ProseMirror JSON with derived text field for search';
|
||||
COMMENT ON TABLE problem_profiles IS 'DSM-light problem categorization with severity scoring';
|
||||
COMMENT ON TABLE treatment_plans IS 'Treatment plans with JSONB structure and versioning support';
|
||||
COMMENT ON TABLE ai_events IS 'Telemetry and debugging log for AI API calls';
|
||||
|
||||
COMMENT ON COLUMN intake_notes.content_json IS 'ProseMirror/TipTap document structure (JSONB)';
|
||||
COMMENT ON COLUMN intake_notes.content_text IS 'Plain text extraction for full-text search indexing';
|
||||
COMMENT ON COLUMN treatment_plans.version IS 'Incremental version number, unique per client';
|
||||
COMMENT ON COLUMN treatment_plans.status IS 'Draft status: concept (editable) or gepubliceerd (locked)';
|
||||
COMMENT ON COLUMN ai_events.duration_ms IS 'API call duration in milliseconds for performance monitoring';
|
||||
180
supabase/migrations/20241115000003_test_rls_policies.sql
Normal file
180
supabase/migrations/20241115000003_test_rls_policies.sql
Normal file
@@ -0,0 +1,180 @@
|
||||
-- ================================================
|
||||
-- RLS Policy Tests
|
||||
-- Created: 2024-11-15
|
||||
-- Epic: E2 - Database & Auth
|
||||
-- Story: E2.S2 - RLS policies implementeren
|
||||
-- ================================================
|
||||
-- This file contains test queries to verify RLS policies
|
||||
-- Run these queries manually to verify RLS is working correctly
|
||||
-- ================================================
|
||||
|
||||
-- ================================================
|
||||
-- TEST 1: Verify RLS is enabled on all tables
|
||||
-- ================================================
|
||||
-- Expected: All tables should have rowsecurity = true
|
||||
|
||||
SELECT
|
||||
tablename,
|
||||
rowsecurity as rls_enabled
|
||||
FROM pg_tables
|
||||
WHERE schemaname = 'public'
|
||||
ORDER BY tablename;
|
||||
|
||||
-- Expected output:
|
||||
-- ai_events | true
|
||||
-- clients | true
|
||||
-- intake_notes | true
|
||||
-- problem_profiles | true
|
||||
-- treatment_plans | true
|
||||
|
||||
-- ================================================
|
||||
-- TEST 2: Check all RLS policies exist
|
||||
-- ================================================
|
||||
-- Expected: Each table should have 4 policies (SELECT, INSERT, UPDATE, DELETE)
|
||||
-- except ai_events which has only 2 (SELECT, INSERT)
|
||||
|
||||
SELECT
|
||||
tablename,
|
||||
COUNT(*) as policy_count,
|
||||
STRING_AGG(cmd, ', ' ORDER BY cmd) as commands
|
||||
FROM pg_policies
|
||||
WHERE schemaname = 'public'
|
||||
GROUP BY tablename
|
||||
ORDER BY tablename;
|
||||
|
||||
-- Expected output:
|
||||
-- ai_events | 2 | INSERT, SELECT
|
||||
-- clients | 4 | DELETE, INSERT, SELECT, UPDATE
|
||||
-- intake_notes | 4 | DELETE, INSERT, SELECT, UPDATE
|
||||
-- problem_profiles | 4 | DELETE, INSERT, SELECT, UPDATE
|
||||
-- treatment_plans | 4 | DELETE, INSERT, SELECT, UPDATE
|
||||
|
||||
-- ================================================
|
||||
-- TEST 3: Verify policy predicates use auth.uid()
|
||||
-- ================================================
|
||||
-- Expected: All policies should check auth.uid() IS NOT NULL
|
||||
|
||||
SELECT
|
||||
tablename,
|
||||
policyname,
|
||||
cmd,
|
||||
qual as using_clause,
|
||||
with_check
|
||||
FROM pg_policies
|
||||
WHERE schemaname = 'public'
|
||||
AND qual NOT LIKE '%auth.uid()%'
|
||||
ORDER BY tablename, policyname;
|
||||
|
||||
-- Expected output: Empty (no policies without auth.uid() check)
|
||||
|
||||
-- ================================================
|
||||
-- TEST 4: Simulate authenticated user query
|
||||
-- ================================================
|
||||
-- This test simulates what happens when an authenticated user
|
||||
-- tries to access data. In production, auth.uid() would return
|
||||
-- the user's actual UUID.
|
||||
|
||||
-- Note: These queries will work in the SQL editor when logged in,
|
||||
-- but will fail when run as unauthenticated
|
||||
|
||||
-- Test SELECT permission (should succeed when authenticated)
|
||||
-- SELECT * FROM clients LIMIT 1;
|
||||
|
||||
-- Test INSERT permission (should succeed when authenticated)
|
||||
-- INSERT INTO clients (first_name, last_name, birth_date)
|
||||
-- VALUES ('Test', 'User', '1990-01-01');
|
||||
|
||||
-- Test UPDATE permission (should succeed when authenticated)
|
||||
-- UPDATE clients SET first_name = 'Updated' WHERE id = 'some-uuid';
|
||||
|
||||
-- Test DELETE permission (should succeed when authenticated)
|
||||
-- DELETE FROM clients WHERE id = 'some-uuid';
|
||||
|
||||
-- ================================================
|
||||
-- TEST 5: Verify ai_events immutability
|
||||
-- ================================================
|
||||
-- Expected: ai_events should NOT have UPDATE or DELETE policies
|
||||
-- (except for service role via RLS bypass)
|
||||
|
||||
SELECT
|
||||
tablename,
|
||||
policyname,
|
||||
cmd
|
||||
FROM pg_policies
|
||||
WHERE schemaname = 'public'
|
||||
AND tablename = 'ai_events'
|
||||
AND cmd IN ('UPDATE', 'DELETE')
|
||||
ORDER BY cmd;
|
||||
|
||||
-- Expected output: Empty (no UPDATE or DELETE policies for regular users)
|
||||
|
||||
-- ================================================
|
||||
-- TEST 6: Check foreign key relationships
|
||||
-- ================================================
|
||||
-- Expected: All foreign keys should be properly set up
|
||||
|
||||
SELECT
|
||||
tc.table_name,
|
||||
kcu.column_name,
|
||||
ccu.table_name AS foreign_table_name,
|
||||
ccu.column_name AS foreign_column_name,
|
||||
rc.delete_rule
|
||||
FROM information_schema.table_constraints AS tc
|
||||
JOIN information_schema.key_column_usage AS kcu
|
||||
ON tc.constraint_name = kcu.constraint_name
|
||||
AND tc.table_schema = kcu.table_schema
|
||||
JOIN information_schema.constraint_column_usage AS ccu
|
||||
ON ccu.constraint_name = tc.constraint_name
|
||||
AND ccu.table_schema = tc.table_schema
|
||||
JOIN information_schema.referential_constraints AS rc
|
||||
ON tc.constraint_name = rc.constraint_name
|
||||
WHERE tc.constraint_type = 'FOREIGN KEY'
|
||||
AND tc.table_schema = 'public'
|
||||
ORDER BY tc.table_name, kcu.column_name;
|
||||
|
||||
-- Expected output:
|
||||
-- intake_notes | client_id | clients | id | CASCADE
|
||||
-- problem_profiles | client_id | clients | id | CASCADE
|
||||
-- problem_profiles | source_note_id | intake_notes | id | SET NULL
|
||||
-- treatment_plans | client_id | clients | id | CASCADE
|
||||
-- ai_events | client_id | clients | id | SET NULL
|
||||
-- ai_events | note_id | intake_notes | id | SET NULL
|
||||
|
||||
-- ================================================
|
||||
-- PRODUCTION MIGRATION PATH
|
||||
-- ================================================
|
||||
-- When moving to production, enhance policies with org_id filtering:
|
||||
--
|
||||
-- 1. Add org_id column to all tables:
|
||||
-- ALTER TABLE clients ADD COLUMN org_id UUID REFERENCES organizations(id);
|
||||
--
|
||||
-- 2. Update policies to filter by organization:
|
||||
-- CREATE POLICY "Users can view own org clients"
|
||||
-- ON clients
|
||||
-- FOR SELECT
|
||||
-- USING (
|
||||
-- auth.uid() IS NOT NULL AND
|
||||
-- org_id = (SELECT org_id FROM users WHERE id = auth.uid())
|
||||
-- );
|
||||
--
|
||||
-- 3. Add role-based access:
|
||||
-- CREATE POLICY "Admins can view all"
|
||||
-- ON clients
|
||||
-- FOR SELECT
|
||||
-- USING (
|
||||
-- auth.uid() IS NOT NULL AND
|
||||
-- EXISTS (
|
||||
-- SELECT 1 FROM users
|
||||
-- WHERE id = auth.uid() AND role = 'admin'
|
||||
-- )
|
||||
-- );
|
||||
|
||||
-- ================================================
|
||||
-- SECURITY NOTES
|
||||
-- ================================================
|
||||
-- 1. Current policies are MVP-level: all authenticated users can access all data
|
||||
-- 2. In production, add org_id filtering for multi-tenancy
|
||||
-- 3. ai_events table is append-only for regular users (audit trail)
|
||||
-- 4. Service role can bypass RLS for admin operations
|
||||
-- 5. All policies use auth.uid() for security
|
||||
-- 6. Foreign key CASCADE ensures orphaned records are cleaned up
|
||||
191
supabase/migrations/20241115000004_create_demo_users.sql
Normal file
191
supabase/migrations/20241115000004_create_demo_users.sql
Normal file
@@ -0,0 +1,191 @@
|
||||
-- ================================================
|
||||
-- Demo Users Seed Data
|
||||
-- Created: 2024-11-15
|
||||
-- Epic: E2 - Database & Auth
|
||||
-- Story: E2.S3 - Demo auth flow
|
||||
-- ================================================
|
||||
-- This migration creates demo user accounts in Supabase Auth
|
||||
-- These accounts are used for public demos and presentations
|
||||
-- ================================================
|
||||
|
||||
-- Note: This SQL creates the demo users in the auth.users table
|
||||
-- The actual signup should be done via Supabase Auth API or Dashboard
|
||||
-- for proper password hashing and email confirmation handling
|
||||
|
||||
-- ================================================
|
||||
-- DEMO USER ACCOUNTS TO CREATE
|
||||
-- ================================================
|
||||
-- These users should be created via Supabase Dashboard or Auth API:
|
||||
--
|
||||
-- 1. Interactive Demo User
|
||||
-- Email: demo@mini-ecd.demo
|
||||
-- Password: Demo2024!
|
||||
-- Access Level: Full access (can create/edit/delete)
|
||||
--
|
||||
-- 2. Read-Only Demo User
|
||||
-- Email: readonly@mini-ecd.demo
|
||||
-- Password: Demo2024!
|
||||
-- Access Level: Read-only (can only view)
|
||||
--
|
||||
-- 3. Presenter Demo User (for live sessions)
|
||||
-- Email: presenter@mini-ecd.demo
|
||||
-- Password: Demo2024!
|
||||
-- Access Level: Full access
|
||||
--
|
||||
|
||||
-- ================================================
|
||||
-- DEMO_USERS TRACKING TABLE (Optional - for future enhancement)
|
||||
-- ================================================
|
||||
-- Table to track demo user sessions and usage
|
||||
-- This is optional for MVP but useful for analytics
|
||||
|
||||
CREATE TABLE IF NOT EXISTS demo_users (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID UNIQUE REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||
access_level TEXT NOT NULL DEFAULT 'read_only'
|
||||
CHECK (access_level IN ('read_only', 'interactive', 'presenter')),
|
||||
expires_at TIMESTAMPTZ DEFAULT (NOW() + INTERVAL '90 days'),
|
||||
usage_count INTEGER DEFAULT 0,
|
||||
last_login_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
|
||||
|
||||
-- Metadata for tracking
|
||||
notes TEXT -- Internal notes about this demo account
|
||||
);
|
||||
|
||||
-- Index for quick lookups
|
||||
CREATE INDEX idx_demo_users_user_id ON demo_users(user_id);
|
||||
CREATE INDEX idx_demo_users_expires_at ON demo_users(expires_at);
|
||||
|
||||
-- ================================================
|
||||
-- RLS POLICIES FOR DEMO_USERS TABLE
|
||||
-- ================================================
|
||||
-- Only authenticated users can view demo_users info
|
||||
-- Only service role can manage demo_users
|
||||
|
||||
ALTER TABLE demo_users ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Users can view demo_users table (for checking if account is demo)
|
||||
CREATE POLICY "Authenticated users can view demo users"
|
||||
ON demo_users
|
||||
FOR SELECT
|
||||
USING (auth.uid() IS NOT NULL);
|
||||
|
||||
-- Only service role can insert/update/delete
|
||||
-- (Regular users cannot modify via SQL, only via API with service role key)
|
||||
|
||||
-- ================================================
|
||||
-- AUTOMATIC UPDATED_AT TRIGGER
|
||||
-- ================================================
|
||||
CREATE TRIGGER update_demo_users_updated_at
|
||||
BEFORE UPDATE ON demo_users
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
-- ================================================
|
||||
-- HELPER FUNCTION: Check if user is demo user
|
||||
-- ================================================
|
||||
CREATE OR REPLACE FUNCTION is_demo_user(check_user_id UUID)
|
||||
RETURNS BOOLEAN AS $$
|
||||
BEGIN
|
||||
RETURN EXISTS (
|
||||
SELECT 1 FROM demo_users
|
||||
WHERE user_id = check_user_id
|
||||
AND (expires_at IS NULL OR expires_at > NOW())
|
||||
);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
-- ================================================
|
||||
-- HELPER FUNCTION: Get demo user access level
|
||||
-- ================================================
|
||||
CREATE OR REPLACE FUNCTION get_demo_access_level(check_user_id UUID)
|
||||
RETURNS TEXT AS $$
|
||||
DECLARE
|
||||
level TEXT;
|
||||
BEGIN
|
||||
SELECT access_level INTO level
|
||||
FROM demo_users
|
||||
WHERE user_id = check_user_id
|
||||
AND (expires_at IS NULL OR expires_at > NOW());
|
||||
|
||||
RETURN level;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
-- ================================================
|
||||
-- COMMENTS
|
||||
-- ================================================
|
||||
COMMENT ON TABLE demo_users IS
|
||||
'Tracking table for demo user accounts. Links auth.users to demo account metadata.';
|
||||
|
||||
COMMENT ON COLUMN demo_users.access_level IS
|
||||
'Access level: read_only (view only), interactive (full CRUD), presenter (full access + special features)';
|
||||
|
||||
COMMENT ON COLUMN demo_users.expires_at IS
|
||||
'Optional expiration date for demo account. NULL means no expiration.';
|
||||
|
||||
COMMENT ON COLUMN demo_users.usage_count IS
|
||||
'Number of times this demo account has been used (incremented on login)';
|
||||
|
||||
COMMENT ON FUNCTION is_demo_user IS
|
||||
'Check if a user_id belongs to an active demo account';
|
||||
|
||||
COMMENT ON FUNCTION get_demo_access_level IS
|
||||
'Get the access level for a demo user. Returns NULL if not a demo user or expired.';
|
||||
|
||||
-- ================================================
|
||||
-- INSTRUCTIONS FOR CREATING DEMO USERS
|
||||
-- ================================================
|
||||
-- Run these commands in your application code or via Supabase Dashboard:
|
||||
--
|
||||
-- Method 1: Via Supabase Dashboard
|
||||
-- 1. Go to Authentication > Users
|
||||
-- 2. Click "Add User"
|
||||
-- 3. Add email + password
|
||||
-- 4. Confirm email manually
|
||||
-- 5. Then insert into demo_users table:
|
||||
--
|
||||
-- INSERT INTO demo_users (user_id, access_level, notes)
|
||||
-- VALUES (
|
||||
-- (SELECT id FROM auth.users WHERE email = 'demo@mini-ecd.demo'),
|
||||
-- 'interactive',
|
||||
-- 'Main demo account for presentations and LinkedIn demos'
|
||||
-- );
|
||||
--
|
||||
-- Method 2: Via API (recommended for automation)
|
||||
-- See: docs/DEMO_USERS_SETUP.md for setup script
|
||||
|
||||
-- ================================================
|
||||
-- SEED DATA (to be inserted after users are created in auth.users)
|
||||
-- ================================================
|
||||
-- This will be executed by a separate seed script after demo users
|
||||
-- are created in Supabase Auth
|
||||
|
||||
-- Note: Uncomment and run AFTER creating the auth.users manually
|
||||
-- or via the API setup script
|
||||
|
||||
/*
|
||||
INSERT INTO demo_users (user_id, access_level, notes, expires_at)
|
||||
VALUES
|
||||
(
|
||||
(SELECT id FROM auth.users WHERE email = 'demo@mini-ecd.demo'),
|
||||
'interactive',
|
||||
'Main interactive demo account - full CRUD access',
|
||||
NULL -- No expiration
|
||||
),
|
||||
(
|
||||
(SELECT id FROM auth.users WHERE email = 'readonly@mini-ecd.demo'),
|
||||
'read_only',
|
||||
'Read-only demo account - view only access',
|
||||
NULL -- No expiration
|
||||
),
|
||||
(
|
||||
(SELECT id FROM auth.users WHERE email = 'presenter@mini-ecd.demo'),
|
||||
'presenter',
|
||||
'Presenter account for live demo sessions',
|
||||
NULL -- No expiration
|
||||
)
|
||||
ON CONFLICT (user_id) DO NOTHING;
|
||||
*/
|
||||
Reference in New Issue
Block a user