work-in-progress atproto PDS
typescript atproto pds atcute
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor: remember me

Mary d50f185d 5edada64

+71 -67
+1
packages/danaus/drizzle/accounts/20260117063532_minor_golden_guardian/migration.sql packages/danaus/drizzle/accounts/20260117065047_stormy_marvel_zombies/migration.sql
··· 85 85 `token` text PRIMARY KEY, 86 86 `did` text NOT NULL, 87 87 `session_id` text, 88 + `remember` integer DEFAULT false NOT NULL, 88 89 `webauthn_challenge` text, 89 90 `created_at` integer NOT NULL, 90 91 `expires_at` integer NOT NULL,
+11 -1
packages/danaus/drizzle/accounts/20260117063532_minor_golden_guardian/snapshot.json packages/danaus/drizzle/accounts/20260117065047_stormy_marvel_zombies/snapshot.json
··· 1 1 { 2 2 "version": "7", 3 3 "dialect": "sqlite", 4 - "id": "932710cc-55eb-4151-8b21-3135d4bb9a08", 4 + "id": "a52583cf-1f20-4ce8-bc86-96528e2d13e1", 5 5 "prevIds": [ 6 6 "00000000-0000-0000-0000-000000000000" 7 7 ], ··· 541 541 "default": null, 542 542 "generated": null, 543 543 "name": "session_id", 544 + "entityType": "columns", 545 + "table": "verify_challenge" 546 + }, 547 + { 548 + "type": "integer", 549 + "notNull": true, 550 + "autoincrement": false, 551 + "default": "false", 552 + "generated": null, 553 + "name": "remember", 544 554 "entityType": "columns", 545 555 "table": "verify_challenge" 546 556 },
+3
packages/danaus/src/accounts/db/schema.ts
··· 242 242 /** session to elevate (null = MFA login, creates new session) */ 243 243 session_id: text().references(() => webSession.id, { onDelete: 'cascade' }), 244 244 245 + /** remember me preference from login form (MFA login only) */ 246 + remember: integer({ mode: 'boolean' }).notNull().default(false), 247 + 245 248 /** WebAuthn challenge (base64url) for authentication */ 246 249 webauthn_challenge: text(), 247 250
+3 -1
packages/danaus/src/accounts/manager.ts
··· 1374 1374 /** 1375 1375 * create a verification challenge for MFA login (no session, creates one on success). 1376 1376 * @param did account did 1377 + * @param remember whether to create a long-lived session on success 1377 1378 * @returns token for the verify page 1378 1379 */ 1379 - createVerifyChallenge(did: Did): string { 1380 + createVerifyChallenge(did: Did, remember: boolean): string { 1380 1381 const token = nanoid(32); 1381 1382 const now = new Date(); 1382 1383 const expiresAt = new Date(now.getTime() + MFA_CHALLENGE_TTL_MS); ··· 1387 1388 token: token, 1388 1389 did: did, 1389 1390 session_id: null, 1391 + remember: remember, 1390 1392 created_at: now, 1391 1393 expires_at: expiresAt, 1392 1394 })
+18 -27
packages/danaus/src/web/controllers/account/security/webauthn/lib/forms.ts
··· 19 19 options: Awaited<ReturnType<typeof generateWebAuthnRegistrationOptions>>; 20 20 } 21 21 22 - // valibot schema for WebAuthn registration response 23 - const authenticatorTransportSchema = v.picklist([ 24 - 'ble', 25 - 'cable', 26 - 'hybrid', 27 - 'internal', 28 - 'nfc', 29 - 'smart-card', 30 - 'usb', 31 - ]); 32 - 33 - const authenticatorAttachmentSchema = v.picklist(['cross-platform', 'platform']); 34 - 35 - const registrationResponseSchema = v.object({ 36 - id: v.string(), 37 - rawId: v.string(), 38 - type: v.literal('public-key'), 39 - response: v.object({ 40 - clientDataJSON: v.string(), 41 - attestationObject: v.string(), 42 - transports: v.optional(v.array(authenticatorTransportSchema)), 43 - }), 44 - clientExtensionResults: v.record(v.string(), v.unknown()), 45 - authenticatorAttachment: v.optional(authenticatorAttachmentSchema), 46 - }); 47 - 48 22 /** 49 23 * initiates WebAuthn registration by generating a challenge. 50 24 * @param did account DID ··· 80 54 v.object({ 81 55 token: v.pipe(v.string(), v.minLength(1)), 82 56 name: v.optional(v.pipe(v.string(), normalizeWhitespace, v.maxLength(32, `Name is too long`))), 83 - response: v.pipe(v.string(), v.minLength(1), v.parseJson(), registrationResponseSchema), 57 + response: v.pipe( 58 + v.string(), 59 + v.parseJson(), 60 + v.object({ 61 + id: v.string(), 62 + rawId: v.string(), 63 + type: v.literal('public-key'), 64 + response: v.object({ 65 + clientDataJSON: v.string(), 66 + attestationObject: v.string(), 67 + transports: v.optional( 68 + v.array(v.picklist(['ble', 'cable', 'hybrid', 'internal', 'nfc', 'smart-card', 'usb'])), 69 + ), 70 + }), 71 + clientExtensionResults: v.record(v.string(), v.unknown()), 72 + authenticatorAttachment: v.optional(v.picklist(['cross-platform', 'platform'])), 73 + }), 74 + ), 84 75 }), 85 76 async (data, issue) => { 86 77 const { accountManager, config } = getAppContext();
+5
packages/danaus/src/web/controllers/login.tsx
··· 4 4 5 5 import { BaseLayout } from '#web/layouts/base.tsx'; 6 6 import Button from '#web/primitives/button.tsx'; 7 + import Checkbox from '#web/primitives/checkbox.tsx'; 7 8 import Field from '#web/primitives/field.tsx'; 8 9 import Input from '#web/primitives/input.tsx'; 9 10 import { routes } from '#web/routes.ts'; ··· 49 50 > 50 51 <Input {...fields._password.as('password')} autocomplete="current-password" required /> 51 52 </Field> 53 + 54 + <Checkbox name="remember" value="true"> 55 + Remember this device 56 + </Checkbox> 52 57 53 58 <Button type="submit" variant="primary"> 54 59 Sign in
+30 -30
packages/danaus/src/web/controllers/login/lib/forms.ts
··· 130 130 // check if MFA is enabled 131 131 if (accountManager.getMfaStatus(account.did) !== null) { 132 132 // create verify challenge and redirect 133 - const token = accountManager.createVerifyChallenge(account.did); 133 + const token = accountManager.createVerifyChallenge(account.did, data.remember ?? false); 134 134 135 135 redirect(routes.verify.index.href(undefined, { token, redirect: data.redirect })); 136 136 } ··· 161 161 challenge: v.string(), 162 162 factor: v.picklist<AuthFactor[]>(['totp', 'recovery', 'password']), 163 163 _code: v.string(), 164 - remember: v.optional(v.boolean(), false), 165 164 redirect: v.string(), 166 165 }), 167 166 async (data) => { ··· 199 198 accountManager.elevateSession(challenge.session_id!); 200 199 redirect(data.redirect); 201 200 } else { 202 - // MFA login: create new session 201 + // MFA login: create new session using remember preference from login 203 202 const { session, token } = await accountManager.createWebSession({ 204 203 did: challenge.did, 205 - remember: data.remember ?? false, 204 + remember: challenge.remember, 206 205 userAgent: request.headers.get('user-agent') ?? undefined, 207 206 }); 208 207 ··· 218 217 }, 219 218 ); 220 219 221 - const authenticationResponseSchema = v.object({ 222 - id: v.string(), 223 - rawId: v.string(), 224 - response: v.object({ 225 - clientDataJSON: v.string(), 226 - authenticatorData: v.string(), 227 - signature: v.string(), 228 - userHandle: v.optional(v.string()), 229 - }), 230 - authenticatorAttachment: v.optional(v.picklist(['cross-platform', 'platform'])), 231 - clientExtensionResults: v.object({ 232 - appid: v.optional(v.boolean()), 233 - credProps: v.optional( 234 - v.object({ 235 - rk: v.optional(v.boolean()), 236 - }), 237 - ), 238 - hmacCreateSecret: v.optional(v.boolean()), 239 - }), 240 - type: v.literal('public-key'), 241 - }); 242 - 243 220 export const verifyWebAuthnForm = form( 244 221 v.object({ 245 222 challenge: v.string(), 246 - response: v.pipe(v.string(), v.minLength(1), v.parseJson(), authenticationResponseSchema), 247 - remember: v.optional(v.boolean(), false), 223 + response: v.pipe( 224 + v.string(), 225 + v.parseJson(), 226 + v.object({ 227 + id: v.string(), 228 + rawId: v.string(), 229 + response: v.object({ 230 + clientDataJSON: v.string(), 231 + authenticatorData: v.string(), 232 + signature: v.string(), 233 + userHandle: v.optional(v.string()), 234 + }), 235 + authenticatorAttachment: v.optional(v.picklist(['cross-platform', 'platform'])), 236 + clientExtensionResults: v.object({ 237 + appid: v.optional(v.boolean()), 238 + credProps: v.optional( 239 + v.object({ 240 + rk: v.optional(v.boolean()), 241 + }), 242 + ), 243 + hmacCreateSecret: v.optional(v.boolean()), 244 + }), 245 + type: v.literal('public-key'), 246 + }), 247 + ), 248 248 redirect: v.string(), 249 249 }), 250 250 async (data) => { ··· 301 301 accountManager.elevateSession(challenge.session_id!); 302 302 redirect(data.redirect); 303 303 } else { 304 - // MFA login: create new session 304 + // MFA login: create new session using remember preference from login 305 305 const { session, token } = await accountManager.createWebSession({ 306 306 did: challenge.did, 307 - remember: data.remember ?? false, 307 + remember: challenge.remember, 308 308 userAgent: request.headers.get('user-agent') ?? undefined, 309 309 }); 310 310
-8
packages/danaus/src/web/controllers/verify.tsx
··· 16 16 import { getAppContext } from '#web/middlewares/app-context.ts'; 17 17 import { tryGetSession } from '#web/middlewares/session.ts'; 18 18 import Button from '#web/primitives/button.tsx'; 19 - import Checkbox from '#web/primitives/checkbox.tsx'; 20 19 import Field from '#web/primitives/field.tsx'; 21 20 import Input from '#web/primitives/input.tsx'; 22 21 import MenuItem from '#web/primitives/menu-item.tsx'; ··· 361 360 <input {...fields.factor.as('hidden', props.factor)} /> 362 361 363 362 {props.children} 364 - 365 - {/* remember checkbox - only for MFA login, not sudo */} 366 - {!props.isSudo && ( 367 - <Checkbox name="remember" value="true"> 368 - Remember this device for 1 year 369 - </Checkbox> 370 - )} 371 363 372 364 <OtherMethodsMenu 373 365 factor={props.factor}