Release 0.11.0: trust device for 30 days
This commit is contained in:
@@ -72,6 +72,7 @@ import {
|
||||
checkPasscode,
|
||||
verifyPasscode,
|
||||
setPasscode as apiSetPasscode,
|
||||
startDeviceTrust,
|
||||
} from '../api'
|
||||
|
||||
export default function Login() {
|
||||
@@ -84,6 +85,13 @@ export default function Login() {
|
||||
const [code, setCode] = useState('')
|
||||
const [passcode, setPasscode] = useState('')
|
||||
const [newPasscode, setNewPasscode] = useState('')
|
||||
// v0.11.0 — "trust this device for 30 days" checkbox, shared by the
|
||||
// OTC and passcode verify steps. The flag rides on the verify POST;
|
||||
// a checked box mints a device-trust row server-side and sets the
|
||||
// long-lived `rfc_device_trust` cookie. Defaults off so the user
|
||||
// makes an explicit choice — auth credentials shouldn't persist by
|
||||
// default.
|
||||
const [trustDevice, setTrustDevice] = useState(false)
|
||||
// v0.8.0 — capture-profile fields.
|
||||
const [firstName, setFirstName] = useState('')
|
||||
const [lastName, setLastName] = useState('')
|
||||
@@ -105,6 +113,28 @@ export default function Login() {
|
||||
else if (step === 'set-passcode') newPasscodeRef.current?.focus()
|
||||
}, [step])
|
||||
|
||||
// v0.11.0 — on mount, try the device-trust cookie path. If the
|
||||
// browser still carries a valid `rfc_device_trust` cookie from a
|
||||
// prior "trust this device" gesture, the server re-establishes the
|
||||
// session without any user input and we redirect home. The cookie
|
||||
// is HttpOnly so we can't peek at it; we just call the endpoint and
|
||||
// see whether it returns 200. 401 (no cookie / invalid / revoked)
|
||||
// is the structural-silent case — the user proceeds to the email
|
||||
// step normally. We do not surface any UI about the attempt; a
|
||||
// failure should be invisible.
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
;(async () => {
|
||||
try {
|
||||
await startDeviceTrust()
|
||||
if (!cancelled) window.location.assign('/')
|
||||
} catch (_) {
|
||||
// No trusted device — fall through to the email step.
|
||||
}
|
||||
})()
|
||||
return () => { cancelled = true }
|
||||
}, [])
|
||||
|
||||
async function submitEmail(e) {
|
||||
e.preventDefault()
|
||||
if (!email.trim() || !email.includes('@')) {
|
||||
@@ -143,7 +173,7 @@ export default function Login() {
|
||||
setBusy(true)
|
||||
setStatus('')
|
||||
try {
|
||||
await verifyPasscode(email.trim(), passcode.trim())
|
||||
await verifyPasscode(email.trim(), passcode.trim(), { trustDevice })
|
||||
// Reload so App.jsx's getMe() picks up the fresh session. A
|
||||
// returning passcode user is by definition already past the
|
||||
// §6.1 capture step (they couldn't have set a passcode while
|
||||
@@ -189,7 +219,7 @@ export default function Login() {
|
||||
setBusy(true)
|
||||
setStatus('')
|
||||
try {
|
||||
await verifyOtc(email.trim(), code.trim())
|
||||
await verifyOtc(email.trim(), code.trim(), { trustDevice })
|
||||
// OTC verified — the server has signed in the user. Fetch the
|
||||
// canonical /api/auth/me to decide where to land:
|
||||
// * needs_profile → §6.1 capture (then /beta-pending).
|
||||
@@ -378,6 +408,19 @@ export default function Login() {
|
||||
required
|
||||
disabled={busy}
|
||||
/>
|
||||
{/* v0.11.0 — trust device for 30 days. The checkbox lives
|
||||
on the verify step so the user makes the trust gesture
|
||||
in the same breath as signing in. Off by default; the
|
||||
user opts in deliberately. */}
|
||||
<label className="otc-trust-device">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={trustDevice}
|
||||
onChange={e => setTrustDevice(e.target.checked)}
|
||||
disabled={busy}
|
||||
/>
|
||||
<span>Trust this device for 30 days</span>
|
||||
</label>
|
||||
<div className="otc-actions">
|
||||
<button type="submit" disabled={busy || !passcode.trim()}>
|
||||
{busy ? 'Signing in…' : 'Sign in'}
|
||||
@@ -420,6 +463,17 @@ export default function Login() {
|
||||
required
|
||||
disabled={busy}
|
||||
/>
|
||||
{/* v0.11.0 — trust device for 30 days. Same shape as the
|
||||
passcode step; the user opts in deliberately. */}
|
||||
<label className="otc-trust-device">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={trustDevice}
|
||||
onChange={e => setTrustDevice(e.target.checked)}
|
||||
disabled={busy}
|
||||
/>
|
||||
<span>Trust this device for 30 days</span>
|
||||
</label>
|
||||
<div className="otc-actions">
|
||||
<button type="submit" disabled={busy || code.length !== 6}>
|
||||
{busy ? 'Signing in…' : 'Sign in'}
|
||||
|
||||
Reference in New Issue
Block a user