favoritter/README.md

220 lines
8.1 KiB
Markdown
Raw Permalink Normal View History

# 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=<caddy-ip>`.
### 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)