Compare commits

..

No commits in common. "main" and "2024.2.1" have entirely different histories.

31 changed files with 1530 additions and 1630 deletions

10
app.js
View File

@ -1,4 +1,5 @@
// OwlBoard - © Fred Boniface 2022-2023 - Licensed under GPLv3 (or later)
// Please see the included LICENSE file
const mode = process.env.NODE_ENV || "development";
@ -12,6 +13,7 @@ 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");
@ -50,14 +52,15 @@ app.use((err, req, res, next) => {
return;
});
// Pre Middleware:
// Global Middleware:
app.use(
cors({
origin: "*", //[/\.owlboard\.info$/, 'localhost:5173', 'localhost:4173']
})
);
app.use(express.json()); //JSON Parsing for POST Requests
//app.use(limiter);
app.use(compression()); // Compress API Data if supported by client
app.use(limiter);
app.use(authenticate);
// 2023 Rationalisation Routes (/api/v2, /misc)
@ -81,9 +84,6 @@ 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,10 +6,6 @@
html {
text-align: center;
width: 100%;
margin: 0;
padding: 0;
background-color: #404c55;
background-image: radial-gradient(#2b343c, #404c55);
}
body {
margin: 0;
@ -29,10 +25,6 @@
background-color: #404c55;
background-image: radial-gradient(#2b343c, #404c55);
}
p {
margin-left: 40px;
margin-right: 40px;
}
#title {
height: 100px;
padding-top: 0px;
@ -51,24 +43,16 @@
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>
@ -77,16 +61,27 @@
<h1>Register for OwlBoard</h1>
<br />
<p>
You'll need to type your registration code in to the OwlBoard app
Tap the button to register this device, or scan the barcode with
another device.
</p>
<br />
<h2>Your Code:</h2>
<span class="digits">987654</span>
<br><br>
<p>
Go back to OwlBoard and enter your code. Go to the registration page and click the link at the top.
</p>
<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 />
<p>
Alternatively copy and paste the link:<br />https://owlboard.info/more/reg/submit?key=>>ACCESSCODE<<
</p>
<p>
This registration is for one device only, you can register again
using the same email address for other devices and access OwlBoard
@ -97,7 +92,7 @@
can safely ignore this email. Your email address has not been stored
by us.
</p>
<p>The registration link will expire after 1 hour.</p>
<p>The registration link will expire after 30 minutes.</p>
</td>
</tr>
</table>

View File

@ -1,10 +1,12 @@
Complete your OwlBoard (Staff) Registration by entering your six digit code.
Complete your OwlBoard (Staff) Registration using the link below.
987654
https://owlboard.info/more/reg/submit?key=>>ACCESSCODE<<
Go back to the OwlBoard app, goto "Menu > Registration" and click on the link at the top to enter your code.
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<<
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 1 hour.
The link will expire after 30 minutes.

2310
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
{
"name": "owlboard-backend",
"version": "2024.2.2",
"description": "Provides LDB, PIS and live train details for the OwlBoard web client",
"name": "owlboard",
"version": "2024.2.1",
"description": "OwlBoard is an API and PWA for live rail departure board in the UK.",
"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.9",
"nodemailer": "^6.9.1",
"pino": "^8.15.1",
"redis": "^4.6.7",
"zlib": "^1.0.5"
},
"devDependencies": {
"@owlboard/ts-types": "^1.1.0",
"@owlboard/ts-types": "^0.1.8",
"@types/express": "^4.17.21",
"@types/jest": "^29.5.3",
"eslint": "^8.39.0",

View File

@ -1,4 +1,4 @@
export const valid: string[] = [
const valid: string[] = [
"owlboard.info",
"avantiwestcoast.co.uk",
"btp.police.uk",
@ -32,3 +32,6 @@ export 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: "2025.03.2",
app: "2024.2.1",
};
module.exports = version;

View File

@ -4,7 +4,6 @@ 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,8 +1,5 @@
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) {
@ -10,7 +7,6 @@ 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 {
@ -46,53 +42,18 @@ async function getStation(req, res, next) {
err.status = 401;
return next(err);
}
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);
res.json(await ldb.get(id, true));
} 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

@ -0,0 +1,54 @@
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

@ -1,40 +0,0 @@
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,7 +1,5 @@
/* API V2 Exclusive Controller */
import { setCache } from "../utils/cacheHeader.utils";
const ldb = require("../services/ldb.services");
const find = require("../services/find.services");
@ -9,7 +7,6 @@ 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;
}
@ -18,7 +15,6 @@ async function getReasonCode(req, res, next) {
} catch (err) {
console.error("ERROR", err.message);
err.status = 500;
setCache(res, "no-store", 5)
next(err);
}
}
@ -27,7 +23,6 @@ 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,11 +1,8 @@
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);
@ -17,7 +14,6 @@ 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,4 +1,3 @@
import { setCache } from "../utils/cacheHeader.utils";
import { logger } from "../utils/logger.utils";
const train = require("../services/trainService.services");
@ -6,11 +5,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));
@ -22,22 +21,20 @@ 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,43 +1,46 @@
import type { NextFunction, Request, Response } from "express";
import { logger } from "../utils/logger.utils";
import { isAuthed } from "../utils/auth.utils";
import { NextFunction, Request, Response } from "express";
const utils = require("../utils/auth.utils");
const logger = require("../utils/logger.utils");
module.exports = async function authCheck(
req: Request,
res: Response,
next: NextFunction
) {
logger.debug("auth.middleware: Auth check begun");
logger.logger.debug("auth.middleware: Auth check begun");
if (process.env.NODE_ENV === "development") {
req.isAuthed = true;
logger.warn("auth.middleware: DEV MODE - Access Granted");
logger.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.info("auth.middleware: Authentication failed");
logger.logger.info("auth.middleware: Authentication failed");
next();
} else if (typeof id === "string") {
const authCheck = (await isAuthed(id)) || false;
const authCheck = (await utils.isAuthed(id)) || false;
if (authCheck) {
// Authenticate
req.isAuthed = true;
logger.logger.info("auth.middleware: Authentication Successful");
next();
} else {
req.isAuthed = false;
logger.info("auth.middleware: Authentication Failed");
logger.logger.info("auth.middleware: Authentication Failed");
next();
}
// Handle cases where UUID passed as an array
} else if (Array.isArray(id)) {
const authCheck = (await isAuthed(id[0])) || false;
const authCheck = (await utils.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.warn(
logger.logger.warn(
"auth.middleware: UUID Passed as Array - Authentication Failed"
);
next();

View File

@ -4,7 +4,6 @@ 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

@ -0,0 +1,225 @@
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,14 +6,10 @@ 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, connOpts);
const client = new MongoClient(uri);
const db = client.db(dbName);
async function query(collection, query, returnId = false) {

View File

@ -5,11 +5,10 @@ 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;
@ -33,7 +32,7 @@ async function get(id, staff = false) {
logger.error(err, "ldbService.get: Error, Unable to find CRS");
return {
obStatus: "LOC_NOT_FOUND",
obMsg: "Location is not available",
obMsg: "UNABLE TO FIND MESSAGE",
};
}
}
@ -70,18 +69,14 @@ async function arrDepBoardStaff(CRS) {
};
const api = new ldb(ldbsvKey, true);
console.time(`Fetch Staff LDB for ${CRS.toUpperCase()}`);
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}
}
const result = await api.call(
"GetArrivalDepartureBoardByCRS",
options,
false,
false
);
console.log("\n\n\nORIGINAL DATA");
console.log("\n" + JSON.stringify(result) + "\n\n\n");
console.timeEnd(`Fetch Staff LDB for ${CRS.toUpperCase()}`);
try {
const _staffLdb = staffStationTransform(result);
@ -133,29 +128,6 @@ 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 {
@ -178,17 +150,6 @@ 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"),
@ -214,5 +175,4 @@ module.exports = {
getServicesByOther,
getReasonCodeList,
getReasonCode,
getNearestStations,
};

View File

@ -10,13 +10,12 @@ import { queryAggregate } from "./dbAccess.services";
import {
getPartialEndTiplocMatchPipeline,
getFullTiplocMatchPipeline,
getPartialStartTiplocMatchPipeline,
} from "../utils/pis.utils";
import { Document } from "mongodb";
export const supported = ["GW", "UK", "HX"];
export const supported = ["GW", "UK"];
export async function findPisByOrigDest(start: string, end: string) {
async function findPisByOrigDest(start: string, end: string) {
logger.debug(
`pisServices.findPisByOrigDest: Searching for Orig: ${start}, Dest: ${end}`
);
@ -41,13 +40,13 @@ export async function findPisByOrigDest(start: string, end: string) {
return search;
}
export async function findPisByCode(
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: cleanCode,
code: parseInt(cleanCode),
};
const search = db.query("pis", query);
return await search;
@ -79,12 +78,8 @@ export async function findByTiplocArray(
if (partialEnd) {
return convertDocument(partialEnd, "first");
} else {
const partialStart = await findPartialStartMatchByTiploc(tiplocArray);
if (partialStart) {
return convertDocument(partialStart, "last");
} else {
return null;
}
// Here, I should search for a partialStart match. For now return null.
return null;
}
}
} catch (err) {
@ -108,13 +103,6 @@ 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,32 +1,34 @@
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 = getDomainFromEmail(body.email);
const domain = await clean.getDomainFromEmail(body.email);
logger.info(`registerServices: Registration request from: ${domain}`);
if (validDomains.includes(domain)) {
if (domains.includes(domain)) {
logger.debug(`registerServices.createRegKey: Key from valid: ${domain}`);
const key = generateCode()
db.addRegReq(key, domain)
const message = await auth.generateConfirmationEmail(body.email, key);
const uuid = await auth.generateKey();
db.addRegReq(uuid, domain);
const message = await auth.generateConfirmationEmail(body.email, uuid);
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 {
@ -36,9 +38,8 @@ async function createRegKey(body) {
async function regUser(req) {
// Add input validation
const regCode = req.uuid.toLocaleUpperCase();
logger.trace(`Read UUID: ${regCode}`);
const res = await auth.checkRequest(regCode);
logger.trace(`Read UUID: ${req.uuid}`);
const res = await auth.checkRequest(req.uuid);
logger.debug(res, "registrationServices");
if (res.result) {
const uuid = await auth.generateKey();
@ -51,8 +52,6 @@ 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,6 +8,8 @@ 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");
@ -17,8 +19,12 @@ 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;
@ -36,9 +42,11 @@ async function hits() {
async function getVersions() {
logger.debug("statsServices.getVersions: Fetching versions");
const mqClt = await db.query("versions", { target: "timetable-mgr" });
const dbMan = await db.query("versions", { target: "dbmanager" });
const mqClt = await db.query("versions", { target: "mq-client" });
const data = {
backend: vers.app,
"db-manager": dbMan[0]?.["version"] || "",
"mq-client": mqClt[0]?.["version"] || "",
};
return data;
@ -47,10 +55,11 @@ async function getVersions() {
async function statistics() {
logger.debug("statsServices.statistics: Fetching statistics");
const timetablePromise = db.query("meta", { type: "CifMetadata" });
const pisPromise = db.query("meta", { type: "PisMetadata" });
const countersPromise = db.query("meta", { target: "counters" });
const timetablePromise = db.query("meta", { target: "timetable" });
const pisPromise = db.query("meta", { target: "pis" });
const corpusPromise = db.query("meta", { target: "corpus" });
const stationsPromise = db.query("meta", {type: "StationsMetadata"});
const reasonCodesPromise = db.query("meta", { target: "reasonCodes" });
const lengthUsersPromise = db.colCount("users");
const lengthRegistrationsPromise = db.colCount("registrations");
@ -61,37 +70,51 @@ async function statistics() {
const lengthReasonCodesPromise = db.colCount("reasonCodes");
const [
counters,
timetable,
pis,
corpus,
reasonCodes,
lengthUsers,
lengthRegistrations,
lengthCorpus,
lengthStations,
lengthPis,
lengthTimetable,
stations,
lengthReasonCodes,
] = await Promise.all([
countersPromise,
timetablePromise,
pisPromise,
corpusPromise,
reasonCodesPromise,
lengthUsersPromise,
lengthRegistrationsPromise,
lengthCorpusPromise,
lengthStationsPromise,
lengthPisPromise,
lengthTimetablePromise,
stationsPromise,
lengthReasonCodesPromise,
]);
return {
hostname: os.hostname() || "Unknown",
runtimeMode: process.env.NODE_ENV || "Unknown",
reset: counters[0]["since"],
updateTimes: {
timetable: (timetable[0]["lastUpdate"]),
pis: pis[0]["lastUpdate"],
corpus: corpus[0]["updated_time"],
stations: stations[0]["lastUpdate"],
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,
},
dbLengths: {
users: lengthUsers,
@ -100,6 +123,7 @@ 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,20 +56,18 @@ export async function findByTrainUid(
) {
// Set the correct date - whether a date or "now" was passed to function
let queryDate: Date;
if (date === 'now') {
queryDate = new Date();
} else if (date instanceof Date) {
if (date instanceof Date) {
queryDate = date;
} else {
queryDate = new Date(date);
queryDate = new 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);
@ -84,6 +82,7 @@ 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,10 +8,5 @@ declare global {
export interface Request {
isAuthed: boolean;
}
export interface Response {
cacheType: string;
cacheSecs: number;
}
}
}

View File

@ -20,6 +20,7 @@ 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);
@ -33,33 +34,20 @@ 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(/987654/g, uuid);
const htmlStr = htmlTpl.replace(/>>ACCESSCODE<</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(/987654/g, uuid),
text: (await txtTpl).replace(/>>ACCESSCODE<</g, uuid),
html: htmlMin,
};
} catch (err) {
@ -76,7 +64,6 @@ module.exports = {
generateKey,
generateConfirmationEmail,
checkRequest,
generateCode
};
export { isAuthed, generateKey, generateConfirmationEmail, checkRequest };

View File

@ -1,9 +0,0 @@
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

@ -1,47 +0,0 @@
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,61 +75,6 @@ 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,6 +5,7 @@ import type {
OB_TrainTT_stopDetail,
Stop,
} from "@owlboard/ts-types";
import { logger } from "../../logger.utils";
export function formatTimetableDetail(
service: Service,
@ -21,7 +22,7 @@ export function formatTimetableDetail(
scheduleEnd: service.scheduleEndDate,
daysRun: service.daysRun,
stops: formatStops(service.stops),
serviceDetail: service.serviceDetail,
vstp: service.vstp,
};
if (pis) {
@ -32,14 +33,6 @@ 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[] = [];
@ -70,22 +63,6 @@ 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,3 +1,5 @@
//const log = require('../utils/log.utils');
import { logger } from "./logger.utils";
function removeNonAlphanumeric(inputString: string) {

View File

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