NixOS module (4 fixes):
- Remove RestrictNamespaces=true: Chromium (launched by single-file for
web captures) needs Linux user namespaces; blocking them broke captures
- Escape TOML string values: labels/paths with quotes or backslashes
would produce invalid archivr-server.toml and prevent startup
- Bracket IPv6 listen addresses: ::1:8080 is rejected by Rust's
SocketAddr parser; [::1]:8080 is the correct form (RFC 2732)
- Add optional storePath per archive: covers archives initialised with
a custom store path outside the default sibling store/ directory
Rust (1 fix):
- extract_client_ip loopback branch: take last XFF value not first.
When a proxy appends to X-Forwarded-For an attacker controls the
first entry; the last entry is always set by the trusted local proxy
Frontend (1 fix):
- Gate fetchArchives on authState === 'authenticated'. The empty-dep
useEffect fired on mount (before login), hit 401, and never retried
after login because authState was not in the dependency array
Fix 1 — NixOS: store/ dir missing from ReadWritePaths
archive_path points to .archivr/; captures write artifacts to the
sibling store/ directory. Whitelisting only .archivr/ caused POST
/captures to fail under ProtectSystem=strict.
Fix: use builtins.dirOf a.path to whitelist the parent, which
contains both .archivr/ and store/.
Fix 2 — Rust: X-Forwarded-For was unconditionally trusted
An attacker could send a different IP on every login attempt,
bypassing LOGIN_MAX_ATTEMPTS entirely.
Fix: use ConnectInfo<SocketAddr> (via into_make_service_with_connect_info)
as the primary rate-limit key; XFF is trusted only when the TCP peer
is loopback (i.e. a local reverse proxy). Tests without a real socket
fall back to XFF unchanged.
Port configuration:
NixOS module: split 'bind' string into separate 'listenAddress'
(default 127.0.0.1) and 'port' (default 8080) options. openFirewall
now uses cfg.port directly instead of parsing it from the bind string.
README NixOS example updated accordingly.
Both handlers accepted AuthUser but served guests.
auth.require_auth()? added as first line closes the gap.
Existing unauthenticated tests updated with session cookies.
5 attempts / 15 min per IP. Excess attempts get 429 + Retry-After.
Client IP read from X-Forwarded-For with loopback fallback.
In-memory; prune inline on each check.
Refactored app() into app_with_state() + thin wrapper.
Uses parking_lot::Mutex (no unwrap, no poisoning).
Injects X-Content-Type-Options, X-Frame-Options, Referrer-Policy,
Content-Security-Policy, and Permissions-Policy on every response.
Registered as outermost layer so it covers setup_guard 503s too.
No new dependencies.
modules/nixos/archivr-server.nix — services.archivr-server NixOS module:
- enable/bind/archives/user/group/openFirewall options
- generates archivr-server.toml from options via pkgs.writeText
- pins auth DB to /var/lib/archivr-server/ (StateDirectory)
- dedicated archivr system user + group
- hardened systemd unit: ProtectSystem=strict, NoNewPrivileges,
PrivateTmp, RestrictNamespaces, etc.; archive paths whitelisted
- openFirewall parses the port from the bind string automatically
flake.nix:
- add self to outputs args
- add aarch64-linux to systems (Raspberry Pi / ARM servers)
- expose nixosModules.archivr-server + nixosModules.default
.gitignore: whitelist modules/ directory
docs/README.md: add 'Hosting on NixOS' section with full example
- LoginPage/SetupPage: centered card layout with display font, styled fields and submit button
- Topbar: fix duplicate Settings nav item; add styled user-menu with username + logout button
- RunsView: format ISO timestamps to readable dates, add colored status badges (completed/failed/running)
- AdminView: view-tabs system, styled admin-table, admin-input, status-badge (active/disabled), btn-primary
- SettingsView: replace all inline styles with form-section/form-field/field-input/btn-primary/btn-danger
- CollectionsView: restructure create form with proper field labels and btn-primary
- TagsView: add Tags section heading with separator
- ContextRail: show visibility as human-readable label not raw number; fix assign-error class
- styles.css: add auth-loading, view-tabs, form utilities, btn variants, status badges,
run-status pills, token-banner/row, checkbox-row, coll-create-form, tag-tree-header CSS
Garnix CI was failing because openssl-sys couldn't find the OpenSSL
library. Added pkgs.openssl to buildInputs for both archivr_cli_unwrapped
and archivr_server_unwrapped to provide the necessary system dependency
and allow pkg-config to locate openssl.pc.
POST /api/archives/:id/captures now returns 202 Accepted immediately with
{ job_uid, status: "pending" }. The capture runs in a tokio::task::spawn_blocking
thread and updates the job status to running → completed/failed in the archive DB.
Core DB (database.rs):
- capture_jobs table: id, job_uid, archive_id, run_uid, status, error_text, created_at, updated_at
- create_capture_job, update_capture_job_status, get_capture_job, fail_stalled_capture_jobs
Core archive (archive.rs):
- CaptureJobSummary type + get_capture_job query
Server (routes.rs):
- capture_handler: creates job, spawns background capture, returns 202 immediately
- GET /api/archives/:id/capture_jobs/:job_uid: poll job status
- main.rs: on startup, marks any 'running' jobs from previous session as 'failed'
Frontend:
- api.js: submitCapture returns job object; pollCaptureJob added
- CaptureDialog.jsx: polls every 500ms after submit, shows 'Running…' state,
handles completed (close + refresh) and failed (show error), cleans up interval
163 tests green. Frontend builds cleanly.