Compare commits

..

47 Commits

Author SHA1 Message Date
11f72679a0 Fix incorrect variable name when finding a partial PIS match
Signed-off-by: Fred Boniface <fred@fjla.uk>
2025-03-12 00:33:46 +00:00
e12b809d04 Final statistics fix - until service rewrite
Signed-off-by: Fred Boniface <fred@fjla.uk>
2025-03-06 22:08:03 +00:00
9ab5a7be99 Fix stats
Signed-off-by: Fred Boniface <fred@fjla.uk>
2025-03-06 22:00:04 +00:00
8b00a9afe5 Patch stats service
Signed-off-by: Fred Boniface <fred@fjla.uk>
2025-03-06 21:56:18 +00:00
c4646bc654 Fix statistics:
- Remove unused values
 - Add additional fetch for Stations data

Signed-off-by: Fred Boniface <fred@fjla.uk>
2025-03-06 21:52:11 +00:00
989d14ff95 Bump version
Signed-off-by: Fred Boniface <fred@fjla.uk>
2025-03-06 21:39:19 +00:00
a561357fbe Add functions & pipeline for partial tiploca match which requires skipping end stations
Signed-off-by: Fred Boniface <fred@fjla.uk>
2025-03-06 21:37:33 +00:00
4d2262f349 Re-patch db connection
Signed-off-by: Fred Boniface <fred@fjla.uk>
2024-11-22 21:17:07 +00:00
eecafee7cf Patch mongodb connection string
Signed-off-by: Fred Boniface <fred@fjla.uk>
2024-11-22 21:09:04 +00:00
d54e223369 Fix authentication database setting
Signed-off-by: Fred Boniface <fred@fjla.uk>
2024-11-22 20:54:58 +00:00
78c5c02c0e Bump version
Signed-off-by: Fred Boniface <fred@fjla.uk>
2024-11-01 19:49:09 +00:00
a793862aa2 Search by code was searching for intergers which was returning no results. Now updated to search the database with code as a string.
Signed-off-by: Fred Boniface <fred@fjla.uk>
2024-11-01 19:47:32 +00:00
be1ee0b285 Bump version 2024-07-05 10:13:07 +01:00
196251eeb6 Update mail templates to try and avoid junk filtering 2024-07-05 10:11:01 +01:00
e913db5d57 Fix statistics to work with timetable-mgr metadata
Signed-off-by: Fred Boniface <fred@fjla.uk>
2024-07-01 13:23:42 +01:00
fdcb43b5c2 Update version to poll timetablemgr instead of mqclient
Signed-off-by: Fred Boniface <fred@fjla.uk>
2024-06-30 21:30:38 +01:00
9031eb53c6 Add find nearest feature
Signed-off-by: Fred Boniface <fred@fjla.uk>
2024-06-30 21:23:22 +01:00
2a9050940d Add routes for find station by nearest
Signed-off-by: Fred Boniface <fred@fjla.uk>
2024-06-30 21:01:06 +01:00
e1fc4b1db2 Add pipeline for finding station by distance
Signed-off-by: Fred Boniface <fred@fjla.uk>
2024-06-30 20:46:37 +01:00
6cfc42f22e DB Setup?
Signed-off-by: Fred Boniface <fred@fjla.uk>
2024-05-01 20:46:12 +01:00
9d51d4e45e Bump version
Signed-off-by: Fred Boniface <fred@fjla.uk>
2024-04-29 11:21:31 +01:00
fde37814a7 Add delay to LDB retry after ENOTFOUND error.
Signed-off-by: Fred Boniface <fred@fjla.uk>
2024-04-29 11:20:27 +01:00
8fa0cf775f Fix LDB Cache, so when data is missing, nothing is cached
Signed-off-by: Fred Boniface <fred@fjla.uk>
2024-04-29 11:16:40 +01:00
afa4ad7915 Remove logging succesful authentications - fills logs with useless lines
Signed-off-by: Fred Boniface <fred@fjla.uk>
2024-04-29 11:10:41 +01:00
d49a5ae034 Bump version
Signed-off-by: Fred Boniface <fred@fjla.uk>
2024-04-24 20:31:43 +01:00
e7b8208edf Add retry to LDB Staff lookup
Signed-off-by: Fred Boniface <fred@fjla.uk>
2024-04-24 20:29:50 +01:00
dad9f46d86 Set Cache-Control headers in route controllers
Signed-off-by: Fred Boniface <fred@fjla.uk>
2024-04-24 20:07:24 +01:00
c698187cdf Remove compression from Express, add CacheHeaders middleware.
Signed-off-by: Fred Boniface <fred@fjla.uk>
2024-04-23 20:47:47 +01:00
90500b88af Bump version
Signed-off-by: Fred Boniface <fred@fjla.uk>
2024-04-23 20:23:01 +01:00
77ca61e178 Adjust query order, remove console.logs
Signed-off-by: Fred Boniface <fred@fjla.uk>
2024-04-23 20:20:51 +01:00
2ff822d8eb Add "HX" (Heathrow Express) to PIS supported operators
Signed-off-by: Fred Boniface <fred@fjla.uk>
2024-04-23 19:54:34 +01:00
09f883a461 Adjust how serviceDetail is presented
Signed-off-by: Fred Boniface <fred@fjla.uk>
2024-04-23 15:08:16 +01:00
d98b560584 Update dependencies
Signed-off-by: Fred Boniface <fred@fjla.uk>
2024-04-23 15:07:05 +01:00
f02ae3c7cd Add Platform, Pass, Arrline, and depline to stopDetail
Signed-off-by: Fred Boniface <fred@fjla.uk>
2024-04-17 13:17:25 +01:00
1f0a39adc6 Fix bug where cancelled service is not processed and sent to client due to no stops arrray being present.
Signed-off-by: Fred Boniface <fred@fjla.uk>
2024-04-16 21:31:09 +01:00
f4b5e9ce37 Add service booleans
Signed-off-by: Fred Boniface <fred@fjla.uk>
2024-04-15 21:50:56 +01:00
a5a73812a9 Tidy 'pis' related code
Signed-off-by: Fred Boniface <fred@fjla.uk>
2024-03-10 21:16:21 +00:00
87532b001d Tidy auth middleware
Signed-off-by: Fred Boniface <fred@fjla.uk>
2024-03-10 21:01:00 +00:00
236d85648d Replace registration stream with code based
Signed-off-by: Fred Boniface <fred@fjla.uk>
2024-03-10 20:43:58 +00:00
91e2657d66 More work on code auth
Signed-off-by: Fred Boniface <fred@fjla.uk>
2024-03-09 20:56:51 +00:00
874b236f09 Needs testing
Signed-off-by: Fred Boniface <fred@fjla.uk>
2024-03-09 20:46:17 +00:00
5904ee37cd Adjust mail templates for registration code
Signed-off-by: Fred Boniface <fred@fjla.uk>
2024-03-09 19:31:59 +00:00
70c9aa2b1e Begin migration to registration codes
Signed-off-by: Fred Boniface <fred@fjla.uk>
2024-03-08 21:39:39 +00:00
8e0b928f27 Temporarily disable PIS and TRAIN auth reference: #71
Signed-off-by: Fred Boniface <fred@fjla.uk>
2024-03-01 22:16:56 +00:00
ac9372515f Bump version
Signed-off-by: Fred Boniface <fred@fjla.uk>
2024-02-22 12:02:32 +00:00
4cc6856a76 Remove limiter, fix find TrainUID by date
Signed-off-by: Fred Boniface <fred@fjla.uk>
2024-02-22 12:02:13 +00:00
d15b7c3c7a Run NPM Audit, Update email template, Bump version
Signed-off-by: Fred Boniface <fred@fjla.uk>
2024-02-19 11:40:41 +00:00
31 changed files with 1630 additions and 1530 deletions

10
app.js
View File

@ -1,5 +1,4 @@
// OwlBoard - © Fred Boniface 2022-2023 - Licensed under GPLv3 (or later) // OwlBoard - © Fred Boniface 2022-2023 - Licensed under GPLv3 (or later)
// Please see the included LICENSE file // Please see the included LICENSE file
const mode = process.env.NODE_ENV || "development"; const mode = process.env.NODE_ENV || "development";
@ -13,7 +12,6 @@ const express = require("express");
const app = express(); const app = express();
// Middleware // Middleware
const compression = require("compression");
const rateLimit = require("express-rate-limit"); const rateLimit = require("express-rate-limit");
const cors = require("cors"); const cors = require("cors");
const authenticate = require("./src/middlewares/auth.middlewares"); const authenticate = require("./src/middlewares/auth.middlewares");
@ -52,15 +50,14 @@ app.use((err, req, res, next) => {
return; return;
}); });
// Global Middleware: // Pre Middleware:
app.use( app.use(
cors({ cors({
origin: "*", //[/\.owlboard\.info$/, 'localhost:5173', 'localhost:4173'] origin: "*", //[/\.owlboard\.info$/, 'localhost:5173', 'localhost:4173']
}) })
); );
app.use(express.json()); //JSON Parsing for POST Requests app.use(express.json()); //JSON Parsing for POST Requests
app.use(compression()); // Compress API Data if supported by client //app.use(limiter);
app.use(limiter);
app.use(authenticate); app.use(authenticate);
// 2023 Rationalisation Routes (/api/v2, /misc) // 2023 Rationalisation Routes (/api/v2, /misc)
@ -84,6 +81,9 @@ mode === "development"
? app.get("/api/v1/ip", (req, res) => res.send(req.ip)) ? app.get("/api/v1/ip", (req, res) => res.send(req.ip))
: null; : null;
// Disable etags
app.set('etag', false)
// Start Express // Start Express
app.listen(srvPort, srvListen, (error) => { app.listen(srvPort, srvListen, (error) => {
if (!error) { if (!error) {

View File

@ -6,6 +6,10 @@
html { html {
text-align: center; text-align: center;
width: 100%; width: 100%;
margin: 0;
padding: 0;
background-color: #404c55;
background-image: radial-gradient(#2b343c, #404c55);
} }
body { body {
margin: 0; margin: 0;
@ -25,6 +29,10 @@
background-color: #404c55; background-color: #404c55;
background-image: radial-gradient(#2b343c, #404c55); background-image: radial-gradient(#2b343c, #404c55);
} }
p {
margin-left: 40px;
margin-right: 40px;
}
#title { #title {
height: 100px; height: 100px;
padding-top: 0px; padding-top: 0px;
@ -43,16 +51,24 @@
text-decoration: none; text-decoration: none;
border-radius: 14px; border-radius: 14px;
} }
.digits {
color: azure;
font-size: xx-large;
font-weight: bolder;
letter-spacing: 0.75ch;
margin-left: 0.75ch;
}
</style> </style>
</head> </head>
<body> <body>
<br /><br /> <br><br>
<table> <table>
<tr> <tr>
<td> <td>
<img <img
src="https://owlboard.info/images/logo/wide_logo.svg" src="https://owlboard.info/images/logo/wide_logo.svg"
id="title" id="title"
alt="OwlBoard Logo"
/> />
</td> </td>
</tr> </tr>
@ -61,27 +77,16 @@
<h1>Register for OwlBoard</h1> <h1>Register for OwlBoard</h1>
<br /> <br />
<p> <p>
Tap the button to register this device, or scan the barcode with You'll need to type your registration code in to the OwlBoard app
another device.
</p> </p>
<br /> <br />
<a <h2>Your Code:</h2>
href="https://owlboard.info/more/reg/submit?key=>>ACCESSCODE<<" <span class="digits">987654</span>
id="button" <br><br>
>Register this device</a
>
<br /><br /><br />
<p>Or scan with the device you want to register</p>
<br />
<img
src="https://barcodes.fjla.uk/generate?type=qr&text=https%3A%2F%2Fowlboard.info%2Fmore%2Freg%2Fsubmit%3Fkey%3D>>ACCESSCODE<<"
alt="Scan barcode to register on another device"
title="Scan to register on another device"
/>
<br /><br /><br />
<p> <p>
Alternatively copy and paste the link:<br />https://owlboard.info/more/reg/submit?key=>>ACCESSCODE<< Go back to OwlBoard and enter your code. Go to the registration page and click the link at the top.
</p> </p>
<br /><br /><br />
<p> <p>
This registration is for one device only, you can register again This registration is for one device only, you can register again
using the same email address for other devices and access OwlBoard using the same email address for other devices and access OwlBoard
@ -92,7 +97,7 @@
can safely ignore this email. Your email address has not been stored can safely ignore this email. Your email address has not been stored
by us. by us.
</p> </p>
<p>The registration link will expire after 30 minutes.</p> <p>The registration link will expire after 1 hour.</p>
</td> </td>
</tr> </tr>
</table> </table>

View File

@ -1,12 +1,10 @@
Complete your OwlBoard (Staff) Registration using the link below. Complete your OwlBoard (Staff) Registration by entering your six digit code.
https://owlboard.info/more/reg/submit?key=>>ACCESSCODE<< 987654
Alternatively you can copy and paste the above link into your browser. Go back to the OwlBoard app, goto "Menu > Registration" and click on the link at the top to enter your code.
You can also view a QR code to register on another device: https://barcodes.fjla.uk/generate?type=qr&text=https%3A%2F%2Fowlboard.info%2Fmore%2Freg%2Fsubmit%3Fkey%3D>>ACCESSCODE<<
If you did not request to register to OwlBoard then you can safely ignore this email. If you did not request to register to OwlBoard then you can safely ignore this email.
Your email address has not been stored by us and will not be required unless you wish to register again. Your email address has not been stored by us and will not be required unless you wish to register again.
The link will expire after 30 minutes. The link will expire after 1 hour.

2312
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
{ {
"name": "owlboard", "name": "owlboard-backend",
"version": "2024.2.1", "version": "2024.2.2",
"description": "OwlBoard is an API and PWA for live rail departure board in the UK.", "description": "Provides LDB, PIS and live train details for the OwlBoard web client",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://git.fjla.uk/owlboard/backend.git" "url": "https://git.fjla.uk/owlboard/backend.git"
@ -27,13 +27,13 @@
"ldbs-json": "^1.2.1", "ldbs-json": "^1.2.1",
"moment-timezone": "^0.5.43", "moment-timezone": "^0.5.43",
"mongodb": "^4.13.0", "mongodb": "^4.13.0",
"nodemailer": "^6.9.1", "nodemailer": "^6.9.9",
"pino": "^8.15.1", "pino": "^8.15.1",
"redis": "^4.6.7", "redis": "^4.6.7",
"zlib": "^1.0.5" "zlib": "^1.0.5"
}, },
"devDependencies": { "devDependencies": {
"@owlboard/ts-types": "^0.1.8", "@owlboard/ts-types": "^1.1.0",
"@types/express": "^4.17.21", "@types/express": "^4.17.21",
"@types/jest": "^29.5.3", "@types/jest": "^29.5.3",
"eslint": "^8.39.0", "eslint": "^8.39.0",

View File

@ -1,4 +1,4 @@
const valid: string[] = [ export const valid: string[] = [
"owlboard.info", "owlboard.info",
"avantiwestcoast.co.uk", "avantiwestcoast.co.uk",
"btp.police.uk", "btp.police.uk",
@ -32,6 +32,3 @@ const valid: string[] = [
"tfwrail.wales", "tfwrail.wales",
"wmtrains.co.uk", "wmtrains.co.uk",
]; ];
module.exports = valid;
export { valid };

View File

@ -5,7 +5,7 @@ interface versions {
const version: versions = { const version: versions = {
api: ["/api/v2"], api: ["/api/v2"],
app: "2024.2.1", app: "2025.03.2",
}; };
module.exports = version; module.exports = version;

View File

@ -4,6 +4,7 @@ const log = require("../utils/logs.utils");
async function post(req, res, next) { async function post(req, res, next) {
try { try {
log.out(`issueControllers.post: Request Body: ${JSON.stringify(req.body)}`); log.out(`issueControllers.post: Request Body: ${JSON.stringify(req.body)}`);
setCache(res, "no-store")
res.json(await issue.processor(req.body)); res.json(await issue.processor(req.body));
} catch (err) { } catch (err) {
console.error("Controller Error", err.message); console.error("Controller Error", err.message);

View File

@ -1,5 +1,8 @@
const ldb = require("../services/ldb.services"); const ldb = require("../services/ldb.services");
import { setCache } from "../utils/cacheHeader.utils";
import { logger } from "../utils/logger.utils";
async function getTrain(req, res, next) { async function getTrain(req, res, next) {
// API v2 Only // API v2 Only
if (!req.isAuthed) { if (!req.isAuthed) {
@ -7,6 +10,7 @@ async function getTrain(req, res, next) {
err.status = 401; err.status = 401;
throw err; throw err;
} }
setCache(res, "private", 240)
let type = req.params.searchType; let type = req.params.searchType;
let id = req.params.id; let id = req.params.id;
try { try {
@ -42,18 +46,53 @@ async function getStation(req, res, next) {
err.status = 401; err.status = 401;
return next(err); return next(err);
} }
res.json(await ldb.get(id, true)); const data = await ldb.get(id, true);
// Only cache if data is present
if (data.data) {
setCache(res, "public", 120);
} else {
setCache(res, "no-store", 120);
}
res.json(data);
} else { } else {
setCache(res, "public", 240)
res.json(await ldb.get(id, false)); res.json(await ldb.get(id, false));
} }
} catch (err) { } catch (err) {
setCache(res, "no-store")
console.error("Unknown Error", err.message); console.error("Unknown Error", err.message);
err.status = 500; err.status = 500;
next(err); next(err);
} }
} }
async function getNearest(req, res, next) {
// API v2 Only
let latitude = req.params.latitude;
let longitude = req.params.longitude;
try {
if (!req.isAuthed) {
const err = new Error("Unauthorized");
err.status = 401;
return next(err)
}
const data = await ldb.getNearestStations(latitude, longitude)
if (data) {
setCache(res, "private", 120)
} else {
setCache(res, "no-store", 120)
}
res.json(data)
} catch (err) {
setCache(res, "no-store")
logger.Error("Error fetching nearest station")
err.status = 500;
next(err)
}
}
module.exports = { module.exports = {
getTrain, getTrain,
getStation, getStation,
getNearest,
}; };

View File

@ -1,54 +0,0 @@
const pis = require("../services/pis.services");
/* Used in /api/v2 */
async function byStartEndCRS(req, res, next) {
if (!req.isAuthed) {
const err = new Error("Unauthorized");
err.status = 401;
return next(err);
}
try {
let startCrs = req.params.startCrs;
let endCrs = req.params.endCrs;
res.json(await pis.findPisByOrigDest(startCrs, endCrs));
} catch (err) {
console.error("Unknown Error", err.message);
return next(err);
}
}
/* Used in /api/v2 */
async function byCode(req, res, next) {
if (!req.isAuthed) {
const err = new Error("Unauthorized");
err.status = 401;
return next(err);
}
try {
let code = req.params.code;
res.json(await pis.findPisByCode(code));
} catch (err) {
console.error("Unknown Error", err.message);
return next(err);
}
}
async function random(req, res, next) {
if (!req.isAuthed) {
const err = new Error("Unauthorized");
err.status = 401;
return next(err);
}
try {
res.json(await pis.findRandom());
} catch (err) {
console.error("Unknown Error", err.message);
return next(err);
}
}
module.exports = {
byStartEndCRS,
byCode,
random,
};

View File

@ -0,0 +1,40 @@
import { Request, Response, NextFunction } from "express";
import { findPisByOrigDest, findPisByCode } from "../services/pis.services";
import { setCache } from "../utils/cacheHeader.utils";
async function byStartEndCRS(req: Request, res: Response, next: NextFunction) {
// if (!req.isAuthed) {
// const err = new Error("Unauthorized");
// err.status = 401;
// return next(err);
// }
try {
let startCrs = req.params.startCrs;
let endCrs = req.params.endCrs;
setCache(res, "public", 600)
res.json(await findPisByOrigDest(startCrs, endCrs));
} catch (err: any) {
console.error("Unknown Error", err.message);
return next(err);
}
}
async function byCode(req: Request, res: Response, next: NextFunction) {
// if (!req.isAuthed) {
// const err = new Error("Unauthorized");
// err.status = 401;
// return next(err);
// }
try {
let code = req.params.code;
res.json(await findPisByCode(code));
} catch (err: any) {
console.error("Unknown Error", err.message);
return next(err);
}
}
module.exports = {
byStartEndCRS,
byCode,
};

View File

@ -1,5 +1,7 @@
/* API V2 Exclusive Controller */ /* API V2 Exclusive Controller */
import { setCache } from "../utils/cacheHeader.utils";
const ldb = require("../services/ldb.services"); const ldb = require("../services/ldb.services");
const find = require("../services/find.services"); const find = require("../services/find.services");
@ -7,6 +9,7 @@ async function getReasonCode(req, res, next) {
try { try {
const code = req.params.code; const code = req.params.code;
if (code === "all") { if (code === "all") {
setCache(res, "public", 604800)
res.json(await ldb.getReasonCodeList()); res.json(await ldb.getReasonCodeList());
next; next;
} }
@ -15,6 +18,7 @@ async function getReasonCode(req, res, next) {
} catch (err) { } catch (err) {
console.error("ERROR", err.message); console.error("ERROR", err.message);
err.status = 500; err.status = 500;
setCache(res, "no-store", 5)
next(err); next(err);
} }
} }
@ -23,6 +27,7 @@ async function getLocationReference(req, res, next) {
try { try {
const searchType = req.params.searchType; const searchType = req.params.searchType;
const id = req.params.id; const id = req.params.id;
setCache(res, "public", 604800)
switch (searchType) { switch (searchType) {
case "name": case "name":
res.json(await find.name(id)); res.json(await find.name(id));

View File

@ -1,8 +1,11 @@
import { setCache } from "../utils/cacheHeader.utils";
const stat = require("../services/stats.services"); const stat = require("../services/stats.services");
async function versions(req, res, next) { async function versions(req, res, next) {
// API v2 // API v2
try { try {
setCache(res, "public", 60)
res.json(await stat.getVersions()); res.json(await stat.getVersions());
} catch (err) { } catch (err) {
console.error("Controller Error", err); console.error("Controller Error", err);
@ -14,6 +17,7 @@ async function versions(req, res, next) {
async function statistics(req, res, next) { async function statistics(req, res, next) {
// Api v2 // Api v2
try { try {
setCache(res, "public", 60)
res.json(await stat.statistics()); res.json(await stat.statistics());
} catch (err) { } catch (err) {
console.error("Controller Error", err); console.error("Controller Error", err);

View File

@ -1,3 +1,4 @@
import { setCache } from "../utils/cacheHeader.utils";
import { logger } from "../utils/logger.utils"; import { logger } from "../utils/logger.utils";
const train = require("../services/trainService.services"); const train = require("../services/trainService.services");
@ -5,11 +6,11 @@ const train = require("../services/trainService.services");
async function getByHeadcodeToday(req, res, next) { async function getByHeadcodeToday(req, res, next) {
// Deprecated - for future removal. // Deprecated - for future removal.
logger.warn("Deprecated Function Called - trainService.services-getByHeadcodeToday") logger.warn("Deprecated Function Called - trainService.services-getByHeadcodeToday")
if (!req.isAuthed) { // if (!req.isAuthed) {
const err = new Error("Unauthorized"); // const err = new Error("Unauthorized");
err.status = 401; // err.status = 401;
next(err); // next(err);
} // }
try { try {
var searchHeadcode = req.params.id; var searchHeadcode = req.params.id;
res.json(await train.findByHeadcodeToday(searchHeadcode)); res.json(await train.findByHeadcodeToday(searchHeadcode));
@ -21,20 +22,22 @@ async function getByHeadcodeToday(req, res, next) {
} }
async function get(req, res, next) { async function get(req, res, next) {
if (!req.isAuthed) { // if (!req.isAuthed) {
const err = new Error("Unauthorized"); // const err = new Error("Unauthorized");
err.status = 401; // err.status = 401;
next(err); // next(err);
} // }
let date = req.params.date; let date = req.params.date;
let searchType = req.params.searchType; let searchType = req.params.searchType;
let id = req.params.id; let id = req.params.id;
try { try {
switch (searchType) { switch (searchType) {
case "headcode": case "headcode":
setCache(res, "private", 1800)
res.json(await train.findByHeadcode(id, date)); res.json(await train.findByHeadcode(id, date));
break; break;
case "byTrainUid": case "byTrainUid":
setCache(res, "private", 1800)
res.json(await train.findByTrainUid(id, date)); res.json(await train.findByTrainUid(id, date));
break; break;
default: default:

View File

@ -1,46 +1,43 @@
import { NextFunction, Request, Response } from "express"; import type { NextFunction, Request, Response } from "express";
import { logger } from "../utils/logger.utils";
const utils = require("../utils/auth.utils"); import { isAuthed } from "../utils/auth.utils";
const logger = require("../utils/logger.utils");
module.exports = async function authCheck( module.exports = async function authCheck(
req: Request, req: Request,
res: Response, res: Response,
next: NextFunction next: NextFunction
) { ) {
logger.logger.debug("auth.middleware: Auth check begun"); logger.debug("auth.middleware: Auth check begun");
if (process.env.NODE_ENV === "development") { if (process.env.NODE_ENV === "development") {
req.isAuthed = true; req.isAuthed = true;
logger.logger.warn("auth.middleware: DEV MODE - Access Granted"); logger.warn("auth.middleware: DEV MODE - Access Granted");
next(); next();
} else { } else {
const id: string | string[] | undefined = req.headers.uuid; const id: string | string[] | undefined = req.headers.uuid;
if (typeof id === "undefined") { if (typeof id === "undefined") {
req.isAuthed = false; req.isAuthed = false;
logger.logger.info("auth.middleware: Authentication failed"); logger.info("auth.middleware: Authentication failed");
next(); next();
} else if (typeof id === "string") { } else if (typeof id === "string") {
const authCheck = (await utils.isAuthed(id)) || false; const authCheck = (await isAuthed(id)) || false;
if (authCheck) { if (authCheck) {
// Authenticate
req.isAuthed = true; req.isAuthed = true;
logger.logger.info("auth.middleware: Authentication Successful");
next(); next();
} else { } else {
req.isAuthed = false; req.isAuthed = false;
logger.logger.info("auth.middleware: Authentication Failed"); logger.info("auth.middleware: Authentication Failed");
next(); next();
} }
// Handle cases where UUID passed as an array
} else if (Array.isArray(id)) { } else if (Array.isArray(id)) {
const authCheck = (await utils.isAuthed(id[0])) || false; const authCheck = (await isAuthed(id[0])) || false;
if (authCheck) { if (authCheck) {
req.isAuthed = true; req.isAuthed = true;
logger.logger.warn(
"auth.middleware: UUID Passed as Array - Authentication Successful"
);
next(); next();
} else { } else {
req.isAuthed = false; req.isAuthed = false;
logger.logger.warn( logger.warn(
"auth.middleware: UUID Passed as Array - Authentication Failed" "auth.middleware: UUID Passed as Array - Authentication Failed"
); );
next(); next();

View File

@ -4,6 +4,7 @@ const ldbCtr = require("../controllers/ldb.controllers");
// PIS // PIS
router.get("/station/:id/:type", ldbCtr.getStation); router.get("/station/:id/:type", ldbCtr.getStation);
router.get("/station/nearest/:latitude/:longitude", ldbCtr.getNearest);
router.get("/train/:searchType/:id", ldbCtr.getTrain); router.get("/train/:searchType/:id", ldbCtr.getTrain);
module.exports = router; module.exports = router;

View File

@ -1,225 +0,0 @@
const db = require("./dbAccess.services");
const clean = require("../utils/sanitizer.utils");
const pis = require("../services/pis.services");
import { logger } from "../utils/logger.utils";
import type { TrainServices, Service, Stop } from "@owlboard/ts-types";
// This function is deprecated and should no longer be used.
// It will be removed in a later version
async function findByHeadcodeToday(headcode: string) {
const sanitizedHeadcode = clean.removeNonAlphanumeric(headcode).toUpperCase();
logger.debug(
`trainServiceServices.findByHeadcode: Searching for headcode ${sanitizedHeadcode}`
);
const now = new Date();
const dayMap = ["su", "m", "t", "w", "th", "f", "s"];
const shortDay = dayMap[now.getDay()]; // Fetch short day from map
const query = {
headcode: sanitizedHeadcode,
scheduleStartDate: { $lte: now },
scheduleEndDate: { $gte: now },
daysRun: { $in: [shortDay] },
};
const queryData = await db.query("timetable", query);
let trainData = await parseTrains(queryData);
let preparedData = [];
for (let trainService in trainData) {
if (pis.supported.includes(trainService?.operator)) {
// Search for PIS Code for each service
const tiplocList = await getPublicStops(trainService?.stops);
//console.log(tiplocList.length); console.log(tiplocList);
if (tiplocList.length) {
const pisDetail = await pis.findByTiplocArray(tiplocList);
trainService["pis"] = pisDetail?.[0]?.["code"] ?? "None";
} else if (trainService?.operator === "GW") {
trainService["pis"] = "0015"; // Not in Service code
// '0015' is a string becuase 0015 is not a valid number..
}
}
preparedData.push(trainService);
}
return preparedData;
}
// Finds a train by its headcode value
async function findByHeadcode(
date: string | Date,
headcode: string
): Promise<TrainServices[]> {
const sanitizedHeadcode = clean.removeNonAlphanumeric(headcode).toUpperCase();
logger.debug(
`trainServiceServices.findByHeadcode: Searching for headcode ${sanitizedHeadcode}`
);
let searchDate;
if (date === "now") {
searchDate = new Date();
} else {
searchDate = new Date(date);
}
searchDate.setHours(12, 0, 0); // Set to midday to avoid any timezone issues
const dayMap = ["su", "m", "t", "w", "th", "f", "s"];
const shortDay = dayMap[searchDate.getDay()]; // Fetch short day from map
const query = {
headcode: sanitizedHeadcode,
scheduleStartDate: { $lte: searchDate },
scheduleEndDate: { $gte: searchDate },
daysRun: { $in: [shortDay] },
};
const pipeline = [
{
$match: query,
},
{
$project: {
operator: 1,
stops: {
$concatArrays: [
[{ $first: "$stops" }],
[{ $arrayElemAt: ["$stops", -1] }],
],
},
trainUid: 1,
stpIndicator: 1,
},
},
];
const queryData: Service[] = await db.queryAggregate("timetable", pipeline);
let filteredData = filterServices(queryData);
return await filteredData;
}
// Finds a train by its trainUid value
async function findByTrainUid(
uid: string,
date: Date | string = new Date()
): Promise<TrainServices | null> {
let queryDate;
if (date === "now") {
queryDate = new Date();
} else {
queryDate = date;
}
const query = {
trainUid: uid,
scheduleStartDate: { $lte: queryDate },
scheduleEndDate: { $gte: queryDate },
};
const queryData = await db.query("timetable", query);
if (queryData.length === 0) {
return queryData;
}
let services;
services = await filterServices(queryData);
console.log(services);
let publicStops;
if (pis.supported.includes(services[0]?.operator)) {
publicStops = await getPublicStops(services[0]?.stops);
if (publicStops.length) {
const pisCode = await pis.findByTiplocArray(publicStops);
services[0].pis = pisCode[0]?.code;
} else if (services[0]?.operator === "GW" && !publicStops.length) {
services[0].pis = "0015";
}
}
return services[0];
}
module.exports = {
findByHeadcodeToday,
findByHeadcode,
findByTrainUid,
};
/* Internal Functions, not to be exported */
/* Accepts the 'stops' array from a db query and produces an
array of only public stops as TIPLOCs. */
async function getPublicStops(data: Stop[]): Promise<string[]> {
let tiplocList = [];
for (const publicStop in data) {
if (data[publicStop]["isPublic"]) {
tiplocList.push(data[publicStop]["tiploc"]);
}
}
return tiplocList;
}
/* Takes a single days data from a headcode query and requeries
using the trainUid, required to ensure any cancellations are
accounted for */
async function parseTrains(data: TrainServices[]): Promise<TrainServices[]> {
let trainUids: string[] = [];
for (const i of data) {
const trainUid = i["trainUid"];
if (!trainUids.includes(trainUid)) {
trainUids.push(trainUid);
}
}
let parsedData: TrainServices[] = [];
for (const i in trainUids) {
const result: TrainServices | null = await findByTrainUid(trainUids[i]);
if (result) {
parsedData.push(result);
}
}
return parsedData;
}
// Filters services based on their STP Indicator
async function filterServices(data: Service[]): Promise<TrainServices[]> {
let stpIndicators = {},
filteredServices = [];
for (const serviceDetail of data) {
const trainUid = serviceDetail["trainUid"];
const stpIndicator = serviceDetail["stpIndicator"];
if (!stpIndicators[trainUid]) {
stpIndicators[trainUid] = {
hasC: false,
hasN: false,
hasO: false,
hasP: false,
};
}
if (stpIndicator === "C") {
stpIndicators[trainUid].hasC = true;
}
if (stpIndicator === "N") {
stpIndicators[trainUid].hasN = true;
}
if (stpIndicator === "O") {
stpIndicators[trainUid].hasO = true;
}
if (stpIndicator === "P") {
stpIndicators[trainUid].hasP = true;
}
}
for (const serviceDetail of data) {
const trainUid = serviceDetail["trainUid"];
const thisStpIndicators = stpIndicators[trainUid];
const stpIndicator = serviceDetail["stpIndicator"];
if (stpIndicator === "C") {
break;
}
if (stpIndicator === "N" && !thisStpIndicators.hasC) {
filteredServices.push(serviceDetail);
} else if (
stpIndicator === "O" &&
!thisStpIndicators.hasC &&
!thisStpIndicators.hasN
) {
filteredServices.push(serviceDetail);
} else if (
stpIndicator === "P" &&
!thisStpIndicators.hasC &&
!thisStpIndicators.hasN &&
!thisStpIndicators.hasO
) {
filteredServices.push(serviceDetail);
}
}
return filteredServices;
}

View File

@ -6,10 +6,14 @@ const dbName = process.env.OWL_DB_NAME || "owlboard";
const dbPort = process.env.OWL_DB_PORT || 27017; const dbPort = process.env.OWL_DB_PORT || 27017;
const dbHost = process.env.OWL_DB_HOST || "localhost"; const dbHost = process.env.OWL_DB_HOST || "localhost";
const uri = `mongodb://${dbUser}:${dbPass}@${dbHost}:${dbPort}`; const uri = `mongodb://${dbUser}:${dbPass}@${dbHost}:${dbPort}`;
const connOpts = {
useUnifiedTopology: true,
authSource: "owlboard",
}
const { MongoClient } = require("mongodb"); const { MongoClient } = require("mongodb");
const client = new MongoClient(uri); const client = new MongoClient(uri, connOpts);
const db = client.db(dbName); const db = client.db(dbName);
async function query(collection, query, returnId = false) { async function query(collection, query, returnId = false) {

View File

@ -5,10 +5,11 @@ const util = require("../utils/ldb.utils");
const san = require("../utils/sanitizer.utils"); const san = require("../utils/sanitizer.utils");
const db = require("../services/dbAccess.services"); const db = require("../services/dbAccess.services");
import { findStationsByDistancePipeline } from "../utils/ldbPipeline.utils";
import { logger } from "../utils/logger.utils"; import { logger } from "../utils/logger.utils";
import { transform as staffStationTransform } from "../utils/processors/ldb/staffStation"; import { transform as staffStationTransform } from "../utils/processors/ldb/staffStation";
import { msgCodes } from "../configs/errorCodes.configs";
const ldbKey = process.env.OWL_LDB_KEY; const ldbKey = process.env.OWL_LDB_KEY;
const ldbsvKey = process.env.OWL_LDB_SVKEY; const ldbsvKey = process.env.OWL_LDB_SVKEY;
@ -32,7 +33,7 @@ async function get(id, staff = false) {
logger.error(err, "ldbService.get: Error, Unable to find CRS"); logger.error(err, "ldbService.get: Error, Unable to find CRS");
return { return {
obStatus: "LOC_NOT_FOUND", obStatus: "LOC_NOT_FOUND",
obMsg: "UNABLE TO FIND MESSAGE", obMsg: "Location is not available",
}; };
} }
} }
@ -69,14 +70,18 @@ async function arrDepBoardStaff(CRS) {
}; };
const api = new ldb(ldbsvKey, true); const api = new ldb(ldbsvKey, true);
console.time(`Fetch Staff LDB for ${CRS.toUpperCase()}`); console.time(`Fetch Staff LDB for ${CRS.toUpperCase()}`);
const result = await api.call( let result
"GetArrivalDepartureBoardByCRS", try {
options, result = await staffApiCallRetry(
false, api,
false "GetArrivalDepartureBoardByCRS",
); options,
console.log("\n\n\nORIGINAL DATA"); 5,
console.log("\n" + JSON.stringify(result) + "\n\n\n"); );
} catch (err) {
logger.error(err, "Error fetching board data");
return {obStatus: "Error", obMsg: "Error fetching data from National Rail", data: null}
}
console.timeEnd(`Fetch Staff LDB for ${CRS.toUpperCase()}`); console.timeEnd(`Fetch Staff LDB for ${CRS.toUpperCase()}`);
try { try {
const _staffLdb = staffStationTransform(result); const _staffLdb = staffStationTransform(result);
@ -128,6 +133,29 @@ async function getServicesByOther(id) {
} }
} }
async function staffApiCallRetry(api, method, options, retries) {
for (let i=0; i < retries; i++) {
try {
return await api.call(method, options, false, false);
} catch (err) {
if (err.code === 'ENOTFOUND') {
logger.warn(err, "DNS ERR")
if (i < retries - 1) {
logger.debug('Retrying API Call')
await delay(500)
continue;
}
}
throw err;
}
}
throw new Error("Max retries exceeded");
}
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function getReasonCodeList() { async function getReasonCodeList() {
logger.debug("ldbService.getReasonCodeList: Fetching reason code list"); logger.debug("ldbService.getReasonCodeList: Fetching reason code list");
try { try {
@ -150,6 +178,17 @@ async function getReasonCode(code) {
} }
} }
async function getNearestStations(lat, long) {
logger.debug(`ldbService.getNearestStations: Fetching nearest stations`)
let pipeline = findStationsByDistancePipeline(4, lat, long)
try {
return await db.queryAggregate("stations", pipeline)
} catch (err) {
logger.error(err, `ldbService.getNearestStations`)
}
}
async function getDateTimeString(date) { async function getDateTimeString(date) {
const year = date.getFullYear(), const year = date.getFullYear(),
month = String(date.getMonth() + 1).padStart(2, "0"), month = String(date.getMonth() + 1).padStart(2, "0"),
@ -175,4 +214,5 @@ module.exports = {
getServicesByOther, getServicesByOther,
getReasonCodeList, getReasonCodeList,
getReasonCode, getReasonCode,
getNearestStations,
}; };

View File

@ -10,12 +10,13 @@ import { queryAggregate } from "./dbAccess.services";
import { import {
getPartialEndTiplocMatchPipeline, getPartialEndTiplocMatchPipeline,
getFullTiplocMatchPipeline, getFullTiplocMatchPipeline,
getPartialStartTiplocMatchPipeline,
} from "../utils/pis.utils"; } from "../utils/pis.utils";
import { Document } from "mongodb"; import { Document } from "mongodb";
export const supported = ["GW", "UK"]; export const supported = ["GW", "UK", "HX"];
async function findPisByOrigDest(start: string, end: string) { export async function findPisByOrigDest(start: string, end: string) {
logger.debug( logger.debug(
`pisServices.findPisByOrigDest: Searching for Orig: ${start}, Dest: ${end}` `pisServices.findPisByOrigDest: Searching for Orig: ${start}, Dest: ${end}`
); );
@ -40,13 +41,13 @@ async function findPisByOrigDest(start: string, end: string) {
return search; return search;
} }
async function findPisByCode( export async function findPisByCode(
code: string code: string
): Promise<OB_Pis_SimpleObject | null> { ): Promise<OB_Pis_SimpleObject | null> {
logger.debug(`pisServices.findPisByCode: Searching for PIS code: ${code}`); logger.debug(`pisServices.findPisByCode: Searching for PIS code: ${code}`);
const cleanCode = clean.removeNonNumeric(code); const cleanCode = clean.removeNonNumeric(code);
const query = { const query = {
code: parseInt(cleanCode), code: cleanCode,
}; };
const search = db.query("pis", query); const search = db.query("pis", query);
return await search; return await search;
@ -78,8 +79,12 @@ export async function findByTiplocArray(
if (partialEnd) { if (partialEnd) {
return convertDocument(partialEnd, "first"); return convertDocument(partialEnd, "first");
} else { } else {
// Here, I should search for a partialStart match. For now return null. const partialStart = await findPartialStartMatchByTiploc(tiplocArray);
return null; if (partialStart) {
return convertDocument(partialStart, "last");
} else {
return null;
}
} }
} }
} catch (err) { } catch (err) {
@ -103,6 +108,13 @@ async function findPartialEndMatchByTiploc(array: string[]): Promise<Document> {
return res[0]; return res[0];
} }
// Uses a pipeline to find a partial match - supporting codes starting with the correct stops.
async function findPartialStartMatchByTiploc(array: string[]): Promise<Document> {
const pipeline = getPartialStartTiplocMatchPipeline(array);
const res = await queryAggregate("pis", pipeline);
return res[0];
}
function convertDocument(doc: Document, skipType: string): OB_Pis_SimpleObject { function convertDocument(doc: Document, skipType: string): OB_Pis_SimpleObject {
return { return {
code: doc.code.toString(), code: doc.code.toString(),

View File

@ -1,34 +1,32 @@
const auth = require("../utils/auth.utils"); const auth = require("../utils/auth.utils");
const db = require("./dbAccess.services"); const db = require("./dbAccess.services");
const mail = require("./mail.services"); const mail = require("./mail.services");
const clean = require("../utils/sanitizer.utils");
const domains = require("../configs/domains.configs");
const errors = require("../configs/errorCodes.configs"); const errors = require("../configs/errorCodes.configs");
import { logger } from "../utils/logger.utils"; import { logger } from "../utils/logger.utils";
import { getDomainFromEmail } from "../utils/sanitizer.utils";
import { valid as validDomains } from "../configs/domains.configs";
import { generateCode } from "../utils/auth.utils";
async function createRegKey(body) { async function createRegKey(body) {
logger.debug("registerServices.createRegKey: Incoming request"); logger.debug("registerServices.createRegKey: Incoming request");
if (body.email) { if (body.email) {
const domain = await clean.getDomainFromEmail(body.email); const domain = getDomainFromEmail(body.email);
logger.info(`registerServices: Registration request from: ${domain}`); logger.info(`registerServices: Registration request from: ${domain}`);
if (domains.includes(domain)) { if (validDomains.includes(domain)) {
logger.debug(`registerServices.createRegKey: Key from valid: ${domain}`); logger.debug(`registerServices.createRegKey: Key from valid: ${domain}`);
const uuid = await auth.generateKey(); const key = generateCode()
db.addRegReq(uuid, domain); db.addRegReq(key, domain)
const message = await auth.generateConfirmationEmail(body.email, uuid); const message = await auth.generateConfirmationEmail(body.email, key);
if (!message) { if (!message) {
const err = new Error("Message generation error"); const err = new Error("Message Generation Error");
logger.error( logger.error(err, "registerServices.createRegKey: Error generating email");
err,
"registerServices.createRegKey: Error generating email"
);
return 500; return 500;
} }
if ((await mail.send(message)) == true) { if ((await mail.send(message)) == true) {
return { status: 201, message: "email sent" }; return {status: 201, message: "email sent"};
} }
return { status: 500, errorCode: 950, errorMsg: errors[950] }; return {status:500, errorCode:950, errorMsg: errors[950]}
} }
return { status: 403, errorCode: 702, errorMsg: errors[702] }; return { status: 403, errorCode: 702, errorMsg: errors[702] };
} else { } else {
@ -38,8 +36,9 @@ async function createRegKey(body) {
async function regUser(req) { async function regUser(req) {
// Add input validation // Add input validation
logger.trace(`Read UUID: ${req.uuid}`); const regCode = req.uuid.toLocaleUpperCase();
const res = await auth.checkRequest(req.uuid); logger.trace(`Read UUID: ${regCode}`);
const res = await auth.checkRequest(regCode);
logger.debug(res, "registrationServices"); logger.debug(res, "registrationServices");
if (res.result) { if (res.result) {
const uuid = await auth.generateKey(); const uuid = await auth.generateKey();
@ -52,6 +51,8 @@ async function regUser(req) {
return { status: 401, errorCode: 703, errorMsg: errors[703] }; return { status: 401, errorCode: 703, errorMsg: errors[703] };
} }
// Currently errors on a correct code as it cannot be found... Ensure uuid is ALL CAPS
async function getUser(uuid) { async function getUser(uuid) {
try { try {
const filter = { const filter = {

View File

@ -8,8 +8,6 @@ async function buildJson() {
let json = {}; let json = {};
json.count = {}; json.count = {};
// Async call all db queries // Async call all db queries
const counters = db.query("meta", { target: "counters" });
const versions = db.query("meta", { target: "versions" });
const userCount = db.colCount("users"); const userCount = db.colCount("users");
const regCount = db.colCount("registrations"); const regCount = db.colCount("registrations");
const pisCount = db.colCount("pis"); const pisCount = db.colCount("pis");
@ -19,12 +17,8 @@ async function buildJson() {
// Insert data // Insert data
json.mode = process.env.NODE_ENV; json.mode = process.env.NODE_ENV;
json.verBkend = vers.app;
json.verApi = vers.api;
json.host = os.hostname(); json.host = os.hostname();
// Await and insert async calls // Await and insert async calls
json.dat = await counters;
json.ver = await versions;
json.count.users = await userCount; json.count.users = await userCount;
json.count.reg = await regCount; json.count.reg = await regCount;
json.count.pis = await pisCount; json.count.pis = await pisCount;
@ -42,11 +36,9 @@ async function hits() {
async function getVersions() { async function getVersions() {
logger.debug("statsServices.getVersions: Fetching versions"); logger.debug("statsServices.getVersions: Fetching versions");
const dbMan = await db.query("versions", { target: "dbmanager" }); const mqClt = await db.query("versions", { target: "timetable-mgr" });
const mqClt = await db.query("versions", { target: "mq-client" });
const data = { const data = {
backend: vers.app, backend: vers.app,
"db-manager": dbMan[0]?.["version"] || "",
"mq-client": mqClt[0]?.["version"] || "", "mq-client": mqClt[0]?.["version"] || "",
}; };
return data; return data;
@ -55,11 +47,10 @@ async function getVersions() {
async function statistics() { async function statistics() {
logger.debug("statsServices.statistics: Fetching statistics"); logger.debug("statsServices.statistics: Fetching statistics");
const countersPromise = db.query("meta", { target: "counters" }); const timetablePromise = db.query("meta", { type: "CifMetadata" });
const timetablePromise = db.query("meta", { target: "timetable" }); const pisPromise = db.query("meta", { type: "PisMetadata" });
const pisPromise = db.query("meta", { target: "pis" });
const corpusPromise = db.query("meta", { target: "corpus" }); const corpusPromise = db.query("meta", { target: "corpus" });
const reasonCodesPromise = db.query("meta", { target: "reasonCodes" }); const stationsPromise = db.query("meta", {type: "StationsMetadata"});
const lengthUsersPromise = db.colCount("users"); const lengthUsersPromise = db.colCount("users");
const lengthRegistrationsPromise = db.colCount("registrations"); const lengthRegistrationsPromise = db.colCount("registrations");
@ -70,51 +61,37 @@ async function statistics() {
const lengthReasonCodesPromise = db.colCount("reasonCodes"); const lengthReasonCodesPromise = db.colCount("reasonCodes");
const [ const [
counters,
timetable, timetable,
pis, pis,
corpus, corpus,
reasonCodes,
lengthUsers, lengthUsers,
lengthRegistrations, lengthRegistrations,
lengthCorpus, lengthCorpus,
lengthStations, lengthStations,
lengthPis, lengthPis,
lengthTimetable, lengthTimetable,
lengthReasonCodes, stations,
] = await Promise.all([ ] = await Promise.all([
countersPromise,
timetablePromise, timetablePromise,
pisPromise, pisPromise,
corpusPromise, corpusPromise,
reasonCodesPromise,
lengthUsersPromise, lengthUsersPromise,
lengthRegistrationsPromise, lengthRegistrationsPromise,
lengthCorpusPromise, lengthCorpusPromise,
lengthStationsPromise, lengthStationsPromise,
lengthPisPromise, lengthPisPromise,
lengthTimetablePromise, lengthTimetablePromise,
lengthReasonCodesPromise, stationsPromise,
]); ]);
return { return {
hostname: os.hostname() || "Unknown", hostname: os.hostname() || "Unknown",
runtimeMode: process.env.NODE_ENV || "Unknown", runtimeMode: process.env.NODE_ENV || "Unknown",
reset: counters[0]["since"],
updateTimes: { updateTimes: {
timetable: timetable[0]["updated"], timetable: (timetable[0]["lastUpdate"]),
pis: pis[0]["updated"], pis: pis[0]["lastUpdate"],
corpus: corpus[0]["updated"], corpus: corpus[0]["updated_time"],
reasonCodes: reasonCodes[0]["updated"], stations: stations[0]["lastUpdate"],
},
requestCounts: {
ldbws_api: counters[0]["ldbws"] || 0,
lsbsvws_api: counters[0]["ldbsvws"] || 0,
corpus_api: counters[0]["corpus_api"] || 0,
timetable_db: counters[0]["timetable"] || 0,
pis_db: counters[0]["pis"] || 0,
corpus_db: counters[0]["corpus"] || 0,
stations_db: counters[0]["stations"] || 0,
}, },
dbLengths: { dbLengths: {
users: lengthUsers, users: lengthUsers,
@ -123,7 +100,6 @@ async function statistics() {
stations: lengthStations, stations: lengthStations,
pis: lengthPis, pis: lengthPis,
timetable: lengthTimetable, timetable: lengthTimetable,
reasonCodes: lengthReasonCodes,
}, },
}; };
} }

View File

@ -34,9 +34,9 @@ export async function findByHeadcode(
const query = { const query = {
headcode: sanitizedHeadcode.toUpperCase(), headcode: sanitizedHeadcode.toUpperCase(),
daysRun: { $in: [shortDay] },
scheduleStartDate: { $lte: searchDate }, scheduleStartDate: { $lte: searchDate },
scheduleEndDate: { $gte: searchDate }, scheduleEndDate: { $gte: searchDate },
daysRun: { $in: [shortDay] },
}; };
const pipeline = getFindByHeadcodePipeline(query); const pipeline = getFindByHeadcodePipeline(query);
@ -56,18 +56,20 @@ export async function findByTrainUid(
) { ) {
// Set the correct date - whether a date or "now" was passed to function // Set the correct date - whether a date or "now" was passed to function
let queryDate: Date; let queryDate: Date;
if (date instanceof Date) { if (date === 'now') {
queryDate = new Date();
} else if (date instanceof Date) {
queryDate = date; queryDate = date;
} else { } else {
queryDate = new Date(); queryDate = new Date(date);
} }
// Build query // Build query
const query = { const query = {
trainUid: uid.toUpperCase(), trainUid: uid.toUpperCase(),
daysRun: { $in: [getShortDay(queryDate)] },
scheduleStartDate: { $lte: queryDate }, scheduleStartDate: { $lte: queryDate },
scheduleEndDate: { $gte: queryDate }, scheduleEndDate: { $gte: queryDate },
daysRun: { $in: [getShortDay(queryDate)] },
}; };
const pipeline = getFindByTrainUidPipeline(query); const pipeline = getFindByTrainUidPipeline(query);
@ -82,7 +84,6 @@ export async function findByTrainUid(
} else { } else {
pis = null; pis = null;
} }
// TODO: Format and return data, the function called is not yet complete
return formatTimetableDetail(services[0], pis); return formatTimetableDetail(services[0], pis);
} }

View File

@ -8,5 +8,10 @@ declare global {
export interface Request { export interface Request {
isAuthed: boolean; isAuthed: boolean;
} }
export interface Response {
cacheType: string;
cacheSecs: number;
}
} }
} }

View File

@ -20,7 +20,6 @@ async function isAuthed(uuid: string): Promise<boolean> {
// Checks whether a registration request key is valid // Checks whether a registration request key is valid
async function checkRequest(key: string) { async function checkRequest(key: string) {
// For some reason db.query seems to return correctly, but the second logs.out statement prints []??? so registration fails!!
const collection = "registrations"; const collection = "registrations";
const query = { uuid: key }; const query = { uuid: key };
const res = await db.query(collection, query); const res = await db.query(collection, query);
@ -34,20 +33,33 @@ async function checkRequest(key: string) {
// Creates an API key for a user // Creates an API key for a user
async function generateKey() { async function generateKey() {
// Needs testing & moving to 'register.utils' ??? Why does it need moving?
return crypt.randomUUID(); return crypt.randomUUID();
} }
export function generateCode(): string {
const characters = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
const codeLength = 6;
let code = '';
const bytes = crypt.randomBytes(codeLength); // Generate random bytes
for (let i = 0; i < codeLength; i++) {
const randomIndex = bytes[i] % characters.length; // Map bytes to characters
code += characters.charAt(randomIndex);
}
return code;
}
async function generateConfirmationEmail(eml: string, uuid: string) { async function generateConfirmationEmail(eml: string, uuid: string) {
try { try {
const htmlTpl = await fs.readFile("mail-templates/register.html", "utf-8"); const htmlTpl = await fs.readFile("mail-templates/register.html", "utf-8");
const htmlStr = htmlTpl.replace(/>>ACCESSCODE<</g, uuid); const htmlStr = htmlTpl.replace(/987654/g, uuid);
const htmlMin = await minifyMail(htmlStr); const htmlMin = await minifyMail(htmlStr);
const txtTpl = fs.readFile("mail-templates/register.txt", "utf-8"); const txtTpl = fs.readFile("mail-templates/register.txt", "utf-8");
return { return {
to: eml, to: eml,
subject: "OwlBoard Registration", subject: "OwlBoard Registration",
text: (await txtTpl).replace(/>>ACCESSCODE<</g, uuid), text: (await txtTpl).replace(/987654/g, uuid),
html: htmlMin, html: htmlMin,
}; };
} catch (err) { } catch (err) {
@ -64,6 +76,7 @@ module.exports = {
generateKey, generateKey,
generateConfirmationEmail, generateConfirmationEmail,
checkRequest, checkRequest,
generateCode
}; };
export { isAuthed, generateKey, generateConfirmationEmail, checkRequest }; export { isAuthed, generateKey, generateConfirmationEmail, checkRequest };

View File

@ -0,0 +1,9 @@
import type { Response } from "express"
export function setCache(res: Response, type="private", time=120): void {
if (type === "no-store") {
res.setHeader('Cache-Control', 'no-store')
return
}
res.setHeader('Cache-Control', `${type}, max-age=${time}`)
}

View File

@ -0,0 +1,47 @@
export function findStationsByDistancePipeline(count: number, latitude: string, longitude: string) {
const numericLatitude = parseFloat(latitude)
const numericLongitude = parseFloat(longitude)
const pipeline = [
{
'$geoNear': {
'near': {
'type': 'Point',
'coordinates': [
numericLongitude, numericLatitude
]
},
'distanceField': 'distance'
}
}, {
'$limit': count
}, {
'$addFields': {
'miles': {
'$divide': [
{
'$round': {
'$multiply': [
{
'$divide': [
'$distance', 1609.34
]
}, 4
]
}
}, 4
]
}
}
}, {
'$project': {
'_id': 0,
'3ALPHA': 1,
'NLCDESC': 1,
'miles': 1
}
}
]
//console.log(JSON.stringify(pipeline))
return pipeline
}

View File

@ -75,6 +75,61 @@ export function getPartialEndTiplocMatchPipeline(query: string[]) {
]; ];
} }
export function getPartialStartTiplocMatchPipeline(query: string[]) {
return [
{
'$match': {
'tiplocs': {
'$all': query
}
}
}, {
'$addFields': {
'query': query
}
}, {
'$match': {
'$expr': {
'$eq': [
{
'$slice': [
'$tiplocs', {
'$size': '$query'
}
]
}, '$query'
]
}
}
}, {
'$addFields': {
'skipStops': {
'$subtract': [
{
'$size': '$tiplocs'
}, {
'$size': '$query'
}
]
}
}
}, {
'$sort': {
'skipStops': 1
}
}, {
'$limit': 1
}, {
'$project': {
'code': 1,
'skipStops': 1,
'toc': 1,
'_id': 0
}
}
]
}
export function getFullTiplocMatchPipeline(query: string[]) { export function getFullTiplocMatchPipeline(query: string[]) {
return [ return [
{ {

View File

@ -5,7 +5,6 @@ import type {
OB_TrainTT_stopDetail, OB_TrainTT_stopDetail,
Stop, Stop,
} from "@owlboard/ts-types"; } from "@owlboard/ts-types";
import { logger } from "../../logger.utils";
export function formatTimetableDetail( export function formatTimetableDetail(
service: Service, service: Service,
@ -22,7 +21,7 @@ export function formatTimetableDetail(
scheduleEnd: service.scheduleEndDate, scheduleEnd: service.scheduleEndDate,
daysRun: service.daysRun, daysRun: service.daysRun,
stops: formatStops(service.stops), stops: formatStops(service.stops),
vstp: service.vstp, serviceDetail: service.serviceDetail,
}; };
if (pis) { if (pis) {
@ -33,6 +32,14 @@ export function formatTimetableDetail(
} }
function formatStops(stops: Stop[]): OB_TrainTT_stopDetail[] { function formatStops(stops: Stop[]): OB_TrainTT_stopDetail[] {
if (!stops) {
return []
}
if (!stops.length) {
return []
}
// Cleanly coerce Stop[] to OB_TrainTT_stopDetail[] // Cleanly coerce Stop[] to OB_TrainTT_stopDetail[]
const formattedStops: OB_TrainTT_stopDetail[] = []; const formattedStops: OB_TrainTT_stopDetail[] = [];
@ -63,6 +70,22 @@ function formatStopTimes(stop: Stop): OB_TrainTT_stopDetail {
if (stop.wttDeparture) { if (stop.wttDeparture) {
formattedStop.wttDeparture = stop.wttDeparture; formattedStop.wttDeparture = stop.wttDeparture;
} }
if (stop.platform) {
formattedStop.platform = stop.platform;
}
if (stop.pass) {
formattedStop.pass = stop.pass;
}
if (stop.arrLine) {
formattedStop.arrLine = stop.arrLine;
}
if (stop.depLine) {
formattedStop.depLine = stop.depLine;
}
return formattedStop; return formattedStop;
} }

View File

@ -1,5 +1,3 @@
//const log = require('../utils/log.utils');
import { logger } from "./logger.utils"; import { logger } from "./logger.utils";
function removeNonAlphanumeric(inputString: string) { function removeNonAlphanumeric(inputString: string) {

View File

@ -0,0 +1,3 @@
// Do I need to setup the database?
// Possibly not, becuase every write will create the document if it doesn't exist