# AdvowareAPI Service ## Übersicht Der AdvowareAPI Service ist der zentrale HTTP-Client für alle Kommunikation mit der Advoware REST-API. Er abstrahiert die komplexe HMAC-512 Authentifizierung und bietet ein einfaches Interface für API-Calls. ## Location `services/advoware.py` ## Verwendung ```python from services.advoware import AdvowareAPI # In Step-Handler async def handler(req, context): advoware = AdvowareAPI(context) result = await advoware.api_call('/employees', method='GET') return {'status': 200, 'body': {'result': result}} ``` ## Klassen ### AdvowareAPI **Constructor**: `__init__(self, context=None)` - `context`: Motia context für Logging (optional) **Attributes**: - `API_BASE_URL`: Base URL der Advoware API - `redis_client`: Redis-Connection für Token-Caching - `product_id`, `app_id`, `api_key`: Auth-Credentials aus Config ## Methoden ### get_access_token(force_refresh=False) Holt Bearer Token aus Redis Cache oder fetcht neuen Token. **Parameters**: - `force_refresh` (bool): Cache ignorieren und neuen Token holen **Returns**: `str` - Bearer Token **Logic**: 1. Wenn kein Redis oder `force_refresh=True`: Fetch new 2. Wenn cached Token existiert und nicht abgelaufen: Return cached 3. Sonst: Fetch new und cache **Caching**: - Key: `advoware_access_token` - TTL: 53 Minuten (55min Lifetime - 2min Safety) - Timestamp-Key: `advoware_token_timestamp` **Example**: ```python api = AdvowareAPI() token = api.get_access_token() # From cache token = api.get_access_token(force_refresh=True) # Fresh ``` ### api_call(endpoint, method='GET', headers=None, params=None, json_data=None, ...) Führt authentifizierten API-Call zu Advoware aus. **Parameters**: - `endpoint` (str): API-Pfad (z.B. `/employees`) - `method` (str): HTTP-Method (GET, POST, PUT, DELETE) - `headers` (dict): Zusätzliche HTTP-Headers - `params` (dict): Query-Parameters - `json_data` (dict): JSON-Body für POST/PUT - `timeout_seconds` (int): Override default timeout **Returns**: `dict|None` - JSON-Response oder None **Logic**: 1. Get Bearer Token (cached oder fresh) 2. Setze Authorization Header 3. Async HTTP-Request mit aiohttp 4. Bei 401: Refresh Token und retry 5. Parse JSON Response 6. Return Result **Error Handling**: - `aiohttp.ClientError`: Network/HTTP errors - `401 Unauthorized`: Auto-refresh Token und retry (einmal) - `Timeout`: Nach `ADVOWARE_API_TIMEOUT_SECONDS` **Example**: ```python # GET Request employees = await api.api_call('/employees', method='GET', params={'limit': 10}) # POST Request new_appt = await api.api_call( '/appointments', method='POST', json_data={'datum': '2026-02-10', 'text': 'Meeting'} ) # PUT Request updated = await api.api_call( '/appointments/123', method='PUT', json_data={'text': 'Updated'} ) # DELETE Request await api.api_call('/appointments/123', method='DELETE') ``` ## Authentifizierung ### HMAC-512 Signature Advoware verwendet HMAC-512 für Request-Signierung: **Message Format**: ``` {product_id}:{app_id}:{nonce}:{timestamp} ``` **Key**: Base64-decoded API Key **Hash**: SHA512 **Output**: Base64-encoded Signature **Implementation**: ```python def _generate_hmac(self, request_time_stamp, nonce=None): if not nonce: nonce = str(uuid.uuid4()) message = f"{self.product_id}:{self.app_id}:{nonce}:{request_time_stamp}" api_key_bytes = base64.b64decode(self.api_key) signature = hmac.new(api_key_bytes, message.encode(), hashlib.sha512) return base64.b64encode(signature.digest()).decode('utf-8') ``` ### Token-Fetch Flow 1. Generate nonce (UUID4) 2. Get current UTC timestamp (ISO format) 3. Generate HMAC signature 4. POST to `https://security.advo-net.net/api/v1/Token`: ```json { "AppID": "...", "Kanzlei": "...", "Database": "...", "User": "...", "Role": 2, "Product": 64, "Password": "...", "Nonce": "...", "HMAC512Signature": "...", "RequestTimeStamp": "..." } ``` 5. Extract `access_token` from response 6. Cache in Redis (53min TTL) ## Redis Usage ### Keys **DB 1** (`REDIS_DB_ADVOWARE_CACHE`): - `advoware_access_token` (string, TTL: 3180s = 53min) - `advoware_token_timestamp` (string, TTL: 3180s) ### Operations ```python # Set Token self.redis_client.set( self.TOKEN_CACHE_KEY, access_token, ex=(self.token_lifetime_minutes - 2) * 60 ) # Get Token cached_token = self.redis_client.get(self.TOKEN_CACHE_KEY) if cached_token: return cached_token.decode('utf-8') ``` ### Fallback Wenn Redis nicht erreichbar: - Logge Warning - Fetche Token bei jedem Request (keine Caching) - Funktioniert, aber langsamer ## Logging ### Log Messages ```python # Via context.logger (wenn vorhanden) context.logger.info("Access token fetched successfully") context.logger.error(f"API call failed: {e}") # Fallback zu Python logging logger.info("Connected to Redis for token caching") logger.debug(f"Token request data: AppID={self.app_id}") ``` ### Log Levels - **DEBUG**: Token Details, Request-Parameter - **INFO**: Token-Fetch, API-Calls, Cache-Hits - **ERROR**: Auth-Fehler, API-Fehler, Network-Fehler ## Configuration ### Environment Variables ```bash # API Settings ADVOWARE_API_BASE_URL=https://www2.advo-net.net:90/ ADVOWARE_PRODUCT_ID=64 ADVOWARE_APP_ID=your_app_id ADVOWARE_API_KEY=base64_encoded_hmac_key ADVOWARE_KANZLEI=your_kanzlei ADVOWARE_DATABASE=your_database ADVOWARE_USER=api_user ADVOWARE_ROLE=2 ADVOWARE_PASSWORD=your_password # Timeouts ADVOWARE_TOKEN_LIFETIME_MINUTES=55 ADVOWARE_API_TIMEOUT_SECONDS=30 # Redis REDIS_HOST=localhost REDIS_PORT=6379 REDIS_DB_ADVOWARE_CACHE=1 REDIS_TIMEOUT_SECONDS=5 ``` ## Error Handling ### Exceptions **AdvowareTokenError**: - Raised when token fetch fails - Beispiel: Invalid credentials, HMAC signature mismatch **aiohttp.ClientError**: - Network errors, HTTP errors (außer 401) - Timeouts, Connection refused, etc. ### Retry Logic **401 Unauthorized**: - Automatic retry mit fresh token (einmal) - Danach: Exception an Caller **Other Errors**: - Keine Retry (fail-fast) - Exception direkt an Caller ## Performance ### Response Time - **With cached token**: 200-800ms (Advoware API Latency) - **With token fetch**: +1-2s für Token-Request - **Timeout**: 30s (konfigurierbar) ### Caching - **Hit Rate**: >99% (Token cached 53min, API calls häufiger) - **Miss**: Nur bei erstem Call oder Token-Expiry ## Testing ### Manual Testing ```python # Test Token Fetch from services.advoware import AdvowareAPI api = AdvowareAPI() token = api.get_access_token(force_refresh=True) print(f"Token: {token[:20]}...") # Test API Call import asyncio async def test(): api = AdvowareAPI() result = await api.api_call('/employees', params={'limit': 5}) print(result) asyncio.run(test()) ``` ### Unit Testing ```python from unittest.mock import AsyncMock, MagicMock, patch import pytest @pytest.mark.asyncio async def test_api_call_with_cached_token(): # Mock Redis redis_mock = MagicMock() redis_mock.get.return_value = b'cached_token' # Mock aiohttp with patch('aiohttp.ClientSession') as session_mock: response_mock = AsyncMock() response_mock.status = 200 response_mock.json = AsyncMock(return_value={'data': 'test'}) session_mock.return_value.__aenter__.return_value.request.return_value.__aenter__.return_value = response_mock api = AdvowareAPI() api.redis_client = redis_mock result = await api.api_call('/test') assert result == {'data': 'test'} redis_mock.get.assert_called_once() ``` ## Security ### Secrets - ✅ API Key aus Environment (nicht hardcoded) - ✅ Password aus Environment - ✅ Token nur in Redis (localhost) - ❌ Token nicht in Logs ### Best Practices - API Key immer Base64-encoded speichern - Token nicht länger als 55min cachen - Redis localhost-only (keine remote connections) - Logs keine credentials enthalten ## Related Documentation - [Configuration](../../docs/CONFIGURATION.md) - [Architecture](../../docs/ARCHITECTURE.md) - [Proxy Steps](../advoware_proxy/README.md)