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:
Jordy Hoogeveen
2026-09-04 11:51:29 +02:00
co-authored by Claude Opus 5
parent 3c67381de9
commit bfec3a25f3
29 changed files with 1168 additions and 428 deletions
+18
View File
@@ -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
+21
View File
@@ -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=
+11
View File
@@ -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
View File
@@ -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
+90 -29
View File
@@ -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
```
---
+38
View File
@@ -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
View File
@@ -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"]
+108 -25
View File
@@ -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`.
---
+195
View File
@@ -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
-41
View File
@@ -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"]
+1
View File
@@ -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": {
+10
View File
@@ -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
+7
View File
@@ -7,6 +7,12 @@ export const db = new Pool({
connectionString: config.DATABASE_URL,
});
/**
* 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,
@@ -31,3 +37,4 @@ await db.query(`
await db.query(`
ALTER TABLE users ADD COLUMN IF NOT EXISTS avatar TEXT;
`);
}
+57 -12
View File
@@ -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")
);
});
}
+117
View File
@@ -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);
});
});
+48 -61
View File
@@ -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
}
}
+10 -11
View File
@@ -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
View File
@@ -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
View File
@@ -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:
+23
View File
@@ -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
-42
View File
@@ -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;"]
-32
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
{
"name": "vibefinance-frontend",
"version": "0.1.5-dev",
"version": "0.1.6-dev",
"private": true,
"type": "module",
"scripts": {
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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 {
Write-Step "[2/4] Bouwen overgeslagen (-NoBuild)"
& (Join-Path $PSScriptRoot "test.ps1") -NoSmoke
if ($LASTEXITCODE -ne 0) {
Write-Error "Tests mislukt — release afgebroken."
exit 1
}
}
} else {
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
View File
@@ -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" \
"$@"
+145
View File
@@ -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 ""
+25
View File
@@ -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" \
"$@"