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>
172 lines
7.1 KiB
Go
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
|
|
}
|