Merge pull request #178 from jaredwolff/fix/initial-load-race-condition
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
const freshData = await refreshUserData();
|
||||||
if (freshData) {
|
if (freshData) {
|
||||||
console.log('Storage usage updated from server');
|
console.log('Storage usage updated from server');
|
||||||
} else {
|
} else {
|
||||||
// Session expired — try silent refresh first
|
// Session expired — try silent refresh first
|
||||||
console.warn('Session may have expired, trying refresh...');
|
console.warn('Session may have expired, trying refresh...');
|
||||||
fetch('/api/auth/refresh', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', ...getCsrfHeaders() }, body: '{}' })
|
try {
|
||||||
.then(r => r.ok ? refreshUserData() : Promise.reject(new Error('refresh failed')))
|
const r = await fetch('/api/auth/refresh', {
|
||||||
.catch(() => {
|
method: 'POST',
|
||||||
|
credentials: 'same-origin',
|
||||||
|
headers: { 'Content-Type': 'application/json', ...getCsrfHeaders() },
|
||||||
|
body: '{}'
|
||||||
|
});
|
||||||
|
if (r.ok) {
|
||||||
|
await refreshUserData();
|
||||||
|
} else {
|
||||||
localStorage.removeItem(USER_DATA_KEY);
|
localStorage.removeItem(USER_DATA_KEY);
|
||||||
window.location.href = '/login?source=session_expired';
|
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');
|
||||||
|
|||||||
Reference in New Issue
Block a user