diff --git a/src/application/dtos/pagination.rs b/src/application/dtos/pagination.rs index 67fac807..802dfe06 100755 --- a/src/application/dtos/pagination.rs +++ b/src/application/dtos/pagination.rs @@ -93,7 +93,7 @@ impl PaginatedResponseDto { page_size, total_items, total_pages, - has_next: page < total_pages - 1, + has_next: total_pages > 0 && page < total_pages - 1, has_prev: page > 0, }; diff --git a/src/common/config.rs b/src/common/config.rs index 42dc6ee4..dd74f372 100755 --- a/src/common/config.rs +++ b/src/common/config.rs @@ -604,30 +604,31 @@ impl AppConfig { } // Auth configuration - if let Ok(jwt_secret) = env::var("OXICLOUD_JWT_SECRET") { - if !jwt_secret.is_empty() { - // SECURITY: Validate JWT secret minimum entropy (RFC 7518 §3.2 - // recommends ≥256 bits for HS256). Panic on dangerously short - // secrets, warn on sub-optimal ones. - let len = jwt_secret.len(); - if config.features.enable_auth && len < 16 { - panic!( - "FATAL: OXICLOUD_JWT_SECRET is dangerously short ({} bytes). \ - Minimum: 32 bytes (256 bits) for HS256. \ - Generate a secure secret with: openssl rand -hex 32", - len - ); - } else if config.features.enable_auth && len < 32 { - tracing::warn!("=========================================================="); - tracing::warn!( - "OXICLOUD_JWT_SECRET is only {} bytes — recommended minimum is 32 (256 bits).", - len - ); - tracing::warn!("Generate a stronger secret with: openssl rand -hex 32"); - tracing::warn!("=========================================================="); - } - config.auth.jwt_secret = jwt_secret; + if let Some(jwt_secret) = env::var("OXICLOUD_JWT_SECRET") + .ok() + .filter(|s| !s.is_empty()) + { + // SECURITY: Validate JWT secret minimum entropy (RFC 7518 §3.2 + // recommends ≥256 bits for HS256). Panic on dangerously short + // secrets, warn on sub-optimal ones. + let len = jwt_secret.len(); + if config.features.enable_auth && len < 16 { + panic!( + "FATAL: OXICLOUD_JWT_SECRET is dangerously short ({} bytes). \ + Minimum: 32 bytes (256 bits) for HS256. \ + Generate a secure secret with: openssl rand -hex 32", + len + ); + } else if config.features.enable_auth && len < 32 { + tracing::warn!("=========================================================="); + tracing::warn!( + "OXICLOUD_JWT_SECRET is only {} bytes — recommended minimum is 32 (256 bits).", + len + ); + tracing::warn!("Generate a stronger secret with: openssl rand -hex 32"); + tracing::warn!("=========================================================="); } + config.auth.jwt_secret = jwt_secret; } // SECURITY: Auto-persist JWT secret to storage so it survives restarts. @@ -642,10 +643,7 @@ impl AppConfig { let persisted = persisted.trim().to_string(); if persisted.len() >= 32 { config.auth.jwt_secret = persisted; - tracing::info!( - "JWT secret loaded from {}", - secret_file.display() - ); + tracing::info!("JWT secret loaded from {}", secret_file.display()); } else { tracing::warn!( "Persisted JWT secret too short ({}B), regenerating", @@ -664,8 +662,7 @@ impl AppConfig { use rand_core::{OsRng, RngCore}; let mut key = [0u8; 32]; OsRng.fill_bytes(&mut key); - let generated_secret: String = - key.iter().map(|b| format!("{:02x}", b)).collect(); + let generated_secret: String = key.iter().map(|b| format!("{:02x}", b)).collect(); // Persist to storage volume so it survives container restarts if let Err(e) = std::fs::write(&secret_file, &generated_secret) { diff --git a/src/common/di.rs b/src/common/di.rs index 0e5e4925..8e4f434b 100755 --- a/src/common/di.rs +++ b/src/common/di.rs @@ -612,7 +612,6 @@ impl AppServiceFactory { path_resolver: None, webdav_lock_store: crate::infrastructure::services::webdav_lock_service::create_webdav_lock_store(), - }; // 9b. Wire admin settings service when auth is available @@ -893,7 +892,6 @@ pub struct AppState { Option>, pub webdav_lock_store: Arc, - } // All AppState construction is done via struct literal in build_app_state(). diff --git a/static/css/components/csp-utilities.css b/static/css/components/csp-utilities.css new file mode 100644 index 00000000..cb25807d --- /dev/null +++ b/static/css/components/csp-utilities.css @@ -0,0 +1,150 @@ +/* CSP-compliant utility classes — replaces inline style="" attributes */ + +/* ── Empty state icons (large, muted) ── */ +.empty-state-icon { + font-size: 48px; + color: #ddd; + margin-bottom: 16px; +} +.empty-state-icon.error { + color: #f44336; +} +.empty-state-icon.spinner { + color: #666; +} + +/* ── Dialog header icons (accent color) ── */ +.dialog-header-icon { + color: #ff5e3a; +} + +/* ── Icon spacing ── */ +.icon-mr { margin-right: 5px; } +.icon-ml { margin-left: 4px; font-size: 12px; } + +/* ── Success check icon ── */ +.check-icon { color: #48bb78; } + +/* ── Move dialog ── */ +.move-dialog-hint { + margin: 0 0 12px; + color: #718096; + font-size: 14px; +} +.folder-select-container { + max-height: 220px; + overflow-y: auto; +} + +/* ── Share dialog sections ── */ +.share-section { + margin: 15px 0; +} + +/* ── Search spinner ── */ +.search-spinner { + margin-right: 8px; +} + +/* ── Search empty state text ── */ +.search-empty-text { + color: var(--text-secondary, #64748b); +} + +/* ── Notification upload current file ── */ +.notif-upload-current { + font-size: 11px; + color: #64748b; + margin: 3px 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* ── Login auth hint ── */ +.auth-hint { + color: var(--text-secondary, #666); + margin-top: 4px; + display: block; +} + +/* ── About modal (userMenu.js profile popup) ── */ +.about-modal-body { max-width: 380px; } +.about-modal-header { text-align: center; padding: 20px 20px 0; } +.about-modal-avatar { + width: 64px; + height: 64px; + border-radius: 50%; + background: linear-gradient(135deg, #3b82f6, #6366f1); + color: #fff; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 24px; + font-weight: 700; + margin-bottom: 12px; +} +.about-modal-username { margin: 0; font-size: 18px; color: #1a1a2e; } +.about-modal-email { margin: 4px 0 0; font-size: 13px; color: #64748b; } +.about-modal-role { + display: inline-block; + margin-top: 8px; + padding: 2px 10px; + border-radius: 10px; + font-size: 11px; + font-weight: 600; +} +.about-modal-role-admin { background: #dbeafe; color: #1d4ed8; } +.about-modal-role-user { background: #f1f5f9; color: #64748b; } +.about-modal-storage { padding: 16px 20px; } +.about-modal-storage-label { + font-size: 12px; + color: #64748b; + text-transform: uppercase; + letter-spacing: .05em; + margin-bottom: 6px; +} +.about-modal-storage-label i { margin-right: 4px; } +.about-modal-bar-bg { + background: #f1f5f9; + border-radius: 6px; + height: 8px; + overflow: hidden; + margin-bottom: 4px; +} +.about-modal-bar-fill { + height: 100%; + border-radius: 6px; + transition: width .3s; +} +.about-modal-bar-text { font-size: 12px; color: #64748b; text-align: right; } +.about-modal-footer { padding: 0 20px 16px; display: flex; justify-content: center; } +.about-modal-close-btn { + padding: 8px 24px; + border: 1px solid #e2e8f0; + border-radius: 8px; + background: #fff; + color: #334155; + font-size: 13px; + font-weight: 600; + cursor: pointer; + transition: background .15s; +} + +/* ── Dark theme overrides ── */ +[data-theme="dark"] .empty-state-icon { color: #475569; } +[data-theme="dark"] .empty-state-icon.error { color: #ef4444; } +[data-theme="dark"] .about-modal-username { color: #f1f5f9; } +[data-theme="dark"] .about-modal-email { color: #94a3b8; } +[data-theme="dark"] .about-modal-role-admin { background: #1e3a5f; color: #60a5fa; } +[data-theme="dark"] .about-modal-role-user { background: #334155; color: #94a3b8; } +[data-theme="dark"] .about-modal-storage-label { color: #94a3b8; } +[data-theme="dark"] .about-modal-bar-bg { background: #334155; } +[data-theme="dark"] .about-modal-bar-text { color: #94a3b8; } +[data-theme="dark"] .about-modal-close-btn { + background: #1e293b; + border-color: #334155; + color: #cbd5e1; +} +[data-theme="dark"] .move-dialog-hint { color: #94a3b8; } +[data-theme="dark"] .search-empty-text { color: #94a3b8; } diff --git a/static/css/components/icons.css b/static/css/components/icons.css new file mode 100644 index 00000000..f2ba2023 --- /dev/null +++ b/static/css/components/icons.css @@ -0,0 +1,19 @@ +/* SVG icon base styles (replaces inline