Commit Graph

1543 Commits

Author SHA1 Message Date
Dionisio Pozo 970f97b91a Merge pull request #570 from swissiety/rfc-4331-quota-properties
feat(webdav): RFC 4331 quota-available-bytes/quota-used-bytes
2026-07-14 12:58:22 +02:00
Dionisio Pozo 3ecfddd6b8 Merge pull request #583 from EdouardVanbelle/doc/app-password
doc(app password): correct doc: webdav login via app password
2026-07-14 12:58:06 +02:00
Dionisio Pozo 0832f48791 Merge pull request #582 from EdouardVanbelle/security
security(auth): specify to agents that OIDC should never be bypassed
2026-07-14 12:23:33 +02:00
Edouard Vanbelle ebb11f19c2 doc(webdav): login is via app password 2026-07-14 12:21:51 +02:00
Dionisio Pozo 2f1bea165c Merge pull request #581 from EdouardVanbelle/vitepress
chore(vitepress): convert ../ references to DioCrafts/OxiCloud links
2026-07-14 12:16:32 +02:00
Dionisio Pozo bfc7a79423 Merge pull request #580 from EdouardVanbelle/feat/user-upgrade
feat(user upgrade): permit upgrade of in invited account into internal account
2026-07-14 12:11:11 +02:00
Edouard Vanbelle f738e3f442 security(auth): specify to agents that OIDC should never be bypassed 2026-07-14 12:07:30 +02:00
Edouard Vanbelle 33d0c460fd chore(vitepress): convert ../ references to github/DioCrafts/OxiCloud links
unblock site generation and link code reference to github DioCrafts/OxiCloud project
2026-07-14 12:02:12 +02:00
Edouard Vanbelle 1fa1966fbe feat(upgrate): add i18n for account upgade 2026-07-14 11:40:16 +02:00
Edouard Vanbelle f331dbf0ee feat(account): upgrade external to internal 2026-07-14 11:10:23 +02:00
Dionisio Pozo 5ae551a93d Merge pull request #577 from EdouardVanbelle/feat/users-perfs-and-filter-dotfiles
feat: users prefs server side + filter dotfiles + filter shares by resource type
2026-07-14 09:07:09 +02:00
Dionisio Pozo 17bfb57af7 Merge pull request #578 from EdouardVanbelle/feat/user-regisration-with-allow-list
feat(registration): add a domain allow list
2026-07-14 09:06:34 +02:00
Edouard Vanbelle 341e354162 test(frontend): correct test to fit sign up/in 2026-07-14 03:27:28 +02:00
Edouard Vanbelle 691d01a458 clippy 2026-07-14 03:18:04 +02:00
Edouard Vanbelle 3350e77080 fix(env): ensure .env is not taken when using --config 2026-07-14 03:16:27 +02:00
Edouard Vanbelle 0ad283e590 fix(magic-link): correct url to fit sveltekit 2026-07-14 03:16:27 +02:00
Edouard Vanbelle e94063d96a test(login/register): via password or magic-link
Password login

┌─────┬────────────────────────────────────────────────────┬────────────────────────┬─────────────────────────────────────────────────────────────────────────────────────────────┐
│  #  │                        Case                        │         Where          │                                          Assertion                                          │
├─────┼────────────────────────────────────────────────────┼────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────┤
│ L1  │ Login by username                                  │ auth_login.hurl Case 1 │ 200 + access_token, user.email match                                                        │
├─────┼────────────────────────────────────────────────────┼────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────┤
│ L2  │ Login by email (dispatch on @)                     │ auth_login.hurl Case 2 │ 200, same session shape as L1                                                               │
├─────┼────────────────────────────────────────────────────┼────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────┤
│ L3  │ Bad password on username path                      │ auth_login.hurl Case 3 │ 403 anti-enum                                                                               │
├─────┼────────────────────────────────────────────────────┼────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────┤
│ L4  │ Bad password on email path                         │ auth_login.hurl Case 4 │ 403 anti-enum (same shape as L3)                                                            │
├─────┼────────────────────────────────────────────────────┼────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────┤
│ L5  │ Unknown username                                   │ auth_login.hurl Case 5 │ 403 anti-enum (same shape as L3)                                                            │
├─────┼────────────────────────────────────────────────────┼────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────┤
│ L6  │ Unknown email                                      │ auth_login.hurl Case 6 │ 403 anti-enum (same shape as L3)                                                            │
├─────┼────────────────────────────────────────────────────┼────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────┤
│ L7  │ /api/auth/oidc/providers reports methods correctly │ auth_login.hurl Case 7 │ password_login_enabled: true, magic_link_login_enabled: true, require_verified_email: false │
└─────┴────────────────────────────────────────────────────┴────────────────────────┴─────────────────────────────────────────────────────────────────────────────────────────────┘

Password registration

┌─────┬───────────────────────────────────────────────────┬──────────────────────────────┬─────────────────────────────────────────────────────────┐
│  #  │                       Case                        │            Where             │                        Assertion                        │
├─────┼───────────────────────────────────────────────────┼──────────────────────────────┼─────────────────────────────────────────────────────────┤
│ R1  │ Classic username + email + password → uniform 200 │ registration.hurl Step 2     │ anti-enum message contains "request received"           │
├─────┼───────────────────────────────────────────────────┼──────────────────────────────┼─────────────────────────────────────────────────────────┤
│ R2  │ Login after register works                        │ registration.hurl Step 2b    │ 200 + session for the new user                          │
├─────┼───────────────────────────────────────────────────┼──────────────────────────────┼─────────────────────────────────────────────────────────┤
│ R3  │ Email collision → uniform 200 (no rewrite)        │ registration.hurl Steps 8-10 │ attacker password doesn't work; original account intact │
├─────┼───────────────────────────────────────────────────┼──────────────────────────────┼────────────────────────────┤
│ R4  │ Username collision → uniform 200                  │ registration.hurl Step 11    │ same anti-enum shape                                    │
├─────┼───────────────────────────────────────────────────┼──────────────────────────────┼────────────────────────────┤
│ R5  │ Off-domain rejection                              │ registration.hurl Step 12    │ 403 RegistrationDomainNotAllowed                        │
├─────┼───────────────────────────────────────────────────┼──────────────────────────────┼────────────────────────────┤
│ R6  │ Case-insensitive domain match                     │ registration.hurl Step 12b   │ uniform 200 on charlie@EXAMPLE.COM                      │
└─────┴───────────────────────────────────────────────────┴──────────────────────────────┴────────────────────────────┘

Magic-link registration (email-only signup)

┌─────┬──────────────────────────────────────────────────────────────────────────────────────────────────┬───────────────────────────────────────────────────┐
│  #  │                                               Case                                               │             Where             │                   Assertion                    │
├─────┼──────────────────────────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────┤
│ MR1 │ Email-only signup → welcome mail queued                                                          │ registration.hurl Step 3      │ uniform 200 + browser-binding cookie set       │
├─────┼──────────────────────────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────┤
│ MR2 │ Welcome mail contains magic-link URL                                                             │ registration.hurl Step 4      │ captured from mock SMTP                        │
├─────┼──────────────────────────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────┤
│ MR3 │ PR 22 cross-browser confirmation page                                                            │ registration.hurl Step 5a     │ 200 HTML "different browser"                   │
├─────┼──────────────────────────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────┤
│ MR4 │ Cookie-bound redemption lands on SPA                                                             │ registration.hurl Step 5b     │ 302 → /files (SvelteKit route, post-migration) │
├─────┼──────────────────────────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────┤
│ MR5 │ email_verified_at stamped after redemption                                                       │ registration.hurl Step 6      │ field present on /api/auth/me                  │
├─────┼──────────────────────────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────┤
│ MR6 │ Second magic-link post-signup                                                                    │ registration.hurl Step 7      │ uniform 200                                    │
├─────┼──────────────────────────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────┤
│ MR7 │ Profile PATCH — no-op, name set, empty-string rejected, username-taken 409, claim-once 409, etc. │ registration.hurl Steps 6a–6i │ full profile lifecycle                         │
└─────┴──────────────────────────────────────────────────────────────────────────────────────────────────┴───────────────────────────────────────────────────┘

Magic-link login (existing account)

┌─────┬──────────────────────────────────────────────────────────┬──────────────────────────────────────┬───────────────────────────────────────┐
│  #  │                           Case                           │                Where                 │                             Assertion                              │
├─────┼──────────────────────────────────────────────────────────┼──────────────────────────────────────┼───────────────────────────────────────┤
│ ML1 │ Baseline password login still works                      │ auth_magic_link_login.hurl Steps 1-2 │ 200                                                                │
├─────┼──────────────────────────────────────────────────────────┼──────────────────────────────────────┼───────────────────────────────────────┤
│ ML2 │ magic-link/send with email identifier                    │ auth_magic_link_login.hurl Step 3    │ uniform 200 + cookie                                               │
├─────┼──────────────────────────────────────────────────────────┼──────────────────────────────────────┼───────────────────────────────────────┤
│ ML3 │ magic-link/send with username identifier (dispatch on @) │ auth_magic_link_login.hurl Step 4    │ uniform 200                                                        │
├─────┼──────────────────────────────────────────────────────────┼──────────────────────────────────────┼───────────────────────────────────────┤
│ ML4 │ Password-user policy: mail actually sent                 │ auth_magic_link_login.hurl Step 5    │ SMTP capture proves permit_magic_link_for_password_users in effect │
├─────┼──────────────────────────────────────────────────────────┼──────────────────────────────────────┼───────────────────────────────────────┤
│ ML5 │ Redemption creates a session                             │ auth_magic_link_login.hurl Steps 6-7 │ 302 → /files, /api/auth/me returns the same user                   │
├─────┼──────────────────────────────────────────────────────────┼──────────────────────────────────────┼───────────────────────────────────────┤
│ ML6 │ Anti-enum on unknown identifier                          │ auth_magic_link_login.hurl Step 8    │ same uniform 200 shape as ML3                                      │
└─────┴──────────────────────────────────────────────────────────┴──────────────────────────────────────┴───────────────────────────────────────┘

OIDC

┌─────┬────────────────────────────────────────────────────────────────────────┬───────────────────┬────────────────────────────────────────────────────────────────────────────────────────────┐
│  #  │                                  Case                                  │       Where       │                                                        Assertion                                                        │
├─────┼────────────────────────────────────────────────────────────────────────┼───────────────────┼────────────────────────────────────────────────────────────────────────────────────────────┤
│ O1  │ Setup local admin (bootstrap)                                          │ oidc.hurl Step 1  │ 201                                                                                                                     │
├─────┼────────────────────────────────────────────────────────────────────────┼───────────────────┼────────────────────────────────────────────────────────────────────────────────────────────┤
│ O2  │ Providers endpoint — OIDC visible                                      │ oidc.hurl Step 2  │ enabled: true, provider_name: MockSSO, password_login_enabled: true, magic_link_login_enabled: false (OIDC-master rule) │
├─────┼────────────────────────────────────────────────────────────────────────┼───────────────────┼────────────────────────────────────────────────────────────────────────────────────────────┤
│ O2b │ Magic-link/send refused (endpoint layer)                               │ oidc.hurl Step 2b │ 403 MagicLinkLoginDisabled — proves the policy gate fires, not a 503 SMTP-unwired                                       │
├─────┼────────────────────────────────────────────────────────────────────────┼───────────────────┼────────────────────────────────────────────────────────────────────────────────────────────┤
│ O3  │ Authorize redirect includes PKCE + state                               │ oidc.hurl Step 3  │ 307 to fake IdP                                                                                                         │
├─────┼────────────────────────────────────────────────────────────────────────┼───────────────────┼────────────────────────────────────────────────────────────────────────────────────────────┤
│ O4  │ IdP round-trip + JIT provisioning                                      │ oidc.hurl Step 4  │ Callback lands on /login?oidc_code=…                                                                                    │
├─────┼────────────────────────────────────────────────────────────────────────┼───────────────────┼────────────────────────────────────────────────────────────────────────────────────────────┤
│ O5  │ Code exchange → session cookies                                        │ oidc.hurl Step 5  │ 200 + all three cookies                                                                                                 │
├─────┼────────────────────────────────────────────────────────────────────────┼───────────────────┼────────────────────────────────────────────────────────────────────────────────────────────┤
│ O6  │ JIT profile mapping (name, given/family, picture, groups → admin role) │ oidc.hurl Step 6  │ every claim reflected on /api/auth/me                                                                                   │
├─────┼────────────────────────────────────────────────────────────────────────┼───────────────────┼────────────────────────────────────────────────────────────────────────────────────────────┤
│ O7  │ Refresh rotation on OIDC session                                       │ oidc.hurl Step 7  │ new access/refresh/CSRF cookies                                                                                         │
├─────┼────────────────────────────────────────────────────────────────────────┼───────────────────┼────────────────────────────────────────────────────────────────────────────────────────────┤
│ O8  │ Refreshed cookies authenticate                                         │ oidc.hurl Step 8  │ 200 on /api/auth/me                                                                                                     │
├─────┼────────────────────────────────────────────────────────────────────────┼───────────────────┼────────────────────────────────────────────────────────────────────────────────────────────┤
│ O9  │ Repeat login = same local user (no dup)                                │ oidc.hurl Step 9  │ user_id stable                                                                                                          │
├─────┼────────────────────────────────────────────────────────────────────────┼───────────────────┼────────────────────────────────────────────────────────────────────────────────────────────┤
│ O10 │ Anti-takeover: unverified email → refused                              │ oidc.hurl Step 10 │ 401/403                                                                                                                 │
├─────┼────────────────────────────────────────────────────────────────────────┼───────────────────┼────────────────────────────────────────────────────────────────────────────────────────────┤
│ O11 │ One-time code replay refused                                           │ oidc.hurl Step 11 │ second /exchange → 401                                                                                                  │
└─────┴────────────────────────────────────────────────────────────────────────┴───────────────────┴────────────────────────────────────────────────────────────────────────────────────────────┘

test
2026-07-14 03:16:25 +02:00
Edouard Vanbelle 01da450cf6 feat(registration): add a domain allow list
add:
 - OXICLOUD_REGISTRATION_ALLOWED_EMAIL_DOMAINS to specify list of domains allowing a self registration
 - OXICLOUD_REQUIRE_VERIFIED_EMAIL=true|false
 - OXICLOUD_AUTH_METHODS=password,magic_link (login methods, OIDC is on top of this)
 - OXICLOUD_AUTH_POLICIES=permit_magic_link_for_password_users (OIDC is on top)
2026-07-14 02:43:39 +02:00
Edouard Vanbelle 3fe6af25f1 fix(loading): fix issue with sveltekit and scripts
fix issues like:
```
Executing inline script violates the following Content Security Policy directive 'script-src 'self''.
Either the 'unsafe-inline' keyword, a hash ('sha256-Vv9My0PApDW3C+xGLu9cH98KLrOg/Qhc7hlT1lK5tyM='),
or a nonce ('nonce-...') is required to enable inline execution. The action has been blocked.
```
2026-07-14 02:26:36 +02:00
Edouard Vanbelle b9d6fa39c0 feat(user-pref): revert view mode as user-prefs serverside
previous change is breaking playwright tests, need to check later changes
2026-07-13 23:16:09 +02:00
M.Schmidt c62f97d8f2 readd removed utility, cleanup/shorten enum usage 2026-07-13 22:22:12 +02:00
Edouard Vanbelle b18f0dc74a test(front): isolate dotfile e2e fixtures under scratch parents 2026-07-13 22:00:53 +02:00
Dionisio Pozo 899a08bebe Merge pull request #575 from EdouardVanbelle/doc/drive
docs(drive): add drive doc
2026-07-13 21:36:22 +02:00
Dionisio Pozo 2d49c3c8de Merge pull request #574 from EdouardVanbelle/fix/nextcloud-with-oidc-and-drive-picker
fix(nc): login OIDC + drive picker
2026-07-13 21:36:08 +02:00
Edouard Vanbelle 063382ad60 test(front): test dotfile view/hidden 2026-07-13 21:21:26 +02:00
Edouard Vanbelle 0296d157a3 chore: remove deprecated playwright wrapped 2026-07-13 21:21:26 +02:00
M.Schmidt 6701ddfc17 Merge branch 'main' into rfc-4331-quota-properties
# Conflicts:
#	src/interfaces/nextcloud/report_handler.rs
#	src/interfaces/nextcloud/webdav_handler.rs
#	tests/api/run.sh
2026-07-13 20:32:01 +02:00
Edouard Vanbelle 5aaf49859e feat(user-perf): add ui user-perf + dotfile filter
- add resource kind filter (file, folder, drive) in shared section (localStorage stored)
- add user preferences serverside store
- add client side dotfile filter (show/hide dotfiles) (user perf stored, default: dotfiles are shown)
  for security trashed dotfile are always displayed

  protection added: if a folder has only hidden items, a notification invite user to display it
  if a user rename or create a hidden item, a notification tells it to user
2026-07-13 20:27:52 +02:00
M.Schmidt e5c8d89da9 fix(webdav): bump storage usage on PUT, not just REST multipart upload
update_file_streaming_with_perms (the method behind every WebDAV/
NextCloud/WOPI PUT) never called the storage-usage-delta hook, so
drives.used_bytes and the RFC 4331 quota-used-bytes property never
reflected content written via WebDAV — only the REST multipart
upload path bumped usage. Extract apply_storage_usage_delta() from
maybe_update_storage_usage() and wire it into both branches: the
overwrite path applies new_size - old_size, the create path applies
the full size.

Also fixes the two RFC 4331 hurl tests that caught this:
nc_webdav_quota_properties.hurl had a Hurl parse error ([BasicAuth]
section keys can't mix literal+template, so the {user}~{folder}
composite marker is now pre-resolved via [Options] variable: before
being referenced as a single template), and both quota-properties
tests now retry the post-upload PROPFIND (matching the existing
drive_quota.hurl/user_envelope_quota.hurl pattern) since the delta
is applied fire-and-forget on a background task.
2026-07-13 20:00:01 +02:00
M.Schmidt 8a405af5e1 refactor(drive): match DriveKind directly instead of Drive::is_personal()
Drops the boolean is_personal() wrapper in favor of matching
DriveKind::Personal/Shared at the two call sites, matching the
exhaustive-match convention already used for DriveKind elsewhere
(as_str, parse, DriveKindDto::from).
2026-07-13 19:51:40 +02:00
Edouard Vanbelle fe66a379b7 docs(drive): add drive doc
documentation for users
2026-07-13 19:16:23 +02:00
Edouard Vanbelle 05ef55a8e0 fix(nc): login OIDC + drive picker
ensure OIDC is supported during nextcloud login

flow is:

    1. nextcloud
    2. oxicloud login ( direct pass or OIDC according config)
    3. drive picker (if user has multiple drive)
    4. success page + backchannel login to nextcloud
2026-07-13 18:30:20 +02:00
Dionisio Pozo 06da428493 Merge pull request #572 from EdouardVanbelle/feat/nextcloud-chrooted-drive
feat/nextcloud chrooted drive
2026-07-13 09:37:10 +02:00
Dionisio Pozo e71ef59a04 Merge pull request #571 from EdouardVanbelle/fix/cached-elements
fix(front): unregister cache prio to 0.8.0
2026-07-13 09:36:51 +02:00
Dionisio Pozo 22e09c09be Merge pull request #538 from swissiety/webdav-litmus-compliance
implement dead properties for nextcloud handler and fixup frontend migration leftover
2026-07-13 09:36:43 +02:00
Dionisio Pozo 8301e0c2cb Merge pull request #569 from EdouardVanbelle/feat/grants-cleanup
feat(grant): clean up expired grants
2026-07-13 09:36:29 +02:00
M.Schmidt c07aeabd85 feat(webdav): drive-aware RFC 4331 quota properties
resolve_quota only ever reported the caller's personal envelope,
ignoring the drive_id already resolved at every PROPFIND call site —
shared drives with their own quota showed the wrong numbers. Adds
AppState::resolve_webdav_quota, shared by both WebDAV surfaces:
nil drive_id or personal drive -> account envelope, shared drive ->
its own storage.drives quota/used_bytes.

Also adds quota-used-bytes/quota-available-bytes to the NextCloud-
compatible surface, which previously had no RFC 4331 support at all.

Registers webdav_quota_properties.hurl and the new
nc_webdav_quota_properties.hurl in tests/api/run.sh — neither was
wired into the suite before this change.
2026-07-13 00:34:17 +02:00
M.Schmidt fdef73380f fix(thumbnail): remove redundant reference in format! arg
clippy::useless_borrows_in_formatting
2026-07-12 22:30:45 +02:00
M.Schmidt 7011fdff5a Merge origin/main into webdav-litmus-compliance 2026-07-12 22:25:12 +02:00
Edouard Vanbelle 52814b4d7c fix(nextcloud): fix chroot + synchronisation
- add better hurl coverage on nextcloud chrooted login
    - fix issue with nextcloud using /{drive name}/~{drive id}/
    - fix trashbin handler

    confusion username vs {username}~{folder id}
2026-07-12 22:19:45 +02:00
Edouard Vanbelle 230927a80e fix(templates): ensure template use frontend css
this fix the nextcloud login + drive selector (chroot)
    fix also invitation / magic link

    also correct the UX: once user has logged in nextcloud, show an explicita page
2026-07-12 22:19:42 +02:00
Edouard Vanbelle f854da30f0 refactor: rustc 1.97.0 (useless borrows in formatting) 2026-07-12 22:04:21 +02:00
Edouard Vanbelle f4d2a7cd61 fix(front): unregister cache prio to 0.8.0
this fix issue https://github.com/AtalayaLabs/OxiCloud/issues/560

    previous version where caching assets, now sveltekit is fully autonomous,
    use a sw.js that clears the cache and unregisters it self
2026-07-12 22:02:11 +02:00
M.Schmidt 3cb12c6fc1 refactor(readability): and reducing condition evaluation twice on same condition 2026-07-12 21:04:37 +02:00
M.Schmidt f017c700f1 feat(webdav): add RFC 4331 quota-available-bytes/quota-used-bytes properties
Threads the caller's account-wide (used, available) storage figures through
PROPFIND for the plain-file WebDAV surface, resolved once per request via
StorageUsagePort::get_user_storage_info and reused for every folder entry
in the response. Unlimited accounts (quota <= 0) omit quota-available-bytes
entirely per RFC 4331 §3, rather than disclosing a sentinel value.
Properties are only advertised as known when the quota subsystem is enabled
and the lookup succeeds; otherwise they fall through to the standard 404
propstat.
2026-07-12 20:22:57 +02:00
Edouard Vanbelle b6df05f4d8 refactor: rustc 1.97.0 (useless borrows in formatting) 2026-07-12 18:53:39 +02:00
Edouard Vanbelle ba620166ee feat(grant): clean up expired grants 2026-07-12 18:37:13 +02:00
Dionisio Pozo 968c768dd4 Merge pull request #567 from moduvoice/i18n/complete-korean
i18n: complete Korean (ko) locale
2026-07-12 17:13:09 +02:00
moduvoice a3801f5836 i18n: complete Korean (ko) locale
The ko.json locale existed but was missing 377 of 1392 keys (~27%),
covering entire feature areas added since the initial translation:
admin plugin management, storage/OIDC/SMTP settings, photos, music
playlists, device pairing, search filters, share dialogs, and more.

Filled in all missing keys with natural Korean translations matching
the existing tone and terminology in the file (파일/폴더/공유/업로드
등), verified full key parity with en.json (1392/1392) and matching
{{placeholder}} interpolation tokens on every translated string.
2026-07-11 23:10:35 +07:00
Dionisio Pozo 9ed360443a Merge pull request #531 from illogic-al/fix/pdf-extract-panic-abort 2026-07-09 21:36:43 +02:00