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
+21
View File
@@ -191,6 +191,27 @@ CREATE INDEX IF NOT EXISTS idx_device_codes_user_id
COMMENT ON TABLE auth.device_codes IS 'OAuth 2.0 Device Authorization Grant (RFC 8628) codes for DAV client authentication';
-- App Passwords (application-specific passwords for DAV clients with HTTP Basic Auth)
CREATE TABLE IF NOT EXISTS auth.app_passwords (
id VARCHAR(36) PRIMARY KEY,
user_id VARCHAR(36) NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
label VARCHAR(255) NOT NULL,
password_hash TEXT NOT NULL,
prefix VARCHAR(50) NOT NULL,
scopes VARCHAR(512) NOT NULL DEFAULT 'webdav,caldav,carddav',
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
last_used_at TIMESTAMP WITH TIME ZONE,
expires_at TIMESTAMP WITH TIME ZONE,
active BOOLEAN NOT NULL DEFAULT TRUE
);
CREATE INDEX IF NOT EXISTS idx_app_passwords_user_id
ON auth.app_passwords(user_id) WHERE active = TRUE;
CREATE INDEX IF NOT EXISTS idx_app_passwords_active
ON auth.app_passwords(user_id, active) WHERE active = TRUE;
COMMENT ON TABLE auth.app_passwords IS 'Application-specific passwords for DAV clients using HTTP Basic Auth';
-- ============================================================
-- 2. CALDAV SCHEMA (RFC 4791)
-- ============================================================