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)
// Please see the included LICENSE file
const mode = process.env.NODE_ENV || "development";
@ -13,7 +12,6 @@ const express = require("express");
const app = express();
// Middleware
const compression = require("compression");
const rateLimit = require("express-rate-limit");
const cors = require("cors");
const authenticate = require("./src/middlewares/auth.middlewares");
@ -52,15 +50,14 @@ app.use((err, req, res, next) => {
return;
});
// Global Middleware:
// Pre Middleware:
app.use(
cors({
origin: "*", //[/\.owlboard\.info$/, 'localhost:5173', 'localhost:4173']
})
);
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);
// 2023 Rationalisation Routes (/api/v2, /misc)
@ -84,6 +81,9 @@ mode === "development"
? app.get("/api/v1/ip", (req, res) => res.send(req.ip))
: null;
// Disable etags
app.set('etag', false)
// Start Express
app.listen(srvPort, srvListen, (error) => {
if (!error) {

View File

@ -6,6 +6,10 @@
html {
text-align: center;
width: 100%;
margin: 0;
padding: 0;
background-color: #404c55;
background-image: radial-gradient(#2b343c, #404c55);
}
body {
margin: 0;
@ -25,6 +29,10 @@
background-color: #404c55;
background-image: radial-gradient(#2b343c, #404c55);
}
p {
margin-left: 40px;
margin-right: 40px;
}
#title {
height: 100px;
padding-top: 0px;
@ -43,16 +51,24 @@
text-decoration: none;
border-radius: 14px;
}
.digits {
color: azure;
font-size: xx-large;
font-weight: bolder;
letter-spacing: 0.75ch;
margin-left: 0.75ch;
}
</style>
</head>
<body>
<br /><br />
<br><br>
<table>
<tr>
<td>
<img
src="https://owlboard.info/images/logo/wide_logo.svg"
id="title"
alt="OwlBoard Logo"
/>
</td>
</tr>
@ -61,27 +77,16 @@
<h1>Register for OwlBoard</h1>
<br />
<p>
Tap the button to register this device, or scan the barcode with
another device.
You'll need to type your registration code in to the OwlBoard app
</p>
<br />
<a
href="https://owlboard.info/more/reg/submit?key=>>ACCESSCODE<<"
id="button"
>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 />
<h2>Your Code:</h2>
<span class="digits">987654</span>
<br><br>
<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>
<br /><br /><br />
<p>
This registration is for one device only, you can register again
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
by us.
</p>
<p>The registration link will expire after 30 minutes.</p>
<p>The registration link will expire after 1 hour.</p>
</td>
</tr>
</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.
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<<
Go back to the OwlBoard app, goto "Menu > Registration" and click on the link at the top to enter your code.
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.
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",
"version": "2024.2.1",
"description": "OwlBoard is an API and PWA for live rail departure board in the UK.",
"name": "owlboard-backend",
"version": "2024.2.2",
"description": "Provides LDB, PIS and live train details for the OwlBoard web client",
"repository": {
"type": "git",
"url": "https://git.fjla.uk/owlboard/backend.git"
@ -27,13 +27,13 @@
"ldbs-json": "^1.2.1",
"moment-timezone": "^0.5.43",
"mongodb": "^4.13.0",
"nodemailer": "^6.9.1",
"nodemailer": "^6.9.9",
"pino": "^8.15.1",
"redis": "^4.6.7",
"zlib": "^1.0.5"
},
"devDependencies": {
"@owlboard/ts-types": "^0.1.8",
"@owlboard/ts-types": "^1.1.0",
"@types/express": "^4.17.21",
"@types/jest": "^29.5.3",
"eslint": "^8.39.0",

View File

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

View File

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

View File

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

View File

@ -1,5 +1,8 @@
const ldb = require("../services/ldb.services");
import { setCache } from "../utils/cacheHeader.utils";
import { logger } from "../utils/logger.utils";
async function getTrain(req, res, next) {
// API v2 Only
if (!req.isAuthed) {
@ -7,6 +10,7 @@ async function getTrain(req, res, next) {
err.status = 401;
throw err;
}
setCache(res, "private", 240)
let type = req.params.searchType;
let id = req.params.id;
try {
@ -42,18 +46,53 @@ async function getStation(req, res, next) {
err.status = 401;
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 {
setCache(res, "public", 240)
res.json(await ldb.get(id, false));
}
} catch (err) {
setCache(res, "no-store")
console.error("Unknown Error", err.message);
err.status = 500;
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 = {
getTrain,
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 */
import { setCache } from "../utils/cacheHeader.utils";
const ldb = require("../services/ldb.services");
const find = require("../services/find.services");
@ -7,6 +9,7 @@ async function getReasonCode(req, res, next) {
try {
const code = req.params.code;
if (code === "all") {
setCache(res, "public", 604800)
res.json(await ldb.getReasonCodeList());
next;
}
@ -15,6 +18,7 @@ async function getReasonCode(req, res, next) {
} catch (err) {
console.error("ERROR", err.message);
err.status = 500;
setCache(res, "no-store", 5)
next(err);
}
}
@ -23,6 +27,7 @@ async function getLocationReference(req, res, next) {
try {
const searchType = req.params.searchType;
const id = req.params.id;
setCache(res, "public", 604800)
switch (searchType) {
case "name":
res.json(await find.name(id));

View File

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

View File

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

View File

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

View File

@ -4,6 +4,7 @@ const ldbCtr = require("../controllers/ldb.controllers");
// PIS
router.get("/station/:id/:type", ldbCtr.getStation);
router.get("/station/nearest/:latitude/:longitude", ldbCtr.getNearest);
router.get("/train/:searchType/:id", ldbCtr.getTrain);
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 dbHost = process.env.OWL_DB_HOST || "localhost";
const uri = `mongodb://${dbUser}:${dbPass}@${dbHost}:${dbPort}`;
const connOpts = {
useUnifiedTopology: true,
authSource: "owlboard",
}
const { MongoClient } = require("mongodb");
const client = new MongoClient(uri);
const client = new MongoClient(uri, connOpts);
const db = client.db(dbName);
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 db = require("../services/dbAccess.services");
import { findStationsByDistancePipeline } from "../utils/ldbPipeline.utils";
import { logger } from "../utils/logger.utils";
import { transform as staffStationTransform } from "../utils/processors/ldb/staffStation";
import { msgCodes } from "../configs/errorCodes.configs";
const ldbKey = process.env.OWL_LDB_KEY;
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");
return {
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);
console.time(`Fetch Staff LDB for ${CRS.toUpperCase()}`);
const result = await api.call(
"GetArrivalDepartureBoardByCRS",
options,
false,
false
);
console.log("\n\n\nORIGINAL DATA");
console.log("\n" + JSON.stringify(result) + "\n\n\n");
let result
try {
result = await staffApiCallRetry(
api,
"GetArrivalDepartureBoardByCRS",
options,
5,
);
} 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()}`);
try {
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() {
logger.debug("ldbService.getReasonCodeList: Fetching reason code list");
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) {
const year = date.getFullYear(),
month = String(date.getMonth() + 1).padStart(2, "0"),
@ -175,4 +214,5 @@ module.exports = {
getServicesByOther,
getReasonCodeList,
getReasonCode,
getNearestStations,
};

View File

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

View File

@ -1,34 +1,32 @@
const auth = require("../utils/auth.utils");
const db = require("./dbAccess.services");
const mail = require("./mail.services");
const clean = require("../utils/sanitizer.utils");
const domains = require("../configs/domains.configs");
const errors = require("../configs/errorCodes.configs");
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) {
logger.debug("registerServices.createRegKey: Incoming request");
if (body.email) {
const domain = await clean.getDomainFromEmail(body.email);
const domain = getDomainFromEmail(body.email);
logger.info(`registerServices: Registration request from: ${domain}`);
if (domains.includes(domain)) {
if (validDomains.includes(domain)) {
logger.debug(`registerServices.createRegKey: Key from valid: ${domain}`);
const uuid = await auth.generateKey();
db.addRegReq(uuid, domain);
const message = await auth.generateConfirmationEmail(body.email, uuid);
const key = generateCode()
db.addRegReq(key, domain)
const message = await auth.generateConfirmationEmail(body.email, key);
if (!message) {
const err = new Error("Message generation error");
logger.error(
err,
"registerServices.createRegKey: Error generating email"
);
const err = new Error("Message Generation Error");
logger.error(err, "registerServices.createRegKey: Error generating email");
return 500;
}
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] };
} else {
@ -38,8 +36,9 @@ async function createRegKey(body) {
async function regUser(req) {
// Add input validation
logger.trace(`Read UUID: ${req.uuid}`);
const res = await auth.checkRequest(req.uuid);
const regCode = req.uuid.toLocaleUpperCase();
logger.trace(`Read UUID: ${regCode}`);
const res = await auth.checkRequest(regCode);
logger.debug(res, "registrationServices");
if (res.result) {
const uuid = await auth.generateKey();
@ -52,6 +51,8 @@ async function regUser(req) {
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) {
try {
const filter = {

View File

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

View File

@ -34,9 +34,9 @@ export async function findByHeadcode(
const query = {
headcode: sanitizedHeadcode.toUpperCase(),
daysRun: { $in: [shortDay] },
scheduleStartDate: { $lte: searchDate },
scheduleEndDate: { $gte: searchDate },
daysRun: { $in: [shortDay] },
};
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
let queryDate: Date;
if (date instanceof Date) {
if (date === 'now') {
queryDate = new Date();
} else if (date instanceof Date) {
queryDate = date;
} else {
queryDate = new Date();
queryDate = new Date(date);
}
// Build query
const query = {
trainUid: uid.toUpperCase(),
daysRun: { $in: [getShortDay(queryDate)] },
scheduleStartDate: { $lte: queryDate },
scheduleEndDate: { $gte: queryDate },
daysRun: { $in: [getShortDay(queryDate)] },
};
const pipeline = getFindByTrainUidPipeline(query);
@ -82,7 +84,6 @@ export async function findByTrainUid(
} else {
pis = null;
}
// TODO: Format and return data, the function called is not yet complete
return formatTimetableDetail(services[0], pis);
}

View File

@ -8,5 +8,10 @@ declare global {
export interface Request {
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
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 query = { uuid: key };
const res = await db.query(collection, query);
@ -34,20 +33,33 @@ async function checkRequest(key: string) {
// Creates an API key for a user
async function generateKey() {
// Needs testing & moving to 'register.utils' ??? Why does it need moving?
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) {
try {
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 txtTpl = fs.readFile("mail-templates/register.txt", "utf-8");
return {
to: eml,
subject: "OwlBoard Registration",
text: (await txtTpl).replace(/>>ACCESSCODE<</g, uuid),
text: (await txtTpl).replace(/987654/g, uuid),
html: htmlMin,
};
} catch (err) {
@ -64,6 +76,7 @@ module.exports = {
generateKey,
generateConfirmationEmail,
checkRequest,
generateCode
};
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[]) {
return [
{

View File

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

View File

@ -1,5 +1,3 @@
//const log = require('../utils/log.utils');
import { logger } from "./logger.utils";
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