phase: 101_tokens_page_overhaul
All gates verified green — no defects found, nothing to fix. Final report:
## Phase 101 — final verification pass (all tasks already complete; verified, not re-implemented)
**Verified against the working tree** (task files 01–04 present in `complete/`):
- Active/revoked table split (Status column gone, section hidden while 0 revoked, per-table search with query state surviving re-renders) in `frontend/index.html` + `tokens.js`
- `generateToken()` refuses blank/whitespace names (`Give the token a name first.`, no request); `|| "token"` fallback deleted
- Atomic rotation: `regenerate_token` service + `POST /api/tokens/{id}/regenerate` (201 new-plaintext-once / 404 / 409 / router-wide 403) + `TokenListItem.revoked_at` (D5)
- Regenerate two-step confirm UI + CSS (`.token-regenerate`, neutral hover, no new hue); A4 pins intact
**Test / lint / coverage results:**
- `uv run pytest` → **2065 passed**
- `uv run pytest --cov=app --cov-report=term-missing` → **TOTAL 99%** (>90% ✓)
- `uv run ruff check . && uv run pyright` → clean (0 errors)
- `uv run pytest tests/e2e/test_tokens_page.py -v --no-cov` → **4 passed** (isolation, DB up)
- Regression, each in isolation: `test_api_tokens.py` **9 passed**, `test_admin_auth.py` **6 passed**, `test_shared_header.py` **6 passed**, `test_theme_semantic_completion.py` **8 passed** (its revoked-pill pin was correctly re-scoped to the revoked table in this phase)
**Completion criteria:** 1 ✓ split+search (E2E 1–2) · 2 ✓ required name (E2E 3 + source pin) · 3 ✓ rotation end-to-end, old token refused at gate (E2E 4 + API 404/409 pinned) · 4 ✓ A4 holds (list carries no plaintext/hashes) · 5 ✓ suite/coverage/lint green · 6 ✓ E2E + regressions green in isolation · 7 commit left to the harness per executor rules (all changes uncommitted in the working tree)
**Deviations:** none. Next pending phase: `98_sync_summary_visibility`.
This commit is contained in:
+37
-1
@@ -15,7 +15,10 @@ Routes (all under ``/api`` via the ``main`` registration):
|
||||
that carries the plaintext ``token``), ``GET /api/tokens``
|
||||
(``TokenList`` — newest first, secret-free), ``POST
|
||||
/api/tokens/{token_id}/revoke`` (204, idempotent; unknown id → 404
|
||||
``token not found``).
|
||||
``token not found``), and ``POST /api/tokens/{token_id}/regenerate``
|
||||
(201 ``TokenCreated`` — the atomic rotation: the old row is revoked
|
||||
and its successor, same label, is created in ONE transaction; already
|
||||
revoked → 409, unknown id → 404).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -87,6 +90,7 @@ def list_tokens(
|
||||
created_at=row.created_at,
|
||||
last_used_at=row.last_used_at,
|
||||
revoked=row.revoked_at is not None,
|
||||
revoked_at=row.revoked_at, # phase 101 D5: null while active
|
||||
)
|
||||
for row in rows
|
||||
]
|
||||
@@ -110,3 +114,35 @@ def revoke_token(
|
||||
raise HTTPException(status_code=404, detail="token not found")
|
||||
db.commit()
|
||||
return Response(status_code=204)
|
||||
|
||||
|
||||
@router.post("/{token_id}/regenerate", response_model=TokenCreated, status_code=201)
|
||||
def regenerate_token(
|
||||
token_id: uuid.UUID,
|
||||
db: Session = Depends(get_db), # noqa: B008
|
||||
) -> TokenCreated:
|
||||
"""Rotate one active token (201) — the rotation is ONE transaction.
|
||||
|
||||
The old row is stamped ``revoked_at`` and the successor (SAME label,
|
||||
fresh hash) is created in the same commit, so the holder's old token
|
||||
is dead the instant the new one is live. This 201 is the NEW token's
|
||||
ONE plaintext moment (A4 — the same contract as the create endpoint;
|
||||
the old plaintext was already one-shot and is never re-shown).
|
||||
A dead token cannot be rotated: an already-revoked id is a 409
|
||||
``token already revoked``, and an unknown id a 404 ``token not
|
||||
found`` (the revoke endpoint's exact message).
|
||||
"""
|
||||
try:
|
||||
result = token_service.regenerate_token(db, token_id)
|
||||
except token_service.TokenAlreadyRevoked:
|
||||
raise HTTPException(
|
||||
status_code=409, detail="token already revoked"
|
||||
) from None
|
||||
if result is None:
|
||||
raise HTTPException(status_code=404, detail="token not found")
|
||||
new_row, plaintext = result
|
||||
db.commit() # ONE commit: the stamp + the create are atomic
|
||||
db.refresh(new_row) # pulls the server-default created_at
|
||||
return TokenCreated(
|
||||
id=new_row.id, label=new_row.label, token=plaintext, created_at=new_row.created_at
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user