Migratie naar 2-container opzet (app + database)
De Express-app serveert nu zelf de gebouwde Vite-frontend op / met een SPA-fallback naar index.html; de aparte nginx-container is vervallen. Bedoeld voor self-hosting op een Raspberry Pi: één image, één origin, geen interne proxy en geen CORS tussen frontend en API. Docker - Nieuwe multi-stage Dockerfile in de root met targets test, development en production; frontend/Dockerfile, frontend/nginx-spa.conf en backend/Dockerfile verwijderd - docker-compose.yml: alleen app + postgres, app publiceert APP_PORT (standaard 3400) naar containerpoort 3001 - docker-compose.dev.yml: dev-container draait API (node --watch) en de Vite dev-server samen via docker/dev-entrypoint.sh, hot reload intact - .dockerignore, .env.example, .gitignore en .gitattributes toegevoegd Backend - express.static + SPA-fallback op STATIC_DIR (default /app/public), met immutable caching voor gehashte assets en no-store voor index.html - Rate limiting alleen nog op /api — statische bestanden liepen anders na 200 requests tegen de limiet aan - Helmet CSP toegespitst op wat de frontend laadt (Google Fonts, inline styles, base64-avatars); upgrade-insecure-requests uit voor intern http - Schema-init verplaatst naar initDb(), niet meer bij import van db.js Tests en CI/CD - backend/test/app.test.js: 11 tests (node --test) voor API-routes, SPA-fallback, cache- en security-headers, zonder database - test.ps1 / test.sh: backend-tests, productiebuild en smoketest op de echte app + postgres stack - azure-pipelines.yml: Test -> Build (incl. smoketest) -> Publish (multi-arch amd64 + arm64, alleen op main/master of een tag) Scripts en documentatie - dev.ps1/dev.sh met targets app en postgres, acties logs en test; dev.ps1 up wist de dev-database niet langer (alleen dev.ps1 down) - release.ps1/release.sh bouwen één image voor amd64 + arm64 en draaien de testsuite vóór het publiceren - README.md en BEHEER.md bijgewerkt voor de nieuwe structuur Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
3c67381de9
commit
bfec3a25f3
@@ -0,0 +1,18 @@
|
||||
# Build-context zo klein mogelijk houden
|
||||
**/node_modules
|
||||
**/dist
|
||||
.git
|
||||
.gitignore
|
||||
.claude
|
||||
*.md
|
||||
!README.md
|
||||
.env
|
||||
.env.*
|
||||
docker-compose*.yml
|
||||
dev.ps1
|
||||
dev.sh
|
||||
release.ps1
|
||||
release.sh
|
||||
test.ps1
|
||||
test.sh
|
||||
azure-pipelines.yml
|
||||
@@ -0,0 +1,21 @@
|
||||
# ── VibeFinance omgevingsvariabelen ──────────────────────────────────────────
|
||||
# Kopieer naar .env en vul in: cp .env.example .env
|
||||
|
||||
# Database (postgres-container)
|
||||
POSTGRES_DB=vibefinance
|
||||
POSTGRES_USER=vibefinance
|
||||
POSTGRES_PASSWORD=kies-een-sterk-wachtwoord
|
||||
|
||||
# App
|
||||
# Poort waarop de app op de host draait (container luistert intern op 3001)
|
||||
APP_PORT=3400
|
||||
# Geheim voor het ondertekenen van JWT's — verplicht in productie
|
||||
JWT_SECRET=vervang-dit-door-een-lange-random-string
|
||||
JWT_EXPIRES=7d
|
||||
# Origin die de API mag aanspreken; bij één container is dat de app zelf
|
||||
CORS_ORIGIN=http://localhost:3400
|
||||
|
||||
# Update-notificaties (leeg = melding uit)
|
||||
UPDATE_CHECK_URL=https://vibehoogie.duckdns.org/api/v1/repos/vibe/VibeFinance/releases/latest
|
||||
# Alleen nodig als de Gitea-repo niet publiek leesbaar is
|
||||
# UPDATE_CHECK_TOKEN=
|
||||
@@ -0,0 +1,11 @@
|
||||
# Zorg dat shell-scripts en Docker-bestanden altijd met LF worden uitgechecked:
|
||||
# met CRLF faalt het shebang in de container ("bad interpreter").
|
||||
* text=auto
|
||||
|
||||
*.sh text eol=lf
|
||||
Dockerfile text eol=lf
|
||||
*.yml text eol=lf
|
||||
docker/* text eol=lf
|
||||
|
||||
# PowerShell mag CRLF houden (Windows-tooling)
|
||||
*.ps1 text eol=crlf
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
|
||||
# Build-output
|
||||
frontend/dist/
|
||||
|
||||
# Omgevingsvariabelen (gebruik .env.example als sjabloon)
|
||||
.env
|
||||
.env.local
|
||||
|
||||
# Logs en rommel
|
||||
*.log
|
||||
.DS_Store
|
||||
@@ -1,5 +1,8 @@
|
||||
# VibeFinance — Beheer
|
||||
|
||||
De app draait in **2 containers**: `app` (Express API + gebouwde frontend) en
|
||||
`postgres`. Eén image, één origin — geen aparte webserver en geen interne proxy.
|
||||
|
||||
---
|
||||
|
||||
## Ontwikkeling
|
||||
@@ -7,22 +10,50 @@
|
||||
```powershell
|
||||
.\dev.ps1 # build + start (standaard)
|
||||
.\dev.ps1 build # alleen bouwen
|
||||
.\dev.ps1 build -Target backend # alleen backend bouwen
|
||||
.\dev.ps1 build -Target frontend # alleen frontend bouwen
|
||||
.\dev.ps1 up -Target backend # alleen backend herbouwen + herstarten
|
||||
.\dev.ps1 up -Target frontend # alleen frontend herbouwen + herstarten
|
||||
.\dev.ps1 push # dev-images naar registry pushen
|
||||
.\dev.ps1 push -Target backend # alleen backend pushen
|
||||
.\dev.ps1 push -Target frontend # alleen frontend pushen
|
||||
.\dev.ps1 up -Target app # alleen de app herbouwen + herstarten
|
||||
.\dev.ps1 up -Target postgres # alleen de database herstarten
|
||||
.\dev.ps1 logs # logs van de app volgen
|
||||
.\dev.ps1 test # testsuite draaien
|
||||
.\dev.ps1 down # stoppen én volumes wissen
|
||||
.\dev.ps1 push # dev-image naar registry pushen
|
||||
```
|
||||
|
||||
> Op macOS/Linux: gebruik `./dev.sh` met dezelfde argumenten.
|
||||
|
||||
> ⚠️ `.\dev.ps1 up` (beide services) en `.\dev.ps1 down` voeren intern `docker compose down -v` uit — dit wist de development-database. Gebruik voor een herstart zonder dataverlies:
|
||||
In dev draait één container beide processen (zie `docker/dev-entrypoint.sh`):
|
||||
|
||||
| Adres | Wat |
|
||||
|---|---|
|
||||
| http://localhost | Vite dev-server met hot reload |
|
||||
| http://localhost:3001/api/health | de API rechtstreeks |
|
||||
|
||||
Vite proxyt `/api` naar `localhost:3001` binnen dezelfde container.
|
||||
|
||||
> ⚠️ Alleen `.\dev.ps1 down` wist de development-database (`docker compose down -v`).
|
||||
> `.\dev.ps1 up` doet een gewone `down` en laat het volume staan.
|
||||
|
||||
---
|
||||
|
||||
## Testen
|
||||
|
||||
```powershell
|
||||
.\test.ps1 # backend-tests + productiebuild + smoketest
|
||||
.\test.ps1 -Unit # alleen de backend-tests
|
||||
.\test.ps1 -NoSmoke # tests + build, zonder de stack te starten
|
||||
```
|
||||
|
||||
Alles loopt in Docker; Node op de host is niet nodig.
|
||||
|
||||
| Stap | Wat er gebeurt |
|
||||
|---|---|
|
||||
| Backend-tests | `Dockerfile` target `test` → `node --test` in `backend/test/`, zonder database |
|
||||
| Productiebuild | `Dockerfile` target `production` — faalt de Vite-build, faalt deze stap |
|
||||
| Smoketest | app + postgres starten en `/api/health` en `/` opvragen |
|
||||
|
||||
Losse backend-tests draaien in een lopende dev-container:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.dev.yml down
|
||||
docker compose -f docker-compose.dev.yml up -d --build
|
||||
docker compose -f docker-compose.dev.yml exec app npm test
|
||||
```
|
||||
|
||||
---
|
||||
@@ -31,16 +62,38 @@ docker compose -f docker-compose.dev.yml up -d --build
|
||||
|
||||
```powershell
|
||||
# 1. Versienummer in package.json van "x.x.x-dev" naar "x.x.x"
|
||||
# 2.
|
||||
.\release.ps1 # bouw, push naar registry, maak Git-tag aan
|
||||
.\release.ps1 -DryRun # droogloop — toont alle stappen zonder uit te voeren
|
||||
.\release.ps1 -NoBuild # sla docker build over, push bestaande images
|
||||
# 2. Changelog.md bijwerken — die sectie wordt de release-notes
|
||||
# 3. Token met release-rechten zetten
|
||||
$env:GITEA_TOKEN = "<token>"
|
||||
|
||||
# 3. Versienummer in package.json naar "x.x.(x+1)-dev" voor volgende cyclus
|
||||
.\release.ps1 # test, bouw (amd64+arm64), push, tag, Gitea-release
|
||||
.\release.ps1 -DryRun # droogloop — toont alle stappen zonder uit te voeren
|
||||
.\release.ps1 -NoBuild # sla docker build over, alleen tag + release
|
||||
.\release.ps1 -SkipTests # sla de testsuite over (niet aanbevolen)
|
||||
|
||||
# 4. Versienummer in package.json naar "x.x.(x+1)-dev" voor volgende cyclus
|
||||
```
|
||||
|
||||
Het script bouwt één image voor `linux/amd64` en `linux/arm64`, zodat dezelfde
|
||||
tag op een Raspberry Pi en op een x86-server werkt.
|
||||
|
||||
---
|
||||
|
||||
## CI/CD (Azure DevOps)
|
||||
|
||||
`azure-pipelines.yml` doet in drie stages hetzelfde als de lokale scripts:
|
||||
|
||||
| Stage | Wat |
|
||||
|---|---|
|
||||
| `Test` | `docker build --target test` + `docker run` → backend-tests |
|
||||
| `Build` | productie-image bouwen en smoketesten tegen een postgres-container |
|
||||
| `Publish` | multi-arch image (amd64 + arm64) pushen naar de registry |
|
||||
|
||||
`Publish` loopt alleen op `main`/`master` of een tag. Vul vóór het eerste gebruik
|
||||
de variabele `dockerRegistryServiceConnection` in met de naam van je Docker
|
||||
Registry service connection in Azure DevOps.
|
||||
|
||||
---
|
||||
## Docker
|
||||
|
||||
```bash
|
||||
@@ -71,29 +124,37 @@ docker exec -i vibefinance_postgres psql -U vibefinance -d vibefinance < backup-
|
||||
|
||||
---
|
||||
|
||||
## Architectuur — nginx proxy
|
||||
## Architectuur — één app-container
|
||||
|
||||
De frontend-container (nginx) serveert de React-app en proxyt `/api/`-verzoeken intern door naar de backend-container op poort 3001. Dit is geconfigureerd in `frontend/nginx-spa.conf`:
|
||||
De Express-app serveert zowel de API als de gebouwde frontend:
|
||||
|
||||
```nginx
|
||||
location /api/ {
|
||||
proxy_pass http://backend:3001;
|
||||
}
|
||||
```
|
||||
| Verzoek | Wat de app doet |
|
||||
|---|---|
|
||||
| `/api/...` | REST API (rate limited) |
|
||||
| `/assets/...` | statische build uit `STATIC_DIR`, 1 jaar `immutable` gecached |
|
||||
| al het overige | `index.html` — fallback voor React-routing |
|
||||
|
||||
> Ontbreekt dit blok, dan krijgen gebruikers "Kan geen verbinding maken met de server" bij het inloggen — ook al draaien alle containers.
|
||||
`STATIC_DIR` staat standaard op `/app/public`; daar zet de `Dockerfile` de
|
||||
Vite-build neer. Bestaat die map niet (bijv. in dev), dan serveert de app
|
||||
alleen de API en logt dat bij het starten.
|
||||
|
||||
> Ontbreekt de build in het image, dan krijg je op `/` een 404 en werkt alleen
|
||||
> `/api/...`. Controleer dan de `frontend-build` stage in de `Dockerfile`.
|
||||
|
||||
---
|
||||
|
||||
## Logs en diagnose
|
||||
|
||||
```bash
|
||||
# Live logs (dev)
|
||||
docker compose -f docker-compose.dev.yml logs -f
|
||||
# Live logs (dev) — beide processen van de app-container
|
||||
docker compose -f docker-compose.dev.yml logs -f app
|
||||
|
||||
# Live logs (productie)
|
||||
docker compose logs -f
|
||||
|
||||
# Shell in de app-container (dev)
|
||||
docker compose -f docker-compose.dev.yml exec app sh
|
||||
|
||||
# Status containers
|
||||
docker compose ps
|
||||
|
||||
@@ -157,7 +218,8 @@ Rollen:
|
||||
- [ ] `POSTGRES_PASSWORD` instellen op een sterk wachtwoord
|
||||
- [ ] `JWT_SECRET` aanpassen (genereer met `openssl rand -hex 64`)
|
||||
- [ ] `CORS_ORIGIN` instellen op je domeinnaam
|
||||
- [ ] `FRONTEND_PORT` aanpassen indien poort 3300 al in gebruik is
|
||||
- [ ] `APP_PORT` aanpassen indien poort 3400 al in gebruik is
|
||||
- [ ] Testsuite groen (`test.ps1` / `./test.sh`)
|
||||
- [ ] TLS afhandelen via een externe reverse proxy (bijv. Nginx Proxy Manager, Caddy, Traefik)
|
||||
|
||||
---
|
||||
@@ -165,8 +227,7 @@ Rollen:
|
||||
## Registry
|
||||
|
||||
```
|
||||
vibehoogie.duckdns.org:3000/vibe/vibefinance-backend
|
||||
vibehoogie.duckdns.org:3000/vibe/vibefinance-frontend
|
||||
vibehoogie.duckdns.org/vibe/vibefinance-app (frontend + backend in één image)
|
||||
|
||||
Tags:
|
||||
latest productie — meest recente release
|
||||
@@ -177,7 +238,7 @@ Tags:
|
||||
|
||||
Inloggen op registry:
|
||||
```bash
|
||||
docker login vibehoogie.duckdns.org:3000
|
||||
docker login vibehoogie.duckdns.org
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -1,5 +1,43 @@
|
||||
# Changelog
|
||||
|
||||
## [0.1.6] - 2026-09-04
|
||||
|
||||
### Gewijzigd
|
||||
- **Migratie naar 2 containers**: de Express-app serveert nu zelf de gebouwde
|
||||
frontend op `/` (met SPA-fallback naar `index.html`); de aparte nginx-container
|
||||
is vervallen. Eén image, één origin, geen interne proxy — bedoeld voor
|
||||
self-hosting op een Raspberry Pi
|
||||
- Nieuwe multi-stage `Dockerfile` in de root met targets `test`, `development`
|
||||
en `production`; `frontend/Dockerfile`, `frontend/nginx-spa.conf` en
|
||||
`backend/Dockerfile` zijn verwijderd
|
||||
- `docker-compose.yml` bevat alleen nog `app` en `postgres`; de app publiceert
|
||||
`APP_PORT` (standaard 3400) naar containerpoort 3001
|
||||
- Dev-stack draait beide processen in één container (`docker/dev-entrypoint.sh`):
|
||||
API met `node --watch` en de Vite dev-server met hot reload
|
||||
- Rate limiting geldt alleen nog voor `/api` — statische bestanden werden anders
|
||||
na 200 requests geblokkeerd
|
||||
- Content-Security-Policy via Helmet toegespitst op wat de frontend laadt
|
||||
(Google Fonts, inline styles, base64-avatars)
|
||||
- Assets met hash worden een jaar `immutable` gecached, `index.html` nooit
|
||||
- Schema-initialisatie zit nu in `initDb()` in plaats van bij het importeren van
|
||||
`db.js`, zodat de app zonder database te importeren is (tests)
|
||||
- `dev.ps1`, `release.ps1` en de shell-wrappers werken met één app-image;
|
||||
`dev.ps1 up` wist de dev-database niet langer (alleen `dev.ps1 down`)
|
||||
- `release.ps1` draait de testsuite vóór het publiceren en bouwt voor
|
||||
`linux/amd64` + `linux/arm64`
|
||||
|
||||
### Toegevoegd
|
||||
- Testsuite: `backend/test/app.test.js` (11 tests met `node --test`) voor de
|
||||
API-routes, SPA-fallback, cache-headers en security-headers
|
||||
- `test.ps1` / `test.sh`: backend-tests, productiebuild en smoketest op de
|
||||
echte 2-container stack
|
||||
- `azure-pipelines.yml`: CI/CD met stages Test → Build (incl. smoketest) →
|
||||
Publish (multi-arch push, alleen op main/master of een tag)
|
||||
- `.env.example`, `.dockerignore` en `.gitignore`
|
||||
- `STATIC_DIR` als omgevingsvariabele (map met de frontend-build)
|
||||
|
||||
---
|
||||
|
||||
## [0.1.5] - 2026-09-04
|
||||
|
||||
### Toegevoegd
|
||||
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
# ╔══════════════════════════════════════════════════════════════════════════╗
|
||||
# ║ VibeFinance — één app-image: Express API + gebouwde Vite-frontend ║
|
||||
# ║ ║
|
||||
# ║ Targets: ║
|
||||
# ║ test backend-tests (npm test, geen database nodig) ║
|
||||
# ║ development API (node --watch) + Vite dev-server, hot reload ║
|
||||
# ║ production API die de statische build op / serveert ║
|
||||
# ╚══════════════════════════════════════════════════════════════════════════╝
|
||||
|
||||
# ── Stage 1: frontend dependencies ────────────────────────────────────────────
|
||||
FROM node:22-alpine AS frontend-deps
|
||||
WORKDIR /build/frontend
|
||||
COPY frontend/package.json frontend/package-lock.json* ./
|
||||
RUN npm install && npm cache clean --force
|
||||
|
||||
# ── Stage 2: frontend bouwen (Vite → dist) ────────────────────────────────────
|
||||
FROM frontend-deps AS frontend-build
|
||||
ARG VITE_API_URL=/api
|
||||
ARG VITE_APP_VERSION=dev
|
||||
ENV VITE_API_URL=$VITE_API_URL
|
||||
ENV VITE_APP_VERSION=$VITE_APP_VERSION
|
||||
COPY frontend/ ./
|
||||
RUN npm run build
|
||||
|
||||
# ── Stage 3: backend dependencies ─────────────────────────────────────────────
|
||||
FROM node:22-alpine AS backend-deps
|
||||
WORKDIR /app
|
||||
COPY backend/package.json backend/package-lock.json* ./
|
||||
RUN npm install && npm cache clean --force
|
||||
|
||||
# ── Stage 4: productie-dependencies (zonder devDependencies) ──────────────────
|
||||
FROM backend-deps AS backend-prod-deps
|
||||
RUN npm prune --omit=dev
|
||||
|
||||
# ── Stage 5: tests (backend, zonder database) ─────────────────────────────────
|
||||
FROM backend-deps AS test
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV=test
|
||||
COPY backend/ ./
|
||||
CMD ["npm", "test"]
|
||||
|
||||
# ── Stage 6: development (API + Vite samen, hot reload) ───────────────────────
|
||||
FROM node:22-alpine AS development
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV=development
|
||||
COPY --from=backend-deps /app/node_modules ./node_modules
|
||||
COPY --from=frontend-deps /build/frontend/node_modules ./frontend/node_modules
|
||||
COPY backend/ ./
|
||||
COPY frontend/ ./frontend/
|
||||
COPY docker/dev-entrypoint.sh /usr/local/bin/dev-entrypoint.sh
|
||||
RUN chmod +x /usr/local/bin/dev-entrypoint.sh
|
||||
# 5173 = Vite dev-server (browser), 3001 = API
|
||||
EXPOSE 5173 3001
|
||||
CMD ["/usr/local/bin/dev-entrypoint.sh"]
|
||||
|
||||
# ── Stage 7: production (minimale image) ──────────────────────────────────────
|
||||
FROM node:22-alpine AS production
|
||||
LABEL maintainer="VibeFinance"
|
||||
ARG APP_VERSION=dev
|
||||
ENV NODE_ENV=production
|
||||
ENV APP_VERSION=$APP_VERSION
|
||||
|
||||
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=backend-prod-deps /app/node_modules ./node_modules
|
||||
COPY backend/package.json ./
|
||||
COPY backend/src/ ./src/
|
||||
# De frontend-build wordt door de backend geserveerd (config.STATIC_DIR)
|
||||
COPY --from=frontend-build /build/frontend/dist ./public/
|
||||
|
||||
RUN chown -R appuser:appgroup /app
|
||||
USER appuser
|
||||
|
||||
EXPOSE 3001
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
|
||||
CMD wget -qO- http://localhost:3001/api/health || exit 1
|
||||
|
||||
CMD ["node", "src/server.js"]
|
||||
@@ -27,13 +27,16 @@ Deze applicatie is volledig ontworpen en ontwikkeld met behulp van [Claude.ai](h
|
||||
|
||||
> Vereisten: Docker Engine 24+ en Docker Compose v2+
|
||||
|
||||
De app draait in **2 containers**: `app` (Express API + gebouwde frontend) en
|
||||
`postgres` (database).
|
||||
|
||||
**`.env`** — kopieer `.env.example` en vul in:
|
||||
|
||||
```env
|
||||
POSTGRES_USER=vibefinance
|
||||
POSTGRES_PASSWORD=kies_een_sterk_wachtwoord
|
||||
JWT_SECRET=minimaal_64_willekeurige_tekens
|
||||
FRONTEND_PORT=3300
|
||||
APP_PORT=3400
|
||||
```
|
||||
|
||||
```bash
|
||||
@@ -42,10 +45,14 @@ openssl rand -hex 64
|
||||
```
|
||||
|
||||
```bash
|
||||
cp .env.example .env # en vul de waarden in
|
||||
docker compose pull && docker compose up -d
|
||||
```
|
||||
|
||||
Navigeer naar `http://<server-ip>:3300` en registreer je account. De **eerste** geregistreerde gebruiker krijgt automatisch de Admin-rol.
|
||||
Navigeer naar `http://<server-ip>:3400` en registreer je account. De **eerste** geregistreerde gebruiker krijgt automatisch de Admin-rol.
|
||||
|
||||
De container luistert intern op poort 3001 en serveert daar zowel de frontend
|
||||
(op `/`) als de API (op `/api`). `APP_PORT` bepaalt de poort op de host.
|
||||
|
||||
### Development (hot reload)
|
||||
|
||||
@@ -53,9 +60,45 @@ Navigeer naar `http://<server-ip>:3300` en registreer je account. De **eerste**
|
||||
.\dev.ps1
|
||||
```
|
||||
|
||||
Frontend bereikbaar op **http://localhost** (poort 80 → Vite dev server op 5173).
|
||||
Eén dev-container draait de API (`node --watch`) én de Vite dev-server samen:
|
||||
|
||||
> **Let op:** `.\dev.ps1 up` en `.\dev.ps1 down` wissen de development-database (volumes). Gebruik voor een gewone herstart `docker compose -f docker-compose.dev.yml down` gevolgd door `docker compose -f docker-compose.dev.yml up -d --build`.
|
||||
| Adres | Wat |
|
||||
|---|---|
|
||||
| http://localhost | de app in de browser (Vite, hot reload) |
|
||||
| http://localhost:3001/api/health | de API rechtstreeks |
|
||||
|
||||
Vite proxyt `/api` naar de API in dezelfde container, dus je hebt geen CORS-
|
||||
uitzonderingen nodig. Handige acties:
|
||||
|
||||
```powershell
|
||||
.\dev.ps1 up -Target app # alleen de app herbouwen en herstarten
|
||||
.\dev.ps1 logs # logs van de app volgen
|
||||
.\dev.ps1 down # stoppen én volumes wissen (dev-database weg)
|
||||
.\dev.ps1 test # testsuite draaien
|
||||
```
|
||||
|
||||
> `.\dev.ps1 up` laat de dev-database staan; alleen `.\dev.ps1 down` wist de volumes.
|
||||
|
||||
### Testen
|
||||
|
||||
```powershell
|
||||
.\test.ps1 # backend-tests + productiebuild + smoketest
|
||||
.\test.ps1 -Unit # alleen de backend-tests
|
||||
.\test.ps1 -NoSmoke # tests + build, zonder de stack te starten
|
||||
```
|
||||
|
||||
Alles loopt in Docker — Node op de host is niet nodig. De backend-tests draaien
|
||||
zonder database via het `test`-target in de `Dockerfile`.
|
||||
|
||||
### Productie release
|
||||
|
||||
```powershell
|
||||
$env:GITEA_TOKEN = "<token met release-rechten>"
|
||||
.\release.ps1
|
||||
```
|
||||
|
||||
Draait de tests, bouwt het app-image voor `amd64` + `arm64` (Raspberry Pi),
|
||||
pusht naar de registry, zet de Git-tag en publiceert de Gitea-release.
|
||||
|
||||
---
|
||||
|
||||
@@ -75,22 +118,28 @@ Frontend bereikbaar op **http://localhost** (poort 80 → Vite dev server op 517
|
||||
|
||||
```
|
||||
vibefinance/
|
||||
├── docker-compose.yml # Productie orchestratie (pre-built images)
|
||||
├── docker-compose.dev.yml # Standalone dev stack (hot reload)
|
||||
├── Dockerfile # Multi-stage: frontend build → app-image
|
||||
├── .dockerignore
|
||||
├── docker-compose.yml # Productie: app + postgres
|
||||
├── docker-compose.dev.yml # Dev: app (API + Vite) + postgres
|
||||
├── azure-pipelines.yml # CI/CD: test → build+smoketest → publish
|
||||
├── .env.example # Kopieer naar .env en vul in
|
||||
├── package.json # Versienummer — single source of truth
|
||||
├── dev.ps1 # Dev script (bouwen, starten, pushen)
|
||||
├── dev.ps1 / dev.sh # Dev script (bouwen, starten, logs, pushen)
|
||||
├── test.ps1 / test.sh # Testsuite (unit + build + smoketest)
|
||||
├── release.ps1 / release.sh # Release (test, push, tag, Gitea-release)
|
||||
│
|
||||
├── frontend/ # React / Vite SPA
|
||||
│ ├── Dockerfile # Multi-stage: dev → build → nginx
|
||||
│ ├── nginx-spa.conf # SPA fallback + /api proxy naar backend
|
||||
│ ├── vite.config.js
|
||||
├── docker/
|
||||
│ └── dev-entrypoint.sh # Start API + Vite samen in de dev-container
|
||||
│
|
||||
├── frontend/ # React / Vite SPA (build → dist)
|
||||
│ ├── vite.config.js # /api proxy naar de API in dezelfde container
|
||||
│ └── src/
|
||||
│ ├── App.jsx # Root – wiring + idle-logout
|
||||
│ ├── main.jsx
|
||||
│ ├── constants/ # Kleuren, tabs, begindata
|
||||
│ ├── utils/ # Formatters, helpers
|
||||
│ ├── hooks/ # useTheme
|
||||
│ ├── hooks/ # useTheme, useUpdateCheck
|
||||
│ ├── context/ # AppContext (global state + API sync)
|
||||
│ ├── components/
|
||||
│ │ ├── ui/ # Primitieve UI (VCard, DonutChart…)
|
||||
@@ -104,25 +153,39 @@ vibefinance/
|
||||
│ ├── SchuldTab.jsx
|
||||
│ └── VoortgangTab.jsx
|
||||
│
|
||||
└── backend/ # Express REST API
|
||||
├── Dockerfile # Multi-stage: dev → production (non-root)
|
||||
└── backend/ # Express REST API + statische frontend
|
||||
├── test/ # node --test integratietests
|
||||
└── src/
|
||||
├── server.js # Entry point
|
||||
├── db.js # PostgreSQL connectie + schema migraties
|
||||
├── config/ # Omgevingsvariabelen
|
||||
├── server.js # Entry point: API op /api, SPA op /
|
||||
├── db.js # PostgreSQL pool + initDb() schema-migraties
|
||||
├── config/ # Omgevingsvariabelen (incl. STATIC_DIR)
|
||||
├── constants/ # Begindata (initialData.js)
|
||||
├── middleware/ # auth.js, errorHandler.js
|
||||
├── routes/ # auth, data, users, health
|
||||
├── routes/ # auth, data, users, health, update-check
|
||||
└── stores/ # userStore, dataStore
|
||||
```
|
||||
|
||||
**Stack:** React 18 · Vite · Node.js 22 · Express 4 · PostgreSQL 18 · JWT · bcrypt · Docker Compose v2
|
||||
|
||||
### Waarom 2 containers
|
||||
|
||||
De frontend werd eerder door een eigen nginx-container geserveerd. Voor
|
||||
self-hosting op een Raspberry Pi is dat een container te veel: de Express-app
|
||||
serveert de statische build nu zelf, dus er is geen aparte webserver en geen
|
||||
interne proxy meer nodig. Dat betekent ook: één origin, dus geen CORS tussen
|
||||
frontend en API.
|
||||
|
||||
| Verzoek | Wat de app doet |
|
||||
|---|---|
|
||||
| `/api/...` | REST API (rate limited) |
|
||||
| `/assets/...` | Statische build, 1 jaar `immutable` gecached |
|
||||
| al het overige | `index.html` — fallback voor React-routing |
|
||||
|
||||
---
|
||||
|
||||
## 🔌 API
|
||||
|
||||
Beveiligde endpoints vereisen `Authorization: Bearer <token>`. Rate limiting: 20 req/15 min op auth-routes, 200 req/15 min globaal.
|
||||
Beveiligde endpoints vereisen `Authorization: Bearer <token>`. Rate limiting: 20 req/15 min op auth-routes, 200 req/15 min op de rest van `/api` (statische bestanden zijn uitgezonderd).
|
||||
|
||||
| Route | Methoden | Omschrijving |
|
||||
|---|---|---|
|
||||
@@ -133,6 +196,7 @@ Beveiligde endpoints vereisen `Authorization: Bearer <token>`. Rate limiting: 20
|
||||
| `/api/data` | GET, PUT, DELETE | Financiële data per gebruiker |
|
||||
| `/api/users` | GET, POST, PATCH, DELETE | Gebruikersbeheer (Admin only) |
|
||||
| `/api/health` | GET | Status + uptime |
|
||||
| `/api/update-check` | GET | Laatste release (voor de update-melding) |
|
||||
|
||||
---
|
||||
|
||||
@@ -150,6 +214,9 @@ Beveiligde endpoints vereisen `Authorization: Bearer <token>`. Rate limiting: 20
|
||||
|
||||
> PostgreSQL 18 gebruikt `PGDATA=/var/lib/postgresql/18/docker` — het volume is daarom gemount op `/var/lib/postgresql` (niet `/data`).
|
||||
|
||||
Het schema wordt bij het starten van de app aangemaakt/bijgewerkt via
|
||||
`initDb()` in `backend/src/db.js`.
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Omgevingsvariabelen
|
||||
@@ -161,8 +228,12 @@ Beveiligde endpoints vereisen `Authorization: Bearer <token>`. Rate limiting: 20
|
||||
| `POSTGRES_USER` | `vibefinance` | Database gebruikersnaam |
|
||||
| `POSTGRES_DB` | `vibefinance` | Database naam |
|
||||
| `JWT_EXPIRES` | `7d` | Geldigheidsduur token |
|
||||
| `CORS_ORIGIN` | — | Externe domeinnaam |
|
||||
| `FRONTEND_PORT` | `3300` | Externe poort frontend |
|
||||
| `CORS_ORIGIN` | `http://localhost` | Externe domeinnaam (één origin, dus zelden nodig) |
|
||||
| `APP_PORT` | `3400` | Poort op de host (container luistert op 3001) |
|
||||
| `PORT` | `3001` | Poort binnen de container |
|
||||
| `STATIC_DIR` | `/app/public` | Map met de frontend-build |
|
||||
| `UPDATE_CHECK_URL` | Gitea releases-API | Bron update-melding (leeg = uit) |
|
||||
| `UPDATE_CHECK_TOKEN` | — | Alleen voor een niet-publieke repo |
|
||||
|
||||
---
|
||||
|
||||
@@ -170,11 +241,23 @@ Beveiligde endpoints vereisen `Authorization: Bearer <token>`. Rate limiting: 20
|
||||
|
||||
- Wachtwoorden gehasht met bcrypt (cost factor 12)
|
||||
- JWT authenticatie met instelbare vervaltijd
|
||||
- Backend uitsluitend bereikbaar via nginx proxy (`/api/` in `nginx-spa.conf`)
|
||||
- Frontend en API op één origin — geen interne proxy, geen CORS-uitzonderingen
|
||||
- Content-Security-Policy via Helmet, toegespitst op wat de app laadt
|
||||
- Database bereikbaar alleen binnen het interne Docker netwerk
|
||||
- Containers draaien als non-root
|
||||
- Helmet security headers
|
||||
- Rate limiting op alle endpoints
|
||||
- App-container draait als non-root
|
||||
- Rate limiting op `/api` (statische bestanden uitgezonderd)
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Testen
|
||||
|
||||
| Laag | Wat | Commando |
|
||||
|---|---|---|
|
||||
| Backend | API-routes, SPA-fallback, cache- en security-headers | `.\test.ps1 -Unit` |
|
||||
| Build | Vite-build + productie-image | `.\test.ps1 -NoSmoke` |
|
||||
| Integratie | Smoketest op de echte app + postgres stack | `.\test.ps1` |
|
||||
|
||||
In CI draaien dezelfde stappen via `azure-pipelines.yml`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
# ╔══════════════════════════════════════════════════════════════════════════╗
|
||||
# ║ VibeFinance CI/CD — 2-container opzet ║
|
||||
# ║ ║
|
||||
# ║ Eén app-image (Express API + gebouwde Vite-frontend) + postgres. ║
|
||||
# ║ ║
|
||||
# ║ Stages: ║
|
||||
# ║ Test backend-tests (Dockerfile target 'test') ║
|
||||
# ║ Build productie-image bouwen + smoketest op de echte stack ║
|
||||
# ║ Publish multi-arch image (amd64 + arm64) naar de registry pushen ║
|
||||
# ║ ║
|
||||
# ║ Vereist in Azure DevOps: ║
|
||||
# ║ - Service connection (type: Docker Registry) naar de registry; vul ║
|
||||
# ║ de naam in bij variabele dockerRegistryServiceConnection ║
|
||||
# ║ - Agent met Docker en buildx (ubuntu-latest heeft dit) ║
|
||||
# ╚══════════════════════════════════════════════════════════════════════════╝
|
||||
|
||||
trigger:
|
||||
branches:
|
||||
include:
|
||||
- main
|
||||
- master
|
||||
- feature/*
|
||||
tags:
|
||||
include:
|
||||
- "*"
|
||||
|
||||
pr:
|
||||
branches:
|
||||
include:
|
||||
- main
|
||||
- master
|
||||
|
||||
variables:
|
||||
# ── Aanpassen per omgeving ──────────────────────────────────────────────
|
||||
dockerRegistryServiceConnection: "vibefinance-registry"
|
||||
registryHost: "vibehoogie.duckdns.org/vibe"
|
||||
imageName: "vibefinance-app"
|
||||
# ── Afgeleid ────────────────────────────────────────────────────────────
|
||||
imageRepo: "$(registryHost)/$(imageName)"
|
||||
|
||||
pool:
|
||||
vmImage: ubuntu-latest
|
||||
|
||||
stages:
|
||||
|
||||
# ── Stage 1: tests ──────────────────────────────────────────────────────
|
||||
- stage: Test
|
||||
displayName: Tests
|
||||
jobs:
|
||||
- job: BackendTests
|
||||
displayName: Backend-tests (node --test)
|
||||
steps:
|
||||
- checkout: self
|
||||
fetchDepth: 1
|
||||
|
||||
- script: |
|
||||
set -euo pipefail
|
||||
docker build --target test -t vibefinance/app:test .
|
||||
docker run --rm vibefinance/app:test
|
||||
displayName: npm test (in Docker, zonder database)
|
||||
|
||||
# ── Stage 2: image bouwen en smoketesten ────────────────────────────────
|
||||
- stage: Build
|
||||
displayName: Build en smoketest
|
||||
dependsOn: Test
|
||||
jobs:
|
||||
- job: BuildImage
|
||||
displayName: Productie-image + smoketest
|
||||
steps:
|
||||
- checkout: self
|
||||
fetchDepth: 1
|
||||
|
||||
- script: |
|
||||
set -euo pipefail
|
||||
APP_VERSION=$(node -p "require('./package.json').version")
|
||||
echo "##vso[task.setvariable variable=appVersion]$APP_VERSION"
|
||||
echo "Versie: $APP_VERSION"
|
||||
displayName: Versienummer uit package.json
|
||||
|
||||
- script: |
|
||||
set -euo pipefail
|
||||
docker build --target production \
|
||||
--build-arg APP_VERSION=$(appVersion) \
|
||||
--build-arg VITE_APP_VERSION=$(appVersion) \
|
||||
--build-arg VITE_API_URL=/api \
|
||||
-t vibefinance/app:ci .
|
||||
displayName: Productie-image bouwen (incl. Vite-build)
|
||||
|
||||
- script: |
|
||||
set -euo pipefail
|
||||
docker network create vibefinance-ci
|
||||
|
||||
docker run -d --name ci-postgres --network vibefinance-ci \
|
||||
-e POSTGRES_DB=vibefinance \
|
||||
-e POSTGRES_USER=vibefinance \
|
||||
-e POSTGRES_PASSWORD=cipassword \
|
||||
postgres:18-alpine
|
||||
|
||||
for i in $(seq 1 30); do
|
||||
if docker exec ci-postgres pg_isready -U vibefinance >/dev/null 2>&1; then break; fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
docker run -d --name ci-app --network vibefinance-ci \
|
||||
-e NODE_ENV=production \
|
||||
-e JWT_SECRET=ci-secret \
|
||||
-e UPDATE_CHECK_URL= \
|
||||
-e DATABASE_URL=postgresql://vibefinance:cipassword@ci-postgres:5432/vibefinance \
|
||||
-p 3001:3001 \
|
||||
vibefinance/app:ci
|
||||
|
||||
gezond=0
|
||||
for i in $(seq 1 30); do
|
||||
if curl -fsS http://localhost:3001/api/health | grep -q ok; then gezond=1; break; fi
|
||||
sleep 2
|
||||
done
|
||||
if [ "$gezond" -ne 1 ]; then
|
||||
echo "App werd niet gezond. Logs:"
|
||||
docker logs ci-app
|
||||
exit 1
|
||||
fi
|
||||
echo "/api/health : ok"
|
||||
|
||||
curl -fsS http://localhost:3001/ | grep -q root
|
||||
echo "/ : index.html"
|
||||
|
||||
curl -fsS http://localhost:3001/voortgang | grep -q root
|
||||
echo "/voortgang : index.html (SPA-fallback)"
|
||||
|
||||
code=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:3001/api/bestaat-niet)
|
||||
test "$code" = "404"
|
||||
echo "/api/... : 404 JSON"
|
||||
displayName: Smoketest op app + postgres
|
||||
|
||||
- script: |
|
||||
docker logs ci-app || true
|
||||
docker rm -f ci-app ci-postgres || true
|
||||
docker network rm vibefinance-ci || true
|
||||
displayName: Opruimen
|
||||
condition: always()
|
||||
|
||||
# ── Stage 3: publiceren ─────────────────────────────────────────────────
|
||||
# Alleen vanaf main/master of een tag; feature-branches en PRs stoppen na Build.
|
||||
- stage: Publish
|
||||
displayName: Publiceren naar registry
|
||||
dependsOn: Build
|
||||
condition: |
|
||||
and(succeeded(),
|
||||
or(eq(variables['Build.SourceBranch'], 'refs/heads/main'),
|
||||
eq(variables['Build.SourceBranch'], 'refs/heads/master'),
|
||||
startsWith(variables['Build.SourceBranch'], 'refs/tags/')))
|
||||
jobs:
|
||||
- job: PushImage
|
||||
displayName: Multi-arch image pushen
|
||||
steps:
|
||||
- checkout: self
|
||||
fetchDepth: 1
|
||||
|
||||
- script: |
|
||||
set -euo pipefail
|
||||
APP_VERSION=$(node -p "require('./package.json').version")
|
||||
echo "##vso[task.setvariable variable=appVersion]$APP_VERSION"
|
||||
displayName: Versienummer bepalen
|
||||
|
||||
- task: Docker@2
|
||||
displayName: Inloggen op de registry
|
||||
inputs:
|
||||
command: login
|
||||
containerRegistry: $(dockerRegistryServiceConnection)
|
||||
|
||||
- script: |
|
||||
set -euo pipefail
|
||||
docker run --privileged --rm tonistiigi/binfmt --install arm64
|
||||
docker buildx create --name vibefinance --use
|
||||
docker buildx inspect --bootstrap
|
||||
displayName: buildx voor arm64 (Raspberry Pi) klaarzetten
|
||||
|
||||
- script: |
|
||||
set -euo pipefail
|
||||
docker buildx build \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--target production \
|
||||
--build-arg APP_VERSION=$(appVersion) \
|
||||
--build-arg VITE_APP_VERSION=$(appVersion) \
|
||||
--build-arg VITE_API_URL=/api \
|
||||
-t $(imageRepo):$(appVersion) \
|
||||
-t $(imageRepo):latest \
|
||||
--push .
|
||||
displayName: Bouwen en pushen (amd64 + arm64)
|
||||
|
||||
- script: |
|
||||
echo "Gepubliceerd:"
|
||||
echo " $(imageRepo):$(appVersion)"
|
||||
echo " $(imageRepo):latest"
|
||||
displayName: Samenvatting
|
||||
@@ -1,41 +0,0 @@
|
||||
# ╔══════════════════════════════════════════════╗
|
||||
# ║ Backend Dockerfile – multi-stage ║
|
||||
# ╚══════════════════════════════════════════════╝
|
||||
|
||||
# ── Stage 1: deps (incl. native build tools) ─────
|
||||
FROM node:22-alpine AS deps
|
||||
WORKDIR /app
|
||||
COPY package.json package-lock.json* ./
|
||||
RUN npm install && npm cache clean --force
|
||||
|
||||
# ── Stage 2: development (hot reload via --watch) ─
|
||||
FROM deps AS development
|
||||
COPY . .
|
||||
EXPOSE 3001
|
||||
CMD ["node", "--watch", "src/server.js"]
|
||||
|
||||
# ── Stage 3: productie-deps (apart gebouwd) ───────
|
||||
FROM deps AS prod-deps
|
||||
RUN npm prune --omit=dev
|
||||
|
||||
# ── Stage 4: production (minimale image) ──────────
|
||||
FROM node:22-alpine AS production
|
||||
LABEL maintainer="VibeFinance"
|
||||
|
||||
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=prod-deps /app/node_modules ./node_modules
|
||||
COPY package.json ./
|
||||
COPY src/ ./src/
|
||||
|
||||
RUN chown -R appuser:appgroup /app
|
||||
USER appuser
|
||||
|
||||
EXPOSE 3001
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
||||
CMD wget -qO- http://localhost:3001/api/health || exit 1
|
||||
|
||||
CMD ["node", "src/server.js"]
|
||||
@@ -5,6 +5,7 @@
|
||||
"scripts": {
|
||||
"start": "node src/server.js",
|
||||
"dev": "node --watch src/server.js",
|
||||
"test": "node --test",
|
||||
"lint": "eslint src --ext .js"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import "dotenv/config";
|
||||
import { dirname, join, resolve } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
export const config = {
|
||||
NODE_ENV: process.env.NODE_ENV || "development",
|
||||
@@ -9,6 +13,12 @@ export const config = {
|
||||
DATABASE_URL: process.env.DATABASE_URL,
|
||||
REDIS_URL: process.env.REDIS_URL,
|
||||
|
||||
// Map met de gebouwde frontend (Vite dist). In het productie-image staat die
|
||||
// op /app/public; bestaat de map niet, dan serveert de app alleen de API.
|
||||
STATIC_DIR: process.env.STATIC_DIR
|
||||
? resolve(process.env.STATIC_DIR)
|
||||
: join(__dirname, "..", "..", "public"),
|
||||
|
||||
// Update-notificaties: welke release-API wordt gepolld (Gitea).
|
||||
// Leeg laten schakelt de melding in de app uit.
|
||||
UPDATE_CHECK_URL: process.env.UPDATE_CHECK_URL
|
||||
|
||||
+13
-6
@@ -7,7 +7,13 @@ export const db = new Pool({
|
||||
connectionString: config.DATABASE_URL,
|
||||
});
|
||||
|
||||
await db.query(`
|
||||
/**
|
||||
* Zet het schema klaar. Wordt bij het starten van de server aangeroepen —
|
||||
* niet bij het importeren, zodat de app ook zonder database te importeren is
|
||||
* (tests, health checks tijdens de build).
|
||||
*/
|
||||
export async function initDb() {
|
||||
await db.query(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id TEXT PRIMARY KEY,
|
||||
naam TEXT NOT NULL,
|
||||
@@ -22,12 +28,13 @@ await db.query(`
|
||||
user_id TEXT PRIMARY KEY,
|
||||
data TEXT NOT NULL
|
||||
);
|
||||
`);
|
||||
`);
|
||||
|
||||
await db.query(`
|
||||
await db.query(`
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS aangemaakt TIMESTAMPTZ NOT NULL DEFAULT NOW();
|
||||
`);
|
||||
`);
|
||||
|
||||
await db.query(`
|
||||
await db.query(`
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS avatar TEXT;
|
||||
`);
|
||||
`);
|
||||
}
|
||||
|
||||
+57
-12
@@ -3,9 +3,12 @@ import cors from "cors";
|
||||
import helmet from "helmet";
|
||||
import rateLimit from "express-rate-limit";
|
||||
import cookieParser from "cookie-parser";
|
||||
import pino from "pino";
|
||||
import pinoHttp from "pino-http";
|
||||
import { existsSync } from "fs";
|
||||
import { join } from "path";
|
||||
import { config } from "./config/index.js";
|
||||
import { logger } from "./logger.js";
|
||||
import { initDb } from "./db.js";
|
||||
import authRouter from "./routes/auth.js";
|
||||
import dataRouter from "./routes/data.js";
|
||||
import usersRouter from "./routes/users.js";
|
||||
@@ -13,23 +16,36 @@ import healthRouter from "./routes/health.js";
|
||||
import updateRouter from "./routes/update.js";
|
||||
import { errorHandler } from "./middleware/errorHandler.js";
|
||||
|
||||
export const logger = pino({
|
||||
level: config.NODE_ENV === "production" ? "info" : "debug",
|
||||
...(config.NODE_ENV !== "production" && {
|
||||
transport: { target: "pino-pretty", options: { colorize: true } },
|
||||
}),
|
||||
});
|
||||
export { logger };
|
||||
|
||||
const app = express();
|
||||
|
||||
// ── Security ──────────────────────────────────────────────────────────────────
|
||||
app.use(helmet());
|
||||
// De app serveert nu ook de frontend, dus het CSP-beleid moet passen bij wat
|
||||
// die pagina laadt: Google Fonts, inline <style> van React en base64-avatars.
|
||||
app.use(helmet({
|
||||
contentSecurityPolicy: {
|
||||
directives: {
|
||||
defaultSrc: ["'self'"],
|
||||
scriptSrc: ["'self'"],
|
||||
styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
|
||||
fontSrc: ["'self'", "https://fonts.gstatic.com", "data:"],
|
||||
imgSrc: ["'self'", "data:", "blob:"],
|
||||
connectSrc: ["'self'"],
|
||||
objectSrc: ["'none'"],
|
||||
baseUri: ["'self'"],
|
||||
formAction: ["'self'"],
|
||||
// Uit: intern draait de app op http achter een reverse proxy.
|
||||
upgradeInsecureRequests: null,
|
||||
},
|
||||
},
|
||||
}));
|
||||
app.use(cors({ origin: config.CORS_ORIGIN, credentials: true }));
|
||||
app.set("trust proxy", 1);
|
||||
|
||||
// ── Rate limiting ─────────────────────────────────────────────────────────────
|
||||
// ── Rate limiting (alleen op de API — niet op statische bestanden) ────────────
|
||||
app.use("/api/auth", rateLimit({ windowMs: 15 * 60 * 1000, max: 20, standardHeaders: true, legacyHeaders: false }));
|
||||
app.use(rateLimit({ windowMs: 15 * 60 * 1000, max: 200, standardHeaders: true, legacyHeaders: false }));
|
||||
app.use("/api", rateLimit({ windowMs: 15 * 60 * 1000, max: 200, standardHeaders: true, legacyHeaders: false }));
|
||||
|
||||
// ── Parsing ───────────────────────────────────────────────────────────────────
|
||||
app.use(express.json({ limit: "2mb" }));
|
||||
@@ -40,7 +56,7 @@ if (config.NODE_ENV !== "test") {
|
||||
app.use(pinoHttp({ logger }));
|
||||
}
|
||||
|
||||
// ── Routes ────────────────────────────────────────────────────────────────────
|
||||
// ── API-routes ────────────────────────────────────────────────────────────────
|
||||
app.use("/api/health", healthRouter);
|
||||
app.use("/api/update-check", updateRouter);
|
||||
app.use("/api/auth", authRouter);
|
||||
@@ -50,13 +66,42 @@ app.use("/api/users", usersRouter);
|
||||
// ── 404 voor overige /api/* ───────────────────────────────────────────────────
|
||||
app.use("/api/*", (_req, res) => res.status(404).json({ error: "Endpoint niet gevonden." }));
|
||||
|
||||
// ── Frontend (Vite build) ─────────────────────────────────────────────────────
|
||||
const indexHtml = join(config.STATIC_DIR, "index.html");
|
||||
export const servesFrontend = existsSync(indexHtml);
|
||||
|
||||
if (servesFrontend) {
|
||||
// Assets hebben een hash in de naam en mogen lang gecached worden;
|
||||
// index.html juist nooit, anders blijft een oude build hangen.
|
||||
app.use(express.static(config.STATIC_DIR, {
|
||||
index: false,
|
||||
setHeaders(res, path) {
|
||||
res.setHeader("Cache-Control", path.endsWith(".html")
|
||||
? "no-store, no-cache, must-revalidate"
|
||||
: "public, max-age=31536000, immutable");
|
||||
},
|
||||
}));
|
||||
|
||||
// SPA-fallback: alle overige GET-routes naar index.html.
|
||||
app.get("*", (_req, res) => {
|
||||
res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
|
||||
res.sendFile(indexHtml);
|
||||
});
|
||||
} else {
|
||||
logger.warn({ dir: config.STATIC_DIR }, "Geen frontend-build gevonden — alleen de API is beschikbaar");
|
||||
}
|
||||
|
||||
// ── Error handler ─────────────────────────────────────────────────────────────
|
||||
app.use(errorHandler);
|
||||
|
||||
// ── Start ─────────────────────────────────────────────────────────────────────
|
||||
if (config.NODE_ENV !== "test") {
|
||||
await initDb();
|
||||
app.listen(config.PORT, () => {
|
||||
logger.info(`VibeFinance API gestart op http://localhost:${config.PORT} [${config.NODE_ENV}]`);
|
||||
logger.info(
|
||||
`VibeFinance gestart op http://localhost:${config.PORT} [${config.NODE_ENV}]` +
|
||||
(servesFrontend ? " — API + frontend" : " — alleen API")
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* Integratietests voor de gecombineerde app-container: de Express-app serveert
|
||||
* zowel de API op /api als de gebouwde frontend op /.
|
||||
*
|
||||
* Draait zonder database: NODE_ENV=test slaat het schema-initialisatie en
|
||||
* app.listen() in server.js over, en geen van de geteste routes raakt de db.
|
||||
*/
|
||||
import { after, before, describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtempSync, mkdirSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
const INDEX_MARKER = "<div id=\"root\"></div>";
|
||||
const ASSET_BODY = "console.log('vibefinance');";
|
||||
|
||||
let server;
|
||||
let basis;
|
||||
|
||||
before(async () => {
|
||||
// Nagebootste Vite-build: index.html + een asset met hash in de naam.
|
||||
const staticDir = mkdtempSync(join(tmpdir(), "vibefinance-static-"));
|
||||
mkdirSync(join(staticDir, "assets"));
|
||||
writeFileSync(join(staticDir, "index.html"), `<!DOCTYPE html><html><body>${INDEX_MARKER}</body></html>`);
|
||||
writeFileSync(join(staticDir, "assets", "index-abc12345.js"), ASSET_BODY);
|
||||
|
||||
process.env.NODE_ENV = "test";
|
||||
process.env.STATIC_DIR = staticDir;
|
||||
process.env.UPDATE_CHECK_URL = ""; // geen uitgaande call in tests
|
||||
process.env.JWT_SECRET = "test-secret";
|
||||
|
||||
const { default: app } = await import("../src/server.js");
|
||||
server = app.listen(0);
|
||||
await new Promise((res) => server.once("listening", res));
|
||||
basis = `http://127.0.0.1:${server.address().port}`;
|
||||
});
|
||||
|
||||
after(() => server?.close());
|
||||
|
||||
describe("API", () => {
|
||||
it("health geeft status ok", async () => {
|
||||
const res = await fetch(`${basis}/api/health`);
|
||||
assert.equal(res.status, 200);
|
||||
const body = await res.json();
|
||||
assert.equal(body.status, "ok");
|
||||
assert.equal(typeof body.uptime, "number");
|
||||
});
|
||||
|
||||
it("onbekende API-route geeft 404 als JSON, niet de SPA", async () => {
|
||||
const res = await fetch(`${basis}/api/bestaat-niet`);
|
||||
assert.equal(res.status, 404);
|
||||
assert.match(res.headers.get("content-type") ?? "", /application\/json/);
|
||||
assert.equal((await res.json()).error, "Endpoint niet gevonden.");
|
||||
});
|
||||
|
||||
it("beschermde route zonder token geeft 401", async () => {
|
||||
const res = await fetch(`${basis}/api/data`);
|
||||
assert.equal(res.status, 401);
|
||||
assert.equal((await res.json()).error, "Niet ingelogd.");
|
||||
});
|
||||
|
||||
it("update-check zonder ingestelde URL meldt niets", async () => {
|
||||
const res = await fetch(`${basis}/api/update-check`);
|
||||
const body = await res.json();
|
||||
assert.equal(res.status, 200);
|
||||
assert.equal(body.latest, null);
|
||||
assert.equal(body.disabled, true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Frontend serveren", () => {
|
||||
it("root geeft index.html", async () => {
|
||||
const res = await fetch(`${basis}/`);
|
||||
assert.equal(res.status, 200);
|
||||
assert.match(res.headers.get("content-type") ?? "", /text\/html/);
|
||||
assert.match(await res.text(), new RegExp(INDEX_MARKER));
|
||||
});
|
||||
|
||||
it("onbekend pad valt terug op index.html (React-routing)", async () => {
|
||||
const res = await fetch(`${basis}/voortgang`);
|
||||
assert.equal(res.status, 200);
|
||||
assert.match(await res.text(), new RegExp(INDEX_MARKER));
|
||||
});
|
||||
|
||||
it("index.html wordt niet gecached", async () => {
|
||||
const res = await fetch(`${basis}/`);
|
||||
assert.match(res.headers.get("cache-control") ?? "", /no-store/);
|
||||
});
|
||||
|
||||
it("assets met hash worden lang gecached", async () => {
|
||||
const res = await fetch(`${basis}/assets/index-abc12345.js`);
|
||||
assert.equal(res.status, 200);
|
||||
assert.equal(await res.text(), ASSET_BODY);
|
||||
assert.match(res.headers.get("cache-control") ?? "", /immutable/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Security headers", () => {
|
||||
it("CSP staat Google Fonts en inline styles toe", async () => {
|
||||
const res = await fetch(`${basis}/`);
|
||||
const csp = res.headers.get("content-security-policy") ?? "";
|
||||
assert.match(csp, /style-src [^;]*'unsafe-inline'/);
|
||||
assert.match(csp, /fonts\.googleapis\.com/);
|
||||
assert.match(csp, /font-src [^;]*fonts\.gstatic\.com/);
|
||||
assert.doesNotMatch(csp, /upgrade-insecure-requests/);
|
||||
});
|
||||
|
||||
it("statische bestanden krijgen geen rate-limit headers", async () => {
|
||||
const res = await fetch(`${basis}/assets/index-abc12345.js`);
|
||||
assert.equal(res.headers.get("ratelimit-limit"), null);
|
||||
});
|
||||
|
||||
it("de API krijgt wel rate-limit headers", async () => {
|
||||
const res = await fetch(`${basis}/api/health`);
|
||||
assert.notEqual(res.headers.get("ratelimit-limit"), null);
|
||||
});
|
||||
});
|
||||
@@ -1,19 +1,20 @@
|
||||
# VibeFinance dev script (PowerShell)
|
||||
# Lokale development cyclus: bouwen, starten, stoppen en optioneel pushen
|
||||
# naar de dev-registry. Gebruikt altijd de -dev images en tags.
|
||||
# Lokale development cyclus voor de 2-container opzet: bouwen, starten,
|
||||
# stoppen en optioneel pushen naar de dev-registry.
|
||||
#
|
||||
# app API (node --watch) + Vite dev-server met hot reload
|
||||
# postgres database
|
||||
#
|
||||
# Gebruik:
|
||||
# .\dev.ps1 # build + herstart containers
|
||||
# .\dev.ps1 build # alleen lokaal bouwen
|
||||
# .\dev.ps1 build -Target backend # alleen backend bouwen
|
||||
# .\dev.ps1 build -Target frontend # alleen frontend bouwen
|
||||
# .\dev.ps1 up # build + herstart containers
|
||||
# .\dev.ps1 up -Target backend # alleen backend bouwen + herstart
|
||||
# .\dev.ps1 up -Target frontend # alleen frontend bouwen + herstart
|
||||
# .\dev.ps1 up -Target app # alleen de app herbouwen + herstarten
|
||||
# .\dev.ps1 up -Target postgres # alleen de database herstarten
|
||||
# .\dev.ps1 down # containers stoppen en volumes verwijderen
|
||||
# .\dev.ps1 logs # logs van de app volgen
|
||||
# .\dev.ps1 test # testsuite draaien (zie ook test.ps1)
|
||||
# .\dev.ps1 push # multi-platform build + push naar registry
|
||||
# .\dev.ps1 push -Target backend # alleen backend multi-platform pushen
|
||||
# .\dev.ps1 push -Target frontend # alleen frontend multi-platform pushen
|
||||
#
|
||||
# Vereisten:
|
||||
# - Docker Desktop actief
|
||||
@@ -22,11 +23,11 @@
|
||||
# - root package.json bevat het versienummer (bijv. "1.0.0-dev")
|
||||
|
||||
param(
|
||||
[ValidateSet("build","up","down","push")]
|
||||
[ValidateSet("build","up","down","logs","test","push")]
|
||||
[string]$Action = "up",
|
||||
|
||||
[ValidateSet("both","frontend","backend")]
|
||||
[string]$Target = "both"
|
||||
[ValidateSet("all","app","postgres")]
|
||||
[string]$Target = "all"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
@@ -36,8 +37,7 @@ $ErrorActionPreference = "Stop"
|
||||
# =============================================================================
|
||||
$appName = "VibeFinance"
|
||||
$registry = "vibehoogie.duckdns.org/vibe"
|
||||
$backendImage = "vibefinance-backend"
|
||||
$frontendImage = "vibefinance-frontend"
|
||||
$appImage = "vibefinance-app"
|
||||
$apiUrl = "/api"
|
||||
# =============================================================================
|
||||
|
||||
@@ -67,18 +67,17 @@ if ($frontendPkg.version -ne $appVersion) {
|
||||
Write-Host "Versie gesynchroniseerd naar frontend: $appVersion" -ForegroundColor Green
|
||||
}
|
||||
|
||||
# Registry tags
|
||||
$backendBase = "$registry/$backendImage"
|
||||
$frontendBase = "$registry/$frontendImage"
|
||||
|
||||
$backendLatestDev = "$backendBase`:latest-dev"
|
||||
$frontendLatestDev = "$frontendBase`:latest-dev"
|
||||
$backendVersionDev = "$backendBase`:$appVersion"
|
||||
$frontendVersionDev= "$frontendBase`:$appVersion"
|
||||
# Registry tags (één image voor de hele app)
|
||||
$appBase = "$registry/$appImage"
|
||||
$appLatestDev = "$appBase`:latest-dev"
|
||||
$appVersionDev = "$appBase`:$appVersion"
|
||||
|
||||
# Pad naar compose file
|
||||
$devCompose = Join-Path $PSScriptRoot "docker-compose.dev.yml"
|
||||
|
||||
# Welke services de actie raakt
|
||||
$services = @(if ($Target -ne "all") { $Target })
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "========================================" -ForegroundColor Cyan
|
||||
Write-Host " $appName Dev" -ForegroundColor Cyan
|
||||
@@ -90,38 +89,20 @@ Write-Host "========================================" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
|
||||
# Functies
|
||||
function Invoke-BackendPush {
|
||||
Write-Host "Backend bouwen en pushen..." -ForegroundColor White
|
||||
function Invoke-AppPush {
|
||||
Write-Host "App bouwen en pushen (amd64 + arm64)..." -ForegroundColor White
|
||||
docker buildx build `
|
||||
--platform linux/amd64,linux/arm64 `
|
||||
--target production `
|
||||
--push `
|
||||
-t $backendLatestDev `
|
||||
-t $backendVersionDev `
|
||||
--build-arg APP_VERSION=$appVersion `
|
||||
(Join-Path $PSScriptRoot "backend")
|
||||
if ($LASTEXITCODE -ne 0) { throw "Backend build mislukt" }
|
||||
Write-Host "Gepusht: $backendVersionDev" -ForegroundColor Green
|
||||
}
|
||||
|
||||
function Invoke-FrontendPush {
|
||||
Write-Host "Frontend bouwen en pushen..." -ForegroundColor White
|
||||
docker buildx build `
|
||||
--platform linux/amd64,linux/arm64 `
|
||||
--push `
|
||||
-t $frontendLatestDev `
|
||||
-t $frontendVersionDev `
|
||||
-t $appLatestDev `
|
||||
-t $appVersionDev `
|
||||
--build-arg APP_VERSION=$appVersion `
|
||||
--build-arg VITE_API_URL=$apiUrl `
|
||||
--build-arg VITE_REPO_URL=$repoUrl `
|
||||
(Join-Path $PSScriptRoot "frontend")
|
||||
if ($LASTEXITCODE -ne 0) { throw "Frontend build mislukt" }
|
||||
Write-Host "Gepusht: $frontendVersionDev" -ForegroundColor Green
|
||||
}
|
||||
|
||||
function Invoke-Push {
|
||||
Write-Host "-- Multi-platform bouwen en pushen -------" -ForegroundColor Cyan
|
||||
if ($Target -eq "both" -or $Target -eq "backend") { Invoke-BackendPush }
|
||||
if ($Target -eq "both" -or $Target -eq "frontend") { Invoke-FrontendPush }
|
||||
--build-arg VITE_APP_VERSION=$appVersion `
|
||||
$PSScriptRoot
|
||||
if ($LASTEXITCODE -ne 0) { throw "Build mislukt" }
|
||||
Write-Host "Gepusht: $appVersionDev" -ForegroundColor Green
|
||||
}
|
||||
|
||||
# Acties
|
||||
@@ -129,7 +110,6 @@ switch ($Action) {
|
||||
|
||||
"build" {
|
||||
Write-Host "-- Lokaal bouwen -------------------------" -ForegroundColor Cyan
|
||||
$services = @(if ($Target -ne "both") { $Target })
|
||||
docker compose --progress plain -f $devCompose build @services
|
||||
if ($LASTEXITCODE -ne 0) { throw "Build mislukt" }
|
||||
Write-Host ""
|
||||
@@ -137,8 +117,9 @@ switch ($Action) {
|
||||
}
|
||||
|
||||
"up" {
|
||||
if ($Target -eq "both") {
|
||||
docker compose -f $devCompose down -v
|
||||
if ($Target -eq "all") {
|
||||
# Let op: geen -v hier, dat zou de dev-database wissen (zie actie down)
|
||||
docker compose -f $devCompose down
|
||||
docker compose --progress plain -f $devCompose up --build -d
|
||||
} else {
|
||||
docker compose -f $devCompose stop $Target
|
||||
@@ -147,6 +128,8 @@ switch ($Action) {
|
||||
if ($LASTEXITCODE -ne 0) { throw "Opstarten mislukt" }
|
||||
Write-Host ""
|
||||
Write-Host "$appName gestart - $appVersion [$Target]" -ForegroundColor Green
|
||||
Write-Host " App : http://localhost/" -ForegroundColor White
|
||||
Write-Host " API : http://localhost:3001/api/health" -ForegroundColor White
|
||||
}
|
||||
|
||||
"down" {
|
||||
@@ -155,19 +138,23 @@ switch ($Action) {
|
||||
Write-Host "$appName gestopt" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
"logs" {
|
||||
$svc = if ($Target -eq "all") { "app" } else { $Target }
|
||||
docker compose -f $devCompose logs -f $svc
|
||||
}
|
||||
|
||||
"test" {
|
||||
& (Join-Path $PSScriptRoot "test.ps1")
|
||||
if ($LASTEXITCODE -ne 0) { throw "Tests mislukt" }
|
||||
}
|
||||
|
||||
"push" {
|
||||
Invoke-Push
|
||||
Invoke-AppPush
|
||||
Write-Host ""
|
||||
Write-Host "========================================" -ForegroundColor Cyan
|
||||
Write-Host " Dev images gepusht: $appVersion [$Target]" -ForegroundColor Green
|
||||
if ($Target -eq "both" -or $Target -eq "backend") {
|
||||
Write-Host " $backendVersionDev" -ForegroundColor White
|
||||
Write-Host " $backendLatestDev" -ForegroundColor White
|
||||
}
|
||||
if ($Target -eq "both" -or $Target -eq "frontend") {
|
||||
Write-Host " $frontendVersionDev" -ForegroundColor White
|
||||
Write-Host " $frontendLatestDev" -ForegroundColor White
|
||||
}
|
||||
Write-Host " Dev image gepusht: $appVersion" -ForegroundColor Green
|
||||
Write-Host " $appVersionDev" -ForegroundColor White
|
||||
Write-Host " $appLatestDev" -ForegroundColor White
|
||||
Write-Host "========================================" -ForegroundColor Cyan
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,24 @@
|
||||
#!/usr/bin/env bash
|
||||
# ── VibeFinance dev wrapper (macOS/Linux) ────────────────────────────────
|
||||
# Roept dev.ps1 aan via PowerShell Core (pwsh).
|
||||
# Roept dev.ps1 aan via PowerShell Core (pwsh). 2-container opzet:
|
||||
# app (API + Vite dev-server) en postgres.
|
||||
#
|
||||
# Gebruik:
|
||||
# ./dev.sh # build + herstart containers
|
||||
# ./dev.sh build # alleen lokaal bouwen
|
||||
# ./dev.sh build -Target backend # alleen backend bouwen
|
||||
# ./dev.sh build -Target frontend # alleen frontend bouwen
|
||||
# ./dev.sh up # build lokaal + herstart containers
|
||||
# ./dev.sh up -Target backend # alleen backend bouwen + herstart
|
||||
# ./dev.sh up -Target frontend # alleen frontend bouwen + herstart
|
||||
# ./dev.sh down # containers stoppen
|
||||
# ./dev.sh up # build + herstart containers
|
||||
# ./dev.sh up -Target app # alleen de app herbouwen + herstarten
|
||||
# ./dev.sh up -Target postgres # alleen de database herstarten
|
||||
# ./dev.sh down # containers stoppen en volumes wissen
|
||||
# ./dev.sh logs # logs van de app volgen
|
||||
# ./dev.sh test # testsuite draaien
|
||||
# ./dev.sh push # multi-platform build + push
|
||||
# ./dev.sh push -Target backend # alleen backend pushen
|
||||
# ./dev.sh push -Target frontend # alleen frontend pushen
|
||||
#
|
||||
# Vereisten:
|
||||
# - pwsh (PowerShell Core) geïnstalleerd: brew install powershell
|
||||
# - Docker Desktop actief
|
||||
# - Docker actief
|
||||
# - Voor 'push': docker buildx builder actief: docker buildx create --use
|
||||
# - Voor 'push': ingelogd op registry via: docker login 10.0.3.108:3000
|
||||
# - Voor 'push': ingelogd op de registry: docker login <registry>
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
|
||||
+20
-35
@@ -1,8 +1,14 @@
|
||||
##
|
||||
## Standalone dev stack — lokaal bouwen en testen
|
||||
## VibeFinance dev-stack — 2 containers, zelfde structuur als productie
|
||||
##
|
||||
## app API (node --watch) + Vite dev-server met hot reload
|
||||
## postgres database
|
||||
##
|
||||
## docker compose -f docker-compose.dev.yml up --build
|
||||
##
|
||||
## De browser praat met Vite op poort 80; Vite proxyt /api naar de API in
|
||||
## dezelfde container (localhost:3001). De API is ook direct bereikbaar op 3001.
|
||||
##
|
||||
|
||||
services:
|
||||
|
||||
@@ -18,72 +24,51 @@ services:
|
||||
networks:
|
||||
- dev
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U vibefinance"]
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-vibefinance}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
frontend:
|
||||
app:
|
||||
build:
|
||||
context: ./frontend
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
target: development
|
||||
args:
|
||||
VITE_APP_VERSION: ${VITE_APP_VERSION:-dev}
|
||||
image: vibefinance/frontend:${VERSION:-latest-dev}
|
||||
container_name: vibefinance-frontend-dev
|
||||
image: vibefinance/app:${VERSION:-latest-dev}
|
||||
container_name: vibefinance-app-dev
|
||||
ports:
|
||||
- "80:5173"
|
||||
volumes:
|
||||
- ./frontend:/app
|
||||
- /app/node_modules
|
||||
networks:
|
||||
- dev
|
||||
depends_on:
|
||||
backend:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-qO-", "http://localhost:5173/"]
|
||||
interval: 15s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 20s
|
||||
|
||||
backend:
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: Dockerfile
|
||||
target: development
|
||||
image: vibefinance/backend:${VERSION:-latest-dev}
|
||||
container_name: vibefinance-backend-dev
|
||||
expose:
|
||||
- "3001"
|
||||
- "80:5173" # Vite dev-server (de app in de browser)
|
||||
- "3001:3001" # API, handig voor curl/Postman
|
||||
volumes:
|
||||
- ./backend:/app
|
||||
- /app/node_modules
|
||||
- ./frontend:/app/frontend
|
||||
- /app/frontend/node_modules
|
||||
environment:
|
||||
NODE_ENV: development
|
||||
PORT: 3001
|
||||
JWT_SECRET: dev-secret-change-in-production
|
||||
JWT_EXPIRES: 7d
|
||||
CORS_ORIGIN: http://localhost
|
||||
UPDATE_CHECK_URL: ${UPDATE_CHECK_URL:-https://vibehoogie.duckdns.org/api/v1/repos/vibe/VibeFinance/releases/latest}
|
||||
DATABASE_URL: postgresql://${POSTGRES_USER:-vibefinance}:${POSTGRES_PASSWORD:-devpassword}@postgres:5432/${POSTGRES_DB:-vibefinance}
|
||||
UPDATE_CHECK_URL: ${UPDATE_CHECK_URL:-https://vibehoogie.duckdns.org/api/v1/repos/vibe/VibeFinance/releases/latest}
|
||||
networks:
|
||||
- dev
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-qO-", "http://localhost:3001/api/health"]
|
||||
# 127.0.0.1 i.p.v. localhost: Vite luistert alleen op IPv4
|
||||
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:3001/api/health && wget -qO- http://127.0.0.1:5173/ > /dev/null || exit 1"]
|
||||
interval: 15s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 15s
|
||||
start_period: 25s
|
||||
|
||||
volumes:
|
||||
postgres_dev_data:
|
||||
|
||||
networks:
|
||||
dev:
|
||||
driver: bridge
|
||||
|
||||
+17
-21
@@ -1,9 +1,12 @@
|
||||
# VibeFinance — docker-compose voor deployment via pre-built images
|
||||
# VibeFinance — productie/self-hosting: 2 containers
|
||||
#
|
||||
# app Express API + gebouwde frontend (poort 3001 in de container)
|
||||
# postgres database met persistent volume
|
||||
#
|
||||
# Zie README.md § Quick Start voor volledige instructies.
|
||||
#
|
||||
# Stappen:
|
||||
# 1. cp .env.example .env (vul JWT_SECRET, CORS_ORIGIN en DB_PASSWORD in)
|
||||
# 1. cp .env.example .env (vul JWT_SECRET, POSTGRES_PASSWORD in)
|
||||
# 2. docker compose pull
|
||||
# 3. docker compose up -d
|
||||
|
||||
@@ -20,46 +23,39 @@ services:
|
||||
volumes:
|
||||
- vibefinance_pgdata:/var/lib/postgresql
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U vibefinance"]
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-vibefinance}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
networks:
|
||||
- vibefinance_net
|
||||
|
||||
backend:
|
||||
image: vibehoogie.duckdns.org/vibe/vibefinance-backend:latest
|
||||
container_name: vibefinance_backend
|
||||
app:
|
||||
image: vibehoogie.duckdns.org/vibe/vibefinance-app:latest
|
||||
container_name: vibefinance_app
|
||||
restart: unless-stopped
|
||||
# Variabelen uit .env (JWT_SECRET, POSTGRES_PASSWORD, ...)
|
||||
env_file: .env
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
PORT: 3001
|
||||
DATABASE_URL: postgresql://${POSTGRES_USER:-vibefinance}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-vibefinance}
|
||||
# Bron voor de update-melding in de app (leeg = melding uit)
|
||||
UPDATE_CHECK_URL: ${UPDATE_CHECK_URL:-https://vibehoogie.duckdns.org/api/v1/repos/vibe/VibeFinance/releases/latest}
|
||||
ports:
|
||||
- "${APP_PORT:-3400}:3001"
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -qO- http://localhost:3001/api/health | grep -q status || exit 1"]
|
||||
interval: 10s
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
start_period: 30s
|
||||
retries: 5
|
||||
start_period: 20s
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- vibefinance_net
|
||||
|
||||
frontend:
|
||||
image: vibehoogie.duckdns.org/vibe/vibefinance-frontend:latest
|
||||
container_name: vibefinance_frontend
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${FRONTEND_PORT:-3400}:80"
|
||||
depends_on:
|
||||
backend:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- vibefinance_net
|
||||
|
||||
volumes:
|
||||
vibefinance_pgdata:
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
#!/bin/sh
|
||||
# Start de API en de Vite dev-server samen in de dev-container.
|
||||
# De browser praat met Vite (5173); Vite proxyt /api naar de API op 3001.
|
||||
set -e
|
||||
|
||||
cd /app
|
||||
node --watch src/server.js &
|
||||
API_PID=$!
|
||||
|
||||
cd /app/frontend
|
||||
node_modules/.bin/vite --host 0.0.0.0 --port 5173 &
|
||||
WEB_PID=$!
|
||||
|
||||
afsluiten() {
|
||||
kill "$API_PID" "$WEB_PID" 2>/dev/null
|
||||
exit 0
|
||||
}
|
||||
trap afsluiten TERM INT
|
||||
|
||||
# Valt één van de twee processen weg, dan stopt de container (compose herstart).
|
||||
wait -n
|
||||
kill "$API_PID" "$WEB_PID" 2>/dev/null
|
||||
exit 1
|
||||
@@ -1,42 +0,0 @@
|
||||
# ╔══════════════════════════════════════════════╗
|
||||
# ║ Frontend Dockerfile – multi-stage ║
|
||||
# ╚══════════════════════════════════════════════╝
|
||||
|
||||
# ── Stage 1: deps ────────────────────────────────
|
||||
FROM node:22-alpine AS deps
|
||||
WORKDIR /app
|
||||
COPY package.json package-lock.json* ./
|
||||
RUN npm install && npm cache clean --force
|
||||
|
||||
# ── Stage 2: development (hot reload) ────────────
|
||||
FROM deps AS development
|
||||
ARG VITE_APP_VERSION=dev
|
||||
ENV VITE_APP_VERSION=$VITE_APP_VERSION
|
||||
COPY . .
|
||||
EXPOSE 5173
|
||||
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0", "--port", "5173"]
|
||||
|
||||
# ── Stage 3: build ───────────────────────────────
|
||||
FROM deps AS builder
|
||||
ARG VITE_API_URL=/api
|
||||
ARG VITE_APP_VERSION=dev
|
||||
ENV VITE_API_URL=$VITE_API_URL
|
||||
ENV VITE_APP_VERSION=$VITE_APP_VERSION
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
# ── Stage 4: production (nginx static) ───────────
|
||||
FROM nginx:1.27-alpine AS production
|
||||
LABEL maintainer="VibeFinance"
|
||||
|
||||
# Kopieer de Vite build output
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
|
||||
# Nginx config voor SPA (history-mode fallback)
|
||||
COPY nginx-spa.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
EXPOSE 80
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
|
||||
CMD wget -qO- http://localhost/ || exit 1
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
@@ -1,32 +0,0 @@
|
||||
server {
|
||||
listen 80;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# Gzip voor statische assets
|
||||
gzip on;
|
||||
gzip_types text/plain text/css application/json application/javascript text/xml application/xml image/svg+xml;
|
||||
|
||||
# Cache-control: assets met hash in naam → lang; index.html → nooit
|
||||
location ~* \.(js|css|woff2?|ttf|eot|svg|png|jpg|ico|webp)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
# Proxy API-calls naar de backend
|
||||
location /api/ {
|
||||
proxy_pass http://backend:3001;
|
||||
proxy_http_version 1.1;
|
||||
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;
|
||||
}
|
||||
|
||||
# SPA fallback – stuur alle routes naar index.html
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
add_header Cache-Control "no-store, no-cache, must-revalidate";
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "vibefinance-frontend",
|
||||
"version": "0.1.5-dev",
|
||||
"version": "0.1.6-dev",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -11,10 +11,10 @@ export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
port: 5173,
|
||||
// In dev: proxy /api naar de backend zodat je geen CORS-issues hebt
|
||||
// In dev: proxy /api naar de API in dezelfde container (zie docker/dev-entrypoint.sh)
|
||||
proxy: {
|
||||
"/api": {
|
||||
target: "http://backend:3001",
|
||||
target: "http://localhost:3001",
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
},
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "vibefinance",
|
||||
"version": "0.1.5-dev",
|
||||
"version": "0.1.6-dev",
|
||||
"description": "VibeFinance — persoonlijk vermogensbeheer",
|
||||
"private": true,
|
||||
"repository": "https://vibehoogie.duckdns.org/vibe/VibeFinance",
|
||||
|
||||
+66
-72
@@ -1,24 +1,27 @@
|
||||
# ── VibeFinance release script (PowerShell) ──────────────────────────────
|
||||
# Productie release: bouwt multi-platform, pusht naar registry,
|
||||
# maakt een Git-tag aan en pusht naar origin.
|
||||
# Productie release voor de 2-container opzet: draait de tests, bouwt het
|
||||
# gecombineerde app-image multi-platform, pusht naar de registry, maakt een
|
||||
# Git-tag en publiceert de release in Gitea.
|
||||
#
|
||||
# Gebruik:
|
||||
# .\release.ps1 # bouw + push images + Git-tag
|
||||
# .\release.ps1 -NoBuild # sla docker build over, alleen Git-tag
|
||||
# .\release.ps1 # test + bouw + push + Git-tag + Gitea-release
|
||||
# .\release.ps1 -NoBuild # sla docker build over, alleen tag + release
|
||||
# .\release.ps1 -SkipTests # sla de testsuite over (niet aanbevolen)
|
||||
# .\release.ps1 -DryRun # toon alle commando's zonder iets uit te voeren
|
||||
# .\release.ps1 -Force # sla conflictcontrole over, overschrijf bestaande tags
|
||||
# .\release.ps1 -Force # sla conflictcontrole over, overschrijf tags
|
||||
#
|
||||
# Vereisten:
|
||||
# - Docker Desktop actief
|
||||
# - Ingelogd op de registry: docker login <registry>
|
||||
# - docker buildx builder actief: docker buildx create --use
|
||||
# - Git geïnstalleerd en working directory is de repo root
|
||||
# - Git geinstalleerd en working directory is de repo root
|
||||
# - root package.json bevat een schoon versienummer ZONDER -dev suffix
|
||||
# - $env:GITEA_TOKEN gezet (token met release-rechten) — nodig voor stap 4,
|
||||
# - GITEA_TOKEN in de omgeving (token met release-rechten) voor stap 5,
|
||||
# de Gitea-release waar de update-melding in de app op afgaat
|
||||
|
||||
param(
|
||||
[switch]$NoBuild,
|
||||
[switch]$SkipTests,
|
||||
[switch]$DryRun,
|
||||
[switch]$Force
|
||||
)
|
||||
@@ -30,9 +33,9 @@ $ErrorActionPreference = "Stop"
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
$appName = "VibeFinance"
|
||||
$registry = "vibehoogie.duckdns.org/vibe"
|
||||
$backendImage = "vibefinance-backend"
|
||||
$frontendImage = "vibefinance-frontend"
|
||||
$appImage = "vibefinance-app"
|
||||
$apiUrl = "/api"
|
||||
$platforms = "linux/amd64,linux/arm64" # arm64 = Raspberry Pi
|
||||
# ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
# ── Versie uit root package.json ───────────────────────────────────────────
|
||||
@@ -60,14 +63,10 @@ if ($frontendPkg.version -ne $appVersion) {
|
||||
|
||||
$gitTag = "$appVersion"
|
||||
|
||||
# ── Registry tags ──────────────────────────────────────────────────────────
|
||||
$backendBase = "$registry/$backendImage"
|
||||
$frontendBase = "$registry/$frontendImage"
|
||||
|
||||
$backendVersionTag = "$backendBase`:$appVersion"
|
||||
$frontendVersionTag = "$frontendBase`:$appVersion"
|
||||
$backendLatestTag = "$backendBase`:latest"
|
||||
$frontendLatestTag = "$frontendBase`:latest"
|
||||
# ── Registry tags (één image voor frontend + backend) ─────────────────────
|
||||
$appBase = "$registry/$appImage"
|
||||
$appVersionTag = "$appBase`:$appVersion"
|
||||
$appLatestTag = "$appBase`:latest"
|
||||
|
||||
# ── Helpers ────────────────────────────────────────────────────────────────
|
||||
function Invoke-Cmd {
|
||||
@@ -116,14 +115,16 @@ Write-Host "══════════════════════
|
||||
Write-Host " $appName Release" -ForegroundColor Cyan
|
||||
Write-Host " Versie : $appVersion" -ForegroundColor Green
|
||||
Write-Host " Git-tag : $gitTag" -ForegroundColor Green
|
||||
Write-Host " Registry: $registry" -ForegroundColor Green
|
||||
Write-Host " Image : $appVersionTag" -ForegroundColor Green
|
||||
Write-Host " Platform: $platforms" -ForegroundColor Green
|
||||
if ($DryRun) { Write-Host " Mode : DRY-RUN (geen wijzigingen)" -ForegroundColor Yellow }
|
||||
if ($NoBuild) { Write-Host " Mode : SKIP BUILD" -ForegroundColor Yellow }
|
||||
if ($SkipTests) { Write-Host " Mode : SKIP TESTS" -ForegroundColor Yellow }
|
||||
if ($Force) { Write-Host " Mode : FORCE (conflictcontrole overgeslagen)" -ForegroundColor Red }
|
||||
Write-Host "════════════════════════════════════════" -ForegroundColor Cyan
|
||||
|
||||
# ── Stap 1: Conflictcontrole ──────────────────────────────────────────────
|
||||
Write-Step "[1/4] Conflictcontrole"
|
||||
Write-Step "[1/5] Conflictcontrole"
|
||||
|
||||
if (-not $DryRun -and -not $Force) {
|
||||
$localTag = git tag --list $gitTag
|
||||
@@ -138,23 +139,13 @@ if (-not $DryRun -and -not $Force) {
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "Controleer registry: $backendVersionTag" -ForegroundColor DarkGray
|
||||
Write-Host "Controleer registry: $appVersionTag" -ForegroundColor DarkGray
|
||||
$ErrorActionPreference = "SilentlyContinue"
|
||||
docker manifest inspect $backendVersionTag 2>$null
|
||||
$backendExists = $LASTEXITCODE -eq 0
|
||||
docker manifest inspect $appVersionTag 2>$null | Out-Null
|
||||
$imageExists = $LASTEXITCODE -eq 0
|
||||
$ErrorActionPreference = "Stop"
|
||||
if ($backendExists) {
|
||||
Write-Error "Registry-tag '$backendVersionTag' bestaat al. Verhoog het versienummer in package.json of gebruik -Force."
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "Controleer registry: $frontendVersionTag" -ForegroundColor DarkGray
|
||||
$ErrorActionPreference = "SilentlyContinue"
|
||||
docker manifest inspect $frontendVersionTag 2>$null
|
||||
$frontendExists = $LASTEXITCODE -eq 0
|
||||
$ErrorActionPreference = "Stop"
|
||||
if ($frontendExists) {
|
||||
Write-Error "Registry-tag '$frontendVersionTag' bestaat al. Verhoog het versienummer in package.json of gebruik -Force."
|
||||
if ($imageExists) {
|
||||
Write-Error "Registry-tag '$appVersionTag' bestaat al. Verhoog het versienummer in package.json of gebruik -Force."
|
||||
exit 1
|
||||
}
|
||||
|
||||
@@ -165,38 +156,44 @@ if (-not $DryRun -and -not $Force) {
|
||||
Write-Host "[DRY-RUN] Conflictcontrole overgeslagen" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
# ── Stap 2: Bouwen en pushen ──────────────────────────────────────────────
|
||||
if (-not $NoBuild) {
|
||||
Write-Step "[2/4] Multi-platform bouwen en pushen"
|
||||
|
||||
Write-Host "Backend bouwen en pushen..." -ForegroundColor White
|
||||
Invoke-Cmd @(
|
||||
"docker", "buildx", "build",
|
||||
"--platform", "linux/amd64,linux/arm64",
|
||||
"--push",
|
||||
"-t", $backendVersionTag,
|
||||
"-t", $backendLatestTag,
|
||||
"--build-arg", "APP_VERSION=$appVersion",
|
||||
(Join-Path $PSScriptRoot "backend")
|
||||
)
|
||||
|
||||
Write-Host "Frontend bouwen en pushen..." -ForegroundColor White
|
||||
Invoke-Cmd @(
|
||||
"docker", "buildx", "build",
|
||||
"--platform", "linux/amd64,linux/arm64",
|
||||
"--push",
|
||||
"-t", $frontendVersionTag,
|
||||
"-t", $frontendLatestTag,
|
||||
"--build-arg", "APP_VERSION=$appVersion",
|
||||
"--build-arg", "VITE_API_URL=$apiUrl",
|
||||
(Join-Path $PSScriptRoot "frontend")
|
||||
)
|
||||
# ── Stap 2: Tests ─────────────────────────────────────────────────────────
|
||||
if (-not $SkipTests) {
|
||||
Write-Step "[2/5] Testsuite"
|
||||
if ($DryRun) {
|
||||
Write-Host "[DRY-RUN] test.ps1 -NoSmoke" -ForegroundColor Yellow
|
||||
} else {
|
||||
& (Join-Path $PSScriptRoot "test.ps1") -NoSmoke
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Tests mislukt — release afgebroken."
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Write-Step "[2/4] Bouwen overgeslagen (-NoBuild)"
|
||||
Write-Step "[2/5] Tests overgeslagen (-SkipTests)"
|
||||
}
|
||||
|
||||
# ── Stap 3: Git-tag aanmaken en pushen ────────────────────────────────────
|
||||
Write-Step "[3/4] Git-tag aanmaken en pushen"
|
||||
# ── Stap 3: Bouwen en pushen ──────────────────────────────────────────────
|
||||
if (-not $NoBuild) {
|
||||
Write-Step "[3/5] Multi-platform bouwen en pushen"
|
||||
|
||||
Invoke-Cmd @(
|
||||
"docker", "buildx", "build",
|
||||
"--platform", $platforms,
|
||||
"--target", "production",
|
||||
"--push",
|
||||
"-t", $appVersionTag,
|
||||
"-t", $appLatestTag,
|
||||
"--build-arg", "APP_VERSION=$appVersion",
|
||||
"--build-arg", "VITE_API_URL=$apiUrl",
|
||||
"--build-arg", "VITE_APP_VERSION=$appVersion",
|
||||
$PSScriptRoot
|
||||
)
|
||||
} else {
|
||||
Write-Step "[3/5] Bouwen overgeslagen (-NoBuild)"
|
||||
}
|
||||
|
||||
# ── Stap 4: Git-tag aanmaken en pushen ────────────────────────────────────
|
||||
Write-Step "[4/5] Git-tag aanmaken en pushen"
|
||||
|
||||
if ($Force) {
|
||||
Write-Host "Bestaande lokale tag verwijderen (indien aanwezig)..." -ForegroundColor Yellow
|
||||
@@ -215,8 +212,8 @@ Invoke-Cmd @("git", "tag", "-a", $gitTag, "-m", "Release $gitTag")
|
||||
Write-Host "Git-tag pushen naar origin" -ForegroundColor White
|
||||
Invoke-Cmd @("git", "push", "origin", "refs/tags/$gitTag")
|
||||
|
||||
# ── Stap 4: Gitea-release aanmaken ────────────────────────────────────────
|
||||
Write-Step "[4/4] Gitea-release aanmaken"
|
||||
# ── Stap 5: Gitea-release aanmaken ────────────────────────────────────────
|
||||
Write-Step "[5/5] Gitea-release aanmaken"
|
||||
|
||||
# De update-melding in de app leest /releases/latest. Een kale Git-tag komt
|
||||
# daar niet in terug, dus er moet een echte release van gemaakt worden.
|
||||
@@ -232,7 +229,6 @@ if (-not $releaseApi) {
|
||||
} elseif (-not $giteaToken) {
|
||||
Write-Host "Geen GITEA_TOKEN in de omgeving — release overgeslagen." -ForegroundColor Yellow
|
||||
Write-Host " Zonder release ziet de app geen update-melding voor $appVersion." -ForegroundColor DarkGray
|
||||
Write-Host ' Token met release-rechten zetten: $env:GITEA_TOKEN = "<token>"' -ForegroundColor DarkGray
|
||||
} else {
|
||||
$notes = Get-ChangelogSection $appVersion
|
||||
if (-not $notes) { $notes = "Release $appVersion" }
|
||||
@@ -260,7 +256,7 @@ if (-not $releaseApi) {
|
||||
Write-Host "Release voor $gitTag bestaat al — overgeslagen." -ForegroundColor Yellow
|
||||
} else {
|
||||
Write-Host "Release aanmaken mislukt ($code): $($_.Exception.Message)" -ForegroundColor Red
|
||||
Write-Host "Tag en images staan al klaar; maak de release eventueel handmatig aan." -ForegroundColor DarkGray
|
||||
Write-Host "Tag en image staan al klaar; maak de release eventueel handmatig aan." -ForegroundColor DarkGray
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -271,11 +267,9 @@ Write-Host ""
|
||||
Write-Host "════════════════════════════════════════" -ForegroundColor Cyan
|
||||
Write-Host " Gepubliceerd: $appVersion" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
Write-Host " Registry images:" -ForegroundColor Green
|
||||
Write-Host " $backendVersionTag" -ForegroundColor White
|
||||
Write-Host " $backendLatestTag" -ForegroundColor White
|
||||
Write-Host " $frontendVersionTag" -ForegroundColor White
|
||||
Write-Host " $frontendLatestTag" -ForegroundColor White
|
||||
Write-Host " Registry image:" -ForegroundColor Green
|
||||
Write-Host " $appVersionTag" -ForegroundColor White
|
||||
Write-Host " $appLatestTag" -ForegroundColor White
|
||||
Write-Host ""
|
||||
Write-Host " Git-tag: $gitTag -> origin" -ForegroundColor Green
|
||||
Write-Host "════════════════════════════════════════" -ForegroundColor Cyan
|
||||
|
||||
+8
-7
@@ -1,12 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
# ── VibeFinance release wrapper (macOS/Linux) ────────────────────────────
|
||||
# Roept release.ps1 aan via PowerShell Core (pwsh).
|
||||
# Roept release.ps1 aan via PowerShell Core (pwsh). Bouwt één app-image
|
||||
# (API + frontend) voor amd64 en arm64.
|
||||
#
|
||||
# Gebruik:
|
||||
# ./release.sh # bouw + push images + Git-tag
|
||||
# ./release.sh -NoBuild # sla docker build over, alleen Git-tag
|
||||
# ./release.sh # test + bouw + push + Git-tag + Gitea-release
|
||||
# ./release.sh -NoBuild # sla docker build over, alleen tag + release
|
||||
# ./release.sh -SkipTests # sla de testsuite over (niet aanbevolen)
|
||||
# ./release.sh -DryRun # toon alle commando's zonder iets uit te voeren
|
||||
# ./release.sh -Force # sla conflictcontrole over, overschrijf bestaande tags
|
||||
# ./release.sh -Force # sla conflictcontrole over, overschrijf tags
|
||||
#
|
||||
# Combinaties:
|
||||
# ./release.sh -Force -NoBuild
|
||||
@@ -14,9 +16,9 @@
|
||||
#
|
||||
# Vereisten:
|
||||
# - pwsh (PowerShell Core) geïnstalleerd: brew install powershell
|
||||
# - Docker Desktop actief
|
||||
# - Docker actief
|
||||
# - docker buildx builder actief: docker buildx create --use
|
||||
# - Ingelogd op de registry: docker login vibetea.vldn.net
|
||||
# - Ingelogd op de registry: docker login vibehoogie.duckdns.org
|
||||
# - Git geïnstalleerd
|
||||
# - root package.json bevat versienummer ZONDER -dev suffix
|
||||
# - GITEA_TOKEN in de omgeving (token met release-rechten) — nodig voor het
|
||||
@@ -31,7 +33,6 @@ if ! command -v pwsh &>/dev/null; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Geef alle argumenten door aan het .ps1 script
|
||||
pwsh -NoProfile -ExecutionPolicy Bypass \
|
||||
-File "$SCRIPT_DIR/release.ps1" \
|
||||
"$@"
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
# ── VibeFinance testscript (PowerShell) ──────────────────────────────────
|
||||
# Draait de volledige testsuite voor de 2-container opzet. Alles gebeurt in
|
||||
# Docker, dus je hebt geen Node op de host nodig.
|
||||
#
|
||||
# 1. Backend-tests (Dockerfile target 'test' → node --test, zonder database)
|
||||
# 2. Productiebuild (Vite-build + app-image; faalt de frontend, faalt dit)
|
||||
# 3. Smoketest (productie-image + postgres, /api/health en / opvragen)
|
||||
#
|
||||
# De smoketest publiceert geen hostpoorten en gebruikt een eigen netwerk, dus
|
||||
# hij botst niet met een draaiende dev-stack.
|
||||
#
|
||||
# Gebruik:
|
||||
# .\test.ps1 # alle stappen
|
||||
# .\test.ps1 -Unit # alleen de backend-tests
|
||||
# .\test.ps1 -NoSmoke # tests + build, zonder de stack te starten
|
||||
|
||||
param(
|
||||
[switch]$Unit,
|
||||
[switch]$NoSmoke
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$appVersion = (Get-Content (Join-Path $PSScriptRoot "package.json") -Raw | ConvertFrom-Json).version
|
||||
$testImage = "vibefinance/app:test-$appVersion"
|
||||
$prodImage = "vibefinance/app:build-$appVersion"
|
||||
$smokeNet = "vibefinance-smoke-net"
|
||||
$smokeDb = "vibefinance-smoke-db"
|
||||
$smokeApp = "vibefinance-smoke-app"
|
||||
|
||||
function Write-Step {
|
||||
param([string]$Label)
|
||||
Write-Host ""
|
||||
Write-Host "── $Label" -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
# ── 1. Backend-tests ──────────────────────────────────────────────────────
|
||||
Write-Step "[1/3] Backend-tests"
|
||||
docker build --target test -t $testImage $PSScriptRoot
|
||||
if ($LASTEXITCODE -ne 0) { Write-Error "Testimage bouwen mislukt"; exit 1 }
|
||||
|
||||
docker run --rm $testImage
|
||||
if ($LASTEXITCODE -ne 0) { Write-Error "Backend-tests mislukt"; exit 1 }
|
||||
Write-Host "Backend-tests geslaagd" -ForegroundColor Green
|
||||
|
||||
if ($Unit) {
|
||||
Write-Host ""
|
||||
Write-Host "Alleen unit-tests gevraagd — klaar." -ForegroundColor Green
|
||||
exit 0
|
||||
}
|
||||
|
||||
# ── 2. Productiebuild (inclusief Vite-build) ──────────────────────────────
|
||||
Write-Step "[2/3] Productie-image bouwen"
|
||||
docker build --target production `
|
||||
--build-arg APP_VERSION=$appVersion `
|
||||
--build-arg VITE_APP_VERSION=$appVersion `
|
||||
--build-arg VITE_API_URL=/api `
|
||||
-t $prodImage $PSScriptRoot
|
||||
if ($LASTEXITCODE -ne 0) { Write-Error "Productiebuild mislukt"; exit 1 }
|
||||
Write-Host "Productie-image gebouwd: $prodImage" -ForegroundColor Green
|
||||
|
||||
if ($NoSmoke) {
|
||||
Write-Host ""
|
||||
Write-Host "Smoketest overgeslagen (-NoSmoke) — klaar." -ForegroundColor Green
|
||||
exit 0
|
||||
}
|
||||
|
||||
# ── 3. Smoketest op het productie-image + postgres ────────────────────────
|
||||
Write-Step "[3/3] Smoketest (app + postgres)"
|
||||
|
||||
function Remove-SmokeStack {
|
||||
docker rm -f $smokeApp $smokeDb 2>$null | Out-Null
|
||||
docker network rm $smokeNet 2>$null | Out-Null
|
||||
}
|
||||
|
||||
# Eventuele resten van een eerdere run opruimen
|
||||
Remove-SmokeStack
|
||||
|
||||
try {
|
||||
docker network create $smokeNet | Out-Null
|
||||
|
||||
docker run -d --name $smokeDb --network $smokeNet `
|
||||
-e POSTGRES_DB=vibefinance `
|
||||
-e POSTGRES_USER=vibefinance `
|
||||
-e POSTGRES_PASSWORD=smokepassword `
|
||||
postgres:18-alpine | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) { throw "Database-container starten mislukt" }
|
||||
|
||||
Write-Host " Wachten op postgres..." -ForegroundColor DarkGray
|
||||
$dbGereed = $false
|
||||
foreach ($poging in 1..30) {
|
||||
docker exec $smokeDb pg_isready -U vibefinance 2>$null | Out-Null
|
||||
if ($LASTEXITCODE -eq 0) { $dbGereed = $true; break }
|
||||
Start-Sleep -Seconds 2
|
||||
}
|
||||
if (-not $dbGereed) { throw "Postgres werd niet bereikbaar" }
|
||||
|
||||
docker run -d --name $smokeApp --network $smokeNet `
|
||||
-e NODE_ENV=production `
|
||||
-e JWT_SECRET=smoke-secret `
|
||||
-e UPDATE_CHECK_URL= `
|
||||
-e DATABASE_URL=postgresql://vibefinance:smokepassword@$smokeDb`:5432/vibefinance `
|
||||
$prodImage | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) { throw "App-container starten mislukt" }
|
||||
|
||||
Write-Host " Wachten op de app..." -ForegroundColor DarkGray
|
||||
$health = $null
|
||||
foreach ($poging in 1..30) {
|
||||
$health = (docker exec $smokeApp wget -qO- http://127.0.0.1:3001/api/health 2>$null) -join ""
|
||||
if ($LASTEXITCODE -eq 0 -and $health -match '"status"') { break }
|
||||
$health = $null
|
||||
Start-Sleep -Seconds 2
|
||||
}
|
||||
if (-not $health) {
|
||||
docker logs --tail 40 $smokeApp
|
||||
throw "App werd niet gezond"
|
||||
}
|
||||
Write-Host " /api/health : ok" -ForegroundColor Green
|
||||
|
||||
# wget levert meerdere regels; samenvoegen tot één string, want -match op een
|
||||
# array filtert de array in plaats van een test te doen.
|
||||
$index = (docker exec $smokeApp wget -qO- http://127.0.0.1:3001/) -join "`n"
|
||||
if ($LASTEXITCODE -ne 0 -or $index -notmatch 'id="root"') { throw "Root gaf geen index.html terug" }
|
||||
Write-Host " / : index.html" -ForegroundColor Green
|
||||
|
||||
$spa = (docker exec $smokeApp wget -qO- http://127.0.0.1:3001/voortgang) -join "`n"
|
||||
if ($LASTEXITCODE -ne 0 -or $spa -notmatch 'id="root"') { throw "SPA-fallback werkt niet" }
|
||||
Write-Host " /voortgang : index.html (SPA-fallback)" -ForegroundColor Green
|
||||
|
||||
$api404 = (docker exec $smokeApp wget -qO- --server-response http://127.0.0.1:3001/api/bestaat-niet 2>&1) -join "`n"
|
||||
if ($api404 -notmatch "404") { throw "Onbekende API-route gaf geen 404" }
|
||||
Write-Host " /api/bestaat-niet: 404" -ForegroundColor Green
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Smoketest geslaagd" -ForegroundColor Green
|
||||
}
|
||||
finally {
|
||||
Remove-SmokeStack
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "════════════════════════════════════════" -ForegroundColor Cyan
|
||||
Write-Host " Alle tests geslaagd - $appVersion" -ForegroundColor Green
|
||||
Write-Host "════════════════════════════════════════" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env bash
|
||||
# ── VibeFinance test wrapper (macOS/Linux) ───────────────────────────────
|
||||
# Roept test.ps1 aan via PowerShell Core (pwsh).
|
||||
#
|
||||
# Gebruik:
|
||||
# ./test.sh # backend-tests + productiebuild + smoketest
|
||||
# ./test.sh -Unit # alleen de backend-tests
|
||||
# ./test.sh -NoSmoke # zonder de stack te starten
|
||||
#
|
||||
# Vereisten:
|
||||
# - pwsh (PowerShell Core): brew install powershell
|
||||
# - Docker actief
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
if ! command -v pwsh &>/dev/null; then
|
||||
echo "Fout: pwsh niet gevonden. Installeer via: brew install powershell" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
pwsh -NoProfile -ExecutionPolicy Bypass \
|
||||
-File "$SCRIPT_DIR/test.ps1" \
|
||||
"$@"
|
||||
Reference in New Issue
Block a user