fix(M10): close pair/stream race + demo-block on cancelPairing
Twee P1's uit code-review:
(1) pair/stream race: de findUnique die de pairing-status leest gebeurde vóór
LISTEN actief was. Als de mobiel approvet tussen die query en LISTEN: pg_notify
fired in dat venster gaat verloren (Postgres queuet niet voor abonnees die
nog niet listen) én was de eerder gelezen status stale. De catch-up state-
event emitte dus 'pending' terwijl de DB inmiddels 'approved' was, en de
desktop bleef hangen tot expiry.
Tweede findUnique toegevoegd ná LISTEN actief is: het venster sluit, omdat
elke approve na dat punt via de notify-handler doorkomt. Aanvullend op de
eerdere client-side fix die 'state' events nu ook routeert (commit d6e71f9).
(2) cancelPairing demo-block: cancel was een DB-write zonder demo-guard,
in tegenspraak met de "demo = 403 op writes"-regel. Demo-blokkade
toegevoegd; bestaande test omgedraaid naar 'wordt geblokkeerd, geen DB-write'.
Quality gates: lint 0 errors, tsc clean, vitest 139/139.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d6e71f915c
commit
a9616ff122
3 changed files with 22 additions and 11 deletions
|
|
@ -159,13 +159,12 @@ describe('actions/pairing', () => {
|
|||
expect(arg.data.status).toBe('cancelled')
|
||||
})
|
||||
|
||||
it('demo-user mag annuleren', async () => {
|
||||
it('demo-user wordt geblokkeerd, geen DB-write', async () => {
|
||||
mockGetSession.mockResolvedValue(SESSION_DEMO)
|
||||
mockPrisma.loginPairing.findUnique.mockResolvedValue(pendingPairing())
|
||||
mockPrisma.loginPairing.update.mockResolvedValue({})
|
||||
|
||||
const res = await cancelPairing(VALID_PAIRING_ID, VALID_SECRET)
|
||||
expect(res).toEqual({ ok: true })
|
||||
expect(res).toEqual({ ok: false, error: 'Niet beschikbaar in demo-modus' })
|
||||
expect(mockPrisma.loginPairing.findUnique).not.toHaveBeenCalled()
|
||||
expect(mockPrisma.loginPairing.update).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -126,6 +126,8 @@ export async function cancelPairing(
|
|||
): Promise<{ ok: true } | ActionFail> {
|
||||
const session = await getSession()
|
||||
if (!session.userId) return { ok: false, error: 'Niet ingelogd' }
|
||||
// Cancel is een DB-write — onder de demo-write-block-regel.
|
||||
if (session.isDemo) return { ok: false, error: 'Niet beschikbaar in demo-modus' }
|
||||
|
||||
const parsed = inputSchema.safeParse({ pairingId, mobileSecret })
|
||||
if (!parsed.success) return { ok: false, error: 'Ongeldige invoer' }
|
||||
|
|
|
|||
|
|
@ -152,17 +152,27 @@ export async function GET(
|
|||
cleanup('pg error')
|
||||
})
|
||||
|
||||
// Initial state — voorkomt race waarbij approve net vóór SSE-open valt
|
||||
// Initial state — dicht de race tussen pair/start en SSE-open. De
|
||||
// *eerste* findUnique (voor cookie-validatie) gebeurde vóór LISTEN
|
||||
// actief was; als de mobiel tussen die query en LISTEN approvet is
|
||||
// de pg_notify verloren (Postgres queuet niet) én is de eerder
|
||||
// gelezen status stale. Lees daarom de status hier opnieuw — nu LISTEN
|
||||
// wel actief is, dus alle approvals na dit punt komen via de notify-
|
||||
// handler door.
|
||||
const fresh = await prisma.loginPairing.findUnique({
|
||||
where: { id: pairingId },
|
||||
select: { status: true },
|
||||
})
|
||||
const currentStatus = fresh?.status ?? pairing.status
|
||||
|
||||
enqueue(
|
||||
`event: state\ndata: ${JSON.stringify({
|
||||
pairing_id: pairingId,
|
||||
status: pairing.status,
|
||||
status: currentStatus,
|
||||
})}\n\n`,
|
||||
)
|
||||
// Pairing was misschien al consumed/cancelled tussen findUnique en LISTEN —
|
||||
// sluit de stream meteen.
|
||||
if (TERMINAL_STATUSES.has(pairing.status)) {
|
||||
await cleanup(`already-${pairing.status}`)
|
||||
if (TERMINAL_STATUSES.has(currentStatus)) {
|
||||
await cleanup(`already-${currentStatus}`)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue