Commit Graph

490 Commits

Author SHA1 Message Date
BillionClaw 2daee68d20 fix(auth): resolve CSP blocking and session refresh loop
Fix two issues causing login loop after successful admin setup:

1. CSP blocking inline styles: The frontend JavaScript dynamically sets
   inline styles (e.g., element.style.display = 'none') for UI state
   management. The CSP header only allowed 'self' for style-src, blocking
   these dynamic styles. Added 'unsafe-inline' to style-src directive.

2. Session refresh 401 errors: The cookie Secure flag defaulted to true
   when OXICLOUD_BASE_URL was not set, causing cookies to not be sent
   over HTTP in Docker deployments. Changed the default to false when
   the base URL is not explicitly set to HTTPS, with clear logging to
   guide users to set OXICLOUD_COOKIE_SECURE=true for production.

Fixes #203
2026-03-17 06:14:02 +08:00
Dionisio Pozo c669c24036 Merge pull request #210 from BillionClaw/clawoss/docs/feature-status-table 2026-03-16 22:59:36 +01:00
BillionClaw 2d4fa1e17f docs(readme): add feature status table 2026-03-17 05:47:09 +08:00
Dionisio Pozo 907ffaa0a3 Merge pull request #209 from BillionClaw/clawoss/fix/armv7-overflow 2026-03-16 21:55:57 +01:00
BillionClaw 0cbbb6b7ac fix: ARMv7 32-bit compilation overflow
The constant 10 * 1024 * 1024 * 1024 (10 GB) overflows on 32-bit systems
where usize is 32-bit (max ~4GB). This caused compilation failures on
ARMv7 architecture.

Fix by using architecture-appropriate limits:
- 64-bit: 10 GB (unchanged)
- 32-bit: 1 GB (safe maximum for 32-bit usize)

Fixes #206
2026-03-17 04:39:18 +08:00
Dionisio Pozo c9ca1f8885 Merge pull request #208 from BillionClaw/clawoss/fix/calendar-owner-id-uuid-type 2026-03-16 20:18:47 +01:00
BillionClaw 7645792d5a fix(calendar): change owner_id from String to Uuid type
The database schema defines owner_id as UUID, but the Calendar entity
stored it as String. This caused a type mismatch error when creating
calendars via CalDAV clients like DAVx:

  column "owner_id" is of type uuid but expression is of type text

Changes:
- Change Calendar.owner_id from String to Uuid
- Update new() and with_id() to accept Uuid
- Update owner_id() getter to return &Uuid
- Update belongs_to() to accept &Uuid
- Update calendar_storage_adapter to pass Uuid directly
- Update tests to use Uuid::new_v4()

Fixes #200
2026-03-17 02:58:34 +08:00
Dionisio Pozo 036a345b4e Merge pull request #207 from jaredwolff/fix/nextcloud-sync-conflict
Fix Nextcloud sync conflict caused by static UUID ETags
2026-03-15 20:28:39 +01:00
Jared Wolff b6bcb7d366 Fix RUSTSEC-2026-0037: update quinn-proto to 0.11.14
Patch denial-of-service vulnerability where invalid QUIC transport
parameters could cause a panic in quinn-proto.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 14:42:34 -04:00
Jared Wolff 1fcd02a519 Fix Nextcloud sync conflict by using content-hash ETags
The Nextcloud Android client compares ETags before and after upload to
verify its write landed. OxiCloud was returning the stable file UUID as
the ETag, which never changed on content updates, causing false
SYNC_CONFLICT errors on every upload.

Five fixes applied:
1. Thread blob_hash (SHA-256) through File entity, FileDto, all read/write
   queries, and all WebDAV/PROPFIND responses as the ETag — changes on
   every content update, no DB migration needed.
2. Honor X-OC-Mtime header: parse the client-supplied mtime and use it
   for updated_at via COALESCE(to_timestamp($n), NOW()).
3. Disable phantom checksum capability (preferredUploadType/supportedTypes)
   that the server never actually implemented, stopping retry loops.
4. Add nc:creation_time and nc:upload_time to PROPFIND responses.
5. Return oc-etag header in chunked upload MOVE (assemble) responses.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 14:14:24 -04:00
Dionisio Pozo 5fb3e31ad7 Merge pull request #202 from JVMerkle/bugfix/cal-color-validation 2026-03-14 16:35:57 +01:00
Julian Merkle 85663ef050 calendar.rs: Allow RGBA colors
The android app "DAVx" creates calendars with RGBA color codes.

fixes #199
2026-03-14 15:48:11 +01:00
Dionisio Pozo 04be94026e Merge pull request #198 from nk-designz/helm-chart 2026-03-12 17:20:02 +01:00
Nico Kahlert 080b87a277 Add documentation for the helm chart 2026-03-12 13:07:19 +01:00
Nico Kahlert a166bd4a99 Created a helm chart for oxicloud with optional wopi 2026-03-12 12:48:22 +01:00
Dionisio Pozo f2cf6e0193 Merge pull request #197 from raenur/documentation-suggestions
Documentation suggestions for accessing oxicloud remotely after first install
2026-03-11 10:37:38 +01:00
Nathan Shepperd 017e82aca0 Update README.md 2026-03-11 08:48:14 +00:00
Nathan Shepperd 769c95fac9 Update example.env
Note in example.env file about accessing remotely via the server's domain name
2026-03-11 08:46:12 +00:00
Dionisio Pozo 5b7ca30060 Merge pull request #195 from jaredwolff/fix/migration-media-sort-date
fix(migrations): handle pre-existing tables missing media_sort_date
2026-03-11 08:38:53 +01:00
Dionisio Pozo f737dc09ce Merge pull request #196 from jaredwolff/fix/ui-fixes
fix(ui): photos view bleeding into trash view
2026-03-11 08:38:03 +01:00
Jared Wolff a0ee538cb7 fix(docker): remove target-cpu=native from Dockerfile
target-cpu=native causes SIGILL in CI because the build script is
compiled with CPU-specific instructions that the Docker builder
doesn't support. Docker images should use generic CPU targets for
portability.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 21:24:49 -04:00
Jared Wolff 9fbc6bf2a4 fix(ui): photos view bleeding into trash view
The trash nav handler manually set view flags instead of calling
setCurrentSection('trash'), which meant the photos container was
never hidden when switching to trash. Use the central view-switching
function so all other views (photos, shared) are properly cleaned up.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 21:22:53 -04:00
Jared Wolff 9ec0f0e0d6 fix(migrations): add ALTER TABLE fallback for media_sort_date column
When the storage.files table already exists from a manual migration,
CREATE TABLE IF NOT EXISTS is skipped entirely, so the new
media_sort_date column is never added. Add an ALTER TABLE ADD COLUMN
IF NOT EXISTS immediately after the CREATE TABLE to handle both fresh
and pre-existing databases.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 21:17:34 -04:00
Dionisio Pozo 7e67db8c98 Merge pull request #192 from jaredwolff/chore/update-claude-md
chore: add pre-commit checks to CLAUDE.md
2026-03-10 11:18:13 +01:00
Dionisio Pozo f1cfed248a Merge pull request #191 from jaredwolff/feat/sql-migration-system
feat: add SQL migration system using sqlx::migrate!()
2026-03-09 20:54:42 +01:00
Jared Wolff f6e2b30688 fix(ci): update references from db/schema.sql to sqlx migrations
CI and Docker builds broke after removing db/ in favor of migrations/.
- ci.yml: point psql init at migrations/20260307000000_initial_schema.sql
- Dockerfile: COPY migrations instead of db; remove unused db copy in final stage
- docker-build.yml: remove db/schema.sql presence check

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 15:43:15 -04:00
Jared Wolff 66c5456a42 chore: add pre-commit checks and update schema refs in CLAUDE.md
- Add pre-commit section: cargo fmt then cargo clippy before committing
- Update db/schema.sql references to migrations/ (sqlx migrations)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 15:35:01 -04:00
Jared Wolff 036390a242 feat: add SQL migration system using sqlx::migrate!()
Replace manual schema.sql application with sqlx's built-in migration
system. Migrations are embedded at compile time and tracked in the
_sqlx_migrations table. Pending migrations run automatically on startup.

- Move db/schema.sql → migrations/20260307000000_initial_schema.sql
- Remove apply_schema() and split_sql_statements() from db.rs
- Add run_migrations() using sqlx::migrate!() macro
- Remove docker-compose schema.sql mount (app handles it now)
- Enable sqlx "migrate" feature in Cargo.toml

Future schema changes: add a new timestamped .sql in migrations/.

Closes #190

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 15:30:52 -04:00
Dionisio Pozo 47638224f3 Merge pull request #188 from zjean/fix/ci-clippy-fmt
fix: resolve clippy and rustfmt CI failures
2026-03-09 14:48:46 +01:00
zjean 18518bedaf fix: resolve clippy warnings and rustfmt issues for CI compliance
Fix all clippy lints (collapsible if, clone on Copy, needless borrow,
redundant bindings, unused params) and apply rustfmt across the codebase.
Update test mocks to match Uuid-based trait signatures.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 14:34:07 +01:00
Diocrafts cf9fe82b5f chore: bump version to v0.5.2 2026-03-09 00:11:47 +01:00
Diocrafts df336da679 feat(frontend): i18n expansion, admin/profile i18n, grid/list view fix, empty state
- Add 5 new locales (hi, ar, ru, ja, ko) — now 14 total
- Admin panel: 117 i18n keys, confirm modal, animated tabs, no inline handlers
- Profile page: 58 i18n keys with data-i18n attributes
- Fix i18n safeT() shadowing bug and translationsLoaded timing
- Fix grid/list view: list header no longer shows in grid mode on login
- Fix classList.toggle hidden sync for view switching across all nav functions
- Revert .hidden important that broke login page rendering
- Add files empty state (no_files + empty_hint) with translations
- Fix language selector dropdown scroll and styling
- Fix admin panel scroll with sticky tabs
2026-03-09 00:08:34 +01:00
Diocrafts f409a9edd7 perf(frontend): add build.rs asset pipeline with oxc + lightningcss
- Bundle 31 JS files → single app.{hash}.js (oxc minifier)
- Bundle 36 CSS files → single app.{hash}.css (lightningcss)
- Resolve CSS @import chains at build time
- Inline theme-init.js to eliminate render-blocking script
- Minify all individual JS/CSS/JSON assets in static-dist/
- Auto-update Service Worker cache manifest with bundle hashes
- FNV hash-based cache-busting filenames

Results: 88 → 12 requests, 640 kB → 96.5 kB transferred (-85%)

Build modes:
- Debug: copies HTML to OUT_DIR, serves original static/
- Release: generates static-dist/ with processed assets

Also:
- Update Dockerfile to include build.rs in cacher stage
- Serve static-dist/ in release Docker builds
- Remove host static/ bind mount from docker-compose
- Switch include_str!() to OUT_DIR for all HTML pages
2026-03-08 13:10:38 +01:00
Dionisio Pozo 75979d280a Add lightcss to frontend tasks 2026-03-08 00:04:36 +01:00
Diocrafts 49ca01ea1c fix(schema): add DROP TRIGGER IF EXISTS before CREATE TRIGGER trg_sync_media_sort_date
The CREATE TRIGGER statement is not idempotent — it fails on restart
when the trigger already exists. Add the same DROP/CREATE guard used
by trg_files_decrement_blob_ref.
2026-03-07 20:55:23 +01:00
Diocrafts 2bc77a0bb1 perf(photos): append-only render eliminates DOM rebuild on scroll
Replace the innerHTML full-rebuild in _render() with two paths:
- _renderFull(): used for first load, group-mode change, and deletions
- _appendBatch(n): append-only for infinite-scroll pages — O(batch)
  instead of O(total). Existing <img> nodes are never destroyed,
  eliminating the visual flash and unnecessary DOM churn.

Also:
- Extract _renderTile() helper (DRY tile HTML generation)
- Extract _observeSentinel() helper
- Scope _setupVideoThumbnails(startIndex) to only process new tiles
- Add data-group attribute on headers for efficient CSS.escape lookup
- Fix stale WebP references in comments (now JPEG)
- Add virtual scrolling idea to TODO-LIST.md for future evaluation
2026-03-07 20:46:04 +01:00
Diocrafts b8638f5131 perf(thumbs): switch thumbnail encoding from WebP to JPEG q=80
Replace all 3 ImageFormat::WebP encode sites with JpegEncoder q=80.
Update fast-path to detect JPEG SOI instead of RIFF/WEBP magic.
Change file extension .webp -> .jpg, Content-Type headers, and
browser toBlob. Remove unused ImageFormat import and stale comments.
The webp feature stays for DECODING uploaded WebP images.
2026-03-07 20:37:19 +01:00
Diocrafts 05108d3e12 perf: denormalize media_sort_date + pre-scale video thumbs
Schema (media_sort_date denormalization):
- Add media_sort_date column to storage.files with DEFAULT created_at
- Add trigger sync_media_sort_date: when file_metadata is upserted,
  copies COALESCE(captured_at, created_at) into files.media_sort_date
- Add partial index idx_files_media_timeline on (user_id, media_sort_date DESC)
  WHERE NOT is_trashed AND media type -- enables Index Scan + Limit (no Sort)
- copy_folder_tree now copies media_sort_date for copied files
- Remove dead idx_file_metadata_captured (no longer needed)

Query optimization (list_media_files):
- Rewrite to use fi.media_sort_date instead of COALESCE(fm.captured_at,...)
- Eliminates LEFT JOIN file_metadata -- one fewer table touch
- Plan: Limit to Index Scan O(LIMIT) instead of Sort O(N)

Video thumbnail pre-scaling (client + server):
- JS: pre-scale canvas to max 400px before toBlob -- 22x less RAM, 15x less BW
- Rust: fast-path in store_external_thumbnail -- if payload is already
  WebP with dims within max_dim, store as-is (zero decode, zero encode)
2026-03-07 20:12:31 +01:00
Diocrafts fc5f101e61 perf(thumbnails): cache-first + ETag 304 eliminates DB queries on GET
Solution A — Cache-first path:
- get_thumbnail now tries moka (RAM) → disk BEFORE any DB query
- Ownership was verified at creation time; UUIDv4 prevents enumeration
- 146-photo timeline: 0 SQL queries instead of 146 per page load

Solution C — ETag / If-None-Match short-circuit:
- Deterministic ETag 'thumb-{id}-{size}' on every response
- If browser sends matching If-None-Match → 304 with zero I/O
- Cache-Control: immutable prevents even conditional requests

Additional improvements:
- Remove moka TTL (was 600s); thumbnails are immutable, weight-only eviction
- generate_all_sizes_background populates moka after disk write;
  first GET after upload served from RAM with zero disk I/O
- DB path only taken on cache miss for images needing generation
2026-03-07 19:48:35 +01:00
Diocrafts f5dd2b9a95 fix: add CSRF header to video thumbnail PUT requests
The double-submit CSRF middleware blocks all mutating requests
without the X-CSRF-Token header. photos.js was uploading video
thumbnails via PUT without it, causing a flood of 403 errors.

- Add getCsrfHeaders() spread to the fetch headers
- Add credentials: 'same-origin' to ensure cookie is sent
2026-03-07 19:24:10 +01:00
Diocrafts 661c9cb688 fix: delete thumbnails when files are permanently deleted
- Inject ThumbnailService into TrashService and FileManagementService
- Call delete_thumbnails() after permanent file deletion in:
  - TrashService::delete_permanently (single item)
  - TrashService::empty_trash (bulk: collects file IDs first)
  - FileManagementService::delete_file
  - FileManagementService::delete_with_cleanup (fallback path)
- All thumbnail cleanup is best-effort (warn on failure, never blocks)
- Prevents orphaned thumbnail files from accumulating on disk
2026-03-07 19:15:36 +01:00
Diocrafts 2aeb97383c fix: instant video thumbnails on tab switch + throttle decodes
Backend:
- 204 response: add Cache-Control: no-store so browser never caches
  'no thumbnail yet' — next GET after PUT upload gets the WebP

Frontend:
- _videoThumbCache (Map): persists fileId → URL across re-renders,
  so switching tabs reuses cached URLs instantly (no re-decode)
- Render: videos with cached URL skip the 204/error/decode cycle
- After PUT succeeds: swap blob URL → server ?v=1 URL so blob is GC'd
- Concurrency throttle: max 3 simultaneous video decodes to avoid
  overwhelming network + CPU when gallery has many videos
- Decode queue: pending videos processed as slots free up
2026-03-07 19:06:37 +01:00
Diocrafts db93b48149 feat: client-side video thumbnail generation (no ffmpeg)
Backend:
- get_thumbnail: for non-images, serve cached thumb or 204 (not 400)
- store_external_thumbnail: validate + re-encode to WebP + persist
- get_cached_thumbnail: memory → disk lookup without generation
- PUT /api/files/{id}/thumbnail/{size} endpoint for client uploads
- ThumbnailPort trait: add get_cached_thumbnail + store_external_thumbnail

Frontend (photos.js):
- On <img> error for video tiles, use <video> + <canvas> to extract
  a frame at 25% duration using browser's native codec
- Show frame immediately via blob URL
- Fire-and-forget PUT to server for permanent caching
- Subsequent visits serve cached WebP instantly (no re-extraction)

Zero server-side dependencies — CPU distributed across clients.
2026-03-07 18:55:44 +01:00
Diocrafts 54ce2d085a style: apply card border/shadow style from Archivos to Fotos tiles
- photo-tile: border-radius 4px → 12px, add 2px solid border + box-shadow
- hover: translateY(-2px) elevation effect matching file-card
- selected: border-color + background tint instead of outline
- grid gaps: 4px → 12/14/10px for daily/monthly/yearly to breathe
2026-03-07 18:34:16 +01:00
Diocrafts 6e10c6a1ac fix: files grid cards now stretch to fill full width like photos view
- grid-template-columns: minmax(200px, 240px) → minmax(200px, 1fr)
- Remove justify-content: start (no longer needed with 1fr)
2026-03-07 18:27:02 +01:00
Diocrafts 03c051f9d8 fix: add blob: to CSP frame-src and media-src for PDF/video viewing
- frame-src: add 'blob:' (wildcard * only matches network schemes)
- media-src: new directive with 'self' blob: (was falling back to default-src)
- Fixes: PDFs blocked in iframe, videos blocked by media-src fallback
2026-03-07 18:17:35 +01:00
Diocrafts 6b02c10942 perf: complete storage-layer UUID migration — eliminate UUID↔String round-trips
- Domain: Folder.owner_id + File.owner_id → Option<Uuid> (was Option<String>)
- Traits: folder_repository signatures accept Uuid directly (was &str)
- Infra: remove all fi.user_id::text / fo.user_id::text SQL casts in repos
- Infra: remove $N::uuid bind casts (columns are native UUID now)
- Services: eliminate to_string() bridge variables in folder_service,
  search_service, trash_service
- DTOs: boundary conversion to String only at API edge (file_dto, folder_dto)
- Ports: default impls compare Option<Uuid> directly
- Stubs: updated signatures to match trait

13 files changed across domain/application/infrastructure layers.
Compiles clean (cargo check passes).
2026-03-07 18:05:52 +01:00
Diocrafts cc98c5b0da fix: add ::uuid/::text SQL casts in storage repos for UUID columns
- folder_db_repository: cast user_id to ::text in SELECT, ::uuid in INSERT/WHERE
- file_blob_write_repository: cast ::uuid in INSERT for user_id column
- file_blob_read_repository: bind Uuid natively instead of .to_string()

Fixes runtime errors:
  - column user_id is of type uuid but expression is of type text
  - operator does not exist: uuid = text
2026-03-07 15:17:15 +01:00
Diocrafts 06ed0455ce perf: migrate all user/session/auth IDs from VARCHAR(36) to native UUID
- Schema: all ~15 VARCHAR(36) columns → UUID with DEFAULT gen_random_uuid()
- Domain entities: User, Session, DeviceCode, AppPassword, Share → id: Uuid
- DTOs: CurrentUser.id → Uuid (API boundary DTOs keep String for JSON)
- Auth middleware: parse JWT claims.sub (String) → Uuid at boundary
- All repository traits, port traits, service impls updated end-to-end
- Handlers: pass Uuid by value (Copy, 16 bytes) instead of String refs
- Settings chain: updated_by column → Uuid (was text, caused setup crash)
- Removed ~650 lines of String↔Uuid conversion boilerplate
- Eliminates per-request heap allocations for ID cloning
- 16-byte binary comparison vs 36-byte string comparison in all queries
- Native UUID indexing in PostgreSQL (btree on 16 bytes vs 36-char text)

85 files changed, 1090 insertions(+), 1739 deletions(-)
2026-03-07 14:59:32 +01:00
Diocrafts 9f08460027 perf: OnceLock for env var, Arc<CurrentUser> in auth, pre-compute query lowercase
- rate_limit: cache OXICLOUD_TRUST_PROXY_HEADERS in OnceLock<bool> to avoid
  syscall on every request (~500ns → ~1ns)
- auth middleware: insert Arc<CurrentUser> instead of bare CurrentUser;
  all 5 extractors now clone Arc (~1ns) instead of 4 Strings (~60-100ns)
- search_service: pre-compute query.to_lowercase() once before loops,
  eliminating N redundant heap allocations per search
2026-03-07 11:23:56 +01:00