feat(access): add End Session button to re-lock a tap-in session
A Bolt Card tap loads the holder's card for the whole session, so an unattended idle menu is transactable by the next person until the 60s IDLE_LOCK_TIMEOUT fires. Give the holder an explicit re-lock: - END_SESSION event on `idle`, guarded to the active gate, targets `locked` (whose entry already clears the access session + loaded card). No-op on a gate-disabled machine that rests at idle. - endSession() store action; IdleView shows a destructive-styled "End Session" button top-right only while accessControl.enabled. - Tests: END_SESSION re-locks when the gate is active; no-op when off. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018ivBosaWmv8vwFE7ejrdHW
This commit is contained in:
parent
ac96921074
commit
a4e3b2ccc8
5 changed files with 53 additions and 0 deletions
|
|
@ -1639,6 +1639,15 @@ export const useAtmStore = defineStore('atm', () => {
|
||||||
send({ type: 'DEV_UNLOCK' })
|
send({ type: 'DEV_UNLOCK' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* End the current tap-in session on demand and re-lock immediately (drops the
|
||||||
|
* loaded Bolt Card via `locked`'s entry), instead of waiting out the idle
|
||||||
|
* timeout. No-op unless the gate is active and we're on the idle menu.
|
||||||
|
*/
|
||||||
|
function endSession() {
|
||||||
|
send({ type: 'END_SESSION' })
|
||||||
|
}
|
||||||
|
|
||||||
// Convenience methods for common events
|
// Convenience methods for common events
|
||||||
function selectCashIn() {
|
function selectCashIn() {
|
||||||
send({ type: 'SELECT_CASH_IN' })
|
send({ type: 'SELECT_CASH_IN' })
|
||||||
|
|
@ -1799,6 +1808,7 @@ export const useAtmStore = defineStore('atm', () => {
|
||||||
grantAccess,
|
grantAccess,
|
||||||
denyAccess,
|
denyAccess,
|
||||||
devUnlock,
|
devUnlock,
|
||||||
|
endSession,
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
initialize,
|
initialize,
|
||||||
|
|
|
||||||
|
|
@ -183,6 +183,21 @@ function handleCashOut() {
|
||||||
?
|
?
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
<!-- End-session button (top-right) — only while the access gate is engaged.
|
||||||
|
A tap-in loads the holder's Bolt Card for the whole session, so give
|
||||||
|
them an explicit way to re-lock the moment they're done instead of
|
||||||
|
relying on the idle timeout (which would leave the card usable by the
|
||||||
|
next person in the meantime). -->
|
||||||
|
<Button
|
||||||
|
v-if="atmStore.accessControl.enabled"
|
||||||
|
variant="outline"
|
||||||
|
size="kiosk"
|
||||||
|
class="absolute top-4 right-4 lg:top-8 lg:right-8 h-14 min-h-0 gap-2 rounded-full border-2 border-destructive/50 px-4 text-sm font-bold text-destructive lg:h-20 lg:px-8 lg:text-2xl"
|
||||||
|
@click="atmStore.endSession()"
|
||||||
|
>
|
||||||
|
🔒 End Session
|
||||||
|
</Button>
|
||||||
|
|
||||||
<!-- Debug toggle (only visible when debug bar is hidden, never in production) -->
|
<!-- Debug toggle (only visible when debug bar is hidden, never in production) -->
|
||||||
<Button
|
<Button
|
||||||
v-if="!atmStore.debugMode && atmStore.allowMockFallback"
|
v-if="!atmStore.debugMode && atmStore.allowMockFallback"
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,25 @@ describe('ATM access control (ADR-003)', () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('user-initiated end of session', () => {
|
||||||
|
it('END_SESSION re-locks immediately when the gate is active', () => {
|
||||||
|
const actor = createActor(createATMMachine({}, { accessControlEnabled: true }))
|
||||||
|
actor.start()
|
||||||
|
actor.send({ type: 'ACCESS_GRANTED', role: 'user', credentialIdHash: 'abc' })
|
||||||
|
expect(actor.getSnapshot().value).toBe('idle')
|
||||||
|
actor.send({ type: 'END_SESSION' })
|
||||||
|
expect(actor.getSnapshot().value).toBe('locked')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('END_SESSION is a no-op when the gate is disabled (stays at idle)', () => {
|
||||||
|
const actor = createActor(createATMMachine()) // gate off → rests at idle
|
||||||
|
actor.start()
|
||||||
|
expect(actor.getSnapshot().value).toBe('idle')
|
||||||
|
actor.send({ type: 'END_SESSION' })
|
||||||
|
expect(actor.getSnapshot().value).toBe('idle')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('build/dev bypass', () => {
|
describe('build/dev bypass', () => {
|
||||||
it('accessBypassFlag opens the gate even when enabled', () => {
|
it('accessBypassFlag opens the gate even when enabled', () => {
|
||||||
const actor = createActor(
|
const actor = createActor(
|
||||||
|
|
|
||||||
|
|
@ -549,6 +549,11 @@ export function createATMMachine(
|
||||||
target: 'cashOut',
|
target: 'cashOut',
|
||||||
actions: ['setStartTime', 'setCashOutFee'],
|
actions: ['setStartTime', 'setCashOutFee'],
|
||||||
},
|
},
|
||||||
|
// User taps "End session": re-lock now rather than waiting out
|
||||||
|
// IDLE_LOCK_TIMEOUT. Guarded to the active gate so a gate-disabled
|
||||||
|
// machine (which rests at idle) never leaves it via this event.
|
||||||
|
// `locked`'s entry clears the access session + loaded card.
|
||||||
|
END_SESSION: { guard: 'accessGateActive', target: 'locked' },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -179,6 +179,10 @@ export type ATMEvent =
|
||||||
| { type: 'ACCESS_GRANTED'; role: AccessRole; credentialIdHash: string }
|
| { type: 'ACCESS_GRANTED'; role: AccessRole; credentialIdHash: string }
|
||||||
| { type: 'ACCESS_DENIED'; reason: string }
|
| { type: 'ACCESS_DENIED'; reason: string }
|
||||||
| { type: 'DEV_UNLOCK' }
|
| { type: 'DEV_UNLOCK' }
|
||||||
|
// User-initiated end of a tap-in session: re-lock immediately instead of
|
||||||
|
// waiting out IDLE_LOCK_TIMEOUT, so a loaded Bolt Card can't be reused by
|
||||||
|
// the next person the moment its holder steps away.
|
||||||
|
| { type: 'END_SESSION' }
|
||||||
| { type: 'SELECT_AMOUNT'; amount: number }
|
| { type: 'SELECT_AMOUNT'; amount: number }
|
||||||
| { type: 'FINISH_INSERTING' }
|
| { type: 'FINISH_INSERTING' }
|
||||||
| { type: 'USER_SCANNED_NPUB'; npub: string }
|
| { type: 'USER_SCANNED_NPUB'; npub: string }
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue