Files
flint/database/migrations/0004_saas_multitenant.sql
T
Puechberty Arthur 3063796eb0 feat: introduce web dashboard with multi-bot management
- add Discord OAuth2 authentication
- allow users to register their bots via token
- implement dynamic bot start/stop system
- store bots in database (multi-tenant)
- replace single-bot env setup with scalable architecture
2026-04-18 01:39:12 +02:00

221 lines
6.5 KiB
PL/PgSQL

CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE TABLE IF NOT EXISTS tenants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
owner_user_id UUID,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
discord_user_id TEXT NOT NULL UNIQUE,
username TEXT NOT NULL,
avatar_url TEXT,
role TEXT NOT NULL CHECK (role IN ('owner', 'member')),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'fk_tenants_owner_user'
) THEN
ALTER TABLE tenants
ADD CONSTRAINT fk_tenants_owner_user
FOREIGN KEY (owner_user_id)
REFERENCES users(id)
ON DELETE SET NULL;
END IF;
END $$;
CREATE TABLE IF NOT EXISTS bots (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
owner_user_id UUID NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
discord_bot_id TEXT NOT NULL,
display_name TEXT NOT NULL,
token_ciphertext TEXT NOT NULL,
token_iv TEXT NOT NULL,
token_tag TEXT NOT NULL,
status TEXT NOT NULL CHECK (status IN ('stopped', 'starting', 'running', 'stopping', 'error')),
last_error TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (tenant_id, discord_bot_id),
UNIQUE (discord_bot_id)
);
CREATE TABLE IF NOT EXISTS bot_runtime_events (
id BIGSERIAL PRIMARY KEY,
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
bot_id UUID NOT NULL REFERENCES bots(id) ON DELETE CASCADE,
level TEXT NOT NULL CHECK (level IN ('info', 'warn', 'error')),
message TEXT NOT NULL,
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_users_tenant_id ON users (tenant_id);
CREATE INDEX IF NOT EXISTS idx_bots_tenant_id ON bots (tenant_id);
CREATE INDEX IF NOT EXISTS idx_bot_runtime_events_tenant_created ON bot_runtime_events (tenant_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_bot_runtime_events_bot_created ON bot_runtime_events (bot_id, created_at DESC);
CREATE OR REPLACE FUNCTION set_updated_at_timestamp()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$;
DROP TRIGGER IF EXISTS trg_users_updated_at ON users;
CREATE TRIGGER trg_users_updated_at
BEFORE UPDATE ON users
FOR EACH ROW
EXECUTE FUNCTION set_updated_at_timestamp();
DROP TRIGGER IF EXISTS trg_bots_updated_at ON bots;
CREATE TRIGGER trg_bots_updated_at
BEFORE UPDATE ON bots
FOR EACH ROW
EXECUTE FUNCTION set_updated_at_timestamp();
ALTER TABLE bot_presence_states
ADD COLUMN IF NOT EXISTS tenant_id UUID,
ADD COLUMN IF NOT EXISTS owner_user_id UUID;
ALTER TABLE bot_member_message_configs
ADD COLUMN IF NOT EXISTS tenant_id UUID,
ADD COLUMN IF NOT EXISTS owner_user_id UUID;
ALTER TABLE bot_log_event_configs
ADD COLUMN IF NOT EXISTS tenant_id UUID,
ADD COLUMN IF NOT EXISTS owner_user_id UUID;
UPDATE bot_presence_states p
SET tenant_id = b.tenant_id,
owner_user_id = b.owner_user_id
FROM bots b
WHERE p.bot_id = b.discord_bot_id
AND (p.tenant_id IS NULL OR p.owner_user_id IS NULL);
UPDATE bot_member_message_configs m
SET tenant_id = b.tenant_id,
owner_user_id = b.owner_user_id
FROM bots b
WHERE m.bot_id = b.discord_bot_id
AND (m.tenant_id IS NULL OR m.owner_user_id IS NULL);
UPDATE bot_log_event_configs l
SET tenant_id = b.tenant_id,
owner_user_id = b.owner_user_id
FROM bots b
WHERE l.bot_id = b.discord_bot_id
AND (l.tenant_id IS NULL OR l.owner_user_id IS NULL);
DELETE FROM bot_presence_states p
WHERE p.tenant_id IS NULL OR p.owner_user_id IS NULL;
DELETE FROM bot_member_message_configs m
WHERE m.tenant_id IS NULL OR m.owner_user_id IS NULL;
DELETE FROM bot_log_event_configs l
WHERE l.tenant_id IS NULL OR l.owner_user_id IS NULL;
ALTER TABLE bot_presence_states
ALTER COLUMN tenant_id SET NOT NULL,
ALTER COLUMN owner_user_id SET NOT NULL;
ALTER TABLE bot_member_message_configs
ALTER COLUMN tenant_id SET NOT NULL,
ALTER COLUMN owner_user_id SET NOT NULL;
ALTER TABLE bot_log_event_configs
ALTER COLUMN tenant_id SET NOT NULL,
ALTER COLUMN owner_user_id SET NOT NULL;
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'fk_bot_presence_states_tenant'
) THEN
ALTER TABLE bot_presence_states
ADD CONSTRAINT fk_bot_presence_states_tenant
FOREIGN KEY (tenant_id)
REFERENCES tenants(id)
ON DELETE CASCADE;
END IF;
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'fk_bot_presence_states_owner_user'
) THEN
ALTER TABLE bot_presence_states
ADD CONSTRAINT fk_bot_presence_states_owner_user
FOREIGN KEY (owner_user_id)
REFERENCES users(id)
ON DELETE RESTRICT;
END IF;
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'fk_bot_member_message_configs_tenant'
) THEN
ALTER TABLE bot_member_message_configs
ADD CONSTRAINT fk_bot_member_message_configs_tenant
FOREIGN KEY (tenant_id)
REFERENCES tenants(id)
ON DELETE CASCADE;
END IF;
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'fk_bot_member_message_configs_owner_user'
) THEN
ALTER TABLE bot_member_message_configs
ADD CONSTRAINT fk_bot_member_message_configs_owner_user
FOREIGN KEY (owner_user_id)
REFERENCES users(id)
ON DELETE RESTRICT;
END IF;
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'fk_bot_log_event_configs_tenant'
) THEN
ALTER TABLE bot_log_event_configs
ADD CONSTRAINT fk_bot_log_event_configs_tenant
FOREIGN KEY (tenant_id)
REFERENCES tenants(id)
ON DELETE CASCADE;
END IF;
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'fk_bot_log_event_configs_owner_user'
) THEN
ALTER TABLE bot_log_event_configs
ADD CONSTRAINT fk_bot_log_event_configs_owner_user
FOREIGN KEY (owner_user_id)
REFERENCES users(id)
ON DELETE RESTRICT;
END IF;
END $$;
CREATE INDEX IF NOT EXISTS idx_bot_presence_states_tenant_id ON bot_presence_states (tenant_id);
CREATE INDEX IF NOT EXISTS idx_bot_member_message_configs_tenant_id ON bot_member_message_configs (tenant_id);
CREATE INDEX IF NOT EXISTS idx_bot_log_event_configs_tenant_id ON bot_log_event_configs (tenant_id);