portfolio-page/backend/README.md

68 lines
2.1 KiB
Markdown

# Audit request endpoint
Small Express service that receives the form on `/de/audit` and forwards it by
email. It is deployed **separately** from the static site: the website is built
with Vite and uploaded by FTP, this service runs on the VPS.
Its dependencies are intentionally kept out of the frontend `package.json` so
they never end up in the browser bundle.
## Endpoints
| Method | Path | Purpose |
| ------ | --------------------- | ----------------------------- |
| `GET` | `/health` | Liveness probe |
| `POST` | `/api/audit-request` | Accepts one audit request |
Expected JSON body:
```json
{
"name": "…",
"email": "…",
"url": "https://…",
"role": "agency | client",
"motivation": "…",
"gdprConsent": "true",
"website": ""
}
```
`website` is a honeypot and must stay empty. A filled honeypot is answered with
`200` on purpose so bots learn nothing and do not retry.
## Setup
```bash
cd backend
npm install
cp .env.example .env # then fill in the SMTP credentials
npm start
```
The process binds to `127.0.0.1` only. Put nginx in front of it with TLS, for
example on `api.sascha-bach.de`, and proxy to the port from `.env`.
Then point the frontend at it by setting `VITE_AUDIT_ENDPOINT` before building:
```bash
VITE_AUDIT_ENDPOINT=https://api.sascha-bach.de/api/audit-request npm run build
```
If the CSP `connect-src` in `public/.htaccess` names a different host than the
one you deploy to, the browser blocks the request - keep the two in sync.
## Protections in place
- CORS allowlist from `ALLOWED_ORIGINS`, never `*`
- Rate limit: 3 requests per IP per 15 minutes
- Request body capped at 10 kB
- Server-side validation of every field (`express-validator`)
- Honeypot field
- Consent is required, not assumed
- CR/LF stripped from values used in mail headers, HTML escaped in the body
- No personal data written to logs
`app.set('trust proxy', 1)` assumes exactly one proxy hop (nginx). Adjust it if
you add a second one, otherwise the rate limiter sees a single IP for everyone.