diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..b00b8a2b --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,260 @@ +# AGENTS.md + +This file provides guidance to coding agents (Claude Code, Codex, Cursor, Aider, …) working with this repository. Claude Code reads it via `@AGENTS.md` in `CLAUDE.md`. + +# Architecture + +This project is split into two parts: +- `/src` — OxiCloud Backend server in **Rust** +- `/frontend` — OxiCloud Frontend: a **SvelteKit (Svelte 5) + TypeScript** single-page app built with Vite + +> The original vanilla-JS/CSS frontend still lives in `/static` and is retained +> during the migration, but new frontend work goes in `/frontend`. Vite builds +> the SvelteKit app to `static-dist/`, which the Rust web layer serves in +> release. + +# Backend part + +## Backend Build & Dev Commands + +```bash +cargo build # Dev build +cargo build --release # Optimized release build +cargo run # Run server (port 8086) +cargo test --workspace # Run all tests (~208) +cargo test # Run a single test by name +cargo test --features test_utils # Run tests that use mockall mocks +cargo clippy -- -D warnings # Lint (zero warnings policy) +cargo fmt --all --check # Format check +cargo fmt --all # Auto-format +RUST_LOG=debug cargo run # Run with debug logging +cargo run --bin generate-openapi # Regenerate resources/gen/openapi.json +``` + +A `justfile` is available for common tasks (`just --list` to see all). Key recipes: `just check` (fmt + clippy), `just test`, `just openapi`. + +Requires **Rust 1.93+** (edition 2024) and **PostgreSQL 13+** (with `pg_trgm` and `ltree` extensions). + +Database setup: `docker compose up -d postgres` — schema is applied automatically via sqlx migrations on app startup. Migration files live in `migrations/`. For local dev, set `DATABASE_URL` in `.env` (see `example.env`). + +## Backend Pre-commit checks + +Always run these before committing, in this order: + +```bash +cargo fmt --all # Auto-format +cargo clippy --all-features --all-targets -- -D warnings # Lint (must pass with zero warnings) +``` + +CI enforces both — commits that fail either check will not merge. + +## Backend Pre-push checks + +When the change touches server code (anything under `src/`, `migrations/`, +`Cargo.toml`, or `tests/`), run the full suite locally before pushing — CI +is slower and a red CI run after a public push wastes maintainer attention: + +```bash +just check # cargo fmt --check + cargo clippy -D warnings +just test # cargo test --workspace +just test-integration # cargo test --tests with integration cfg +just api-test # Hurl API + WebDAV scenarios +``` + +Run them in that order — `just check` is fastest and catches the most +common issues first. Don't push if any step fails; investigate locally. + +## Backend Architecture + +Hexagonal / Clean Architecture with four layers. Dependencies point inward only. + +### Layer structure (`src/`) + +- **`domain/`** — Core business entities (`entities/`) and repository trait definitions (`repositories/`). Pure Rust, no framework dependencies. Entity types: `File`, `Folder`, `User`, `Calendar`, `CalendarEvent`, `Contact`, `Share`, `TrashedItem`, `Session`, `DeviceCode`, `AppPassword`. + +- **`application/`** — Use cases and orchestration. + - `ports/` — Trait definitions (inbound/outbound) for storage, auth, caching, compression, dedup, thumbnails, chunked uploads, CalDAV/CardDAV, etc. This is the hexagonal "ports" layer. + - `services/` — Use case implementations (`FileManagementService`, `FolderService`, `ShareService`, `TrashService`, `CalendarService`, `ContactService`, `SearchService`, `BatchOperations`, etc.). + - `adapters/` — CalDAV/CardDAV protocol adapters (iCalendar/vCard parsing). + - `dtos/` — Data transfer objects for API boundaries. + +- **`infrastructure/`** — Concrete implementations of ports. + - `repositories/pg/` — All PostgreSQL repository implementations (via `sqlx`). Uses `auth` schema for users/sessions, `storage` schema for files/folders/blobs (content-addressable dedup with ltree paths). + - `services/` — JWT, password hashing (Argon2), OIDC, compression, thumbnails, chunked uploads, WOPI discovery, WebDAV locking, file content caching (moka). + - `adapters/` — CalDAV/CardDAV storage adapters bridging domain traits to PG. + - `db.rs` — Dual connection pool setup (user pool + maintenance pool). + +- **`interfaces/`** — HTTP layer (Axum). + - `api/handlers/` — REST API handlers for files, folders, auth, admin, search, shares, WebDAV, CalDAV, CardDAV, WOPI, chunked uploads, batch operations. + - `api/routes.rs` — Route registration, splits protected vs public routes. + - `nextcloud/` — NextCloud-compatible API (WebDAV, OCS, login flow v2, trashbin) with Basic Auth middleware. + - `middleware/` — Auth (JWT validation), CSRF, rate limiting. + - `web/` — Static file serving. + +- **`common/`** — Cross-cutting concerns. + - `di.rs` — `AppServiceFactory` builds all services and produces `AppState` (the central DI container passed to Axum). This is the composition root. + - `config.rs` — `AppConfig::from_env()` loads all `OXICLOUD_*` env vars. + +### Key patterns + +- **DI via `AppState`**: All services are `Arc`-wrapped and assembled in `common/di.rs`. `AppState` is wrapped in `Arc` and passed as Axum state. Many services are `Option>` because they depend on features being enabled (auth, WOPI, trash, etc.). + +- **Content-addressable storage**: Files use BLAKE3 blob dedup. `storage.file_blobs` stores content; `storage.file_metadata` references blobs with ref-counting. See `file_blob_write_repository.rs` and `file_blob_read_repository.rs`. + +- **ltree paths**: Folder hierarchy uses PostgreSQL `ltree` for efficient subtree queries (recursive copies, moves, searches). + +- **Dual DB pools**: `DbPools` in `infrastructure/db.rs` separates user-facing queries from maintenance/background tasks to prevent starvation. + +- **Feature flags**: Major features (auth, trash, search, sharing, quotas) are toggled via `OXICLOUD_ENABLE_*` env vars in `FeaturesConfig`. + +- **UUID columns**: All ID columns use native PostgreSQL `UUID` type. SQL queries must use `::uuid` casts when passing string parameters to UUID columns. + +### Database schemas + +- `auth` schema: `users`, `sessions`, `app_passwords`, `device_codes`, `admin_settings` +- `storage` schema: `folders`, `file_metadata`, `file_blobs`, `trash`, `shares`, `favorites`, `recent_items`, `nextcloud_object_ids` +- `caldav` schema: `calendars`, `calendar_events` +- `carddav` schema: `address_books`, `contacts`, `contact_groups`, `contact_group_members` + +Schema definition: `migrations/` (sqlx migrations, applied on startup) + +### Protocol support + +The server exposes multiple protocol interfaces simultaneously: +- REST API under `/api/` +- WebDAV at `/webdav/` (RFC 4918) +- CalDAV at `/caldav/` +- CardDAV at `/carddav/` +- NextCloud-compatible API at `/remote.php/`, `/ocs/`, `/status.php` +- WOPI at `/wopi/` (when enabled) +- Well-known discovery at `/.well-known/caldav` and `/.well-known/carddav` + +### Test organization + +Tests are primarily `#[cfg(test)]` modules within source files (~36 files have inline tests). Dedicated test files exist at `*_test.rs` alongside their source. The `test_utils` feature flag enables `mockall` mock generation for trait-heavy testing. No separate `tests/` directory. + +### Code duplication + +Never duplicate logic across handlers or services. If the same behaviour is needed in more than one place, extract it into a shared function, method, or service before writing the second callsite. Preferred homes by layer: +- Cross-handler request logic → method on `CoreServices` or `AppState` (`common/di.rs`) +- Reusable infrastructure behaviour → method on the relevant service struct +- Shared port behaviour → default method on the trait + +### Authorization (AuthZ) + +**AuthZ is enforced exclusively in the application service layer, never in handlers.** All permission checks go through `AuthorizationEngine` (port: `application/ports/authorization_ports.rs`) via service methods named with the `_with_perms` suffix. HTTP handlers (REST, WebDAV, NextCloud, CalDAV, CardDAV) authenticate the caller and pass `caller_id` into the service — they MUST NOT perform their own ownership/permission checks. The authentication middleware extracts the caller; the service decides if the action is allowed. + +This rule prevents drift between layers and ensures every code path goes through the same policy. New service methods that touch a user-scoped resource must take `caller_id: Uuid` and call `authz.require(...)` before any read or mutation. + +### Audit logging for denials and rejections + +**Every permission denial or auth rejection MUST emit a structured audit log line before returning the error.** Without one, security-relevant outcomes are invisible to operators and incident response loses its primary signal. + +The convention: + +```rust +tracing::info!( + target: "audit", + event = ".", // e.g. "authz.denied", "auth.login_rejected", + // "magic_link.redemption_rejected", + // "user_profile.rejected" + reason = "", // stable machine-readable key for filtering + // (e.g. "bad_password", "expired", "no_visibility_path") + // …structured fields naming the actors / targets… + caller_id = %caller_id, // or subject_id, user_id, granted_by, etc. + target_id = %target_id, // or resource_id, subject_id, etc. + "👮🏻‍♂️ human-readable message: …", // helpful for live tailing, do not parse +); +``` + +Rules: + +- **`target: "audit"`** routes the line to the audit channel (separable from operational `oxicloud::*` debug noise). +- **`event`** uses the dotted form `.` and stays stable — log aggregators key off it. +- **`reason`** is a machine-readable enum-style key. Don't reword across releases. New denial cause → new `reason` value, never repurpose an existing one. +- **Structured fields** carry every actor/target involved (`caller_id`, `target_id`, `resource_id`, `subject_id`, role, is_external flag, etc.). Request id and client IP come from the request-scope span automatically — don't duplicate them. +- **Anti-enumeration is preserved.** Returning `NotFound` to the caller while logging the real reason internally is the canonical pattern (e.g. `user_profile.rejected` with `reason = "external_caller_no_relationship"` returns 404, never 403). Operators see the truth; the attacker sees the same response shape regardless of whether the user exists. +- **Success paths stay quiet** by default — every authorized request would otherwise flood the log. Use `tracing::debug!` with `target: "oxicloud::authz"` (or similar) when a low-volume granted-trace helps debugging. Reserve `tracing::info!(target: "audit", …)` for outcomes worth surfacing in security reviews. + +Canonical examples to mirror: `authz.denied` in `application/ports/authorization_ports.rs::require`, `auth.login_rejected` and `magic_link.redemption_rejected` and `user_profile.rejected` in `application/services/auth_application_service.rs`. + +# Frontend part + +The frontend is a **SvelteKit** single-page app (Svelte 5 + TypeScript, Vite, +`adapter-static`) under `frontend/`. Vite builds it to `static-dist/`, which the +Rust web layer serves in release (unmatched client routes fall back to the SPA +shell); `PROFILE=dev` serves the unbuilt source. The legacy vanilla frontend in +`static/` is retained for now but is **not** where new work goes. + +## Frontend Build & Dev Commands + +Run from `frontend/` (or via the `fe-*` justfile recipes from the repo root): + +```bash +npm ci # install deps (just fe-install) +npm run dev # Vite dev server + HMR (just fe-dev) — backend must run on :8086 +npm run build # build the SPA → static-dist/ (just fe-build) +npm run check # svelte-check + ESLint + Stylelint + Prettier (just fe-check) +npm run test:unit # Vitest (just fe-test) +npm run format # prettier --write . +``` + +`just dev` runs the backend and the Vite dev server together. CI uses **Node 24**; Node 22+ works locally. + +## Frontend Architecture (`frontend/src/`) + +- `routes/` — SvelteKit pages (`+page.svelte`, `+layout.svelte`), one folder per route (`files/[...path]`, `photos`, `shared`, `trash`, `admin`, `s/[token]`, …). +- `lib/components/` — reusable Svelte components (`AppShell`, `PhotoLightbox`, `ShareDialog`, `Modal`, …). +- `lib/api/` — HTTP layer: `client.ts` (`apiFetch`/`apiJson`), `csrf.ts` (`getCsrfHeaders`), `types.ts` (API DTO types — map the backend here), and `endpoints/*.ts` (one module per area: files, folders, photos, people, grants, …). +- `lib/stores/` — global reactive state as `*.svelte.ts` rune stores (`session`, `ui`, `theme`, `dialogs`). +- `lib/composables/` — reusable rune logic (`useSelection`, `useOwnerCache`). +- `lib/i18n/` — bespoke reactive i18n; `t(key, [params], fallback)` reads `frontend/static/locales/*.json` (16 locales) with `{{param}}` interpolation and an English fallback. +- `lib/icons/` — `Icon.svelte` + a generated Font Awesome `registry.ts`. +- `lib/utils/`, `lib/vendor/` — shared helpers and minimal typings/loaders for vendored libs. +- `lib/styles/` — global CSS (`app.css`, `base/`, `ported/`). +- `static/` — served at the web root: `locales/`, `vendors/` (maplibre-gl, pmtiles, hash-wasm), `workers/` (deltaWorker), optional `basemaps/`. + +## Code conventions + +### Svelte / TypeScript + +- **Svelte 5 runes** — `$state`, `$derived`, `$props`, `$effect`, `$bindable`. No legacy `export let` for new components. +- **TypeScript everywhere** (`lang="ts"` in components). **No `any`** — `typescript-eslint` recommended is enforced; prefer precise types, `unknown` + narrowing, or a minimal declared interface for an untyped global (see `lib/vendor/maplibre.ts`). +- ES Modules; `camelCase` for variables/functions, `PascalCase` for components/classes; `const`/`let`, never `var`. +- API DTO shapes live in `lib/api/types.ts`; call the backend through `lib/api/endpoints/*` — don't bare-`fetch` `/api` from components. + +### Code duplication + +Never duplicate logic across modules/components. Extract shared behaviour: +- DOM/UI helpers → `lib/utils/` +- API wrappers → the relevant `lib/api/endpoints/*` module +- Cross-component state/logic → a `lib/stores/*.svelte.ts` store or a `lib/composables/*` +- Shared markup → a component (e.g. `PhotoLightbox` is shared by the photos grid, People and Places) + +### CSS + +- BEM methodology for class names (`.block__element--modifier`). +- Component styles live in the component's scoped `