Files
ss-tools/specs/031-maintenance-banner/ux_reference.md
busya 320f82ab95 feat(auth): implement API key authentication and management
Introduce a new API key authentication mechanism to support service-to-service
communication (e.g., Airflow, CI/CD) without browser-based JWT login.

- Add `APIKey` model and `APIKeyPrincipal` dependency for RBAC.
- Implement `require_api_key_or_jwt` dependency with environment scoping.
- Add admin CRUD endpoints for API key lifecycle management.
- Add API key management UI in System Settings.
- Update maintenance API to support explicit `environment_id` and API key auth.
- Add i18n support for API keys and connection settings.
- Include Python and Bash usage examples in the `examples/` directory.
- Update technical specifications and documentation.
2026-05-25 11:35:27 +03:00

359 lines
21 KiB
Markdown

# UX Reference: Maintenance Banner for Dashboards
**Feature Branch**: `031-maintenance-banner`
**Created**: 2026-05-21
**Status**: Draft
**Updated**: 2026-05-25 — Added API Key management UX flow
## 1. User Persona & Context
* **Who is the user?**:
- **ETL Operator**: System administrator or DevOps engineer running ETL pipelines (Airflow, cron, dbt). Does not interact with the ss-tools UI directly — API only.
- **ss-tools Administrator**: Platform administrator managing maintenance mode settings through the web UI.
- **Dashboard Viewer**: Business user viewing dashboards in Superset. Sees the maintenance banner but does not trigger it.
* **What is their goal?**:
- Operator: Automatically warn dashboard users that data is being refreshed and may be inaccurate.
- Administrator: Control the maintenance process, manually remove banners if needed, configure exclusions.
- Dashboard viewer: Understand why dashboard data appears incomplete.
* **Context**:
- Operator calls REST API from a pipeline (Airflow DAG, shell script, CI/CD).
- Administrator works in the ss-tools web interface (SvelteKit SPA, desktop).
- Dashboard viewer sees the result in Superset (third-party web application).
## 2. The "Happy Path" Narrative
**Maintenance start (Operator → ss-tools → Superset)**
An Airflow DAG finishes clearing staging tables and triggers: `curl -X POST https://ss-tools/api/maintenance/start -d '{"tables":["raw.sales","raw.inventory"],"start_time":"2026-05-21T22:00:00+03:00","end_time":"2026-05-22T02:00:00+03:00"}'`. The API immediately returns `{"task_id":"task-abc","maintenance_id":"m-abc123","status":"pending"}`. The operator polls `GET /api/tasks/task-abc`. Within 60 seconds, the task completes, and 12 dashboards in Superset display an orange banner at the very top. If the operator doesn't know the end time, they can omit `end_time` — the banner will show «End: TBD» instead of a specific time. Dashboard viewers see the warning immediately upon opening the page.
**Maintenance end (Operator → ss-tools → Superset)**
The ETL pipeline completes successfully. The Airflow DAG calls `curl -X POST https://ss-tools/api/maintenance/m-abc123/end`. The API returns `{"task_id":"task-end-abc","status":"pending"}`. Upon task completion, banners disappear from all dashboards. In the ss-tools UI, the administrator sees the event marked as "Completed".
## 3. API Key Management UX Flow
**Location:** Settings → System Settings → "API Keys" section (NOT in Maintenance panel).
API keys are centralized infrastructure, reusable across tools. They live in the System Settings page alongside other cross-cutting configurations (environments, auth, etc.). Maintenance is just the first consumer.
### 3.1 Issuing a New API Key
**Step 1: Navigate to Settings → System → API Keys**
The System Settings page contains an "API Keys" section showing a table of existing keys (or empty state if none):
```
┌─ API Keys ─────────────────────────────────────────────────┐
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Name Prefix Env Permissions Actv │ │
│ │─────────────────────────────────────────────────────│ │
│ │ Airflow ETL ssk_M7xqaP… ss-dev start,end 🟢 ✕ │ │
│ │ GitLab CI ssk_K9yFbZ… ss-dev s/e/a 🟢 ✕ │ │
│ │ Old staging ssk_T1vLmC… ss-dev start 🔴 · │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ No API keys yet. Generate one for external tool access. │ ← empty state
│ │
│ [+ Generate New Key] │
└──────────────────────────────────────────────────────────────┘
```
- **Permission notation:** `s` = maintenance:start, `e` = maintenance:end, `a` = maintenance:end_all
- **Status:** 🟢 = active, 🔴 = revoked
- **Revoke button:** `✕` on active keys; `·` (dot — no action) on revoked keys
- **Empty state:** dashed border, grey text, centered
**Step 2: Fill in the generation form**
Clicking `[+ Generate New Key]` expands an inline form (not a modal — matches the existing settings pattern):
```
┌─ Generate API Key ──────────────────────────────────────┐
│ │
│ Name ────────────────────────────────────────────────── │
│ ┌───────────────────────────────────────────────────┐ │
│ │ Airflow Production ETL │ │
│ └───────────────────────────────────────────────────┘ │
│ │
│ Environment ────────────────────────────────────────── │
│ ┌──────────────────────────────────────▼─────────────┐ │
│ │ ss-dev │ │
│ └────────────────────────────────────────────────────┘ │
│ │
│ Permissions ────────────────────────────────────────── │
│ ☑ maintenance:start Start new maintenance events │
│ ☑ maintenance:end End a specific event │
│ ☐ maintenance:end_all End ALL active events at once │
│ │
│ Expires (optional) ─────────────────────────────────── │
│ ┌──────────────────────┐ 📅 │
│ │ 2027-01-01 │ │
│ └──────────────────────┘ │
│ │
│ [Cancel] [Generate Key] │
└───────────────────────────────────────────────────────────┘
```
**Step 3: Copy the raw key (ONCE!)**
After successful generation, the form is replaced by a **read-once reveal panel**:
```
┌─ ⚠️ API Key Generated — Copy It Now! ───────────────────┐
│ │
│ This is the ONLY time the full key will be displayed. │
│ Store it securely (password manager, CI/CD variable). │
│ It CANNOT be retrieved later. If lost, you must │
│ revoke this key and generate a new one. │
│ │
│ ┌───────────────────────────────────────────────────┐ │
│ │ ssk_M7xqaP2zL9vR4nF8kC1bH5jT6dW0yA3 │ │
│ │ [📋 Copy] │ │
│ └───────────────────────────────────────────────────┘ │
│ │
│ Name: Airflow Production ETL │
│ Prefix: ssk_M7xqaP… │
│ Environment: ss-dev │
│ Permissions: maintenance:start, maintenance:end │
│ Created: 2026-05-25 19:45 UTC │
│ Expires: 2027-01-01 │
│ │
│ [I have copied the key — dismiss] │
└───────────────────────────────────────────────────────────┘
```
**UX rules for the reveal panel:**
- Warning text in **red/bold** — user must understand this is one-time
- Key displayed in monospace `<pre>` with a visual "code block" background
- `[📋 Copy]` button copies to clipboard; on click → shows "Copied!" for 1.5s (green checkmark icon)
- `[I have copied the key — dismiss]` — replaces the panel with the updated key list
- Panel CANNOT be reopened — the raw key is gone from memory
- If user closes without copying: key is irrecoverable. They must revoke and regenerate. This is intentional.
**Step 4: Key appears in the list**
After dismissal, the key list is updated with the new entry (prefix only):
```
Name Prefix Env Perms Status
─────────────────────────────────────────────────────────
Airflow Production ETL ssk_M7xqaP… ss-dev s/e 🟢 ✕ ← NEW
GitLab CI/CD ssk_K9yFbZ… ss-dev s/e/a 🟢 ✕
```
### 3.2 Revoking an API Key
**Step 1: Click `✕` on a key row**
Triggers a confirmation dialog (native `confirm()` or inline confirmation):
```
┌─ Revoke API Key? ───────────────────────────────────────┐
│ │
│ Are you sure you want to revoke: │
│ │
│ "Airflow Production ETL" (ssk_M7xqaP…) │
│ │
│ → All external tools using this key will immediately │
│ receive 401 Unauthorized responses. │
│ │
│ → The key entry will be preserved for audit but │
│ marked as INACTIVE. │
│ │
│ [Cancel] [Revoke Key] │
└──────────────────────────────────────────────────────────┘
```
**Step 2: Key becomes inactive**
After confirmation, the row updates:
```
Airflow Production ETL ssk_M7xqaP… ss-dev s/e 🔴 · ← REVOKED
```
- `🔴` = inactive status
- `✕` replaced by `·` (no action — already revoked)
- Row is slightly greyed out (opacity 0.6)
### 3.3 State Machine
```
┌──────────────┐
│ IDLE │←────────────────────────────┐
│ (key list) │ │
└──────┬───────┘ │
│ │
[+Generate] │ │
▼ │
┌──────────────┐ submit form ┌─────────┐│
│ FORM │ ────────────────→ │ REVEAL ││
│ (name,env, │ │ ⚠️ once ││
│ perm,expiry) │ └────┬─────┘│
└──────┬───────┘ │ │
│ [Cancel] │ │
└─────────────────────────────────┤ │
│dismiss│
▼ │
┌──────────────┐ click [✕] ┌──────────┐ │
│ IDLE │ ───────────────→ │ CONFIRM │ │
│ (list updated)│ │ "Revoke?" │ │
└──────────────┘ └─────┬─────┘ │
↑ │ │
│ [Cancel] │ │
└─────────────────────────────────┤ │
│[Revoke]│
▼ │
┌──────────────┐ │
│ IDLE │─┘
│ (key.inactive) │
└──────────────┘
```
### 3.4 Error & Edge Case Handling
| Scenario | UX Response |
|----------|-------------|
| Network failure during generation | Toast error: "Failed to generate API key. Check your connection and try again." Form stays open — retry possible. |
| User closes REVEAL panel without copying | Key is **gone forever**. Admin must revoke (now-inactive key) and regenerate. The warning text in the panel explicitly states this. |
| Double-click on [Revoke] | Second click → 404 from API. UI handles: "Key already revoked" inline message on the row. No dialog. |
| Revoke already-inactive key | UI prevents: no ✕ button on inactive rows (rendered as `·`). |
| Empty name field submission | Client-side validation: "Name is required" red text below the input. [Generate Key] button disabled until filled. |
| No permissions selected | Client-side validation: "Select at least one permission" red text. Button disabled. |
| Key generation API returns 403 | (Admin-only endpoint). If somehow triggered by non-admin: Toast "Forbidden: admin privileges required." |
### 3.5 Component States
| Component | State | Visual |
|-----------|-------|--------|
| Key list | **idle** | Table with rows |
| Key list | **empty** | Dashed border box: "No API keys yet. Generate one for external tool access." + button |
| Key list | **loading** | 3 skeleton rows (`animate-pulse bg-gray-200 h-8 w-full rounded`) |
| Key list | **error** | Toast with error message; list shows stale data |
| Generation form | **idle** | Collapsed — only `[+ Generate New Key]` visible |
| Generation form | **open** | Form fields expanded |
| Generation form | **submitting** | Button shows spinner: `[⠋ Generating...]` |
| Generation form | **error** | Toast with error; form stays open |
| Reveal panel | **visible** | Warning + code block + copy button |
| Reveal panel | **copied** | Copy button shows "✅ Copied!" for 1.5s |
| Revoke dialog | **confirming** | Button shows `[⠋ Revoking...]` |
| Row | **active** | 🟢 green circle + ✕ button |
| Row | **inactive** | 🔴 red circle + `·` dot (no button) |
| Row | **revoking** | 🔄 spinner replacing the ✕ button |
### 3.6 Accessibility
- All buttons have `aria-label` (e.g., `"Revoke Airflow Production ETL key"`)
- Reveal panel uses `role="alert"` for screen reader announcement
- Copy button announces "Copied" via `aria-live="polite"`
- Form fields have associated `<label>` elements
- Confirmation dialog is focus-trapped while open
- Key display uses `<code>` element for screen reader pronunciation of the key string
## 4. Interaction Flows
*(existing sections 4 through end remain unchanged)*
### 4.1 Maintenance Start (External Tool via API Key)
**Using API Key authentication:**
```bash
# 1. Admin generates key once (via Settings → System → API Keys)
# 2. Admin stores key in CI/CD variable: MAINTENANCE_API_KEY=ssk_M7xqa...
# 3. External tool uses X-API-Key header
curl -X POST https://ss-tools.example.com/api/maintenance/start \
-H "X-API-Key: $MAINTENANCE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"tables": ["public.messages"],
"start_time": "2026-05-25T22:00:00Z",
"end_time": "2026-05-26T02:00:00Z",
"environment_id": "ss-dev",
"message": "Scheduled ETL refresh"
}'
# Response: {"task_id":"abc123","maintenance_id":"m-xyz","status":"pending"}
```
**Auth decision flow:**
1. Request arrives with `X-API-Key: ssk_...`
2. System checks `Authorization: Bearer <jwt>` — absent
3. System resolves API key: hash incoming value, lookup in DB by `key_hash`
4. Key found, `active=true`, `expires_at` not past → APIKeyPrincipal created
5. Permissions checked: key has `maintenance:start` → allowed
6. Task dispatched; audit log records `"actor": "api_key: Airflow Production ETL"`
**If auth fails:**
- Invalid key → `401 {"detail":"Invalid API key"}`
- Expired key → `401 {"detail":"API key has expired"}`
- Revoked key → `401 {"detail":"API key has been revoked"}`
- Insufficient permission → `403 {"detail":"API key lacks maintenance:start permission"}`
- Environment mismatch → `400 {"detail":"API key restricted to ss-dev, got ss-prod"}`
### 4.2 Banner Appearance in Superset
*(existing content unchanged — banner rendering details)*
### 4.3 Maintenance End
*(existing content unchanged)*
### 4.4 Dashboard Hub Indicator
*(existing content unchanged)*
### 4.5 Error Recovery
*(existing content unchanged — add API key error path)*
**API Key authentication errors:**
- **401 Invalid key**: External tool should check that `$MAINTENANCE_API_KEY` matches the current key. If key was rotated, update the CI/CD variable.
- **401 Expired key**: Admin must generate a new key (Settings → API Keys → Generate). Update CI/CD variable.
- **403 Permission denied**: Admin must update the key's permissions. Or generate a new key with the required rights.
- **400 Environment mismatch**: The key is scoped to a specific env. Either change the key's environment or use a different key.
## 5. Visual Design
### 5.1 API Key Section (in MaintenanceSettingsPanel)
- Section title: `<details><summary>` collapsible, matching other settings sections
- Key list table: inline Tailwind table (`min-w-full divide-y`), matches EventsTable pattern
- Prefix column: `<code>` element, monospace font (`font-mono text-sm`)
- Status: `<span class="inline-block w-2 h-2 rounded-full">` green (`bg-green-500`) or red (`bg-red-400`)
- Revoke button: `<button class="text-red-500 hover:text-red-700">✕</button>`
- Generate button: `<Button variant="secondary" size="sm">` from `$lib/ui/Button.svelte`
### 5.2 Reveal Panel (Read-Once Key Display)
- Background: yellow-50 border yellow-400 (warning style)
- Warning icon: `⚠️` before title
- Key code block: `<pre class="bg-gray-900 text-green-400 p-4 rounded font-mono text-sm break-all">`
- Copy button: positioned overlapping the code block (top-right corner)
- "Copied!" indicator: absolute positioned, green text, `transition-opacity duration-300`
### 5.3 Revoke Confirmation
- Inline expand panel below the key row (not a modal)
- Red-tinted background (`bg-red-50 border-red-200`)
- Two buttons: `[Cancel]` (grey) and `[Revoke Key]` (red outline)
## 6. Localization
API key UI strings (EN/RU):
| EN | RU |
|----|----|
| API Keys | API-ключи |
| Generate New Key | Создать новый ключ |
| Revoke Key | Отозвать ключ |
| Copy to clipboard | Скопировать |
| Copied! | Скопировано! |
| This is the ONLY time the key will be shown | Ключ показывается ЕДИНСТВЕННЫЙ раз |
| Key has been revoked | Ключ отозван |
| Invalid API key | Неверный API-ключ |
| API key has expired | API-ключ истёк |
| API key lacks {permission} permission | API-ключ не имеет права {permission} |