Brendan Chen 86870db726 feat(auth): family-wide session constants + mint primitive + auth factory
Adds src/constants.ts exporting the family-wide session policy
(SESSION_TTL_DAYS=30, SESSION_TTL_SECONDS=2_592_000,
SESSION_RENEW_THRESHOLD_SECONDS=3600, LOGOUT_PATHS) so every
bchen.dev app shares the same persistence window.

Introduces issueSessionCookie as the single mint site for
fastify-jwt sign + setCookie, replacing inlined jwt.sign +
setCookie calls in pages.ts and api/v1/auth.ts. The cookie now
carries Max-Age=SESSION_TTL_SECONDS so it persists across browser
restarts.

Converts requireAuth into a makeRequireAuth(config) factory; route
plugins build their own preHandler at registration time. Threads
through pages.ts, api/v1/auth.ts, and api/v1/files.ts.

SESSION_COOKIE_NAME stays 'token' in this commit so existing tests
remain green; the rename to 'nanodrop_session' lands in a follow-up.
JWT_EXPIRY env var is still read; its removal also lands in a
follow-up so each commit builds cleanly.
2026-05-09 10:10:47 -07:00
2026-03-03 16:36:06 -08:00

Nanodrop

Vibe coded alternative to Google Drive for sharing files

  • 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)

npm install
JWT_SECRET=changeme npm run dev

Create a user first:

npm run register-user -- --username alice --password secret

Then open http://localhost:3000.


Docker

Build and run

docker compose up -d

Requires a .env file (or environment variables) with at minimum:

JWT_SECRET=your-secret-here

All data (database + uploads) is stored in the nanodrop-data Docker volume.

Register a user in Docker

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

Set TRUST_PROXY=true when running behind a reverse proxy so Nanodrop sees the real client IP in logs.

Caddy (Caddyfile):

files.example.com {
    reverse_proxy localhost:3000

    request_body {
        max_size 110MB
    }
}

Caddy sets X-Forwarded-For and handles TLS automatically. Set COOKIE_SECURE=true since traffic to the app arrives over HTTPS.

nginx (sites-available/nanodrop):

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:

[Definition]
failregex = ^.* AUTH_FAILURE ip=<HOST> .*$
ignoreregex =

2. Create a jail

/etc/fail2ban/jail.d/nanodrop.conf:

[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:

# docker-compose.yml override
volumes:
  - /var/lib/nanodrop:/app/data

Then set logpath = /var/lib/nanodrop/nanodrop.log.

3. Reload fail2ban

sudo fail2ban-client reload
sudo fail2ban-client status nanodrop

Development

npm run dev      # start with hot reload via tsx
npm test         # run all tests (vitest)
npm run build    # type check (tsc --noEmit)
Description
Vibe coded file sharing platform
Readme 362 KiB
Languages
TypeScript 87.9%
CSS 8.7%
EJS 3.1%
Dockerfile 0.3%