actions/pairing.ts (Server Actions, volgt docs/patterns/server-action.md): - getPairingForApproval(pairingId, mobileSecret): auth + Zod + lookup + status + expiry + verifyToken-check; retourneert UA/IP/username voor de bevestigingspagina. Demo MAG aanroepen (read-only). - approvePairing: zelfde checks PLUS demo-blokkade (session.isDemo). Update status pending→approved, zet user_id + approved_at, bumpt expires_at +5min. Postgres-trigger emit pg_notify automatisch — desktop-SSE pikt het op. - cancelPairing: status pending→cancelled. Demo mag annuleren. - Tagged-union return-type uit loadPendingPairing voor schone discriminatie. app/(app)/m/pair/page.tsx (Server Component, achter (app)/layout-guard): - Geen searchParams uitlezen — page leest URL niet. Alleen statische uitleg + PairConfirmation client-island. app/(app)/m/pair/pair-confirmation.tsx (Client Component): - useEffect parseert window.location.hash voor #id=…&s=… (server ziet de fragment nooit) - Roept getPairingForApproval om UA/IP/username op te halen - Toont kaart "Inloggen als <username> op dit apparaat?" met UA + IP + expliciete waarschuwing tegen phishing-QR; Bevestig/Annuleer-knoppen - Na approve: window.history.replaceState wist de hash zodat back/forward de secret niet meer onthult; transitioneert naar success-state - queueMicrotask voor synchrone setState om React-Compiler "cascading renders" warning te vermijden Tests __tests__/actions/pairing.test.ts (11 cases): - getPairingForApproval: ok + 5 fail-paths (geen sessie, approved, verlopen, verkeerd secret, ongeldige cuid) - approvePairing: happy + demo-block + verkeerd secret (geen DB-write) - cancelPairing: happy + demo mag annuleren Quality gates: lint 0 errors, tsc clean, vitest 132/132. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
26 lines
925 B
TypeScript
26 lines
925 B
TypeScript
// ST-1005: Mobiele bevestigingspagina voor de QR-pairing-flow (M10).
|
|
//
|
|
// Server Component achter de bestaande (app)/layout.tsx auth-guard — onbekende
|
|
// mobielen worden eerst naar /login gestuurd. Bewust géén searchParams
|
|
// uitlezen: het mobileSecret zit in het URL-fragment (#id=…&s=…), wat alleen
|
|
// client-side leesbaar is. De Client Component PairConfirmation parseert
|
|
// location.hash en doet de Server Action-calls.
|
|
|
|
import { PairConfirmation } from './pair-confirmation'
|
|
|
|
export const metadata = {
|
|
title: 'Inloggen op desktop',
|
|
}
|
|
|
|
export default function PairPage() {
|
|
return (
|
|
<main className="container mx-auto max-w-md py-12">
|
|
<h1 className="text-2xl font-semibold">Inloggen op desktop</h1>
|
|
<p className="text-muted-foreground mt-2">
|
|
Bevestig hieronder dat je wilt inloggen op het apparaat dat de QR-code
|
|
toont.
|
|
</p>
|
|
<PairConfirmation />
|
|
</main>
|
|
)
|
|
}
|