lewis.diff
755 lines 27 kB view raw
1diff --git a/frontend/src/routes/OAuth2FA.svelte b/frontend/src/routes/OAuth2FA.svelte 2index 2eaf565..a9218e6 100644 3--- a/frontend/src/routes/OAuth2FA.svelte 4+++ b/frontend/src/routes/OAuth2FA.svelte 5@@ -32,7 +32,7 @@ 6 method: 'POST', 7 headers: { 8 'Content-Type': 'application/json', 9- 'Accept': 'application/json' 10+ Accept: 'application/json' 11 }, 12 body: JSON.stringify({ 13 request_uri: requestUri, 14@@ -50,6 +50,11 @@ 15 16 if (data.redirect_uri) { 17 window.location.href = data.redirect_uri 18+ const a = document.createElement('a') 19+ a.href = data.redirect_uri 20+ a.style.display = 'none' 21+ document.body.appendChild(a) 22+ a.click() 23 return 24 } 25 26diff --git a/frontend/src/routes/OAuthAccounts.svelte b/frontend/src/routes/OAuthAccounts.svelte 27index 013b3e9..29289e1 100644 28--- a/frontend/src/routes/OAuthAccounts.svelte 29+++ b/frontend/src/routes/OAuthAccounts.svelte 30@@ -58,7 +58,7 @@ 31 method: 'POST', 32 headers: { 33 'Content-Type': 'application/json', 34- 'Accept': 'application/json' 35+ Accept: 'application/json' 36 }, 37 body: JSON.stringify({ 38 request_uri: requestUri, 39@@ -80,12 +80,19 @@ 40 } 41 42 if (data.needs_2fa) { 43- navigate(routes.oauth2fa, { params: { request_uri: requestUri, channel: data.channel || '' } }) 44+ navigate(routes.oauth2fa, { 45+ params: { request_uri: requestUri, channel: data.channel || '' } 46+ }) 47 return 48 } 49 50 if (data.redirect_uri) { 51 window.location.href = data.redirect_uri 52+ const a = document.createElement('a') 53+ a.href = data.redirect_uri 54+ a.style.display = 'none' 55+ document.body.appendChild(a) 56+ a.click() 57 return 58 } 59 60@@ -128,12 +135,7 @@ 61 62 <div class="accounts-list"> 63 {#each accounts as account} 64- <button 65- type="button" 66- class="account-item" 67- class:disabled={submitting} 68- onclick={() => !submitting && handleSelectAccount(account.did)} 69- > 70+ <button type="button" class="account-item" class:disabled={submitting} onclick={() => !submitting && handleSelectAccount(account.did)}> 71 <div class="account-info"> 72 <span class="account-handle">@{account.handle}</span> 73 <span class="account-email">{account.email}</span> 74@@ -202,7 +204,9 @@ 75 cursor: pointer; 76 text-align: left; 77 width: 100%; 78- transition: border-color var(--transition-fast), box-shadow var(--transition-fast); 79+ transition: 80+ border-color var(--transition-fast), 81+ box-shadow var(--transition-fast); 82 } 83 84 .account-item:hover:not(.disabled) { 85diff --git a/frontend/src/routes/OAuthConsent.svelte b/frontend/src/routes/OAuthConsent.svelte 86index 2123f13..5486456 100644 87--- a/frontend/src/routes/OAuthConsent.svelte 88+++ b/frontend/src/routes/OAuthConsent.svelte 89@@ -57,12 +57,7 @@ 90 const data: ConsentData = await response.json() 91 consentData = data 92 93- scopeSelections = Object.fromEntries( 94- data.scopes.map((scope) => [ 95- scope.scope, 96- scope.required ? true : scope.granted ?? true, 97- ]) 98- ) 99+ scopeSelections = Object.fromEntries(data.scopes.map((scope) => [scope.scope, scope.required ? true : (scope.granted ?? true)])) 100 101 if (!data.show_consent) { 102 await submitConsent() 103@@ -93,8 +88,8 @@ 104 body: JSON.stringify({ 105 request_uri: consentData.request_uri, 106 approved_scopes: approvedScopes, 107- remember: rememberChoice, 108- }), 109+ remember: rememberChoice 110+ }) 111 }) 112 113 if (!response.ok) { 114@@ -107,6 +102,11 @@ 115 const data = await response.json() 116 if (data.redirect_uri) { 117 window.location.href = data.redirect_uri 118+ const a = document.createElement('a') 119+ a.href = data.redirect_uri 120+ a.style.display = 'none' 121+ document.body.appendChild(a) 122+ a.click() 123 } 124 } catch { 125 error = $_('oauth.error.genericError') 126@@ -135,7 +135,7 @@ 127 } 128 129 function handleScopeToggle(scope: string) { 130- const scopeInfo = consentData?.scopes.find(s => s.scope === scope) 131+ const scopeInfo = consentData?.scopes.find((s) => s.scope === scope) 132 if (scopeInfo?.required) return 133 scopeSelections[scope] = !scopeSelections[scope] 134 } 135@@ -144,7 +144,7 @@ 136 return scopes.reduce( 137 (groups, scope) => ({ 138 ...groups, 139- [scope.category]: [...(groups[scope.category] ?? []), scope], 140+ [scope.category]: [...(groups[scope.category] ?? []), scope] 141 }), 142 {} as Record<string, ScopeInfo[]> 143 ) 144@@ -176,7 +176,9 @@ 145 <img src={consentData.logo_uri} alt="" class="client-logo" /> 146 {/if} 147 <h1>{consentData.client_name || $_('oauth.consent.title')}</h1> 148- <p class="subtitle">{$_('oauth.consent.appWantsAccess', { values: { app: '' } })}</p> 149+ <p class="subtitle"> 150+ {$_('oauth.consent.appWantsAccess', { values: { app: '' } })} 151+ </p> 152 {#if consentData.client_uri} 153 <a href={consentData.client_uri} target="_blank" rel="noopener noreferrer" class="client-link"> 154 {consentData.client_uri} 155@@ -186,7 +188,9 @@ 156 157 <div class="account-info"> 158 {#if consentData.is_delegation} 159- <div class="delegation-badge">{$_('oauthConsent.delegatedAccess')}</div> 160+ <div class="delegation-badge"> 161+ {$_('oauthConsent.delegatedAccess')} 162+ </div> 163 <div class="delegation-info"> 164 <div class="info-row"> 165 <span class="label">{$_('oauthConsent.actingAs')}</span> 166@@ -204,7 +208,17 @@ 167 {#if consentData.delegation_level && consentData.delegation_level !== 'Owner'} 168 <div class="permissions-notice"> 169 <div class="notice-header"> 170- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg> 171+ <svg 172+ xmlns="http://www.w3.org/2000/svg" 173+ width="16" 174+ height="16" 175+ viewBox="0 0 24 24" 176+ fill="none" 177+ stroke="currentColor" 178+ stroke-width="2" 179+ stroke-linecap="round" 180+ stroke-linejoin="round"><circle cx="12" cy="12" r="10" /><line x1="12" y1="8" x2="12" y2="12" /><line x1="12" y1="16" x2="12.01" y2="16" /></svg 181+ > 182 <span>{$_('oauthConsent.permissionsLimited')}</span> 183 </div> 184 <p class="notice-text"> 185@@ -213,7 +227,9 @@ 186 {:else if consentData.delegation_level === 'Editor'} 187 {$_('oauthConsent.editorLimitedDesc')} 188 {:else} 189- {$_('oauthConsent.permissionsLimitedDesc', { values: { level: consentData.delegation_level } })} 190+ {$_('oauthConsent.permissionsLimitedDesc', { 191+ values: { level: consentData.delegation_level } 192+ })} 193 {/if} 194 </p> 195 </div> 196@@ -243,12 +259,7 @@ 197 <h3 class="category-title">{category}</h3> 198 {#each scopes as scope} 199 <label class="scope-item" class:required={scope.required}> 200- <input 201- type="checkbox" 202- checked={scopeSelections[scope.scope]} 203- disabled={scope.required || submitting} 204- onchange={() => handleScopeToggle(scope.scope)} 205- /> 206+ <input type="checkbox" checked={scopeSelections[scope.scope]} disabled={scope.required || submitting} onchange={() => handleScopeToggle(scope.scope)} /> 207 <div class="scope-info"> 208 <span class="scope-name">{scope.display_name}</span> 209 <span class="scope-description">{scope.description}</span> 210@@ -524,7 +535,7 @@ 211 border-style: dashed; 212 } 213 214- .scope-item input[type="checkbox"] { 215+ .scope-item input[type='checkbox'] { 216 flex-shrink: 0; 217 width: 18px; 218 height: 18px; 219diff --git a/frontend/src/routes/OAuthDelegation.svelte b/frontend/src/routes/OAuthDelegation.svelte 220index 77d0c19..6976dde 100644 221--- a/frontend/src/routes/OAuthDelegation.svelte 222+++ b/frontend/src/routes/OAuthDelegation.svelte 223@@ -1,11 +1,7 @@ 224 <script lang="ts"> 225 import { navigate, routes } from '../lib/router.svelte' 226 import { _ } from '../lib/i18n' 227- import { 228- prepareRequestOptions, 229- serializeAssertionResponse, 230- type WebAuthnRequestOptionsResponse, 231- } from '../lib/webauthn' 232+ import { prepareRequestOptions, serializeAssertionResponse, type WebAuthnRequestOptionsResponse } from '../lib/webauthn' 233 234 let delegatedDid = $state<string | null>(null) 235 let delegatedHandle = $state<string | null>(null) 236@@ -123,7 +119,7 @@ 237 method: 'POST', 238 headers: { 239 'Content-Type': 'application/json', 240- 'Accept': 'application/json' 241+ Accept: 'application/json' 242 }, 243 body: JSON.stringify({ 244 request_uri: requestUri, 245@@ -141,9 +137,9 @@ 246 const { options } = await startResponse.json() 247 const publicKeyOptions = prepareRequestOptions(options as WebAuthnRequestOptionsResponse) 248 249- const credential = await navigator.credentials.get({ 250+ const credential = (await navigator.credentials.get({ 251 publicKey: publicKeyOptions 252- }) as PublicKeyCredential | null 253+ })) as PublicKeyCredential | null 254 255 if (!credential) { 256 error = $_('oauthDelegation.passkeyCancelled') 257@@ -157,7 +153,7 @@ 258 method: 'POST', 259 headers: { 260 'Content-Type': 'application/json', 261- 'Accept': 'application/json' 262+ Accept: 'application/json' 263 }, 264 body: JSON.stringify({ 265 request_uri: requestUri, 266@@ -182,12 +178,19 @@ 267 } 268 269 if (data.needs_2fa) { 270- navigate(routes.oauth2fa, { params: { request_uri: requestUri, channel: data.channel || '' } }) 271+ navigate(routes.oauth2fa, { 272+ params: { request_uri: requestUri, channel: data.channel || '' } 273+ }) 274 return 275 } 276 277 if (data.redirect_uri) { 278 window.location.href = data.redirect_uri 279+ const a = document.createElement('a') 280+ a.href = data.redirect_uri 281+ a.style.display = 'none' 282+ document.body.appendChild(a) 283+ a.click() 284 return 285 } 286 287@@ -216,7 +219,7 @@ 288 method: 'POST', 289 headers: { 290 'Content-Type': 'application/json', 291- 'Accept': 'application/json' 292+ Accept: 'application/json' 293 }, 294 body: JSON.stringify({ 295 request_uri: requestUri, 296@@ -241,12 +244,19 @@ 297 } 298 299 if (data.needs_2fa) { 300- navigate(routes.oauth2fa, { params: { request_uri: requestUri, channel: data.channel || '' } }) 301+ navigate(routes.oauth2fa, { 302+ params: { request_uri: requestUri, channel: data.channel || '' } 303+ }) 304 return 305 } 306 307 if (data.redirect_uri) { 308 window.location.href = data.redirect_uri 309+ const a = document.createElement('a') 310+ a.href = data.redirect_uri 311+ a.style.display = 'none' 312+ document.body.appendChild(a) 313+ a.click() 314 return 315 } 316 317@@ -271,7 +281,7 @@ 318 method: 'POST', 319 headers: { 320 'Content-Type': 'application/json', 321- 'Accept': 'application/json' 322+ Accept: 'application/json' 323 }, 324 body: JSON.stringify({ request_uri: requestUri }) 325 }) 326@@ -279,6 +289,11 @@ 327 const data = await response.json() 328 if (data.redirect_uri) { 329 window.location.href = data.redirect_uri 330+ const a = document.createElement('a') 331+ a.href = data.redirect_uri 332+ a.style.display = 'none' 333+ document.body.appendChild(a) 334+ a.click() 335 } 336 } catch { 337 window.history.back() 338@@ -301,7 +316,9 @@ 339 <header class="page-header"> 340 <h1>{$_('oauthDelegation.title')}</h1> 341 <p class="subtitle"> 342- {$_('oauthDelegation.isDelegated', { values: { handle: delegatedHandle } })} 343+ {$_('oauthDelegation.isDelegated', { 344+ values: { handle: delegatedHandle } 345+ })} 346 <br />{$_('oauthDelegation.enterControllerHandle')} 347 </p> 348 </header> 349@@ -337,7 +354,12 @@ 350 <header class="page-header"> 351 <h1>{$_('oauthDelegation.signInAsController')}</h1> 352 <p class="subtitle"> 353- {$_('oauthDelegation.authenticateAs', { values: { controller: '@' + controllerIdentifier.replace(/^@/, ''), delegated: delegatedHandle } })} 354+ {$_('oauthDelegation.authenticateAs', { 355+ values: { 356+ controller: '@' + controllerIdentifier.replace(/^@/, ''), 357+ delegated: delegatedHandle 358+ } 359+ })} 360 </p> 361 </header> 362 363@@ -354,12 +376,7 @@ 364 <div class="auth-methods"> 365 <div class="passkey-method"> 366 <h3>{$_('oauthDelegation.signInWithPasskey')}</h3> 367- <button 368- type="button" 369- class="passkey-btn" 370- onclick={handlePasskeyLogin} 371- disabled={submitting} 372- > 373+ <button type="button" class="passkey-btn" onclick={handlePasskeyLogin} disabled={submitting}> 374 <svg class="passkey-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 375 <path d="M15 7a4 4 0 1 0-8 0 4 4 0 0 0 8 0z" /> 376 <path d="M17 17v4l3-2-3-2z" /> 377@@ -378,14 +395,7 @@ 378 <div class="password-method"> 379 <h3>{$_('oauthDelegation.password')}</h3> 380 <div class="field"> 381- <input 382- type="password" 383- bind:value={password} 384- disabled={submitting} 385- required 386- autocomplete="current-password" 387- placeholder={$_('oauthDelegation.enterPassword')} 388- /> 389+ <input type="password" bind:value={password} disabled={submitting} required autocomplete="current-password" placeholder={$_('oauthDelegation.enterPassword')} /> 390 </div> 391 392 <label class="remember-device"> 393@@ -401,14 +411,7 @@ 394 {:else} 395 <div class="field"> 396 <label for="password">{$_('oauthDelegation.password')}</label> 397- <input 398- id="password" 399- type="password" 400- bind:value={password} 401- disabled={submitting} 402- required 403- autocomplete="current-password" 404- /> 405+ <input id="password" type="password" bind:value={password} disabled={submitting} required autocomplete="current-password" /> 406 </div> 407 408 <label class="remember-device"> 409@@ -584,8 +587,8 @@ 410 font-weight: var(--font-medium); 411 } 412 413- input[type="password"], 414- input[type="text"] { 415+ input[type='password'], 416+ input[type='text'] { 417 padding: var(--space-3); 418 border: 1px solid var(--border-color); 419 border-radius: var(--radius-md); 420@@ -677,7 +680,9 @@ 421 border-radius: var(--radius-md); 422 font-size: var(--text-base); 423 cursor: pointer; 424- transition: background-color var(--transition-fast), border-color var(--transition-fast); 425+ transition: 426+ background-color var(--transition-fast), 427+ border-color var(--transition-fast); 428 } 429 430 .passkey-btn:hover:not(:disabled) { 431diff --git a/frontend/src/routes/OAuthLogin.svelte b/frontend/src/routes/OAuthLogin.svelte 432index 2f954a1..924e43c 100644 433--- a/frontend/src/routes/OAuthLogin.svelte 434+++ b/frontend/src/routes/OAuthLogin.svelte 435@@ -1,11 +1,7 @@ 436 <script lang="ts"> 437 import { navigate, routes, getFullUrl } from '../lib/router.svelte' 438 import { _ } from '../lib/i18n' 439- import { 440- prepareRequestOptions, 441- serializeAssertionResponse, 442- type WebAuthnRequestOptionsResponse, 443- } from '../lib/webauthn' 444+ import { prepareRequestOptions, serializeAssertionResponse, type WebAuthnRequestOptionsResponse } from '../lib/webauthn' 445 446 let username = $state('') 447 let password = $state('') 448@@ -53,7 +49,7 @@ 449 450 try { 451 const response = await fetch(`/oauth/authorize?request_uri=${encodeURIComponent(requestUri)}`, { 452- headers: { 'Accept': 'application/json' } 453+ headers: { Accept: 'application/json' } 454 }) 455 if (response.ok) { 456 const data = await response.json() 457@@ -100,7 +96,9 @@ 458 if (!hasPassword && !hasPasskeys && isDelegated && data.did) { 459 const requestUri = getRequestUri() 460 if (requestUri) { 461- navigate(routes.oauthDelegation, { params: { request_uri: requestUri, delegated_did: data.did } }) 462+ navigate(routes.oauthDelegation, { 463+ params: { request_uri: requestUri, delegated_did: data.did } 464+ }) 465 return 466 } 467 } 468@@ -115,7 +113,6 @@ 469 } 470 } 471 472- 473 async function handlePasskeyLogin() { 474 const requestUri = getRequestUri() 475 if (!requestUri || !username) { 476@@ -131,7 +128,7 @@ 477 method: 'POST', 478 headers: { 479 'Content-Type': 'application/json', 480- 'Accept': 'application/json' 481+ Accept: 'application/json' 482 }, 483 body: JSON.stringify({ 484 request_uri: requestUri, 485@@ -149,9 +146,9 @@ 486 const { options } = await startResponse.json() 487 const publicKeyOptions = prepareRequestOptions(options as WebAuthnRequestOptionsResponse) 488 489- const credential = await navigator.credentials.get({ 490+ const credential = (await navigator.credentials.get({ 491 publicKey: publicKeyOptions 492- }) as PublicKeyCredential | null 493+ })) as PublicKeyCredential | null 494 495 if (!credential) { 496 error = $_('common.error') 497@@ -165,7 +162,7 @@ 498 method: 'POST', 499 headers: { 500 'Content-Type': 'application/json', 501- 'Accept': 'application/json' 502+ Accept: 'application/json' 503 }, 504 body: JSON.stringify({ 505 request_uri: requestUri, 506@@ -187,12 +184,19 @@ 507 } 508 509 if (data.needs_2fa) { 510- navigate(routes.oauth2fa, { params: { request_uri: requestUri, channel: data.channel || '' } }) 511+ navigate(routes.oauth2fa, { 512+ params: { request_uri: requestUri, channel: data.channel || '' } 513+ }) 514 return 515 } 516 517 if (data.redirect_uri) { 518 window.location.href = data.redirect_uri 519+ const a = document.createElement('a') 520+ a.href = data.redirect_uri 521+ a.style.display = 'none' 522+ document.body.appendChild(a) 523+ a.click() 524 return 525 } 526 527@@ -225,7 +229,7 @@ 528 method: 'POST', 529 headers: { 530 'Content-Type': 'application/json', 531- 'Accept': 'application/json' 532+ Accept: 'application/json' 533 }, 534 body: JSON.stringify({ 535 request_uri: requestUri, 536@@ -249,12 +253,19 @@ 537 } 538 539 if (data.needs_2fa) { 540- navigate(routes.oauth2fa, { params: { request_uri: requestUri, channel: data.channel || '' } }) 541+ navigate(routes.oauth2fa, { 542+ params: { request_uri: requestUri, channel: data.channel || '' } 543+ }) 544 return 545 } 546 547 if (data.redirect_uri) { 548 window.location.href = data.redirect_uri 549+ const a = document.createElement('a') 550+ a.href = data.redirect_uri 551+ a.style.display = 'none' 552+ document.body.appendChild(a) 553+ a.click() 554 return 555 } 556 557@@ -279,7 +290,7 @@ 558 method: 'POST', 559 headers: { 560 'Content-Type': 'application/json', 561- 'Accept': 'application/json' 562+ Accept: 'application/json' 563 }, 564 body: JSON.stringify({ request_uri: requestUri }) 565 }) 566@@ -287,6 +298,11 @@ 567 const data = await response.json() 568 if (data.redirect_uri) { 569 window.location.href = data.redirect_uri 570+ const a = document.createElement('a') 571+ a.href = data.redirect_uri 572+ a.style.display = 'none' 573+ document.body.appendChild(a) 574+ a.click() 575 } 576 } catch { 577 window.history.back() 578@@ -313,15 +329,7 @@ 579 <form onsubmit={handleSubmit}> 580 <div class="field"> 581 <label for="username">{$_('register.handle')}</label> 582- <input 583- id="username" 584- type="text" 585- bind:value={username} 586- placeholder={$_('register.emailPlaceholder')} 587- disabled={submitting} 588- required 589- autocomplete="username" 590- /> 591+ <input id="username" type="text" bind:value={username} placeholder={$_('register.emailPlaceholder')} disabled={submitting} required autocomplete="username" /> 592 </div> 593 594 {#if passkeySupported && username.length >= 3} 595@@ -334,7 +342,11 @@ 596 class:passkey-unavailable={!hasPasskeys || checkingSecurityStatus || !securityStatusChecked} 597 onclick={handlePasskeyLogin} 598 disabled={submitting || !hasPasskeys || !username || checkingSecurityStatus || !securityStatusChecked} 599- title={checkingSecurityStatus ? $_('oauth.login.passkeyHintChecking') : hasPasskeys ? $_('oauth.login.passkeyHintAvailable') : $_('oauth.login.passkeyHintNotAvailable')} 600+ title={checkingSecurityStatus 601+ ? $_('oauth.login.passkeyHintChecking') 602+ : hasPasskeys 603+ ? $_('oauth.login.passkeyHintAvailable') 604+ : $_('oauth.login.passkeyHintNotAvailable')} 605 > 606 <svg class="passkey-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 607 <path d="M15 7a4 4 0 1 0-8 0 4 4 0 0 0 8 0z" /> 608@@ -393,14 +405,7 @@ 609 {:else} 610 <div class="field"> 611 <label for="password">{$_('oauth.login.password')}</label> 612- <input 613- id="password" 614- type="password" 615- bind:value={password} 616- disabled={submitting} 617- required 618- autocomplete="current-password" 619- /> 620+ <input id="password" type="password" bind:value={password} disabled={submitting} required autocomplete="current-password" /> 621 </div> 622 623 <label class="remember-device"> 624@@ -420,7 +425,9 @@ 625 </form> 626 627 <p class="help-links"> 628- <a href={getFullUrl(routes.resetPassword)}>{$_('login.forgotPassword')}</a> &middot; <a href={getFullUrl(routes.requestPasskeyRecovery)}>{$_('login.lostPasskey')}</a> 629+ <a href={getFullUrl(routes.resetPassword)}>{$_('login.forgotPassword')}</a> 630+ &middot; 631+ <a href={getFullUrl(routes.requestPasskeyRecovery)}>{$_('login.lostPasskey')}</a> 632 </p> 633 </div> 634 635@@ -560,8 +567,8 @@ 636 font-weight: var(--font-medium); 637 } 638 639- input[type="text"], 640- input[type="password"] { 641+ input[type='text'], 642+ input[type='password'] { 643 padding: var(--space-3); 644 border: 1px solid var(--border-color); 645 border-radius: var(--radius-md); 646@@ -640,7 +647,6 @@ 647 background: var(--accent-hover); 648 } 649 650- 651 .passkey-btn { 652 display: flex; 653 align-items: center; 654@@ -654,7 +660,10 @@ 655 border-radius: var(--radius-md); 656 font-size: var(--text-base); 657 cursor: pointer; 658- transition: background-color var(--transition-fast), border-color var(--transition-fast), opacity var(--transition-fast); 659+ transition: 660+ background-color var(--transition-fast), 661+ border-color var(--transition-fast), 662+ opacity var(--transition-fast); 663 } 664 665 .passkey-btn:hover:not(:disabled) { 666diff --git a/frontend/src/routes/OAuthPasskey.svelte b/frontend/src/routes/OAuthPasskey.svelte 667index 88281a0..afdb925 100644 668--- a/frontend/src/routes/OAuthPasskey.svelte 669+++ b/frontend/src/routes/OAuthPasskey.svelte 670@@ -4,7 +4,7 @@ 671 import { 672 prepareRequestOptions, 673 serializeAssertionResponse, 674- type WebAuthnRequestOptionsResponse, 675+ type WebAuthnRequestOptionsResponse 676 } from '../lib/webauthn' 677 678 let loading = $state(false) 679@@ -37,7 +37,7 @@ 680 const startResponse = await fetch(`/oauth/authorize/passkey?request_uri=${encodeURIComponent(requestUri)}`, { 681 method: 'GET', 682 headers: { 683- 'Accept': 'application/json' 684+ Accept: 'application/json' 685 } 686 }) 687 688@@ -67,7 +67,7 @@ 689 method: 'POST', 690 headers: { 691 'Content-Type': 'application/json', 692- 'Accept': 'application/json' 693+ Accept: 'application/json' 694 }, 695 body: JSON.stringify({ 696 request_uri: requestUri, 697@@ -85,6 +85,11 @@ 698 699 if (finishData.redirect_uri) { 700 window.location.href = finishData.redirect_uri 701+ const a = document.createElement('a') 702+ a.href = finishData.redirect_uri 703+ a.style.display = 'none' 704+ document.body.appendChild(a) 705+ a.click() 706 return 707 } 708 709diff --git a/frontend/src/routes/OAuthTotp.svelte b/frontend/src/routes/OAuthTotp.svelte 710index fe0b632..ab5236f 100644 711--- a/frontend/src/routes/OAuthTotp.svelte 712+++ b/frontend/src/routes/OAuthTotp.svelte 713@@ -28,7 +28,7 @@ 714 method: 'POST', 715 headers: { 716 'Content-Type': 'application/json', 717- 'Accept': 'application/json' 718+ Accept: 'application/json' 719 }, 720 body: JSON.stringify({ 721 request_uri: requestUri, 722@@ -47,6 +47,11 @@ 723 724 if (data.redirect_uri) { 725 window.location.href = data.redirect_uri 726+ const a = document.createElement('a') 727+ a.href = data.redirect_uri 728+ a.style.display = 'none' 729+ document.body.appendChild(a) 730+ a.click() 731 return 732 } 733 734@@ -108,11 +113,7 @@ 735 </div> 736 737 <label class="trust-device-label"> 738- <input 739- type="checkbox" 740- bind:checked={trustDevice} 741- disabled={submitting} 742- /> 743+ <input type="checkbox" bind:checked={trustDevice} disabled={submitting} /> 744 <span>{$_('oauth.totp.trustDevice')}</span> 745 </label> 746 747@@ -245,7 +246,7 @@ 748 margin-top: var(--space-2); 749 } 750 751- .trust-device-label input[type="checkbox"] { 752+ .trust-device-label input[type='checkbox'] { 753 width: auto; 754 margin: 0; 755 }