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,28 +1,59 @@
|
|||||||
# VibeFinance — Beheer
|
# 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
|
## Ontwikkeling
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
.\dev.ps1 # build + start (standaard)
|
.\dev.ps1 # build + start (standaard)
|
||||||
.\dev.ps1 build # alleen bouwen
|
.\dev.ps1 build # alleen bouwen
|
||||||
.\dev.ps1 build -Target backend # alleen backend bouwen
|
.\dev.ps1 up -Target app # alleen de app herbouwen + herstarten
|
||||||
.\dev.ps1 build -Target frontend # alleen frontend bouwen
|
.\dev.ps1 up -Target postgres # alleen de database herstarten
|
||||||
.\dev.ps1 up -Target backend # alleen backend herbouwen + herstarten
|
.\dev.ps1 logs # logs van de app volgen
|
||||||
.\dev.ps1 up -Target frontend # alleen frontend herbouwen + herstarten
|
.\dev.ps1 test # testsuite draaien
|
||||||
.\dev.ps1 push # dev-images naar registry pushen
|
.\dev.ps1 down # stoppen én volumes wissen
|
||||||
.\dev.ps1 push -Target backend # alleen backend pushen
|
.\dev.ps1 push # dev-image naar registry pushen
|
||||||
.\dev.ps1 push -Target frontend # alleen frontend pushen
|
|
||||||
```
|
```
|
||||||
|
|
||||||
> Op macOS/Linux: gebruik `./dev.sh` met dezelfde argumenten.
|
> 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
|
```bash
|
||||||
docker compose -f docker-compose.dev.yml down
|
docker compose -f docker-compose.dev.yml exec app npm test
|
||||||
docker compose -f docker-compose.dev.yml up -d --build
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -31,16 +62,38 @@ docker compose -f docker-compose.dev.yml up -d --build
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# 1. Versienummer in package.json van "x.x.x-dev" naar "x.x.x"
|
# 1. Versienummer in package.json van "x.x.x-dev" naar "x.x.x"
|
||||||
# 2.
|
# 2. Changelog.md bijwerken — die sectie wordt de release-notes
|
||||||
.\release.ps1 # bouw, push naar registry, maak Git-tag aan
|
# 3. Token met release-rechten zetten
|
||||||
.\release.ps1 -DryRun # droogloop — toont alle stappen zonder uit te voeren
|
$env:GITEA_TOKEN = "<token>"
|
||||||
.\release.ps1 -NoBuild # sla docker build over, push bestaande images
|
|
||||||
|
|
||||||
# 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
|
## Docker
|
||||||
|
|
||||||
```bash
|
```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
|
| Verzoek | Wat de app doet |
|
||||||
location /api/ {
|
|---|---|
|
||||||
proxy_pass http://backend:3001;
|
| `/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
|
## Logs en diagnose
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Live logs (dev)
|
# Live logs (dev) — beide processen van de app-container
|
||||||
docker compose -f docker-compose.dev.yml logs -f
|
docker compose -f docker-compose.dev.yml logs -f app
|
||||||
|
|
||||||
# Live logs (productie)
|
# Live logs (productie)
|
||||||
docker compose logs -f
|
docker compose logs -f
|
||||||
|
|
||||||
|
# Shell in de app-container (dev)
|
||||||
|
docker compose -f docker-compose.dev.yml exec app sh
|
||||||
|
|
||||||
# Status containers
|
# Status containers
|
||||||
docker compose ps
|
docker compose ps
|
||||||
|
|
||||||
@@ -157,7 +218,8 @@ Rollen:
|
|||||||
- [ ] `POSTGRES_PASSWORD` instellen op een sterk wachtwoord
|
- [ ] `POSTGRES_PASSWORD` instellen op een sterk wachtwoord
|
||||||
- [ ] `JWT_SECRET` aanpassen (genereer met `openssl rand -hex 64`)
|
- [ ] `JWT_SECRET` aanpassen (genereer met `openssl rand -hex 64`)
|
||||||
- [ ] `CORS_ORIGIN` instellen op je domeinnaam
|
- [ ] `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)
|
- [ ] TLS afhandelen via een externe reverse proxy (bijv. Nginx Proxy Manager, Caddy, Traefik)
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -165,8 +227,7 @@ Rollen:
|
|||||||
## Registry
|
## Registry
|
||||||
|
|
||||||
```
|
```
|
||||||
vibehoogie.duckdns.org:3000/vibe/vibefinance-backend
|
vibehoogie.duckdns.org/vibe/vibefinance-app (frontend + backend in één image)
|
||||||
vibehoogie.duckdns.org:3000/vibe/vibefinance-frontend
|
|
||||||
|
|
||||||
Tags:
|
Tags:
|
||||||
latest productie — meest recente release
|
latest productie — meest recente release
|
||||||
@@ -177,7 +238,7 @@ Tags:
|
|||||||
|
|
||||||
Inloggen op registry:
|
Inloggen op registry:
|
||||||
```bash
|
```bash
|
||||||
docker login vibehoogie.duckdns.org:3000
|
docker login vibehoogie.duckdns.org
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -1,5 +1,43 @@
|
|||||||
# Changelog
|
# 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
|
## [0.1.5] - 2026-09-04
|
||||||
|
|
||||||
### Toegevoegd
|
### 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+
|
> 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`** — kopieer `.env.example` en vul in:
|
||||||
|
|
||||||
```env
|
```env
|
||||||
POSTGRES_USER=vibefinance
|
POSTGRES_USER=vibefinance
|
||||||
POSTGRES_PASSWORD=kies_een_sterk_wachtwoord
|
POSTGRES_PASSWORD=kies_een_sterk_wachtwoord
|
||||||
JWT_SECRET=minimaal_64_willekeurige_tekens
|
JWT_SECRET=minimaal_64_willekeurige_tekens
|
||||||
FRONTEND_PORT=3300
|
APP_PORT=3400
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -42,10 +45,14 @@ openssl rand -hex 64
|
|||||||
```
|
```
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
cp .env.example .env # en vul de waarden in
|
||||||
docker compose pull && docker compose up -d
|
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)
|
### Development (hot reload)
|
||||||
|
|
||||||
@@ -53,9 +60,45 @@ Navigeer naar `http://<server-ip>:3300` en registreer je account. De **eerste**
|
|||||||
.\dev.ps1
|
.\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/
|
vibefinance/
|
||||||
├── docker-compose.yml # Productie orchestratie (pre-built images)
|
├── Dockerfile # Multi-stage: frontend build → app-image
|
||||||
├── docker-compose.dev.yml # Standalone dev stack (hot reload)
|
├── .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
|
├── .env.example # Kopieer naar .env en vul in
|
||||||
├── package.json # Versienummer — single source of truth
|
├── 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
|
├── docker/
|
||||||
│ ├── Dockerfile # Multi-stage: dev → build → nginx
|
│ └── dev-entrypoint.sh # Start API + Vite samen in de dev-container
|
||||||
│ ├── nginx-spa.conf # SPA fallback + /api proxy naar backend
|
│
|
||||||
│ ├── vite.config.js
|
├── frontend/ # React / Vite SPA (build → dist)
|
||||||
|
│ ├── vite.config.js # /api proxy naar de API in dezelfde container
|
||||||
│ └── src/
|
│ └── src/
|
||||||
│ ├── App.jsx # Root – wiring + idle-logout
|
│ ├── App.jsx # Root – wiring + idle-logout
|
||||||
│ ├── main.jsx
|
│ ├── main.jsx
|
||||||
│ ├── constants/ # Kleuren, tabs, begindata
|
│ ├── constants/ # Kleuren, tabs, begindata
|
||||||
│ ├── utils/ # Formatters, helpers
|
│ ├── utils/ # Formatters, helpers
|
||||||
│ ├── hooks/ # useTheme
|
│ ├── hooks/ # useTheme, useUpdateCheck
|
||||||
│ ├── context/ # AppContext (global state + API sync)
|
│ ├── context/ # AppContext (global state + API sync)
|
||||||
│ ├── components/
|
│ ├── components/
|
||||||
│ │ ├── ui/ # Primitieve UI (VCard, DonutChart…)
|
│ │ ├── ui/ # Primitieve UI (VCard, DonutChart…)
|
||||||
@@ -104,25 +153,39 @@ vibefinance/
|
|||||||
│ ├── SchuldTab.jsx
|
│ ├── SchuldTab.jsx
|
||||||
│ └── VoortgangTab.jsx
|
│ └── VoortgangTab.jsx
|
||||||
│
|
│
|
||||||
└── backend/ # Express REST API
|
└── backend/ # Express REST API + statische frontend
|
||||||
├── Dockerfile # Multi-stage: dev → production (non-root)
|
├── test/ # node --test integratietests
|
||||||
└── src/
|
└── src/
|
||||||
├── server.js # Entry point
|
├── server.js # Entry point: API op /api, SPA op /
|
||||||
├── db.js # PostgreSQL connectie + schema migraties
|
├── db.js # PostgreSQL pool + initDb() schema-migraties
|
||||||
├── config/ # Omgevingsvariabelen
|
├── config/ # Omgevingsvariabelen (incl. STATIC_DIR)
|
||||||
├── constants/ # Begindata (initialData.js)
|
├── constants/ # Begindata (initialData.js)
|
||||||
├── middleware/ # auth.js, errorHandler.js
|
├── middleware/ # auth.js, errorHandler.js
|
||||||
├── routes/ # auth, data, users, health
|
├── routes/ # auth, data, users, health, update-check
|
||||||
└── stores/ # userStore, dataStore
|
└── stores/ # userStore, dataStore
|
||||||
```
|
```
|
||||||
|
|
||||||
**Stack:** React 18 · Vite · Node.js 22 · Express 4 · PostgreSQL 18 · JWT · bcrypt · Docker Compose v2
|
**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
|
## 🔌 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 |
|
| 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/data` | GET, PUT, DELETE | Financiële data per gebruiker |
|
||||||
| `/api/users` | GET, POST, PATCH, DELETE | Gebruikersbeheer (Admin only) |
|
| `/api/users` | GET, POST, PATCH, DELETE | Gebruikersbeheer (Admin only) |
|
||||||
| `/api/health` | GET | Status + uptime |
|
| `/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`).
|
> 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
|
## ⚙️ Omgevingsvariabelen
|
||||||
@@ -161,8 +228,12 @@ Beveiligde endpoints vereisen `Authorization: Bearer <token>`. Rate limiting: 20
|
|||||||
| `POSTGRES_USER` | `vibefinance` | Database gebruikersnaam |
|
| `POSTGRES_USER` | `vibefinance` | Database gebruikersnaam |
|
||||||
| `POSTGRES_DB` | `vibefinance` | Database naam |
|
| `POSTGRES_DB` | `vibefinance` | Database naam |
|
||||||
| `JWT_EXPIRES` | `7d` | Geldigheidsduur token |
|
| `JWT_EXPIRES` | `7d` | Geldigheidsduur token |
|
||||||
| `CORS_ORIGIN` | — | Externe domeinnaam |
|
| `CORS_ORIGIN` | `http://localhost` | Externe domeinnaam (één origin, dus zelden nodig) |
|
||||||
| `FRONTEND_PORT` | `3300` | Externe poort frontend |
|
| `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)
|
- Wachtwoorden gehasht met bcrypt (cost factor 12)
|
||||||
- JWT authenticatie met instelbare vervaltijd
|
- 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
|
- Database bereikbaar alleen binnen het interne Docker netwerk
|
||||||
- Containers draaien als non-root
|
- App-container draait als non-root
|
||||||
- Helmet security headers
|
- Rate limiting op `/api` (statische bestanden uitgezonderd)
|
||||||
- Rate limiting op alle endpoints
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 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": {
|
"scripts": {
|
||||||
"start": "node src/server.js",
|
"start": "node src/server.js",
|
||||||
"dev": "node --watch src/server.js",
|
"dev": "node --watch src/server.js",
|
||||||
|
"test": "node --test",
|
||||||
"lint": "eslint src --ext .js"
|
"lint": "eslint src --ext .js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import "dotenv/config";
|
import "dotenv/config";
|
||||||
|
import { dirname, join, resolve } from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
NODE_ENV: process.env.NODE_ENV || "development",
|
NODE_ENV: process.env.NODE_ENV || "development",
|
||||||
@@ -9,6 +13,12 @@ export const config = {
|
|||||||
DATABASE_URL: process.env.DATABASE_URL,
|
DATABASE_URL: process.env.DATABASE_URL,
|
||||||
REDIS_URL: process.env.REDIS_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).
|
// Update-notificaties: welke release-API wordt gepolld (Gitea).
|
||||||
// Leeg laten schakelt de melding in de app uit.
|
// Leeg laten schakelt de melding in de app uit.
|
||||||
UPDATE_CHECK_URL: process.env.UPDATE_CHECK_URL
|
UPDATE_CHECK_URL: process.env.UPDATE_CHECK_URL
|
||||||
|
|||||||
+28
-21
@@ -7,27 +7,34 @@ export const db = new Pool({
|
|||||||
connectionString: config.DATABASE_URL,
|
connectionString: config.DATABASE_URL,
|
||||||
});
|
});
|
||||||
|
|
||||||
await db.query(`
|
/**
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
* Zet het schema klaar. Wordt bij het starten van de server aangeroepen —
|
||||||
id TEXT PRIMARY KEY,
|
* niet bij het importeren, zodat de app ook zonder database te importeren is
|
||||||
naam TEXT NOT NULL,
|
* (tests, health checks tijdens de build).
|
||||||
email TEXT UNIQUE NOT NULL,
|
*/
|
||||||
wachtwoord TEXT NOT NULL,
|
export async function initDb() {
|
||||||
rol TEXT NOT NULL DEFAULT 'Viewer',
|
await db.query(`
|
||||||
actief BOOLEAN NOT NULL DEFAULT true,
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
aangemaakt TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
id TEXT PRIMARY KEY,
|
||||||
);
|
naam TEXT NOT NULL,
|
||||||
|
email TEXT UNIQUE NOT NULL,
|
||||||
|
wachtwoord TEXT NOT NULL,
|
||||||
|
rol TEXT NOT NULL DEFAULT 'Viewer',
|
||||||
|
actief BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
aangemaakt TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS user_data (
|
CREATE TABLE IF NOT EXISTS user_data (
|
||||||
user_id TEXT PRIMARY KEY,
|
user_id TEXT PRIMARY KEY,
|
||||||
data TEXT NOT NULL
|
data TEXT NOT NULL
|
||||||
);
|
);
|
||||||
`);
|
`);
|
||||||
|
|
||||||
await db.query(`
|
await db.query(`
|
||||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS aangemaakt TIMESTAMPTZ NOT NULL DEFAULT NOW();
|
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;
|
ALTER TABLE users ADD COLUMN IF NOT EXISTS avatar TEXT;
|
||||||
`);
|
`);
|
||||||
|
}
|
||||||
|
|||||||
+61
-16
@@ -3,9 +3,12 @@ import cors from "cors";
|
|||||||
import helmet from "helmet";
|
import helmet from "helmet";
|
||||||
import rateLimit from "express-rate-limit";
|
import rateLimit from "express-rate-limit";
|
||||||
import cookieParser from "cookie-parser";
|
import cookieParser from "cookie-parser";
|
||||||
import pino from "pino";
|
|
||||||
import pinoHttp from "pino-http";
|
import pinoHttp from "pino-http";
|
||||||
|
import { existsSync } from "fs";
|
||||||
|
import { join } from "path";
|
||||||
import { config } from "./config/index.js";
|
import { config } from "./config/index.js";
|
||||||
|
import { logger } from "./logger.js";
|
||||||
|
import { initDb } from "./db.js";
|
||||||
import authRouter from "./routes/auth.js";
|
import authRouter from "./routes/auth.js";
|
||||||
import dataRouter from "./routes/data.js";
|
import dataRouter from "./routes/data.js";
|
||||||
import usersRouter from "./routes/users.js";
|
import usersRouter from "./routes/users.js";
|
||||||
@@ -13,23 +16,36 @@ import healthRouter from "./routes/health.js";
|
|||||||
import updateRouter from "./routes/update.js";
|
import updateRouter from "./routes/update.js";
|
||||||
import { errorHandler } from "./middleware/errorHandler.js";
|
import { errorHandler } from "./middleware/errorHandler.js";
|
||||||
|
|
||||||
export const logger = pino({
|
export { logger };
|
||||||
level: config.NODE_ENV === "production" ? "info" : "debug",
|
|
||||||
...(config.NODE_ENV !== "production" && {
|
|
||||||
transport: { target: "pino-pretty", options: { colorize: true } },
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
// ── Security ──────────────────────────────────────────────────────────────────
|
// ── 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.use(cors({ origin: config.CORS_ORIGIN, credentials: true }));
|
||||||
app.set("trust proxy", 1);
|
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("/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 ───────────────────────────────────────────────────────────────────
|
// ── Parsing ───────────────────────────────────────────────────────────────────
|
||||||
app.use(express.json({ limit: "2mb" }));
|
app.use(express.json({ limit: "2mb" }));
|
||||||
@@ -40,23 +56,52 @@ if (config.NODE_ENV !== "test") {
|
|||||||
app.use(pinoHttp({ logger }));
|
app.use(pinoHttp({ logger }));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Routes ────────────────────────────────────────────────────────────────────
|
// ── API-routes ────────────────────────────────────────────────────────────────
|
||||||
app.use("/api/health", healthRouter);
|
app.use("/api/health", healthRouter);
|
||||||
app.use("/api/update-check", updateRouter);
|
app.use("/api/update-check", updateRouter);
|
||||||
app.use("/api/auth", authRouter);
|
app.use("/api/auth", authRouter);
|
||||||
app.use("/api/data", dataRouter);
|
app.use("/api/data", dataRouter);
|
||||||
app.use("/api/users", usersRouter);
|
app.use("/api/users", usersRouter);
|
||||||
|
|
||||||
// ── 404 voor overige /api/* ───────────────────────────────────────────────────
|
// ── 404 voor overige /api/* ───────────────────────────────────────────────────
|
||||||
app.use("/api/*", (_req, res) => res.status(404).json({ error: "Endpoint niet gevonden." }));
|
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 ─────────────────────────────────────────────────────────────
|
// ── Error handler ─────────────────────────────────────────────────────────────
|
||||||
app.use(errorHandler);
|
app.use(errorHandler);
|
||||||
|
|
||||||
// ── Start ─────────────────────────────────────────────────────────────────────
|
// ── Start ─────────────────────────────────────────────────────────────────────
|
||||||
if (config.NODE_ENV !== "test") {
|
if (config.NODE_ENV !== "test") {
|
||||||
|
await initDb();
|
||||||
app.listen(config.PORT, () => {
|
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)
|
# VibeFinance dev script (PowerShell)
|
||||||
# Lokale development cyclus: bouwen, starten, stoppen en optioneel pushen
|
# Lokale development cyclus voor de 2-container opzet: bouwen, starten,
|
||||||
# naar de dev-registry. Gebruikt altijd de -dev images en tags.
|
# stoppen en optioneel pushen naar de dev-registry.
|
||||||
|
#
|
||||||
|
# app API (node --watch) + Vite dev-server met hot reload
|
||||||
|
# postgres database
|
||||||
#
|
#
|
||||||
# Gebruik:
|
# Gebruik:
|
||||||
# .\dev.ps1 # build + herstart containers
|
# .\dev.ps1 # build + herstart containers
|
||||||
# .\dev.ps1 build # alleen lokaal bouwen
|
# .\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 # build + herstart containers
|
||||||
# .\dev.ps1 up -Target backend # alleen backend bouwen + herstart
|
# .\dev.ps1 up -Target app # alleen de app herbouwen + herstarten
|
||||||
# .\dev.ps1 up -Target frontend # alleen frontend bouwen + herstart
|
# .\dev.ps1 up -Target postgres # alleen de database herstarten
|
||||||
# .\dev.ps1 down # containers stoppen en volumes verwijderen
|
# .\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 # 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:
|
# Vereisten:
|
||||||
# - Docker Desktop actief
|
# - Docker Desktop actief
|
||||||
@@ -22,11 +23,11 @@
|
|||||||
# - root package.json bevat het versienummer (bijv. "1.0.0-dev")
|
# - root package.json bevat het versienummer (bijv. "1.0.0-dev")
|
||||||
|
|
||||||
param(
|
param(
|
||||||
[ValidateSet("build","up","down","push")]
|
[ValidateSet("build","up","down","logs","test","push")]
|
||||||
[string]$Action = "up",
|
[string]$Action = "up",
|
||||||
|
|
||||||
[ValidateSet("both","frontend","backend")]
|
[ValidateSet("all","app","postgres")]
|
||||||
[string]$Target = "both"
|
[string]$Target = "all"
|
||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
@@ -34,11 +35,10 @@ $ErrorActionPreference = "Stop"
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
# PROJECTCONFIGURATIE - pas alleen dit blok aan voor een nieuw project
|
# PROJECTCONFIGURATIE - pas alleen dit blok aan voor een nieuw project
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
$appName = "VibeFinance"
|
$appName = "VibeFinance"
|
||||||
$registry = "vibehoogie.duckdns.org/vibe"
|
$registry = "vibehoogie.duckdns.org/vibe"
|
||||||
$backendImage = "vibefinance-backend"
|
$appImage = "vibefinance-app"
|
||||||
$frontendImage = "vibefinance-frontend"
|
$apiUrl = "/api"
|
||||||
$apiUrl = "/api"
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
# Versie uit root package.json
|
# Versie uit root package.json
|
||||||
@@ -67,18 +67,17 @@ if ($frontendPkg.version -ne $appVersion) {
|
|||||||
Write-Host "Versie gesynchroniseerd naar frontend: $appVersion" -ForegroundColor Green
|
Write-Host "Versie gesynchroniseerd naar frontend: $appVersion" -ForegroundColor Green
|
||||||
}
|
}
|
||||||
|
|
||||||
# Registry tags
|
# Registry tags (één image voor de hele app)
|
||||||
$backendBase = "$registry/$backendImage"
|
$appBase = "$registry/$appImage"
|
||||||
$frontendBase = "$registry/$frontendImage"
|
$appLatestDev = "$appBase`:latest-dev"
|
||||||
|
$appVersionDev = "$appBase`:$appVersion"
|
||||||
$backendLatestDev = "$backendBase`:latest-dev"
|
|
||||||
$frontendLatestDev = "$frontendBase`:latest-dev"
|
|
||||||
$backendVersionDev = "$backendBase`:$appVersion"
|
|
||||||
$frontendVersionDev= "$frontendBase`:$appVersion"
|
|
||||||
|
|
||||||
# Pad naar compose file
|
# Pad naar compose file
|
||||||
$devCompose = Join-Path $PSScriptRoot "docker-compose.dev.yml"
|
$devCompose = Join-Path $PSScriptRoot "docker-compose.dev.yml"
|
||||||
|
|
||||||
|
# Welke services de actie raakt
|
||||||
|
$services = @(if ($Target -ne "all") { $Target })
|
||||||
|
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host "========================================" -ForegroundColor Cyan
|
Write-Host "========================================" -ForegroundColor Cyan
|
||||||
Write-Host " $appName Dev" -ForegroundColor Cyan
|
Write-Host " $appName Dev" -ForegroundColor Cyan
|
||||||
@@ -90,38 +89,20 @@ Write-Host "========================================" -ForegroundColor Cyan
|
|||||||
Write-Host ""
|
Write-Host ""
|
||||||
|
|
||||||
# Functies
|
# Functies
|
||||||
function Invoke-BackendPush {
|
function Invoke-AppPush {
|
||||||
Write-Host "Backend bouwen en pushen..." -ForegroundColor White
|
Write-Host "App bouwen en pushen (amd64 + arm64)..." -ForegroundColor White
|
||||||
docker buildx build `
|
docker buildx build `
|
||||||
--platform linux/amd64,linux/arm64 `
|
--platform linux/amd64,linux/arm64 `
|
||||||
|
--target production `
|
||||||
--push `
|
--push `
|
||||||
-t $backendLatestDev `
|
-t $appLatestDev `
|
||||||
-t $backendVersionDev `
|
-t $appVersionDev `
|
||||||
--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 `
|
|
||||||
--build-arg APP_VERSION=$appVersion `
|
--build-arg APP_VERSION=$appVersion `
|
||||||
--build-arg VITE_API_URL=$apiUrl `
|
--build-arg VITE_API_URL=$apiUrl `
|
||||||
--build-arg VITE_REPO_URL=$repoUrl `
|
--build-arg VITE_APP_VERSION=$appVersion `
|
||||||
(Join-Path $PSScriptRoot "frontend")
|
$PSScriptRoot
|
||||||
if ($LASTEXITCODE -ne 0) { throw "Frontend build mislukt" }
|
if ($LASTEXITCODE -ne 0) { throw "Build mislukt" }
|
||||||
Write-Host "Gepusht: $frontendVersionDev" -ForegroundColor Green
|
Write-Host "Gepusht: $appVersionDev" -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 }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Acties
|
# Acties
|
||||||
@@ -129,7 +110,6 @@ switch ($Action) {
|
|||||||
|
|
||||||
"build" {
|
"build" {
|
||||||
Write-Host "-- Lokaal bouwen -------------------------" -ForegroundColor Cyan
|
Write-Host "-- Lokaal bouwen -------------------------" -ForegroundColor Cyan
|
||||||
$services = @(if ($Target -ne "both") { $Target })
|
|
||||||
docker compose --progress plain -f $devCompose build @services
|
docker compose --progress plain -f $devCompose build @services
|
||||||
if ($LASTEXITCODE -ne 0) { throw "Build mislukt" }
|
if ($LASTEXITCODE -ne 0) { throw "Build mislukt" }
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
@@ -137,8 +117,9 @@ switch ($Action) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
"up" {
|
"up" {
|
||||||
if ($Target -eq "both") {
|
if ($Target -eq "all") {
|
||||||
docker compose -f $devCompose down -v
|
# 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
|
docker compose --progress plain -f $devCompose up --build -d
|
||||||
} else {
|
} else {
|
||||||
docker compose -f $devCompose stop $Target
|
docker compose -f $devCompose stop $Target
|
||||||
@@ -147,6 +128,8 @@ switch ($Action) {
|
|||||||
if ($LASTEXITCODE -ne 0) { throw "Opstarten mislukt" }
|
if ($LASTEXITCODE -ne 0) { throw "Opstarten mislukt" }
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host "$appName gestart - $appVersion [$Target]" -ForegroundColor Green
|
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" {
|
"down" {
|
||||||
@@ -155,19 +138,23 @@ switch ($Action) {
|
|||||||
Write-Host "$appName gestopt" -ForegroundColor Yellow
|
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" {
|
"push" {
|
||||||
Invoke-Push
|
Invoke-AppPush
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host "========================================" -ForegroundColor Cyan
|
Write-Host "========================================" -ForegroundColor Cyan
|
||||||
Write-Host " Dev images gepusht: $appVersion [$Target]" -ForegroundColor Green
|
Write-Host " Dev image gepusht: $appVersion" -ForegroundColor Green
|
||||||
if ($Target -eq "both" -or $Target -eq "backend") {
|
Write-Host " $appVersionDev" -ForegroundColor White
|
||||||
Write-Host " $backendVersionDev" -ForegroundColor White
|
Write-Host " $appLatestDev" -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 "========================================" -ForegroundColor Cyan
|
Write-Host "========================================" -ForegroundColor Cyan
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,24 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# ── VibeFinance dev wrapper (macOS/Linux) ────────────────────────────────
|
# ── 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:
|
# Gebruik:
|
||||||
# ./dev.sh # build + herstart containers
|
# ./dev.sh # build + herstart containers
|
||||||
# ./dev.sh build # alleen lokaal bouwen
|
# ./dev.sh build # alleen lokaal bouwen
|
||||||
# ./dev.sh build -Target backend # alleen backend bouwen
|
# ./dev.sh up # build + herstart containers
|
||||||
# ./dev.sh build -Target frontend # alleen frontend bouwen
|
# ./dev.sh up -Target app # alleen de app herbouwen + herstarten
|
||||||
# ./dev.sh up # build lokaal + herstart containers
|
# ./dev.sh up -Target postgres # alleen de database herstarten
|
||||||
# ./dev.sh up -Target backend # alleen backend bouwen + herstart
|
# ./dev.sh down # containers stoppen en volumes wissen
|
||||||
# ./dev.sh up -Target frontend # alleen frontend bouwen + herstart
|
# ./dev.sh logs # logs van de app volgen
|
||||||
# ./dev.sh down # containers stoppen
|
# ./dev.sh test # testsuite draaien
|
||||||
# ./dev.sh push # multi-platform build + push
|
# ./dev.sh push # multi-platform build + push
|
||||||
# ./dev.sh push -Target backend # alleen backend pushen
|
|
||||||
# ./dev.sh push -Target frontend # alleen frontend pushen
|
|
||||||
#
|
#
|
||||||
# Vereisten:
|
# Vereisten:
|
||||||
# - pwsh (PowerShell Core) geïnstalleerd: brew install powershell
|
# - 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': 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
|
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
|
## 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:
|
services:
|
||||||
|
|
||||||
@@ -18,72 +24,51 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- dev
|
- dev
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready -U vibefinance"]
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-vibefinance}"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
|
|
||||||
frontend:
|
app:
|
||||||
build:
|
build:
|
||||||
context: ./frontend
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
target: development
|
target: development
|
||||||
args:
|
args:
|
||||||
VITE_APP_VERSION: ${VITE_APP_VERSION:-dev}
|
VITE_APP_VERSION: ${VITE_APP_VERSION:-dev}
|
||||||
image: vibefinance/frontend:${VERSION:-latest-dev}
|
image: vibefinance/app:${VERSION:-latest-dev}
|
||||||
container_name: vibefinance-frontend-dev
|
container_name: vibefinance-app-dev
|
||||||
ports:
|
ports:
|
||||||
- "80:5173"
|
- "80:5173" # Vite dev-server (de app in de browser)
|
||||||
volumes:
|
- "3001:3001" # API, handig voor curl/Postman
|
||||||
- ./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"
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./backend:/app
|
- ./backend:/app
|
||||||
- /app/node_modules
|
- /app/node_modules
|
||||||
|
- ./frontend:/app/frontend
|
||||||
|
- /app/frontend/node_modules
|
||||||
environment:
|
environment:
|
||||||
NODE_ENV: development
|
NODE_ENV: development
|
||||||
PORT: 3001
|
PORT: 3001
|
||||||
JWT_SECRET: dev-secret-change-in-production
|
JWT_SECRET: dev-secret-change-in-production
|
||||||
JWT_EXPIRES: 7d
|
JWT_EXPIRES: 7d
|
||||||
CORS_ORIGIN: http://localhost
|
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}
|
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:
|
networks:
|
||||||
- dev
|
- dev
|
||||||
depends_on:
|
depends_on:
|
||||||
postgres:
|
postgres:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
healthcheck:
|
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
|
interval: 15s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
start_period: 15s
|
start_period: 25s
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres_dev_data:
|
postgres_dev_data:
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
dev:
|
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.
|
# Zie README.md § Quick Start voor volledige instructies.
|
||||||
#
|
#
|
||||||
# Stappen:
|
# 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
|
# 2. docker compose pull
|
||||||
# 3. docker compose up -d
|
# 3. docker compose up -d
|
||||||
|
|
||||||
@@ -20,46 +23,39 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- vibefinance_pgdata:/var/lib/postgresql
|
- vibefinance_pgdata:/var/lib/postgresql
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready -U vibefinance"]
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-vibefinance}"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
networks:
|
networks:
|
||||||
- vibefinance_net
|
- vibefinance_net
|
||||||
|
|
||||||
backend:
|
app:
|
||||||
image: vibehoogie.duckdns.org/vibe/vibefinance-backend:latest
|
image: vibehoogie.duckdns.org/vibe/vibefinance-app:latest
|
||||||
container_name: vibefinance_backend
|
container_name: vibefinance_app
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
# Variabelen uit .env (JWT_SECRET, POSTGRES_PASSWORD, ...)
|
||||||
env_file: .env
|
env_file: .env
|
||||||
environment:
|
environment:
|
||||||
|
NODE_ENV: production
|
||||||
|
PORT: 3001
|
||||||
DATABASE_URL: postgresql://${POSTGRES_USER:-vibefinance}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-vibefinance}
|
DATABASE_URL: postgresql://${POSTGRES_USER:-vibefinance}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-vibefinance}
|
||||||
# Bron voor de update-melding in de app (leeg = melding uit)
|
# 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}
|
UPDATE_CHECK_URL: ${UPDATE_CHECK_URL:-https://vibehoogie.duckdns.org/api/v1/repos/vibe/VibeFinance/releases/latest}
|
||||||
|
ports:
|
||||||
|
- "${APP_PORT:-3400}:3001"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "wget -qO- http://localhost:3001/api/health | grep -q status || exit 1"]
|
test: ["CMD-SHELL", "wget -qO- http://localhost:3001/api/health | grep -q status || exit 1"]
|
||||||
interval: 10s
|
interval: 30s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 10
|
retries: 5
|
||||||
start_period: 30s
|
start_period: 20s
|
||||||
depends_on:
|
depends_on:
|
||||||
postgres:
|
postgres:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
networks:
|
networks:
|
||||||
- vibefinance_net
|
- 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:
|
volumes:
|
||||||
vibefinance_pgdata:
|
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",
|
"name": "vibefinance-frontend",
|
||||||
"version": "0.1.5-dev",
|
"version": "0.1.6-dev",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ export default defineConfig({
|
|||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
server: {
|
server: {
|
||||||
port: 5173,
|
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: {
|
proxy: {
|
||||||
"/api": {
|
"/api": {
|
||||||
target: "http://backend:3001",
|
target: "http://localhost:3001",
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
secure: false,
|
secure: false,
|
||||||
},
|
},
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "vibefinance",
|
"name": "vibefinance",
|
||||||
"version": "0.1.5-dev",
|
"version": "0.1.6-dev",
|
||||||
"description": "VibeFinance — persoonlijk vermogensbeheer",
|
"description": "VibeFinance — persoonlijk vermogensbeheer",
|
||||||
"private": true,
|
"private": true,
|
||||||
"repository": "https://vibehoogie.duckdns.org/vibe/VibeFinance",
|
"repository": "https://vibehoogie.duckdns.org/vibe/VibeFinance",
|
||||||
|
|||||||
+72
-78
@@ -1,24 +1,27 @@
|
|||||||
# ── VibeFinance release script (PowerShell) ──────────────────────────────
|
# ── VibeFinance release script (PowerShell) ──────────────────────────────
|
||||||
# Productie release: bouwt multi-platform, pusht naar registry,
|
# Productie release voor de 2-container opzet: draait de tests, bouwt het
|
||||||
# maakt een Git-tag aan en pusht naar origin.
|
# gecombineerde app-image multi-platform, pusht naar de registry, maakt een
|
||||||
|
# Git-tag en publiceert de release in Gitea.
|
||||||
#
|
#
|
||||||
# Gebruik:
|
# Gebruik:
|
||||||
# .\release.ps1 # bouw + push images + Git-tag
|
# .\release.ps1 # test + bouw + push + Git-tag + Gitea-release
|
||||||
# .\release.ps1 -NoBuild # sla docker build over, alleen Git-tag
|
# .\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 -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:
|
# Vereisten:
|
||||||
# - Docker Desktop actief
|
# - Docker Desktop actief
|
||||||
# - Ingelogd op de registry: docker login <registry>
|
# - Ingelogd op de registry: docker login <registry>
|
||||||
# - docker buildx builder actief: docker buildx create --use
|
# - 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
|
# - 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
|
# de Gitea-release waar de update-melding in de app op afgaat
|
||||||
|
|
||||||
param(
|
param(
|
||||||
[switch]$NoBuild,
|
[switch]$NoBuild,
|
||||||
|
[switch]$SkipTests,
|
||||||
[switch]$DryRun,
|
[switch]$DryRun,
|
||||||
[switch]$Force
|
[switch]$Force
|
||||||
)
|
)
|
||||||
@@ -28,11 +31,11 @@ $ErrorActionPreference = "Stop"
|
|||||||
# ════════════════════════════════════════════════════════════════════════════
|
# ════════════════════════════════════════════════════════════════════════════
|
||||||
# ── PROJECTCONFIGURATIE — pas alleen dit blok aan voor een nieuw project ──
|
# ── PROJECTCONFIGURATIE — pas alleen dit blok aan voor een nieuw project ──
|
||||||
# ════════════════════════════════════════════════════════════════════════════
|
# ════════════════════════════════════════════════════════════════════════════
|
||||||
$appName = "VibeFinance"
|
$appName = "VibeFinance"
|
||||||
$registry = "vibehoogie.duckdns.org/vibe"
|
$registry = "vibehoogie.duckdns.org/vibe"
|
||||||
$backendImage = "vibefinance-backend"
|
$appImage = "vibefinance-app"
|
||||||
$frontendImage = "vibefinance-frontend"
|
$apiUrl = "/api"
|
||||||
$apiUrl = "/api"
|
$platforms = "linux/amd64,linux/arm64" # arm64 = Raspberry Pi
|
||||||
# ════════════════════════════════════════════════════════════════════════════
|
# ════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
# ── Versie uit root package.json ───────────────────────────────────────────
|
# ── Versie uit root package.json ───────────────────────────────────────────
|
||||||
@@ -60,14 +63,10 @@ if ($frontendPkg.version -ne $appVersion) {
|
|||||||
|
|
||||||
$gitTag = "$appVersion"
|
$gitTag = "$appVersion"
|
||||||
|
|
||||||
# ── Registry tags ──────────────────────────────────────────────────────────
|
# ── Registry tags (één image voor frontend + backend) ─────────────────────
|
||||||
$backendBase = "$registry/$backendImage"
|
$appBase = "$registry/$appImage"
|
||||||
$frontendBase = "$registry/$frontendImage"
|
$appVersionTag = "$appBase`:$appVersion"
|
||||||
|
$appLatestTag = "$appBase`:latest"
|
||||||
$backendVersionTag = "$backendBase`:$appVersion"
|
|
||||||
$frontendVersionTag = "$frontendBase`:$appVersion"
|
|
||||||
$backendLatestTag = "$backendBase`:latest"
|
|
||||||
$frontendLatestTag = "$frontendBase`:latest"
|
|
||||||
|
|
||||||
# ── Helpers ────────────────────────────────────────────────────────────────
|
# ── Helpers ────────────────────────────────────────────────────────────────
|
||||||
function Invoke-Cmd {
|
function Invoke-Cmd {
|
||||||
@@ -116,14 +115,16 @@ Write-Host "══════════════════════
|
|||||||
Write-Host " $appName Release" -ForegroundColor Cyan
|
Write-Host " $appName Release" -ForegroundColor Cyan
|
||||||
Write-Host " Versie : $appVersion" -ForegroundColor Green
|
Write-Host " Versie : $appVersion" -ForegroundColor Green
|
||||||
Write-Host " Git-tag : $gitTag" -ForegroundColor Green
|
Write-Host " Git-tag : $gitTag" -ForegroundColor Green
|
||||||
Write-Host " Registry: $registry" -ForegroundColor Green
|
Write-Host " Image : $appVersionTag" -ForegroundColor Green
|
||||||
if ($DryRun) { Write-Host " Mode : DRY-RUN (geen wijzigingen)" -ForegroundColor Yellow }
|
Write-Host " Platform: $platforms" -ForegroundColor Green
|
||||||
if ($NoBuild) { Write-Host " Mode : SKIP BUILD" -ForegroundColor Yellow }
|
if ($DryRun) { Write-Host " Mode : DRY-RUN (geen wijzigingen)" -ForegroundColor Yellow }
|
||||||
if ($Force) { Write-Host " Mode : FORCE (conflictcontrole overgeslagen)" -ForegroundColor Red }
|
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
|
Write-Host "════════════════════════════════════════" -ForegroundColor Cyan
|
||||||
|
|
||||||
# ── Stap 1: Conflictcontrole ──────────────────────────────────────────────
|
# ── Stap 1: Conflictcontrole ──────────────────────────────────────────────
|
||||||
Write-Step "[1/4] Conflictcontrole"
|
Write-Step "[1/5] Conflictcontrole"
|
||||||
|
|
||||||
if (-not $DryRun -and -not $Force) {
|
if (-not $DryRun -and -not $Force) {
|
||||||
$localTag = git tag --list $gitTag
|
$localTag = git tag --list $gitTag
|
||||||
@@ -138,23 +139,13 @@ if (-not $DryRun -and -not $Force) {
|
|||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Host "Controleer registry: $backendVersionTag" -ForegroundColor DarkGray
|
Write-Host "Controleer registry: $appVersionTag" -ForegroundColor DarkGray
|
||||||
$ErrorActionPreference = "SilentlyContinue"
|
$ErrorActionPreference = "SilentlyContinue"
|
||||||
docker manifest inspect $backendVersionTag 2>$null
|
docker manifest inspect $appVersionTag 2>$null | Out-Null
|
||||||
$backendExists = $LASTEXITCODE -eq 0
|
$imageExists = $LASTEXITCODE -eq 0
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
if ($backendExists) {
|
if ($imageExists) {
|
||||||
Write-Error "Registry-tag '$backendVersionTag' bestaat al. Verhoog het versienummer in package.json of gebruik -Force."
|
Write-Error "Registry-tag '$appVersionTag' 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."
|
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,38 +156,44 @@ if (-not $DryRun -and -not $Force) {
|
|||||||
Write-Host "[DRY-RUN] Conflictcontrole overgeslagen" -ForegroundColor Yellow
|
Write-Host "[DRY-RUN] Conflictcontrole overgeslagen" -ForegroundColor Yellow
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Stap 2: Bouwen en pushen ──────────────────────────────────────────────
|
# ── Stap 2: Tests ─────────────────────────────────────────────────────────
|
||||||
if (-not $NoBuild) {
|
if (-not $SkipTests) {
|
||||||
Write-Step "[2/4] Multi-platform bouwen en pushen"
|
Write-Step "[2/5] Testsuite"
|
||||||
|
if ($DryRun) {
|
||||||
Write-Host "Backend bouwen en pushen..." -ForegroundColor White
|
Write-Host "[DRY-RUN] test.ps1 -NoSmoke" -ForegroundColor Yellow
|
||||||
Invoke-Cmd @(
|
} else {
|
||||||
"docker", "buildx", "build",
|
& (Join-Path $PSScriptRoot "test.ps1") -NoSmoke
|
||||||
"--platform", "linux/amd64,linux/arm64",
|
if ($LASTEXITCODE -ne 0) {
|
||||||
"--push",
|
Write-Error "Tests mislukt — release afgebroken."
|
||||||
"-t", $backendVersionTag,
|
exit 1
|
||||||
"-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")
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
Write-Step "[2/4] Bouwen overgeslagen (-NoBuild)"
|
Write-Step "[2/5] Tests overgeslagen (-SkipTests)"
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Stap 3: Git-tag aanmaken en pushen ────────────────────────────────────
|
# ── Stap 3: Bouwen en pushen ──────────────────────────────────────────────
|
||||||
Write-Step "[3/4] Git-tag aanmaken 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) {
|
if ($Force) {
|
||||||
Write-Host "Bestaande lokale tag verwijderen (indien aanwezig)..." -ForegroundColor Yellow
|
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
|
Write-Host "Git-tag pushen naar origin" -ForegroundColor White
|
||||||
Invoke-Cmd @("git", "push", "origin", "refs/tags/$gitTag")
|
Invoke-Cmd @("git", "push", "origin", "refs/tags/$gitTag")
|
||||||
|
|
||||||
# ── Stap 4: Gitea-release aanmaken ────────────────────────────────────────
|
# ── Stap 5: Gitea-release aanmaken ────────────────────────────────────────
|
||||||
Write-Step "[4/4] Gitea-release aanmaken"
|
Write-Step "[5/5] Gitea-release aanmaken"
|
||||||
|
|
||||||
# De update-melding in de app leest /releases/latest. Een kale Git-tag komt
|
# 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.
|
# daar niet in terug, dus er moet een echte release van gemaakt worden.
|
||||||
@@ -232,7 +229,6 @@ if (-not $releaseApi) {
|
|||||||
} elseif (-not $giteaToken) {
|
} elseif (-not $giteaToken) {
|
||||||
Write-Host "Geen GITEA_TOKEN in de omgeving — release overgeslagen." -ForegroundColor Yellow
|
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 " 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 {
|
} else {
|
||||||
$notes = Get-ChangelogSection $appVersion
|
$notes = Get-ChangelogSection $appVersion
|
||||||
if (-not $notes) { $notes = "Release $appVersion" }
|
if (-not $notes) { $notes = "Release $appVersion" }
|
||||||
@@ -260,7 +256,7 @@ if (-not $releaseApi) {
|
|||||||
Write-Host "Release voor $gitTag bestaat al — overgeslagen." -ForegroundColor Yellow
|
Write-Host "Release voor $gitTag bestaat al — overgeslagen." -ForegroundColor Yellow
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Release aanmaken mislukt ($code): $($_.Exception.Message)" -ForegroundColor Red
|
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 "════════════════════════════════════════" -ForegroundColor Cyan
|
||||||
Write-Host " Gepubliceerd: $appVersion" -ForegroundColor Green
|
Write-Host " Gepubliceerd: $appVersion" -ForegroundColor Green
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host " Registry images:" -ForegroundColor Green
|
Write-Host " Registry image:" -ForegroundColor Green
|
||||||
Write-Host " $backendVersionTag" -ForegroundColor White
|
Write-Host " $appVersionTag" -ForegroundColor White
|
||||||
Write-Host " $backendLatestTag" -ForegroundColor White
|
Write-Host " $appLatestTag" -ForegroundColor White
|
||||||
Write-Host " $frontendVersionTag" -ForegroundColor White
|
|
||||||
Write-Host " $frontendLatestTag" -ForegroundColor White
|
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host " Git-tag: $gitTag -> origin" -ForegroundColor Green
|
Write-Host " Git-tag: $gitTag -> origin" -ForegroundColor Green
|
||||||
Write-Host "════════════════════════════════════════" -ForegroundColor Cyan
|
Write-Host "════════════════════════════════════════" -ForegroundColor Cyan
|
||||||
|
|||||||
+8
-7
@@ -1,12 +1,14 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# ── VibeFinance release wrapper (macOS/Linux) ────────────────────────────
|
# ── 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:
|
# Gebruik:
|
||||||
# ./release.sh # bouw + push images + Git-tag
|
# ./release.sh # test + bouw + push + Git-tag + Gitea-release
|
||||||
# ./release.sh -NoBuild # sla docker build over, alleen Git-tag
|
# ./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 -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:
|
# Combinaties:
|
||||||
# ./release.sh -Force -NoBuild
|
# ./release.sh -Force -NoBuild
|
||||||
@@ -14,9 +16,9 @@
|
|||||||
#
|
#
|
||||||
# Vereisten:
|
# Vereisten:
|
||||||
# - pwsh (PowerShell Core) geïnstalleerd: brew install powershell
|
# - pwsh (PowerShell Core) geïnstalleerd: brew install powershell
|
||||||
# - Docker Desktop actief
|
# - Docker actief
|
||||||
# - docker buildx builder actief: docker buildx create --use
|
# - 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
|
# - Git geïnstalleerd
|
||||||
# - root package.json bevat versienummer ZONDER -dev suffix
|
# - root package.json bevat versienummer ZONDER -dev suffix
|
||||||
# - GITEA_TOKEN in de omgeving (token met release-rechten) — nodig voor het
|
# - 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
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Geef alle argumenten door aan het .ps1 script
|
|
||||||
pwsh -NoProfile -ExecutionPolicy Bypass \
|
pwsh -NoProfile -ExecutionPolicy Bypass \
|
||||||
-File "$SCRIPT_DIR/release.ps1" \
|
-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