Profile page showed "Not Authenticated" because fetch calls to
/api/auth/me and /api/auth/change-password were missing
credentials: 'same-origin', preventing HttpOnly cookies from being sent.
The logout function fired a non-awaited POST /logout then immediately
redirected to /login. The login page's session probe would find the
cookies still valid and refresh the token, redirecting back to the app.
Fix by awaiting the fetch and clearing local state before redirect.
- Rate limit login (5/min), register (3/hr), refresh (10/min) per IP
- Account lockout after 5 consecutive failed logins (15 min cooldown)
- Fix stored XSS in admin panel (escapeHtml on all user-controlled data)
- All limits configurable via OXICLOUD_RATE_LIMIT_* / OXICLOUD_LOCKOUT_* env vars
- Zero new dependencies (uses existing moka crate for in-memory caches)
- Includes unit tests for lockout service
- Add breadcrumb navigation to move dialog for folder navigation
- Add 'Copy' button alongside 'Move' button in the dialog
- Implement copyFile and copyFolder functions in fileOps
- Add copy handler for batch operations
- Add CSS styles for btn-outline button (light and dark mode)
- Add translations for new dialog strings
fix: properly show home folder contents in move dialog
- Use effectiveParentId for all checks and rendering
- Show 'Select this folder' option for home folder
- Only show 'go to parent' when breadcrumb has items (navigated into subfolders)
fix: improve move dialog UX
- Hide breadcrumb at home folder level (not needed)
- Only show 'Select this folder' option after navigating into subfolders
- Show 'no subfolders' message when there are no folders to navigate
- Properly display subfolders for navigation
- Remove 'Root' option from move dialog (users move within their home folder)
- Show children of current folder instead of flat folder list
- Add breadcrumb navigation for folder browsing
- Add 'go to parent' navigation option
- Add 'select this folder' option to choose current location
- Add CSS styles for new navigation elements
- Add dark mode support for move dialog
- Add i18n translations for new strings (en, es)
- Add hamburger menu toggle button in top bar (visible on mobile only)
- Sidebar slides in/out from the left with smooth transition
- Add dark overlay when sidebar is open on mobile
- Close sidebar on overlay click, nav item click, or escape key
- Support RTL languages (sidebar slides from right)
- Add dark theme styles for toggle button
- Responsive breakpoint at 768px
- Add breadcrumb navigation with folder hierarchy display
- Create setCurrentSection() helper to centralize view state management
- Derive nav item section dynamically from DOM data-i18n attribute
- Remove remnant /shared page and consolidate to SPA sharedView
- Add search input to sharedView for client-side filtering
- Fix 'Go to Files' button to use switchToFilesView()
The admin user list now displays an OIDC/Local badge per user and
hides the password reset button for OIDC-provisioned accounts.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implement the Web Application Open Platform Interface (WOPI) protocol
to enable collaborative document editing with Collabora Online and
OnlyOffice through OxiCloud.
Backend:
- WOPI token service with HMAC-SHA256 signed access tokens
- WOPI lock service with in-memory lock management and expiry
- WOPI discovery service for auto-detecting editor capabilities
- WOPI HTTP handler: CheckFileInfo, GetFile, PutFile, Lock/Unlock
- File entity extended with owner_id for WOPI file-info responses
- Configuration via WOPI_* environment variables
- Services wired through DI in AppState
Frontend:
- WOPI editor component with modal and new-tab viewing modes
- Context menu integration for opening files in online editors
- Inline viewer integration for document preview
Infrastructure:
- Docker Compose file for local Collabora/OnlyOffice dev setup
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Added formatQuotaSize() function to display ∞ for unlimited (0) quota
- Added format_quota_size() Rust function matching JavaScript behavior
- Updated quota defaulting logic from '||' to '== null' check
- This allows 0 (unlimited) to pass through while defaulting to 10 GB
only when the value is null/undefined
- Call sites now use dedicated formatQuotaSize() or format_quota_size()
instead of options parameter for cleaner API
- Add breadcrumbPath array to app state for tracking folder hierarchy
- Rewrite updateBreadcrumb() to render full path: Home > folder > subfolder
- Each breadcrumb segment is clickable to navigate back to that level
- Current folder shown in bold (non-clickable), parent folders as links
- Reset breadcrumb path on tab switch, home navigation, and initial load
- Update navigateFolder/selectFolder to push to breadcrumb path
- Enhanced breadcrumb CSS with hover effects and dark mode support
- Fix recursive CTE: add missing RECURSIVE keyword in move_to_trash and restore_from_trash SQL queries (relation 'descendants' does not exist)
- Fix folder deletion: delete descendant files before folder to avoid 'duplicate key violates unique constraint idx_files_unique_name_at_root'
- Simplify trash model: only mark the folder as trashed, not child files (implicit trash via parent)
- Update trash_items view: filter to show only top-level trashed items
- Update schema.sql: change files.folder_id FK from ON DELETE SET NULL to ON DELETE CASCADE
- Fix trash view icons: folders and files now show correct visual icons (folder-icon, pdf-icon, etc.) in trash view
- Frontend refactoring: extract inline CSS/JS from admin.html and profile.html into dedicated external files
- Frontend cleanup: replace all inline style attributes with CSS classes
- Frontend cleanup: replace style.display JS
- Fix recursive CTE: add missing RECURSIVE keyword in move_to_trash and restore_from_trash SQL queries (relation 'descendants' d
Browsers send the full relative path (e.g. 'Screenshots/file.png') as
the multipart filename when uploading folders via webkitRelativePath.
The File entity rejects names containing '/' or '\', causing all files
in a folder upload to fail with 'Invalid file name'.
Three fixes:
- Backend: strip path components from multipart filename in file_handler,
keeping only the basename. Also prevents path-traversal attacks.
- Frontend (fileOperations.js): explicitly pass file.name as the third
argument to FormData.append() in uploadFolderFiles() to override the
browser's relative path.
- Frontend (ui.js): detect folder drops in drag-and-drop handlers by
checking webkitRelativePath, and route them to uploadFolderFiles()
instead of uploadFiles() so subfolders are created first.
Closes#121
- Fix 'folder_id is required' upload error by adding create_home_folder
through the full hexagonal architecture (trait, service, repository, auth)
- Fix double upload issue with _isUploading concurrency guard
- Fix Share context menu doing nothing (ID collision between sharedView
and main share dialog resolved with sv- prefix)
- Fix Compartidos tab duplicate headers and broken layout
- Add missing .shared-dialog CSS with dark mode support
- Fix dark mode white backgrounds on empty-state, shared-filters,
trash-actions, action-btn, and header
- Fix i18n key mismatches in sharedView
- Bump version to 0.4.1
Closes#120
V1: Add owner-scoped folder pagination (list_folders_by_owner_paginated)
- New method in FolderRepository trait, PG implementation, service & handler
- Prevents IDOR by filtering folder listings to authenticated user
V2: Enforce ownership checks on folder mutations
- rename_folder, move_folder, delete_folder now require caller_id
- Service verifies folder.owner_id == caller_id (returns 404 on mismatch)
- Propagated to folder_handler, batch_handler, batch_operations, webdav_handler
- delete_folder_with_trash upgraded from OptionalAuthUser to AuthUser
- download_folder_zip now checks ownership before streaming
V3: Fix XSS in frontend via DOM APIs
- sharedView.js: innerHTML → createElement + textContent
- contextMenus.js: innerHTML → DOM construction for share dialog
Cleanup: removed unused OptionalAuthUser import, updated all stubs/mocks
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.
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.
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.
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
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
- 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
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
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
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
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.
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
- 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
- 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
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.
- Add missing .confirm-dialog.active { display: flex; opacity: 1 } CSS rule.
The confirm dialog was created with display:none and the .active class
was added, but no CSS rule changed it to visible — so the user never
saw the confirmation prompt and delete appeared to do nothing.
- Capture file/folder target before closeContextMenu in delete handlers
to prevent null reference race condition (same as rename/share fix).