diff --git a/README.md b/README.md new file mode 100644 index 0000000..beab663 --- /dev/null +++ b/README.md @@ -0,0 +1,220 @@ +# Favoritter + +A self-hosted web application for collecting and sharing favorites. Movies, songs, cars, lego sets, flowers, pictures — anything you love. + +Built as a single Go binary with SQLite, server-rendered HTML, and [HTMX](https://htmx.org/) for interactivity. Designed to be easy to deploy for technical users who can share access with friends and family. + +## Features + +- **Favorites** — Add favorites with a description, optional URL, optional image upload, and tags +- **Tags** — Autocomplete from existing tags, browse by tag +- **Privacy** — Each favorite can be public or private, with a configurable default +- **User profiles** — Public or limited visibility, with avatar, bio, and display name +- **Admin panel** — User management, tag management, signup request approval, site settings +- **Signup modes** — Open registration, approval-required, or closed +- **Atom feeds** — Global, per-user, and per-tag feeds +- **Import/Export** — JSON and CSV, for data portability +- **JSON API** — Full REST API under `/api/v1/` for third-party clients +- **OpenGraph** — Rich link previews when sharing favorites +- **Accessible** — Semantic HTML, keyboard navigation, screen reader support, `lang="nb"`, WCAG 2.2 AA target +- **Proxy-aware** — Supports deployment behind Caddy/nginx on a different machine (WireGuard, Tailscale) + +## Quick Start + +### Run directly + +```bash +# Build +go build -o favoritter ./cmd/favoritter + +# Run (creates admin user on first start) +FAVORITTER_ADMIN_USERNAME=admin \ +FAVORITTER_ADMIN_PASSWORD=changeme \ +./favoritter +``` + +Open http://localhost:8080 in your browser. + +### Run with Podman/Docker + +```bash +BUILDAH_FORMAT=docker podman build \ + --build-arg BUILD_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ + --build-arg GIT_REVISION="$(git describe --always --dirty)" \ + -t favoritter . + +podman run -d \ + -p 8080:8080 \ + -v favoritter-data:/data \ + -e FAVORITTER_ADMIN_USERNAME=admin \ + -e FAVORITTER_ADMIN_PASSWORD=changeme \ + favoritter +``` + +## Configuration + +All configuration is via environment variables. Sensible defaults are provided for everything except the initial admin credentials. + +| Variable | Default | Description | +|----------|---------|-------------| +| `FAVORITTER_ADMIN_USERNAME` | *(none)* | Initial admin username (created on first run) | +| `FAVORITTER_ADMIN_PASSWORD` | *(none)* | Initial admin password | +| `FAVORITTER_DB_PATH` | `./data/favoritter.db` | SQLite database file path | +| `FAVORITTER_LISTEN` | `:8080` | Listen address (e.g. `:8080`, `10.0.0.5:8080`) | +| `FAVORITTER_BASE_PATH` | `/` | Base path for subpath deployment (e.g. `/faves`) | +| `FAVORITTER_EXTERNAL_URL` | *(auto)* | Public URL (e.g. `https://faves.example.com`). Used for feeds, cookies, OpenGraph. If unset, inferred from Host header. | +| `FAVORITTER_TRUSTED_PROXIES` | `127.0.0.1` | Comma-separated IPs/CIDRs to trust for `X-Forwarded-For` (e.g. `100.64.0.0/10` for Tailscale) | +| `FAVORITTER_UPLOAD_DIR` | `./data/uploads` | Directory for uploaded images | +| `FAVORITTER_MAX_UPLOAD_SIZE` | `10485760` | Maximum upload size in bytes (10 MB) | +| `FAVORITTER_SESSION_LIFETIME` | `720h` | Session cookie lifetime (30 days) | +| `FAVORITTER_ARGON2_MEMORY` | `65536` | Argon2id memory cost in KiB (64 MB) | +| `FAVORITTER_ARGON2_TIME` | `3` | Argon2id iterations | +| `FAVORITTER_ARGON2_PARALLELISM` | `2` | Argon2id parallelism | +| `FAVORITTER_RATE_LIMIT` | `60` | Auth endpoint rate limit (requests/minute/IP) | +| `FAVORITTER_SITE_NAME` | `Favoritter` | Site name shown in UI and feeds | +| `FAVORITTER_DEV_MODE` | `false` | Enable live template reload and debug logging | + +## Deployment + +### Behind Caddy (subdomain) + +``` +faves.example.com { + reverse_proxy 10.0.0.5:8080 +} +``` + +Set `FAVORITTER_EXTERNAL_URL=https://faves.example.com` and `FAVORITTER_TRUSTED_PROXIES=`. + +### Behind Caddy (subpath) + +``` +example.com { + handle_path /faves/* { + reverse_proxy 10.0.0.5:8080 + } +} +``` + +Set `FAVORITTER_BASE_PATH=/faves` and `FAVORITTER_EXTERNAL_URL=https://example.com/faves`. + +### Remote proxy (WireGuard/Tailscale) + +Caddy and Favoritter can run on different machines. Set `FAVORITTER_TRUSTED_PROXIES` to the proxy's IP or CIDR (e.g. `100.64.0.0/10` for Tailscale, `10.0.0.0/24` for WireGuard). + +## API + +The JSON API lives under `/api/v1/`. Authenticate by posting to `/api/v1/auth/login` to get a session cookie, then use it for subsequent requests. + +```bash +# Login +curl -c cookies.txt -X POST http://localhost:8080/api/v1/auth/login \ + -H 'Content-Type: application/json' \ + -d '{"username":"admin","password":"changeme"}' + +# List your faves +curl -b cookies.txt http://localhost:8080/api/v1/faves + +# Create a fave +curl -b cookies.txt -X POST http://localhost:8080/api/v1/faves \ + -H 'Content-Type: application/json' \ + -d '{"description":"Blade Runner 2049","url":"https://example.com","privacy":"public","tags":["film","sci-fi"]}' + +# Export +curl -b cookies.txt http://localhost:8080/api/v1/export/json +``` + +See the [route list](#routes) for all available endpoints. + +## Routes + +### Web + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/` | Home page / public feed | +| GET/POST | `/login` | Login | +| GET/POST | `/signup` | Signup (mode-dependent) | +| POST | `/logout` | Logout | +| GET/POST | `/reset-password` | Forced password reset | +| GET | `/faves` | Own faves list | +| GET | `/faves/new` | New fave form | +| POST | `/faves` | Create fave | +| GET | `/faves/{id}` | View fave | +| GET | `/faves/{id}/edit` | Edit fave form | +| POST | `/faves/{id}` | Update fave | +| DELETE | `/faves/{id}` | Delete fave | +| GET | `/tags/search?q=` | Tag autocomplete (HTMX) | +| GET | `/tags/{name}` | Browse by tag | +| GET | `/u/{username}` | Public profile | +| GET/POST | `/settings` | User settings | +| POST | `/settings/avatar` | Upload avatar | +| POST | `/settings/password` | Change password | +| GET | `/export` | Export page | +| GET | `/export/json` | Download JSON | +| GET | `/export/csv` | Download CSV | +| GET/POST | `/import` | Import page | +| GET | `/feed.xml` | Global Atom feed | +| GET | `/u/{username}/feed.xml` | User Atom feed | +| GET | `/tags/{name}/feed.xml` | Tag Atom feed | +| GET | `/admin` | Admin dashboard | +| GET/POST | `/admin/users` | User management | +| GET | `/admin/tags` | Tag management | +| GET/POST | `/admin/signup-requests` | Signup request management | +| GET/POST | `/admin/settings` | Site settings | +| GET | `/health` | Health check | + +### API (`/api/v1/`) + +| Method | Path | Description | +|--------|------|-------------| +| POST | `/api/v1/auth/login` | Login, returns session | +| POST | `/api/v1/auth/logout` | Logout | +| GET | `/api/v1/faves` | List own faves | +| POST | `/api/v1/faves` | Create fave | +| GET | `/api/v1/faves/{id}` | Get fave | +| PUT | `/api/v1/faves/{id}` | Update fave | +| DELETE | `/api/v1/faves/{id}` | Delete fave | +| GET | `/api/v1/tags?q=` | Search tags | +| GET | `/api/v1/users/{username}` | Public profile | +| GET | `/api/v1/users/{username}/faves` | User's public faves | +| GET | `/api/v1/export/json` | Export own faves | +| POST | `/api/v1/import` | Import faves | + +## Tech Stack + +- **Go** (1.22+ stdlib router, `html/template`, `embed`, `log/slog`) +- **SQLite** via [modernc.org/sqlite](https://pkg.go.dev/modernc.org/sqlite) (pure Go, no CGO) +- **HTMX** for interactive UI without a JS framework +- **Pico CSS** for semantic HTML styling +- **Argon2id** for password hashing +- **gorilla/feeds** for Atom feed generation + +Only 3 external Go dependencies. + +## Security + +- Argon2id password hashing with timing-attack prevention +- CSRF double-submit cookie pattern (auto-included by HTMX) +- Security headers: CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy +- Rate limiting on authentication endpoints +- Image uploads: EXIF metadata stripped, re-encoded, UUID filenames, path traversal protection +- Proxy-aware: trusts `X-Forwarded-For` only from configured proxy CIDRs +- Session cookies: HttpOnly, Secure (from `EXTERNAL_URL` or `X-Forwarded-Proto`), SameSite=Lax + +## Development + +```bash +# Run in dev mode (live template reload, debug logging) +FAVORITTER_DEV_MODE=true \ +FAVORITTER_ADMIN_USERNAME=admin \ +FAVORITTER_ADMIN_PASSWORD=dev \ +go run ./cmd/favoritter + +# Run tests +go test ./... +``` + +## License + +[GNU Affero General Public License v3.0](LICENSE) (AGPL-3.0-or-later)