# Shield

> Production-ready, cPanel-compatible Node.js SaaS for detecting and taking down
> non-consensual intimate imagery (NCII). Multi-tenant, RBAC, Stripe billing,
> 3-tier detection, multi-key API rotation.

## Stack

- **Runtime**: Node.js 18+, CommonJS
- **HTTP**: Express 4
- **DB**: better-sqlite3 (file-based, no external service required)
- **Email**: transactional email (transactional)
- **Search**: search proxy (Google SERP scraping, multi-key)
- **AI fallback**: vision-AI provider (vision + chat) — minimised via 3-tier pipeline
- **Billing**: Stripe (Checkout + Customer Portal + Webhooks)
- **Auth**: bcrypt (12 rounds) + express-session (SQLite store) + CSRF
- **Image/video**: sharp + pHash, client-side Canvas frame extraction
- **Frontend**: vanilla HTML/CSS/JS (no build step)
- **Hosting**: Phusion Passenger / cPanel compatible

## Project layout

```
ncii-shield/
  app.js                 ← main entry
  package.json
  .env.example
  .htaccess              ← cPanel / Passenger config
  seed-admin.js          ← CLI: create the first superadmin
  config/                ← db, session, stripe, plans
  middleware/            ← auth, csrf, rateLimit, planGate
  models/                ← user, case, finding, takedown, apiKey, auditLog, usageLog
  routes/                ← auth, dashboard, cases, scan, takedown, billing, webhook, admin, api
  services/              ← keyRotator, detect, scrape, ai, email, hash, platformContacts
  jobs/                  ← dailyReset (cron + alert sweep)
  public/
    css/main.css
    js/{dashboard,admin,video-frames}.js
    views/{index,login,register,dashboard,case,scan,takedown,billing,admin,404}.html
  data/                  ← SQLite files (sessions.sqlite, ncii.sqlite) — auto-created
```

## Quick start (local)

```bash
cd ncii-shield
cp .env.example .env          # fill in real keys
npm install
node seed-admin.js --email=you@example.com --password='Strong!Pass1' --name='You'
npm start
# → http://localhost:3000
```

## cPanel deployment

1. Upload the `ncii-shield/` directory to your home folder, e.g. `~/ncii-shield`.
2. Set the **Application Root** in cPanel → Setup Node.js App to `~/ncii-shield`
   and the **Application Startup File** to `app.js`. Choose Node 18+.
3. Add the env vars from `.env.example` via the cPanel UI (or copy `.env`).
4. Copy `.htaccess` to the document root Passenger will serve from.
5. SSH in and run:
   ```bash
   cd ~/ncii-shield
   npm install --omit=dev
   node seed-admin.js --email=you@example.com --password='Strong!Pass1'
   ```
6. Restart the app from the cPanel Node.js UI.

> **Notes**
> - SQLite files are kept in `data/` — make sure that directory is writable.
> - Phusion Passenger picks up `app.js` automatically; no PM2 needed.
> - Set `TRUST_PROXY=1` so rate limits and IPs work behind Passenger.

## API key rotation

External service calls (transactional email, search proxy, vision-AI provider) **must** go through
`services/keyRotator.js`. It:

- Picks a random active key (shuffle)
- Retries on quota / 429 errors with the next key
- Marks exhausted keys as disabled until the next UTC midnight
- Logs every call against the chosen key (`calls_today`, `calls_total`)
- Resets `calls_today` via `node-cron` at 00:00 UTC
- Sends an admin email (via transactional email) if **all** keys for a service are exhausted

Add/edit/delete/toggle keys from `/admin` → API keys. The `.env` values are
seeded into the table on first boot as a fallback.

## Detection pipeline (3 tiers)

1. **URL pattern match** — free, instant. Substring match against the keyword
   list. Confirms and skips AI.
2. **Title/snippet keyword + victim name co-occurrence** — free, instant.
   Same keyword list against the scraped result. ≥2 hits also confirm.
3. **vision-AI provider AI fallback** — only if (1) and (2) are both inconclusive. Returns
   `{ isNcii, confidence }`; threshold `0.75` confirms.

Every AI call is logged to `ai_calls` (tokens, cost estimate, response time,
success flag). The admin analytics panel shows the **% AI avoided** — your
biggest cost saver.

## Plan limits

Stored in `config/plans.js` — never hardcoded in routes. Edit there to change.
`middleware/planGate.js` reads it and returns `402` with an upgrade prompt when
over limit. Monthly usage is tracked per user in `usage_log` and resets on the
billing-cycle date.

| Plan     | Price    | Cases | Scans | Takedowns | Users | API |
|----------|----------|------:|------:|----------:|------:|:---:|
| Free     | $0       |     1 |    10 |         5 |     1 |  ✗  |
| Standard | $29/mo   |     5 |   100 |        50 |     1 |  ✗  |
| Pro      | $79/mo   |    20 |   500 |       200 |     3 |  ✗  |
| Agency   | $199/mo  |    50 | 2,000 |     1,000 |    10 |  ✓  |

Stripe price IDs are wired via env (`STRIPE_PRICE_STANDARD`, etc.).

## RBAC

- **superadmin** — full platform control. Seeded via `seed-admin.js` only.
  No public registration can produce this role.
- **admin** — agency plan user. Manages their org's sub-users and cases.
- **user** — default. Sees only their own cases/data.

## Security checklist

- bcrypt 12 rounds, password policy enforced server-side
- Session cookies: httpOnly, sameSite=lax, secure in prod, 24h rolling
- CSRF: double-submit cookie on every unsafe method
- Login lockout: 5 attempts → 15 min (configurable via env)
- Helmet with strict CSP (Stripe domains whitelisted)
- Stripe webhook signature verification (raw body)
- All write actions audit-logged with actor + IP
- All `api_keys.key_value` are masked in API responses

## REST API (Agency plan)

```
GET    /api/v1/usage
GET    /api/v1/cases
POST   /api/v1/cases
GET    /api/v1/cases/:id
GET    /api/v1/findings
GET    /api/v1/takedowns
POST   /api/v1/takedowns
```

Auth: `Authorization: Bearer <api_token>` (admin sets the token; the column is
`users.api_token` — set it via SQL or extend admin to expose it).

## License

MIT

## Deploying to cPanel / Phusion Passenger

### Prerequisites
- cPanel account with SSH access
- Node.js 18+ (use the cPanel "Setup Node.js App" tool, "Run npm install" enabled)
- A real domain pointed at the account, with a verified SSL cert
- Outbound HTTPS to `api.<search-proxy>`, `api.<email>`, `api.vision-AI provider.com`

### One-time setup
1. Upload the tarball and extract to a path **outside** `public_html`:
   ```sh
   cd ~
   tar -xzf ncii-shield-1.2.0.tar.gz   # creates ~/ncii-shield/
   cd ncii-shield
   npm install --omit=dev
   ```
2. Copy `.env.example` to `.env` and fill in the **non-secret** values:
   - `NODE_ENV=production`
   - `PORT=3001`   (any free port — Passenger will proxy to it)
   - `TRUST_PROXY=1`   (so express-session sees `X-Forwarded-Proto: https` and sets the Secure cookie)
   - `SESSION_SECRET=<openssl rand -hex 64>`
   - `TOTP_ISSUER=Shield`
   - `PUBLIC_BASE_URL=https://takedowns.vesamuni.com`
3. **Don't put API keys in `.env`.** They live in the `api_keys` table
   (added in /admin → API keys, or via `scripts/seed-test-keys.js` for
   the initial dev key set).
4. In cPanel → "Setup Node.js App":
   - Application root: `ncii-shield`
   - Application URL: the public hostname
   - Application startup file: `app.js`
   - Passenger log file: leave default
5. Make sure `data/` and `sessions.sqlite` are **writable by the
   cPanel user** (`chmod 700 data`).
6. Restart the application from cPanel. The first request to
   `/auth/login` should serve 200. The first time you log in,
   the superadmin gets a `TWO_FA_REQUIRED` 401 on every admin route
   — that's expected. Run:
   ```sh
   curl -X POST https://takedowns.vesamuni.com/auth/login \
     -H 'content-type: application/json' \
     -d '{"email":"admin@yourdomain.com","password":"…"}' \
     -c cookies.txt
   curl -X POST https://takedowns.vesamuni.com/admin/2fa/start \
     -b cookies.txt -c cookies.txt
   # scan the QR with Google Authenticator
   curl -X POST https://takedowns.vesamuni.com/admin/2fa/verify \
     -b cookies.txt -c cookies.txt \
     -H 'content-type: application/json' \
     -d '{"token":"123456"}'
   ```
   The verify response sets `twofa_verified_at` in the session and
   gives you the one-time backup codes. Save them somewhere safe.

### Schema migrations
- The app runs `db.init()` on every startup, which applies lightweight
  migrations (added columns, CHECK-constraint widening for `takedowns`).
  These are idempotent and safe to run on every restart.
- If you ever need to wipe the DB, delete `data/ncii.sqlite` and
  `data/sessions.sqlite` and restart. The next request will rebuild
  the schema. **This deletes all users, cases, and API keys.**

### Production toggles
| Env var | Default | What it does |
|---------|---------|--------------|
| `NODE_ENV` | `development` | Enables Secure cookies, HSTS preload, BCRYPT 13, etc. when `production` |
| `TRUST_PROXY` | `1` | Number of reverse proxies in front of Node. Set to your CDN hop count. |
| `NCII_ALERT_TEST_MODE` | `0` | Shim email + search proxy for alert-pipeline QA. NEVER enable in prod. |
| `NCII_DEEP_DISCOVERY_TEST_MODE` | `0` | Skip search proxy, run pipeline against fixture. NEVER enable in prod. |
| `NCII_REQUIRE_SUPERADMIN_2FA` | `1` | When `0`, the admin gate lets superadmins in without 2FA (dev only). |

### Backups
- Run `npm run backup:data` before every production deploy and on a nightly
  schedule. It uses SQLite's backup API, so WAL-mode writes are captured
  consistently.
- The backup is written to `BACKUP_DIR` (default: `./backups`) as
  `prod-data-<timestamp>/`, with `ncii.sqlite`, copied evidence/record files,
  and `manifest.json` containing table counts plus `PRAGMA integrity_check`.
- To restore on production, stop the Node app, copy the backed-up
  `ncii.sqlite` to the configured `DB_PATH`, copy the backed-up `data-files/`
  contents into `data/`, then restart the app so `db.init()` can apply any
  pending idempotent migrations.
- `SESSION_DB_DIR` controls where `sessions.sqlite` is stored. Keep it on
  persistent disk in production; use a temp path only for local previews.
- `data/evidence/case_<id>/scan_<id>_<ts>.json` is the audit record
  for each deep discovery scan — keep these forever (legal hold).

### Hardening checklist before going live
- [ ] `SESSION_SECRET` is unique per environment and rotated quarterly
- [ ] `data/` is not web-accessible (Passenger / `.htaccess` should already do this)
- [ ] `STRIPE_WEBHOOK_SECRET` is set in `.env` AND verified by a real webhook test
- [ ] The default superadmin password is changed
- [ ] 2FA is enrolled for the superadmin
- [ ] At least one search proxy key with >50% quota headroom is in the `api_keys` table
- [ ] At least one transactional email key whose `from` domain is **verified** in transactional email's dashboard
- [ ] Cron / external monitor pings `https://<host>/health` every minute
- [ ] `npm run backup:data` runs nightly, retained 30 days
