Files
breakpilot-core/consent-service/internal/database/migrate_oauth.go
Benjamin Admin 92c86ec6ba [split-required] [guardrail-change] Enforce 500 LOC budget across all services
Install LOC guardrails (check-loc.sh, architecture.md, pre-commit hook)
and split all 44 files exceeding 500 LOC into domain-focused modules:

- consent-service (Go): models, handlers, services, database splits
- backend-core (Python): security_api, rbac_api, pdf_service, auth splits
- admin-core (TypeScript): 5 page.tsx + sidebar extractions
- pitch-deck (TypeScript): 6 slides, 3 UI components, engine.ts splits
- voice-service (Python): enhanced_task_orchestrator split

Result: 0 violations, 36 exempted (pipeline, tests, pure-data files).
Go build verified clean. No behavior changes — pure structural splits.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-27 00:09:30 +02:00

172 lines
7.1 KiB
Go

package database
import (
"context"
"fmt"
)
// migrateOAuth creates OAuth 2.0 and 2FA tables (Phases 6-7),
// plus default seed data for OAuth clients, cookie categories,
// and legal documents.
func migrateOAuth(db *DB) error {
ctx := context.Background()
migrations := []string{
// =============================================
// Phase 6: OAuth 2.0 Authorization Code Flow
// =============================================
// OAuth 2.0 Clients
`CREATE TABLE IF NOT EXISTS oauth_clients (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
client_id VARCHAR(64) UNIQUE NOT NULL,
client_secret VARCHAR(255),
name VARCHAR(255) NOT NULL,
description TEXT,
redirect_uris JSONB NOT NULL DEFAULT '[]',
scopes JSONB NOT NULL DEFAULT '["openid", "profile", "email"]',
grant_types JSONB NOT NULL DEFAULT '["authorization_code", "refresh_token"]',
is_public BOOLEAN DEFAULT FALSE,
is_active BOOLEAN DEFAULT TRUE,
created_by UUID REFERENCES users(id),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
)`,
// OAuth 2.0 Authorization Codes
`CREATE TABLE IF NOT EXISTS oauth_authorization_codes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
code VARCHAR(255) UNIQUE NOT NULL,
client_id VARCHAR(64) NOT NULL REFERENCES oauth_clients(client_id),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
redirect_uri TEXT NOT NULL,
scopes JSONB NOT NULL DEFAULT '[]',
code_challenge VARCHAR(255),
code_challenge_method VARCHAR(10),
expires_at TIMESTAMPTZ NOT NULL,
used_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW()
)`,
// OAuth 2.0 Access Tokens
`CREATE TABLE IF NOT EXISTS oauth_access_tokens (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
token_hash VARCHAR(255) UNIQUE NOT NULL,
client_id VARCHAR(64) NOT NULL REFERENCES oauth_clients(client_id),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
scopes JSONB NOT NULL DEFAULT '[]',
expires_at TIMESTAMPTZ NOT NULL,
revoked_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW()
)`,
// OAuth 2.0 Refresh Tokens
`CREATE TABLE IF NOT EXISTS oauth_refresh_tokens (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
token_hash VARCHAR(255) UNIQUE NOT NULL,
access_token_id UUID REFERENCES oauth_access_tokens(id) ON DELETE CASCADE,
client_id VARCHAR(64) NOT NULL REFERENCES oauth_clients(client_id),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
scopes JSONB NOT NULL DEFAULT '[]',
expires_at TIMESTAMPTZ NOT NULL,
revoked_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW()
)`,
// =============================================
// Phase 7: Two-Factor Authentication (2FA/TOTP)
// =============================================
// User TOTP secrets and recovery codes
`CREATE TABLE IF NOT EXISTS user_totp (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID UNIQUE NOT NULL REFERENCES users(id) ON DELETE CASCADE,
secret VARCHAR(255) NOT NULL,
verified BOOLEAN DEFAULT FALSE,
recovery_codes JSONB DEFAULT '[]',
enabled_at TIMESTAMPTZ,
last_used_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
)`,
// 2FA challenges during login
`CREATE TABLE IF NOT EXISTS two_factor_challenges (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
challenge_id VARCHAR(255) UNIQUE NOT NULL,
ip_address INET,
user_agent TEXT,
expires_at TIMESTAMPTZ NOT NULL,
used_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW()
)`,
// Add 2FA required flag to users
`ALTER TABLE users ADD COLUMN IF NOT EXISTS two_factor_enabled BOOLEAN DEFAULT FALSE`,
`ALTER TABLE users ADD COLUMN IF NOT EXISTS two_factor_verified_at TIMESTAMPTZ`,
// Phase 6 & 7 Indexes
`CREATE INDEX IF NOT EXISTS idx_oauth_clients_client_id ON oauth_clients(client_id)`,
`CREATE INDEX IF NOT EXISTS idx_oauth_auth_codes_code ON oauth_authorization_codes(code)`,
`CREATE INDEX IF NOT EXISTS idx_oauth_auth_codes_user ON oauth_authorization_codes(user_id)`,
`CREATE INDEX IF NOT EXISTS idx_oauth_access_tokens_hash ON oauth_access_tokens(token_hash)`,
`CREATE INDEX IF NOT EXISTS idx_oauth_access_tokens_user ON oauth_access_tokens(user_id)`,
`CREATE INDEX IF NOT EXISTS idx_oauth_refresh_tokens_hash ON oauth_refresh_tokens(token_hash)`,
`CREATE INDEX IF NOT EXISTS idx_oauth_refresh_tokens_user ON oauth_refresh_tokens(user_id)`,
`CREATE INDEX IF NOT EXISTS idx_user_totp_user ON user_totp(user_id)`,
`CREATE INDEX IF NOT EXISTS idx_two_factor_challenges_id ON two_factor_challenges(challenge_id)`,
`CREATE INDEX IF NOT EXISTS idx_two_factor_challenges_user ON two_factor_challenges(user_id)`,
// Insert default OAuth client for BreakPilot PWA (public client with PKCE)
`INSERT INTO oauth_clients (client_id, name, description, redirect_uris, scopes, grant_types, is_public)
VALUES (
'breakpilot-pwa',
'BreakPilot PWA',
'Official BreakPilot Progressive Web Application',
'["http://localhost:8000/oauth/callback", "http://localhost:8000/app/oauth/callback"]',
'["openid", "profile", "email", "consent:read", "consent:write"]',
'["authorization_code", "refresh_token"]',
true
) ON CONFLICT (client_id) DO NOTHING`,
// Insert default cookie categories
`INSERT INTO cookie_categories (name, display_name_de, display_name_en, description_de, description_en, is_mandatory, sort_order)
VALUES
('necessary', 'Notwendige Cookies', 'Necessary Cookies',
'Diese Cookies sind für die Grundfunktionen der Website unbedingt erforderlich.',
'These cookies are essential for the basic functions of the website.',
true, 1),
('functional', 'Funktionale Cookies', 'Functional Cookies',
'Diese Cookies ermöglichen erweiterte Funktionen und Personalisierung.',
'These cookies enable enhanced functionality and personalization.',
false, 2),
('analytics', 'Analyse Cookies', 'Analytics Cookies',
'Diese Cookies helfen uns zu verstehen, wie Besucher mit der Website interagieren.',
'These cookies help us understand how visitors interact with the website.',
false, 3),
('marketing', 'Marketing Cookies', 'Marketing Cookies',
'Diese Cookies werden verwendet, um Werbung relevanter für Sie zu gestalten.',
'These cookies are used to make advertising more relevant to you.',
false, 4)
ON CONFLICT (name) DO NOTHING`,
// Insert default legal documents
`INSERT INTO legal_documents (type, name, description, is_mandatory, sort_order)
VALUES
('terms', 'Allgemeine Geschäftsbedingungen', 'Die allgemeinen Geschäftsbedingungen für die Nutzung von BreakPilot.', true, 1),
('privacy', 'Datenschutzerklärung', 'Informationen über die Verarbeitung Ihrer personenbezogenen Daten.', true, 2),
('cookies', 'Cookie-Richtlinie', 'Informationen über die Verwendung von Cookies auf unserer Website.', false, 3),
('community', 'Community Guidelines', 'Regeln für das Verhalten in der BreakPilot Community.', true, 4)
ON CONFLICT DO NOTHING`,
}
for _, migration := range migrations {
if _, err := db.Pool.Exec(ctx, migration); err != nil {
return fmt.Errorf("migrateOAuth: %w", err)
}
}
return nil
}