- JavaScript 90.6%
- CSS 6.3%
- HTML 2.1%
- Dockerfile 1%
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> |
||
|---|---|---|
| config | ||
| public | ||
| .gitignore | ||
| docker-compose.yaml | ||
| Dockerfile | ||
| nixpacks.toml | ||
| package-lock.json | ||
| package.json | ||
| README.en.md | ||
| README.md | ||
| server.js | ||
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
- How The App Works
- Important Files
- Run Locally
- Environment Variables
- Coolify Deployment
- First Admin Login
- Reset The Admin Account
- The Admin Portal
- Organization Configuration
- Cache
- Public Routes And API
- WordPress Embed
- Connect A New Organization
- Security
- Troubleshooting
- Changelog
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/configand/app/cachemust 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
- On startup, the app reads
config/settings.json. - If that file does not exist yet, the app creates a default configuration.
- The app fetches used bikes from
baseUrl + usedPath. - The app fetches new bikes from
baseUrl + newPath. - List pages provide item IDs and titles.
- Detail pages provide prices, images, and specifications.
- Results are cached in memory and on disk.
- The API, frontend, and WordPress embed read from that cache.
- 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
Secureattribute whenNODE_ENV=productionis 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
- Push the repository to GitHub/GitLab.
- Create a new project in Coolify.
- Choose New Resource and select the Git repository.
- Choose Docker Compose as the deployment type.
- Use this compose file:
docker-compose.yaml
- Set these environment variables in Coolify:
NODE_ENV=production
SESSION_SECRET=put-a-long-random-secret-here
- Attach your domain to the
voorraadportaalservice on container port3000. - Use this health check path:
/api/health
- 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/configstores admin settings, password hash, MFA secret, Fietsenwijk URL, and hidden IDs./app/cachestores 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/configand/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
- Create a new Coolify resource/deployment.
- Set
NODE_ENV=productionand your ownSESSION_SECRET. - Make sure
/app/configand/app/cacheare persistent. - Deploy the app.
- Log in at
/admin/login. - Go to
/admin/securityand change the admin password. - Optionally enable MFA and/or the IP allowlist.
- Go to
/admin/configand fill inbaseUrl,usedPath,newPath,apiBaseUrl, andreturnUrl. - Go to
/admin/cacheand click force refresh. - 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 theSecureattribute whenNODE_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.ipcombined withtrust proxy, so it cannot be bypassed through a forgedX-Forwarded-Forheader. - 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
returnparameter on/fiets/:idis validated before it is used. - Admin responses receive
X-Frame-Options: DENYandCache-Control: no-store. - All responses receive
X-Content-Type-Options: nosniffandReferrer-Policy: no-referrer.
Production Recommendations
- Always set your own long
SESSION_SECRET. - Set
NODE_ENV=productionso the session cookie isSecure. - 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/configpersistent? - 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
voorraadportaalservice HOST=0.0.0.0andPORT=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 committingnode_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:
Securecookie 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/refreshand/api/health. - Fixed WordPress return navigation.
- Improved price normalization and detail scraping.
- Added image proxy.