security(v3): harden multi-tenant isolation and enhance failure feedback
This commit is contained in:
@@ -1,266 +1,70 @@
|
||||
# Aether API V3 Frontend Integration Guide (Svelte/TypeScript)
|
||||
|
||||
This guide explains how to update or create frontend functions to interact with the new **Aether API V3 CRUD** and **Action** endpoints. V3 introduces a nested URL structure, a powerful POST-based search, and specialized "Action" routes for binary data.
|
||||
This guide defines the standards for interacting with the **Aether API V3 CRUD** and **Action** endpoints.
|
||||
|
||||
---
|
||||
|
||||
## 1. Key Differences (V2 vs V3)
|
||||
## 1. Authentication and Security (Mandatory)
|
||||
|
||||
| Feature | CRUD V2 (Legacy) | CRUD V3 (Modern) |
|
||||
| ------------------ | --------------------- | ---------------------------------------------------- |
|
||||
| **Base Prefix** | `/v2/crud` | `/v3/crud` |
|
||||
| **List Suffix** | Uses `/list` | **No suffix** (e.g., `/v3/crud/journal/`) |
|
||||
| **Nested Path** | Not supported in URL | **Supported** (e.g., `/v3/crud/journal/{id}/entry/`) |
|
||||
| **Object Aliases** | Limited | **Supported** (e.g., `entry` maps to `journal_entry`)|
|
||||
| **View Selection** | `tbl_alt`, `mdl_alt` | **`view` parameter** (e.g., `?view=enriched`) |
|
||||
| **Complex Search** | Limited to GET `jp` | **POST `/search`** (Unlimited size + Hybrid params) |
|
||||
| **Full-Text** | Manual column names | **Reserved `q` property** in SearchQuery |
|
||||
|
||||
---
|
||||
|
||||
## 2. Authentication and Security (Mandatory)
|
||||
|
||||
As of January 2026, the V3 architecture enforces strict **Multi-Tenant Isolation** and **Machine Authorization**. Most requests require two levels of validation.
|
||||
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.** Every request must provide a valid `x-aether-api-key` in the header. This identifies the application or script.
|
||||
|
||||
**Mandatory for all requests.** identifies the application or client.
|
||||
* **Header:** `x-aether-api-key: <your_app_key>`
|
||||
* **Status Code:** `403 Forbidden` if missing, invalid, or expired.
|
||||
* **Status Code:** `403 Forbidden` if missing or invalid.
|
||||
|
||||
### B. The "Visa" (Account Context)
|
||||
Once the API Key is validated, you must specify the context of your request.
|
||||
|
||||
1. **Standard User Access**: Provide the `x-account-id` (the random string ID).
|
||||
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_random>`
|
||||
2. **Administrative Bypass**: For authorized scripts needing global access.
|
||||
* **Header:** `x-no-account-id: bypass`
|
||||
3. **Guest / Anonymous Access**: Provide a **Safe Guest JWT**.
|
||||
* **Header:** `x-aether-api-key: <your_app_key>` (No Account Header)
|
||||
* **Query Param:** `?jwt=<guest_token>`
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## 3. Implementing V3 CRUD Functions
|
||||
## 2. Bootstrapping (The FQDN Handshake)
|
||||
|
||||
### A. List & Single Object (GET)
|
||||
```ts
|
||||
// GET /v3/crud/journal/{id}?view=enriched
|
||||
export async function get_ae_obj_v3({ api_cfg, obj_type, obj_id, view = 'default' }) {
|
||||
const endpoint = `/v3/crud/${obj_type}/${obj_id}`;
|
||||
return await get_object({ api_cfg, endpoint, params: { view } });
|
||||
}
|
||||
```
|
||||
|
||||
### B. Advanced & Hybrid Search (POST)
|
||||
```ts
|
||||
// POST /v3/crud/{obj_type}/search
|
||||
export async function search_ae_obj_v3({ api_cfg, obj_type, search_query, enabled = 'enabled' }) {
|
||||
const endpoint = `/v3/crud/${obj_type}/search`;
|
||||
return await post_object({ api_cfg, endpoint, params: { enabled }, data: search_query });
|
||||
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:**
|
||||
```json
|
||||
{
|
||||
"and": [
|
||||
{ "field": "fqdn", "op": "eq", "value": "demo.oneskyit.com" }
|
||||
]
|
||||
}
|
||||
```
|
||||
**Results:**
|
||||
* Returns 200 + a list containing the `account_id` and `site_id` random strings.
|
||||
* ** ڈیزائن Choice:** If the domain is not found, it returns **200 OK with an empty list `[]`**. It is NOT a 404.
|
||||
|
||||
---
|
||||
|
||||
## 4. Hosted File Management (Modern V3 Actions)
|
||||
## 3. Standard CRUD Patterns
|
||||
|
||||
V3 uses specialized **"Action"** routes for binary operations to separate processing logic from standard metadata CRUD.
|
||||
### A. GET by ID
|
||||
Used when the ID is known.
|
||||
* **Endpoint:** `GET /v3/crud/{obj_type}/{id_random}`
|
||||
* **Security:** Returns 403 if the record doesn't belong to your `x-account-id`.
|
||||
|
||||
### A. Upload Action
|
||||
**Path**: `POST /v3/action/hosted_file/upload`
|
||||
**Format**: `multipart/form-data`
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `file_list` | File[] | Yes | One or more files to upload. |
|
||||
| `account_id` | String | Yes | Random ID of the owner account. |
|
||||
| `link_to_type`| String | Yes | Object type to link (e.g., `archive_content`). |
|
||||
| `link_to_id` | String | Yes | Random ID of the object to link. |
|
||||
|
||||
**Query Parameters:**
|
||||
- `allowed_extensions`: Whitelist check (e.g., `?allowed_extensions=png&allowed_extensions=jpg`).
|
||||
- `delay_ms`: Simulate network delay for UI testing.
|
||||
|
||||
**Features:**
|
||||
- **Automatic Deduplication:** API checks SHA256 hashes. If a file exists on the server, it reuses the record and creates a new link instead of a duplicate upload.
|
||||
- **Relational Integrity:** Automatically creates `hosted_file_link` records.
|
||||
|
||||
### B. Download & Streaming Action
|
||||
**Path**: `GET /v3/action/hosted_file/{id}/download`
|
||||
|
||||
**Query Parameters:**
|
||||
| Parameter | Type | Description |
|
||||
| :--- | :--- | :--- |
|
||||
| `key` | String | **Temporary V3.0 Auth:** Pass any valid `account_id_random` to bypass headers. |
|
||||
| `site_key` | String | Bypass headers via `access_key` from the `site` table. |
|
||||
| `filename` | String | Override the response filename. |
|
||||
|
||||
**Features:**
|
||||
- **ID Vision:** Automatically resolves `{id}` if it belongs to a container object (e.g., `event_file`) instead of a direct `hosted_file`.
|
||||
- **Streaming:** Supports standard `Range` headers for large files and video seeking.
|
||||
- **Testing:** Supports `delay_ms` query parameter.
|
||||
|
||||
> [!WARNING]
|
||||
> **TEMPORARY SOLUTION (V3.0):** The `?key=` and `?site_key=` unauthenticated access patterns are intended to unblock the frontend for inline images and direct links where custom headers are not possible. This will be replaced by a standardized Signed URL or Read-Token system in **Version 3.1**. Please do not rely on this pattern for long-term security architecture.
|
||||
|
||||
### C. Hash-Based Download (Content-Addressable)
|
||||
**Path**: `GET /v3/action/hosted_file/hash/{sha256}/download`
|
||||
|
||||
**Features:**
|
||||
- **Local Caching:** Ideal for systems like the Events Launcher that cache files locally by hash.
|
||||
- **Flexible Auth:** Supports `api_key` in the query parameter (e.g., `?api_key=<key>`) for simple machine-to-machine requests.
|
||||
- **Zero DB Lookup:** Resolves the physical path deterministically from the hash, bypassing database latency.
|
||||
|
||||
### D. Deletion & Cleanup Action
|
||||
**Path**: `DELETE /v3/action/hosted_file/{id}`
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `link_to_type`| Query | null | The type of the link to remove. |
|
||||
| `link_to_id` | Query | null | The random ID of the link to remove. |
|
||||
| `rm_orphan` | Query | false | If true, physically delete file if no links remain. |
|
||||
| `method` | Query | `hide` | Cleanup method: `hide`, `disable`, or `delete` (hard). |
|
||||
| `fake_delete` | Query | false | **Testing Mode:** Verifies file/record existence without modification. |
|
||||
### 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.
|
||||
|
||||
---
|
||||
|
||||
## 5. Event File Management (Specialized V3 Actions)
|
||||
## 4. Troubleshooting 403 Forbidden
|
||||
|
||||
While `hosted_file` handles generic storage, `event_file` actions are context-aware and atomic for the Event module.
|
||||
|
||||
### A. Atomic Upload Action
|
||||
**Path**: `POST /v3/action/event_file/upload`
|
||||
**Format**: `multipart/form-data`
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `file_list` | File[] | Yes | Files to upload. |
|
||||
| `account_id`| String | Yes | Owner account. |
|
||||
| `for_type` | String | Yes | Parent object type (e.g., `event_session`). |
|
||||
| `for_id` | String | Yes | Random ID of the parent object. |
|
||||
| `event_id` | String | No | Optional event context. |
|
||||
| `title` | String | No | Display title for the file. |
|
||||
|
||||
**Features:**
|
||||
- **Atomic Creation:** Automatically creates the `hosted_file`, the `hosted_file_link`, AND the `event_file` association in one request.
|
||||
- **Intelligent Updates:** If the same file is uploaded again for the same object, it updates the metadata instead of creating a duplicate association.
|
||||
- **Enriched Return:** Returns full `event_file` objects with nested `hosted_file` data.
|
||||
|
||||
### B. Specialized Download
|
||||
**Path**: `GET /v3/action/event_file/{id}/download`
|
||||
*Semantic alias for the universal hosted_file downloader.*
|
||||
|
||||
### C. Link Existing Hosted File
|
||||
**Path**: `POST /v3/action/event_file/from_hosted_file/{hosted_file_id}`
|
||||
|
||||
Use this when a file is already in standard storage (e.g., from a previous session or general archive) and you want to link it to an Event object without re-uploading.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `hosted_file_id` | Path | Yes | String ID of the physical file. |
|
||||
| `for_type` | Body | Yes | Target object type (e.g., `event_session`). |
|
||||
| `for_id` | Body | Yes | String ID of the target object. |
|
||||
| `event_id` | Body | No | Optional event context. |
|
||||
|
||||
**Features:**
|
||||
- **Vision IDs:** Accepts string IDs in both the path and JSON body.
|
||||
- **Relational Integrity:** Automatically creates both `hosted_file_link` and `event_file` records.
|
||||
- **Enriched Return:** Returns the full `event_file` object.
|
||||
|
||||
---
|
||||
|
||||
## 6. Hosted File Management (Legacy)
|
||||
|
||||
The following endpoints are maintained for backward compatibility but should be migrated to V3 Actions.
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
| :--- | :--- | :--- |
|
||||
| `POST` | `/hosted_file/upload_files` | Legacy multi-upload. |
|
||||
| `GET` | `/hosted_file/{id}/download` | Legacy download. |
|
||||
| `GET` | `/hosted_file/{id}/stream` | Legacy buffered streamer. |
|
||||
| `DELETE` | `/hosted_file/{id}` | Legacy deletion. |
|
||||
|
||||
---
|
||||
|
||||
## 6. The "ID Vision" Standard (2026)
|
||||
|
||||
V3 uses a **String-Only ID Vision**. The frontend NEVER handles or stores database integers.
|
||||
|
||||
1. **Automatic Mapping:** Internal `id_random` fields are mapped to clean names (e.g., `id`, `event_id`, `account_id`) in JSON responses.
|
||||
2. **Intelligent Resolution:** You can send random string IDs back to the API in any `*_id` field; the API resolves them to integers before database insertion.
|
||||
3. **Suffix Compatibility:** The `_random` suffix (e.g., `event_id_random`) is maintained for backward compatibility with V2 clients. Standard V3 frontend integration should prioritize fields without the suffix.
|
||||
|
||||
---
|
||||
|
||||
## 9. Real-Time Communication (V3 WebSockets)
|
||||
|
||||
V3 WebSockets provide a granular, high-performance messaging layer.
|
||||
|
||||
- **Guide**: [Aether API V3 WebSocket Integration Guide](./GUIDE__V3_FRONTEND_WEBSOCKETS.md)
|
||||
- **Endpoint**: `ws://[api_domain]/v3/ws/group/{group_id}/client/{client_id}`
|
||||
- **Key Requirement**: All messages must conform to the `WS_Message_V3` schema.
|
||||
|
||||
---
|
||||
|
||||
## 10. Structured Error Handling
|
||||
|
||||
V3 returns machine-readable error objects in `meta.details` for failures.
|
||||
|
||||
### HTTP Status Codes
|
||||
- **`400 Bad Request`**: Used for client-driven errors including invalid search fields, validation failures, and constraint violations.
|
||||
- **`403 Forbidden`**: Missing or invalid API Key / Account Context.
|
||||
- **`404 Not Found`**: Object ID does not exist.
|
||||
- **`500 Internal Server Error`**: Unexpected server crash or database connection failure.
|
||||
|
||||
### Common Error Categories
|
||||
Found in `meta.details.category`:
|
||||
- `database_duplicate`: Non-unique value (Code 1062). -> **400**
|
||||
- `database_constraint`: Foreign key violation (Codes 1451, 1452). -> **400**
|
||||
- `database_schema`: Invalid column name or missing field in the requested `view` (Codes 1054, 1146). -> **400**
|
||||
- `validation`: Pydantic validation failed (Check `details` for field-specific errors). -> **400**
|
||||
|
||||
---
|
||||
|
||||
## 8. Data Store Cascading Lookup (V3)
|
||||
|
||||
V3 provides a specialized endpoint for retrieving configuration or content snippets by a human-friendly `code`. This uses a **hierarchical fallback** logic to find the "best fit" record for the current user and object context.
|
||||
|
||||
### A. The V3 Lookup Endpoint
|
||||
**Path:** `GET /v3/data_store/code/{code}`
|
||||
|
||||
**Query Parameters:**
|
||||
| Parameter | Type | Required | Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `for_type` | String | No | Parent object type (e.g., `event`, `person`). |
|
||||
| `for_id` | String | No | Parent object random ID (Vision ID). |
|
||||
| `limit` | Integer | No | **Dynamic Return:** Default `1` (returns single object). If `> 1`, returns a list. |
|
||||
|
||||
### B. Cascading Logic (The Hierarchy of Truth)
|
||||
The API automatically resolves the most specific record available using the following priority:
|
||||
**Object Override > Account Override > Global System Default.**
|
||||
|
||||
| Specificity | `account_id` | `for_type` | `for_id` | `code` | `Result` |
|
||||
| :--- | :--- | :--- | :--- | :--- | :--- |
|
||||
| **Global** | `NULL` | `NULL` | `NULL` | `'site_config'` | System default (blue theme). |
|
||||
| **Account** | `'abc123_rd'` | `NULL` | `NULL` | `'site_config'` | Acme Corp default (green theme). |
|
||||
| **Object** | `'abc123_rd'` | `'event'` | `'evt_xyz_rd'` | `'site_config'` | Main Conference override (gold theme). |
|
||||
|
||||
### C. Rules for Frontend Agents
|
||||
1. **Vision IDs Only:** Always use random string IDs (e.g., `evt_xyz_rd`) for `for_id`. Never use database integers.
|
||||
2. **Explicit Context:** If you are within an Event or Person context, always provide `for_type` and `for_id`. The API will handle the fallback if a specific record doesn't exist.
|
||||
3. **Automatic JSON Parsing:** If the record `type` is `'json'`, the API returns a structured object/list under the `json` key. You do not need to call `JSON.parse()`.
|
||||
4. **Handling the Return:**
|
||||
* `limit=1` (Default): Returns a single **Object** in `data`.
|
||||
* `limit>1`: Returns a **List** of objects in `data`.
|
||||
|
||||
### D. Example Implementation
|
||||
```ts
|
||||
// GET /v3/data_store/code/event_launcher_main_info?for_type=event&for_id=nmBfuGFeR0k&limit=1
|
||||
export async function get_data_store_v3({ api_cfg, code, for_type, for_id, limit = 1 }) {
|
||||
const endpoint = `/v3/data_store/code/${code}`;
|
||||
const params = { for_type, for_id, limit };
|
||||
return await get_object({ api_cfg, endpoint, params });
|
||||
}
|
||||
```
|
||||
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).
|
||||
|
||||
|
||||
Reference in New Issue
Block a user