Geen omschrijving
- HTML 48.9%
- JavaScript 31.1%
- CSS 7.9%
- Dockerfile 7.1%
- Shell 5%
Bug ontdekt tijdens visuele smoke-test: Alpine startte bij DOMContentLoaded en zag `x-data="portaal()"` maar `window.portaal` was nog niet gedefinieerd omdat alpine.min.js met defer VÓÓR app.js executeerde in document-volgorde. Resultaat: cascade van "portaal is not defined"-errors + lege pagina. Fix: app.js komt nu EERST in de defer-queue, alpine.min.js daarna. Alpine ziet `window.portaal` gedefinieerd zodra 'ie start. |
||
|---|---|---|
| src | ||
| .env.example | ||
| .gitignore | ||
| Dockerfile | ||
| entrypoint.sh | ||
| nginx.conf | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| tailwind.config.js | ||
Memoriaal — Klantportaal
Externe SPA voor klanten van Memoriaal Administratiebureau & Belastingadviseurs.
Communiceert alleen via HTTPS/JSON met de DMS-backend
(/portaal-api/v1/...). Geen eigen database, geen server-side render — puur
statische bestanden geserveerd door Nginx (unprivileged).
Deploy op Coolify
- Coolify dashboard → + New Resource → Public / Private Git Repository
- Repo-URL:
https://code.securityasaservice.org/MemAdmBur/memoriaal-klankportaal.git - Build pack:
Dockerfile - Port exposed:
8080(was80— nginx-unprivileged luistert op 8080) - Environment variables:
API_BASE_URL=https://testdms.memoriaal.com/portaal-api/v1 - Domains:
klant.memoriaal.com(Coolify Traefik regelt TLS) - DMS-kant moet
PORTAAL_URL=https://klant.memoriaal.comgezet hebben — de CORS/CSP-headers gaan dan automatisch open.
Lokale test
docker build -t klantportaal .
docker run --rm -p 8080:8080 \
-e API_BASE_URL=https://testdms.memoriaal.com/portaal-api/v1 \
klantportaal
# open http://localhost:8080
Voor dev tegen een lokale/andere backend die niet op *.memoriaal.com staat:
docker run --rm -p 8080:8080 \
-e API_BASE_URL=http://host.docker.internal:8000/portaal-api/v1 \
-e PORTAAL_API_ALLOW_ANY=1 \
klantportaal
Bouwlogica
- Multi-stage Dockerfile:
- Stage 1 (
node:22-alpine):npm ci(integrity-check via lockfile), Tailwind compileertsrc/css/input.css → dist/app.css. - Stage 2 (
nginxinc/nginx-unprivileged:alpine): kopieert de gebouwde CSS, Alpine uitnode_modules, HTML, JS, images. Draait als UID 101 op poort 8080.
- Stage 1 (
index.htmlis de shell (Tailwind-classes, geen inline styles/scripts).src/js/app.jsbevat alle Alpine-logic.entrypoint.shvalideertAPI_BASE_URL(moethttps://*.memoriaal.com/...ofPORTAAL_API_ALLOW_ANY=1), substitueert__API_BASE_URL__inapp.jsen${API_HOST}in de CSP van nginx.
Beveiliging
- Sessie: HttpOnly + Secure +
SameSite=Nonecookie (portaal_session, 12u TTL). JS ziet het token niet — XSS kan het niet exfiltreren. - CSRF: elke unsafe request stuurt
X-Portaal-CSRF: 1; backend eist die header wanneer de sessie via cookie komt. Cross-site forms zonder preflight-goedkeuring worden geweigerd. - CSP:
script-src 'self' 'unsafe-eval'(unsafe-evalnodig voor Alpine's expression-evaluator),connect-src 'self' <API-host>(host wordt dynamisch geset uitAPI_BASE_URL). Geen externe CDN's meer. - HSTS:
max-age=63072000; includeSubDomains. - Frame-Options:
DENY— SPA hoort nooit in een iframe. - Nginx-container: draait als niet-root user (UID 101), poort 8080.
- Dependencies: Tailwind en Alpine gepind via
package-lock.json+ gebundeld in de image (geen SRI-onderhoud nodig, geen CDN-compromise- risico). - Aanval-oppervlak: alleen de DMS
/portaal-api/. Zie DMSv2-repobackend/klantportaal/voor rate-limits, watermarking, en klant-scoping.