diff --git a/GETTING_STARTED.md b/GETTING_STARTED.md index 5a17777..55c2a0b 100644 --- a/GETTING_STARTED.md +++ b/GETTING_STARTED.md @@ -3,12 +3,14 @@ ## 🚀 5-Minute Setup ### Step 1: Initial Setup + ```bash # From the project root ./scripts/init.sh ``` This will: + - Check Docker and Docker Compose installation - Create `.env` file from template - Build all Docker images @@ -17,6 +19,7 @@ This will: ### Step 2: Configure Database Connection Edit `.env` with your database details: + ```env POSTGRES_HOST=your-db-host.com POSTGRES_USER_AUTH=transitAUTH @@ -28,11 +31,13 @@ JWT_SECRET=your-secure-secret ``` ### Step 3: Start Services + ```bash ./scripts/deploy.sh start ``` ### Step 4: Access Admin UI + ``` http://localhost:5174 ``` @@ -42,29 +47,32 @@ Log in with your real user code and password. ## 📱 Admin UI Features ### Stops Management + - Create new transit stops - Set geolocation (latitude, longitude) - View all stops - Link stops to bus lines ### Bus Lines + - Create new bus routes - Define start and end stops - Set estimated travel time - Assign stops to lines with order ### Fleet + - View fleet vehicles (read-only) - See vehicle details ## 🐳 Docker Services -| Port | Service | URL | -|------|---------|-----| -| 5174 | Admin UI | http://localhost:5174 | -| 5000 | Auth API | http://localhost:5000 | -| 5001 | Transit API | http://localhost:5001 | -| 4000 | Employee API | http://localhost:4000 | +| Port | Service | URL | +| ---- | ------------ | ---------------------------------------- | +| 5174 | Admin UI | http://localhost:5174 | +| 5000 | Auth API | https://auth.xtransit.testinstance.nl | +| 5001 | Transit API | https://transit.xtransit.testinstance.nl | +| 4000 | Employee API | http://localhost:4000 | ## 🔧 Common Commands @@ -91,6 +99,7 @@ Log in with your real user code and password. Use the real login flow with user code and password. ### Production + - Keep `DEV_BYPASS=0` and `DEV_MOCK=0` - Configure proper admin user codes in `ADMIN_USER_CODES` - Ensure JWTs include the `userCode` claim @@ -105,6 +114,7 @@ Use the real login flow with user code and password. ## ⚠️ Troubleshooting ### "Address already in use" + ```bash # Find and stop the process using the port lsof -i :5174 # Find process on port 5174 @@ -112,6 +122,7 @@ kill -9 # Kill the process ``` ### "Cannot connect to database" + ```bash # Check database credentials in .env # Verify database is accessible from your machine @@ -119,6 +130,7 @@ kill -9 # Kill the process ``` ### "Admin UI not loading" + ```bash # Check container is running docker-compose ps admin_ui @@ -131,7 +143,9 @@ docker-compose restart admin_ui ``` ### "Permission denied" errors + The transit API user needs proper database permissions: + ```sql -- Grant permissions to transitAPI user GRANT CONNECT ON DATABASE "XTransit" TO transitAPI; @@ -142,7 +156,7 @@ GRANT SELECT, INSERT, UPDATE ON all tables IN SCHEMA public TO transitAPI; ## 📚 For More Information - **DEPLOYMENT.md** - Full deployment guide for production -- **DOCKER_README.md** - Docker and container information +- **DOCKER_README.md** - Docker and container information - **admin_ui/README.md** - Admin UI specific details ## 🎯 Next Steps diff --git a/README.md b/README.md index 149967c..557e0ef 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,9 @@ Dit repository bevat een suite van Node.js microservices, een admin webapp en ee - `employee_api` — Employee onboarding / management (standaard port 4000) - `transit_api` — Transit/schedule endpoints (standaard port 5001) - `error_log_api` — Centrale foutlogging en viewer (standaard port 5003) + +De error API is beschikbaar via `https://error.xtransit.testinstance.nl`. + - `admin_ui` — React + Vite beheerinterface - `deprecated/` — legacy Flutter bus terminal, retained for reference while moving the client to a Windows XP-era build path @@ -88,7 +91,7 @@ POSTGRES_PASSWORD_ERROR_LOG=strong_password_error_log JWT_SECRET=super_secret_jwt_key_here # Optioneel: centraal error log endpoint voor clients/services -ERROR_LOG_API_URL=http://error_log_api:5003/error_log +ERROR_LOG_API_URL=https://error.xtransit.testinstance.nl/error_log ``` Opmerking: de exacte variabelen worden gelezen door `docker-compose.yml`. Pas de namen/waarden aan naar jouw omgeving. @@ -243,8 +246,8 @@ CREATE TABLE IF NOT EXISTS data.errors ( id BIGSERIAL PRIMARY KEY, "date" TIMESTAMPTZ NOT NULL DEFAULT NOW(), "service" TEXT NOT NULL, - "partOfService" TEXT, - "error" TEXT NOT NULL + partofservice TEXT, + error TEXT NOT NULL ); ``` @@ -356,7 +359,7 @@ The Transit API exposes public (unauthenticated) schedule endpoints: **Example:** ```bash -curl http://localhost:5001/schedules?date=2026-05-06 +curl https://transit.xtransit.testinstance.nl/schedules?date=2026-05-06 ``` **TimetableViewer Component:** diff --git a/admin_ui/README.md b/admin_ui/README.md index 7922d7c..000c0c1 100644 --- a/admin_ui/README.md +++ b/admin_ui/README.md @@ -46,9 +46,9 @@ npm install Create a `.env` file in the `admin_ui` directory: ```env -VITE_AUTH_API=http://localhost:5000 -VITE_TRANSIT_API=http://localhost:5001 -VITE_ERROR_LOG_API=http://localhost:5003 +VITE_AUTH_API=https://auth.xtransit.testinstance.nl +VITE_TRANSIT_API=https://transit.xtransit.testinstance.nl +VITE_ERROR_LOG_API=https://error.xtransit.testinstance.nl ``` ### Run Development Server @@ -102,10 +102,10 @@ http://localhost:5174 ### Environment Variables -| Variable | Description | Default | -| ------------------ | -------------------------------------------------------- | --------------------- | -| `VITE_AUTH_API` | Auth API URL (same-origin path recommended in Docker) | http://localhost:5000 | -| `VITE_TRANSIT_API` | Transit API URL (same-origin path recommended in Docker) | http://localhost:5001 | +| Variable | Description | Default | +| ------------------ | --------------- | ---------------------------------------- | +| `VITE_AUTH_API` | Auth API URL | https://auth.xtransit.testinstance.nl | +| `VITE_TRANSIT_API` | Transit API URL | https://transit.xtransit.testinstance.nl | ### Runtime Configuration diff --git a/admin_ui/SCHEDULE_API.md b/admin_ui/SCHEDULE_API.md index 4415cbe..c4d8de8 100644 --- a/admin_ui/SCHEDULE_API.md +++ b/admin_ui/SCHEDULE_API.md @@ -246,7 +246,7 @@ The schedule manager uses: ### Create a schedule (Admin) ```bash -curl -X POST http://localhost:5001/admin/schedules \ +curl -X POST https://transit.xtransit.testinstance.nl/admin/schedules \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Content-Type: application/json" \ -d '{ @@ -261,13 +261,13 @@ curl -X POST http://localhost:5001/admin/schedules \ ### Get today's schedules (Public) ```bash -curl http://localhost:5001/schedules?date=$(date +%Y-%m-%d) +curl https://transit.xtransit.testinstance.nl/schedules?date=$(date +%Y-%m-%d) ``` ### Get schedules for specific line (Public) ```bash -curl http://localhost:5001/schedules?lineId=3 +curl https://transit.xtransit.testinstance.nl/schedules?lineId=3 ``` ## Future Enhancements diff --git a/admin_ui/docker-compose.yml b/admin_ui/docker-compose.yml index 6f5941b..7485d4a 100644 --- a/admin_ui/docker-compose.yml +++ b/admin_ui/docker-compose.yml @@ -1,9 +1,9 @@ -version: '3.8' +version: "3.8" services: admin-ui: build: . ports: - "5174:80" environment: - VITE_AUTH_API: ${VITE_AUTH_API:-http://localhost:5000} - VITE_TRANSIT_API: ${VITE_TRANSIT_API:-http://localhost:5001} + VITE_AUTH_API: ${VITE_AUTH_API:-https://auth.xtransit.testinstance.nl} + VITE_TRANSIT_API: ${VITE_TRANSIT_API:-https://transit.xtransit.testinstance.nl} diff --git a/admin_ui/docker-entrypoint.sh b/admin_ui/docker-entrypoint.sh index 9bdfc4c..1e6879d 100755 --- a/admin_ui/docker-entrypoint.sh +++ b/admin_ui/docker-entrypoint.sh @@ -3,9 +3,9 @@ set -e cat > /usr/share/nginx/html/env-config.js < { - const rawValue = value || '' - if (!rawValue) return fallbackPath - if (/^https?:\/\//i.test(rawValue) || rawValue.startsWith('//') || rawValue.startsWith('/')) { - return rawValue + const rawValue = value || ""; + if (!rawValue) return fallbackPath; + if ( + /^https?:\/\//i.test(rawValue) || + rawValue.startsWith("//") || + rawValue.startsWith("/") + ) { + return rawValue; } - return `${window.location.protocol}//${rawValue}` -} + return `${window.location.protocol}//${rawValue}`; +}; -const AUTH_API = normalizeApiBaseUrl(RUNTIME.AUTH_API || import.meta.env.VITE_AUTH_API, 'http://localhost:5000') -const TRANSIT_API = normalizeApiBaseUrl(RUNTIME.TRANSIT_API || import.meta.env.VITE_TRANSIT_API, 'http://localhost:5001') -const ERROR_LOG_API = normalizeApiBaseUrl(RUNTIME.ERROR_LOG_API || import.meta.env.VITE_ERROR_LOG_API, 'http://localhost:5003') +const AUTH_API = normalizeApiBaseUrl( + RUNTIME.AUTH_API || import.meta.env.VITE_AUTH_API, + "https://auth.xtransit.testinstance.nl", +); +const TRANSIT_API = normalizeApiBaseUrl( + RUNTIME.TRANSIT_API || import.meta.env.VITE_TRANSIT_API, + "https://transit.xtransit.testinstance.nl", +); +const ERROR_LOG_API = normalizeApiBaseUrl( + RUNTIME.ERROR_LOG_API || import.meta.env.VITE_ERROR_LOG_API, + "https://error.xtransit.testinstance.nl", +); -const reportClientError = async ({ service, partOfService, error }) => { +const reportClientError = async ({ service, partofservice, error }) => { try { await fetch(`${ERROR_LOG_API}/error_log`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, + method: "POST", + headers: { "Content-Type": "application/json" }, body: JSON.stringify({ service, - partOfService, - error: typeof error === 'string' ? error : JSON.stringify(error), + partofservice, + error: typeof error === "string" ? error : JSON.stringify(error), }), - }) + }); } catch (e) { // ignore logging failures } -} +}; -export const authClient = axios.create({ baseURL: AUTH_API }) +export const authClient = axios.create({ baseURL: AUTH_API }); authClient.interceptors.response.use( - response => response, - error => { + (response) => response, + (error) => { void reportClientError({ - service: 'admin_ui', - partOfService: 'authClient', - error: error?.response?.data?.error || error?.message || 'authClient_error', - }) - return Promise.reject(error) - } -) + service: "admin_ui", + partofservice: "authClient", + error: + error?.response?.data?.error || error?.message || "authClient_error", + }); + return Promise.reject(error); + }, +); export const transitClient = (token) => { const client = axios.create({ baseURL: TRANSIT_API, - headers: { Authorization: `Bearer ${token}` } - }) + headers: { Authorization: `Bearer ${token}` }, + }); // Auto-logout on unauthorized responses to avoid delayed failures client.interceptors.response.use( - response => response, - error => { + (response) => response, + (error) => { try { - const status = error?.response?.status + const status = error?.response?.status; if (status === 401 || status === 403) { - localStorage.removeItem('token') - localStorage.removeItem('user') + localStorage.removeItem("token"); + localStorage.removeItem("user"); // reload to let App show the Login screen - if (typeof window !== 'undefined') window.location.reload() + if (typeof window !== "undefined") window.location.reload(); } void reportClientError({ - service: 'admin_ui', - partOfService: 'transitClient', - error: error?.response?.data?.error || error?.message || 'transitClient_error', - }) + service: "admin_ui", + partofservice: "transitClient", + error: + error?.response?.data?.error || + error?.message || + "transitClient_error", + }); } catch (e) { // ignore } - return Promise.reject(error) - } - ) + return Promise.reject(error); + }, + ); - return client -} + return client; +}; -export const errorLogClient = (token) => axios.create({ - baseURL: ERROR_LOG_API, - headers: token ? { Authorization: `Bearer ${token}` } : {}, -}) +export const errorLogClient = (token) => + axios.create({ + baseURL: ERROR_LOG_API, + headers: token ? { Authorization: `Bearer ${token}` } : {}, + }); -export { reportClientError } +export { reportClientError }; diff --git a/admin_ui/src/components/ErrorLogViewer.jsx b/admin_ui/src/components/ErrorLogViewer.jsx index 410b798..9c8c372 100644 --- a/admin_ui/src/components/ErrorLogViewer.jsx +++ b/admin_ui/src/components/ErrorLogViewer.jsx @@ -1,42 +1,44 @@ -import React, { useEffect, useMemo, useState } from 'react' -import { errorLogClient } from '../api' +import React, { useEffect, useMemo, useState } from "react"; +import { errorLogClient } from "../api"; const formatDate = (value) => { - if (!value) return '—' + if (!value) return "—"; try { - return new Date(value).toLocaleString() + return new Date(value).toLocaleString(); } catch (_) { - return String(value) + return String(value); } -} +}; export default function ErrorLogViewer({ token }) { - const client = useMemo(() => errorLogClient(token), [token]) - const [logs, setLogs] = useState([]) - const [service, setService] = useState('') - const [partOfService, setPartOfService] = useState('') - const [loading, setLoading] = useState(false) - const [message, setMessage] = useState(null) + const client = useMemo(() => errorLogClient(token), [token]); + const [logs, setLogs] = useState([]); + const [service, setService] = useState(""); + const [partofservice, setPartofservice] = useState(""); + const [loading, setLoading] = useState(false); + const [message, setMessage] = useState(null); const loadLogs = async () => { - setLoading(true) - setMessage(null) + setLoading(true); + setMessage(null); try { - const params = { limit: 100 } - if (service) params.service = service - if (partOfService) params.partOfService = partOfService - const res = await client.get('/error_log', { params }) - setLogs(res.data.errors || []) + const params = { limit: 100 }; + if (service) params.service = service; + if (partofservice) params.partofservice = partofservice; + const res = await client.get("/error_log", { params }); + setLogs(res.data.errors || []); } catch (err) { - setMessage(err.response?.data?.error || err.message || 'Kon fouten niet laden') + setMessage( + err.response?.data?.error || err.message || "Kon fouten niet laden", + ); } finally { - setLoading(false) + setLoading(false); } - } + }; useEffect(() => { - loadLogs() - }, [service, partOfService]) + loadLogs(); + }, [service, partofservice]); return (
@@ -46,29 +48,41 @@ export default function ErrorLogViewer({ token }) {

Laatste foutmeldingen uit de centrale logging service.

- setService(e.target.value)} /> - setPartOfService(e.target.value)} /> + setService(e.target.value)} + /> + setPartofservice(e.target.value)} + />
{message &&
{message}
}
- {logs.map(log => ( + {logs.map((log) => (
{log.service}
{formatDate(log.date)}
-
{log.partOfService || '—'}
+
+ {log.partofservice || "—"} +
{log.error}
))}
- {!logs.length && !message &&
Nog geen foutmeldingen gevonden.
} + {!logs.length && !message && ( +
Nog geen foutmeldingen gevonden.
+ )}
- ) + ); } diff --git a/admin_ui/src/components/TimetableViewer.jsx b/admin_ui/src/components/TimetableViewer.jsx index 7196c47..a726cdb 100644 --- a/admin_ui/src/components/TimetableViewer.jsx +++ b/admin_ui/src/components/TimetableViewer.jsx @@ -1,72 +1,85 @@ -import React, { useEffect, useState } from 'react' +import React, { useEffect, useState } from "react"; -export default function TimetableViewer({ apiBaseUrl = 'http://localhost:5001' }) { - const [schedules, setSchedules] = useState([]) - const [summary, setSummary] = useState(null) - const [selectedDate, setSelectedDate] = useState(new Date().toISOString().split('T')[0]) - const [selectedLineId, setSelectedLineId] = useState('') - const [lines, setLines] = useState([]) - const [loading, setLoading] = useState(false) - const [error, setError] = useState(null) +export default function TimetableViewer({ + apiBaseUrl = "https://transit.xtransit.testinstance.nl", +}) { + const [schedules, setSchedules] = useState([]); + const [summary, setSummary] = useState(null); + const [selectedDate, setSelectedDate] = useState( + new Date().toISOString().split("T")[0], + ); + const [selectedLineId, setSelectedLineId] = useState(""); + const [lines, setLines] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); // Fetch available bus lines useEffect(() => { const fetchLines = async () => { try { - const res = await fetch(`${apiBaseUrl}/admin/bus-lines`) - const data = await res.json() - setLines(data.busLines || []) + const res = await fetch(`${apiBaseUrl}/admin/bus-lines`); + const data = await res.json(); + setLines(data.busLines || []); } catch (err) { - console.error('Failed to load lines:', err) + console.error("Failed to load lines:", err); } - } - fetchLines() - }, [apiBaseUrl]) + }; + fetchLines(); + }, [apiBaseUrl]); // Fetch schedules based on date and line filter useEffect(() => { const fetchSchedules = async () => { - setLoading(true) - setError(null) + setLoading(true); + setError(null); try { - let url = `${apiBaseUrl}/schedules/overview?date=${selectedDate}` + let url = `${apiBaseUrl}/schedules/overview?date=${selectedDate}`; if (selectedLineId) { - url += `&lineId=${selectedLineId}` + url += `&lineId=${selectedLineId}`; } - const res = await fetch(url) - const data = await res.json() - setSchedules(data.schedules || []) - setSummary(data.summary || null) + const res = await fetch(url); + const data = await res.json(); + setSchedules(data.schedules || []); + setSummary(data.summary || null); } catch (err) { - setError('Failed to load schedules: ' + err.message) + setError("Failed to load schedules: " + err.message); } finally { - setLoading(false) + setLoading(false); } - } - fetchSchedules() - }, [selectedDate, selectedLineId, apiBaseUrl]) + }; + fetchSchedules(); + }, [selectedDate, selectedLineId, apiBaseUrl]); - const timeSlots = generateTimeSlots() - const schedulesBySlot = groupSchedulesByTimeSlot(schedules, timeSlots) + const timeSlots = generateTimeSlots(); + const schedulesBySlot = groupSchedulesByTimeSlot(schedules, timeSlots); return ( -
+

Public Transit Timetable

- -
+ +
setSelectedDate(e.target.value)} - style={{ padding: '8px', borderRadius: '6px', border: '1px solid #ccc' }} + onChange={(e) => setSelectedDate(e.target.value)} + style={{ + padding: "8px", + borderRadius: "6px", + border: "1px solid #ccc", + }} /> assignDriver(schedule.id, event.target.value) } + style={{ width: "100%", marginTop: "0.75rem" }} > {drivers.map((driver) => ( @@ -253,16 +271,17 @@ export default function WeeklyScheduleEditor({ type="button" className="danger" onClick={() => removeSchedule(schedule.id)} + style={{ width: "100%", marginTop: "0.5rem" }} > Delete -
- - ))} -
- )} -
- ))} + + ))} +
+ )} + + ))} + ); } diff --git a/auth_api/index.js b/auth_api/index.js index a6869ed..8eb0cbe 100644 --- a/auth_api/index.js +++ b/auth_api/index.js @@ -1,41 +1,43 @@ -const express = require('express'); -const bcrypt = require('bcryptjs'); -const jwt = require('jsonwebtoken'); -const db = require('./db'); -require('dotenv').config(); +const express = require("express"); +const bcrypt = require("bcryptjs"); +const jwt = require("jsonwebtoken"); +const db = require("./db"); +require("dotenv").config(); const app = express(); app.use(express.json()); // Simple CORS middleware for browser-based admin UI testing. app.use((req, res, next) => { - res.setHeader('Access-Control-Allow-Origin', '*'); - res.setHeader('Access-Control-Allow-Methods', 'GET,POST,OPTIONS'); - res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); - if (req.method === 'OPTIONS') { + res.setHeader("Access-Control-Allow-Origin", "*"); + res.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS"); + res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization"); + if (req.method === "OPTIONS") { return res.sendStatus(204); } next(); }); -const ERROR_LOG_API_URL = process.env.ERROR_LOG_API_URL || 'http://localhost:5003/error_log'; +const ERROR_LOG_API_URL = + process.env.ERROR_LOG_API_URL || + "https://error.xtransit.testinstance.nl/error_log"; function friendlyErrorMessage(statusCode) { - if (statusCode === 400) return 'Controleer je invoer en probeer opnieuw.'; - if (statusCode === 401) return 'Je sessie is verlopen. Log opnieuw in.'; - if (statusCode === 403) return 'Je hebt hier geen toegang toe.'; - if (statusCode === 404) return 'Niet gevonden.'; - return 'Er ging iets mis. Probeer het later opnieuw.'; + if (statusCode === 400) return "Controleer je invoer en probeer opnieuw."; + if (statusCode === 401) return "Je sessie is verlopen. Log opnieuw in."; + if (statusCode === 403) return "Je hebt hier geen toegang toe."; + if (statusCode === 404) return "Niet gevonden."; + return "Er ging iets mis. Probeer het later opnieuw."; } -async function reportError({ partOfService, error }) { +async function reportError({ partofservice, error }) { try { await fetch(ERROR_LOG_API_URL, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, + method: "POST", + headers: { "Content-Type": "application/json" }, body: JSON.stringify({ - service: 'auth_api', - partOfService, + service: "auth_api", + partofservice, error, }), }); @@ -45,9 +47,13 @@ async function reportError({ partOfService, error }) { app.use((req, res, next) => { const originalJson = res.json.bind(res); res.json = (body) => { - if (res.statusCode >= 400 && body && typeof body === 'object') { - const rawError = body.error || body.message || body.details || `HTTP ${res.statusCode}`; - void reportError({ partOfService: `${req.method} ${req.originalUrl}`, error: rawError }); + if (res.statusCode >= 400 && body && typeof body === "object") { + const rawError = + body.error || body.message || body.details || `HTTP ${res.statusCode}`; + void reportError({ + partofservice: `${req.method} ${req.originalUrl}`, + error: rawError, + }); return originalJson({ error: friendlyErrorMessage(res.statusCode) }); } return originalJson(body); @@ -58,46 +64,59 @@ app.use((req, res, next) => { const JWT_SECRET = process.env.JWT_SECRET; const ADMIN_USER_CODES = new Set( - String(process.env.ADMIN_USER_CODES || '') - .split(',') - .map(c => Number(c.trim())) - .filter(Number.isFinite) + String(process.env.ADMIN_USER_CODES || "") + .split(",") + .map((c) => Number(c.trim())) + .filter(Number.isFinite), ); - -app.post('/login', async (req, res) => { +app.post("/login", async (req, res) => { try { const { userCode, password } = req.body; - if (!userCode || !password) return res.status(400).json({ error: 'userCode and password required' }); + if (!userCode || !password) + return res.status(400).json({ error: "userCode and password required" }); - const result = await db.query('SELECT id, usercode, password FROM workers.users WHERE usercode = $1', [userCode]); + const result = await db.query( + "SELECT id, usercode, password FROM workers.users WHERE usercode = $1", + [userCode], + ); const user = result.rows[0]; - if (!user) return res.status(401).json({ error: 'invalid_credentials' }); + if (!user) return res.status(401).json({ error: "invalid_credentials" }); const ok = await bcrypt.compare(password, user.password); - if (!ok) return res.status(401).json({ error: 'invalid_credentials' }); + if (!ok) return res.status(401).json({ error: "invalid_credentials" }); const isAdmin = ADMIN_USER_CODES.has(Number(user.usercode)); const payload = { userId: user.id, userCode: user.usercode, isAdmin }; - const token = jwt.sign(payload, JWT_SECRET, { expiresIn: '1h' }); + const token = jwt.sign(payload, JWT_SECRET, { expiresIn: "1h" }); - res.json({ token, user: { id: user.id, userCode: user.usercode, job: user.job, isAdmin } }); + res.json({ + token, + user: { id: user.id, userCode: user.usercode, job: user.job, isAdmin }, + }); } catch (err) { - await reportError({ partOfService: `${req.method} ${req.originalUrl}`, error: err?.stack || err?.message || String(err) }); + await reportError({ + partofservice: `${req.method} ${req.originalUrl}`, + error: err?.stack || err?.message || String(err), + }); console.error(err); res.status(500).json({ error: friendlyErrorMessage(500) }); } }); -app.get('/me', async (req, res) => { +app.get("/me", async (req, res) => { const auth = req.headers.authorization; - if (!auth || !auth.startsWith('Bearer ')) return res.status(401).json({ error: 'missing_token' }); + if (!auth || !auth.startsWith("Bearer ")) + return res.status(401).json({ error: "missing_token" }); const token = auth.slice(7); try { const payload = jwt.verify(token, JWT_SECRET); res.json({ user: payload }); } catch (err) { - void reportError({ partOfService: `${req.method} ${req.originalUrl}`, error: err?.stack || err?.message || String(err) }); + void reportError({ + partofservice: `${req.method} ${req.originalUrl}`, + error: err?.stack || err?.message || String(err), + }); res.status(401).json({ error: friendlyErrorMessage(401) }); } }); diff --git a/docker-compose.yml b/docker-compose.yml index 5856578..9fcb509 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,7 +12,7 @@ services: PG_DB: ${POSTGRES_DB} PG_USER: ${POSTGRES_USER_LOGGER} PG_PASS: ${POSTGRES_PASSWORD_LOGGER} - ERROR_LOG_API_URL: ${ERROR_LOG_API_URL} + ERROR_LOG_API_URL: ${ERROR_LOG_API_URL:-https://error.xtransit.testinstance.nl/error_log} restart: unless-stopped auth_api: @@ -30,7 +30,7 @@ services: PG_PASS: ${POSTGRES_PASSWORD_AUTH} ADMIN_USER_CODES: ${ADMIN_USER_CODES} DEV_BYPASS: ${DEV_BYPASS:-0} - ERROR_LOG_API_URL: ${ERROR_LOG_API_URL} + ERROR_LOG_API_URL: ${ERROR_LOG_API_URL:-https://error.xtransit.testinstance.nl/error_log} depends_on: - error_log_api restart: unless-stopped @@ -47,7 +47,7 @@ services: PG_DB: ${POSTGRES_DB} PG_USER: ${POSTGRES_USER_EMPLOYEE} PG_PASS: ${POSTGRES_PASSWORD_EMPLOYEE} - ERROR_LOG_API_URL: ${ERROR_LOG_API_URL} + ERROR_LOG_API_URL: ${ERROR_LOG_API_URL:-https://error.xtransit.testinstance.nl/error_log} depends_on: - error_log_api restart: unless-stopped @@ -66,7 +66,7 @@ services: PG_USER: ${POSTGRES_USER_TRANSIT} PG_PASS: ${POSTGRES_PASSWORD_TRANSIT} DEV_MOCK: ${DEV_MOCK:-0} - ERROR_LOG_API_URL: ${ERROR_LOG_API_URL} + ERROR_LOG_API_URL: ${ERROR_LOG_API_URL:-https://error.xtransit.testinstance.nl/error_log} depends_on: - error_log_api restart: unless-stopped @@ -79,7 +79,7 @@ services: environment: AUTH_API: ${AUTH_API} TRANSIT_API: ${TRANSIT_API} - ERROR_LOG_API: ${ERROR_LOG_API} + ERROR_LOG_API: ${ERROR_LOG_API:-https://error.xtransit.testinstance.nl} depends_on: - auth_api - transit_api diff --git a/employee_api/index.js b/employee_api/index.js index b05c417..3a956a0 100644 --- a/employee_api/index.js +++ b/employee_api/index.js @@ -1,81 +1,104 @@ -const express = require('express'); -const bcrypt = require('bcryptjs'); -const db = require('./db'); -require('dotenv').config(); +const express = require("express"); +const bcrypt = require("bcryptjs"); +const db = require("./db"); +require("dotenv").config(); const app = express(); app.use(express.json()); -const ERROR_LOG_API_URL = process.env.ERROR_LOG_API_URL || 'http://localhost:5003/error_log'; +const ERROR_LOG_API_URL = + process.env.ERROR_LOG_API_URL || + "https://error.xtransit.testinstance.nl/error_log"; function friendlyErrorMessage(statusCode) { - if (statusCode === 400) return 'Controleer je invoer en probeer opnieuw.'; - if (statusCode === 401) return 'Je sessie is verlopen. Log opnieuw in.'; - if (statusCode === 403) return 'Je hebt hier geen toegang toe.'; - if (statusCode === 404) return 'Niet gevonden.'; - return 'Er ging iets mis. Probeer het later opnieuw.'; + if (statusCode === 400) return "Controleer je invoer en probeer opnieuw."; + if (statusCode === 401) return "Je sessie is verlopen. Log opnieuw in."; + if (statusCode === 403) return "Je hebt hier geen toegang toe."; + if (statusCode === 404) return "Niet gevonden."; + return "Er ging iets mis. Probeer het later opnieuw."; } -async function reportError({ partOfService, error }) { - try { - await fetch(ERROR_LOG_API_URL, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - service: 'employee_api', - partOfService, - error, - }), - }); - } catch (_) {} +async function reportError({ partofservice, error }) { + try { + await fetch(ERROR_LOG_API_URL, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + service: "employee_api", + partofservice, + error, + }), + }); + } catch (_) {} } app.use((req, res, next) => { - const originalJson = res.json.bind(res); - res.json = (body) => { - if (res.statusCode >= 400 && body && typeof body === 'object') { - const rawError = body.error || body.message || body.details || `HTTP ${res.statusCode}`; - void reportError({ partOfService: `${req.method} ${req.originalUrl}`, error: rawError }); - return originalJson({ error: friendlyErrorMessage(res.statusCode) }); - } - return originalJson(body); - }; + const originalJson = res.json.bind(res); + res.json = (body) => { + if (res.statusCode >= 400 && body && typeof body === "object") { + const rawError = + body.error || body.message || body.details || `HTTP ${res.statusCode}`; + void reportError({ + partofservice: `${req.method} ${req.originalUrl}`, + error: rawError, + }); + return originalJson({ error: friendlyErrorMessage(res.statusCode) }); + } + return originalJson(body); + }; - next(); + next(); }); // Employee onboarding -app.post('/createemployee', async (req, res) => { - try { - const {firstname, lastname, job, password} = req.body; - if (!firstname || !lastname || !job || !password) return res.status(400).json({error: 'Firstname, Lastname, Job title and password are required'}); +app.post("/createemployee", async (req, res) => { + try { + const { firstname, lastname, job, password } = req.body; + if (!firstname || !lastname || !job || !password) + return res.status(400).json({ + error: "Firstname, Lastname, Job title and password are required", + }); - const resultCreateEmployee = await db.query('INSERT INTO workers.employees (firstname, lastname, job) VALUES ($1, $2, $3) RETURNING id', [firstname, lastname, job]); - const newEmployee = resultCreateEmployee.rows[0]; - const encryptedpassword = await bcrypt.hash(password, 12); - const resultCreateUser = await db.query('INSERT INTO workers.users (employee, password) VALUES ($1, $2) RETURNING usercode', [newEmployee.id, encryptedpassword]); - const newUser = resultCreateUser.rows[0]; - return res.status(201).json({ employee: firstname, code: newUser.usercoden }); - } - catch (err) { - void reportError({ partOfService: `${req.method} ${req.originalUrl}`, error: err?.stack || err?.message || String(err) }); - console.error(err); - res.status(500).json({ error: friendlyErrorMessage(500) }); + const resultCreateEmployee = await db.query( + "INSERT INTO workers.employees (firstname, lastname, job) VALUES ($1, $2, $3) RETURNING id", + [firstname, lastname, job], + ); + const newEmployee = resultCreateEmployee.rows[0]; + const encryptedpassword = await bcrypt.hash(password, 12); + const resultCreateUser = await db.query( + "INSERT INTO workers.users (employee, password) VALUES ($1, $2) RETURNING usercode", + [newEmployee.id, encryptedpassword], + ); + const newUser = resultCreateUser.rows[0]; + return res + .status(201) + .json({ employee: firstname, code: newUser.usercoden }); + } catch (err) { + void reportError({ + partofservice: `${req.method} ${req.originalUrl}`, + error: err?.stack || err?.message || String(err), + }); + console.error(err); + res.status(500).json({ error: friendlyErrorMessage(500) }); } -}) +}); -app.get('/health', async (_req, res, _next) => { - try { - return res.status(200).json({endpoint: 'reachable'}) - } - catch (err) { - void reportError({ partOfService: `${req.method} ${req.originalUrl}`, error: err?.stack || err?.message || String(err) }); - console.error(err); - res.status(500).json({ endpoint: 'unreachable', error: friendlyErrorMessage(500) }) - } -}) +app.get("/health", async (_req, res, _next) => { + try { + return res.status(200).json({ endpoint: "reachable" }); + } catch (err) { + void reportError({ + partofservice: `${req.method} ${req.originalUrl}`, + error: err?.stack || err?.message || String(err), + }); + console.error(err); + res + .status(500) + .json({ endpoint: "unreachable", error: friendlyErrorMessage(500) }); + } +}); const PORT = process.env.PORT || 4000; app.listen(PORT, () => console.log(`Auth API listening on ${PORT}`)); -module.exports = app; \ No newline at end of file +module.exports = app; diff --git a/error_log_api/index.js b/error_log_api/index.js index 618f212..cbb69db 100644 --- a/error_log_api/index.js +++ b/error_log_api/index.js @@ -1,29 +1,29 @@ -const express = require('express'); -const cors = require('cors'); -const jwt = require('jsonwebtoken'); -require('dotenv').config(); +const express = require("express"); +const cors = require("cors"); +const jwt = require("jsonwebtoken"); +require("dotenv").config(); -const db = require('./db'); +const db = require("./db"); const app = express(); -app.use(express.json({ limit: '64kb' })); +app.use(express.json({ limit: "64kb" })); app.use(cors()); const PORT = Number(process.env.PORT || 5003); -const JWT_SECRET = process.env.JWT_SECRET || ''; +const JWT_SECRET = process.env.JWT_SECRET || ""; // Admin verification is done by `auth_api` and carried in the JWT as `isAdmin`. function toFriendlyError(statusCode) { - if (statusCode === 400) return 'Controleer je invoer en probeer opnieuw.'; - if (statusCode === 401) return 'Je sessie is verlopen. Log opnieuw in.'; - if (statusCode === 403) return 'Je hebt hier geen toegang toe.'; - if (statusCode === 404) return 'Niet gevonden.'; - return 'Er ging iets mis. Probeer het later opnieuw.'; + if (statusCode === 400) return "Controleer je invoer en probeer opnieuw."; + if (statusCode === 401) return "Je sessie is verlopen. Log opnieuw in."; + if (statusCode === 403) return "Je hebt hier geen toegang toe."; + if (statusCode === 404) return "Niet gevonden."; + return "Er ging iets mis. Probeer het later opnieuw."; } function normalizeErrorText(value) { - if (!value) return 'Unknown error'; - if (typeof value === 'string') return value; + if (!value) return "Unknown error"; + if (typeof value === "string") return value; try { return JSON.stringify(value); } catch (_) { @@ -31,20 +31,28 @@ function normalizeErrorText(value) { } } -async function insertErrorLog({ service, partOfService, error }) { - const serviceName = String(service || 'unknown_service').slice(0, 120); - const part = partOfService ? String(partOfService).slice(0, 240) : null; +async function insertErrorLog({ + service, + partofservice, + partOfService, + error, +}) { + const serviceName = String(service || "unknown_service").slice(0, 120); + const part = + partofservice || partOfService + ? String(partofservice || partOfService).slice(0, 240) + : null; const errorText = String(normalizeErrorText(error)).slice(0, 8000); await db.query( - 'INSERT INTO data.errors ("service", "partOfService", "error") VALUES ($1, $2, $3)', - [serviceName, part, errorText] + "INSERT INTO data.errors (service, partofservice, error) VALUES ($1, $2, $3)", + [serviceName, part, errorText], ); } function verifyAdmin(req, res, next) { const auth = req.headers.authorization; - if (!auth || !auth.startsWith('Bearer ')) { + if (!auth || !auth.startsWith("Bearer ")) { return res.status(401).json({ error: toFriendlyError(401) }); } @@ -61,33 +69,37 @@ function verifyAdmin(req, res, next) { } } -app.get('/health', async (_req, res) => { - res.json({ status: 'ok', service: 'error_log_api' }); +app.get("/health", async (_req, res) => { + res.json({ status: "ok", service: "error_log_api" }); }); -app.post('/error_log', async (req, res) => { +app.post("/error_log", async (req, res) => { try { const service = req.body?.service; - const partOfService = req.body?.partOfService || null; + const partofservice = + req.body?.partofservice || req.body?.partOfService || null; const error = req.body?.error; if (!service || !error) { return res.status(400).json({ error: toFriendlyError(400) }); } - await insertErrorLog({ service, partOfService, error }); + await insertErrorLog({ service, partofservice, error }); return res.status(201).json({ ok: true }); } catch (err) { - console.error('POST /error_log failed:', err); + console.error("POST /error_log failed:", err); return res.status(500).json({ error: toFriendlyError(500) }); } }); -app.get('/error_log', verifyAdmin, async (req, res) => { +app.get("/error_log", verifyAdmin, async (req, res) => { try { const limit = Math.min(Number(req.query.limit || 100), 500); const service = req.query.service ? String(req.query.service) : null; - const partOfService = req.query.partOfService ? String(req.query.partOfService) : null; + const partofservice = + req.query.partofservice || req.query.partOfService + ? String(req.query.partofservice || req.query.partOfService) + : null; const where = []; const params = []; @@ -96,25 +108,25 @@ app.get('/error_log', verifyAdmin, async (req, res) => { params.push(service); where.push(`"service" = $${params.length}`); } - if (partOfService) { - params.push(partOfService); - where.push(`"partOfService" ILIKE $${params.length}`); - params[params.length - 1] = `%${partOfService}%`; + if (partofservice) { + params.push(partofservice); + where.push(`partofservice ILIKE $${params.length}`); + params[params.length - 1] = `%${partofservice}%`; } params.push(limit); const rows = await db.query( - `SELECT id, "date", "service", "partOfService", "error" + `SELECT id, date, service, partofservice, error FROM data.errors - ${where.length ? `WHERE ${where.join(' AND ')}` : ''} + ${where.length ? `WHERE ${where.join(" AND ")}` : ""} ORDER BY "date" DESC, id DESC LIMIT $${params.length}`, - params + params, ); return res.json({ errors: rows.rows }); } catch (err) { - console.error('GET /error_log failed:', err); + console.error("GET /error_log failed:", err); return res.status(500).json({ error: toFriendlyError(500) }); } }); @@ -122,12 +134,12 @@ app.get('/error_log', verifyAdmin, async (req, res) => { app.use(async (err, req, res, _next) => { try { await insertErrorLog({ - service: 'error_log_api', - partOfService: `${req.method} ${req.originalUrl}`, + service: "error_log_api", + partofservice: `${req.method} ${req.originalUrl}`, error: err?.stack || err?.message || String(err), }); } catch (logErr) { - console.error('Failed to log internal error:', logErr); + console.error("Failed to log internal error:", logErr); } if (res.headersSent) return; diff --git a/scripts/deploy.sh b/scripts/deploy.sh index c49e83c..71af1ea 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -14,8 +14,8 @@ case $COMMAND in echo "" echo "📋 Service URLs:" echo " Admin UI: http://localhost:5174" - echo " Auth API: http://localhost:5000" - echo " Transit API: http://localhost:5001" + echo " Auth API: https://auth.xtransit.testinstance.nl" + echo " Transit API: https://transit.xtransit.testinstance.nl" echo " Employee API: http://localhost:4000" ;; diff --git a/transit_api/README.md b/transit_api/README.md index c9fe502..4a4b468 100644 --- a/transit_api/README.md +++ b/transit_api/README.md @@ -53,7 +53,7 @@ Note: `ADMIN_USER_CODES` has been moved to the `auth_api` service. Configure adm npm start ``` -API draait op `http://localhost:5001` +API draait op `https://transit.xtransit.testinstance.nl` ## API Endpoints @@ -154,7 +154,7 @@ De legacy endpoint **GET** `/schedules` blijft beschikbaar voor bestaande client Voorbeeld: ```bash -curl "http://localhost:5001/schedules/overview?date=2026-05-06&chauffeurId=12&busStopId=3" +curl "https://transit.xtransit.testinstance.nl/schedules/overview?date=2026-05-06&chauffeurId=12&busStopId=3" ``` ### Navigation diff --git a/transit_api/index.js b/transit_api/index.js index 6dff590..dd6faca 100644 --- a/transit_api/index.js +++ b/transit_api/index.js @@ -10,7 +10,8 @@ app.use(express.json()); app.use(cors()); const ERROR_LOG_API_URL = - process.env.ERROR_LOG_API_URL || "http://localhost:5003/error_log"; + process.env.ERROR_LOG_API_URL || + "https://error.xtransit.testinstance.nl/error_log"; function friendlyErrorMessage(statusCode) { if (statusCode === 400) return "Controleer je invoer en probeer opnieuw."; @@ -20,14 +21,14 @@ function friendlyErrorMessage(statusCode) { return "Er ging iets mis. Probeer het later opnieuw."; } -async function reportError({ partOfService, error }) { +async function reportError({ partofservice, error }) { try { await fetch(ERROR_LOG_API_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ service: "transit_api", - partOfService, + partofservice, error, }), }); @@ -41,7 +42,7 @@ app.use((req, res, next) => { const rawError = body.error || body.message || body.details || `HTTP ${res.statusCode}`; void reportError({ - partOfService: `${req.method} ${req.originalUrl}`, + partofservice: `${req.method} ${req.originalUrl}`, error: rawError, }); return originalJson({ error: friendlyErrorMessage(res.statusCode) }); diff --git a/windows_xp_driver_ui/README.md b/windows_xp_driver_ui/README.md index 972e61e..c3609ff 100644 --- a/windows_xp_driver_ui/README.md +++ b/windows_xp_driver_ui/README.md @@ -2,9 +2,9 @@ Native Win32 driver client targeting Windows XP-era systems. -The client authenticates with `auth_api` at `192.168.68.66:5000`, then requests -assigned weekly schedule templates from `transit_api` at -`192.168.68.66:5001/driver/schedules/daily`. +The client authenticates with `https://auth.xtransit.testinstance.nl`, then requests +assigned weekly schedule templates from +`https://transit.xtransit.testinstance.nl/driver/schedules/daily`. Schedules are assigned through `transit.shifts`; the client does not expect a `driver_id`, `bus_id`, or `weekdays` column on `transit.schedules`. diff --git a/windows_xp_driver_ui/TransitDriverXPSetup.exe b/windows_xp_driver_ui/TransitDriverXPSetup.exe index d3b05d4..c910257 100644 Binary files a/windows_xp_driver_ui/TransitDriverXPSetup.exe and b/windows_xp_driver_ui/TransitDriverXPSetup.exe differ diff --git a/windows_xp_driver_ui/driver_xp.c b/windows_xp_driver_ui/driver_xp.c index 403605c..397f24f 100644 --- a/windows_xp_driver_ui/driver_xp.c +++ b/windows_xp_driver_ui/driver_xp.c @@ -25,7 +25,7 @@ #define ID_ROUTES_LABEL 116 #define MAX_BUF 65535 -#define API_HOST "192.168.68.66" +#define API_HOST "auth.xtransit.testinstance.nl" #define AUTH_PORT 5000 #define TRANSIT_PORT 5001 @@ -538,6 +538,7 @@ static int HttpRequestText(const char *method, const char *src; const char *slash; const char *colon; + int secure = 0; int port = 80; DWORD flags = 0; DWORD status = 0; @@ -554,6 +555,7 @@ static int HttpRequestText(const char *method, src += 7; } else if (strncmp(src, "https://", 8) == 0) { src += 8; + secure = 1; } host[0] = '\0'; @@ -596,7 +598,7 @@ static int HttpRequestText(const char *method, } if (host[0] == '\0') { - snprintf(host, sizeof(host), "192.168.68.66"); + snprintf(host, sizeof(host), "%s", API_HOST); } if (port == 0) { @@ -633,7 +635,7 @@ static int HttpRequestText(const char *method, NULL, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, - 0); + secure ? WINHTTP_FLAG_SECURE : 0); if (!hRequest) { HttpErrorMessage("Unable to open request", GetLastError()); goto cleanup; @@ -747,7 +749,7 @@ static void PerformLogin(HWND hwnd, APPSTATE *state) userCode, password); SetStatusText(hwnd, "Logging in..."); - if (!HttpRequestText("POST", "http://192.168.68.66:5000/login", payload, NULL, response, sizeof(response))) { + if (!HttpRequestText("POST", "https://auth.xtransit.testinstance.nl/login", payload, NULL, response, sizeof(response))) { SetOutputText(hwnd, "Driver service unreachable. Please try again."); SetStatusText(hwnd, "Login failed"); return; @@ -784,7 +786,7 @@ static void PerformRefresh(HWND hwnd, APPSTATE *state) if (date[0] == '\0') { snprintf(date, sizeof(date), "2026-09-17"); } - snprintf(scheduleUrl, sizeof(scheduleUrl), "http://192.168.68.66:5001/driver/schedules/daily?date=%s", date); + snprintf(scheduleUrl, sizeof(scheduleUrl), "https://transit.xtransit.testinstance.nl/driver/schedules/daily?date=%s", date); SetStatusText(hwnd, "Loading schedule..."); if (!HttpRequestText("GET", scheduleUrl, NULL, authHeader, response, sizeof(response))) { @@ -825,7 +827,7 @@ static void PerformLoadStops(HWND hwnd, APPSTATE *state) } snprintf(authHeader, sizeof(authHeader), "Authorization: Bearer %s\r\n", state->token); - snprintf(stopsUrl, sizeof(stopsUrl), "http://192.168.68.66:5001/bus-lines/%d/stops", lineId); + snprintf(stopsUrl, sizeof(stopsUrl), "https://transit.xtransit.testinstance.nl/bus-lines/%d/stops", lineId); SetStatusText(hwnd, "Loading stop details..."); if (!HttpRequestText("GET", stopsUrl, NULL, authHeader, response, sizeof(response))) { diff --git a/windows_xp_driver_ui/driver_xp.exe b/windows_xp_driver_ui/driver_xp.exe index f433101..0b42adc 100755 Binary files a/windows_xp_driver_ui/driver_xp.exe and b/windows_xp_driver_ui/driver_xp.exe differ