diff --git a/README.md b/README.md new file mode 100644 index 0000000..03b6b74 --- /dev/null +++ b/README.md @@ -0,0 +1,152 @@ +# Nanodrop + +A minimal self-hosted file sharing platform. Upload a file, get a shareable link. No frills. + +- Server-rendered HTML, no client-side framework +- SQLite database, files stored on disk +- JWT authentication via httpOnly cookies +- REST API alongside the web UI + +## Stack + +- **Runtime:** Node.js 22 +- **Framework:** Fastify +- **Database:** SQLite (better-sqlite3) +- **Language:** TypeScript (ESM, run directly via tsx) + +--- + +## Quick start (local) + +```sh +npm install +JWT_SECRET=changeme npm run dev +``` + +Create a user first: + +```sh +npm run register-user -- --username alice --password secret +``` + +Then open `http://localhost:3000`. + +--- + +## Docker + +### Build and run + +```sh +docker compose up -d +``` + +Requires a `.env` file (or environment variables) with at minimum: + +```env +JWT_SECRET=your-secret-here +``` + +All data (database + uploads) is stored in the `nanodrop-data` Docker volume. + +### Register a user in Docker + +```sh +docker compose run --rm register-user --username alice --password secret +``` + +### Environment variables + +| Variable | Default | Description | +|---|---|---| +| `JWT_SECRET` | *(required)* | Secret key for signing JWTs | +| `JWT_EXPIRY` | `7d` | JWT token lifetime | +| `PORT` | `3000` | Port to listen on | +| `HOST` | `0.0.0.0` | Host to bind | +| `BASE_URL` | `http://localhost:3000` | Public base URL (used in share links) | +| `DB_PATH` | `./data/nanodrop.db` | SQLite database path | +| `UPLOAD_DIR` | `./data/uploads` | Upload storage directory | +| `LOG_FILE` | `./data/nanodrop.log` | Auth and access log path | +| `MAX_FILE_SIZE` | `104857600` | Max upload size in bytes (100 MB) | +| `COOKIE_SECURE` | `false` | Set `true` when serving over HTTPS | +| `TRUST_PROXY` | `false` | Set `true` when behind a reverse proxy | + +### Reverse proxy (nginx example) + +When running behind nginx, set `TRUST_PROXY=true` so Nanodrop sees the real client IP in logs. + +```nginx +server { + server_name files.example.com; + + location / { + proxy_pass http://127.0.0.1:3000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + client_max_body_size 110M; + } +} +``` + +--- + +## Fail2ban + +Nanodrop logs failed login attempts to `LOG_FILE` (default `./data/nanodrop.log`) in this format: + +``` +[2026-03-03T12:00:00.000Z] AUTH_FAILURE ip=203.0.113.42 user-agent="curl/8.0" username="admin" +``` + +### 1. Create a filter + +`/etc/fail2ban/filter.d/nanodrop.conf`: + +```ini +[Definition] +failregex = ^.* AUTH_FAILURE ip= .*$ +ignoreregex = +``` + +### 2. Create a jail + +`/etc/fail2ban/jail.d/nanodrop.conf`: + +```ini +[nanodrop] +enabled = true +filter = nanodrop +logpath = /path/to/nanodrop.log +maxretry = 5 +findtime = 60 +bantime = 600 +``` + +Adjust `logpath` to wherever your `LOG_FILE` is. With Docker, the log file lives inside the `nanodrop-data` volume — mount it to a host path or bind-mount a host directory instead of the named volume to make it accessible to fail2ban: + +```yaml +# docker-compose.yml override +volumes: + - /var/lib/nanodrop:/app/data +``` + +Then set `logpath = /var/lib/nanodrop/nanodrop.log`. + +### 3. Reload fail2ban + +```sh +sudo fail2ban-client reload +sudo fail2ban-client status nanodrop +``` + +--- + +## Development + +```sh +npm run dev # start with hot reload via tsx +npm test # run all tests (vitest) +npm run build # type check (tsc --noEmit) +```