Commit Graph

217 Commits

Author SHA1 Message Date
Dionisio 7737ed90c7 feat: folder ownership scoping, batch operations integration, frontend audit fixes
Backend:
- Add owner_id to Folder entity + FolderDto (DB user_id column)
- Add list_folders_by_owner to FolderRepository trait + PG impl
- Add list_folders_for_owner to FolderUseCase + FolderService
- Rewrite FolderHandler: all endpoints now scope by AuthUser
- Remove dead handler methods (list_folders_inner, list_folders_for_user, is_user_home_folder, folder_belongs_to_user)
- Add ownership check in get_folder (returns 404 on mismatch)

Batch operations:
- Add trash_service + zip_service to BatchOperationService
- New methods: trash_files, trash_folders, move_folders, download_zip
- New handlers: trash_batch, move_folders_batch, download_batch
- New routes: POST /api/batch/trash, /api/batch/folders/move, /api/batch/download

Frontend:
- Replace findUserHomeFolder (~130 lines) with resolveHomeFolder (~35 lines)
- Remove client-side folder filtering in loadFiles (backend now scopes)
- Rewrite batchDelete: N requests -> 1 POST /api/batch/trash
- Rewrite batchMove: N requests -> 2 POST max (files + folders)
- Rewrite batchDownload: N requests -> 1 POST /api/batch/download (ZIP)
- Search moved to backend, share system uses backend API
- Dark mode fixes, frontend audit improvements
2026-02-15 23:45:11 +01:00
Dionisio 6e1b77f244 chore: remove orphan test_cache.rs and unused memmap2 dependency 2026-02-15 21:20:20 +01:00
Dionisio 1527223d62 refactor: use typed Vec<Jwk> in JWKS parsing, remove double deserialize 2026-02-15 18:19:11 +01:00
Dionisio Pozo b59ff75489 Merge pull request #115 from gbw/feature/es256-jwks-support
Merged: ES256 JWKS support for OIDC
2026-02-15 18:09:36 +01:00
Dionisio 0be7ef8c0b style: fix clippy collapsible_if + cargo fmt 2026-02-15 18:04:32 +01:00
Dionisio 1ed20f425f perf: Phase 4+5 optimizations — uploads 10x, downloads 2x, concurrent 2x. moka cache, 512KB buffers, remove sync_all, hash-on-write, preloaded queries, bench.sh v3, gitignore storage/. 500MB upload 12.6s->1.3s (392MB/s). RSS 69-113MB, 0 swap. 2026-02-15 17:56:47 +01:00
George Wu 77c0dd5907 Add ES256 JWKS support for OIDC
- Simplified oidc_service.rs to use jsonwebtoken's DecodingKey::from_jwk()
- Automatically handles key type detection (RSA/EC) and algorithm detection
- Supports RS256, RS384, RS512, ES256, and ES384 algorithms
- Much cleaner code by leveraging library's built-in JWK parsing
2026-02-14 13:24:25 -08:00
Dionisio fac0b5e77b fix: critical bugs from deep audit
- Fix copy_files() data loss: implement real copy_file across full stack
  (FileWritePort, FileManagementUseCase, stubs, service, repository with
  atomic CTE + dedup ref_count increment, batch_operations caller)
- Fix plaintext password in replace_default_admin: hash password via
  PasswordHasherPort before User::new()
- Fix CalendarService hardcoded user_id: unify CalendarUseCase trait with
  explicit user_id parameter on all methods, remove zombie _for_user
  duplicates and hardcoded 'current_user_id', update 20 CalDAV handler
  call sites
- Previous session: migrate DedupService to PostgreSQL (storage.blobs),
  atomic CTEs with compensation for file/folder repository operations
2026-02-14 20:22:19 +01:00
Dionisio 3179e1dd91 quick fix 2026-02-14 19:30:49 +01:00
Dionisio e071841ec2 docs: update documentation to reflect 100% blob storage model
Rewrite documentation to match the new architecture where all
file metadata lives in PostgreSQL and content is stored as
content-addressed blobs via DedupService.

Updated files:
- internal-architecture.md: complete rewrite — new DB schema,
  blob repos (FolderDb, FileBlobRead/Write, TrashDb), updated
  DI container, service groups, architecture diagram, data flows
- file-system-safety.md: repurposed as storage-safety.md —
  covers PostgreSQL ACID guarantees + DedupService atomic writes
- caching-architecture.md: updated repo references to blob repos,
  removed write-behind cache section, updated upload/download flows
- trash-feature-summary.md: rewritten for soft-delete model
  (is_trashed flag, trash_items VIEW, TrashDbRepository)
- share-integration.md: clarified ShareFsRepository scope,
  updated DI snippet, added blob storage context note
- deduplication.md: updated DI snippet (dedup injected into repos)
- deployment.md: updated feature matrix (file storage requires DB)
- important-delta-sync-implementation.md: updated DI references

Removed legacy references: IdMappingPort, StorageMediator,
WriteBehindCache, FsFileRepository, FsFolderRepository,
TrashFsRepository, folder_ids.json, file_ids.json.
2026-02-14 18:27:30 +01:00
Dionisio 5d2bc36d74 upgrade docker 2026-02-14 18:13:05 +01:00
Dionisio bc01840fa4 chore: remove dead code from blob storage migration
Remove legacy abstractions that are no longer used after the
100% blob storage model migration (#113):

- IdMappingPort trait from application/ports/outbound.rs
- storage_mediator.rs module (StorageMediator trait + impls)
- StorageMediator impl from PathService
- write_behind_cache.rs (FS-based, incompatible with blob model)

The WriteBehindCachePort trait and Optional fields in services
are preserved for potential future blob-compatible caching.

-968 lines of dead code removed. Build clean, RC=0.
2026-02-14 18:10:37 +01:00
Dionisio 3c7c16f07e feat(#113): 100% blob storage model — PostgreSQL metadata + DedupService blobs
BREAKING CHANGE: Storage model completely rewritten. All file/folder
metadata now lives in PostgreSQL (storage schema). File content stored
as content-addressable blobs via DedupService. Filesystem directories
are no longer used for user storage.

New components:
- storage.folders / storage.files / storage.trash_items (PG schema)
- FolderDbRepository: virtual folders backed by PG
- FileBlobReadRepository: file reads via PG metadata + dedup blobs
- FileBlobWriteRepository: file writes via PG metadata + dedup blobs
- TrashDbRepository: soft-delete trash using is_trashed flags

Removed legacy FS components (~5500 lines deleted):
- FolderFsRepository, FileFsReadRepository, FileFsWriteRepository
- CompositeFileRepository, ParallelFileProcessor
- IdMappingService, IdMappingOptimizer, FileMetadataCache
- BufferPool, FileSystemUtils, RepositoryErrors
- TrashFsRepository, FolderFsRepositoryTrash

DI rewired: build_app_state() now requires PgPool (no FS fallback).
FileUploadService.new_with_read() and FileRetrievalService.new_with_cache()
constructors added for blob model (no write-behind needed).

Closes #113
2026-02-14 17:54:25 +01:00
Dionisio f25987e553 fix(#106): [SECURITY] scope recent files and favorites per user
Root cause: localStorage keys 'oxicloud_recent_files' and
'oxicloud_favorites' were global — shared across all users on the same
browser. When user A logged out and user B logged in, user B could see
(and access) user A's recent files and favorites.

Fixes applied:

recent.js:
- Storage key now user-specific: 'oxicloud_recent_files_{username}'
- getStorageKey() derives key from current user in localStorage
- migrateFromLegacyKey() moves data from old global key on init
- Legacy global key is always removed after migration

favorites.js:
- Same pattern: 'oxicloud_favorites_{username}'
- getStorageKey() + migrateFromLegacyKey() added

auth.js (logout):
- Clears user-specific recent and favorites keys before removing
  user data, plus removes any legacy global keys

Bumps service worker cache to v13.
2026-02-14 12:50:44 +01:00
Dionisio ebb0aee84e fix(#105): ensure folders are nested inside user home folder
Root cause: when window.app.currentPath was empty/falsy (due to timing,
page state reset, or initialization), the frontend sent parent_id: null.
The backend then created folders at the storage root instead of inside
the user's home folder.

Backend fix (folder_handler.rs):
- Added AuthUser extractor to create_folder handler
- When parent_id is None, auto-resolves the user's home folder
  ('My Folder - {username}') as the parent folder
- Folders are now always created inside the user's directory tree

Frontend fix (fileOperations.js):
- Changed parent_id fallback from null to window.app.userHomeFolderId
- Prevents sending null parent_id even if currentPath is reset

Search in subfolders: no fix needed — search_recursive() already
traverses the filesystem correctly; it was only failing because folders
were physically flat instead of nested.

Bumps service worker cache to v12.
2026-02-14 12:45:46 +01:00
Dionisio 9290aea591 fix(#107): trash/recent/favorites list view rendering bugs
Trash view:
- Fix 'Invalid Date': use item.trashed_at (ISO 8601) instead of item.deleted_at * 1000
- Fix literal i18n key 'files.file_types.file': determine type from file extension
  (pdf, image, video, audio, text, document) since trash DTO has no mime_type
- Fix column alignment: remove checkbox from trash header (not applicable),
  add .trash-header CSS class matching the 5-column grid layout

Recent view:
- Fix header/row column misalignment: add empty placeholder div for indicator
  column and .recent-header CSS class matching the 5-column grid
- Fix missing i18n key: use 'recent.accessed' instead of 'files.last_accessed'
- Fix default typeLabel not internationalized: use i18n.t('files.file_types.document')

Favorites view:
- Fix header/row column misalignment: add empty placeholder div for indicator
  column and .favorites-header CSS class matching the 5-column grid
- Fix default typeLabel not internationalized: use i18n.t('files.file_types.document')

Bump SW cache to v11.
2026-02-14 11:06:29 +01:00
Dionisio 1672889044 feat(#93): notification bell with upload progress
Replace the floating upload toast with a notification bell in the top bar
(between language selector and user avatar). All upload progress, completion,
and quota errors now flow through the bell dropdown panel.

- Add notification bell button with animated badge counter
- Dropdown panel shows per-file upload progress bars and overall batch progress
- Bell rings on new notifications when panel is closed
- Upload success/error states with color-coded icons
- Quota exceeded errors shown as notification items
- Clear all button to dismiss notifications
- Panel auto-opens when upload starts
- Full dark mode support
- Mutual exclusion with user menu (opening one closes the other)
- i18n keys for en/es (notifications.title, notifications.empty)
- SW cache bump to v10

Files:
- static/js/notifications.js (new module)
- static/index.html: bell markup + remove old toast
- static/css/style.css: bell + panel styles + dark mode
- static/js/fileOperations.js: redirect upload progress to notification bell
- static/js/app.js: close bell when user menu opens
- static/locales/{en,es}.json: i18n keys
- static/sw.js: cache v10 + notifications.js asset
2026-02-14 10:46:23 +01:00
Dionisio 3f60765d9a fix(#104): enforce storage quota on uploads & fix usage tracking
Backend:
- Add QuotaExceeded error kind mapped to HTTP 507 Insufficient Storage
- Add check_storage_quota() and get_user_storage_info() to StorageUsagePort
- Enforce quota in upload_file_with_cache, upload_file_with_thumbnails (AuthUser extractor)
- Enforce quota in chunked upload create_upload handler
- Add update_user_storage_usage_by_username() for username-based lookup
- Fix extract_username_from_path() to handle subfolders (take first segment only)
- Fix maybe_update_storage_usage() to use username-based lookup instead of passing
  username to get_user_by_id (which always failed silently)

Frontend:
- Parse and display quota error messages on upload failure (507 / QuotaExceeded)
- Stop remaining uploads when quota is exceeded
- Call refreshUserData() after uploads to update storage usage display
- Bump service worker cache to v9
2026-02-14 10:34:07 +01:00
Dionisio 1c5cf97cc5 security: fix audit vulnerabilities (RUSTSEC-2026-0007, RUSTSEC-2021-0141)
- Update bytes 1.11.0 -> 1.11.1 (fixes integer overflow in BytesMut::reserve, CVE-2026-25541)
- Replace unmaintained dotenv 0.15.0 with dotenvy 0.15.7 (RUSTSEC-2021-0141)
- Note: rsa 0.9.10 (RUSTSEC-2023-0071) has no patch yet, pulled transitively via jsonwebtoken
2026-02-14 01:37:03 +01:00
Dionisio 4c98c5a657 style: apply cargo fmt to entire codebase
Standardize code formatting across all 173 Rust source files
using rustfmt. No functional changes - purely cosmetic.

This establishes a consistent code style baseline for the
project going forward.
2026-02-14 01:29:34 +01:00
Dionisio 67137a3ef2 refactor: apply clippy auto-fixes (162 warnings resolved)
- Fix needless borrows and references
- Collapse nested if statements
- Replace manual strip_prefix with str::strip_prefix()
- Remove redundant closures in map/unwrap_or_else
- Use Iterator::next_back() instead of rev().next()
- Simplify map_or patterns
- Use std::io::Error::other() instead of new(ErrorKind::Other, ..)
- Use div_ceil() instead of manual ceiling division
- Consolidate format! string arguments
- Various other idiomatic Rust improvements

39 files changed, 220 insertions(+), 320 deletions(-)
2026-02-14 01:26:02 +01:00
Dionisio 516b8727d2 fix: dark mode toggle and search auth headers (#102)
Dark mode:
- Toggle now applies data-theme='dark' attribute to <html>
- Theme applied immediately on page load to prevent FOUC
- Comprehensive dark mode CSS covering all UI components:
  sidebar, top bar, search, file cards, list view, context
  menus, modals, dialogs, notifications, user menu, etc.

Search:
- Add Authorization headers to all search API fetch calls
  (searchFiles, advancedSearch, clearSearchCache)
- Fix missing checkbox column in search results list header
2026-02-14 00:45:45 +01:00
Dionisio 67968ed22d fix: size column alignment in list view (#101)
Use fixed column widths for Type (100px), Size (110px), and
Modified (160px) instead of flexible 1fr units that caused
values to overlap when content was wider than available space.
2026-02-14 00:22:37 +01:00
Dionisio 099d1d1c55 fix: share URL respects scheme in OXICLOUD_SERVER_HOST (#103)
When OXICLOUD_SERVER_HOST contains a full URL with scheme (e.g.
https://oxi.example.com), the share link no longer prepends
http:// or appends :port, avoiding malformed URLs like
http://https://host:8085/s/token.

- Add AppConfig::base_url() helper with smart URL construction
- Replace all 6 inline format!() calls in share_service.rs
- Replace inline construction in di.rs (admin settings base URL)
- Priority: OXICLOUD_BASE_URL > full-URL host > http://host:port
2026-02-14 00:18:59 +01:00
Dionisio 82dd7a5c56 feat: multi-select for batch file/folder actions (#100)
- Add checkboxes to list view items (grid view already had them)
- Add 'select all' checkbox in list view header
- Add batch action bar with Delete, Move, and Download buttons
- Batch delete: moves all selected items to trash in one operation
- Batch move: reuses existing move dialog in batch mode
- Batch download: downloads each selected item
- Keyboard shortcuts: Ctrl+A (select all), Escape (deselect), Delete key
- Shift+click for range selection in both grid and list views
- Selection state synced between grid and list views
- New multiSelect.js module manages selection state and batch operations
2026-02-14 00:12:18 +01:00
Dionisio 68d169266c fix: extend logo link to include OxiCloud wordmark (#97)
The clickable area now covers both the logo icon and the 'OxiCloud'
text on profile.html and admin.html. The page indicator (· Profile,
· Admin) remains outside the link as expected.
2026-02-13 23:03:40 +01:00
Dionisio 5bf1e4b607 chore: migrate to Rust Edition 2024
- Update edition from 2021 to 2024 in Cargo.toml
- Remove explicit `ref` bindings in pattern matches (di.rs, carddav_adapter.rs)
  Edition 2024 uses implicit ref binding modes
- Refactor folder_handler.rs: change `impl IntoResponse` return types to
  concrete `axum::response::Response` to avoid lifetime capture issues
  (Edition 2024 captures all in-scope lifetimes in `impl Trait`)
- All 101 tests pass, zero warnings
2026-02-13 23:00:16 +01:00
Dionisio 28a353e17e chore: bump version to 0.3.5 2026-02-13 22:45:59 +01:00
Dionisio 4560a8042c fix(routes): serve pages with clean URLs (no .html extension)
Add server-side routes for /profile, /admin, and /shared that
serve their respective HTML pages directly (same pattern already
used for /login). Updated all frontend references to use clean
URLs instead of .html extensions.

Fixes #99
2026-02-13 22:44:20 +01:00
Dionisio 6f3baabbb2 fix(ui): close About modal on ESC key press
Add keydown listener for Escape to dismiss the About OxiCloud
modal overlay, matching standard modal behavior.

Fixes #98
2026-02-13 22:38:26 +01:00
Dionisio c7fe075e7f fix(ui): make logo clickable on profile, admin, and shared pages
Follow-up to ce9971b — the logo was only fixed in index.html.
Now profile.html, admin.html, and shared.html also wrap their
logo elements in <a href='/'> links.

Fixes #97
2026-02-13 22:37:31 +01:00
Dionisio f58f5ab742 fix(ui): hide password change form when password login is disabled
The profile page checked user.auth_provider to hide the password
form, but the backend doesn't return that field. Now also queries
/api/auth/oidc/providers and hides the password section when
password_login_enabled is false.

Fixes #96
2026-02-13 22:35:26 +01:00
Dionisio 5771d82b21 refactor: remove Spanish folder naming convention, keep English only
Remove all 'Mi Carpeta - ' references from backend and frontend.
Only 'My Folder - {username}' is now recognized as the home folder
naming convention.
2026-02-13 22:33:27 +01:00
Dionisio 05135529ce fix(security): scope root folder listing to authenticated user
Non-admin users were seeing all users' root folders, including the
admin's. Three root causes fixed:

1. Backend: list_root_folders now extracts AuthUser and filters
   results so each user only sees their own home folder at the
   root level (folders matching 'My Folder - {username}' or
   'Mi Carpeta - {username}').

2. Frontend: findUserHomeFolder() searched only for the Spanish
   pattern 'Mi Carpeta - {username}' but the backend creates
   folders with the English pattern 'My Folder - {username}'.
   Now checks both naming conventions.

3. Frontend: when the home folder was not found, the code fell
   back to folderList[0] — which was usually the admin's folder.
   Removed that dangerous fallback; now shows empty root instead.

Fixes #94
2026-02-13 22:31:05 +01:00
Dionisio ce9971b9e4 fix(ui): make top-left logo a clickable link to home
Wrap the logo-container div in an <a href='/'> so clicking the
OxiCloud logo navigates back to the root/home view, matching the
widely accepted web convention.

Fixes #97
2026-02-13 22:22:57 +01:00
Dionisio d889e325ac fix: increase body limit to 10 GB to prevent upload truncation (#95)
Axum's default body limit for Multipart extraction is 2 MB.
OxiCloud never overrode this default, so any file upload larger
than ~2 MB was silently truncated.

Added DefaultBodyLimit::max(10 GB) both globally on the app
router and specifically on the file upload routes, matching the
chunked upload capability already in place for large files.
2026-02-13 22:13:24 +01:00
Dionisio aba7ea9d79 feat: add file upload progress toast with per-file tracking (#93)
Replaced the hidden dropzone-only progress bar with a floating
upload toast that appears at the bottom-right corner whenever
files are being uploaded (button or drag-and-drop).

Features:
- Per-file progress bar with real byte-level tracking via XHR
- Spinning icon while uploading, green check on success, red on error
- Overall progress bar and file counter in the footer
- Auto-hides 4 seconds after all uploads complete
- Dismiss button to minimise the toast
- Works for both file and folder uploads
- i18n keys added to all 8 locale files
- Service worker cache bumped to v3
2026-02-13 22:08:36 +01:00
Dionisio 40bf43b292 fix: cap default storage quota to available disk space (#92)
Previously the admin quota was hardcoded to 100 GB and the regular
user quota to 1 GB, regardless of actual disk capacity. On systems
with less than 100 GB free this produced misleading quota values.

Added the fs2 crate to query available disk space on the storage
filesystem. A new capped_quota() helper now returns
min(default_quota, available_disk_space) when assigning quotas
during user registration, admin setup, and OIDC provisioning.
2026-02-13 21:58:54 +01:00
Dionisio 177f82ca16 fix: make checkAuthentication async to fix app.js parse error (#90)
checkAuthentication() used await for the OIDC exchange fetch but was
declared as a regular function, not async. This caused a JavaScript
syntax error that prevented the entire app.js file from parsing,
resulting in a completely non-interactive frontend after OIDC login.

Also bumped service worker cache version to v2 so browsers
discard stale cached JS files on the next load.
2026-02-13 21:49:01 +01:00
Dionisio c384a08763 fix: fetch OIDC discovery before building authorization URL (#91)
get_authorize_url() was synchronous and fell back to constructing
{issuer}/authorize when the discovery cache was empty. This produced
incorrect URLs for providers like Keycloak whose authorization
endpoint is {issuer}/protocol/openid-connect/auth.

Made get_authorize_url() async so it can call get_discovery() to
fetch the real authorization_endpoint from .well-known/openid-configuration
before the first redirect. The discovery document is cached after the
initial fetch.
2026-02-13 21:42:41 +01:00
Dionisio 33ed3bd66c Fix: Complete OIDC login flow - exchange code for tokens on frontend
The backend redirected to /?oidc_code=<code> after successful OIDC auth,
but the frontend never exchanged this code for JWT tokens. The user
was redirected back to the login page every time.

- app.js: Detect oidc_code in URL params before token check, call
  POST /api/auth/oidc/exchange, store tokens, reload clean
- auth.js: Fallback handler if oidc_code lands on login page

Fixes #90
2026-02-13 21:33:45 +01:00
Dionisio d0d5489994 Fix: add default-run to Cargo.toml so cargo run works without --bin
Fixes the 'could not determine which binary to run' error.

Ref #80
2026-02-13 20:47:26 +01:00
Dionisio 53625776cb Fix: Add OIDC/SSO login button and hide password form when OIDC-only
- Added SSO login button on login page that appears when OIDC is configured
- Button shows provider name (e.g. 'Sign in with Authentik')
- When 'Disable password login' is enabled, hides password form and
  shows only the SSO button
- Added auth divider between password and SSO sections
- Added i18n keys (or, sso_login, sso_login_provider) for all 8 locales
- Fixed missing comma in it.json locale file

Fixes #88, Fixes #89
2026-02-13 19:59:13 +01:00
Dionisio c2c9bb700d Fix: Hide system directories (.blobs, .trash, .dedup_temp) from all users
- Added directory name filtering in folder_fs_repository.rs
- Filters out directories starting with '.' in list_folders, list_folders_paginated, and count_directory_items
- Matches existing file listing behavior
- Also added Italian language to popular languages list

Fixes #87
2026-02-13 19:46:38 +01:00
Dionisio Pozo f34edefea7 Merge pull request #86 from albanobattistella/main
Create Italian localization file it.json
2026-02-13 19:21:57 +01:00
Dionisio 1be3e4230a feat: admin can create users manually + disable registration (#85)
Backend:
- POST /api/admin/users — admin-only user creation endpoint
  - username & password required, email optional (auto-generated placeholder)
  - role, quota_bytes, active all configurable
  - creates personal folder automatically
- PUT /api/admin/users/{id}/password — admin password reset
- GET/PUT /api/admin/settings/registration — toggle public registration
  - Supports env var OXICLOUD_DISABLE_REGISTRATION override
  - Blocks POST /api/auth/register when disabled
- AdminCreateUserDto, AdminResetPasswordDto added to settings DTOs
- registration_enabled field added to DashboardStatsDto

Frontend (admin.html):
- 'Create User' button in Users tab with full modal form
  (username, password, email, role, quota)
- 'Reset Password' button per user in actions column
- 'Allow public self-registration' toggle in Dashboard > System
  with warning banner when disabled

Closes #85
2026-02-13 16:46:59 +01:00
albanobattistella 31e97f46f5 Create Italian localization file it.json
Added Italian localization for the application.
2026-02-13 16:31:47 +01:00
Dionisio 12ceea9e54 fix: select only filename (without extension) when renaming files
When renaming a file (e.g. image.png), the input now selects only
'image' instead of 'image.png', preventing accidental extension changes.
Folders still select the full name. Uses setSelectionRange(0, lastDot)
to position cursor selection up to the last dot.

Closes suggestion from issue #83 feedback.
2026-02-13 15:40:58 +01:00
Dionisio 11b7e15319 next todo 2026-02-13 13:22:58 +01:00
Dionisio df0ebc36f1 chore: bump version to 0.3.4 2026-02-13 12:38:41 +01:00