# AccountForger Backend - Verifikationsserver Spezifikation ## 1. Executive Summary **AccountForger** ist ein Desktop-Tool zur automatisierten Erstellung von Social-Media-Accounts (Instagram, Facebook, TikTok, X, etc.). Die Browser-Automation läuft lokal beim Kunden, aber die **Verifikation** (Email-Codes, SMS-Codes) wird über einen zentralen Server abgewickelt. ### Architektur-Übersicht ``` ┌─────────────────────────────────────────────────────────────────┐ │ KUNDE │ │ ┌─────────────────┐ ┌──────────────────┐ │ │ │ Desktop Client │ │ RUTX11 Router │ │ │ │ (AccountForger)│ │ (SMS-Empfang) │ │ │ └────────┬────────┘ └────────┬─────────┘ │ └───────────┼────────────────────────────────┼────────────────────┘ │ HTTPS (API) │ HTTPS (Webhook) ▼ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ VERIFIKATIONSSERVER (VPS) │ │ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │ │ │ Email │ │ SMS │ │ Client Registry │ │ │ │ Service │ │ Service │ │ (API-Key → Nummern) │ │ │ │ IMAP Polling│ │ Webhook │ │ │ │ │ └─────────────┘ └─────────────┘ └─────────────────────────┘ │ │ │ │ PostgreSQL FastAPI │ └─────────────────────────────────────────────────────────────────┘ ``` ### Scope-Abgrenzung | In Scope (dieses Backend) | Out of Scope | |---------------------------|--------------| | Email-Verifikations-API | Admin-Panel/Dashboard | | SMS-Verifikations-API | Kundenverwaltung (CRUD) | | RUTX11 Webhook-Empfang | Router-Konfiguration | | Phone-Rotation-Logik | Abrechnung/Billing | | API-Key-Validierung | Lizenz-Management | | Health-Check & Monitoring | Desktop-Client | **Wichtig:** Clients, Router und Telefonnummern werden manuell oder über ein separates Admin-System in der Datenbank angelegt. Dieses Backend geht davon aus, dass diese Daten bereits existieren. --- ## 2. Client-Architektur (Kontext) Der Desktop-Client folgt einer **Clean Architecture** mit Domain-Driven Design. Relevante Komponenten für die Backend-Integration: ### 2.1 Hauptkomponenten ``` AccountForger/ ├── social_networks/ │ ├── base_automation.py # Basis-Klasse für alle Plattformen │ ├── instagram/ │ │ ├── instagram_automation.py # Hauptlogik │ │ └── instagram_verification.py # Email-Code-Eingabe │ ├── facebook/ │ │ └── facebook_verification.py │ └── ... (tiktok, x, gmail, ok_ru, vk) ├── controllers/platform_controllers/ │ └── base_worker_thread.py # QThread für Account-Erstellung ├── utils/ │ └── email_handler.py # AKTUELL: IMAP-Polling (wird ersetzt) └── browser/ └── playwright_manager.py # Browser-Steuerung ``` ### 2.2 Aktuelle Email-Verifikation (zu ersetzen) Der `EmailHandler` (`utils/email_handler.py`) macht aktuell direktes IMAP-Polling: ```python # AKTUELLE Implementierung (wird ersetzt) class EmailHandler: def get_verification_code(self, target_email, platform, max_attempts=30): # Verbindet direkt zu IMAP-Server mail = imaplib.IMAP4_SSL(self.config["imap_server"]) mail.login(self.config["imap_user"], self.config["imap_pass"]) # Sucht nach Emails, extrahiert Code per Regex ... ``` **Problem:** IMAP-Credentials liegen im Client. **Lösung:** Client fragt Server-API, Server macht IMAP-Polling. ### 2.3 Aktuelle SMS-Verifikation (nicht implementiert) SMS-Verifikation ist ein Placeholder: ```python # social_networks/facebook/facebook_verification.py def handle_sms_verification(self, phone_number: str, timeout: int = 120): logger.warning("SMS-Verifikation noch nicht implementiert") return None ``` --- ## 3. Backend API-Spezifikation ### 3.1 OpenAPI-Endpunkte ```yaml openapi: 3.0.3 info: title: AccountForger Verification API version: 1.0.0 description: Backend für Email- und SMS-Verifikation servers: - url: https://verify.example.com/api/v1 security: - ApiKeyAuth: [] components: securitySchemes: ApiKeyAuth: type: apiKey in: header name: X-API-Key description: Client-spezifischer API-Key schemas: EmailRequest: type: object required: - platform properties: platform: type: string enum: [instagram, facebook, tiktok, x, gmail, twitter, vk, ok_ru] description: Zielplattform für die Registrierung preferred_domain: type: string description: Bevorzugte Email-Domain (optional) EmailResponse: type: object properties: request_id: type: string format: uuid email_address: type: string format: email expires_at: type: string format: date-time SMSRequest: type: object required: - platform properties: platform: type: string enum: [instagram, facebook, tiktok, x, gmail, twitter, vk, ok_ru] SMSResponse: type: object properties: request_id: type: string format: uuid phone_number: type: string description: Telefonnummer im Format +49... expires_at: type: string format: date-time CodeResponse: type: object properties: status: type: string enum: [pending, received, expired, failed] code: type: string nullable: true description: Verifikationscode (null wenn noch nicht empfangen) received_at: type: string format: date-time nullable: true WebhookPayload: type: object required: - sender - message properties: sender: type: string description: Absender-Nummer message: type: string description: SMS-Inhalt timestamp: type: string format: date-time sim_slot: type: integer enum: [1, 2] PhoneAvailability: type: object properties: available_count: type: integer phones: type: array items: type: object properties: phone_number: type: string cooldown_until: type: string format: date-time nullable: true last_platform: type: string nullable: true Error: type: object properties: error: type: string message: type: string retry_after: type: integer description: Sekunden bis Retry (bei Rate-Limit) paths: /verification/email/request: post: summary: Email-Adresse für Verifikation anfordern operationId: requestEmail requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/EmailRequest' responses: '201': description: Email-Adresse zugewiesen content: application/json: schema: $ref: '#/components/schemas/EmailResponse' '401': description: Ungültiger API-Key '429': description: Rate-Limit erreicht headers: Retry-After: schema: type: integer '503': description: Keine Email-Adresse verfügbar /verification/email/code/{request_id}: get: summary: Email-Verifikationscode abfragen (Polling) operationId: getEmailCode parameters: - name: request_id in: path required: true schema: type: string format: uuid responses: '200': description: Status des Verifikationscodes content: application/json: schema: $ref: '#/components/schemas/CodeResponse' '404': description: Request nicht gefunden '408': description: Timeout - kein Code empfangen /verification/sms/request: post: summary: Telefonnummer für SMS-Verifikation anfordern operationId: requestSMS requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/SMSRequest' responses: '201': description: Telefonnummer zugewiesen content: application/json: schema: $ref: '#/components/schemas/SMSResponse' '401': description: Ungültiger API-Key '429': description: Rate-Limit erreicht '503': description: Keine Telefonnummer verfügbar /verification/sms/code/{request_id}: get: summary: SMS-Verifikationscode abfragen (Polling) operationId: getSMSCode parameters: - name: request_id in: path required: true schema: type: string format: uuid responses: '200': description: Status des Verifikationscodes content: application/json: schema: $ref: '#/components/schemas/CodeResponse' '404': description: Request nicht gefunden '408': description: Timeout - kein Code empfangen /webhook/rutx11/sms: post: summary: SMS-Webhook vom RUTX11 Router operationId: receiveSMS security: - RouterToken: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/WebhookPayload' responses: '200': description: SMS empfangen und verarbeitet '401': description: Ungültiger Router-Token '422': description: Ungültiges Payload-Format /phone/available: get: summary: Verfügbare Telefonnummern abfragen operationId: getAvailablePhones responses: '200': description: Liste verfügbarer Nummern content: application/json: schema: $ref: '#/components/schemas/PhoneAvailability' /health: get: summary: Health-Check operationId: healthCheck security: [] responses: '200': description: Service ist gesund content: application/json: schema: type: object properties: status: type: string enum: [healthy, degraded] database: type: string redis: type: string imap_connections: type: integer ``` ### 3.2 PostgreSQL-Schema ```sql -- ===================================================== -- EXTENSION für UUID -- ===================================================== CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -- ===================================================== -- CLIENTS (Lizenznehmer) -- Werden MANUELL oder über separates Admin-System angelegt -- ===================================================== CREATE TABLE clients ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), license_key VARCHAR(50) UNIQUE NOT NULL, name VARCHAR(255) NOT NULL, api_key_hash VARCHAR(255) NOT NULL, -- bcrypt hash tier VARCHAR(20) DEFAULT 'standard' CHECK (tier IN ('standard', 'premium', 'enterprise')), is_active BOOLEAN DEFAULT true, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); CREATE INDEX idx_clients_api_key_hash ON clients(api_key_hash); CREATE INDEX idx_clients_license_key ON clients(license_key); -- ===================================================== -- TIER LIMITS (Rate-Limiting pro Tier) -- ===================================================== CREATE TABLE tier_limits ( tier VARCHAR(20) PRIMARY KEY, email_requests_per_hour INTEGER NOT NULL, sms_requests_per_hour INTEGER NOT NULL, max_concurrent_verifications INTEGER NOT NULL ); INSERT INTO tier_limits VALUES ('standard', 50, 20, 5), ('premium', 200, 100, 20), ('enterprise', 1000, 500, 100); -- ===================================================== -- ROUTERS (RUTX11 beim Kunden) -- Werden MANUELL angelegt nach Router-Versand -- ===================================================== CREATE TABLE routers ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), client_id UUID NOT NULL REFERENCES clients(id) ON DELETE CASCADE, router_token VARCHAR(100) UNIQUE NOT NULL, -- Für Webhook-Auth model VARCHAR(50) DEFAULT 'RUTX11', serial_number VARCHAR(100), webhook_url VARCHAR(500), -- Für Health-Checks is_online BOOLEAN DEFAULT false, last_seen_at TIMESTAMP WITH TIME ZONE, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); CREATE INDEX idx_routers_client_id ON routers(client_id); CREATE INDEX idx_routers_token ON routers(router_token); -- ===================================================== -- PHONE NUMBERS (eSIMs in den Routern) -- Werden MANUELL angelegt nach eSIM-Aktivierung -- ===================================================== CREATE TABLE phone_numbers ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), router_id UUID NOT NULL REFERENCES routers(id) ON DELETE CASCADE, phone_number VARCHAR(20) UNIQUE NOT NULL, -- Format: +49... esim_slot INTEGER NOT NULL CHECK (esim_slot IN (1, 2)), carrier VARCHAR(100), -- z.B. "Telekom", "Vodafone" is_active BOOLEAN DEFAULT true, cooldown_until TIMESTAMP WITH TIME ZONE, -- Gesperrt bis usage_count INTEGER DEFAULT 0, last_used_at TIMESTAMP WITH TIME ZONE, last_platform VARCHAR(50), -- Letzte Plattform created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), UNIQUE(router_id, esim_slot) ); CREATE INDEX idx_phone_numbers_router_id ON phone_numbers(router_id); CREATE INDEX idx_phone_numbers_cooldown ON phone_numbers(cooldown_until) WHERE is_active = true; -- ===================================================== -- EMAIL ACCOUNTS (Catch-All Domains für Server) -- Werden MANUELL angelegt -- ===================================================== CREATE TABLE email_accounts ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), email_address VARCHAR(255) UNIQUE NOT NULL, -- z.B. info@domain.com imap_server VARCHAR(255) NOT NULL, imap_port INTEGER DEFAULT 993, password_encrypted BYTEA NOT NULL, -- AES-256 verschlüsselt domain VARCHAR(255) NOT NULL, -- Catch-All Domain is_active BOOLEAN DEFAULT true, last_checked_at TIMESTAMP WITH TIME ZONE, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); CREATE INDEX idx_email_accounts_domain ON email_accounts(domain) WHERE is_active = true; -- ===================================================== -- VERIFICATION REQUESTS (Kernentität) -- ===================================================== CREATE TYPE verification_type AS ENUM ('email', 'sms'); CREATE TYPE verification_status AS ENUM ('pending', 'polling', 'received', 'expired', 'failed'); CREATE TABLE verification_requests ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), client_id UUID NOT NULL REFERENCES clients(id), -- Request-Details platform VARCHAR(50) NOT NULL, type verification_type NOT NULL, -- Email-spezifisch email_address VARCHAR(255), email_account_id UUID REFERENCES email_accounts(id), -- SMS-spezifisch phone_number_id UUID REFERENCES phone_numbers(id), -- Status status verification_status DEFAULT 'pending', verification_code VARCHAR(20), -- Timestamps created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), expires_at TIMESTAMP WITH TIME ZONE NOT NULL, code_received_at TIMESTAMP WITH TIME ZONE, -- Constraints CHECK ( (type = 'email' AND email_address IS NOT NULL) OR (type = 'sms' AND phone_number_id IS NOT NULL) ) ); CREATE INDEX idx_verification_requests_client ON verification_requests(client_id); CREATE INDEX idx_verification_requests_status ON verification_requests(status) WHERE status = 'pending'; CREATE INDEX idx_verification_requests_email ON verification_requests(email_address) WHERE type = 'email'; CREATE INDEX idx_verification_requests_phone ON verification_requests(phone_number_id) WHERE type = 'sms'; CREATE INDEX idx_verification_requests_expires ON verification_requests(expires_at) WHERE status IN ('pending', 'polling'); -- ===================================================== -- SMS MESSAGES (Empfangene SMS vom Router) -- ===================================================== CREATE TABLE sms_messages ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), router_id UUID NOT NULL REFERENCES routers(id), phone_number VARCHAR(20) NOT NULL, -- Empfänger-Nummer sender VARCHAR(50) NOT NULL, -- Absender (z.B. "Instagram") message_body TEXT NOT NULL, sim_slot INTEGER, -- Matching processed BOOLEAN DEFAULT false, matched_request_id UUID REFERENCES verification_requests(id), extracted_code VARCHAR(20), -- Timestamps received_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), processed_at TIMESTAMP WITH TIME ZONE ); CREATE INDEX idx_sms_messages_router ON sms_messages(router_id); CREATE INDEX idx_sms_messages_unprocessed ON sms_messages(received_at) WHERE processed = false; CREATE INDEX idx_sms_messages_phone ON sms_messages(phone_number, received_at DESC); -- ===================================================== -- RATE LIMIT TRACKING -- ===================================================== CREATE TABLE rate_limit_tracking ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), client_id UUID NOT NULL REFERENCES clients(id), request_type VARCHAR(20) NOT NULL, -- 'email' oder 'sms' window_start TIMESTAMP WITH TIME ZONE NOT NULL, request_count INTEGER DEFAULT 1, UNIQUE(client_id, request_type, window_start) ); CREATE INDEX idx_rate_limit_client ON rate_limit_tracking(client_id, request_type, window_start DESC); -- ===================================================== -- AUDIT LOG (Optional, für Debugging) -- ===================================================== CREATE TABLE audit_log ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), client_id UUID REFERENCES clients(id), action VARCHAR(100) NOT NULL, details JSONB, ip_address INET, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); CREATE INDEX idx_audit_log_client ON audit_log(client_id, created_at DESC); ``` ### 3.3 Entity-Relationship-Diagramm ```mermaid erDiagram CLIENTS ||--o{ ROUTERS : "besitzt" CLIENTS ||--o{ VERIFICATION_REQUESTS : "erstellt" CLIENTS }o--|| TIER_LIMITS : "hat" ROUTERS ||--o{ PHONE_NUMBERS : "enthält" ROUTERS ||--o{ SMS_MESSAGES : "empfängt" PHONE_NUMBERS ||--o{ VERIFICATION_REQUESTS : "wird_verwendet_für" EMAIL_ACCOUNTS ||--o{ VERIFICATION_REQUESTS : "wird_verwendet_für" VERIFICATION_REQUESTS ||--o| SMS_MESSAGES : "wird_gematcht_mit" CLIENTS { uuid id PK string license_key UK string api_key_hash string tier boolean is_active } ROUTERS { uuid id PK uuid client_id FK string router_token UK boolean is_online } PHONE_NUMBERS { uuid id PK uuid router_id FK string phone_number UK int esim_slot timestamp cooldown_until } EMAIL_ACCOUNTS { uuid id PK string email_address UK string imap_server bytea password_encrypted string domain } VERIFICATION_REQUESTS { uuid id PK uuid client_id FK string platform enum type string email_address uuid phone_number_id FK enum status string verification_code } SMS_MESSAGES { uuid id PK uuid router_id FK string sender string message_body uuid matched_request_id FK } ``` --- ## 4. Services - Implementierungsdetails ### 4.1 EmailService **Zweck:** IMAP-Polling nach Verifikations-Emails und Code-Extraktion. ```python # app/services/email_service.py import asyncio import aioimaplib import re from typing import Optional from datetime import datetime, timedelta from email import message_from_bytes from email.header import decode_header class EmailService: """Service für IMAP-Polling und Email-Code-Extraktion.""" # Plattform-spezifische Regex-Patterns für Code-Extraktion CODE_PATTERNS = { "instagram": [ r"(\d{6}) ist dein Instagram-Code", r"(\d{6}) is your Instagram code", r"Dein Code ist (\d{6})", r"Your code is (\d{6})", r"\b(\d{6})\b" # Fallback: 6-stellige Zahl ], "facebook": [ r"FB-(\d{5})", r"Bestätigungscode lautet (\d{5})", r"\b(\d{5})\b" # Fallback: 5-stellige Zahl ], "tiktok": [ r"(\d{6}) ist dein Bestätigungscode", r"(\d{6}) is your confirmation code", r"\b(\d{6})\b" ], "x": [ r"(\d{6}) ist dein X Verifizierungscode", r"(\d{6}) is your X verification code", r"\b(\d{6})\b" ], "default": [ r"[Cc]ode[:\s]*(\d{4,8})", r"\b(\d{6})\b" ] } # Betreff-Keywords pro Plattform SUBJECT_KEYWORDS = { "instagram": ["instagram", "bestätigungscode", "verification code"], "facebook": ["facebook", "fb-", "bestätigungscode"], "tiktok": ["tiktok", "bestätigungscode", "confirmation"], "x": ["verification code", "verifizierungscode"], "default": ["code", "verification", "bestätigung"] } def __init__(self, db_session, encryption_service): self.db = db_session self.encryption = encryption_service self._imap_connections = {} async def poll_for_code( self, request_id: str, email_address: str, platform: str, timeout_seconds: int = 120 ) -> Optional[str]: """ Pollt IMAP-Server nach Verifikations-Email. Args: request_id: ID der Verifikationsanfrage email_address: Ziel-Email-Adresse platform: Plattform (instagram, facebook, etc.) timeout_seconds: Maximale Wartezeit Returns: Verifikationscode oder None """ # Email-Account für Domain ermitteln domain = email_address.split("@")[1] email_account = await self._get_email_account_for_domain(domain) if not email_account: raise ValueError(f"Kein Email-Account für Domain {domain} konfiguriert") # IMAP-Verbindung herstellen/wiederverwenden imap = await self._get_imap_connection(email_account) start_time = datetime.utcnow() poll_interval = 2 # Sekunden while (datetime.utcnow() - start_time).seconds < timeout_seconds: # Nach neuen Emails suchen code = await self._search_and_extract_code( imap, email_address, platform, since=start_time - timedelta(minutes=5) ) if code: # Request aktualisieren await self._update_request_with_code(request_id, code) return code await asyncio.sleep(poll_interval) # Timeout await self._mark_request_expired(request_id) return None async def _search_and_extract_code( self, imap: aioimaplib.IMAP4_SSL, target_email: str, platform: str, since: datetime ) -> Optional[str]: """Durchsucht Emails nach Verifikationscode.""" # INBOX auswählen await imap.select("INBOX") # Suche nach Emails seit timestamp date_str = since.strftime("%d-%b-%Y") _, data = await imap.search(f'(SINCE "{date_str}")') email_ids = data[0].split() # Neueste zuerst for email_id in reversed(email_ids[-20:]): # Max 20 Emails prüfen _, msg_data = await imap.fetch(email_id, "(RFC822)") if not msg_data or not msg_data[1]: continue msg = message_from_bytes(msg_data[1]) # Empfänger prüfen to_addr = self._extract_email_from_header(msg.get("To", "")) if to_addr.lower() != target_email.lower(): continue # Betreff prüfen subject = self._decode_header(msg.get("Subject", "")) if not self._is_relevant_subject(subject, platform): continue # Body extrahieren und Code suchen body = self._extract_body(msg) code = self._extract_code(body, platform) if code: return code return None def _extract_code(self, text: str, platform: str) -> Optional[str]: """Extrahiert Verifikationscode aus Text.""" patterns = self.CODE_PATTERNS.get(platform, []) + self.CODE_PATTERNS["default"] for pattern in patterns: match = re.search(pattern, text, re.IGNORECASE) if match: return match.group(1) return None def _is_relevant_subject(self, subject: str, platform: str) -> bool: """Prüft ob Betreff zur Plattform passt.""" keywords = self.SUBJECT_KEYWORDS.get(platform, []) + self.SUBJECT_KEYWORDS["default"] subject_lower = subject.lower() return any(kw in subject_lower for kw in keywords) ``` ### 4.2 SMSService **Zweck:** Verarbeitung eingehender SMS via Webhook und Matching mit offenen Requests. ```python # app/services/sms_service.py import re from typing import Optional from datetime import datetime class SMSService: """Service für SMS-Verarbeitung und Request-Matching.""" # Code-Patterns für SMS (kürzer als Email-Patterns) SMS_CODE_PATTERNS = { "instagram": [r"(\d{6})"], "facebook": [r"(\d{5})", r"FB-(\d{5})"], "tiktok": [r"(\d{6})"], "x": [r"(\d{6})"], "default": [r"(\d{4,8})"] } # Absender-Keywords pro Plattform SENDER_KEYWORDS = { "instagram": ["instagram", "32665"], "facebook": ["facebook", "32665", "fb"], "tiktok": ["tiktok"], "x": ["twitter", "x.com"], } def __init__(self, db_session): self.db = db_session async def process_incoming_sms( self, router_token: str, sender: str, message: str, phone_number: str, sim_slot: Optional[int] = None ) -> dict: """ Verarbeitet eingehende SMS vom Router-Webhook. Args: router_token: Token zur Router-Authentifizierung sender: Absender der SMS message: SMS-Inhalt phone_number: Empfänger-Nummer (eSIM) sim_slot: SIM-Slot (1 oder 2) Returns: dict mit Verarbeitungsergebnis """ # Router validieren router = await self._validate_router_token(router_token) if not router: raise PermissionError("Ungültiger Router-Token") # Router als online markieren await self._update_router_last_seen(router.id) # SMS in DB speichern sms_record = await self._store_sms( router_id=router.id, phone_number=phone_number, sender=sender, message=message, sim_slot=sim_slot ) # Offenen Request finden request = await self._find_matching_request(phone_number) if not request: # Kein offener Request - SMS trotzdem speichern return { "status": "stored", "message": "SMS gespeichert, kein offener Request", "sms_id": str(sms_record.id) } # Code extrahieren code = self._extract_code_from_sms(message, request.platform) if code: # Request aktualisieren await self._update_request_with_code(request.id, code, sms_record.id) return { "status": "matched", "request_id": str(request.id), "code": code } return { "status": "stored", "message": "SMS gespeichert, kein Code extrahiert" } async def _find_matching_request(self, phone_number: str): """ Findet offenen Request für Telefonnummer. Matching-Logik: 1. Status = 'pending' oder 'polling' 2. phone_number stimmt überein 3. Noch nicht abgelaufen 4. Ältester Request zuerst (FIFO) """ query = """ SELECT vr.* FROM verification_requests vr JOIN phone_numbers pn ON vr.phone_number_id = pn.id WHERE pn.phone_number = $1 AND vr.status IN ('pending', 'polling') AND vr.expires_at > NOW() AND vr.type = 'sms' ORDER BY vr.created_at ASC LIMIT 1 """ return await self.db.fetch_one(query, phone_number) def _extract_code_from_sms(self, message: str, platform: str) -> Optional[str]: """Extrahiert Code aus SMS basierend auf Plattform.""" patterns = self.SMS_CODE_PATTERNS.get(platform, []) + self.SMS_CODE_PATTERNS["default"] for pattern in patterns: match = re.search(pattern, message) if match: return match.group(1) return None ``` ### 4.3 PhoneRotationService **Zweck:** Intelligente Auswahl von Telefonnummern mit Cooldown und Platform-Awareness. ```python # app/services/phone_rotation_service.py from datetime import datetime, timedelta from typing import Optional, List import random class PhoneRotationService: """Service für intelligente Telefonnummern-Rotation.""" # Cooldown-Zeiten pro Plattform (in Minuten) PLATFORM_COOLDOWNS = { "instagram": 60, # 1 Stunde "facebook": 45, "tiktok": 30, "x": 30, "default": 30 } def __init__(self, db_session): self.db = db_session async def get_available_phone( self, client_id: str, platform: str ) -> Optional[dict]: """ Wählt optimale Telefonnummer für Client und Plattform. Auswahlkriterien (Priorität): 1. Nummer gehört zum Client (über Router) 2. Nummer ist aktiv 3. Cooldown abgelaufen 4. Wurde nicht kürzlich für dieselbe Plattform verwendet 5. Geringste Gesamtnutzung (Load-Balancing) Returns: dict mit phone_number und id, oder None """ query = """ SELECT pn.id, pn.phone_number, pn.usage_count, pn.last_platform, pn.last_used_at, pn.cooldown_until FROM phone_numbers pn JOIN routers r ON pn.router_id = r.id WHERE r.client_id = $1 AND pn.is_active = true AND r.is_online = true AND (pn.cooldown_until IS NULL OR pn.cooldown_until < NOW()) ORDER BY -- Bevorzuge Nummern die nicht für diese Plattform verwendet wurden CASE WHEN pn.last_platform = $2 THEN 1 ELSE 0 END ASC, -- Dann nach Nutzungscount (Load-Balancing) pn.usage_count ASC, -- Dann zufällig für Variation RANDOM() LIMIT 1 """ phone = await self.db.fetch_one(query, client_id, platform) if phone: return { "id": str(phone["id"]), "phone_number": phone["phone_number"] } return None async def mark_phone_used( self, phone_id: str, platform: str ) -> None: """ Markiert Nummer als verwendet und setzt Cooldown. """ cooldown_minutes = self.PLATFORM_COOLDOWNS.get(platform, 30) cooldown_until = datetime.utcnow() + timedelta(minutes=cooldown_minutes) query = """ UPDATE phone_numbers SET usage_count = usage_count + 1, last_used_at = NOW(), last_platform = $2, cooldown_until = $3 WHERE id = $1 """ await self.db.execute(query, phone_id, platform, cooldown_until) async def get_available_count(self, client_id: str) -> int: """Gibt Anzahl verfügbarer Nummern zurück.""" query = """ SELECT COUNT(*) FROM phone_numbers pn JOIN routers r ON pn.router_id = r.id WHERE r.client_id = $1 AND pn.is_active = true AND r.is_online = true AND (pn.cooldown_until IS NULL OR pn.cooldown_until < NOW()) """ result = await self.db.fetch_one(query, client_id) return result["count"] async def release_phone(self, phone_id: str) -> None: """Gibt Nummer vorzeitig frei (z.B. bei Fehler).""" query = """ UPDATE phone_numbers SET cooldown_until = NULL WHERE id = $1 """ await self.db.execute(query, phone_id) ``` ### 4.4 AuthService **Zweck:** API-Key-Validierung und Rate-Limiting. ```python # app/services/auth_service.py import bcrypt from datetime import datetime, timedelta from typing import Optional from fastapi import HTTPException, Header class AuthService: """Service für Authentifizierung und Rate-Limiting.""" def __init__(self, db_session): self.db = db_session async def validate_api_key(self, api_key: str) -> dict: """ Validiert API-Key und gibt Client-Info zurück. Returns: dict mit client_id, tier, etc. Raises: HTTPException 401 bei ungültigem Key """ if not api_key: raise HTTPException(status_code=401, detail="API-Key fehlt") # Alle aktiven Clients laden query = """ SELECT id, license_key, api_key_hash, tier, name FROM clients WHERE is_active = true """ clients = await self.db.fetch_all(query) # Key gegen alle Hashes prüfen for client in clients: if bcrypt.checkpw(api_key.encode(), client["api_key_hash"].encode()): return { "client_id": str(client["id"]), "license_key": client["license_key"], "tier": client["tier"], "name": client["name"] } raise HTTPException(status_code=401, detail="Ungültiger API-Key") async def check_rate_limit( self, client_id: str, tier: str, request_type: str # 'email' oder 'sms' ) -> bool: """ Prüft und aktualisiert Rate-Limit. Returns: True wenn Request erlaubt, sonst HTTPException 429 """ # Tier-Limits laden limit_query = """ SELECT email_requests_per_hour, sms_requests_per_hour FROM tier_limits WHERE tier = $1 """ limits = await self.db.fetch_one(limit_query, tier) if not limits: limits = {"email_requests_per_hour": 50, "sms_requests_per_hour": 20} max_requests = ( limits["email_requests_per_hour"] if request_type == "email" else limits["sms_requests_per_hour"] ) # Aktuelle Stunde window_start = datetime.utcnow().replace(minute=0, second=0, microsecond=0) # Anzahl Requests in dieser Stunde count_query = """ SELECT COALESCE(SUM(request_count), 0) as total FROM rate_limit_tracking WHERE client_id = $1 AND request_type = $2 AND window_start >= $3 """ result = await self.db.fetch_one(count_query, client_id, request_type, window_start) current_count = result["total"] if current_count >= max_requests: # Rate-Limit erreicht retry_after = 3600 - (datetime.utcnow() - window_start).seconds raise HTTPException( status_code=429, detail="Rate-Limit erreicht", headers={"Retry-After": str(retry_after)} ) # Request zählen upsert_query = """ INSERT INTO rate_limit_tracking (client_id, request_type, window_start, request_count) VALUES ($1, $2, $3, 1) ON CONFLICT (client_id, request_type, window_start) DO UPDATE SET request_count = rate_limit_tracking.request_count + 1 """ await self.db.execute(upsert_query, client_id, request_type, window_start) return True # FastAPI Dependency async def get_current_client( x_api_key: str = Header(..., alias="X-API-Key"), db = Depends(get_db) ) -> dict: """FastAPI Dependency für API-Key-Validierung.""" auth_service = AuthService(db) return await auth_service.validate_api_key(x_api_key) ``` --- ## 5. Sequenzdiagramme ### 5.1 Email-Verifikations-Flow ```mermaid sequenceDiagram participant C as Desktop Client participant S as Verification Server participant DB as PostgreSQL participant IMAP as IMAP Server participant P as Platform (Instagram) C->>S: POST /verification/email/request
{platform: "instagram"} S->>S: API-Key validieren S->>DB: Rate-Limit prüfen S->>DB: Freie Email-Adresse wählen S->>DB: Verification Request anlegen S-->>C: 201 {request_id, email_address} Note over C: Client registriert Account
mit der Email-Adresse C->>P: Account-Registrierung mit Email P->>IMAP: Verifikations-Email senden loop Polling (max 120s) C->>S: GET /verification/email/code/{request_id} S->>IMAP: Emails abrufen (IMAP) alt Email gefunden S->>S: Code extrahieren (Regex) S->>DB: Code speichern S-->>C: 200 {status: "received", code: "123456"} else Email nicht gefunden S-->>C: 200 {status: "pending", code: null} end end Note over C: Client gibt Code ein C->>P: Verifikationscode eingeben P-->>C: Account verifiziert ``` ### 5.2 SMS-Verifikations-Flow ```mermaid sequenceDiagram participant C as Desktop Client participant S as Verification Server participant DB as PostgreSQL participant R as RUTX11 Router participant P as Platform (Instagram) C->>S: POST /verification/sms/request
{platform: "instagram"} S->>S: API-Key validieren S->>DB: Rate-Limit prüfen S->>DB: PhoneRotation: Freie Nummer wählen S->>DB: Verification Request anlegen S->>DB: Cooldown setzen S-->>C: 201 {request_id, phone_number: "+49..."} Note over C: Client registriert Account
mit der Telefonnummer C->>P: Account-Registrierung mit Telefon P->>R: SMS senden an +49... Note over R: Router empfängt SMS R->>S: POST /webhook/rutx11/sms
{sender, message, sim_slot} S->>S: Router-Token validieren S->>DB: SMS speichern S->>DB: Offenen Request finden (phone_number match) S->>S: Code extrahieren (Regex) S->>DB: Request mit Code aktualisieren S-->>R: 200 OK loop Polling (parallel) C->>S: GET /verification/sms/code/{request_id} alt Code empfangen S->>DB: Request Status abrufen S-->>C: 200 {status: "received", code: "123456"} else Noch kein Code S-->>C: 200 {status: "pending", code: null} end end Note over C: Client gibt Code ein C->>P: Verifikationscode eingeben P-->>C: Account verifiziert ``` ### 5.3 Webhook-Processing ```mermaid sequenceDiagram participant R as RUTX11 Router participant S as Verification Server participant DB as PostgreSQL R->>S: POST /webhook/rutx11/sms Note over R,S: Header: X-Router-Token: ABC123 Note over R,S: Body: {sender, message, timestamp, sim_slot} S->>DB: SELECT * FROM routers
WHERE router_token = 'ABC123' alt Router nicht gefunden S-->>R: 401 Unauthorized else Router gefunden S->>DB: UPDATE routers SET
last_seen_at = NOW(),
is_online = true S->>DB: INSERT INTO sms_messages
(router_id, sender, message...) S->>DB: SELECT * FROM verification_requests
WHERE phone_number = ...
AND status = 'pending'
AND expires_at > NOW() alt Request gefunden S->>S: Code extrahieren S->>DB: UPDATE verification_requests
SET code = '123456',
status = 'received' S->>DB: UPDATE sms_messages
SET matched_request_id = ... S-->>R: 200 {status: "matched"} else Kein Request S-->>R: 200 {status: "stored"} end end ``` --- ## 6. Fehlerszenarien & Edge Cases ### 6.1 Error Responses | Szenario | HTTP Status | Response Body | Client-Aktion | |----------|-------------|---------------|---------------| | Ungültiger API-Key | 401 | `{"error": "unauthorized", "message": "Ungültiger API-Key"}` | Lizenz prüfen | | Rate-Limit erreicht | 429 | `{"error": "rate_limit", "message": "...", "retry_after": 1800}` | Warten und Retry | | Keine Email verfügbar | 503 | `{"error": "no_resource", "message": "Keine Email-Adresse verfügbar"}` | Später versuchen | | Keine Nummer verfügbar | 503 | `{"error": "no_resource", "message": "Keine Telefonnummer verfügbar"}` | Später versuchen | | Request nicht gefunden | 404 | `{"error": "not_found", "message": "Request nicht gefunden"}` | Neuen Request starten | | Code-Timeout | 408 | `{"error": "timeout", "message": "Kein Code innerhalb Timeout empfangen"}` | Retry oder manuell | | Router offline | 503 | `{"error": "router_offline", "message": "Router nicht erreichbar"}` | Support kontaktieren | | Ungültiger Router-Token | 401 | `{"error": "unauthorized", "message": "Ungültiger Router-Token"}` | Router-Config prüfen | ### 6.2 Timeout-Behandlung ```python # Email-Verifikation DEFAULT_EMAIL_TIMEOUT = 120 # Sekunden MAX_EMAIL_TIMEOUT = 300 # SMS-Verifikation DEFAULT_SMS_TIMEOUT = 180 # SMS kann länger dauern MAX_SMS_TIMEOUT = 300 # Request-Expiration REQUEST_EXPIRY_BUFFER = 60 # Request läuft 60s nach Timeout ab ``` ### 6.3 Retry-Logik (Client-seitig) ```python # Empfohlene Polling-Intervalle INITIAL_POLL_INTERVAL = 2 # Sekunden MAX_POLL_INTERVAL = 5 BACKOFF_FACTOR = 1.5 # Empfohlene Retry-Strategie bei Fehlern MAX_RETRIES = 3 RETRY_DELAYS = [5, 15, 30] # Sekunden ``` --- ## 7. Integration mit Desktop-Client ### 7.1 VerificationClient-Klasse (NEU) Diese Datei muss im Client erstellt werden: `utils/verification_client.py` ```python # utils/verification_client.py """ Client für die Kommunikation mit dem Verifikationsserver. Ersetzt direktes IMAP-Polling durch API-Calls. """ import requests import time import logging from typing import Optional, Dict, Any from dataclasses import dataclass from enum import Enum logger = logging.getLogger("verification_client") class VerificationStatus(Enum): PENDING = "pending" POLLING = "polling" RECEIVED = "received" EXPIRED = "expired" FAILED = "failed" @dataclass class VerificationResponse: """Response von einer Verifikationsanfrage.""" request_id: str email_address: Optional[str] = None phone_number: Optional[str] = None expires_at: Optional[str] = None @dataclass class CodeResponse: """Response vom Code-Polling.""" status: VerificationStatus code: Optional[str] = None received_at: Optional[str] = None class VerificationClientError(Exception): """Basis-Exception für VerificationClient.""" pass class RateLimitError(VerificationClientError): """Rate-Limit erreicht.""" def __init__(self, retry_after: int): self.retry_after = retry_after super().__init__(f"Rate-Limit erreicht. Retry nach {retry_after} Sekunden.") class NoResourceError(VerificationClientError): """Keine Ressource (Email/Telefon) verfügbar.""" pass class VerificationTimeoutError(VerificationClientError): """Timeout beim Warten auf Code.""" pass class VerificationClient: """ Client für den Verifikationsserver. Verwendung: client = VerificationClient( server_url="https://verify.example.com", api_key="your-api-key" ) # Email-Verifikation response = client.request_email("instagram") email = response.email_address # ... Account registrieren mit email ... code = client.poll_for_code(response.request_id, "email", timeout=120) # SMS-Verifikation response = client.request_sms("instagram") phone = response.phone_number # ... Account registrieren mit phone ... code = client.poll_for_code(response.request_id, "sms", timeout=180) """ DEFAULT_TIMEOUT = 120 # Sekunden POLL_INTERVAL = 2 # Sekunden MAX_POLL_INTERVAL = 5 def __init__(self, server_url: str, api_key: str): """ Initialisiert den VerificationClient. Args: server_url: Basis-URL des Verifikationsservers (ohne /api/v1) api_key: API-Key für Authentifizierung """ self.server_url = server_url.rstrip("/") self.api_key = api_key self.session = requests.Session() self.session.headers.update({ "X-API-Key": api_key, "Content-Type": "application/json", "User-Agent": "AccountForger/1.0" }) logger.info(f"VerificationClient initialisiert für {server_url}") def _make_request( self, method: str, endpoint: str, json: Dict = None, timeout: int = 30 ) -> Dict[str, Any]: """Führt HTTP-Request aus mit Error-Handling.""" url = f"{self.server_url}/api/v1{endpoint}" try: response = self.session.request( method=method, url=url, json=json, timeout=timeout ) # Error-Handling basierend auf Status-Code if response.status_code == 401: raise VerificationClientError("Ungültiger API-Key") if response.status_code == 429: retry_after = int(response.headers.get("Retry-After", 60)) raise RateLimitError(retry_after) if response.status_code == 503: data = response.json() raise NoResourceError(data.get("message", "Keine Ressource verfügbar")) if response.status_code == 408: raise VerificationTimeoutError("Server-seitiger Timeout") if response.status_code == 404: raise VerificationClientError("Request nicht gefunden") response.raise_for_status() return response.json() except requests.exceptions.Timeout: raise VerificationClientError("Verbindungs-Timeout") except requests.exceptions.ConnectionError: raise VerificationClientError("Verbindung zum Server fehlgeschlagen") def request_email(self, platform: str, preferred_domain: str = None) -> VerificationResponse: """ Fordert eine Email-Adresse für Verifikation an. Args: platform: Zielplattform (instagram, facebook, tiktok, x, etc.) preferred_domain: Bevorzugte Domain (optional) Returns: VerificationResponse mit request_id und email_address """ payload = {"platform": platform} if preferred_domain: payload["preferred_domain"] = preferred_domain data = self._make_request("POST", "/verification/email/request", json=payload) logger.info(f"Email angefordert für {platform}: {data.get('email_address')}") return VerificationResponse( request_id=data["request_id"], email_address=data["email_address"], expires_at=data.get("expires_at") ) def request_sms(self, platform: str) -> VerificationResponse: """ Fordert eine Telefonnummer für SMS-Verifikation an. Args: platform: Zielplattform Returns: VerificationResponse mit request_id und phone_number """ payload = {"platform": platform} data = self._make_request("POST", "/verification/sms/request", json=payload) logger.info(f"Telefonnummer angefordert für {platform}: {data.get('phone_number')}") return VerificationResponse( request_id=data["request_id"], phone_number=data["phone_number"], expires_at=data.get("expires_at") ) def get_code_status(self, request_id: str, verification_type: str) -> CodeResponse: """ Fragt Status eines Verifikationscodes ab (einmalig). Args: request_id: ID der Verifikationsanfrage verification_type: "email" oder "sms" Returns: CodeResponse mit Status und ggf. Code """ endpoint = f"/verification/{verification_type}/code/{request_id}" data = self._make_request("GET", endpoint) return CodeResponse( status=VerificationStatus(data["status"]), code=data.get("code"), received_at=data.get("received_at") ) def poll_for_code( self, request_id: str, verification_type: str, timeout: int = None, callback: callable = None ) -> Optional[str]: """ Pollt Server bis Code empfangen wurde. Args: request_id: ID der Verifikationsanfrage verification_type: "email" oder "sms" timeout: Maximale Wartezeit in Sekunden callback: Optional - wird bei jedem Poll aufgerufen mit (elapsed_seconds, status) Returns: Verifikationscode oder None bei Timeout """ timeout = timeout or self.DEFAULT_TIMEOUT start_time = time.time() poll_interval = self.POLL_INTERVAL logger.info(f"Starte Polling für {verification_type} Request {request_id}") while True: elapsed = time.time() - start_time if elapsed >= timeout: logger.warning(f"Timeout nach {elapsed:.0f}s für Request {request_id}") return None try: response = self.get_code_status(request_id, verification_type) if callback: callback(elapsed, response.status) if response.status == VerificationStatus.RECEIVED and response.code: logger.info(f"Code empfangen: {response.code} nach {elapsed:.0f}s") return response.code if response.status in (VerificationStatus.EXPIRED, VerificationStatus.FAILED): logger.warning(f"Request fehlgeschlagen: {response.status}") return None except VerificationClientError as e: logger.warning(f"Polling-Fehler: {e}") # Weitermachen trotz Fehler # Warten vor nächstem Poll (mit Backoff) time.sleep(poll_interval) poll_interval = min(poll_interval * 1.2, self.MAX_POLL_INTERVAL) return None def get_available_phones(self) -> Dict[str, Any]: """ Fragt verfügbare Telefonnummern ab. Returns: dict mit available_count und phones Liste """ return self._make_request("GET", "/phone/available") def health_check(self) -> bool: """ Prüft Erreichbarkeit des Servers. Returns: True wenn Server erreichbar und gesund """ try: # Health-Endpoint braucht keinen API-Key response = requests.get( f"{self.server_url}/api/v1/health", timeout=5 ) data = response.json() return data.get("status") in ("healthy", "degraded") except Exception: return False # Singleton-Pattern für globale Instanz _verification_client: Optional[VerificationClient] = None def get_verification_client() -> Optional[VerificationClient]: """Gibt die globale VerificationClient-Instanz zurück.""" return _verification_client def init_verification_client(server_url: str, api_key: str) -> VerificationClient: """ Initialisiert die globale VerificationClient-Instanz. Sollte beim App-Start aufgerufen werden. """ global _verification_client _verification_client = VerificationClient(server_url, api_key) return _verification_client ``` ### 7.2 Server-Konfigurationsdatei (NEU) Erstelle `config/server_config.json`: ```json { "verification_server": { "url": "https://verify.example.com", "api_key": "", "timeout_email": 120, "timeout_sms": 180, "enabled": true } } ``` ### 7.3 Zu ändernde Dateien im Client | Datei | Änderung | |-------|----------| | `social_networks/base_automation.py` | `verification_client` Parameter hinzufügen | | `social_networks/instagram/instagram_verification.py` | API-Calls statt direktem EmailHandler | | `social_networks/facebook/facebook_verification.py` | API-Calls statt direktem EmailHandler | | `social_networks/tiktok/tiktok_verification.py` | SMS via API | | `controllers/platform_controllers/base_worker_thread.py` | VerificationClient initialisieren | | `utils/email_handler.py` | Deprecation-Warning, später entfernen | | `config/server_config.json` | NEU: Server-Konfiguration | | `utils/verification_client.py` | NEU: Server-Client | ### 7.4 Beispiel-Integration für Instagram ```python # social_networks/instagram/instagram_verification.py (GEÄNDERT) class InstagramVerification: def __init__(self, automation): self.automation = automation # NEU: VerificationClient statt EmailHandler self.verification_client = automation.verification_client def wait_for_email_code(self, email: str, timeout: int = 120) -> Optional[str]: """Wartet auf Email-Verifikationscode via Server.""" if not self.verification_client: # Fallback auf alten EmailHandler (Übergangsphase) return self._legacy_wait_for_code(email, timeout) try: # Request wurde bereits beim Account-Erstellen gemacht # Hier nur noch Polling request_id = self.automation.current_verification_request_id if not request_id: logger.error("Keine Verification-Request-ID vorhanden") return None code = self.verification_client.poll_for_code( request_id=request_id, verification_type="email", timeout=timeout, callback=lambda elapsed, status: logger.debug(f"Polling: {elapsed}s, Status: {status}") ) return code except Exception as e: logger.error(f"Fehler beim Warten auf Email-Code: {e}") return None ``` --- ## 8. Testbeispiele ### 8.1 Curl-Befehle ```bash # Health-Check curl -X GET https://verify.example.com/api/v1/health # Email anfordern curl -X POST https://verify.example.com/api/v1/verification/email/request \ -H "X-API-Key: your-api-key" \ -H "Content-Type: application/json" \ -d '{"platform": "instagram"}' # Email-Code abfragen curl -X GET https://verify.example.com/api/v1/verification/email/code/550e8400-e29b-41d4-a716-446655440000 \ -H "X-API-Key: your-api-key" # SMS anfordern curl -X POST https://verify.example.com/api/v1/verification/sms/request \ -H "X-API-Key: your-api-key" \ -H "Content-Type: application/json" \ -d '{"platform": "instagram"}' # SMS-Code abfragen curl -X GET https://verify.example.com/api/v1/verification/sms/code/550e8400-e29b-41d4-a716-446655440000 \ -H "X-API-Key: your-api-key" # Verfügbare Telefonnummern curl -X GET https://verify.example.com/api/v1/phone/available \ -H "X-API-Key: your-api-key" # Webhook simulieren (Router) curl -X POST https://verify.example.com/api/v1/webhook/rutx11/sms \ -H "X-Router-Token: router-token-abc" \ -H "Content-Type: application/json" \ -d '{ "sender": "Instagram", "message": "123456 ist dein Instagram-Bestätigungscode", "timestamp": "2026-01-17T10:30:00Z", "sim_slot": 1 }' ``` ### 8.2 Python-Integrations-Test ```python # tests/test_verification_integration.py import pytest from utils.verification_client import VerificationClient, RateLimitError @pytest.fixture def client(): return VerificationClient( server_url="https://verify.example.com", api_key="test-api-key" ) def test_health_check(client): """Server sollte erreichbar sein.""" assert client.health_check() is True def test_email_request(client): """Email-Request sollte funktionieren.""" response = client.request_email("instagram") assert response.request_id is not None assert response.email_address is not None assert "@" in response.email_address def test_sms_request(client): """SMS-Request sollte funktionieren.""" response = client.request_sms("instagram") assert response.request_id is not None assert response.phone_number is not None assert response.phone_number.startswith("+") def test_rate_limit(client): """Rate-Limit sollte greifen.""" # Viele Requests in kurzer Zeit for _ in range(100): try: client.request_email("instagram") except RateLimitError as e: assert e.retry_after > 0 return pytest.fail("Rate-Limit wurde nicht erreicht") def test_full_email_flow(client, mock_imap_server): """Vollständiger Email-Verifikationsflow.""" # 1. Email anfordern response = client.request_email("instagram") # 2. Simuliere eingehende Email mock_imap_server.send_verification_email( to=response.email_address, code="123456", platform="instagram" ) # 3. Code abrufen code = client.poll_for_code( request_id=response.request_id, verification_type="email", timeout=10 ) assert code == "123456" ``` --- ## 9. Deployment ### 9.1 Docker Compose ```yaml # docker-compose.yml version: '3.8' services: api: build: context: . dockerfile: Dockerfile ports: - "8000:8000" environment: - DATABASE_URL=postgresql://verify:${DB_PASSWORD}@db:5432/verification - REDIS_URL=redis://redis:6379 - SECRET_KEY=${SECRET_KEY} - ENCRYPTION_KEY=${ENCRYPTION_KEY} - LOG_LEVEL=INFO depends_on: db: condition: service_healthy redis: condition: service_started restart: unless-stopped healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8000/api/v1/health"] interval: 30s timeout: 10s retries: 3 db: image: postgres:15-alpine environment: - POSTGRES_DB=verification - POSTGRES_USER=verify - POSTGRES_PASSWORD=${DB_PASSWORD} volumes: - pgdata:/var/lib/postgresql/data - ./init.sql:/docker-entrypoint-initdb.d/init.sql healthcheck: test: ["CMD-SHELL", "pg_isready -U verify -d verification"] interval: 10s timeout: 5s retries: 5 restart: unless-stopped redis: image: redis:7-alpine volumes: - redisdata:/data restart: unless-stopped celery-worker: build: . command: celery -A app.worker worker -l info -Q email_polling environment: - DATABASE_URL=postgresql://verify:${DB_PASSWORD}@db:5432/verification - REDIS_URL=redis://redis:6379 - ENCRYPTION_KEY=${ENCRYPTION_KEY} depends_on: - db - redis restart: unless-stopped celery-beat: build: . command: celery -A app.worker beat -l info environment: - DATABASE_URL=postgresql://verify:${DB_PASSWORD}@db:5432/verification - REDIS_URL=redis://redis:6379 depends_on: - db - redis restart: unless-stopped volumes: pgdata: redisdata: ``` ### 9.2 Dockerfile ```dockerfile # Dockerfile FROM python:3.11-slim WORKDIR /app # System-Dependencies RUN apt-get update && apt-get install -y --no-install-recommends \ build-essential \ curl \ && rm -rf /var/lib/apt/lists/* # Python-Dependencies COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # App-Code COPY app/ ./app/ # Non-root User RUN useradd -m appuser && chown -R appuser:appuser /app USER appuser # Health-Check HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ CMD curl -f http://localhost:8000/api/v1/health || exit 1 # Start CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] ``` ### 9.3 Umgebungsvariablen (.env.example) ```bash # .env.example # Datenbank DB_PASSWORD=your-secure-password-here # Sicherheit SECRET_KEY=your-secret-key-for-jwt-etc ENCRYPTION_KEY=your-32-byte-encryption-key # Optional: Externe Services SENTRY_DSN=https://... # Logging LOG_LEVEL=INFO # IMAP (falls zentral konfiguriert) # IMAP_DEFAULT_SERVER=imap.example.com # IMAP_DEFAULT_PORT=993 ``` ### 9.4 Sicherheitsanforderungen | Anforderung | Implementierung | |-------------|-----------------| | HTTPS-Only | Nginx/Traefik Reverse Proxy mit Let's Encrypt | | API-Key-Hashing | bcrypt mit Cost Factor 12 | | IMAP-Passwörter | AES-256-GCM verschlüsselt in DB | | Rate-Limiting | Per-Client basierend auf Tier | | IP-Whitelist (Webhooks) | Optional: Nur Router-IPs für /webhook/* | | Audit-Logging | Alle sensitiven Operationen loggen | | Request-Expiration | Automatische Bereinigung alter Requests | --- ## 10. Implementierungsreihenfolge ### Phase 1: Basis-Setup (MVP) 1. FastAPI-Projekt aufsetzen 2. PostgreSQL-Schema deployen 3. Health-Check Endpoint 4. Basis-Auth (API-Key-Validierung) ### Phase 2: Email-Service 1. IMAP-Connection-Pool 2. Email-Polling-Service 3. Code-Extraktion (Regex) 4. `/verification/email/*` Endpoints 5. Background-Task für Polling (Celery) ### Phase 3: SMS-Webhook 1. `/webhook/rutx11/sms` Endpoint 2. Router-Token-Validierung 3. SMS-Speicherung 4. Request-Matching-Logik 5. `/verification/sms/*` Endpoints ### Phase 4: Phone-Rotation 1. PhoneRotationService 2. Cooldown-Management 3. Platform-Aware Auswahl 4. `/phone/available` Endpoint ### Phase 5: Rate-Limiting & Monitoring 1. Rate-Limit-Tracking 2. Tier-basierte Limits 3. Prometheus-Metriken 4. Logging-Aggregation 5. Health-Check erweitern --- ## Appendix A: Projektstruktur (Backend) ``` accountforger-server/ ├── app/ │ ├── __init__.py │ ├── main.py # FastAPI Entry Point │ ├── config.py # Settings/Config │ ├── dependencies.py # FastAPI Dependencies │ │ │ ├── api/ │ │ ├── __init__.py │ │ └── v1/ │ │ ├── __init__.py │ │ ├── router.py # API Router │ │ ├── verification.py # Verifikations-Endpoints │ │ ├── webhooks.py # RUTX11 Webhook │ │ ├── phone.py # Phone-Availability │ │ └── health.py # Health-Check │ │ │ ├── services/ │ │ ├── __init__.py │ │ ├── email_service.py # IMAP-Polling │ │ ├── sms_service.py # Webhook-Verarbeitung │ │ ├── phone_rotation.py # Nummern-Auswahl │ │ ├── auth_service.py # API-Key-Validierung │ │ └── encryption.py # AES für IMAP-Passwörter │ │ │ ├── models/ │ │ ├── __init__.py │ │ ├── client.py # Client Model │ │ ├── router.py # Router Model │ │ ├── phone_number.py # PhoneNumber Model │ │ ├── verification.py # VerificationRequest Model │ │ └── sms_message.py # SMSMessage Model │ │ │ ├── schemas/ │ │ ├── __init__.py │ │ ├── verification.py # Pydantic Schemas │ │ └── webhook.py # Webhook Schemas │ │ │ ├── db/ │ │ ├── __init__.py │ │ ├── database.py # SQLAlchemy Setup │ │ └── migrations/ # Alembic Migrations │ │ │ └── worker/ │ ├── __init__.py │ ├── celery.py # Celery App │ └── tasks.py # Background Tasks │ ├── tests/ │ ├── __init__.py │ ├── conftest.py │ ├── test_email_service.py │ ├── test_sms_service.py │ └── test_api.py │ ├── docker-compose.yml ├── Dockerfile ├── requirements.txt ├── alembic.ini ├── .env.example └── README.md ``` --- ## Appendix B: requirements.txt ``` # Web Framework fastapi==0.109.0 uvicorn[standard]==0.27.0 python-multipart==0.0.6 # Database sqlalchemy==2.0.25 asyncpg==0.29.0 alembic==1.13.1 # Redis & Celery redis==5.0.1 celery==5.3.6 # IMAP aioimaplib==1.0.1 # Security bcrypt==4.1.2 cryptography==41.0.7 python-jose[cryptography]==3.3.0 # HTTP Client (für Health-Checks) httpx==0.26.0 # Validation pydantic==2.5.3 pydantic-settings==2.1.0 email-validator==2.1.0 # Utilities python-dateutil==2.8.2 # Testing pytest==7.4.4 pytest-asyncio==0.23.3 # Monitoring (optional) prometheus-client==0.19.0 sentry-sdk[fastapi]==1.39.1 ``` --- *Dokument erstellt für Backend-Implementierung. Version 1.0*