Co-authored-by: gobha <jasafpro@gmail.com> Co-committed-by: gobha <jasafpro@gmail.com>
160 lines
6.1 KiB
PL/PgSQL
160 lines
6.1 KiB
PL/PgSQL
-- ==========================================
|
|
-- Chat Switchboard — 001 Core
|
|
-- ==========================================
|
|
-- Users (with auth fields), refresh tokens, platform policies,
|
|
-- global settings, user presence.
|
|
-- Consolidated v0.28.0: merges 001 + 018 (auth_abstraction) + 016 (presence).
|
|
-- ==========================================
|
|
|
|
-- ── 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, -- nullable for mTLS/OIDC
|
|
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,
|
|
|
|
-- Auth abstraction (v0.24.0)
|
|
auth_source VARCHAR(20) NOT NULL DEFAULT 'builtin'
|
|
CHECK (auth_source IN ('builtin', 'mtls', 'oidc')),
|
|
external_id TEXT,
|
|
handle VARCHAR(100) NOT NULL,
|
|
|
|
-- 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
|
|
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 UNIQUE INDEX IF NOT EXISTS idx_users_handle ON users(LOWER(handle));
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_users_external_id
|
|
ON users(auth_source, external_id) WHERE external_id IS NOT NULL;
|
|
|
|
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.auth_source IS 'builtin=password, mtls=cert, oidc=SSO';
|
|
COMMENT ON COLUMN users.external_id IS 'IdP subject claim (OIDC) or cert fingerprint (mTLS)';
|
|
COMMENT ON COLUMN users.handle IS 'Unique @mention handle, auto-generated from username';
|
|
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()
|
|
);
|
|
|
|
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),
|
|
('retention_ttl_days', '{"value": 0}'::jsonb)
|
|
ON CONFLICT (key) DO NOTHING;
|
|
|
|
|
|
-- =========================================
|
|
-- USER PRESENCE (v0.23.1)
|
|
-- =========================================
|
|
|
|
CREATE TABLE IF NOT EXISTS user_presence (
|
|
user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
|
|
last_seen TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
status VARCHAR(20) NOT NULL DEFAULT 'online'
|
|
CHECK (status IN ('online', 'away', 'offline'))
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_user_presence_last_seen ON user_presence(last_seen DESC);
|
|
|
|
COMMENT ON TABLE user_presence IS 'Heartbeat-driven presence. Rows upserted every 30s by active clients.';
|