Refactor API base URL handling for dynamic environment detection; update navigation stacks and integrate SafeAreaProvider for improved layout management across screens.

This commit is contained in:
bobbert committed 2026-04-06 01:12:36 +01:00
1 parent 8acf4c923a
commit d7bc5d38d1
18 files changed
+806 -166

No files matched your search

+14
View File
@@ -0,0 +1,14 @@
{
"name": "mad-treasure-hunt-server",
"version": "1.0.0",
"description": "RESTful CRUDL API server for MAD Treasure Hunt",
"private": true,
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "node --watch server.js"
},
"engines": {
"node": ">=18.0.0"
}
}
+713
View File
@@ -0,0 +1,713 @@
const http = require('http');
const fs = require('fs');
const path = require('path');
// --- Configuration ---
const PORT = process.env.PORT || 3000;
const DATA_DIR = path.join(__dirname, 'data');
// --- Data Helpers ---
const readJson = (fileName) => JSON.parse(fs.readFileSync(path.join(DATA_DIR, fileName), 'utf-8'));
const writeJson = (fileName, data) => fs.writeFileSync(path.join(DATA_DIR, fileName), JSON.stringify(data, null, 2));
// --- Utility Helpers ---
const hashPassword = (value) => {
let hash = 0;
for (let i = 0; i < value.length; i += 1) {
hash = (hash << 5) - hash + value.charCodeAt(i);
hash |= 0;
}
return `H_${Math.abs(hash)}`;
};
const nextId = (rows, key) => rows.reduce((max, row) => {
const val = Number(row[key] || 0);
return val > max ? val : max;
}, 0) + 1;
const generateCode = (len = 5) => {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
let code = '';
for (let i = 0; i < len; i++) code += chars.charAt(Math.floor(Math.random() * chars.length));
return code;
};
// --- Request / Response Helpers ---
const parseBody = (req) => new Promise((resolve, reject) => {
let body = '';
req.on('data', (chunk) => { body += chunk.toString(); });
req.on('end', () => {
try { resolve(body ? JSON.parse(body) : {}); }
catch { reject(new Error('Invalid JSON body')); }
});
req.on('error', reject);
});
const sendJson = (res, statusCode, payload) => {
const headers = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE,OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
};
if (statusCode === 204) {
res.writeHead(204, headers);
res.end();
return;
}
headers['Content-Type'] = 'application/json';
res.writeHead(statusCode, headers);
res.end(JSON.stringify(payload));
};
// --- Simple Router ---
const routes = [];
const addRoute = (method, pattern, handler) => {
const keys = [];
const regexStr = pattern.replace(/:([a-zA-Z]+)/g, (_, key) => {
keys.push(key);
return '([^/]+)';
});
routes.push({ method, regex: new RegExp(`^${regexStr}$`), keys, handler });
};
const matchRoute = (method, pathname) => {
for (const r of routes) {
if (r.method !== method) continue;
const match = pathname.match(r.regex);
if (match) {
const params = {};
r.keys.forEach((key, i) => { params[key] = match[i + 1]; });
return { handler: r.handler, params };
}
}
return null;
};
// ======================================================================
// AUTH ROUTES
// ======================================================================
addRoute('POST', '/auth/login', async (req, res) => {
const payload = await parseBody(req);
const users = readJson('users.json');
const user = users.find((u) => u.email.toLowerCase() === String(payload.email || '').toLowerCase());
if (!user) return sendJson(res, 400, { message: 'Email not found' });
if (user.passwordHash !== hashPassword(String(payload.password || '')))
return sendJson(res, 400, { message: 'Invalid password' });
sendJson(res, 200, user);
});
addRoute('POST', '/auth/register', async (req, res) => {
const payload = await parseBody(req);
const username = String(payload.username || '').trim();
const email = String(payload.email || '').trim().toLowerCase();
const password = String(payload.password || '');
const confirmPassword = String(payload.confirmPassword || '');
const accountType = String(payload.accountType || 'Individual');
if (!username || !email || !password || !confirmPassword)
return sendJson(res, 400, { message: 'Missing required registration fields' });
if (password !== confirmPassword)
return sendJson(res, 400, { message: 'Passwords do not match' });
const users = readJson('users.json');
if (users.some((u) => u.email.toLowerCase() === email))
return sendJson(res, 400, { message: 'Email already in use' });
const user = {
Uid: nextId(users, 'Uid'),
username,
email,
passwordHash: hashPassword(password),
isBusiness: accountType === 'Business/School',
IsAcceptedAdmin: false,
Gid: null,
SGid: null,
TGid: null,
};
users.push(user);
writeJson('users.json', users);
sendJson(res, 201, user);
});
// ======================================================================
// USERS CRUDL
// ======================================================================
addRoute('GET', '/users', (req, res) => {
sendJson(res, 200, readJson('users.json'));
});
addRoute('GET', '/users/:id', (req, res, params) => {
const users = readJson('users.json');
const user = users.find((u) => String(u.Uid) === params.id);
if (!user) return sendJson(res, 404, { message: 'User not found' });
sendJson(res, 200, user);
});
addRoute('PUT', '/users/:id', async (req, res, params) => {
const payload = await parseBody(req);
const users = readJson('users.json');
const idx = users.findIndex((u) => String(u.Uid) === params.id);
if (idx === -1) return sendJson(res, 404, { message: 'User not found' });
users[idx] = { ...users[idx], ...payload, Uid: users[idx].Uid };
writeJson('users.json', users);
sendJson(res, 200, users[idx]);
});
addRoute('DELETE', '/users/:id', (req, res, params) => {
const users = readJson('users.json');
const idx = users.findIndex((u) => String(u.Uid) === params.id);
if (idx === -1) return sendJson(res, 404, { message: 'User not found' });
users.splice(idx, 1);
writeJson('users.json', users);
sendJson(res, 204, null);
});
// ======================================================================
// GAME TYPES (read-only list)
// ======================================================================
addRoute('GET', '/game-types', (req, res) => {
sendJson(res, 200, readJson('game-types.json'));
});
// ======================================================================
// GROUPS CRUDL
// ======================================================================
addRoute('GET', '/groups', (req, res, params, query) => {
let groups = readJson('groups.json');
if (query.CreatedByUid) groups = groups.filter((g) => String(g.CreatedByUid) === query.CreatedByUid);
sendJson(res, 200, groups);
});
addRoute('GET', '/groups/:gid', (req, res, params) => {
const groups = readJson('groups.json');
const group = groups.find((g) => String(g.Gid) === params.gid);
if (!group) return sendJson(res, 404, { message: 'Group not found' });
sendJson(res, 200, group);
});
addRoute('POST', '/groups', async (req, res) => {
const payload = await parseBody(req);
const groups = readJson('groups.json');
const group = {
Gid: nextId(groups, 'Gid'),
GroupName: payload.GroupName || 'Unnamed Group',
BusinessOrSchoolName: payload.BusinessOrSchoolName || null,
GameType: payload.GameType || 'private',
AdminJoinCode: generateCode(),
CreatedByUid: payload.CreatedByUid,
MaxMemberSubgroups: payload.MaxMemberSubgroups || 1,
TeamsEnabled: payload.TeamsEnabled !== undefined ? payload.TeamsEnabled : true,
ApprovedAdmins: [payload.CreatedByUid],
CreatedAt: new Date().toISOString(),
};
groups.push(group);
writeJson('groups.json', groups);
// Create default admin and member subgroups
const subgroups = readJson('subgroups.json');
const adminSg = {
SGid: nextId(subgroups, 'SGid'),
SubGroupName: 'Admins',
Gid: group.Gid,
JoinCode: null,
IsAdminGroup: true,
CacheTriggerMeters: 20,
};
subgroups.push(adminSg);
const memberSg = {
SGid: nextId(subgroups, 'SGid'),
SubGroupName: 'Members',
Gid: group.Gid,
JoinCode: generateCode(),
IsAdminGroup: false,
CacheTriggerMeters: 20,
};
subgroups.push(memberSg);
writeJson('subgroups.json', subgroups);
// Add creator as admin membership
const memberships = readJson('subgroup-memberships.json');
memberships.push({
id: nextId(memberships, 'id'),
Uid: payload.CreatedByUid,
Gid: group.Gid,
SGid: adminSg.SGid,
IsAcceptedAdmin: true,
});
writeJson('subgroup-memberships.json', memberships);
// Initialise empty game-data entry
const gameData = readJson('game-data.json');
gameData.push({ Gid: group.Gid, GeoCaches: [] });
writeJson('game-data.json', gameData);
// Update the creating user
const users = readJson('users.json');
const uIdx = users.findIndex((u) => u.Uid === payload.CreatedByUid);
if (uIdx !== -1) {
users[uIdx].Gid = group.Gid;
users[uIdx].SGid = adminSg.SGid;
users[uIdx].IsAcceptedAdmin = true;
writeJson('users.json', users);
}
sendJson(res, 201, group);
});
addRoute('PUT', '/groups/:gid', async (req, res, params) => {
const payload = await parseBody(req);
const groups = readJson('groups.json');
const idx = groups.findIndex((g) => String(g.Gid) === params.gid);
if (idx === -1) return sendJson(res, 404, { message: 'Group not found' });
groups[idx] = { ...groups[idx], ...payload, Gid: groups[idx].Gid };
writeJson('groups.json', groups);
sendJson(res, 200, groups[idx]);
});
addRoute('DELETE', '/groups/:gid', (req, res, params) => {
const groups = readJson('groups.json');
const idx = groups.findIndex((g) => String(g.Gid) === params.gid);
if (idx === -1) return sendJson(res, 404, { message: 'Group not found' });
groups.splice(idx, 1);
writeJson('groups.json', groups);
sendJson(res, 204, null);
});
// ======================================================================
// SUBGROUPS CRUDL
// ======================================================================
addRoute('GET', '/subgroups', (req, res, params, query) => {
let rows = readJson('subgroups.json');
if (query.Gid) rows = rows.filter((r) => String(r.Gid) === query.Gid);
sendJson(res, 200, rows);
});
addRoute('GET', '/subgroups/:id', (req, res, params) => {
const rows = readJson('subgroups.json');
const row = rows.find((r) => String(r.SGid) === params.id);
if (!row) return sendJson(res, 404, { message: 'Subgroup not found' });
sendJson(res, 200, row);
});
addRoute('POST', '/subgroups', async (req, res) => {
const payload = await parseBody(req);
const rows = readJson('subgroups.json');
payload.SGid = nextId(rows, 'SGid');
if (!payload.JoinCode) payload.JoinCode = generateCode();
rows.push(payload);
writeJson('subgroups.json', rows);
sendJson(res, 201, payload);
});
addRoute('PUT', '/subgroups/:id', async (req, res, params) => {
const payload = await parseBody(req);
const rows = readJson('subgroups.json');
const idx = rows.findIndex((r) => String(r.SGid) === params.id);
if (idx === -1) return sendJson(res, 404, { message: 'Subgroup not found' });
rows[idx] = { ...rows[idx], ...payload, SGid: rows[idx].SGid };
writeJson('subgroups.json', rows);
sendJson(res, 200, rows[idx]);
});
addRoute('DELETE', '/subgroups/:id', (req, res, params) => {
const rows = readJson('subgroups.json');
const idx = rows.findIndex((r) => String(r.SGid) === params.id);
if (idx === -1) return sendJson(res, 404, { message: 'Subgroup not found' });
rows.splice(idx, 1);
writeJson('subgroups.json', rows);
sendJson(res, 204, null);
});
// ======================================================================
// SUBGROUP MEMBERSHIPS CRUDL
// ======================================================================
addRoute('GET', '/subgroup-memberships', (req, res, params, query) => {
let rows = readJson('subgroup-memberships.json');
if (query.Gid) rows = rows.filter((r) => String(r.Gid) === query.Gid);
if (query.Uid) rows = rows.filter((r) => String(r.Uid) === query.Uid);
if (query.SGid) rows = rows.filter((r) => String(r.SGid) === query.SGid);
sendJson(res, 200, rows);
});
addRoute('GET', '/subgroup-memberships/:id', (req, res, params) => {
const rows = readJson('subgroup-memberships.json');
const row = rows.find((r) => String(r.id) === params.id);
if (!row) return sendJson(res, 404, { message: 'Membership not found' });
sendJson(res, 200, row);
});
addRoute('POST', '/subgroup-memberships', async (req, res) => {
const payload = await parseBody(req);
const rows = readJson('subgroup-memberships.json');
// If a JoinCode was supplied, look up the matching subgroup
if (payload.JoinCode) {
const subgroups = readJson('subgroups.json');
const sg = subgroups.find((s) => s.JoinCode === payload.JoinCode);
if (!sg) return sendJson(res, 400, { message: 'Invalid join code' });
payload.Gid = sg.Gid;
payload.SGid = sg.SGid;
}
payload.id = nextId(rows, 'id');
payload.IsAcceptedAdmin = payload.IsAcceptedAdmin || false;
rows.push(payload);
writeJson('subgroup-memberships.json', rows);
// Update user record with group / subgroup
const users = readJson('users.json');
const uIdx = users.findIndex((u) => u.Uid === payload.Uid);
if (uIdx !== -1) {
users[uIdx].Gid = payload.Gid;
users[uIdx].SGid = payload.SGid;
writeJson('users.json', users);
}
sendJson(res, 201, payload);
});
addRoute('PUT', '/subgroup-memberships/:id', async (req, res, params) => {
const payload = await parseBody(req);
const rows = readJson('subgroup-memberships.json');
const idx = rows.findIndex((r) => String(r.id) === params.id);
if (idx === -1) return sendJson(res, 404, { message: 'Membership not found' });
rows[idx] = { ...rows[idx], ...payload, id: rows[idx].id };
writeJson('subgroup-memberships.json', rows);
sendJson(res, 200, rows[idx]);
});
addRoute('DELETE', '/subgroup-memberships/:id', (req, res, params) => {
const rows = readJson('subgroup-memberships.json');
const idx = rows.findIndex((r) => String(r.id) === params.id);
if (idx === -1) return sendJson(res, 404, { message: 'Membership not found' });
rows.splice(idx, 1);
writeJson('subgroup-memberships.json', rows);
sendJson(res, 204, null);
});
// ======================================================================
// TEAMS CRUDL
// ======================================================================
addRoute('GET', '/teams', (req, res, params, query) => {
let rows = readJson('teams.json');
if (query.Gid) rows = rows.filter((r) => String(r.Gid) === query.Gid);
sendJson(res, 200, rows);
});
addRoute('GET', '/teams/:id', (req, res, params) => {
const rows = readJson('teams.json');
const row = rows.find((r) => String(r.Tid) === params.id);
if (!row) return sendJson(res, 404, { message: 'Team not found' });
sendJson(res, 200, row);
});
addRoute('POST', '/teams', async (req, res) => {
const payload = await parseBody(req);
const rows = readJson('teams.json');
payload.Tid = nextId(rows, 'Tid');
if (!payload.JoinCode) payload.JoinCode = generateCode();
rows.push(payload);
writeJson('teams.json', rows);
sendJson(res, 201, payload);
});
addRoute('PUT', '/teams/:id', async (req, res, params) => {
const payload = await parseBody(req);
const rows = readJson('teams.json');
const idx = rows.findIndex((r) => String(r.Tid) === params.id);
if (idx === -1) return sendJson(res, 404, { message: 'Team not found' });
rows[idx] = { ...rows[idx], ...payload, Tid: rows[idx].Tid };
writeJson('teams.json', rows);
sendJson(res, 200, rows[idx]);
});
addRoute('DELETE', '/teams/:id', (req, res, params) => {
const rows = readJson('teams.json');
const idx = rows.findIndex((r) => String(r.Tid) === params.id);
if (idx === -1) return sendJson(res, 404, { message: 'Team not found' });
rows.splice(idx, 1);
writeJson('teams.json', rows);
sendJson(res, 204, null);
});
// ======================================================================
// TEAM MEMBERS CRUDL
// ======================================================================
addRoute('GET', '/team-members', (req, res, params, query) => {
let rows = readJson('team-members.json');
if (query.Tid) rows = rows.filter((r) => String(r.Tid) === query.Tid);
if (query.Uid) rows = rows.filter((r) => String(r.Uid) === query.Uid);
sendJson(res, 200, rows);
});
addRoute('GET', '/team-members/:id', (req, res, params) => {
const rows = readJson('team-members.json');
const row = rows.find((r) => String(r.id) === params.id);
if (!row) return sendJson(res, 404, { message: 'Team member not found' });
sendJson(res, 200, row);
});
addRoute('POST', '/team-members', async (req, res) => {
const payload = await parseBody(req);
const rows = readJson('team-members.json');
// If a JoinCode was supplied, look up the matching team
if (payload.JoinCode) {
const teams = readJson('teams.json');
const team = teams.find((t) => t.JoinCode === payload.JoinCode);
if (!team) return sendJson(res, 400, { message: 'Invalid team join code' });
payload.Tid = team.Tid;
}
payload.id = nextId(rows, 'id');
rows.push(payload);
writeJson('team-members.json', rows);
// Update user TGid
const users = readJson('users.json');
const uIdx = users.findIndex((u) => u.Uid === payload.Uid);
if (uIdx !== -1) {
users[uIdx].TGid = payload.Tid;
writeJson('users.json', users);
}
sendJson(res, 201, payload);
});
addRoute('PUT', '/team-members/:id', async (req, res, params) => {
const payload = await parseBody(req);
const rows = readJson('team-members.json');
const idx = rows.findIndex((r) => String(r.id) === params.id);
if (idx === -1) return sendJson(res, 404, { message: 'Team member not found' });
rows[idx] = { ...rows[idx], ...payload, id: rows[idx].id };
writeJson('team-members.json', rows);
sendJson(res, 200, rows[idx]);
});
addRoute('DELETE', '/team-members/:id', (req, res, params) => {
const rows = readJson('team-members.json');
const idx = rows.findIndex((r) => String(r.id) === params.id);
if (idx === -1) return sendJson(res, 404, { message: 'Team member not found' });
rows.splice(idx, 1);
writeJson('team-members.json', rows);
sendJson(res, 204, null);
});
// ======================================================================
// ADMIN WAITLIST CRUDL
// ======================================================================
addRoute('GET', '/admin-waitlist', (req, res, params, query) => {
let rows = readJson('admin-waitlist.json');
if (query.Gid) rows = rows.filter((r) => String(r.Gid) === query.Gid);
sendJson(res, 200, rows);
});
addRoute('POST', '/admin-waitlist', async (req, res) => {
const payload = await parseBody(req);
const rows = readJson('admin-waitlist.json');
payload.id = nextId(rows, 'id');
rows.push(payload);
writeJson('admin-waitlist.json', rows);
sendJson(res, 201, payload);
});
addRoute('DELETE', '/admin-waitlist/:id', (req, res, params) => {
const rows = readJson('admin-waitlist.json');
const idx = rows.findIndex((r) => String(r.id) === params.id);
if (idx === -1) return sendJson(res, 404, { message: 'Waitlist entry not found' });
rows.splice(idx, 1);
writeJson('admin-waitlist.json', rows);
sendJson(res, 204, null);
});
// ======================================================================
// GAME DATA CRUDL
// ======================================================================
addRoute('GET', '/game-data', (req, res) => {
sendJson(res, 200, readJson('game-data.json'));
});
addRoute('GET', '/game-data/:gid', (req, res, params) => {
const rows = readJson('game-data.json');
const row = rows.find((r) => String(r.Gid) === params.gid);
if (!row) return sendJson(res, 404, { message: 'Game data not found' });
sendJson(res, 200, row);
});
// ======================================================================
// CACHES (nested under game-data) CRUDL
// ======================================================================
// Helper: convert storage cache to API response format
const toCacheApi = (cache, gid) => ({
id: cache.CacheId,
coordinates: { latitude: cache.Latitude, longitude: cache.Longitude },
radius: cache.TriggerMeters,
clue: cache.Title,
groupId: Number(gid),
subgroupId: cache.SGid,
ClaimedByUid: cache.ClaimedByUid || null,
ClaimedByTid: cache.ClaimedByTid || null,
});
// Helper: convert API payload to storage cache format
const toCacheStorage = (data, cacheId) => ({
CacheId: cacheId,
Title: data.clue || data.Title || '',
Latitude: data.latitude != null ? data.latitude : (data.Latitude || 0),
Longitude: data.longitude != null ? data.longitude : (data.Longitude || 0),
TriggerMeters: data.radius || data.TriggerMeters || 20,
SGid: data.subgroupId != null ? data.subgroupId : (data.SGid || null),
ClaimedByUid: data.ClaimedByUid || null,
ClaimedByTid: data.ClaimedByTid || null,
});
// GET /game-data/:gid/caches — list caches for a group
addRoute('GET', '/game-data/:gid/caches', (req, res, params, query) => {
const rows = readJson('game-data.json');
const entry = rows.find((r) => String(r.Gid) === params.gid);
if (!entry) return sendJson(res, 404, { message: 'Game data not found' });
let caches = (entry.GeoCaches || []).map((c) => toCacheApi(c, params.gid));
if (query.SGid) caches = caches.filter((c) => String(c.subgroupId) === query.SGid);
sendJson(res, 200, caches);
});
// GET /game-data/:gid/caches/:cacheId — read single cache
addRoute('GET', '/game-data/:gid/caches/:cacheId', (req, res, params) => {
const rows = readJson('game-data.json');
const entry = rows.find((r) => String(r.Gid) === params.gid);
if (!entry) return sendJson(res, 404, { message: 'Game data not found' });
const cache = (entry.GeoCaches || []).find((c) => c.CacheId === params.cacheId);
if (!cache) return sendJson(res, 404, { message: 'Cache not found' });
sendJson(res, 200, toCacheApi(cache, params.gid));
});
// POST /game-data/:gid/caches — create a new cache
addRoute('POST', '/game-data/:gid/caches', async (req, res, params) => {
const payload = await parseBody(req);
const rows = readJson('game-data.json');
const entry = rows.find((r) => String(r.Gid) === params.gid);
if (!entry) return sendJson(res, 404, { message: 'Game data not found' });
const existingIds = (entry.GeoCaches || []).map((c) => {
const num = parseInt(String(c.CacheId).replace(/\D/g, ''), 10);
return isNaN(num) ? 0 : num;
});
const nextNum = existingIds.length > 0 ? Math.max(...existingIds) + 1 : 1;
const cacheId = `C${nextNum}`;
const stored = toCacheStorage(payload, cacheId);
if (!entry.GeoCaches) entry.GeoCaches = [];
entry.GeoCaches.push(stored);
writeJson('game-data.json', rows);
sendJson(res, 201, toCacheApi(stored, params.gid));
});
// PUT /game-data/:gid/caches/:cacheId — update a cache
addRoute('PUT', '/game-data/:gid/caches/:cacheId', async (req, res, params) => {
const payload = await parseBody(req);
const rows = readJson('game-data.json');
const entry = rows.find((r) => String(r.Gid) === params.gid);
if (!entry) return sendJson(res, 404, { message: 'Game data not found' });
const idx = (entry.GeoCaches || []).findIndex((c) => c.CacheId === params.cacheId);
if (idx === -1) return sendJson(res, 404, { message: 'Cache not found' });
const existing = entry.GeoCaches[idx];
const updated = { ...existing, ...toCacheStorage(payload, params.cacheId) };
entry.GeoCaches[idx] = updated;
writeJson('game-data.json', rows);
sendJson(res, 200, toCacheApi(updated, params.gid));
});
// DELETE /game-data/:gid/caches/:cacheId — delete a cache
addRoute('DELETE', '/game-data/:gid/caches/:cacheId', (req, res, params) => {
const rows = readJson('game-data.json');
const entry = rows.find((r) => String(r.Gid) === params.gid);
if (!entry) return sendJson(res, 404, { message: 'Game data not found' });
const idx = (entry.GeoCaches || []).findIndex((c) => c.CacheId === params.cacheId);
if (idx === -1) return sendJson(res, 404, { message: 'Cache not found' });
entry.GeoCaches.splice(idx, 1);
writeJson('game-data.json', rows);
sendJson(res, 204, null);
});
// ======================================================================
// HEALTH CHECK
// ======================================================================
addRoute('GET', '/health', (req, res) => {
sendJson(res, 200, { ok: true });
});
// ======================================================================
// SERVER
// ======================================================================
const server = http.createServer(async (req, res) => {
// CORS preflight
if (req.method === 'OPTIONS') return sendJson(res, 200, { ok: true });
const url = new URL(req.url, 'http://localhost');
const pathname = url.pathname.replace(/\/+$/, '') || '/';
const query = Object.fromEntries(url.searchParams);
const matched = matchRoute(req.method, pathname);
if (matched) {
try {
await matched.handler(req, res, matched.params, query);
} catch (err) {
sendJson(res, 500, { message: err.message });
}
return;
}
sendJson(res, 404, { message: 'Not found' });
});
server.listen(PORT, '0.0.0.0', () => {
process.stdout.write(`Server running on http://0.0.0.0:${PORT}\n`);
process.stdout.write(` -> Local: http://localhost:${PORT}\n`);
// Log the LAN IP so users know what to point to from other devices
const nets = require('os').networkInterfaces();
for (const name of Object.keys(nets)) {
for (const net of nets[name]) {
if (net.family === 'IPv4' && !net.internal) {
process.stdout.write(` -> Network: http://${net.address}:${PORT}\n`);
}
}
}
});
server.on('error', (err) => {
if (err.code === 'EADDRINUSE') {
process.stderr.write(`ERROR: Port ${PORT} is already in use.\n`);
} else {
process.stderr.write(`ERROR: ${err.message}\n`);
}
process.exit(1);
});