this repo has no description

General linting, document react-native-streamplace-oauth-problem

lewis 041fb933 3c47a8f0

+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 </div> 112 113 <div class="field"> 114 - <label>{$_('migration.inbound.chooseHandle.authMethod')}</label> 115 <div class="auth-method-options"> 116 <label class="auth-option" class:selected={authMethod === 'password'}> 117 <input
··· 111 </div> 112 113 <div class="field"> 114 + <span class="field-label">{$_('migration.inbound.chooseHandle.authMethod')}</span> 115 <div class="auth-method-options"> 116 <label class="auth-option" class:selected={authMethod === 'password'}> 117 <input
-18
frontend/src/routes/Admin.svelte
··· 674 padding: var(--space-7); 675 } 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 .config-form { 696 max-width: 500px; 697 }
··· 674 padding: var(--space-7); 675 } 676 677 .config-form { 678 max-width: 500px; 679 }
-9
frontend/src/routes/AppPasswords.svelte
··· 234 margin-bottom: var(--space-7); 235 } 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 .created-password { 247 display: flex; 248 flex-direction: column;
··· 234 margin-bottom: var(--space-7); 235 } 236 237 .created-password { 238 display: flex; 239 flex-direction: column;
-6
frontend/src/routes/Comms.svelte
··· 412 margin: var(--space-2) 0 0 0; 413 } 414 415 - .loading { 416 - text-align: center; 417 - color: var(--text-secondary); 418 - padding: var(--space-7); 419 - } 420 - 421 .split-layout { 422 display: grid; 423 grid-template-columns: 1fr;
··· 412 margin: var(--space-2) 0 0 0; 413 } 414 415 .split-layout { 416 display: grid; 417 grid-template-columns: 1fr;
-19
frontend/src/routes/Controllers.svelte
··· 453 margin: var(--space-2) 0 0 0; 454 } 455 456 - .loading, 457 .empty { 458 text-align: center; 459 color: var(--text-secondary); 460 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 } 480 481 .constraint-notice {
··· 453 margin: var(--space-2) 0 0 0; 454 } 455 456 .empty { 457 text-align: center; 458 color: var(--text-secondary); 459 padding: var(--space-4); 460 } 461 462 .constraint-notice {
-10
frontend/src/routes/DelegationAudit.svelte
··· 216 margin: var(--space-2) 0 0 0; 217 } 218 219 - .loading, 220 .empty { 221 text-align: center; 222 color: var(--text-secondary); 223 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 } 234 235 .audit-list {
··· 216 margin: var(--space-2) 0 0 0; 217 } 218 219 .empty { 220 text-align: center; 221 color: var(--text-secondary); 222 padding: var(--space-7); 223 } 224 225 .audit-list {
-6
frontend/src/routes/DidDocumentEditor.svelte
··· 439 margin-top: var(--space-6); 440 } 441 442 - .loading { 443 - text-align: center; 444 - padding: var(--space-9); 445 - color: var(--text-secondary); 446 - } 447 - 448 @media (max-width: 600px) { 449 .field-row { 450 flex-direction: column;
··· 439 margin-top: var(--space-6); 440 } 441 442 @media (max-width: 600px) { 443 .field-row { 444 flex-direction: column;
-9
frontend/src/routes/InviteCodes.svelte
··· 192 margin-bottom: var(--space-7); 193 } 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 .created-code { 205 padding: var(--space-6); 206 background: var(--success-bg);
··· 192 margin-bottom: var(--space-7); 193 } 194 195 .created-code { 196 padding: var(--space-6); 197 background: var(--success-bg);
+10 -8
frontend/src/routes/Migration.svelte
··· 74 75 if (!hasOAuthCallback) { 76 if (hasPendingMigration()) { 77 - resumeInfo = getResumeInfo() 78 - if (resumeInfo) { 79 - if (resumeInfo.step === 'success') { 80 clearMigrationState() 81 - resumeInfo = null 82 } else { 83 const stored = loadMigrationState() 84 if (stored && stored.direction === 'inbound') { 85 direction = 'inbound' 86 - inboundFlow = createInboundMigrationFlow() 87 - inboundFlow.resumeFromState(stored) 88 } 89 } 90 } ··· 94 clearOfflineState() 95 } else { 96 direction = 'offline-inbound' 97 - offlineFlow = createOfflineInboundMigrationFlow() 98 - offlineFlow.tryResume() 99 } 100 } 101 }
··· 74 75 if (!hasOAuthCallback) { 76 if (hasPendingMigration()) { 77 + const info = getResumeInfo() 78 + if (info) { 79 + if (info.step === 'success') { 80 clearMigrationState() 81 } else { 82 + resumeInfo = info 83 const stored = loadMigrationState() 84 if (stored && stored.direction === 'inbound') { 85 direction = 'inbound' 86 + const flow = createInboundMigrationFlow() 87 + flow.resumeFromState(stored) 88 + inboundFlow = flow 89 } 90 } 91 } ··· 95 clearOfflineState() 96 } else { 97 direction = 'offline-inbound' 98 + const flow = createOfflineInboundMigrationFlow() 99 + flow.tryResume() 100 + offlineFlow = flow 101 } 102 } 103 }
+2 -2
frontend/src/routes/OAuthConsent.svelte
··· 93 body: JSON.stringify({ 94 request_uri: consentData.request_uri, 95 approved_scopes: approvedScopes, 96 - remember: rememberChoice 97 - }) 98 }) 99 100 if (!response.ok) {
··· 93 body: JSON.stringify({ 94 request_uri: consentData.request_uri, 95 approved_scopes: approvedScopes, 96 + remember: rememberChoice, 97 + }), 98 }) 99 100 if (!response.ok) {
-6
frontend/src/routes/Register.svelte
··· 516 color: var(--error-text); 517 } 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 .radio-group { 526 display: flex; 527 flex-direction: column;
··· 516 color: var(--error-text); 517 } 518 519 .radio-group { 520 display: flex; 521 flex-direction: column;
-6
frontend/src/routes/RepoExplorer.svelte
··· 599 color: var(--success-text); 600 } 601 602 - .loading-text { 603 - text-align: center; 604 - color: var(--text-secondary); 605 - padding: var(--space-7); 606 - } 607 - 608 .toolbar { 609 display: flex; 610 gap: var(--space-2);
··· 599 color: var(--success-text); 600 } 601 602 .toolbar { 603 display: flex; 604 gap: var(--space-2);
-6
frontend/src/routes/Security.svelte
··· 795 margin: var(--space-2) 0 0 0; 796 } 797 798 - .loading { 799 - text-align: center; 800 - color: var(--text-secondary); 801 - padding: var(--space-7); 802 - } 803 - 804 section { 805 padding: var(--space-6); 806 background: var(--bg-secondary);
··· 795 margin: var(--space-2) 0 0 0; 796 } 797 798 section { 799 padding: var(--space-6); 800 background: var(--bg-secondary);
+1 -2
frontend/src/routes/Settings.svelte
··· 960 font-size: var(--text-xs); 961 } 962 963 - .empty, 964 - .loading { 965 color: var(--text-secondary); 966 font-size: var(--text-sm); 967 margin-bottom: var(--space-4);
··· 960 font-size: var(--text-xs); 961 } 962 963 + .empty { 964 color: var(--text-secondary); 965 font-size: var(--text-sm); 966 margin-bottom: var(--space-4);
-6
frontend/src/routes/TrustedDevices.svelte
··· 244 font-size: var(--text-sm); 245 } 246 247 - .loading { 248 - text-align: center; 249 - padding: var(--space-7); 250 - color: var(--text-secondary); 251 - } 252 - 253 .empty-state { 254 text-align: center; 255 padding: var(--space-8) var(--space-4);
··· 244 font-size: var(--text-sm); 245 } 246 247 .empty-state { 248 text-align: center; 249 padding: var(--space-8) var(--space-4);
+8
frontend/src/styles/migration.css
··· 3 margin: 0 auto; 4 } 5 6 .step-indicator { 7 display: flex; 8 align-items: center;
··· 3 margin: 0 auto; 4 } 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 + 14 .step-indicator { 15 display: flex; 16 align-items: center;
+4
src/lib.rs
··· 550 .route("/authorize/deny", post(oauth::endpoints::authorize_deny)) 551 .route("/authorize/consent", get(oauth::endpoints::consent_get)) 552 .route("/authorize/consent", post(oauth::endpoints::consent_post)) 553 .route("/delegation/auth", post(oauth::endpoints::delegation_auth)) 554 .route( 555 "/delegation/totp",
··· 550 .route("/authorize/deny", post(oauth::endpoints::authorize_deny)) 551 .route("/authorize/consent", get(oauth::endpoints::consent_get)) 552 .route("/authorize/consent", post(oauth::endpoints::consent_post)) 553 + .route( 554 + "/authorize/redirect", 555 + get(oauth::endpoints::authorize_redirect), 556 + ) 557 .route("/delegation/auth", post(oauth::endpoints::delegation_auth)) 558 .route( 559 "/delegation/totp",
+129 -62
src/oauth/endpoints/authorize.rs
··· 22 const DEVICE_COOKIE_NAME: &str = "oauth_device_id"; 23 24 fn redirect_see_other(uri: &str) -> Response { 25 - (StatusCode::SEE_OTHER, [(LOCATION, uri.to_string())]).into_response() 26 } 27 28 fn redirect_to_frontend_error(error: &str, description: &str) -> Response { ··· 783 { 784 return show_login_error("An error occurred. Please try again.", json_response); 785 } 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 if json_response { 793 if let Some(cookie) = new_cookie { 794 ( 795 StatusCode::OK, ··· 800 } else { 801 Json(serde_json::json!({"redirect_uri": redirect_url})).into_response() 802 } 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 } else { 810 - redirect_see_other(&redirect_url) 811 } 812 } 813 ··· 984 "An error occurred. Please try again.", 985 ); 986 } 987 - let redirect_url = build_success_redirect( 988 &request_data.parameters.redirect_uri, 989 &code.0, 990 request_data.parameters.state.as_deref(), ··· 1012 '?' 1013 }; 1014 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 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 1020 redirect_url.push_str(&format!( 1021 - "&iss={}", 1022 url_encode(&format!("https://{}", pds_hostname)) 1023 )); 1024 redirect_url 1025 } 1026 1027 #[derive(Debug, Serialize)] 1028 pub struct AuthorizeDenyResponse { 1029 pub error: String, ··· 1367 } 1368 }; 1369 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; 1375 } 1376 1377 let did = flow_state.did().unwrap().to_string(); ··· 1420 && !has_granular_scopes 1421 && !form.approved_scopes.contains(&"atproto".to_string()) 1422 { 1423 - return ( 1424 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(); 1431 } 1432 let final_approved: Vec<String> = if user_denied_some_granular { 1433 form.approved_scopes ··· 1439 form.approved_scopes.clone() 1440 }; 1441 if final_approved.is_empty() { 1442 - return ( 1443 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(); 1450 } 1451 let approved_scope_str = final_approved.join(" "); 1452 let has_valid_scope = final_approved.iter().all(|s| { ··· 1462 || s.starts_with("include:") 1463 }); 1464 if !has_valid_scope { 1465 - return ( 1466 StatusCode::BAD_REQUEST, 1467 - Json(serde_json::json!({ 1468 - "error": "invalid_request", 1469 - "error_description": "Invalid scope format" 1470 - })), 1471 - ) 1472 - .into_response(); 1473 } 1474 if form.remember { 1475 let preferences: Vec<db::ScopePreference> = requested_scopes ··· 1503 .await 1504 .is_err() 1505 { 1506 - return ( 1507 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(); 1514 } 1515 - let redirect_url = build_success_redirect( 1516 - &request_data.parameters.redirect_uri, 1517 &code.0, 1518 request_data.parameters.state.as_deref(), 1519 request_data.parameters.response_mode.as_deref(), 1520 ); 1521 - Json(serde_json::json!({ 1522 - "redirect_uri": redirect_url 1523 - })) 1524 - .into_response() 1525 } 1526 1527 pub async fn authorize_2fa_post( ··· 1630 "An error occurred. Please try again.", 1631 ); 1632 } 1633 - let redirect_url = build_success_redirect( 1634 &request_data.parameters.redirect_uri, 1635 &code.0, 1636 request_data.parameters.state.as_deref(), ··· 1725 "An error occurred. Please try again.", 1726 ); 1727 } 1728 - let redirect_url = build_success_redirect( 1729 &request_data.parameters.redirect_uri, 1730 &code.0, 1731 request_data.parameters.state.as_deref(), ··· 2367 .into_response(); 2368 } 2369 2370 - let redirect_url = build_success_redirect( 2371 &request_data.parameters.redirect_uri, 2372 &code.0, 2373 request_data.parameters.state.as_deref(),
··· 22 const DEVICE_COOKIE_NAME: &str = "oauth_device_id"; 23 24 fn redirect_see_other(uri: &str) -> 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() 37 } 38 39 fn redirect_to_frontend_error(error: &str, description: &str) -> Response { ··· 794 { 795 return show_login_error("An error occurred. Please try again.", json_response); 796 } 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 + ); 804 if let Some(cookie) = new_cookie { 805 ( 806 StatusCode::OK, ··· 811 } else { 812 Json(serde_json::json!({"redirect_uri": redirect_url})).into_response() 813 } 814 } else { 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 + } 830 } 831 } 832 ··· 1003 "An error occurred. Please try again.", 1004 ); 1005 } 1006 + let redirect_url = build_intermediate_redirect_url( 1007 &request_data.parameters.redirect_uri, 1008 &code.0, 1009 request_data.parameters.state.as_deref(), ··· 1031 '?' 1032 }; 1033 redirect_url.push(separator); 1034 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 1035 redirect_url.push_str(&format!( 1036 + "iss={}", 1037 url_encode(&format!("https://{}", pds_hostname)) 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))); 1043 redirect_url 1044 } 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 + 1098 #[derive(Debug, Serialize)] 1099 pub struct AuthorizeDenyResponse { 1100 pub error: String, ··· 1438 } 1439 }; 1440 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 + ); 1448 + } 1449 + if flow_state.is_pending() { 1450 + return json_error(StatusCode::FORBIDDEN, "access_denied", "Not authenticated"); 1451 } 1452 1453 let did = flow_state.did().unwrap().to_string(); ··· 1496 && !has_granular_scopes 1497 && !form.approved_scopes.contains(&"atproto".to_string()) 1498 { 1499 + return json_error( 1500 StatusCode::BAD_REQUEST, 1501 + "invalid_request", 1502 + "The atproto scope was requested and must be approved", 1503 + ); 1504 } 1505 let final_approved: Vec<String> = if user_denied_some_granular { 1506 form.approved_scopes ··· 1512 form.approved_scopes.clone() 1513 }; 1514 if final_approved.is_empty() { 1515 + return json_error( 1516 StatusCode::BAD_REQUEST, 1517 + "invalid_request", 1518 + "At least one scope must be approved", 1519 + ); 1520 } 1521 let approved_scope_str = final_approved.join(" "); 1522 let has_valid_scope = final_approved.iter().all(|s| { ··· 1532 || s.starts_with("include:") 1533 }); 1534 if !has_valid_scope { 1535 + return json_error( 1536 StatusCode::BAD_REQUEST, 1537 + "invalid_request", 1538 + "Invalid scope format", 1539 + ); 1540 } 1541 if form.remember { 1542 let preferences: Vec<db::ScopePreference> = requested_scopes ··· 1570 .await 1571 .is_err() 1572 { 1573 + return json_error( 1574 StatusCode::INTERNAL_SERVER_ERROR, 1575 + "server_error", 1576 + "Failed to complete authorization", 1577 + ); 1578 } 1579 + let redirect_uri = &request_data.parameters.redirect_uri; 1580 + let intermediate_url = build_intermediate_redirect_url( 1581 + redirect_uri, 1582 &code.0, 1583 request_data.parameters.state.as_deref(), 1584 request_data.parameters.response_mode.as_deref(), 1585 ); 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() 1592 } 1593 1594 pub async fn authorize_2fa_post( ··· 1697 "An error occurred. Please try again.", 1698 ); 1699 } 1700 + let redirect_url = build_intermediate_redirect_url( 1701 &request_data.parameters.redirect_uri, 1702 &code.0, 1703 request_data.parameters.state.as_deref(), ··· 1792 "An error occurred. Please try again.", 1793 ); 1794 } 1795 + let redirect_url = build_intermediate_redirect_url( 1796 &request_data.parameters.redirect_uri, 1797 &code.0, 1798 request_data.parameters.state.as_deref(), ··· 2434 .into_response(); 2435 } 2436 2437 + let redirect_url = build_intermediate_redirect_url( 2438 &request_data.parameters.redirect_uri, 2439 &code.0, 2440 request_data.parameters.state.as_deref(),
+35 -1
src/util.rs
··· 154 } 155 156 pub fn build_full_url(path: &str) -> String { 157 - format!("{}{}", pds_public_url(), path) 158 } 159 160 pub fn json_to_ipld(value: &JsonValue) -> Ipld { ··· 354 return; 355 } 356 panic!("Failed to find CID link in parsed CBOR"); 357 } 358 }
··· 154 } 155 156 pub fn build_full_url(path: &str) -> String { 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) 167 } 168 169 pub fn json_to_ipld(value: &JsonValue) -> Ipld { ··· 363 return; 364 } 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 + ); 391 } 392 }