mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 23:53:05 +00:00
WebAuthn platform passkeys: opt-in enrollment after a password login, a password-less sign-in that mints the same 14-day campaign-token, and revocation from the profile. The session is minted through Payload's public pieces (checkLoginPermission, addSessionToUser, getFieldsToSign, jwtSign) inside a transaction with an advisory lock, so a passkey login neither bypasses a lockout nor evicts the account's other sessions. The rpID is derived from NEXT_PUBLIC_SITE_URL and resolves to null when the request host disagrees, so previews never offer a ceremony the browser would refuse.
65 lines
2.4 KiB
TypeScript
65 lines
2.4 KiB
TypeScript
/**
|
|
* Browser-side memory for the biometric-enrollment prompt (B40). Sibling of
|
|
* `recentVisits.ts` / `campaignGeolocation.ts`: no Payload, no Next, just
|
|
* `localStorage` behind failure-tolerant reads.
|
|
*
|
|
* Two flags, and the split is the whole point: the server knows HOW MANY
|
|
* passkeys an account has, never WHICH device it is being read on. So "already
|
|
* enrolled here" and "stop offering" can only be answered locally, and they are
|
|
* kept in `localStorage` rather than `sessionStorage` — a decision about this
|
|
* phone should outlive the tab (the PWA install toast, which is a suggestion
|
|
* rather than an answer, deliberately uses `sessionStorage` instead).
|
|
*/
|
|
|
|
const CAMPAIGN_BIOMETRICS_ENROLLED_KEY = 'teqo:campaign:biometrics-enrolled'
|
|
/** Exported because the e2e spec pre-sets it instead of hardcoding the string. */
|
|
export const CAMPAIGN_BIOMETRICS_PROMPT_DISMISSED_KEY = 'teqo:campaign:biometrics-prompt-dismissed'
|
|
|
|
const read = (key: string): boolean => {
|
|
if (typeof window === 'undefined') return false
|
|
try {
|
|
return window.localStorage.getItem(key) === '1'
|
|
} catch {
|
|
// localStorage throws in private mode / with storage disabled.
|
|
return false
|
|
}
|
|
}
|
|
|
|
const write = (key: string): void => {
|
|
if (typeof window === 'undefined') return
|
|
try {
|
|
window.localStorage.setItem(key, '1')
|
|
} catch {
|
|
// Ignore quota / private-mode failures: the prompt reappearing is a far
|
|
// smaller problem than a thrown error inside a toast callback.
|
|
}
|
|
}
|
|
|
|
const clear = (key: string): void => {
|
|
if (typeof window === 'undefined') return
|
|
try {
|
|
window.localStorage.removeItem(key)
|
|
} catch {
|
|
// Same tolerance as above.
|
|
}
|
|
}
|
|
|
|
export const wasCampaignBiometricsEnrolledHere = (): boolean =>
|
|
read(CAMPAIGN_BIOMETRICS_ENROLLED_KEY)
|
|
|
|
export const markCampaignBiometricsEnrolledHere = (): void =>
|
|
write(CAMPAIGN_BIOMETRICS_ENROLLED_KEY)
|
|
|
|
/**
|
|
* Called when the account has no passkeys at all: whatever this browser
|
|
* believed about "enrolled here" is stale, and a person who revoked every
|
|
* device should be offered enrollment again.
|
|
*/
|
|
export const forgetCampaignBiometricsEnrollment = (): void =>
|
|
clear(CAMPAIGN_BIOMETRICS_ENROLLED_KEY)
|
|
|
|
export const wasCampaignBiometricsPromptDismissed = (): boolean =>
|
|
read(CAMPAIGN_BIOMETRICS_PROMPT_DISMISSED_KEY)
|
|
|
|
export const dismissCampaignBiometricsPrompt = (): void =>
|
|
write(CAMPAIGN_BIOMETRICS_PROMPT_DISMISSED_KEY)
|