No description
  • JavaScript 90.6%
  • CSS 6.3%
  • HTML 2.1%
  • Dockerfile 1%
Find a file
Jesse van Mullem a2c6b0c441 Schoon repo op voor white-label uitrol
Maak settings.example.json neutraal en voeg een .gitignore toe zodat
per-klant config (settings.json met wachtwoordhash en MFA-secret),
cache en node_modules niet meer worden meegecommit. De /-  en
/voorraad-route vullen de sitenaam dynamisch in. Verwijder ongebruikte
legacy-bestanden en de verouderde WordPress-plugin die naar een dode
backend wezen; de WordPress-embed loopt volledig via /embed.js.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 16:58:57 +02:00
config Schoon repo op voor white-label uitrol 2026-05-18 16:58:57 +02:00
public Schoon repo op voor white-label uitrol 2026-05-18 16:58:57 +02:00
.gitignore Schoon repo op voor white-label uitrol 2026-05-18 16:58:57 +02:00
docker-compose.yaml change docker compose extension 2026-05-13 12:45:28 +02:00
Dockerfile make project ready for coolify 2026-05-13 12:37:13 +02:00
nixpacks.toml make project ready for coolify 2026-05-13 12:37:13 +02:00
package-lock.json push all for backup 2026-05-13 11:50:22 +02:00
package.json make project ready for coolify 2026-05-13 12:37:13 +02:00
README.en.md Schoon repo op voor white-label uitrol 2026-05-18 16:58:57 +02:00
README.md Schoon repo op voor white-label uitrol 2026-05-18 16:58:57 +02:00
server.js Schoon repo op voor white-label uitrol 2026-05-18 16:58:57 +02:00

Voorraadportaal v21

Nederlandse versie: README.md

White-label inventory portal for a bicycle shop or another organization using a Fietsenwijk source. The app fetches inventory from Fietsenwijk, enriches the data, caches the result, and exposes it through a public frontend, a JSON/XML API, a WordPress embed, and a secured admin environment.

Developer: Jesse van Mullem Company: Je-Ma ICT Beheer


Table Of Contents


In Short

  • One deployment is intended for one organization.
  • The source is a Fietsenwijk installation with separate paths for used and new bikes.
  • The app runs as a Node/Express server on port 3000 (Node 20 or higher).
  • Runtime settings are stored in config/settings.json.
  • Cache and admin logs are stored in cache/.
  • In Coolify, /app/config and /app/cache must be persistent.
  • The admin login is available at /admin/login, not /admin.
  • The admin portal has a redesigned interface with dashboard, configuration, cache, items, and security management.

How The App Works

  1. On startup, the app reads config/settings.json.
  2. If that file does not exist yet, the app creates a default configuration.
  3. The app fetches used bikes from baseUrl + usedPath.
  4. The app fetches new bikes from baseUrl + newPath.
  5. List pages provide item IDs and titles.
  6. Detail pages provide prices, images, and specifications.
  7. Results are cached in memory and on disk.
  8. The API, frontend, and WordPress embed read from that cache.
  9. The admin portal lets you manage configuration, cache, security, and hidden items.

Important Files

File Purpose
server.js Express server: scraping, parsing, API, frontend routes, and admin portal.
public/ Public frontend files.
config/settings.example.json Example configuration.
config/settings.json Runtime configuration, created automatically on first start.
cache/fietsen.json Disk cache for inventory data.
cache/admin-log.json Audit log of admin actions.
Dockerfile Production container.
docker-compose.yaml Recommended Coolify Compose stack.
nixpacks.toml Fallback for Nixpacks deployments.
.env.example Example environment variables.

Run Locally

Install dependencies and start the server:

npm install
npm start

The app will run at:

http://localhost:3000

Useful local URLs:

http://localhost:3000/
http://localhost:3000/voorraad
http://localhost:3000/api/health
http://localhost:3000/admin/login

Locally the app runs without HTTPS by default. The session cookie only receives the Secure attribute when NODE_ENV=production is set, so do not set that variable locally.

Environment Variables

The app supports these variables:

Variable Default Description
NODE_ENV none Set to production in production. This makes the session cookie Secure automatically.
HOST 0.0.0.0 Server bind address. For Docker/Coolify this must be 0.0.0.0.
PORT 3000 Internal port used by Express.
SESSION_SECRET dev fallback Secret for admin sessions. You must set this yourself in production.
CONFIG_DIR ./config Directory containing settings.json.
CACHE_DIR ./cache Directory for cache and admin logs.

Always use a long, random SESSION_SECRET in production, for example:

openssl rand -base64 32

If the app runs in production without its own SESSION_SECRET, a warning appears in the logs.

Coolify Deployment

Recommended setup: Docker Compose with docker-compose.yaml.

Step By Step

  1. Push the repository to GitHub/GitLab.
  2. Create a new project in Coolify.
  3. Choose New Resource and select the Git repository.
  4. Choose Docker Compose as the deployment type.
  5. Use this compose file:
docker-compose.yaml
  1. Set these environment variables in Coolify:
NODE_ENV=production
SESSION_SECRET=put-a-long-random-secret-here
  1. Attach your domain to the voorraadportaal service on container port 3000.
  2. Use this health check path:
/api/health
  1. Deploy.

After deployment, test:

https://yourdomain.com/api/health
https://yourdomain.com/admin/login

Persistent Storage

The Compose stack uses volumes for:

/app/config
/app/cache

Why this matters:

  • /app/config stores admin settings, password hash, MFA secret, Fietsenwijk URL, and hidden IDs.
  • /app/cache stores the inventory cache and admin log.
  • Without persistent storage, this data is lost after a redeploy.

Dockerfile Without Compose

You can also use the Dockerfile build pack.

Coolify settings:

  • Port exposes: 3000
  • Health check path: /api/health
  • Persistent storage: /app/config and /app/cache
  • Environment variables:
NODE_ENV=production
HOST=0.0.0.0
PORT=3000
SESSION_SECRET=put-a-long-random-secret-here

Nixpacks

nixpacks.toml starts the app with:

node server.js

And installs dependencies with:

npm install --omit=dev --no-audit --no-fund

Nixpacks deployments also need persistent storage for /app/config and /app/cache.

First Admin Login

After the first start, the app creates a default admin account:

Username: admin
Password: change-me-now

Log in via:

/admin/login

As long as the default password is still active, the login page shows a hint with these credentials and a warning appears in the logs. Once you change the password, those messages disappear automatically.

After the first login, immediately go to:

/admin/security

Change:

  • the admin password (minimum 10 characters, with confirmation);
  • the MFA settings (disabled by default, see Security);
  • the IP allowlist, if needed.

Reset The Admin Account

If you can still log in, change the password via /admin/security.

If you can no longer log in, open the container terminal in Coolify and run:

node -e "const fs=require('fs');const crypto=require('crypto');const file='/app/config/settings.json';const c=JSON.parse(fs.readFileSync(file,'utf8'));const password='NewStrongPassword123!';const salt=crypto.randomBytes(16).toString('hex');c.admin=c.admin||{};c.admin.username='admin';c.admin.passwordSalt=salt;c.admin.passwordHash=crypto.pbkdf2Sync(password,salt,100000,64,'sha512').toString('hex');c.admin.mfaEnabled=false;c.security=c.security||{};c.security.allowlistEnabled=false;fs.writeFileSync(file,JSON.stringify(c,null,2));console.log('Admin reset: admin / '+password);"

This command also disables MFA and the IP allowlist so you are guaranteed to get back in. Then restart the container. The login will be:

Username: admin
Password: NewStrongPassword123!

Change the password again in /admin/security afterward.

The Admin Portal

The admin portal has a redesigned interface with a fixed sidebar, a dashboard with statistic tiles, and instant confirmation messages after every action.

Admin Routes

Route Purpose
/admin/login Log in.
/admin/mfa MFA verification when MFA is enabled.
/admin/dashboard Status, inventory statistics, quick actions, and audit log.
/admin/config Edit organization configuration.
/admin/cache View, refresh, or clear cache.
/admin/items View cached items and check hidden IDs.
/admin/security Manage password, MFA, and IP allowlist.
/admin/logout Log out.

Note: /admin itself is not a route. Use /admin/login.

Pages

  • Dashboard tiles with counts (total, used, new, cache age), a status overview with badges for IP allowlist and MFA, quick actions, and the last twelve audit log entries.
  • Configuration a Huisstijl (branding) card (site name, logo, accent color) and a Bron en cache (source and cache) card (source URLs, cache duration, and hidden item IDs).
  • Cache overview of the cache file and the last refresh, plus buttons to refresh or clear.
  • Items a table of the first 60 scraped items and the list of hidden IDs.
  • Security change the password, enable/disable MFA, and manage the IP allowlist.

Organization Configuration

The most important fields in config/settings.json:

Field Description
siteName Name shown in the app/admin.
logoUrl Optional logo URL for the login page and admin sidebar. Empty = default icon.
accentColor Accent color (hex) for the portal's visual style. Empty = default dark blue.
baseUrl Base URL of the Fietsenwijk installation.
usedPath Path for used bikes, default /fietsen/?cat=1.
newPath Path for new bikes, default /fietsen/?cat=2.
apiBaseUrl Public API URL, useful for embeds/config.
returnUrl URL detail pages link back to.
cacheTtlMs Cache duration in milliseconds.
hiddenIds Item IDs that should be hidden.
admin Admin username, password hash, salt, MFA secret, and MFA status.
security IP allowlist settings.

Example:

{
  "siteName": "Voorraadportaal",
  "logoUrl": "",
  "accentColor": "",
  "baseUrl": "https://example.hst.fietsenwijk.nl",
  "usedPath": "/fietsen/?cat=1",
  "newPath": "/fietsen/?cat=2",
  "apiBaseUrl": "https://api.example.com",
  "returnUrl": "https://example.com/inventory/",
  "cacheTtlMs": 1800000,
  "hiddenIds": []
}

Usually you edit this through /admin/config.

White-Label And Branding

The portal can be white-labeled per customer, entirely through /admin/config in the Huisstijl (branding) card:

  • Site name shown on the login page, in the admin sidebar, and in page titles.
  • Accent color a hex value (for example #0a7d2c) that sets the color of the sidebar, buttons, focus rings, and the login background. Invalid values fall back to the default dark blue.
  • Logo URL an optional http(s) URL to a logo that replaces the default shield icon on the login page and in the sidebar.

A fresh deployment starts with a neutral configuration: baseUrl, apiBaseUrl, and returnUrl are empty. You fill everything in per customer through the admin portal, so the same codebase works for multiple organizations without code changes.

Cache

The app uses two cache layers:

Cache Location Behavior
Memory cache In the Node process Fast, disappears on restart.
Disk cache cache/fietsen.json Persists if /app/cache is persistent.

Default cache duration: 30 minutes.

Manually refresh cache:

/api/fietsen?refresh=1
/api/refresh

Manage cache through admin:

/admin/cache

Public Routes And API

Frontend:

/
/voorraad
/fiets/:id

API:

/api/fietsen
/api/fietsen?refresh=1
/api/fietsen.xml
/api/fietsen.xml?refresh=1
/api/refresh
/api/health

Media and embed:

/image/:id
/embed.js

/api/fietsen returns JSON:

{
  "success": true,
  "count": 0,
  "fietsen": [],
  "laatsteUpdate": "2026-05-13T00:00:00.000Z",
  "source": {
    "used": "https://example.hst.fietsenwijk.nl/fietsen/?cat=1",
    "new": "https://example.hst.fietsenwijk.nl/fietsen/?cat=2"
  }
}

A bike item includes, among other fields, id, title, state, stateLabel, price, image, rawImage, url, detailUrl, and specification fields such as kleur, maat, wielmaat, and modeljaar.

/api/health returns status information about cache, source configuration, and the last refresh error if one exists.

WordPress Embed

Simple embed:

<div id="bvb-voorraad"></div>
<script src="https://api.yourdomain.com/embed.js"></script>

Embed with type filter:

<div class="busvolbikes-voorraad" data-type="used"></div>
<script src="https://api.yourdomain.com/embed.js"></script>

Possible data-type values: all, used, new.

The embed fetches data from /api/fietsen on the same server that serves embed.js; no separate plugin or configuration is needed.

Connect A New Organization

  1. Create a new Coolify resource/deployment.
  2. Set NODE_ENV=production and your own SESSION_SECRET.
  3. Make sure /app/config and /app/cache are persistent.
  4. Deploy the app.
  5. Log in at /admin/login.
  6. Go to /admin/security and change the admin password.
  7. Optionally enable MFA and/or the IP allowlist.
  8. Go to /admin/config and fill in baseUrl, usedPath, newPath, apiBaseUrl, and returnUrl.
  9. Go to /admin/cache and click force refresh.
  10. Test /api/health, /api/fietsen?refresh=1, and /voorraad.

Security

The admin portal is protected on multiple levels.

Authentication

  • Passwords are stored as a PBKDF2 hash (SHA-512, 100,000 iterations) with a unique salt per password.
  • Password verification runs in constant time, so no information leaks through response timing.
  • A new password must be at least 10 characters long and must be entered twice for confirmation when changing it.
  • The login and MFA routes have rate limiting (a maximum of 10 attempts per 15 minutes).
  • Failed and successful logins are recorded in the audit log (cache/admin-log.json).

Sessions

  • Sessions are signed with SESSION_SECRET.
  • The session cookie is HttpOnly, SameSite=Lax, and automatically receives the Secure attribute when NODE_ENV=production.
  • On a successful login the session is regenerated (protection against session fixation).
  • Sessions expire after 8 hours and are extended on activity.

Two-Factor Authentication (MFA)

  • MFA uses TOTP and works with standard authenticator apps.
  • MFA is disabled by default. You enable it via /admin/security.
  • Add the displayed secret key to your authenticator app first, and only then enable MFA, otherwise you can lock yourself out.

IP Allowlist

  • The allowlist can restrict all admin routes to known IP addresses.
  • The client address is determined via req.ip combined with trust proxy, so it cannot be bypassed through a forged X-Forwarded-For header.
  • The allowlist can only be enabled when your current IP address is in the list; this prevents you from locking yourself out.

Headers And Hardening

  • All dynamic values on admin and detail pages are HTML-escaped (protection against XSS).
  • The return parameter on /fiets/:id is validated before it is used.
  • Admin responses receive X-Frame-Options: DENY and Cache-Control: no-store.
  • All responses receive X-Content-Type-Options: nosniff and Referrer-Policy: no-referrer.

Production Recommendations

  • Always set your own long SESSION_SECRET.
  • Set NODE_ENV=production so the session cookie is Secure.
  • Change the default admin password immediately after the first login.
  • Consider enabling MFA and the IP allowlist.
  • Run the app behind HTTPS (Coolify/Traefik handles this).

Troubleshooting

/admin Does Not Work

Use /admin/login. /admin itself is not a route.

Login Does Not Work

Check:

  • Are you using /admin/login?
  • Is /app/config persistent?
  • Has the password already been changed?
  • Is the IP allowlist enabled and is your IP in it?
  • Is MFA enabled while you do not have the code?
  • If needed, reset the admin account through the container terminal.

Deploy Is Green But The Site Does Not Open

Check in Coolify:

  • container port is 3000
  • health check path is /api/health
  • domain is attached to the voorraadportaal service
  • HOST=0.0.0.0 and PORT=3000

Staying Logged In Does Not Work

Check that NODE_ENV=production is set and that the app runs behind HTTPS. A Secure cookie does not work over an unencrypted connection.

Data Disappears After Redeploy

Then /app/config and/or /app/cache are not attached as persistent storage.

/api/fietsen Is Empty

Check baseUrl, usedPath, newPath, whether the Fietsenwijk page is reachable from the server, and /api/health for lastRefreshError.

Images Do Not Load

The app proxies images through /image/:id. Check whether the Fietsenwijk detail page and image wrapper are reachable from the server.

Changelog

v21

  • Admin portal fully redesigned: fixed sidebar, dashboard with statistic tiles, and confirmation messages after actions.
  • White-label options added: configurable site name, logo, and accent color through /admin/config.
  • Default configuration made neutral so a fresh deployment starts blank.
  • Repository cleaned up: removed unused legacy files, added .gitignore, and stopped committing node_modules.
  • MFA handling fixed: the setting now actually works and persists after a restart. MFA is disabled by default.
  • HTML escaping added on all admin and detail pages (XSS protection).
  • The IP allowlist now uses the reliable client address and can no longer be bypassed through a forged header.
  • Session security improved: Secure cookie in production, session regeneration on login, rolling sessions.
  • Constant-time password verification; minimum password length raised to 10 characters with a confirmation field.
  • Security headers added; failed logins are logged.

v20

  • Added admin backend.
  • Added login, MFA, and IP allowlist.
  • Added configuration management.
  • Added cache management.
  • Added hidden IDs management.
  • Added Coolify/Docker/Nixpacks deployment.

v19 And Older

  • Added disk cache.
  • Added /api/refresh and /api/health.
  • Fixed WordPress return navigation.
  • Improved price normalization and detail scraping.
  • Added image proxy.