package db import ( "context" "database/sql" "fmt" "git.dustin.coffee/hobokenchicken/dumpsterChat/internal/config" _ "github.com/jackc/pgx/v5/stdlib" ) type DB struct { *sql.DB } func New(cfg *config.Config) (*DB, error) { db, err := sql.Open("pgx", cfg.DatabaseDSN()) if err != nil { return nil, fmt.Errorf("open database: %w", err) } if err := db.Ping(); err != nil { return nil, fmt.Errorf("ping database: %w", err) } return &DB{db}, nil } func (d *DB) RunMigrations() error { _, err := d.ExecContext(context.Background(), migrationsSQL) return err } const migrationsSQL = ` CREATE TABLE IF NOT EXISTS users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), username VARCHAR(32) NOT NULL UNIQUE, display_name VARCHAR(32), email VARCHAR(255) NOT NULL UNIQUE, password_hash VARCHAR(255), avatar TEXT, bio VARCHAR(250) DEFAULT '', accent_color VARCHAR(7) DEFAULT '', status VARCHAR(16) NOT NULL DEFAULT 'offline', status_text VARCHAR(128) DEFAULT '', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_users_username ON users(username); CREATE TABLE IF NOT EXISTS sessions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, token VARCHAR(255) NOT NULL UNIQUE, expires_at TIMESTAMPTZ NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS servers ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name VARCHAR(100) NOT NULL, icon TEXT, owner_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS channels ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), server_id UUID NOT NULL REFERENCES servers(id) ON DELETE CASCADE, name VARCHAR(100) NOT NULL, type VARCHAR(16) NOT NULL DEFAULT 'text', category VARCHAR(100) NOT NULL DEFAULT 'general', position INTEGER NOT NULL DEFAULT 0, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE (server_id, name) ); CREATE TABLE IF NOT EXISTS members ( user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, server_id UUID NOT NULL REFERENCES servers(id) ON DELETE CASCADE, joined_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), PRIMARY KEY (user_id, server_id) ); CREATE TABLE IF NOT EXISTS messages ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), channel_id UUID NOT NULL REFERENCES channels(id) ON DELETE CASCADE, author_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, content TEXT NOT NULL, reply_to UUID REFERENCES messages(id) ON DELETE SET NULL, edited_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS roles ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), server_id UUID NOT NULL REFERENCES servers(id) ON DELETE CASCADE, name VARCHAR(100) NOT NULL, color VARCHAR(7), permissions BIGINT NOT NULL DEFAULT 0, position INTEGER NOT NULL DEFAULT 0, is_default BOOLEAN NOT NULL DEFAULT FALSE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS member_roles ( user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, server_id UUID NOT NULL REFERENCES servers(id) ON DELETE CASCADE, role_id UUID NOT NULL REFERENCES roles(id) ON DELETE CASCADE, PRIMARY KEY (user_id, role_id) ); CREATE INDEX IF NOT EXISTS idx_messages_channel_created ON messages(channel_id, created_at DESC); CREATE INDEX IF NOT EXISTS idx_sessions_token ON sessions(token); CREATE INDEX IF NOT EXISTS idx_members_server_user ON members(server_id, user_id); CREATE TABLE IF NOT EXISTS reactions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), message_id UUID NOT NULL REFERENCES messages(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, emoji VARCHAR(64) NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE (message_id, user_id, emoji) ); CREATE INDEX IF NOT EXISTS idx_reactions_message ON reactions(message_id); CREATE TABLE IF NOT EXISTS invites ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), code VARCHAR(16) NOT NULL UNIQUE, server_id UUID NOT NULL REFERENCES servers(id) ON DELETE CASCADE, created_by UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, expires_at TIMESTAMPTZ, max_uses INTEGER, use_count INTEGER NOT NULL DEFAULT 0, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_invites_code ON invites(code); CREATE TABLE IF NOT EXISTS bots ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name VARCHAR(100) NOT NULL, avatar TEXT, description TEXT DEFAULT '', owner_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, token VARCHAR(64) NOT NULL UNIQUE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS bot_servers ( bot_id UUID NOT NULL REFERENCES bots(id) ON DELETE CASCADE, server_id UUID NOT NULL REFERENCES servers(id) ON DELETE CASCADE, added_by UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, added_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), PRIMARY KEY (bot_id, server_id) ); CREATE TABLE IF NOT EXISTS slash_commands ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), bot_id UUID NOT NULL REFERENCES bots(id) ON DELETE CASCADE, server_id UUID NOT NULL REFERENCES servers(id) ON DELETE CASCADE, name VARCHAR(32) NOT NULL, description TEXT DEFAULT '', options JSONB DEFAULT '[]', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE (server_id, name) ); CREATE TABLE IF NOT EXISTS webhooks ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), channel_id UUID NOT NULL REFERENCES channels(id) ON DELETE CASCADE, name VARCHAR(100) NOT NULL, token VARCHAR(64) NOT NULL, avatar TEXT, created_by UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_bots_owner ON bots(owner_id); CREATE INDEX IF NOT EXISTS idx_bots_token ON bots(token); CREATE INDEX IF NOT EXISTS idx_bot_servers_bot ON bot_servers(bot_id); CREATE INDEX IF NOT EXISTS idx_slash_commands_server ON slash_commands(server_id, name); CREATE INDEX IF NOT EXISTS idx_webhooks_channel ON webhooks(channel_id); CREATE INDEX IF NOT EXISTS idx_webhooks_token ON webhooks(token); -- Add token_hash column for secure webhook token storage ALTER TABLE webhooks ADD COLUMN IF NOT EXISTS token_hash VARCHAR(64); CREATE INDEX IF NOT EXISTS idx_webhooks_token_hash ON webhooks(token_hash); CREATE TABLE IF NOT EXISTS push_subscriptions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, endpoint TEXT NOT NULL, p256dh TEXT NOT NULL, auth TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE (user_id, endpoint) ); CREATE TABLE IF NOT EXISTS webauthn_credentials ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, credential_id BYTEA NOT NULL UNIQUE, public_key BYTEA NOT NULL, aaguid UUID, sign_count BIGINT NOT NULL DEFAULT 0, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_push_subscriptions_user ON push_subscriptions(user_id); CREATE INDEX IF NOT EXISTS idx_webauthn_credentials_user ON webauthn_credentials(user_id); `