# 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*