feat(idaa): server-side Novi member verification endpoint
Proxies GET /customers/{uuid} to Novi AMS server-to-server so members'
browser IPs are no longer in the call path, eliminating false "Access
Denied" for users on hotel/conference WiFi, VPNs, and CDN-filtered nets.
- New router: GET /v3/action/idaa/novi_member/{uuid}
- Business logic in app/methods/idaa_novi_verify_methods.py
- Redis cache (4h TTL, key: idaa:novi_member:{uuid})
- 404 never cached (recently-joined member anti-pattern)
- Email space→+ normalization (Novi quirk)
- Display name: "FirstName L." format with Name field fallback
- Registered in registry.py under /v3/action/idaa tag
- 9 unit tests covering all response paths (200/404/429/503/unreachable,
cache hit, email normalization, display name format)
- Frontend guide (Section 12) and tests/README updated with full spec
and migration table for frontend hand-off
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -638,6 +638,61 @@ const url = URL.createObjectURL(blob);
|
||||
|
||||
---
|
||||
|
||||
## 12. IDAA: Server-Side Novi Member Verification
|
||||
|
||||
Verifies a Novi AMS member UUID by proxying the Novi API call through the Aether backend. This eliminates false "Access Denied" failures for members on hotel/conference WiFi, VPNs, and Cloudflare-filtered networks — the Novi call originates from the server's IP, not the member's browser IP.
|
||||
|
||||
- **Method:** `GET`
|
||||
- **Path:** `/v3/action/idaa/novi_member/{uuid}`
|
||||
- **Auth:** Standard V3 (`x-aether-api-key` + `x-account-id` or `?jwt=`)
|
||||
|
||||
### Request
|
||||
|
||||
| Parameter | Location | Required | Description |
|
||||
|---|---|---|---|
|
||||
| `uuid` | Path | Yes | Novi member UUID (from Novi AMS) |
|
||||
|
||||
### Response on success (`200 OK`)
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"verified": true,
|
||||
"full_name": "Alice S.",
|
||||
"email": "alice+member@idaa.org"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `full_name`: `"{FirstName} {LastName[0]}."` format. Falls back to the Novi `Name` field if first/last are absent.
|
||||
- `email`: Novi `Email` field with space → `+` normalization applied (Novi quirk — `alice member@idaa.org` → `alice+member@idaa.org`).
|
||||
|
||||
### Error responses
|
||||
|
||||
| Status | Meaning | Frontend action |
|
||||
|---|---|---|
|
||||
| `404` | UUID not found in Novi, or Novi returned 200 with no identity data (empty-member anti-pattern — member may have just joined) | Treat as denied / not a member |
|
||||
| `429` | Novi rate limit hit | Surface as `'rate_limited'`; advise retry |
|
||||
| `503` | Novi unreachable or Novi 5xx error | Surface as `'api_error'`; advise retry |
|
||||
|
||||
### Migration from direct Novi call
|
||||
|
||||
The frontend's `+layout.svelte:verify_novi_uuid()` currently calls Novi directly from the browser. Replace that `fetch()` with this endpoint. Response code mapping:
|
||||
|
||||
| Direct Novi result | This endpoint returns | Frontend state |
|
||||
|---|---|---|
|
||||
| `200` with identity data | `200` | `verified` |
|
||||
| `200` with no identity data | `404` | `denied` |
|
||||
| `404` | `404` | `denied` |
|
||||
| `429` | `429` | `'rate_limited'` |
|
||||
| Network error / Novi 5xx | `503` | `'api_error'` |
|
||||
|
||||
### Caching
|
||||
|
||||
Verified results are cached in Redis (`idaa:novi_member:{uuid}`, 4-hour TTL). `404` results are **never** cached so recently-joined members are not incorrectly denied on their next attempt.
|
||||
|
||||
---
|
||||
|
||||
## 11. Troubleshooting 403 Forbidden
|
||||
|
||||
If you receive a 403 on a valid ID:
|
||||
|
||||
Reference in New Issue
Block a user