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>
These fields from v_event_session_w_file_count were lost during the v1/v2
-> v3 migration. Added to Event_Session_Base model and to searchable_fields
in the event_session object definition.
Fields are only available via the alt view (v_event_session_w_file_count).
To search: use ?view=alt on the nested search endpoint.
To retrieve: use ?inc_file_count=true on the GET endpoint.
Also:
- Updated ARCH__V3_DEVELOPMENT_STANDARDS.md: expanded Field Evolution
Checklist with alt-view field rules, Docker restart requirement, and
documented the ?view= parameter as a live (not proposed) feature.
- Updated TODO__Agents.md: marked migration gap audit as complete.
- Added regression test to test_e2e_v3_search_engine.py.
Add account_name, account_code, account_enable, account_enable_from/to,
site_enable_from/to, site_domain_access_key, logo_path, style_href,
script_src, and google_tracking_id to Site_Domain_Base.
These fields were present in Site_Domain_FQDN_ID_Base but were lost
during the v1/v2 -> v3 migration. The v_site_domain view already
provides them via JOINs, so no DB changes are required.
Google Sheets embeds \xa0 (non-breaking space) in 12-hour time values
(e.g. "3:00\xa0PM") and when date/time columns are combined. This caused
MariaDB datetime INSERTs to fail with an OperationalError.
Adds _clean_datetime() which strips \xa0, normalizes whitespace, and
parses common import formats (M/D/YYYY H:MM AM/PM, etc.) into
YYYY-MM-DD HH:MM:SS before the DB write. Applied to all four datetime
fields: session and presentation start/end.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Make root_url a required query param on GET /v3/action/user/{id}/email_auth_key_url
(previously Optional[str]=None, which produced a malformed link in the emailed URL)
- Update GUIDE__AE_API_V3_for_Frontend.md: document root_url as required, add magic link
URL format, note valid_email=True side effect, add 404 error, expand 403 conditions
for authenticate, add 400 for verify_password when no password is set
- Add test_e2e_v3_user_action_routes.py and test_e2e_v3_user_auth_routes.py to tests/README.md
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All three were missing the transient-connection retry that sql_update and
run_sql_select already had. On OperationalError (stale/dropped connection),
each now retries once with a fresh engine.connect() without disposing the pool.
IntegrityError (duplicate key, FK violation, NOT NULL) continues to return
None without retrying — the same data would fail again and None signals a
data conflict to callers, distinct from False (error) or an int (success).
sql_insert_or_update retry is safe because ON DUPLICATE KEY UPDATE is idempotent.
sql_insert retry is safe because OperationalError means MariaDB rolled back.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wraps the deprecated global `db = engine.connect()` in a try/except so
a Docker startup race (MariaDB not yet ready) no longer crashes the
Gunicorn worker before it can serve any requests.
Sets db=None on failure; reconnect_db() on the lifespan bootstrap path
re-establishes it once credentials are confirmed.
TODO (P3 full): migrate lib_schema_v3.py:39 and lib_api_crud_v3.py:166
off the global db to engine.connect() context managers, then remove it.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Added AE_DB_POOL_SIZE and AE_DB_POOL_MAX_OVERFLOW to config.py with
defaults matching prior hardcoded values (10/20). Wired into settings.DB
property so create_ae_engine() reads them without fallback.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
On OperationalError, sql_update and run_sql_select were calling
sql_connect() → reconnect_db() which disposes the entire connection
pool mid-flight, killing other in-flight connections under concurrency.
Removed the sql_connect() calls; the existing retry blocks already open
a fresh engine.connect() context manager, and pool_pre_ping=True handles
stale connection detection. Also drops the now-unused sql_connect import.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
#3 (zombie import) is genuinely a 2-line fix — remove the import from api.py:10 and move db_connection.py to trash. Zero functional change since db is only in a commented-out line.
Child objects in the nested endpoint inherit account context from their
parent via the FK relationship and do not carry their own account_id
column (e.g. event_badge, journal_entry). Injecting account_id into
data_to_insert would cause INSERT failures for any child whose table
has no account_id column but whose model has the field (from the view).
The original code set account_id in obj_data before model instantiation,
where the root_validator immediately stripped it — a harmless no-op.
The previous commit turned that dead line into a live injection by moving
it after serialization, which would break journal_entry creates on
non-bypass auth. Removed entirely.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
After the sanitize_payload order fix, account_id was being re-injected
into data_to_insert for models that explicitly list account_id in
fields_to_exclude_from_db (e.g. event_badge, event_device). Those tables
have no account_id column, causing INSERT failures.
Guard the post-sanitize account_id injection in both api_crud_v3.py and
api_crud_v3_nested.py by checking fields_to_exclude_from_db first.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
In the nested POST handler (api_crud_v3_nested.py), sanitize_payload was
running before model instantiation. For secondary FK fields like
event_badge_template_id, sanitize_payload resolved the random string →
integer, then the model's root_validator stripped the integer back to None
(Vision ID anti-leakage guard). Only the parent FK survived because it was
explicitly re-injected after serialization.
Fix: moved sanitize_payload to run on data_to_insert after serialization,
matching the flat V3 POST pattern (api_crud_v3.py). Also moved account_id
injection to after sanitize_payload, fixing a latent bug where account_id
was silently written as NULL on non-bypass auth.
Adds regression test to test_e2e_v3_demo_parity.py that creates an
event_badge via nested POST with event_badge_template_id and verifies the
field is non-None in the response.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All create_update_*_v4 functions for event_badge, event_person,
event_person_profile, event_presenter, and event_presentation were
calling sql_update without record_id. Vision ID models use Optional[str]
IDs with a root_validator that strips integer values, so the serialized
dict contained no id key and sql_update could not identify the row.
Fix: pass record_id=<integer_id> explicitly to sql_update in all five
functions. Also fix walrus-operator false-negative: None return from
sql_update (0 rows affected — record unchanged) is not an error and
should not abort sub-object cascade; use explicit `is False` check.
Also broadens Axonius badge_type_code mapping to substring match so
future ticket name variants still resolve correctly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add tests/e2e/test_e2e_jitsi_token.py: verifies moderator/attendee claims,
room isolation, input validation, and exp claim correctness
- Update Jitsi section comment in api.py with actionable secret rotation TODO
(must update JWT_APP_SECRET here AND in dgr_zone_jitsi .env, then restart
prosody + jicofo)
Removed the 403 guard that blocked non-moderators. is_moderator is
already passed through to the token payload, so participants get
"moderator": false as expected.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Uncommented and completed access_key + referrer handling in
lookup_site_domain_fqdn() and the GET /site/domain/fqdn/{fqdn} route.
These routes are disabled in registry.py and not currently active.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New router: /v3/action/user/ (api_v3_actions_user.py)
- POST /authenticate — credentials in body (not query params; security fix)
- POST /verify_password
- POST /{user_id}/change_password — optional current-password verification
- GET /{user_id}/new_auth_key
- GET /{user_id}/email_auth_key_url
Registered in registry.py under /v3/action/user with V3 AccountContext auth.
Bug fixes (from audit in previous session):
- user.py: fix broken @router.get decorator (authenticate was unreachable)
- user.py + user_methods.py: fix AttributeError id_random → id (Vision ID)
- user_models.py: add fields_to_exclude_from_db to User_New_Base; narrow
collision prevention to self-reference IDs only
- user_models.py: pre-inject hashed password in root_validator(pre=True) so
exclude_unset=True in CRUD POST handler includes it (was writing NULL)
- api_crud_v3.py: move sanitize_payload + account_id injection to after
model validation (fixes FK integer collision with Vision ID constraints)
Docs: GUIDE__AE_API_V3_for_Frontend.md — new Section 7 with full migration
table (legacy → V3), request/response docs for all 5 action endpoints,
and V3 CRUD search equivalents for the 3 lookup routes.
Tests: tests/e2e/test_e2e_v3_user_action_routes.py — 19 tests, 19/19 pass.
Legacy tests/e2e/test_e2e_v3_user_auth_routes.py — 22/22 still pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Extracted apply_vision_id_fix() helper to lib_api_crud_v3.py — single
source of truth for the fix that ensures {obj_type}_id in responses is
always the random string, never the DB integer.
- Applied to all response-returning paths in api_crud_v3.py:
GET single, GET list, POST search, POST create, PATCH update.
- Applied to all response-returning paths in api_crud_v3_nested.py:
GET child list, POST search, POST create, GET single child, PATCH child.
- Removed duplicate get_child_obj and patch_child_obj route handlers in
api_crud_v3_nested.py — FastAPI silently routes to only the first
matching handler, so the second definitions were unreachable dead code.
Covers all 23 V3 CRUD models still using the old integer-alias pattern.
The archive_content model was already migrated to Vision IDs; this fix
ensures every other model gets correct responses without individual migration.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace integer `id` (alias archive_content_id) with Vision string fields:
`id: Optional[str]` and `archive_content_id: Optional[str]` — both always
hold the random string ID, never the DB integer.
- Add `root_validator(pre=True)` (map_v3_ids) that maps id_random /
archive_content_id_random → id and archive_content_id, with collision
prevention to reject any integer that arrives in these fields.
- Remove old `archive_content_id_lookup` integer validator (superseded by
sanitize_payload + root_validator).
- Keep `id_random` (alias archive_content_id_random) in responses for
backward compatibility; add id, archive_content_id, id_random to
fields_to_exclude_from_db so they never appear in INSERT/UPDATE payloads.
Generic CRUD layer safety net (post_obj + post_child_obj):
- After building resp_data on create, swap any integer {obj_type}_id with
the corresponding {obj_type}_id_random value — catches models not yet
migrated to Vision IDs.
- Fix return_obj=False fallback to return obj_id as the random string.
Docs: add Section 3D to GUIDE__AE_API_V3_for_Frontend.md documenting the
Vision ID convention — {obj_type}_id is always the random string; the
_id_random suffix is a legacy artifact that frontend code should phase out.
Fixes: POST /v3/crud/archive/{id}/archive_content/ returning integer ID,
breaking the subsequent PATCH flow (422 min_length validation failure).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Ported the legacy '/hosted_file/hash/{hash}' endpoint to the V3 actions router.
The new endpoint '/v3/action/hosted_file/hash/{hosted_file_hash}' supports:
- ID Vision: returns random string IDs instead of internal integers
- Local Check: verifies physical file existence on disk (check_for_local=True)
- Deduplication: enables frontend to check for existing files before upload
Also added PROJECT document for AE Hosted Files migration tracking.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tags remaining live-but-deprecated routes so every call logs a warning,
giving visibility before the next round of removals.
- registry.py: add DeprecationParams to importing and user routers
- api.py: add DeprecationParams to /request_jwt and /temp_token individually
- user.py: inherits deprecation warning via registry router-level dependency
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Disabled legacy routes that are superseded by V3 equivalents. Code is
commented out (not deleted) pending final verification and cleanup pass.
- registry.py: remove sql, lookup (/lu), websockets, websockets_redis;
clean up dead imports (contact, event_person, etc.)
- data_store.py: comment out legacy CRUD and code-lookup endpoints;
keep V3 code-lookup routes active; add TODO for action path rename
- api.py: comment out Api_Base CRUD, get_id (internal ID leak),
and sql_test (debug) endpoints
- aether_cfg.py: comment out legacy Flask cfg endpoint
- user.py: comment out legacy user endpoints
- util_email.py: minor cleanup
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Reverts the PARTITION BY name change — group is the correct dedup key.
Partitioning by name broke country deduplication (two US records both
survived, causing Svelte each_key_duplicate on alpha_2_code='US').
Root cause is bad seed data in lu_v3_time_zone: group='United States'
for 13 US/* zones and group='Europe' for 63 Europe/* zones instead of
group=name. A separate DB UPDATE is required to fix those rows.
Tests updated to assert:
- No duplicate alpha_2_code in country list (PARTITION BY group regression)
- All 13 US/* and Europe/* spot-check zones present (pending DB data fix)
- priority-only timezone count == 72 (pending DB data fix)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>