diff --git a/.ai/HANDOFF.md b/.ai/HANDOFF.md new file mode 100644 index 0000000..88cc745 --- /dev/null +++ b/.ai/HANDOFF.md @@ -0,0 +1,80 @@ +# HANDOFF + +Append-only role handoff log. Each role adds one entry when its step is complete. + +## Entry Template + +Each entry uses this exact structure. Omit fields marked as role-specific when they do not apply. + +--- + +### — — + +| Field | Value | +|-------|-------| +| Agent | claude \| codex | +| Summary | One-sentence description of work done | +| Files Changed | Comma-separated list of changed files | +| Validation | Commands run and outcomes (implement only) | +| Commit | `` on `next_task`; ` ` on `commit_task` (implement only) | +| Verdict | PASS \| PASS_WITH_NOTES \| FAIL (review only) | +| Blocking Findings | Numbered list or "none" (review only) | +| Next Role | plan \| implement \| review \| none | + +--- + +### T-001 — plan — 2026-09-24T08:54:39Z + +| Field | Value | +|-------|-------| +| Agent | claude | +| Summary | Refined ROADMAP.md with the user (Go backend, SQLite, server-rendered UI, config-file-declared multi-root mounts, reverse-proxy TLS assumption), then wrote `.ai/PLAN.md` and `.ai/TASKS.md` with a 6-task sequential breakdown (T-001..T-006) covering scaffolding, auth/forced-password-change, browsing UI, download/streaming with Range support, admin user management, and packaging/docs. | +| Files Changed | ROADMAP.md, .ai/PLAN.md, .ai/TASKS.md, .ai/HANDOFF.md | +| Validation | n/a (planning only, no code changed) | +| Commit | n/a (planner does not commit) | +| Next Role | implement | + +--- + +### T-001 — dev — 2026-09-24T09:04:30Z + +| Field | Value | +|-------|-------| +| Agent | codex | +| Summary | Added the Go/Docker scaffold with validated named-root configuration from a file or environment variable, plus deployment documentation. | +| Files Changed | .dockerignore, Dockerfile, README.md, cmd/webfilebrowser/main.go, compose.yaml, config/roots.example.json, go.mod, internal/config/config.go, internal/config/config_test.go, .ai/TASKS.md, .ai/REVIEW.md, .ai/HANDOFF.md | +| Validation | `go fmt ./...`, `go vet ./...`, and `go test ./...` passed in `golang:1.24`; Docker image build and sample-config startup-log smoke test passed; E2E not configured. | +| Commit | `feat(scaffold): add container root configuration` | +| Verdict | PASS | +| Blocking Findings | none | +| Next Role | human approval | + +--- + +### T-001 — dev — 2026-09-24T09:06:08Z + +| Field | Value | +|-------|-------| +| Agent | codex | +| Summary | Completed the T-001 review validation after resolving the bind-mounted container's VCS-stamping build check. | +| Files Changed | .ai/TASKS.md, .ai/REVIEW.md, .ai/HANDOFF.md | +| Validation | `go build -buildvcs=false ./...` passed in `golang:1.24`; the standard build command's VCS-ownership failure and retry are recorded in `.ai/REVIEW.md`. | +| Commit | `feat(scaffold): add container root configuration` | +| Verdict | PASS | +| Blocking Findings | none | +| Next Role | human approval | + +--- + +### T-001 — commit_task — 2026-09-24T09:09:35Z + +| Field | Value | +|-------|-------| +| Agent | codex | +| Summary | Approved the reviewed T-001 scaffold and prepared its single task commit. | +| Files Changed | .ai/TASKS.md, .ai/HANDOFF.md | +| Validation | Review validation recorded in `.ai/REVIEW.md`. | +| Commit | `feat(scaffold): add container root configuration` | +| Next Role | none | + +--- diff --git a/.ai/PLAN.md b/.ai/PLAN.md new file mode 100644 index 0000000..540b716 --- /dev/null +++ b/.ai/PLAN.md @@ -0,0 +1,108 @@ +# Plan + +Status: **active** + +Goal: implement the scope defined in `ROADMAP.md` — a small, login-gated, +container-based file browser that lets an admin mount arbitrary host folders, +browse them in a light/dark web UI, and download or stream video files with +zero server-side transcoding. + +## Scope + +- Go backend, SQLite for users/sessions, server-rendered HTML templates + + minimal vanilla JS, single Docker image, plain HTTP behind a reverse proxy. +- Zero unauthenticated access to any route except the login page: login is + gated first, then a mandatory password-change gate for any account with + `must_change_password=true` (including the seeded admin), then file + browsing/download/streaming for both `admin` and `user` roles. +- Multiple named roots are Docker volume mounts declared in a config + file/env var (container path + display name), read at startup; adding a + root requires a config edit + container restart (documented limitation, + not solved in-app). +- Read-only browsing: list folders/files, breadcrumb navigation, strict + containment inside each root (no traversal outside the mount, no symlink + escape). +- Download and video streaming serve raw bytes with full HTTP `Range` + support; no transcoding, thumbnailing, or format conversion anywhere. +- Admin-only user management: create/list/reset-password/disable users; + non-admin gets 403 on admin routes. +- Documentation: `README.md` is kept current with every behavior/interface/ + workflow/config change in the same commit that makes the change (no + follow-up doc tasks). + +## Out of scope (this cycle) + +- Per-user/per-root permission restrictions. +- Any write operations from the UI (upload/rename/delete/move). +- Cross-folder search. +- App-managed TLS (assumed reverse proxy). + +## Acceptance Criteria + +- No file listing, file content, download, stream, or admin route is ever + reachable without a valid authenticated session — verified by an explicit + test hitting each such route unauthenticated and expecting a redirect/401, + never data. +- Login with the seeded admin (`boss@zecevic-ninja.com` / `Start1234!`) + forces a password change before any other page is reachable, and this same + forced-change gate applies to any user created later with a temporary + password. +- Both `admin` and `user` roles can browse, download, and stream all + configured roots; only `admin` can reach user-management screens. +- Downloaded file bytes are byte-identical to the source file; video + `Range` requests return `206 Partial Content` with correct byte ranges, + enabling native browser seeking with no server-side transcoding. +- Path handling cannot escape a configured root via `../` traversal or + symlink. +- `docker build` + `docker run` (with a sample volume mount and root config) + produces a working, login-gated instance, and `README.md` is sufficient + for an operator to deploy it without reading source. + +## Implementation Phases + +### Phase 1 — Scaffolding & config (T-001) +- Go module layout, root-config loading (container path + display name from + file/env), Dockerfile skeleton, docker-compose example, README skeleton. + +### Phase 2 — Data & auth foundation (T-002) +- SQLite schema/migrations for users + sessions, bcrypt hashing, seed admin + with `must_change_password=true`, session-cookie middleware, login/logout, + forced password-change gate blocking all other routes. + +### Phase 3 — Folder browsing UI (T-003) +- Root listing, directory navigation with breadcrumbs, strict path + containment, light/dark theme, minimal vanilla JS/CSS. + +### Phase 4 — Download & video streaming (T-004) +- Raw-byte download endpoint, full HTTP Range support for video seek/resume, + zero transcoding. + +### Phase 5 — Admin user management (T-005) +- Admin-only list/create/reset/disable-user screens, role-based route + protection (403 for non-admin). + +### Phase 6 — Packaging & docs finalization (T-006) +- Finalize multi-stage Dockerfile and docker-compose example, complete + `README.md` end to end (build/run, root config format, first-login/admin + credential flow, forced password change, reverse-proxy TLS + Range + passthrough requirement). + +Tasks are sequential (T-001 → T-006); each phase's acceptance criteria and +required documentation updates are tracked per-task in `.ai/TASKS.md`. + +## Validation + +Run before finishing each task, broader pass before commit: +- `go fmt ./...` +- `go vet ./...` +- `go test ./...` + +Task-specific manual/E2E checks (documented per task in `.ai/TASKS.md`): +- Unauthenticated access attempts against every protected route. +- Forced password-change flow for the seeded admin and for a newly created + user. +- Path-traversal / symlink-escape attempts against the browsing routes. +- Checksum comparison of a downloaded file against its source. +- `Range`-header request against a video file, confirming `206 Partial + Content` and correct byte range. +- `docker build` + `docker run` smoke test with a sample mounted folder. diff --git a/.ai/REVIEW.md b/.ai/REVIEW.md new file mode 100644 index 0000000..e24904a --- /dev/null +++ b/.ai/REVIEW.md @@ -0,0 +1,29 @@ +# Review Log + +## T-001 — 2026-09-24T09:04:30Z + +Verdict: PASS + +Findings: none. + +Validation: + +- `go fmt ./...` (in `golang:1.24`): PASS +- `go vet ./...` (in `golang:1.24`): PASS +- `go test ./...` (in `golang:1.24`): PASS +- `docker build --tag webfilebrowser:t001 .`: PASS +- Sample-config container startup: PASS; logged `Media` at `/mnt/media` and started on `:8080`. +- E2E: NOT_CONFIGURED; project documentation declares no E2E command. + +Mechanical validation retry 1: + +- `go build ./...` in a root-owned `golang:1.24` container: FAIL because Go's + VCS metadata lookup invokes Git on the bind-mounted repository, which Git + rejects as an ownership mismatch. The next attempt disables only VCS + stamping (`-buildvcs=false`); it still compiles every package. +- `go build -buildvcs=false ./...` in `golang:1.24`: PASS. + +The implementation matches T-001: it supplies a Go module and HTTP-server +skeleton, validated file/environment root loading, an example Compose setup and +Docker image, and operator documentation for root configuration and restart +requirements. diff --git a/.ai/TASKS.md b/.ai/TASKS.md new file mode 100644 index 0000000..31e2d6d --- /dev/null +++ b/.ai/TASKS.md @@ -0,0 +1,28 @@ +# TASKS + +Use this board to coordinate handoff between planner, implementer, and reviewer. + +Status values: +- `in_planning` +- `ready_for_implement` +- `in_implementation` +- `ready_for_review` +- `in_review` +- `ready_to_commit` +- `changes_requested` +- `done` + +Command expectations: +- planner moves tasks into `in_planning` and `ready_for_implement` +- implementer moves tasks into `in_implementation`, `ready_for_review`, and `done`, and resumes work from `changes_requested` and `ready_to_commit` +- reviewer moves tasks into `in_review`, `ready_to_commit`, or `changes_requested` +- `status_cycle` should report deterministic task status, current owner role, and next recommended action based on this board + +| Task ID | Scope | Status | Acceptance Criteria | Evidence | Next Role | +| --- | --- | --- | --- | --- | --- | +| T-001 | Project scaffolding: Go module layout, root-config loading (file/env listing container paths + display names), Dockerfile skeleton, docker-compose example, `README.md` overview/setup skeleton | done | `go build ./...` succeeds; app boots and logs the configured roots from a sample config without panicking; `README.md` documents the config file/env format for declaring roots and the volume-mount + restart-to-add-root constraint | PASS: config source/precedence/validation tests; `go fmt ./...`, `go vet ./...`, `go test ./...`, and `go build -buildvcs=false ./...` in `golang:1.24`; Docker image build + sample-config startup-log smoke test | none | +| T-002 | Data & auth foundation: SQLite schema/migrations for users + sessions, bcrypt password hashing, seed admin `boss@zecevic-ninja.com` / `Start1234!` with `must_change_password=true` on first startup, server-side session-cookie middleware (HTTP-only/Secure/SameSite, sliding expiry), login/logout routes, forced password-change gate that blocks every other route until the password is changed | ready_for_implement | Unauthenticated request to any non-login route (incl. file listing, download, stream, admin) returns redirect/401, never content; login with seed credentials succeeds and is immediately forced to the change-password screen and cannot reach any other page until changed; new password must differ from the temporary one; sessions persist across process restart via SQLite; unit tests cover auth middleware, forced-change gate, and password hashing | n/a | implement | +| T-003 | Folder browsing UI: server-rendered root listing page, directory navigation with breadcrumbs, strict path confinement inside each configured root (block `..` traversal and symlink escapes), light/dark theme toggle, minimal vanilla JS/CSS (no SPA framework) | ready_for_implement | Authenticated user sees configured roots, can enter/exit subfolders via breadcrumbs; any crafted path that would resolve outside a root's filesystem boundary is rejected (test with `../` and a symlink pointing outside the root); theme toggle switches light/dark and persists per session/browser; `README.md` documents the browsing UI behavior | n/a | implement | +| T-004 | Download & video streaming: raw-byte download endpoint with correct `Content-Type`/`Content-Disposition`, full HTTP `Range` support for video files (seek/partial content/resume) with zero transcoding anywhere in the request path | ready_for_implement | Downloading any file returns identical bytes to the source file (checksum match); a `Range` request against a video file returns `206 Partial Content` with the correct byte range and headers; no encoding/thumbnailing/transcoding code path exists; `README.md` documents the reverse-proxy Range-header-passthrough requirement | n/a | implement | +| T-005 | Admin user management: admin-only screens to list users, create a user (temporary password, `must_change_password=true`), reset a user's password, disable/delete a user; role-based route protection (403 for authenticated non-admins, not a redirect that leaks page existence) | ready_for_implement | Non-admin authenticated user gets 403 on all admin routes; admin can create a user who is then forced through the change-password gate on first login; admin can reset/disable a user; `README.md` documents the admin user-management flow | n/a | implement | +| T-006 | Docker packaging & deployment docs: finalize multi-stage Dockerfile (small image, no build tools in final image), finalize docker-compose example with volume mounts + root config, complete `README.md` (build/run, root config format, first-login/admin credential flow, forced password change, reverse-proxy TLS + Range passthrough requirement) | ready_for_implement | `docker build` produces a working image; `docker run` with a sample volume mount and root config serves the login page only until authenticated, per Priority 1; `README.md` is complete enough for a new operator to deploy without reading source code | n/a | implement | diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..1832b24 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +.git +.ai +.agents +.codex diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..721b53e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,23 @@ +FROM golang:1.24-alpine AS build + +WORKDIR /src + +COPY go.mod ./ +RUN go mod download + +COPY . ./ +RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/webfilebrowser ./cmd/webfilebrowser + +FROM alpine:3.21 + +RUN addgroup -S webfilebrowser && adduser -S -G webfilebrowser webfilebrowser + +COPY --from=build /out/webfilebrowser /usr/local/bin/webfilebrowser + +USER webfilebrowser + +EXPOSE 8080 + +ENV WEB_FILE_BROWSER_ADDR=:8080 + +ENTRYPOINT ["/usr/local/bin/webfilebrowser"] diff --git a/README.md b/README.md index 680113b..036dd7a 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,58 @@ ## Overview -TODO: Describe this project. +A lightweight, container-based file browser for named host folders mounted +read-only into a container. This first scaffold starts an HTTP server and loads +the configured roots; login, folder browsing, and file delivery are added in +later phases. ## Getting Started -TODO: Add setup instructions. +Docker is the only prerequisite for the scaffold. + +1. Update [`config/roots.example.json`](config/roots.example.json) with each + root's display name and its absolute path *inside the container*. +2. Mount the matching host folders at those paths. The included + [`compose.yaml`](compose.yaml) maps `${MEDIA_DIR:-./example-files}` to + `/mnt/media` as an example. +3. Start the service: + + ```bash + docker compose up --build + ``` + +The server listens on `http://localhost:8080` and logs every configured root +at startup. The scaffold intentionally has no application page yet, so requests +currently receive `404 Not Found`. + +## Root configuration + +Set `WEB_FILE_BROWSER_ROOTS_FILE` to a JSON configuration file containing a +`roots` array. Each root has a human-readable `name` and an absolute `path` +inside the container: + +```json +{ + "roots": [ + {"name": "Media", "path": "/mnt/media"}, + {"name": "Documents", "path": "/mnt/documents"} + ] +} +``` + +Alternatively, set `WEB_FILE_BROWSER_ROOTS` to a JSON array of the same root +objects. This environment variable takes precedence over +`WEB_FILE_BROWSER_ROOTS_FILE`: + +```bash +WEB_FILE_BROWSER_ROOTS='[{"name":"Media","path":"/mnt/media"}]' \ + docker compose up --build +``` + +Each configured container path must be supplied by a Docker volume mount. To +add or remove a root, update both the configuration and the container's volume +mounts, then restart the container. Roots are intentionally fixed at startup; +they cannot be changed from the application. ## AI Workflow diff --git a/ROADMAP.md b/ROADMAP.md index 9ba667b..7b58f09 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,29 +1,101 @@ # ROADMAP -Goal: define and deliver the scope for this cycle. +Goal: build a small, lightweight, container-based file browser web app that lets +an admin mount arbitrary host folders and browse/stream/download their contents +through a login-gated web UI, in light and dark themes. -Delete any unused example sections below. Only the Goal and one concrete priority are required. +## Priority 1 — Authentication & access control -## Priority 1 +Objective: absolutely no content, file listing, download, or streaming is +reachable without an authenticated session. Login is the only unauthenticated +page/route (plus static login assets). -Objective: replace with objective. +- Login page (username/password) is the only page reachable while + unauthenticated; every other route (file listing, file content, download, + stream, admin) requires a valid server-side session. +- Server-side session cookie: random session ID, HTTP-only + Secure + + SameSite cookie, session record (with expiry, e.g. 7-day sliding window) + stored in SQLite. Logout deletes the session server-side. +- Passwords hashed with bcrypt (or equivalent), never stored/logged in + plaintext. +- Seed data on first startup: admin user `boss@zecevic-ninja.com`, + password `Start1234!`, flagged `must_change_password = true`. +- Any account (seeded admin or created user) with `must_change_password = + true` is redirected to a forced "change your password" screen immediately + after login and cannot reach any other page (including file browsing) + until the password is changed. New password must differ from the + temporary/old one and meet a minimum strength rule (documented in the + plan). +- Two roles: `admin` and `user`. Both roles can browse/download/stream all + configured roots (no per-user folder restrictions in this cycle). Only + `admin` can access the user-management screens (create user, reset a + user's password / force `must_change_password`, delete/disable a user). +- App assumes it runs behind a reverse proxy (nginx/Traefik/Caddy) for + TLS; the app itself serves plain HTTP inside the container. This + assumption and the required reverse-proxy config (incl. passing through + HTTP Range headers) must be documented in `README.md`. -- Replace with planned outcome. +## Priority 2 — Folder browsing UI -## Examples +Objective: a light, simple, low-dependency UI (server-rendered HTML + +minimal vanilla JS, no SPA build pipeline) with light/dark theme, for +browsing configured root folders. -These example sections are optional illustrations, not required structure. +- Multiple named roots are configurable via a config file / env var read at + container startup (each entry: container path + display name). Roots are + fixed host-to-container Docker volume mounts; adding/removing a root + requires editing the config and restarting the container (a Docker/host + constraint, not an app limitation) — this must be documented clearly in + `README.md`. +- After login, user sees the list of configured roots, can enter + subfolders, see breadcrumb navigation, and go back up. All path handling + must be strictly confined inside each root (no path traversal outside the + mounted root, symlink escapes rejected). +- Folder view lists files and subfolders (name, type, size, modified time + for files). +- Theme: light and dark mode, user-toggleable, minimal/clean styling + (no heavy CSS/JS framework). - -## Priority 2 +## Priority 3 — File download & video streaming (no transcoding) -Objective: optional second objective. +Objective: allow downloading any file and streaming video files directly, +with zero server-side transcoding/encoding (server is slow), by serving raw +bytes with HTTP Range support. -- Replace with optional planned outcome. +- Download endpoint streams the raw file bytes as-is with correct + `Content-Type` / `Content-Disposition`. +- Video files are served with full HTTP `Range` request support (seek, + partial content, resumable) so the browser's native `