Tranquil PDS! Moved to https://tangled.org/tranquil.farm/tranquil-pds

Functional & typesafe codebase #15

closed opened by oyster.cafe targeting main from functional--typesafe-backend
Labels

None yet.

assignee

None yet.

Participants 1
AT URI
at://did:plc:3fwecdnvtcscjnrx2p4n7alz/sh.tangled.repo.pull/3mbnt5wiaao22
+213 -177
Interdiff #0 #1
.sqlx/query-032ac69a52c0baa269988f662516a54823770aee565f4cf5da2fc1f9b89b6bbb.json

This file has not been changed.

.sqlx/query-05fd99170e31e68fa5028c862417cdf535cd70e09fde0a8a28249df0070eb2fc.json

This file has not been changed.

.sqlx/query-0710b57fb9aa933525f617b15e6e2e5feaa9c59c38ec9175568abdacda167107.json

This file has not been changed.

.sqlx/query-0ec60bb854a4991d0d7249a68f7445b65c8cc8c723baca221d85f5e4f2478b99.json

This file has not been changed.

.sqlx/query-24a7686c535e4f0332f45daa20cfce2209635090252ac3692823450431d03dc6.json

This file has not been changed.

.sqlx/query-29ef76852bb89af1ab9e679ceaa4abcf8bc8268a348d3be0da9840d1708d20b5.json

This file has not been changed.

.sqlx/query-4445cc86cdf04894b340e67661b79a3c411917144a011f50849b737130b24dbe.json

This file has not been changed.

.sqlx/query-4560c237741ce9d4166aecd669770b3360a3ac71e649b293efb88d92c3254068.json

This file has not been changed.

.sqlx/query-4649e8daefaf4cfefc5cb2de8b3813f13f5892f653128469be727b686e6a0f0a.json

This file has not been changed.

.sqlx/query-47fe4a54857344d8f789f37092a294cd58f64b4fb431b54b5deda13d64525e88.json

This file has not been changed.

.sqlx/query-49cbc923cc4a0dcf7dea4ead5ab9580ff03b717586c4ca2d5343709e2dac86b6.json

This file has not been changed.

.sqlx/query-4d8189361d1da271e300041599561ac07a02ffa9a926f94508d7873c4ca07e65.json

This file has not been changed.

.sqlx/query-5a016f289caf75177731711e56e92881ba343c73a9a6e513e205c801c5943ec0.json

This file has not been changed.

.sqlx/query-5a036d95feedcbe6fb6396b10a7b4bd6a2eedeefda46a23e6a904cdbc3a65d45.json

This file has not been changed.

.sqlx/query-785a864944c5939331704c71b0cd3ed26ffdd64f3fd0f26ecc28b6a0557bbe8f.json

This file has not been changed.

.sqlx/query-7caa8f9083b15ec1209dda35c4c6f6fba9fe338e4a6a10636b5389d426df1631.json

This file has not been changed.

.sqlx/query-82717b6f61cd79347e1ca7e92c4413743ba168d1e0d8b85566711e54d4048f81.json

This file has not been changed.

.sqlx/query-9ad422bf3c43e3cfd86fc88c73594246ead214ca794760d3fe77bb5cf4f27be5.json

This file has not been changed.

.sqlx/query-9b035b051769e6b9d45910a8bb42ac0f84c73de8c244ba4560f004ee3f4b7002.json

This file has not been changed.

.sqlx/query-9e772a967607553a0ab800970eaeadcaab7e06bdb79e0c89eb919b1bc1d6fabe.json

This file has not been changed.

.sqlx/query-a23a390659616779d7dbceaa3b5d5171e70fa25e3b8393e142cebcbff752f0f5.json

This file has not been changed.

.sqlx/query-a802d7d860f263eace39ce82bb27b633cec7287c1cc177f0e1d47ec6571564d5.json

This file has not been changed.

.sqlx/query-b0fca342e85dea89a06b4fee144cae4825dec587b1387f0fee401458aea2a2e5.json

This file has not been changed.

.sqlx/query-cd3b8098ad4c1056c1d23acd8a6b29f7abfe18ee6f559bd94ab16274b1cfdfee.json

This file has not been changed.

.sqlx/query-cda68f9b6c60295a196fc853b70ec5fd51a8ffaa2bac5942c115c99d1cbcafa3.json

This file has not been changed.

.sqlx/query-d529d6dc9858c1da360f0417e94a3b40041b043bae57e95002d4bf5df46a4ab4.json

This file has not been changed.

.sqlx/query-e20cbe2a939d790aaea718b084a80d8ede655ba1cc0fd4346d7e91d6de7d6cf3.json

This file has not been changed.

.sqlx/query-e64cd36284d10ab7f3d9f6959975a1a627809f444b0faff7e611d985f31b90e9.json

This file has not been changed.

.sqlx/query-f26c13023b47b908ec96da2e6b8bf8b34ca6a2246c20fc96f76f0e95530762a7.json

This file has not been changed.

.sqlx/query-f29da3bdfbbc547b339b4cdb059fac26435b0feec65cf1c56f851d1c4d6b1814.json

This file has not been changed.

.sqlx/query-f7af28963099aec12cf1d4f8a9a03699bb3a90f39bc9c4c0f738a37827e8f382.json

This file has not been changed.

Cargo.lock

This file has not been changed.

Cargo.toml

This file has not been changed.

frontend/deno.json

This file has not been changed.

frontend/deno.lock

This file has not been changed.

frontend/package.json

This file has not been changed.

frontend/src/App.svelte

This file has not been changed.

frontend/src/components/ReauthModal.svelte

This file has not been changed.

frontend/src/components/Skeleton.svelte

This file has not been changed.

frontend/src/components/Toast.svelte

This file has not been changed.

frontend/src/components/migration/InboundWizard.svelte

This file has not been changed.

frontend/src/components/migration/OfflineInboundWizard.svelte

This file has not been changed.

frontend/src/lib/api-validated.ts

This file has not been changed.

frontend/src/lib/api.ts

This file has not been changed.

frontend/src/lib/auth.svelte.ts

This file has not been changed.

frontend/src/lib/crypto.ts

This file has not been changed.

frontend/src/lib/migration/atproto-client.ts

This file has not been changed.

frontend/src/lib/migration/blob-migration.ts

This file has not been changed.

frontend/src/lib/migration/flow.svelte.ts

This file has not been changed.

frontend/src/lib/migration/offline-flow.svelte.ts

This file has not been changed.

frontend/src/lib/migration/plc-ops.ts

This file has not been changed.

frontend/src/lib/migration/storage.ts

This file has not been changed.

frontend/src/lib/oauth.ts

This file has not been changed.

frontend/src/lib/registration/VerificationStep.svelte

This file has not been changed.

frontend/src/lib/registration/flow.svelte.ts

This file has not been changed.

frontend/src/lib/registration/types.ts

This file has not been changed.

frontend/src/lib/router.svelte.ts

This file has not been changed.

frontend/src/lib/toast.svelte.ts

This file has not been changed.

frontend/src/lib/types/api.ts

This file has not been changed.

frontend/src/lib/types/branded.ts

This file has not been changed.

frontend/src/lib/types/exhaustive.ts

This file has not been changed.

frontend/src/lib/types/index.ts

This file has not been changed.

frontend/src/lib/types/result.ts

This file has not been changed.

frontend/src/lib/types/routes.ts

This file has not been changed.

frontend/src/lib/types/schemas.ts

This file has not been changed.

frontend/src/lib/utils/array.ts

This file has not been changed.

frontend/src/lib/utils/async.ts

This file has not been changed.

frontend/src/lib/utils/index.ts

This file has not been changed.

frontend/src/lib/utils/option.ts

This file has not been changed.

frontend/src/lib/validation.ts

This file has not been changed.

frontend/src/lib/webauthn.ts

This file has not been changed.

frontend/src/routes/ActAs.svelte

This file has not been changed.

-18
frontend/src/routes/Admin.svelte
··· 674 674 padding: var(--space-7); 675 675 } 676 676 677 - .message { 678 - padding: var(--space-3); 679 - border-radius: var(--radius-md); 680 - margin-bottom: var(--space-4); 681 - } 682 - 683 - .message.error { 684 - background: var(--error-bg); 685 - border: 1px solid var(--error-border); 686 - color: var(--error-text); 687 - } 688 - 689 - .message.success { 690 - background: var(--success-bg); 691 - border: 1px solid var(--success-border); 692 - color: var(--success-text); 693 - } 694 - 695 677 .config-form {
-9
frontend/src/routes/AppPasswords.svelte
··· 234 234 margin-bottom: var(--space-7); 235 235 } 236 236 237 - .error { 238 - padding: var(--space-3); 239 - background: var(--error-bg); 240 - border: 1px solid var(--error-border); 241 - border-radius: var(--radius-md); 242 - color: var(--error-text); 243 - margin-bottom: var(--space-4); 244 - } 245 - 246 237 .created-password { 247 238 248 239
-6
frontend/src/routes/Comms.svelte
··· 412 412 margin: var(--space-2) 0 0 0; 413 413 } 414 414 415 - .loading { 416 - text-align: center; 417 - color: var(--text-secondary); 418 - padding: var(--space-7); 419 - } 420 - 421 415 .split-layout { 422 416 423 417
-19
frontend/src/routes/Controllers.svelte
··· 453 453 margin: var(--space-2) 0 0 0; 454 454 } 455 455 456 - .loading, 457 456 .empty { 458 457 text-align: center; 459 458 color: var(--text-secondary); 460 459 padding: var(--space-4); 461 - } 462 - 463 - .message { 464 - padding: var(--space-3); 465 - border-radius: var(--radius-md); 466 - margin-bottom: var(--space-4); 467 - } 468 - 469 - .message.error { 470 - background: var(--error-bg); 471 - border: 1px solid var(--error-border); 472 - color: var(--error-text); 473 - } 474 - 475 - .message.success { 476 - background: var(--success-bg); 477 - border: 1px solid var(--success-border); 478 - color: var(--success-text); 479 460 } 480 461 481 462 .constraint-notice {
frontend/src/routes/Dashboard.svelte

This file has not been changed.

-10
frontend/src/routes/DelegationAudit.svelte
··· 216 216 margin: var(--space-2) 0 0 0; 217 217 } 218 218 219 - .loading, 220 219 .empty { 221 220 text-align: center; 222 221 color: var(--text-secondary); 223 222 padding: var(--space-7); 224 - } 225 - 226 - .message.error { 227 - padding: var(--space-3); 228 - background: var(--error-bg); 229 - border: 1px solid var(--error-border); 230 - border-radius: var(--radius-md); 231 - color: var(--error-text); 232 - margin-bottom: var(--space-4); 233 223 } 234 224 235 225 .audit-list {
-6
frontend/src/routes/DidDocumentEditor.svelte
··· 439 439 margin-top: var(--space-6); 440 440 } 441 441 442 - .loading { 443 - text-align: center; 444 - padding: var(--space-9); 445 - color: var(--text-secondary); 446 - } 447 - 448 442 @media (max-width: 600px) { 449 443 .field-row { 450 444 flex-direction: column;
-9
frontend/src/routes/InviteCodes.svelte
··· 192 192 margin-bottom: var(--space-7); 193 193 } 194 194 195 - .error { 196 - padding: var(--space-3); 197 - background: var(--error-bg); 198 - border: 1px solid var(--error-border); 199 - border-radius: var(--radius-md); 200 - color: var(--error-text); 201 - margin-bottom: var(--space-4); 202 - } 203 - 204 195 .created-code { 205 196 206 197
frontend/src/routes/Login.svelte

This file has not been changed.

+10 -8
frontend/src/routes/Migration.svelte
··· 74 74 75 75 if (!hasOAuthCallback) { 76 76 if (hasPendingMigration()) { 77 - resumeInfo = getResumeInfo() 78 - if (resumeInfo) { 79 - if (resumeInfo.step === 'success') { 77 + const info = getResumeInfo() 78 + if (info) { 79 + if (info.step === 'success') { 80 80 clearMigrationState() 81 - resumeInfo = null 82 81 } else { 82 + resumeInfo = info 83 83 const stored = loadMigrationState() 84 84 if (stored && stored.direction === 'inbound') { 85 85 direction = 'inbound' 86 - inboundFlow = createInboundMigrationFlow() 87 - inboundFlow.resumeFromState(stored) 86 + const flow = createInboundMigrationFlow() 87 + flow.resumeFromState(stored) 88 + inboundFlow = flow 88 89 } 89 90 } 90 91 } ··· 94 95 clearOfflineState() 95 96 } else { 96 97 direction = 'offline-inbound' 97 - offlineFlow = createOfflineInboundMigrationFlow() 98 - offlineFlow.tryResume() 98 + const flow = createOfflineInboundMigrationFlow() 99 + flow.tryResume() 100 + offlineFlow = flow 99 101 } 100 102 } 101 103 }
frontend/src/routes/OAuth2FA.svelte

This file has not been changed.

frontend/src/routes/OAuthAccounts.svelte

This file has not been changed.

+2 -2
frontend/src/routes/OAuthConsent.svelte
··· 93 93 body: JSON.stringify({ 94 94 request_uri: consentData.request_uri, 95 95 approved_scopes: approvedScopes, 96 - remember: rememberChoice 97 - }) 96 + remember: rememberChoice, 97 + }), 98 98 }) 99 99 100 100 if (!response.ok) {
frontend/src/routes/OAuthDelegation.svelte

This file has not been changed.

frontend/src/routes/OAuthLogin.svelte

This file has not been changed.

frontend/src/routes/OAuthPasskey.svelte

This file has not been changed.

frontend/src/routes/OAuthTotp.svelte

This file has not been changed.

frontend/src/routes/RecoverPasskey.svelte

This file has not been changed.

-6
frontend/src/routes/Register.svelte
··· 516 516 color: var(--error-text); 517 517 } 518 518 519 - .section-hint { 520 - font-size: var(--text-sm); 521 - color: var(--text-secondary); 522 - margin: 0 0 var(--space-5) 0; 523 - } 524 - 525 519 .radio-group {
frontend/src/routes/RegisterPasskey.svelte

This file has not been changed.

-6
frontend/src/routes/RepoExplorer.svelte
··· 599 599 color: var(--success-text); 600 600 } 601 601 602 - .loading-text { 603 - text-align: center; 604 - color: var(--text-secondary); 605 - padding: var(--space-7); 606 - } 607 - 608 602 .toolbar { 609 603 610 604
frontend/src/routes/RequestPasskeyRecovery.svelte

This file has not been changed.

frontend/src/routes/ResetPassword.svelte

This file has not been changed.

-6
frontend/src/routes/Security.svelte
··· 795 795 margin: var(--space-2) 0 0 0; 796 796 } 797 797 798 - .loading { 799 - text-align: center; 800 - color: var(--text-secondary); 801 - padding: var(--space-7); 802 - } 803 - 804 798 section { 805 799 padding: var(--space-6); 806 800 background: var(--bg-secondary);
frontend/src/routes/Sessions.svelte

This file has not been changed.

+1 -2
frontend/src/routes/Settings.svelte
··· 960 960 font-size: var(--text-xs); 961 961 } 962 962 963 - .empty, 964 - .loading { 963 + .empty { 965 964 color: var(--text-secondary); 966 965 font-size: var(--text-sm); 967 966 margin-bottom: var(--space-4);
-6
frontend/src/routes/TrustedDevices.svelte
··· 244 244 font-size: var(--text-sm); 245 245 } 246 246 247 - .loading { 248 - text-align: center; 249 - padding: var(--space-7); 250 - color: var(--text-secondary); 251 - } 252 - 253 247 .empty-state { 254 248 text-align: center; 255 249 padding: var(--space-8) var(--space-4);
frontend/src/routes/Verify.svelte

This file has not been changed.

frontend/src/tests/AppPasswords.test.ts

This file has not been changed.

frontend/src/tests/Login.test.ts

This file has not been changed.

frontend/src/tests/migration/storage.test.ts

This file has not been changed.

frontend/src/tests/mocks.ts

This file has not been changed.

frontend/src/tests/utils.ts

This file has not been changed.

frontend/tsconfig.json

This file has not been changed.

justfile

This file has not been changed.

src/api/actor/preferences.rs

This file has not been changed.

src/api/admin/account/delete.rs

This file has not been changed.

src/api/admin/account/email.rs

This file has not been changed.

src/api/admin/account/info.rs

This file has not been changed.

src/api/admin/account/search.rs

This file has not been changed.

src/api/admin/account/update.rs

This file has not been changed.

src/api/admin/invite.rs

This file has not been changed.

src/api/admin/status.rs

This file has not been changed.

src/api/age_assurance.rs

This file has not been changed.

src/api/backup.rs

This file has not been changed.

src/api/delegation.rs

This file has not been changed.

src/api/error.rs

This file has not been changed.

src/api/identity/account.rs

This file has not been changed.

src/api/identity/did.rs

This file has not been changed.

src/api/identity/plc/request.rs

This file has not been changed.

src/api/identity/plc/sign.rs

This file has not been changed.

src/api/identity/plc/submit.rs

This file has not been changed.

src/api/mod.rs

This file has not been changed.

src/api/moderation/mod.rs

This file has not been changed.

src/api/notification_prefs.rs

This file has not been changed.

src/api/proxy.rs

This file has not been changed.

src/api/repo/blob.rs

This file has not been changed.

src/api/repo/import.rs

This file has not been changed.

src/api/repo/meta.rs

This file has not been changed.

src/api/repo/record/batch.rs

This file has not been changed.

src/api/repo/record/delete.rs

This file has not been changed.

src/api/repo/record/read.rs

This file has not been changed.

src/api/repo/record/validation.rs

This file has not been changed.

src/api/repo/record/write.rs

This file has not been changed.

src/api/responses.rs

This file has not been changed.

src/api/server/account_status.rs

This file has not been changed.

src/api/server/app_password.rs

This file has not been changed.

src/api/server/email.rs

This file has not been changed.

src/api/server/invite.rs

This file has not been changed.

src/api/server/migration.rs

This file has not been changed.

src/api/server/passkey_account.rs

This file has not been changed.

src/api/server/passkeys.rs

This file has not been changed.

src/api/server/password.rs

This file has not been changed.

src/api/server/reauth.rs

This file has not been changed.

src/api/server/service_auth.rs

This file has not been changed.

src/api/server/session.rs

This file has not been changed.

src/api/server/signing_key.rs

This file has not been changed.

src/api/server/totp.rs

This file has not been changed.

src/api/server/trusted_devices.rs

This file has not been changed.

src/api/server/verify_email.rs

This file has not been changed.

src/api/server/verify_token.rs

This file has not been changed.

src/api/temp.rs

This file has not been changed.

src/api/validation.rs

This file has not been changed.

src/api/verification.rs

This file has not been changed.

src/appview/mod.rs

This file has not been changed.

src/auth/extractor.rs

This file has not been changed.

src/auth/mod.rs

This file has not been changed.

src/auth/scope_check.rs

This file has not been changed.

src/comms/service.rs

This file has not been changed.

src/delegation/db.rs

This file has not been changed.

+4
src/lib.rs
··· 550 550 .route("/authorize/deny", post(oauth::endpoints::authorize_deny)) 551 551 .route("/authorize/consent", get(oauth::endpoints::consent_get)) 552 552 .route("/authorize/consent", post(oauth::endpoints::consent_post)) 553 + .route( 554 + "/authorize/redirect", 555 + get(oauth::endpoints::authorize_redirect), 556 + ) 553 557 .route("/delegation/auth", post(oauth::endpoints::delegation_auth)) 554 558 .route( 555 559 "/delegation/totp",
src/main.rs

This file has not been changed.

src/oauth/db/device.rs

This file has not been changed.

src/oauth/db/mod.rs

This file has not been changed.

src/oauth/db/request.rs

This file has not been changed.

src/oauth/db/token.rs

This file has not been changed.

src/oauth/dpop.rs

This file has not been changed.

+129 -62
src/oauth/endpoints/authorize.rs
··· 22 22 const DEVICE_COOKIE_NAME: &str = "oauth_device_id"; 23 23 24 24 fn redirect_see_other(uri: &str) -> Response { 25 - (StatusCode::SEE_OTHER, [(LOCATION, uri.to_string())]).into_response() 25 + ( 26 + StatusCode::SEE_OTHER, 27 + [ 28 + (LOCATION, uri.to_string()), 29 + (axum::http::header::CACHE_CONTROL, "no-store".to_string()), 30 + ( 31 + SET_COOKIE, 32 + "bfCacheBypass=foo; max-age=1; SameSite=Lax".to_string(), 33 + ), 34 + ], 35 + ) 36 + .into_response() 26 37 } 27 38 28 39 fn redirect_to_frontend_error(error: &str, description: &str) -> Response { ··· 783 794 { 784 795 return show_login_error("An error occurred. Please try again.", json_response); 785 796 } 786 - let redirect_url = build_success_redirect( 787 - &request_data.parameters.redirect_uri, 788 - &code.0, 789 - request_data.parameters.state.as_deref(), 790 - request_data.parameters.response_mode.as_deref(), 791 - ); 792 797 if json_response { 798 + let redirect_url = build_intermediate_redirect_url( 799 + &request_data.parameters.redirect_uri, 800 + &code.0, 801 + request_data.parameters.state.as_deref(), 802 + request_data.parameters.response_mode.as_deref(), 803 + ); 793 804 if let Some(cookie) = new_cookie { 794 805 ( 795 806 StatusCode::OK, ··· 800 811 } else { 801 812 Json(serde_json::json!({"redirect_uri": redirect_url})).into_response() 802 813 } 803 - } else if let Some(cookie) = new_cookie { 804 - ( 805 - StatusCode::SEE_OTHER, 806 - [(SET_COOKIE, cookie), (LOCATION, redirect_url)], 807 - ) 808 - .into_response() 809 814 } else { 810 - redirect_see_other(&redirect_url) 815 + let redirect_url = build_success_redirect( 816 + &request_data.parameters.redirect_uri, 817 + &code.0, 818 + request_data.parameters.state.as_deref(), 819 + request_data.parameters.response_mode.as_deref(), 820 + ); 821 + if let Some(cookie) = new_cookie { 822 + ( 823 + StatusCode::SEE_OTHER, 824 + [(SET_COOKIE, cookie), (LOCATION, redirect_url)], 825 + ) 826 + .into_response() 827 + } else { 828 + redirect_see_other(&redirect_url) 829 + } 811 830 } 812 831 } 813 832 ··· 984 1003 "An error occurred. Please try again.", 985 1004 ); 986 1005 } 987 - let redirect_url = build_success_redirect( 1006 + let redirect_url = build_intermediate_redirect_url( 988 1007 &request_data.parameters.redirect_uri, 989 1008 &code.0, 990 1009 request_data.parameters.state.as_deref(), ··· 1012 1031 '?' 1013 1032 }; 1014 1033 redirect_url.push(separator); 1015 - redirect_url.push_str(&format!("code={}", url_encode(code))); 1016 - if let Some(req_state) = state { 1017 - redirect_url.push_str(&format!("&state={}", url_encode(req_state))); 1018 - } 1019 1034 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 1020 1035 redirect_url.push_str(&format!( 1021 - "&iss={}", 1036 + "iss={}", 1022 1037 url_encode(&format!("https://{}", pds_hostname)) 1023 1038 )); 1039 + if let Some(req_state) = state { 1040 + redirect_url.push_str(&format!("&state={}", url_encode(req_state))); 1041 + } 1042 + redirect_url.push_str(&format!("&code={}", url_encode(code))); 1024 1043 redirect_url 1025 1044 } 1026 1045 1046 + fn build_intermediate_redirect_url( 1047 + redirect_uri: &str, 1048 + code: &str, 1049 + state: Option<&str>, 1050 + response_mode: Option<&str>, 1051 + ) -> String { 1052 + let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 1053 + let mut url = format!( 1054 + "https://{}/oauth/authorize/redirect?redirect_uri={}&code={}", 1055 + pds_hostname, 1056 + url_encode(redirect_uri), 1057 + url_encode(code) 1058 + ); 1059 + if let Some(s) = state { 1060 + url.push_str(&format!("&state={}", url_encode(s))); 1061 + } 1062 + if let Some(rm) = response_mode { 1063 + url.push_str(&format!("&response_mode={}", url_encode(rm))); 1064 + } 1065 + url 1066 + } 1067 + 1068 + #[derive(Debug, Deserialize)] 1069 + pub struct AuthorizeRedirectParams { 1070 + redirect_uri: String, 1071 + code: String, 1072 + state: Option<String>, 1073 + response_mode: Option<String>, 1074 + } 1075 + 1076 + pub async fn authorize_redirect(Query(params): Query<AuthorizeRedirectParams>) -> Response { 1077 + let final_url = build_success_redirect( 1078 + &params.redirect_uri, 1079 + &params.code, 1080 + params.state.as_deref(), 1081 + params.response_mode.as_deref(), 1082 + ); 1083 + tracing::info!( 1084 + final_url = %final_url, 1085 + client_redirect = %params.redirect_uri, 1086 + "authorize_redirect performing 303 redirect" 1087 + ); 1088 + ( 1089 + StatusCode::SEE_OTHER, 1090 + [ 1091 + (axum::http::header::LOCATION, final_url), 1092 + (axum::http::header::CACHE_CONTROL, "no-store".to_string()), 1093 + ], 1094 + ) 1095 + .into_response() 1096 + } 1097 + 1027 1098 #[derive(Debug, Serialize)] 1028 1099 1029 1100 ··· 1367 1438 } 1368 1439 }; 1369 1440 1370 - if let Some(err_response) = validate_auth_flow_state(&flow_state, true) { 1371 - if flow_state.is_expired() { 1372 - let _ = db::delete_authorization_request(&state.db, &form.request_uri).await; 1373 - } 1374 - return err_response; 1441 + if flow_state.is_expired() { 1442 + let _ = db::delete_authorization_request(&state.db, &form.request_uri).await; 1443 + return json_error( 1444 + StatusCode::BAD_REQUEST, 1445 + "invalid_request", 1446 + "Authorization request has expired", 1447 + ); 1375 1448 } 1449 + if flow_state.is_pending() { 1450 + return json_error(StatusCode::FORBIDDEN, "access_denied", "Not authenticated"); 1451 + } 1376 1452 1377 1453 let did = flow_state.did().unwrap().to_string(); 1378 1454 let original_scope_str = request_data ··· 1420 1496 && !has_granular_scopes 1421 1497 && !form.approved_scopes.contains(&"atproto".to_string()) 1422 1498 { 1423 - return ( 1499 + return json_error( 1424 1500 StatusCode::BAD_REQUEST, 1425 - Json(serde_json::json!({ 1426 - "error": "invalid_request", 1427 - "error_description": "The atproto scope was requested and must be approved" 1428 - })), 1429 - ) 1430 - .into_response(); 1501 + "invalid_request", 1502 + "The atproto scope was requested and must be approved", 1503 + ); 1431 1504 } 1432 1505 let final_approved: Vec<String> = if user_denied_some_granular { 1433 1506 form.approved_scopes ··· 1439 1512 form.approved_scopes.clone() 1440 1513 }; 1441 1514 if final_approved.is_empty() { 1442 - return ( 1515 + return json_error( 1443 1516 StatusCode::BAD_REQUEST, 1444 - Json(serde_json::json!({ 1445 - "error": "invalid_request", 1446 - "error_description": "At least one scope must be approved" 1447 - })), 1448 - ) 1449 - .into_response(); 1517 + "invalid_request", 1518 + "At least one scope must be approved", 1519 + ); 1450 1520 } 1451 1521 let approved_scope_str = final_approved.join(" "); 1452 1522 let has_valid_scope = final_approved.iter().all(|s| { ··· 1462 1532 || s.starts_with("include:") 1463 1533 }); 1464 1534 if !has_valid_scope { 1465 - return ( 1535 + return json_error( 1466 1536 StatusCode::BAD_REQUEST, 1467 - Json(serde_json::json!({ 1468 - "error": "invalid_request", 1469 - "error_description": "Invalid scope format" 1470 - })), 1471 - ) 1472 - .into_response(); 1537 + "invalid_request", 1538 + "Invalid scope format", 1539 + ); 1473 1540 } 1474 1541 if form.remember { 1475 1542 let preferences: Vec<db::ScopePreference> = requested_scopes ··· 1503 1570 .await 1504 1571 .is_err() 1505 1572 { 1506 - return ( 1573 + return json_error( 1507 1574 StatusCode::INTERNAL_SERVER_ERROR, 1508 - Json(serde_json::json!({ 1509 - "error": "server_error", 1510 - "error_description": "Failed to complete authorization" 1511 - })), 1512 - ) 1513 - .into_response(); 1575 + "server_error", 1576 + "Failed to complete authorization", 1577 + ); 1514 1578 } 1515 - let redirect_url = build_success_redirect( 1516 - &request_data.parameters.redirect_uri, 1579 + let redirect_uri = &request_data.parameters.redirect_uri; 1580 + let intermediate_url = build_intermediate_redirect_url( 1581 + redirect_uri, 1517 1582 &code.0, 1518 1583 request_data.parameters.state.as_deref(), 1519 1584 request_data.parameters.response_mode.as_deref(), 1520 1585 ); 1521 - Json(serde_json::json!({ 1522 - "redirect_uri": redirect_url 1523 - })) 1524 - .into_response() 1586 + tracing::info!( 1587 + intermediate_url = %intermediate_url, 1588 + client_redirect = %redirect_uri, 1589 + "consent_post returning JSON with intermediate URL (for 303 redirect)" 1590 + ); 1591 + Json(serde_json::json!({ "redirect_uri": intermediate_url })).into_response() 1525 1592 } 1526 1593 1527 1594 pub async fn authorize_2fa_post( ··· 1630 1697 "An error occurred. Please try again.", 1631 1698 ); 1632 1699 } 1633 - let redirect_url = build_success_redirect( 1700 + let redirect_url = build_intermediate_redirect_url( 1634 1701 &request_data.parameters.redirect_uri, 1635 1702 &code.0, 1636 1703 request_data.parameters.state.as_deref(), ··· 1725 1792 "An error occurred. Please try again.", 1726 1793 ); 1727 1794 } 1728 - let redirect_url = build_success_redirect( 1795 + let redirect_url = build_intermediate_redirect_url( 1729 1796 &request_data.parameters.redirect_uri, 1730 1797 &code.0, 1731 1798 request_data.parameters.state.as_deref(), ··· 2367 2434 .into_response(); 2368 2435 } 2369 2436 2370 - let redirect_url = build_success_redirect( 2437 + let redirect_url = build_intermediate_redirect_url( 2371 2438 &request_data.parameters.redirect_uri, 2372 2439 &code.0, 2373 2440 request_data.parameters.state.as_deref(),
src/oauth/endpoints/delegation.rs

This file has not been changed.

src/oauth/endpoints/token/grants.rs

This file has not been changed.

src/oauth/endpoints/token/mod.rs

This file has not been changed.

src/oauth/endpoints/token/types.rs

This file has not been changed.

src/oauth/scopes/parser.rs

This file has not been changed.

src/oauth/types.rs

This file has not been changed.

src/oauth/verify.rs

This file has not been changed.

src/scheduled.rs

This file has not been changed.

src/storage/mod.rs

This file has not been changed.

src/sync/blob.rs

This file has not been changed.

src/sync/commit.rs

This file has not been changed.

src/sync/crawl.rs

This file has not been changed.

src/sync/deprecated.rs

This file has not been changed.

src/sync/frame.rs

This file has not been changed.

src/sync/repo.rs

This file has not been changed.

src/sync/util.rs

This file has not been changed.

src/types.rs

This file has not been changed.

+35 -1
src/util.rs
··· 154 154 } 155 155 156 156 pub fn build_full_url(path: &str) -> String { 157 - format!("{}{}", pds_public_url(), path) 157 + let normalized_path = if !path.starts_with("/xrpc/") 158 + && (path.starts_with("/com.atproto.") 159 + || path.starts_with("/app.bsky.") 160 + || path.starts_with("/_")) 161 + { 162 + format!("/xrpc{}", path) 163 + } else { 164 + path.to_string() 165 + }; 166 + format!("{}{}", pds_public_url(), normalized_path) 158 167 } 159 168 160 169 pub fn json_to_ipld(value: &JsonValue) -> Ipld { ··· 354 363 return; 355 364 } 356 365 panic!("Failed to find CID link in parsed CBOR"); 366 + } 367 + 368 + #[test] 369 + fn test_build_full_url_adds_xrpc_prefix_for_atproto_paths() { 370 + unsafe { std::env::set_var("PDS_HOSTNAME", "example.com") }; 371 + assert_eq!( 372 + build_full_url("/com.atproto.server.getSession"), 373 + "https://example.com/xrpc/com.atproto.server.getSession" 374 + ); 375 + assert_eq!( 376 + build_full_url("/app.bsky.feed.getTimeline"), 377 + "https://example.com/xrpc/app.bsky.feed.getTimeline" 378 + ); 379 + assert_eq!( 380 + build_full_url("/_health"), 381 + "https://example.com/xrpc/_health" 382 + ); 383 + assert_eq!( 384 + build_full_url("/xrpc/com.atproto.server.getSession"), 385 + "https://example.com/xrpc/com.atproto.server.getSession" 386 + ); 387 + assert_eq!( 388 + build_full_url("/oauth/token"), 389 + "https://example.com/oauth/token" 390 + ); 357 391 } 358 392 }
src/validation/mod.rs

This file has not been changed.

tests/admin_email.rs

This file has not been changed.

tests/delete_account.rs

This file has not been changed.

tests/import_verification.rs

This file has not been changed.

tests/lifecycle_record.rs

This file has not been changed.

+23
KNOWN_ISSUES.md
··· 1 + # Known Issues 2 + 3 + ## stream.place iOS app OAuth flow fails 4 + 5 + OAuth flow with stream.place's iOS app (using expo-web-browser's ASWebAuthenticationSession) does not complete. After user approves consent, the redirect from our PDS to stream.place's callback URL is not followed by ASWebAuthenticationSession. 6 + 7 + What does work with stream.place: everything else :P 8 + - Desktop browsers 9 + - ios safari (regular browser) 10 + - ASWebAuthenticationSession using the reference pds 11 + 12 + What fails: 13 + - ASWebAuthenticationSession with this pds 14 + 15 + Attempted fixes (all failed): 16 + - HTTP 302/303/307 redirects 17 + - JavaScript navigation 18 + - Meta refresh 19 + - Form auto-submit 20 + - Removing CORS headers 21 + - HTTP/1.1 instead of HTTP/2 22 + - Minimal response headers 23 +
+1 -1
frontend/src/components/migration/ChooseHandleStep.svelte
··· 111 111 </div> 112 112 113 113 <div class="field"> 114 - <label>{$_('migration.inbound.chooseHandle.authMethod')}</label> 114 + <span class="field-label">{$_('migration.inbound.chooseHandle.authMethod')}</span> 115 115 <div class="auth-method-options"> 116 116 <label class="auth-option" class:selected={authMethod === 'password'}> 117 117 <input
+8
frontend/src/styles/migration.css
··· 3 3 margin: 0 auto; 4 4 } 5 5 6 + .field-label { 7 + display: block; 8 + font-size: var(--text-sm); 9 + font-weight: var(--font-medium); 10 + color: var(--text-primary); 11 + margin-bottom: var(--space-2); 12 + } 13 + 6 14 .step-indicator { 7 15 display: flex; 8 16 align-items: center;

History

2 rounds 0 comments
sign up or login to add to the discussion
5 commits
expand
Functional typesafe backend
More local methods to not proxy
More functional and typesafe frontend
Typechecks and linting
General linting, document react-native-streamplace-oauth-problem
expand 0 comments
closed without merging
4 commits
expand
Functional typesafe backend
More local methods to not proxy
More functional and typesafe frontend
Typechecks and linting
expand 0 comments