cleanup
This commit is contained in:
345
bitbylaw/services/ADVOWARE_SERVICE.md
Normal file
345
bitbylaw/services/ADVOWARE_SERVICE.md
Normal file
@@ -0,0 +1,345 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user