2225 Zeilen
66 KiB
Markdown
2225 Zeilen
66 KiB
Markdown
# 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<br/>{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<br/>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<br/>{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<br/>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<br/>{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<br/>WHERE router_token = 'ABC123'
|
|
|
|
alt Router nicht gefunden
|
|
S-->>R: 401 Unauthorized
|
|
else Router gefunden
|
|
S->>DB: UPDATE routers SET<br/>last_seen_at = NOW(),<br/>is_online = true
|
|
|
|
S->>DB: INSERT INTO sms_messages<br/>(router_id, sender, message...)
|
|
|
|
S->>DB: SELECT * FROM verification_requests<br/>WHERE phone_number = ...<br/>AND status = 'pending'<br/>AND expires_at > NOW()
|
|
|
|
alt Request gefunden
|
|
S->>S: Code extrahieren
|
|
S->>DB: UPDATE verification_requests<br/>SET code = '123456',<br/>status = 'received'
|
|
S->>DB: UPDATE sms_messages<br/>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*
|