134 lines
4.8 KiB
PL/PgSQL
134 lines
4.8 KiB
PL/PgSQL
-- ==========================================
|
|
-- Chat Switchboard — 001 Core
|
|
-- ==========================================
|
|
-- Users, authentication, platform policies, and global settings.
|
|
-- Foundation tables with no external FK dependencies.
|
|
--
|
|
-- ICD §2 (Auth), §14 (User Profile & Settings), §16 (Platform Admin)
|
|
-- v0.22.8: Domain-grouped migration (replaces monolith 001 + incrementals)
|
|
-- ==========================================
|
|
|
|
-- ── Extensions ──────────────────────────────
|
|
|
|
CREATE EXTENSION IF NOT EXISTS "pgcrypto"; -- gen_random_uuid()
|
|
CREATE EXTENSION IF NOT EXISTS "vector"; -- pgvector (KB embeddings)
|
|
|
|
-- ── Utility: auto-update updated_at ─────────
|
|
|
|
CREATE OR REPLACE FUNCTION update_updated_at()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
NEW.updated_at = NOW();
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
|
|
-- =========================================
|
|
-- USERS
|
|
-- =========================================
|
|
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
username VARCHAR(50) NOT NULL,
|
|
email VARCHAR(255) NOT NULL,
|
|
password_hash TEXT NOT NULL,
|
|
display_name VARCHAR(100),
|
|
avatar_url TEXT,
|
|
role VARCHAR(20) DEFAULT 'user'
|
|
CHECK (role IN ('user', 'admin')),
|
|
is_active BOOLEAN DEFAULT true,
|
|
settings JSONB DEFAULT '{}'::jsonb,
|
|
|
|
-- Vault: per-user encryption key (UEK) for BYOK API keys
|
|
encrypted_uek BYTEA,
|
|
uek_salt BYTEA,
|
|
uek_nonce BYTEA,
|
|
vault_set BOOLEAN NOT NULL DEFAULT false,
|
|
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
|
last_login_at TIMESTAMPTZ
|
|
);
|
|
|
|
-- Case-insensitive unique indexes (no bare UNIQUE constraint)
|
|
CREATE UNIQUE INDEX IF NOT EXISTS users_username_ci ON users (LOWER(username));
|
|
CREATE UNIQUE INDEX IF NOT EXISTS users_email_ci ON users (LOWER(email));
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
|
|
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
|
|
|
|
DROP TRIGGER IF EXISTS users_updated_at ON users;
|
|
CREATE TRIGGER users_updated_at BEFORE UPDATE ON users
|
|
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
|
|
|
COMMENT ON COLUMN users.encrypted_uek IS 'UEK encrypted with Argon2id(password)-derived key';
|
|
COMMENT ON COLUMN users.uek_salt IS 'Argon2id salt for UEK derivation';
|
|
COMMENT ON COLUMN users.uek_nonce IS 'AES-GCM nonce for UEK wrapping';
|
|
COMMENT ON COLUMN users.vault_set IS 'true once UEK has been generated and wrapped';
|
|
|
|
|
|
-- =========================================
|
|
-- AUTH — REFRESH TOKENS
|
|
-- =========================================
|
|
|
|
CREATE TABLE IF NOT EXISTS refresh_tokens (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
token_hash TEXT NOT NULL UNIQUE,
|
|
expires_at TIMESTAMPTZ NOT NULL,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
revoked_at TIMESTAMPTZ
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_refresh_tokens_user ON refresh_tokens(user_id);
|
|
CREATE INDEX IF NOT EXISTS idx_refresh_tokens_hash ON refresh_tokens(token_hash) WHERE revoked_at IS NULL;
|
|
|
|
|
|
-- =========================================
|
|
-- PLATFORM POLICIES
|
|
-- =========================================
|
|
|
|
CREATE TABLE IF NOT EXISTS platform_policies (
|
|
key VARCHAR(50) PRIMARY KEY,
|
|
value TEXT NOT NULL,
|
|
updated_by UUID REFERENCES users(id),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
-- Secure by default
|
|
INSERT INTO platform_policies (key, value) VALUES
|
|
('allow_user_byok', 'false'),
|
|
('allow_user_personas', 'false'),
|
|
('allow_raw_model_access', 'true'),
|
|
('allow_registration', 'true'),
|
|
('default_user_active', 'false'),
|
|
('allow_team_providers', 'true'),
|
|
('kb_direct_access', 'true')
|
|
ON CONFLICT (key) DO NOTHING;
|
|
|
|
COMMENT ON TABLE platform_policies IS 'Global admin switches controlling platform behavior';
|
|
|
|
|
|
-- =========================================
|
|
-- GLOBAL SETTINGS
|
|
-- =========================================
|
|
|
|
CREATE TABLE IF NOT EXISTS global_settings (
|
|
key VARCHAR(100) PRIMARY KEY,
|
|
value JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_by UUID REFERENCES users(id)
|
|
);
|
|
|
|
INSERT INTO global_settings (key, value) VALUES
|
|
('registration', '{"enabled": true}'::jsonb),
|
|
('site', '{"name": "Chat Switchboard", "tagline": "Multi-Model AI Chat"}'::jsonb),
|
|
('banner', '{"enabled": false, "text": "", "bg": "#007a33", "fg": "#ffffff"}'::jsonb),
|
|
('model_roles', '{
|
|
"utility": { "primary": null, "fallback": null },
|
|
"embedding": { "primary": null, "fallback": null },
|
|
"generation": { "primary": null, "fallback": null }
|
|
}'::jsonb)
|
|
ON CONFLICT (key) DO NOTHING;
|