-- Migration 005: User Linked Accounts (OAuth providers) -- Supports Discord, GitHub, Google, and other OAuth providers -- ============================================================================ -- USER LINKED ACCOUNTS -- ============================================================================ CREATE TABLE IF NOT EXISTS user_linked_accounts ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, provider VARCHAR(50) NOT NULL CHECK (provider IN ('discord', 'github', 'google', 'twitter', 'twitch', 'roblox')), provider_user_id VARCHAR(200) NOT NULL, provider_username VARCHAR(200), provider_email VARCHAR(300), provider_avatar VARCHAR(500), access_token TEXT, refresh_token TEXT, token_expires_at TIMESTAMP WITH TIME ZONE, scopes TEXT[], metadata JSONB, linked_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), UNIQUE(user_id, provider), UNIQUE(provider, provider_user_id) ); CREATE INDEX IF NOT EXISTS idx_linked_accounts_user ON user_linked_accounts(user_id); CREATE INDEX IF NOT EXISTS idx_linked_accounts_provider ON user_linked_accounts(provider); CREATE INDEX IF NOT EXISTS idx_linked_accounts_provider_user ON user_linked_accounts(provider, provider_user_id); -- ============================================================================ -- DISCORD PENDING LINKS (for bot-based verification) -- ============================================================================ CREATE TABLE IF NOT EXISTS discord_pending_links ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), discord_id VARCHAR(100) NOT NULL, discord_username VARCHAR(100), verification_code VARCHAR(20) NOT NULL, expires_at TIMESTAMP WITH TIME ZONE NOT NULL, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), UNIQUE(discord_id) ); CREATE INDEX IF NOT EXISTS idx_pending_links_code ON discord_pending_links(verification_code); CREATE INDEX IF NOT EXISTS idx_pending_links_expires ON discord_pending_links(expires_at); -- ============================================================================ -- USER FOLLOWED ARMS (for activity feed filtering) -- ============================================================================ CREATE TABLE IF NOT EXISTS user_followed_arms ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, arm_id VARCHAR(50) NOT NULL CHECK (arm_id IN ('labs', 'gameforge', 'corp', 'foundation', 'devlink', 'nexus', 'ethos')), followed_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), UNIQUE(user_id, arm_id) ); CREATE INDEX IF NOT EXISTS idx_followed_arms_user ON user_followed_arms(user_id); CREATE INDEX IF NOT EXISTS idx_followed_arms_arm ON user_followed_arms(arm_id); -- ============================================================================ -- ENABLE ROW LEVEL SECURITY -- ============================================================================ ALTER TABLE user_linked_accounts ENABLE ROW LEVEL SECURITY; ALTER TABLE discord_pending_links ENABLE ROW LEVEL SECURITY; ALTER TABLE user_followed_arms ENABLE ROW LEVEL SECURITY; -- ============================================================================ -- RLS POLICIES -- ============================================================================ -- Linked Accounts: Users can only see their own CREATE POLICY "linked_accounts_select" ON user_linked_accounts FOR SELECT USING (user_id = auth.uid()); CREATE POLICY "linked_accounts_insert" ON user_linked_accounts FOR INSERT WITH CHECK (user_id = auth.uid()); CREATE POLICY "linked_accounts_update" ON user_linked_accounts FOR UPDATE USING (user_id = auth.uid()); CREATE POLICY "linked_accounts_delete" ON user_linked_accounts FOR DELETE USING (user_id = auth.uid()); -- Pending Links: Service role only (Discord bot) CREATE POLICY "pending_links_service" ON discord_pending_links FOR ALL USING (auth.role() = 'service_role'); -- Followed Arms: Users can manage their own CREATE POLICY "followed_arms_select" ON user_followed_arms FOR SELECT USING (user_id = auth.uid()); CREATE POLICY "followed_arms_insert" ON user_followed_arms FOR INSERT WITH CHECK (user_id = auth.uid()); CREATE POLICY "followed_arms_delete" ON user_followed_arms FOR DELETE USING (user_id = auth.uid()); -- ============================================================================ -- TRIGGER FOR updated_at -- ============================================================================ CREATE OR REPLACE FUNCTION update_linked_accounts_timestamp() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = NOW(); RETURN NEW; END; $$ LANGUAGE plpgsql; DROP TRIGGER IF EXISTS trigger_linked_accounts_updated ON user_linked_accounts; CREATE TRIGGER trigger_linked_accounts_updated BEFORE UPDATE ON user_linked_accounts FOR EACH ROW EXECUTE FUNCTION update_linked_accounts_timestamp(); -- ============================================================================ -- CLEANUP EXPIRED PENDING LINKS -- ============================================================================ CREATE OR REPLACE FUNCTION cleanup_expired_pending_links() RETURNS void AS $$ BEGIN DELETE FROM discord_pending_links WHERE expires_at < NOW(); END; $$ LANGUAGE plpgsql;