fix(auth): resolve race condition causing files not to load on initial visit

The cached-user-data path in checkAuthentication() fired resolveHomeFolder()
and loadFiles() concurrently with refreshUserData() using non-blocking .then()
chains. When the session cookie was expired, the folder/file API calls received
401 errors before the session could be refreshed. Now awaits session validation
before loading files.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jared Wolff
2026-03-06 13:01:08 -05:00
parent 9aa35aa0ea
commit 5b5a9173bc
3 changed files with 44 additions and 37 deletions
+1 -1
View File
@@ -674,4 +674,4 @@ async fn oidc_exchange(
); );
cookie_auth::append_csrf_cookie(response.headers_mut(), auth_response.expires_in); cookie_auth::append_csrf_cookie(response.headers_mut(), auth_response.expires_in);
Ok(response) Ok(response)
} }
+15 -20
View File
@@ -106,7 +106,11 @@ impl FileHandler {
if let Some(ref fid) = folder_id { if let Some(ref fid) = folder_id {
use crate::application::ports::inbound::FolderUseCase; use crate::application::ports::inbound::FolderUseCase;
let folder_service = &state.applications.folder_service; let folder_service = &state.applications.folder_service;
if folder_service.get_folder_owned(fid, &auth_user.id).await.is_err() { if folder_service
.get_folder_owned(fid, &auth_user.id)
.await
.is_err()
{
tracing::warn!( tracing::warn!(
"⛔ UPLOAD REJECTED (IDOR): user='{}' attempted upload to folder '{}' owned by another user", "⛔ UPLOAD REJECTED (IDOR): user='{}' attempted upload to folder '{}' owned by another user",
auth_user.username, auth_user.username,
@@ -366,10 +370,8 @@ impl FileHandler {
.unwrap() .unwrap()
.into_response() .into_response()
} }
Err(err) => { Err(err) => AppError::internal_error(format!("Thumbnail generation failed: {}", err))
AppError::internal_error(format!("Thumbnail generation failed: {}", err)) .into_response(),
.into_response()
}
} }
} }
@@ -542,9 +544,7 @@ impl FileHandler {
.unwrap() .unwrap()
.into_response(), .into_response(),
}, },
Err(err) => { Err(err) => AppError::from(err).into_response(),
AppError::from(err).into_response()
}
} }
} }
@@ -594,9 +594,7 @@ impl FileHandler {
.insert(header::ETAG, header::HeaderValue::from_str(&etag).unwrap()); .insert(header::ETAG, header::HeaderValue::from_str(&etag).unwrap());
resp resp
} }
Err(err) => { Err(err) => AppError::from(err).into_response(),
AppError::from(err).into_response()
}
} }
} }
@@ -635,8 +633,7 @@ impl FileHandler {
let file_path = state.core.dedup_service.blob_path(&blob_hash); let file_path = state.core.dedup_service.blob_path(&blob_hash);
tokio::spawn(async move { tokio::spawn(async move {
tracing::info!("🖼️ Generating thumbnails for: {}", file_id); tracing::info!("🖼️ Generating thumbnails for: {}", file_id);
thumbnail_service thumbnail_service.generate_all_sizes_background(file_id, file_path);
.generate_all_sizes_background(file_id, file_path);
}); });
} }
Err(e) => { Err(e) => {
@@ -726,7 +723,7 @@ impl FileHandler {
match result { match result {
Ok(_) => StatusCode::NO_CONTENT.into_response(), Ok(_) => StatusCode::NO_CONTENT.into_response(),
Err(err) => AppError::from(err).into_response() Err(err) => AppError::from(err).into_response(),
} }
} }
@@ -758,7 +755,7 @@ impl FileHandler {
let mgmt = &state.applications.file_management_service; let mgmt = &state.applications.file_management_service;
match mgmt.rename_file_owned(&id, &auth_user.id, &new_name).await { match mgmt.rename_file_owned(&id, &auth_user.id, &new_name).await {
Ok(file_dto) => (StatusCode::OK, Json(file_dto)).into_response(), Ok(file_dto) => (StatusCode::OK, Json(file_dto)).into_response(),
Err(err) => AppError::from(err).into_response() Err(err) => AppError::from(err).into_response(),
} }
} }
@@ -778,7 +775,7 @@ impl FileHandler {
.await .await
{ {
Ok(file) => (StatusCode::OK, Json(file)).into_response(), Ok(file) => (StatusCode::OK, Json(file)).into_response(),
Err(err) => AppError::from(err).into_response() Err(err) => AppError::from(err).into_response(),
} }
} }
@@ -797,7 +794,7 @@ impl FileHandler {
let mgmt = &state.applications.file_management_service; let mgmt = &state.applications.file_management_service;
match mgmt.move_file_owned(&id, &auth_user.id, folder_id).await { match mgmt.move_file_owned(&id, &auth_user.id, folder_id).await {
Ok(file_dto) => (StatusCode::OK, Json(file_dto)).into_response(), Ok(file_dto) => (StatusCode::OK, Json(file_dto)).into_response(),
Err(err) => AppError::from(err).into_response() Err(err) => AppError::from(err).into_response(),
} }
} }
@@ -856,9 +853,7 @@ impl FileHandler {
}) })
.collect(); .collect();
format!( format!("{disposition}; filename=\"{ascii_safe}\"; filename*=UTF-8''{encoded}")
"{disposition}; filename=\"{ascii_safe}\"; filename*=UTF-8''{encoded}"
)
} }
/// Build a 201 Created JSON response. /// Build a 201 Created JSON response.
+28 -16
View File
@@ -92,24 +92,36 @@ async function checkAuthentication() {
window.updateStorageUsageDisplay(userData); window.updateStorageUsageDisplay(userData);
refreshUserData().then(freshData => { // Validate session BEFORE loading files to avoid 401 race condition
if (freshData) { const freshData = await refreshUserData();
console.log('Storage usage updated from server'); if (freshData) {
} else { console.log('Storage usage updated from server');
// Session expired — try silent refresh first } else {
console.warn('Session may have expired, trying refresh...'); // Session expired — try silent refresh first
fetch('/api/auth/refresh', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', ...getCsrfHeaders() }, body: '{}' }) console.warn('Session may have expired, trying refresh...');
.then(r => r.ok ? refreshUserData() : Promise.reject(new Error('refresh failed'))) try {
.catch(() => { const r = await fetch('/api/auth/refresh', {
localStorage.removeItem(USER_DATA_KEY); method: 'POST',
window.location.href = '/login?source=session_expired'; credentials: 'same-origin',
}); headers: { 'Content-Type': 'application/json', ...getCsrfHeaders() },
body: '{}'
});
if (r.ok) {
await refreshUserData();
} else {
localStorage.removeItem(USER_DATA_KEY);
window.location.href = '/login?source=session_expired';
return;
}
} catch (err) {
localStorage.removeItem(USER_DATA_KEY);
window.location.href = '/login?source=session_expired';
return;
} }
}).catch(err => { }
console.warn('Could not refresh user data:', err);
});
resolveHomeFolder().then(() => window.loadFiles()); await resolveHomeFolder();
window.loadFiles();
} else { } else {
// No cached user data — must verify session from server // No cached user data — must verify session from server
console.log('No cached user data, fetching from server'); console.log('No cached user data, fetching from server');