Files
OSIT-AE-API-FastAPI/documentation/GUIDE__AE_API_V3_for_Frontend.md
Scott Idem 687472f4e3 feat(user): V3 action endpoints + auth bug fixes (19/19 + 22/22 tests)
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>
2026-03-25 21:54:09 -04:00

22 KiB
Raw Blame History

Aether API V3 Frontend Integration Guide (Svelte/TypeScript)

This guide defines the standards for interacting with the Aether API V3 CRUD and Action endpoints.


1. Authentication and Security (Mandatory)

V3 architecture enforces strict Multi-Tenant Isolation and Machine Authorization. Requests require two levels of validation.

A. The "Entry Ticket" (API Key)

Mandatory for all requests. identifies the application or client.

  • Header: x-aether-api-key: <your_app_key>
  • Status Code: 403 Forbidden if missing or invalid.

B. The "Visa" (Account Context)

Required for any non-public data (Journals, Badges, Users, etc.).

  1. Standard Access: Provide the x-account-id (the random string ID).
    • Header: x-account-id: <account_id>
  2. Administrative Bypass: For authorized scripts needing global access.
    • Header: x-no-account-id: bypass
  3. Token Access: Provide a JWT in the query string.
    • Query Param: ?jwt=<token>

Caution

UNSUPPORTED HEADERS: The header x-aether-api-token is NOT recognized by the V3 API. If you send it, the backend will treat you as a guest and block access to private data.


2. Bootstrapping (The FQDN Handshake)

When the frontend first loads and doesn't know the account_id, it performs a "handshake" using its domain name.

Endpoint: POST /v3/crud/site_domain/search Body:

{
  "and": [
    { "field": "fqdn", "op": "eq", "value": "demo.oneskyit.com" }
  ]
}

Results:

  • Returns 200 + a list containing the account_id (random string ID) and site_id (random string ID).
  • ** デザイン Choice:** If the domain is not found, it returns 200 OK with an empty list []. It is NOT a 404.

3. Standard CRUD Patterns

A. GET by ID

Used when the ID is known.

  • Endpoint: GET /v3/crud/{obj_type}/{id}
  • Security: Returns 403 if the record doesn't belong to your x-account-id.

B. POST Search

The primary way to retrieve data.

  • Endpoint: POST /v3/crud/{obj_type}/search
  • Security: Automatically filters results to only show records belonging to your x-account-id. If no account context is provided, it will return 0 records for private objects.

C. POST Create / PATCH Update

Modify data in the system.

  • Endpoints:
    • POST /v3/crud/{obj_type}/
    • PATCH /v3/crud/{obj_type}/{id}
  • Strict Mode (Default): The API validates your payload against the Pydantic model. If you send fields that do not exist in the model, the database might return a 400 "Unknown column" error.
  • Permissive Mode (Header): To allow the frontend to send "extra" fields (like local UI state) without causing errors, use the following header:
    • Header: x-ae-ignore-extra-fields: true
    • Behavior: When set to true, the backend will automatically strip any fields from the payload that are not defined in the object's model before attempting to save to the database.

D. ID Fields in Responses (Vision ID Convention)

Important

V3 responses always use random string IDs — never database integers.

All V3 responses — POST create, GET single, GET list, search, and PATCH update — contain:

Field Type Use
{obj_type}_id string Primary public ID. Use this for subsequent PATCH calls and UI routing.
{obj_type}_id_random string Legacy alias. Same value as {obj_type}_id. Present for backward compat only.

Example — create then immediately PATCH:

const created = await postArchiveContent(archiveId, payload);
const newId = created.data.archive_content_id; // random string e.g. "xK9mP3qRtL2"

// Use it directly in the PATCH URL — no lookup needed
await patchArchiveContent(newId, { name: 'Updated Name' });
// PATCH /v3/crud/archive/{archive_id}/archive_content/{newId}

Note on _id_random suffix: The {obj_type}_id_random field is a legacy artifact from the pre-Vision model. Once you confirm {obj_type}_id is a random string (length 1122), you do not need _id_random as a fallback. New code should only read {obj_type}_id.


4. V3 Uniform Lookup System

The V3 Lookup system provides a hierarchical, deduplicated interface for standardized reference tables (Countries, Timezones, etc.). It supports global defaults, account-level overrides, and object-level overrides, with optional site-specific whitelisting.

How the hierarchy works

Each lookup table (lu_v3_country, lu_v3_time_zone, etc.) can hold multiple rows for the same logical item at different scopes:

Scope account_id for_type / for_id Wins over
Global default NULL NULL / NULL nothing
Account override set NULL / NULL Global default
Object override set set Account override + Global default

The API uses ROW_NUMBER() PARTITION BY group to collapse all rows for the same item down to the single highest-priority winner before returning results. group is the identity key — it is what makes two rows "the same item competing for priority."

Important

The group field is not a display label. It is the deduplication key. Each lookup type uses a different natural key for group:

Lookup type group value Example
country ISO alpha-2 code "US", "CA", "GB"
country_subdivision subdivision code "US-NY", "CA-ON"
time_zone IANA timezone name "America/New_York", "US/Eastern"

For time_zone, group and name must always be identical — there is no concept of "override all US timezones as a group." Each timezone is its own identity.

A. List Lookups

Retrieve the deduplicated, ranked list for a lookup type.

  • Endpoint: GET /v3/lookup/{lu_type}/list
  • Available Types: country, country_subdivision, time_zone
  • Parameters:
    • site_id (Optional): Random ID of the site — applies a Whitelist Policy (see §C).
    • only_priority (Optional): true returns only priority=1 items (e.g., common time zones).
    • for_type / for_id (Optional): Object context — activates object-level override matching.
    • include_disabled (Optional): true includes shadowed/disabled records (useful for admin views).

Frontend keying: Always key Svelte {#each} blocks on group, not id or name. group is guaranteed unique in the response. Keying on id will break if an account override wins (different id, same logical item).

B. Resolve Identity

Resolves a string to a single lookup record.

  • Endpoint: GET /v3/lookup/{lu_type}/resolve?q=VALUE
  • Usage: Use when you have an external code (e.g., ISO "US") and need the full Aether record. Scans name, group, and other identity fields.

C. Site Whitelist Policy

To restrict which lookup items appear for a specific site, add a lookup_policy to site.cfg_json:

{
  "lookup_policy": {
    "country": ["US", "CA", "GB"],
    "time_zone": ["America/New_York", "US/Eastern"]
  }
}

Whitelist values must match the group field — i.e., the natural key for that type (ISO code for country, IANA name for time zone). Using a display name will silently return no results for that item.

D. Adding and managing client overrides

When a client needs a customized label or wants to hide/reorder lookup items, create override records rather than modifying global defaults.

Rules:

  1. Never modify global default rows (account_id = NULL). Those are shared across all accounts. Any change there affects every client.
  2. Set group to the exact same value as the global default row for the item you are overriding. If group doesn't match, the override creates a new item instead of replacing the existing one.
  3. Set account_id to the client's account ID. Leave for_type / for_id null unless the override is specific to a single object (e.g., one site).

Example — rename "US/Eastern" for one account:

INSERT INTO lu_v3_time_zone
  (account_id, name, name_override, `group`, enable, priority, sort)
VALUES
  (42, 'US/Eastern', 'Eastern Time (Client Label)', 'US/Eastern', 1, 1, 50);

The name_override field is the display label the frontend should prefer when set. group = 'US/Eastern' ensures this row competes with — and wins over — the global default in the PARTITION BY group deduplication.

To disable an item for one account (hide it from their dropdowns):

INSERT INTO lu_v3_time_zone
  (account_id, name, `group`, enable)
VALUES
  (42, 'US/Samoa', 'US/Samoa', 0);

Setting enable = 0 on an account-scoped row shadows the global default for that account only.

To remove a client override (revert to global default):

Simply delete the row where account_id = <client> and group = '<item>'. The global default row is unaffected and immediately resumes winning.

E. Adding new global lookup items

When seeding new lookup data (e.g., adding timezones in bulk):

  1. Set group = name for every row (for time_zone). This is a hard invariant — if group is set to a regional label like "United States" instead of the timezone name, the entire group collapses to a single winner and all but one entry disappear from the API response.
  2. Set account_id = NULL and for_type = NULL / for_id = NULL for global defaults.
  3. After seeding, verify with:
    -- Should return 0 rows; any result means multiple items will collapse into one
    SELECT `group`, COUNT(*) AS cnt
    FROM lu_v3_time_zone
    WHERE account_id IS NULL
    GROUP BY `group`
    HAVING cnt > 1;
    

5. Event File Data Retrieval (Hosted Files)

Every Event File (event_file) must have a linked Hosted File (hosted_file). The Hosted File itself is a metadata record for binary content (files), which is accessed via separate Action endpoints (e.g., /v3/action/hosted_file/download). This API endpoint provides metadata about the associated hosted file. To retrieve this additional metadata:

  • Endpoint: GET /v3/crud/event_file/{event_file_id}
  • Query Parameter: Add inc_hosted_file=true
    • Example: /v3/crud/event_file/<event_file_id>?inc_hosted_file=true

Response Impact:

  1. Top-Level Convenience Fields: The response will include top-level fields for commonly needed hosted file data. These are populated directly from the SQL view via JOINs.
    • hosted_file_hash_sha256 (string)
    • hosted_file_subdirectory_path (string)
    • hosted_file_content_type (string)
    • hosted_file_size (string - in bytes)
  2. Nested Hosted File Object: A full hosted_file object will be nested under the hosted_file key. This object (Hosted_File_Base model) will contain all its standard fields, including id (random string ID), hash_sha256, content_type, size, etc.

6. Hosted File Actions: Convert & Clip (Frontend Notes)

These helper endpoints let the frontend request small server-side transformations without uploading new blobs. They return a newly-created hosted_file metadata object on success.

  • Convert (PDF → Image)

    • Method: GET
    • Path: /v3/action/hosted_file/{hosted_file_id}/convert_file
    • Required query params: link_to_type, link_to_id
    • Optional query params: filename_no_ext (defaults to automated_hosted_file_conversion), to_type (defaults to webp)
    • Auth: standard V3 headers (x-aether-api-key, x-account-id / x-no-account-id / ?jwt=)
    • Behavior: converts the first page of a PDF to webp or png, saves a new hosted_file, and returns its metadata. Returns 400 on failure.
  • Clip Video

    • Method: GET
    • Path: /v3/action/hosted_file/{hosted_file_id}/clip_video
    • Required query params: link_to_type, link_to_id, start_time, end_time (format HH:MM:SS)
    • Optional query params: filename_no_ext (defaults to automated_hosted_file_clip_video), reencode (bool), scale_down (bool)
    • Auth: standard V3 headers
    • Behavior: extracts a clip using ffmpeg and saves it as a new hosted_file. Defaults to stream-copying to be fast; set reencode=true to force H.264 or scale_down=true to resize. Returns 400 on failure.
    • Behavior: extracts a clip using ffmpeg and saves it as a new hosted_file.
      • Defaults to stream-copying to be fast; set reencode=true to force H.264 or scale_down=true to resize.
      • For longer-running clips you can schedule the job in the background by adding ?background=true. When scheduled the API returns 202 Accepted and the clip runs asynchronously on the server; check the returned hosted_file record later via the standard V3 hosted_file endpoints.
      • Returns 400 on synchronous failure; returns 202 when scheduled successfully.

Frontend guidance:

  • Call these routes with the same link_to_type / link_to_id you plan to associate the resulting hosted_file with — the server resolves random IDs for you.
  • After a successful response, use the V3 hosted_file action endpoints (download/delete) to manage or retrieve the new file.
  • These endpoints run synchronously and can take time for large inputs; for heavy or batch workloads use a queued job pattern instead.
  • These endpoints may take time for large inputs. Prefer using ?background=true to schedule work and receive a 202 Accepted response for async processing. For heavy or batch workloads use a queued job pattern instead.

7. User Actions (/v3/action/user/)

Stateful user account operations that are not standard CRUD. All require x-aether-api-key.

Important

Migration from legacy /user/* routes: The table below maps each legacy endpoint to its V3 replacement. Run both in parallel during transition; remove legacy routes once traffic logs confirm they are quiet.

Legacy V3 Replacement
GET /user/authenticate POST /v3/action/user/authenticate
POST /user/verify_password POST /v3/action/user/verify_password
PATCH /user/{id}/change_password POST /v3/action/user/{id}/change_password
GET /user/{id}/new_auth_key GET /v3/action/user/{id}/new_auth_key
GET /user/{id}/email_auth_key_url GET /v3/action/user/{id}/email_auth_key_url
GET /user/lookup POST /v3/crud/user/search
GET /user/lookup_email POST /v3/crud/user/search
GET /user/lookup_username POST /v3/crud/user/search

A. Authenticate

Authenticate a user by username + password or user_id + auth_key.

  • Method: POST
  • Path: /v3/action/user/authenticate
  • Auth: x-aether-api-key + x-account-id (scopes username lookups to the correct account)
  • Security improvement: Credentials are in the POST body, not query params — safe from URL logging.

Request body:

{ "username": "scott", "password": "MyPassword123!" }

or:

{ "user_id": "<user_id_random>", "auth_key": "<one_time_key>", "valid_email": true }
  • valid_email (optional bool): if true, marks email_verified = true on success.
  • inc_user_role_list (optional query param, default false): include role list in the returned user object.

Response on success: Full user object (same shape as GET /v3/crud/user/{id}).

Errors: 400 missing credentials, 403 wrong password or account disabled, 404 user not found.

Auth key flow: Auth keys are one-time-use — the key is cleared from the DB immediately on successful authentication. Request a new one via GET /v3/action/user/{id}/new_auth_key.


B. Verify Password

Check a user's current password without changing it.

  • Method: POST
  • Path: /v3/action/user/verify_password
  • Auth: x-aether-api-key + x-account-id

Request body:

{ "user_id": "<user_id_random>", "current_password": "MyPassword123!" }

or use "username" instead of "user_id" to look up by username within the account.

Response: data: true on match. 403 on mismatch, 404 if user not found.


C. Change Password

Change a user's password. Optionally verify the current password first.

  • Method: POST
  • Path: /v3/action/user/{user_id}/change_password
  • Auth: x-aether-api-key + x-account-id

Request body:

{ "new_password": "NewPassword456!", "current_password": "MyPassword123!" }
  • new_password is required (minimum 10 characters).
  • current_password is optional. If provided, it is verified before the change is applied. Omit it for admin-driven resets.

Response: data: true on success. 403 if current_password provided but wrong.


D. Generate New Auth Key

Generate a fresh one-time-use auth key for the user and write it to the DB.

  • Method: GET
  • Path: /v3/action/user/{user_id}/new_auth_key
  • Auth: x-aether-api-key + x-account-id

Response:

{ "data": { "auth_key": "<new_key>" } }

The returned key can then be passed to /authenticate (as auth_key) or embedded in a login URL. The user record must have allow_auth_key = true for key-based authentication to work.


E. Email Auth Key URL

Generate a new auth key and email a one-time login link to the user's email address.

  • Method: GET
  • Path: /v3/action/user/{user_id}/email_auth_key_url
  • Auth: x-aether-api-key + x-account-id

Query Parameters:

Parameter Type Default Description
root_url string null Base URL the login link is built from.
key_param_name string auth_key Query param name used for the auth key in the generated link.

Response: data: true on success (email sent). 500 if delivery failed (check account email config and that the user account is enabled with allow_auth_key = true).


The three legacy lookup routes (lookup, lookup_email, lookup_username) are replaced by standard V3 CRUD search:

// Look up by user_id (Vision ID)
POST /v3/crud/user/search
{ "and": [{ "field": "id_random", "op": "eq", "value": "<user_id>" }] }

// Look up by email
POST /v3/crud/user/search
{ "and": [{ "field": "email", "op": "eq", "value": "user@example.com" }] }

// Look up by username
POST /v3/crud/user/search
{ "and": [{ "field": "username", "op": "eq", "value": "scott" }] }

Results are automatically scoped to the x-account-id provided in the request.


9. Event Exhibit Tracking Export (Leads Export)

Allows an exhibitor to download all lead-capture records for their exhibit as a CSV or XLSX file.

  • Method: GET
  • Path: /v3/action/event_exhibit/{exhibit_id}/tracking_export
  • Auth: Standard V3 headers (x-aether-api-key + x-account-id or ?jwt=)

Query Parameters

Parameter Type Default Description
file_type CSV | XLSX CSV Output format.
return_file bool true true → file download response. false → JSON body with row data.

Response

  • Content-Type: text/csv (CSV) or application/vnd.openxmlformats-officedocument.spreadsheetml.sheet (XLSX)
  • Content-Disposition: attachment; filename="leads_export_<timestamp>.csv"
  • If there are no tracking records, a valid file with headers only is returned (not a 404).

Columns Returned

Fixed columns (always present), followed by any custom question columns flattened from responses_json:

event_exhibit_tracking_id, created_on, updated_on, event_exhibit_name, event_badge_full_name, event_badge_email, event_badge_professional_title, event_badge_affiliations, event_badge_location, event_badge_country, external_person_id, exhibitor_notes, priority, enable, hide, [custom question codes…]

Note: exhibitor_notes has HTML tags stripped automatically for clean CSV output.

Permission Requirement — leads_api_access

Important

This endpoint enforces a per-exhibit permission flag. The event_exhibit record must have leads_api_access = true set in the database, OR the caller must have manager-level account access (JWT with manager: true).

If leads_api_access is false or null on the exhibit, the API returns:

{ "detail": "Access denied: leads API access is not enabled for this exhibit." }

Fix: Enable the flag on the exhibit record via PATCH /v3/crud/event_exhibit/{id} with { "leads_api_access": true }, or set it directly in the database/admin panel.

Dual purpose of leads_api_access

This flag serves two related but distinct roles:

  1. 3rd-party API access (original intent): Controls whether external systems (exhibitor apps, badge-scanning devices, etc.) are permitted to push or pull lead data for this exhibit via the API.
  2. UI export gate (new): The frontend should read leads_api_access from the exhibit record and use it to show or hide the export/download button. Only render the button when the flag is true — this prevents users from triggering a request that will always 403.

The recommended pattern is to fetch the exhibit record first and gate the UI on this field before the user ever sees the export option. The API enforces the same check server-side as a safety net.

Example Request

const resp = await fetch(
  `https://dev-api.oneskyit.com/v3/action/event_exhibit/${exhibitId}/tracking_export?file_type=CSV&return_file=true`,
  {
    headers: {
      'x-aether-api-key': API_KEY,
      'x-account-id': accountId,
    },
  }
);
// resp is a file blob — use URL.createObjectURL() or trigger a download
const blob = await resp.blob();
const url = URL.createObjectURL(blob);

10. Troubleshooting 403 Forbidden

If you receive a 403 on a valid ID:

  1. Verify x-aether-api-key is correct.
  2. Ensure you are sending x-account-id and NOT x-aether-api-token.
  3. Verify the record actually belongs to the account ID you are sending.
  4. Check if the object is marked public_read: True in the registry. (Posts and Archive Content allow guest access; Journals and Badges do not).