# Phase 1 - Lock Scope + Contract

Status: approved for implementation  
Date: 2026-02-25  
Target environment: port `21006` first, then promote to `22222`

## 1) Goal

Enable each logged-in user to save API settings as reusable profiles ("phases"), including:
- one user -> many profiles
- one profile -> many token sources
- choose one profile to run `/live/accounts` and `/live/dashboard`

This phase locks product scope and API/data contracts before coding.

## 2) In scope

- Profile model for per-user API setup
- Token source model inside each profile
- Optional account preset model inside each profile
- RBAC rules for who can read/write profiles
- Request/response contracts for new profile endpoints
- Compatibility contract with existing dashboard flow
- Error code contract
- Acceptance criteria for Phase 2+ implementation

## 3) Out of scope (later phases)

- Shared profile across users/teams
- Full secret rotation UI
- Bulk import/export profiles
- Profile version history UI
- Auto budget action execution changes

## 4) Terms

- User: authenticated account from `auth_users`
- Profile (Phase): saved API config owned by exactly one user
- Token Source: one token record under a profile (`sourceKey`, label, encrypted token)
- Account Preset: saved account refs under a profile
- Default Profile: one profile per user flagged as default

## 5) RBAC contract

- `viewer`: read own profiles, cannot create/update/delete
- `analyst`: full CRUD on own profiles
- `admin`: full CRUD on own profiles (cross-user management is out of scope for now)

Owner rule: every profile operation is constrained by `user_id = session.userId`.

## 6) Data contract (DB)

## 6.1 `auth_user_api_profiles`

- `id` (pk)
- `user_id` (fk -> `auth_users.id`, required)
- `profile_name` (required, 1..80)
- `description` (optional, 0..255)
- `is_default` (bool, default false)
- `is_active` (bool, default true)
- `created_at`, `updated_at`, `last_used_at`

Constraints:
- unique `(user_id, profile_name)`
- max one default profile per user (enforced in service logic + index strategy per DB)

## 6.2 `auth_user_api_sources`

- `id` (pk)
- `profile_id` (fk -> profiles.id, required)
- `source_key` (required, regex `^[a-z0-9_-]{1,32}$`)
- `source_label` (required, 1..80)
- `access_token_enc` (required, encrypted at rest)
- `is_default_token` (bool, default false)
- `created_at`, `updated_at`

Constraints:
- unique `(profile_id, source_key)`
- at least one source per profile

## 6.3 `auth_user_api_accounts` (optional preset)

- `id` (pk)
- `profile_id` (fk -> profiles.id)
- `source_key` (required)
- `account_id` (normalized `act_...`, required)
- `created_at`

Constraints:
- unique `(profile_id, source_key, account_id)`

## 6.4 `auth_user_api_audit` (minimal audit)

- `id` (pk)
- `user_id`
- `profile_id` (nullable for create-fail/system)
- `action` (`profile.create|profile.update|profile.delete|profile.use|profile.set_default`)
- `metadata_json` (small payload, no raw token)
- `created_at`

## 7) Security contract

- Raw token is never returned by read APIs.
- Tokens are stored encrypted (`access_token_enc`) using server key `TOKEN_ENCRYPTION_KEY`.
- Read APIs return only masked token preview (example: `EAAJ...4OG`).
- Logs and audit metadata must not contain raw token.

## 8) API contract

Base path: `/auth/token-profiles`

## 8.1 GET `/auth/token-profiles`

Purpose: list profiles for current user.

Response (200):
```json
{
  "ok": true,
  "profiles": [
    {
      "id": 12,
      "profileName": "Q4_2025_main",
      "description": "main reporting profile",
      "isDefault": true,
      "isActive": true,
      "lastUsedAt": "2026-02-25T00:00:00.000Z",
      "sources": [
        {
          "id": 44,
          "sourceKey": "default",
          "sourceLabel": "Default token",
          "isDefaultToken": true,
          "tokenMasked": "EAAJ...4OG"
        }
      ],
      "accountPresets": [
        { "sourceKey": "default", "accountId": "act_280799" }
      ]
    }
  ]
}
```

## 8.2 POST `/auth/token-profiles`

Role: `analyst` or `admin`

Request:
```json
{
  "profileName": "phase_scale_chat",
  "description": "low cpc scale rule",
  "isDefault": false,
  "sources": [
    {
      "sourceKey": "token_1",
      "sourceLabel": "Client A token",
      "accessToken": "EAA....",
      "isDefaultToken": false
    }
  ],
  "accountPresets": [
    { "sourceKey": "token_1", "accountId": "act_123456" }
  ]
}
```

Response (201): `{ "ok": true, "profile": { ...same shape as list item... } }`

## 8.3 PATCH `/auth/token-profiles/:id`

Role: `analyst` or `admin`, owner only.

Partial update allowed for name/description/isActive/sources/accountPresets.

Response (200): `{ "ok": true, "profile": { ... } }`

## 8.4 DELETE `/auth/token-profiles/:id`

Role: `analyst` or `admin`, owner only.

Response (200): `{ "ok": true }`

Rule:
- if deleting default profile and user has other active profiles, auto-select latest profile as new default.

## 8.5 POST `/auth/token-profiles/:id/set-default`

Role: `analyst` or `admin`, owner only.

Response (200): `{ "ok": true, "defaultProfileId": 12 }`

## 9) Integration contract with existing live endpoints

Endpoints:
- `POST /live/accounts`
- `POST /live/dashboard`

New request fields:
- `profileId?: number`

Resolution order (locked behavior):
1. If `profileId` provided -> load token sources from saved profile.
2. Else if inline `tokenSources` provided -> use inline payload.
3. Else -> fallback to server default token (`API_ACCESS_TOKEN_ENC` หรือ `API_ACCESS_TOKEN`) if available.
4. If nothing available -> validation error.

Ownership:
- `profileId` must belong to logged-in user, else `404` (do not leak existence).

## 10) Validation contract

- `profileName`: required, trimmed, 1..80
- `description`: optional, max 255
- `sources`: required for create, min 1, max 20
- each `sourceKey` unique inside profile
- `accountId` normalized to `act_...`
- max `accountPresets` per profile: 200

## 11) Error contract (backward compatible)

Keep existing style and add stable code:

```json
{
  "ok": false,
  "error": "profile not found",
  "errorCode": "PROFILE_NOT_FOUND"
}
```

Common codes:
- `UNAUTHORIZED`
- `FORBIDDEN`
- `PROFILE_NOT_FOUND`
- `PROFILE_NAME_CONFLICT`
- `INVALID_PROFILE_PAYLOAD`
- `TOKEN_SOURCE_REQUIRED`
- `INVALID_ACCOUNT_ID`
- `TOKEN_ENCRYPTION_KEY_MISSING`

## 12) Non-functional contract

- No downtime migration path
- No raw token leak in API/logs/UI
- P95 profile list API < 150 ms (local DB)
- Deterministic behavior for default profile switch

## 13) Acceptance criteria (Phase 1 exit)

- Scope approved (this document)
- Data/API/RBAC/error contracts finalized
- Team agrees on integration order (`21006` -> `22222`)
- No unresolved blocker for Phase 2 migration work

## 14) Implementation handoff to Phase 2

Build in this sequence:
1. DB migration
2. encryption utility
3. repository/service
4. profile endpoints
5. live endpoint integration
6. UI profile picker/save flow
7. tests and rollout
