fix: URL-decode DAV paths with spaces + feat: app passwords for Basic Auth

Bug fix:
- URL-decode paths in extract_webdav_path(), extract_caldav_path(),
  extract_carddav_path() so folders with spaces (e.g. 'My Folder') no
  longer return 404 when accessed via encoded URIs (%20)
- Properly encode href values in PROPFIND/PROPPATCH/LOCK XML responses
- Decode Destination header in MOVE/COPY operations

New feature - App Passwords (API keys for DAV clients):
- POST /api/auth/app-passwords  → create (shows token once)
- GET  /api/auth/app-passwords  → list (prefix only)
- DELETE /api/auth/app-passwords/:id → revoke
- Auth middleware now accepts both Bearer JWT and Basic Auth
- Argon2 hashed, scoped (webdav/caldav/carddav), optional expiry
- Compatible with DAVx5, Thunderbird, rclone, curl

Tested: 12/12 E2E tests pass (create, list, WebDAV/CalDAV/CardDAV
Basic Auth, URL-decode with spaces, wrong password 401, revoke, post-
revoke 401).
This commit is contained in:
Dionisio
2026-03-01 20:34:12 +01:00
parent 48d853360e
commit 81987e9321
21 changed files with 963 additions and 68 deletions
+34
View File
@@ -541,6 +541,7 @@ impl AppServiceFactory {
wopi_lock_service: None,
wopi_discovery_service: None,
device_auth_service: None,
app_password_service: None,
};
// 9b. Wire admin settings service when auth is available
@@ -617,6 +618,37 @@ impl AppServiceFactory {
app_state.device_auth_service = Some(device_auth_svc);
tracing::info!("Device Authorization Grant (RFC 8628) service initialized");
}
// 9d. Wire App Password service
{
use crate::application::services::app_password_service::AppPasswordService;
use crate::infrastructure::repositories::AppPasswordPgRepository;
let app_pw_repo: Arc<dyn crate::application::ports::auth_ports::AppPasswordStoragePort> =
Arc::new(AppPasswordPgRepository::new(pool.clone()));
let hasher: Arc<dyn crate::application::ports::auth_ports::PasswordHasherPort> =
Arc::new(
crate::infrastructure::services::password_hasher::Argon2PasswordHasher::new(
self.config.auth.hash_memory_cost,
self.config.auth.hash_time_cost,
self.config.auth.hash_parallelism,
),
);
let user_repo: Arc<dyn crate::application::ports::auth_ports::UserStoragePort> =
Arc::new(crate::infrastructure::repositories::UserPgRepository::new(
pool.clone(),
));
let base_url = self.config.base_url();
let app_pw_svc = Arc::new(AppPasswordService::new(
app_pw_repo,
hasher,
user_repo,
base_url,
));
app_state.app_password_service = Some(app_pw_svc);
tracing::info!("App Password service initialized");
}
}
// 10. Wire CalDAV/CardDAV services
@@ -812,6 +844,8 @@ pub struct AppState {
Option<Arc<crate::infrastructure::services::wopi_discovery_service::WopiDiscoveryService>>,
pub device_auth_service:
Option<Arc<crate::application::services::device_auth_service::DeviceAuthService>>,
pub app_password_service:
Option<Arc<crate::application::services::app_password_service::AppPasswordService>>,
}
// All AppState construction is done via struct literal in build_app_state().