Brendan Chen 0f0c2f0e96 feat(auth): sliding session renewal middleware
Adds src/middleware/session-renewal.ts with slideSessionIfNeeded —
a pure function that re-mints the session cookie iff the JWT is
older than SESSION_RENEW_THRESHOLD_SECONDS (1 hour). Wired into
makeRequireAuth so every authenticated request bumps the cookie's
expiration window forward, giving the 'stay signed in unless you
clear cookies' UX.

Logout paths are explicitly guarded via LOGOUT_PATHS so the
renewer never resurrects a session the user is actively
terminating. Query-string strip prevents /logout?next=foo bypass.
Opportunistic-auth blocks (GET /, GET /f/:id) verify JWT directly
without going through makeRequireAuth, so they don't slide — by
design, a public file view shouldn't extend the owner's session.

Tests cover threshold semantics, both logout paths, query-string
handling, missing iat (legacy token forces refresh), and a full
integration suite simulating 25-day and 31-day jumps via
vi.setSystemTime.
2026-05-09 10:15:31 -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%