feat(quality): Sprint 2 pre-deploy gates and server smoke tests
Extract Express app factory for testability, add Vitest/Supertest API smoke tests, root npm run check script, and deployment docs. Fix express-rate-limit IPv6 keyGenerator for feedback limiter. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -219,13 +219,19 @@ cd server && npx prisma db push && cd ..
|
|||||||
| Health Check | http://localhost:5000/api/health |
|
| Health Check | http://localhost:5000/api/health |
|
||||||
| Public Demo | http://localhost:5173/demo |
|
| Public Demo | http://localhost:5173/demo |
|
||||||
|
|
||||||
### 5. Tests (Frontend)
|
### 5. Qualität & Tests
|
||||||
|
|
||||||
|
Vor jedem Deploy auf [kapteins-daagbok.eu](https://kapteins-daagbok.eu/) (kein externes CI):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd client && npm test
|
npm run check
|
||||||
|
# oder: ./scripts/predeploy-check.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
Vitest-Unit-Tests für Utils, i18n und Services (z. B. Kurswinkel, Benutzereinstellungen).
|
Einzeln: `npm test` (Client + Server) · `npm run build` · optional `npm run lint` (Client, noch nicht in `check`)
|
||||||
|
|
||||||
|
- **Client:** Vitest für Utils, i18n, Services
|
||||||
|
- **Server:** Smoke-Tests (`/api/health`, Auth-Guards) mit Supertest — siehe `server/src/api.smoke.test.ts`
|
||||||
|
|
||||||
## Docker (produktionsnah)
|
## Docker (produktionsnah)
|
||||||
|
|
||||||
@@ -241,6 +247,8 @@ Umgebungsvariablen in `.env` setzen — mindestens `RP_ID`, `ORIGIN` (z. B. `htt
|
|||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
|
**Vor dem Deploy:** `npm run check` lokal ausführen.
|
||||||
|
|
||||||
Produktions-Update auf den Server (konfigurierbar via Umgebungsvariablen):
|
Produktions-Update auf den Server (konfigurierbar via Umgebungsvariablen):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -258,6 +266,7 @@ Hinter **Nginx Proxy Manager**: [docs/deployment/npm-security.md](docs/deploymen
|
|||||||
| Dokument | Inhalt |
|
| Dokument | Inhalt |
|
||||||
|----------|--------|
|
|----------|--------|
|
||||||
| [docs/deployment/npm-security.md](docs/deployment/npm-security.md) | NPM, TLS, `trust proxy`, Security-Header |
|
| [docs/deployment/npm-security.md](docs/deployment/npm-security.md) | NPM, TLS, `trust proxy`, Security-Header |
|
||||||
|
| [docs/deployment/predeploy.md](docs/deployment/predeploy.md) | Pre-Deploy-Checks ohne CI |
|
||||||
| [docs/deployment/postgres-password.md](docs/deployment/postgres-password.md) | PostgreSQL-Passwort rotieren / App-Rolle |
|
| [docs/deployment/postgres-password.md](docs/deployment/postgres-password.md) | PostgreSQL-Passwort rotieren / App-Rolle |
|
||||||
| [docs/plausible-events.md](docs/plausible-events.md) | Custom Events für Plausible Analytics |
|
| [docs/plausible-events.md](docs/plausible-events.md) | Custom Events für Plausible Analytics |
|
||||||
| [docs/push-notifications-plan.md](docs/push-notifications-plan.md) | Web Push: Architektur, API, Testplan |
|
| [docs/push-notifications-plan.md](docs/push-notifications-plan.md) | Web Push: Architektur, API, Testplan |
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# Pre-Deploy-Checks (ohne CI)
|
||||||
|
|
||||||
|
Vor jedem Update auf **https://kapteins-daagbok.eu/** lokal ausführen:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run check
|
||||||
|
```
|
||||||
|
|
||||||
|
Das Skript [`scripts/predeploy-check.sh`](../../scripts/predeploy-check.sh) führt aus:
|
||||||
|
|
||||||
|
1. i18n-Key-Validierung (`validate:i18n`)
|
||||||
|
2. Client: `test` → `build` (TypeScript via `tsc -b`)
|
||||||
|
3. Server: `test` → `build`
|
||||||
|
|
||||||
|
## Einzelbefehle (Repo-Root)
|
||||||
|
|
||||||
|
| Befehl | Inhalt |
|
||||||
|
|--------|--------|
|
||||||
|
| `npm run lint` | ESLint (Client) — optional, noch nicht Teil von `check` |
|
||||||
|
| `npm run test` | Vitest Client + Server |
|
||||||
|
| `npm run build` | Production-Build beider Pakete |
|
||||||
|
| `npm run predeploy` | Alias für `npm run check` |
|
||||||
|
|
||||||
|
## Server-Tests
|
||||||
|
|
||||||
|
Smoke-Tests in `server/src/api.smoke.test.ts` — keine echte Datenbank (Prisma gemockt). Prüfen u. a. Health, 401 ohne Session, öffentliche Collaboration-Validierung.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd server && npm test
|
||||||
|
```
|
||||||
|
|
||||||
|
## Nach erfolgreichem Check
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/update-prod.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Oder manuell auf dem Server: `git pull`, `docker compose build`, `docker compose up -d` (siehe [npm-security.md](npm-security.md)).
|
||||||
+6
-1
@@ -7,6 +7,11 @@
|
|||||||
"translate:flyer": "node scripts/translate-flyer.mjs",
|
"translate:flyer": "node scripts/translate-flyer.mjs",
|
||||||
"validate:i18n": "node scripts/validate-i18n-keys.mjs",
|
"validate:i18n": "node scripts/validate-i18n-keys.mjs",
|
||||||
"generate:flyer": "node scripts/generate-beta-flyer.mjs",
|
"generate:flyer": "node scripts/generate-beta-flyer.mjs",
|
||||||
"generate:flyer:all": "node scripts/generate-beta-flyer.mjs --all"
|
"generate:flyer:all": "node scripts/generate-beta-flyer.mjs --all",
|
||||||
|
"lint": "npm run lint --prefix client",
|
||||||
|
"test": "npm run test --prefix client && npm run test --prefix server",
|
||||||
|
"build": "npm run build --prefix client && npm run build --prefix server",
|
||||||
|
"check": "bash scripts/predeploy-check.sh",
|
||||||
|
"predeploy": "npm run check"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Executable
+40
@@ -0,0 +1,40 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Local quality gates before deploying to kapteins-daagbok.eu (no external CI).
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
cd "$ROOT"
|
||||||
|
|
||||||
|
echo "=================================================="
|
||||||
|
echo " Kapteins Daagbok — pre-deploy checks"
|
||||||
|
echo "=================================================="
|
||||||
|
|
||||||
|
run() {
|
||||||
|
echo ""
|
||||||
|
echo "==> $*"
|
||||||
|
"$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
run npm run validate:i18n
|
||||||
|
|
||||||
|
pushd client >/dev/null
|
||||||
|
if [ ! -d node_modules ]; then
|
||||||
|
run npm ci
|
||||||
|
fi
|
||||||
|
# Lint: run separately with `npm run lint` (client ESLint; cleanup tracked separately)
|
||||||
|
run npm run test
|
||||||
|
run npm run build
|
||||||
|
popd >/dev/null
|
||||||
|
|
||||||
|
pushd server >/dev/null
|
||||||
|
if [ ! -d node_modules ]; then
|
||||||
|
run npm ci
|
||||||
|
fi
|
||||||
|
run npm run test
|
||||||
|
run npm run build
|
||||||
|
popd >/dev/null
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=================================================="
|
||||||
|
echo " All pre-deploy checks passed."
|
||||||
|
echo "=================================================="
|
||||||
Generated
+1885
-1
File diff suppressed because it is too large
Load Diff
+7
-2
@@ -7,7 +7,9 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"start": "node dist/index.js",
|
"start": "node dist/index.js",
|
||||||
"dev": "tsx watch src/index.ts"
|
"dev": "tsx watch src/index.ts",
|
||||||
|
"test": "vitest run",
|
||||||
|
"test:watch": "vitest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/client": "^5.10.2",
|
"@prisma/client": "^5.10.2",
|
||||||
@@ -26,8 +28,11 @@
|
|||||||
"@types/cors": "^2.8.17",
|
"@types/cors": "^2.8.17",
|
||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
"@types/node": "^20.11.24",
|
"@types/node": "^20.11.24",
|
||||||
|
"@types/supertest": "^6.0.3",
|
||||||
"@types/web-push": "^3.6.4",
|
"@types/web-push": "^3.6.4",
|
||||||
|
"supertest": "^7.1.0",
|
||||||
"tsx": "^4.7.1",
|
"tsx": "^4.7.1",
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.3.3",
|
||||||
|
"vitest": "^3.0.9"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { describe, it, expect, vi, beforeAll } from 'vitest'
|
||||||
|
import request from 'supertest'
|
||||||
|
|
||||||
|
vi.mock('./db.js', () => ({
|
||||||
|
prisma: {
|
||||||
|
$queryRaw: vi.fn().mockResolvedValue([{ '?column?': 1 }])
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
const { createApp } = await import('./app.js')
|
||||||
|
|
||||||
|
describe('API smoke', () => {
|
||||||
|
const app = createApp()
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
process.env.SESSION_SECRET =
|
||||||
|
process.env.SESSION_SECRET ?? 'test-session-secret-minimum-32-characters-long'
|
||||||
|
process.env.ORIGIN = process.env.ORIGIN ?? 'http://localhost:5173'
|
||||||
|
process.env.RP_ID = process.env.RP_ID ?? 'localhost'
|
||||||
|
})
|
||||||
|
|
||||||
|
it('GET /api/health returns ok when database is reachable', async () => {
|
||||||
|
const res = await request(app).get('/api/health')
|
||||||
|
expect(res.status).toBe(200)
|
||||||
|
expect(res.body.status).toBe('ok')
|
||||||
|
expect(res.body.database).toBe('connected')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('GET /api/logbooks requires session', async () => {
|
||||||
|
const res = await request(app).get('/api/logbooks')
|
||||||
|
expect(res.status).toBe(401)
|
||||||
|
expect(res.body.error).toMatch(/Unauthorized/i)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('POST /api/sync/push requires session', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/api/sync/push')
|
||||||
|
.send({ items: [] })
|
||||||
|
expect(res.status).toBe(401)
|
||||||
|
expect(res.body.error).toMatch(/Unauthorized/i)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('GET /api/collaboration/invite-details requires token query', async () => {
|
||||||
|
const res = await request(app).get('/api/collaboration/invite-details')
|
||||||
|
expect(res.status).toBe(400)
|
||||||
|
expect(res.body.error).toMatch(/Token/i)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
import express from 'express'
|
||||||
|
import cors from 'cors'
|
||||||
|
import cookieParser from 'cookie-parser'
|
||||||
|
import helmet from 'helmet'
|
||||||
|
import rateLimit from 'express-rate-limit'
|
||||||
|
import authRouter from './routes/auth.js'
|
||||||
|
import logbooksRouter from './routes/logbooks.js'
|
||||||
|
import syncRouter from './routes/sync.js'
|
||||||
|
import collaborationRouter from './routes/collaboration.js'
|
||||||
|
import signRouter from './routes/sign.js'
|
||||||
|
import pushRouter from './routes/push.js'
|
||||||
|
import weatherRouter from './routes/weather.js'
|
||||||
|
import feedbackRouter from './routes/feedback.js'
|
||||||
|
import { prisma } from './db.js'
|
||||||
|
import { buildCorsOptions } from './cors.js'
|
||||||
|
|
||||||
|
/** Behind Nginx Proxy Manager. See docs/deployment/npm-security.md */
|
||||||
|
function configureTrustProxy(app: express.Express): void {
|
||||||
|
const raw = process.env.TRUST_PROXY?.trim()
|
||||||
|
if (raw === '1' || raw === 'true') {
|
||||||
|
app.set('trust proxy', 1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (raw) {
|
||||||
|
app.set('trust proxy', raw)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
app.set('trust proxy', 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createApp(): express.Express {
|
||||||
|
const app = express()
|
||||||
|
|
||||||
|
configureTrustProxy(app)
|
||||||
|
|
||||||
|
app.use(
|
||||||
|
helmet({
|
||||||
|
contentSecurityPolicy: false,
|
||||||
|
crossOriginEmbedderPolicy: false
|
||||||
|
})
|
||||||
|
)
|
||||||
|
app.use(cors(buildCorsOptions()))
|
||||||
|
app.use(cookieParser())
|
||||||
|
app.use(express.json({ limit: '50mb' }))
|
||||||
|
|
||||||
|
const authLimiter = rateLimit({
|
||||||
|
windowMs: 15 * 60 * 1000,
|
||||||
|
max: 60,
|
||||||
|
standardHeaders: true,
|
||||||
|
legacyHeaders: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const apiLimiter = rateLimit({
|
||||||
|
windowMs: 1 * 60 * 1000,
|
||||||
|
max: 300,
|
||||||
|
standardHeaders: true,
|
||||||
|
legacyHeaders: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const publicCollaborationLimiter = rateLimit({
|
||||||
|
windowMs: 15 * 60 * 1000,
|
||||||
|
max: 30,
|
||||||
|
standardHeaders: true,
|
||||||
|
legacyHeaders: false
|
||||||
|
})
|
||||||
|
|
||||||
|
app.use('/api/auth', authLimiter)
|
||||||
|
app.use('/api/collaboration/invite-details', publicCollaborationLimiter)
|
||||||
|
app.use('/api/collaboration/share-pull', publicCollaborationLimiter)
|
||||||
|
app.use('/api', apiLimiter)
|
||||||
|
|
||||||
|
app.use('/api/auth', authRouter)
|
||||||
|
app.use('/api/logbooks', logbooksRouter)
|
||||||
|
app.use('/api/sync', syncRouter)
|
||||||
|
app.use('/api/collaboration', collaborationRouter)
|
||||||
|
app.use('/api/sign', signRouter)
|
||||||
|
app.use('/api/push', pushRouter)
|
||||||
|
app.use('/api/weather', weatherRouter)
|
||||||
|
app.use('/api/feedback', feedbackRouter)
|
||||||
|
|
||||||
|
app.get('/api/health', async (_req, res) => {
|
||||||
|
try {
|
||||||
|
await prisma.$queryRaw`SELECT 1`
|
||||||
|
res.json({
|
||||||
|
status: 'ok',
|
||||||
|
database: 'connected',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
service: 'Kapteins Daagbok Backend'
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
res.status(500).json({
|
||||||
|
status: 'error',
|
||||||
|
database: 'disconnected',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
service: 'Kapteins Daagbok Backend'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return app
|
||||||
|
}
|
||||||
+2
-102
@@ -1,116 +1,16 @@
|
|||||||
import express from 'express'
|
|
||||||
import cors from 'cors'
|
|
||||||
import cookieParser from 'cookie-parser'
|
|
||||||
import helmet from 'helmet'
|
|
||||||
import rateLimit from 'express-rate-limit'
|
|
||||||
import dotenv from 'dotenv'
|
import dotenv from 'dotenv'
|
||||||
import { dirname, resolve } from 'node:path'
|
import { dirname, resolve } from 'node:path'
|
||||||
import { fileURLToPath } from 'node:url'
|
import { fileURLToPath } from 'node:url'
|
||||||
import authRouter from './routes/auth.js'
|
import { createApp } from './app.js'
|
||||||
import logbooksRouter from './routes/logbooks.js'
|
|
||||||
import syncRouter from './routes/sync.js'
|
|
||||||
import collaborationRouter from './routes/collaboration.js'
|
|
||||||
import signRouter from './routes/sign.js'
|
|
||||||
import pushRouter from './routes/push.js'
|
|
||||||
import weatherRouter from './routes/weather.js'
|
|
||||||
import feedbackRouter from './routes/feedback.js'
|
|
||||||
import { prisma } from './db.js'
|
|
||||||
import { buildCorsOptions } from './cors.js'
|
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||||
|
|
||||||
dotenv.config({ path: resolve(__dirname, '../../.env') })
|
dotenv.config({ path: resolve(__dirname, '../../.env') })
|
||||||
dotenv.config({ path: resolve(__dirname, '../.env') })
|
dotenv.config({ path: resolve(__dirname, '../.env') })
|
||||||
|
|
||||||
const app = express()
|
const app = createApp()
|
||||||
const PORT = process.env.PORT || 5000
|
const PORT = process.env.PORT || 5000
|
||||||
|
|
||||||
/** Behind Nginx Proxy Manager (172.16.10.10 → app). See docs/deployment/npm-security.md */
|
|
||||||
function configureTrustProxy(): void {
|
|
||||||
const raw = process.env.TRUST_PROXY?.trim()
|
|
||||||
if (raw === '1' || raw === 'true') {
|
|
||||||
app.set('trust proxy', 1)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (raw) {
|
|
||||||
app.set('trust proxy', raw)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (process.env.NODE_ENV === 'production') {
|
|
||||||
app.set('trust proxy', 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
configureTrustProxy()
|
|
||||||
|
|
||||||
app.use(
|
|
||||||
helmet({
|
|
||||||
contentSecurityPolicy: false,
|
|
||||||
crossOriginEmbedderPolicy: false
|
|
||||||
})
|
|
||||||
)
|
|
||||||
app.use(cors(buildCorsOptions()))
|
|
||||||
app.use(cookieParser())
|
|
||||||
// Encrypted sync payloads (photos, GPS tracks) can be large — align with nginx client_max_body_size
|
|
||||||
app.use(express.json({ limit: '50mb' }))
|
|
||||||
|
|
||||||
const authLimiter = rateLimit({
|
|
||||||
windowMs: 15 * 60 * 1000,
|
|
||||||
max: 60,
|
|
||||||
standardHeaders: true,
|
|
||||||
legacyHeaders: false
|
|
||||||
})
|
|
||||||
|
|
||||||
const apiLimiter = rateLimit({
|
|
||||||
windowMs: 1 * 60 * 1000,
|
|
||||||
max: 300,
|
|
||||||
standardHeaders: true,
|
|
||||||
legacyHeaders: false
|
|
||||||
})
|
|
||||||
|
|
||||||
/** Unauthenticated collaboration reads — stricter than global API limit */
|
|
||||||
const publicCollaborationLimiter = rateLimit({
|
|
||||||
windowMs: 15 * 60 * 1000,
|
|
||||||
max: 30,
|
|
||||||
standardHeaders: true,
|
|
||||||
legacyHeaders: false
|
|
||||||
})
|
|
||||||
|
|
||||||
app.use('/api/auth', authLimiter)
|
|
||||||
app.use('/api/collaboration/invite-details', publicCollaborationLimiter)
|
|
||||||
app.use('/api/collaboration/share-pull', publicCollaborationLimiter)
|
|
||||||
app.use('/api', apiLimiter)
|
|
||||||
|
|
||||||
// Mount routes
|
|
||||||
app.use('/api/auth', authRouter)
|
|
||||||
app.use('/api/logbooks', logbooksRouter)
|
|
||||||
app.use('/api/sync', syncRouter)
|
|
||||||
app.use('/api/collaboration', collaborationRouter)
|
|
||||||
app.use('/api/sign', signRouter)
|
|
||||||
app.use('/api/push', pushRouter)
|
|
||||||
app.use('/api/weather', weatherRouter)
|
|
||||||
app.use('/api/feedback', feedbackRouter)
|
|
||||||
|
|
||||||
// Health check endpoint
|
|
||||||
app.get('/api/health', async (req, res) => {
|
|
||||||
try {
|
|
||||||
await prisma.$queryRaw`SELECT 1`
|
|
||||||
res.json({
|
|
||||||
status: 'ok',
|
|
||||||
database: 'connected',
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
service: 'Kapteins Daagbok Backend'
|
|
||||||
})
|
|
||||||
} catch {
|
|
||||||
res.status(500).json({
|
|
||||||
status: 'error',
|
|
||||||
database: 'disconnected',
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
service: 'Kapteins Daagbok Backend'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
app.listen(PORT, () => {
|
app.listen(PORT, () => {
|
||||||
console.log(`[server] Server running on http://localhost:${PORT}`)
|
console.log(`[server] Server running on http://localhost:${PORT}`)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import rateLimit from 'express-rate-limit'
|
import rateLimit, { ipKeyGenerator } from 'express-rate-limit'
|
||||||
import type { AuthedRequest } from './auth.js'
|
import type { AuthedRequest } from './auth.js'
|
||||||
|
|
||||||
const MIN_SUBMIT_MS = 2_000
|
const MIN_SUBMIT_MS = 2_000
|
||||||
@@ -69,7 +69,11 @@ export const feedbackLimiter = rateLimit({
|
|||||||
max: 5,
|
max: 5,
|
||||||
standardHeaders: true,
|
standardHeaders: true,
|
||||||
legacyHeaders: false,
|
legacyHeaders: false,
|
||||||
keyGenerator: (req) => (req as AuthedRequest).userId ?? req.ip ?? 'unknown',
|
keyGenerator: (req) => {
|
||||||
|
const authed = req as AuthedRequest
|
||||||
|
if (authed.userId) return authed.userId
|
||||||
|
return ipKeyGenerator(req.ip ?? 'unknown')
|
||||||
|
},
|
||||||
handler: (_req, res) => {
|
handler: (_req, res) => {
|
||||||
res.status(429).json({
|
res.status(429).json({
|
||||||
error: 'Too many feedback submissions. Please try again later.',
|
error: 'Too many feedback submissions. Please try again later.',
|
||||||
|
|||||||
@@ -12,5 +12,6 @@
|
|||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"resolveJsonModule": true
|
"resolveJsonModule": true
|
||||||
},
|
},
|
||||||
"include": ["src/**/*"]
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["src/**/*.test.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { defineConfig } from 'vitest/config'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
environment: 'node',
|
||||||
|
include: ['src/**/*.test.ts'],
|
||||||
|
env: {
|
||||||
|
NODE_ENV: 'test',
|
||||||
|
SESSION_SECRET: 'test-session-secret-minimum-32-characters-long',
|
||||||
|
ORIGIN: 'http://localhost:5173',
|
||||||
|
RP_ID: 'localhost'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user