# rotten.network — backend

Node.js/Express backend. Owns everything server-side: Discord OAuth login,
sessions, the SQLite database, posts/likes/comments, and the tool APIs
(IP/domain geo, Discord profile lookup). The PHP frontend only renders pages
and calls this backend.

## Run locally

```bash
cd backend
cp .env.example .env      # then fill it in (see below)
npm install
npm start                 # or: npm run dev  (auto-restart)
```

It listens on `http://127.0.0.1:4123` by default (port 3000 is used by the
separate api.rotten.network app on this server).

## .env

| Key | What |
|-----|------|
| `PORT` | Port to listen on (Apache proxies `/api/` here). Default 4123. |
| `PUBLIC_ORIGIN` | Public site origin, e.g. `https://rotten.network`. Used for OAuth redirect + secure cookies. |
| `SESSION_SECRET` | Long random string that signs session cookies. Generate: `node -e "console.log(require('crypto').randomBytes(48).toString('hex'))"` |
| `DISCORD_CLIENT_ID` / `DISCORD_CLIENT_SECRET` | From the Discord Developer Portal → your app → OAuth2. |
| `DISCORD_REDIRECT_URI` | Must match a redirect added in the portal: `https://rotten.network/api/auth/callback` |
| `DISCORD_BOT_TOKEN` | Optional. Bot tab → token. Enables the Discord Lookup tool's profile fetch. |
| `ADMIN_DISCORD_IDS` | Comma-separated Discord **user** IDs granted the `/admin` panel. Your own account's user ID (Developer Mode → Copy User ID), not the client ID. |
| `DB_PATH` | SQLite file path (relative to `backend/`). Default `./data/rotten.sqlite`. |
| `UPLOAD_MAX_MB` | Max size of an admin project upload. Default 512. |

Project files are stored in `backend/uploads/` (outside the web root) and are only
reachable through the authenticated download route — never served statically.

## Movies tab

Drop video files directly into `backend/movies/` (or wherever `MOVIES_DIR` points)
on the server — no upload form, just copy files in via scp/sftp. Every call to
`/api/movies` re-scans that folder and syncs a lightweight DB table (adds new
files, drops rows for files you removed). Titles are derived from the filename;
rename the file on disk to rename the title.

Supported containers: mp4, m4v, mov, webm, mkv, avi, wmv, flv, mpg, mpeg, ogv, ts,
3gp. All of them stream (with working seek/scrub, via HTTP range requests), but
browsers can only really *decode* mp4/m4v/mov/webm/ogv with common codecs (h264/
aac) inside the `<video>` tag — mkv/avi/wmv/flv/ts often won't play in-page
depending on their codec, so those show a "may not play in-browser" badge and a
download link as a fallback.

## API surface

| Method | Path | Purpose |
|--------|------|---------|
| GET | `/api/health` | liveness check |
| GET | `/api/me` | current user + CSRF token + feature flags |
| GET | `/api/auth/discord` | start Discord OAuth |
| GET | `/api/auth/callback` | OAuth callback (sets session cookie) |
| GET | `/api/auth/logout` | clear session |
| GET | `/api/posts` | list posts (includes `liked` for the caller) |
| GET | `/api/posts/:id` | single post + comments |
| POST | `/api/posts` | create post (auth + CSRF) |
| POST | `/api/posts/:id/like` | toggle like (auth + CSRF) |
| POST | `/api/posts/:id/comments` | add comment (auth + CSRF) |
| GET | `/api/users/:id` | public profile + that user's posts |
| GET | `/api/admin/users` | all accounts + linked login IPs (admin only) |
| GET | `/api/projects` | list projects (login required) |
| GET | `/api/projects/:id/download` | download a project file (login required) |
| POST | `/api/projects` | upload a project file (admin, multipart) |
| DELETE | `/api/projects/:id` | delete a project + its file (admin) |
| GET | `/api/movies` | list movies (login required) — re-scans `MOVIES_DIR` each call |
| GET | `/api/movies/:id/stream` | stream a movie, range-request aware (login required) |
| GET | `/api/tools/geo?q=` | geolocate IP/domain (blank = caller IP) |
| GET | `/api/tools/discord?id=` | resolve a Discord ID to a profile |
| GET | `/api/tools/dns?domain=` | resolve A/AAAA/MX/TXT/NS/CNAME records |
| GET | `/api/tools/whois?domain=` | registrar, dates, age, nameservers (RDAP) |
| POST | `/api/tools/webhook` | send a test message to a Discord webhook |

Each login records the caller's IP (Cloudflare `CF-Connecting-IP` / `X-Forwarded-For`)
in a `logins` table; the admin panel joins those to accounts.

Auth is a signed `rot_sess` cookie (HttpOnly, SameSite=Lax). State-changing
requests must send `X-CSRF-Token` matching the token from `/api/me`.

## Running in production

Keep it alive with a process manager. Example systemd unit
(`/etc/systemd/system/rotten-backend.service`):

```ini
[Unit]
Description=rotten.network backend
After=network.target

[Service]
WorkingDirectory=/var/www/html/backend
ExecStart=/usr/bin/node src/server.js
Restart=always
User=www-data
EnvironmentFile=/var/www/html/backend/.env

[Install]
WantedBy=multi-user.target
```

Then: `sudo systemctl enable --now rotten-backend`, and make sure Apache proxies
`/api/` to `127.0.0.1:4123` (see `rotten.network.conf`).
