feat: asset catalog API, 태그 DB 기초, 마이그레이션 3건 및 도메인/테스트 갱신 (2026-05-19)

This commit is contained in:
2026-05-19 22:15:55 +00:00
parent e2226e28b6
commit 158929b2d9
33 changed files with 2575 additions and 102 deletions
+21 -11
View File
@@ -68,22 +68,32 @@
- Request validation and auth resolution should happen before persistence mutation or response rendering.
- The health endpoint remains unauthenticated so infrastructure can probe readiness without a token.
## Flow: `v1 API — Service-Token Authentication`
## Flow: `v1 API — DB Token Authentication`
1. v1 endpoint receives Bearer token from Authorization header.
2. `require_read` resolves the token: checks env tokens first, then DB lookup via `get_token_principal()`.
3. If DB token: validates `token_source == "db"` and `token_type == "service"`, else returns HTTP 403.
4. `AuthContext` is populated with `user_id`, `token_type`, `permission`, and `runtime_id`.
2. `require_read` / `require_admin` resolves the token: checks env tokens first, then DB lookup via `get_token_principal()`.
3. Env tokens → HTTP 403 (all /v1 endpoints require DB tokens).
4. `AuthContext` is populated with `user_id`, `token_type`, `permission`, `runtime_id`, and `daemon_id`.
## Flow: `v1 API — GET /v1/assets`
1. Service-token auth required (see above).
2. Daemon-id guard: if `auth.daemon_id is None` → HTTP 403 ("daemon_id not assigned to this token"). NULL tokens are fail-closed.
3. `list_portfolios_with_strategies()` fetches only the token owner's portfolios scoped to `managed_by_daemon_id=auth.daemon_id`; `owner_user_id=auth.user_id` prevents cross-user leakage.
4. Optional `?status=<value>` query param filters portfolios by status field; `include_disabled=False` by default.
5. For portfolios with `active_strategy_id`, `get_strategy_set()` fetches config and JSON-parses `strategy_params`; failures fall back to `{}`.
6. Returns `AssetsResponse` with `total` and `assets[]` using field names: `cash`, `quantity`, `broker_type`, `strategy_params`.
7. `asset_id` in each asset item equals `portfolio.portfolio_id` (server-generated UUID after BAT-006 migration).
1. DB token required; `token_type` drives branching:
- `service`: daemon-id guard — if `auth.daemon_id is None` → HTTP 403 (fail-closed). `list_portfolios_with_strategies(owner_user_id=auth.user_id, managed_by_daemon_id=auth.daemon_id, include_disabled=False, market=market)`.
- `user`: `list_portfolios_with_strategies(owner_user_id=auth.user_id, include_disabled=True, market=market)`.
- `admin`: `list_portfolios_with_strategies(owner_user_id=filter_user_id, include_disabled=True, market=market)` where `filter_user_id = ?user_id param if provided, else auth.user_id`.
2. Optional `?status=<value>` query param post-filters portfolios by status field.
3. Optional `?market=<code>` query param filters by `assets.markets` array (SQL: `%s = ANY(a.markets)`).
4. For portfolios with `active_strategy_id`, `get_strategy_set()` fetches config and JSON-parses `strategy_params`; failures fall back to `{}`.
5. Returns `AssetsResponse` with `total` and `assets[]` using field names: `cash`, `quantity`, `broker_type`, `strategy_params`.
6. `asset_id` in each asset item equals `portfolio.portfolio_id` (server-generated UUID).
## Flow: `Admin Asset Catalog CRUD — POST/PATCH /v1/assets`
1. Admin token required (`require_admin`).
2. `POST /v1/assets`: `repo.insert_asset(symbol, markets, display_name, status)` → 201 `AssetCatalogResponse`.
3. `PATCH /v1/assets/{asset_id}`: `repo.update_asset(asset_id, markets, display_name, status)` → 200 `AssetCatalogResponse`.
4. `PATCH /v1/assets/{asset_id}/deactivate`: `repo.update_asset(asset_id, status="inactive")` → 200 `AssetCatalogResponse`.
5. Non-admin callers receive HTTP 403.
## Flow: `v1 API — GET /v1/user_info`