Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature/1309: API version separation in NodeJS backend #1318

Merged
merged 15 commits into from
Apr 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion new-backend/.env
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,17 @@ NODE_ENV=development
# Port that the HTTP server will listen on
PORT=3002

# *** Backend API versions ***
# Hajk's backend comes with a versioned API.
# The initial, .NET compatible API is version 1.
# The improved version, which includes e.g. consolidated loading of
# config in client is version 2.
#
# The recommended setting is to leave this empty (i.e. outcommented). This means that all
# currently supported versions of the API are enabled.
# If you want to enable only specific version(s), supply a comma-separated list, e.g.
#API_VERSIONS=2,3 # Disables /api/v1, enables /api/v2 and /api/v3

# When an Express app is running behind a proxy, app.set('trust proxy') should be
# set. This setting simply passes its value to Express as the value of 'trust proxy'.
# To sum up: if set to true, the client IP will be extracted from the leftmost portion
Expand All @@ -43,7 +54,12 @@ REQUEST_LIMIT=1000kb
SESSION_SECRET=mySecret

# Control which directories will be statically exposed on the HTTP server.
# / can contain Hajk's client app
# Because the endpoints of the Static Exposer are not versioned, we must
# decide which API version we want to use for those routes. The default value
# is "LATEST" but you can specify any of the allowed API versions. Usually,
# the default value is fine.
STATIC_EXPOSER_VERSION=LATEST
# Expose Hajk's client app directly under /
EXPOSE_CLIENT=true
# If we expose /admin, we want probably to restrict access to it. Make sure
# to enable AD_* settings below in order for this to work.
Expand Down
42 changes: 21 additions & 21 deletions new-backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion new-backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"fast-xml-parser": "^4.0.12",
"helmet": "^6.0.1",
"http-proxy-middleware": "^2.0.6",
"log4js": "^6.7.1",
"log4js": "^6.9.1",
"node-windows": "^1.0.0-beta.8"
},
"devDependencies": {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { createProxyMiddleware } from "http-proxy-middleware";
import log4js from "log4js";
import fs from "fs";

// Value of process.env.LOG_LEVEL will be one of the allowed
// log4js-values. We will customize HPM to use log4js too,
Expand All @@ -21,13 +20,12 @@ const logLevels = {
};

// Grab a logger
const logger = log4js.getLogger("proxy.fmeServer");
const logger = log4js.getLogger("proxy.fmeServer.v1");

export default function fmeServerProxy(err, req, res, next) {
return createProxyMiddleware({
target: process.env.FME_SERVER_BASE_URL,
logLevel: logLevels[process.env.LOG_LEVEL],
logProvider: () => logger,
logLevel: "silent", // We don't care about logLevels[process.env.LOG_LEVEL] in this case as we log success and errors ourselves
changeOrigin: true,
secure: process.env.FME_SERVER_SECURE === "true", // should SSL certs be verified?
onProxyReq: (proxyReq, req, res) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import log4js from "log4js";
import ad from "../services/activedirectory.service";

const logger = log4js.getLogger("router");
const logger = log4js.getLogger("router.v1");

export default async function restrictAdmin(req, res, next) {
logger.trace("Attempt to access admin API methods");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import log4js from "log4js";
import ad from "../services/activedirectory.service";

const logger = log4js.getLogger("hajk.static.restrict");
const logger = log4js.getLogger("hajk.static.restrict.v1");
/**
* @summary Determine if current user is member in any of the required groups in order to access a given path.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,12 @@ const logLevels = {
};

// Grab a logger
const logger = log4js.getLogger("proxy.sokigo");
const logger = log4js.getLogger("proxy.sokigo.v1");

export default function sokigoFBProxy(err, req, res, next) {
return createProxyMiddleware({
target: process.env.FB_SERVICE_BASE_URL,
logLevel: logLevels[process.env.LOG_LEVEL],
logProvider: () => logger,
logLevel: "silent", // We don't care about logLevels[process.env.LOG_LEVEL] in this case as we log success and errors ourselves
pathRewrite: (originalPath, req) => {
// Remove the portion that shouldn't be there when we proxy the request
// and split the remaining string on "?" to separate any query params
Expand Down
15 changes: 15 additions & 0 deletions new-backend/server/apis/v1/router.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import * as express from "express";

import configRouter from "./controllers/config/router";
import mapconfigRouter from "./controllers/mapconfig/router";
import settingsRouter from "./controllers/settings/router";
import informativeRouter from "./controllers/informative/router";
import adRouter from "./controllers/ad/router";

export default express
.Router()
.use("/config", configRouter)
.use("/informative", informativeRouter)
.use("/mapconfig", mapconfigRouter)
.use("/settings", settingsRouter)
.use("/ad", adRouter);
43 changes: 43 additions & 0 deletions new-backend/server/apis/v2/controllers/ad/controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import ActiveDirectoryService from "../../services/activedirectory.service";
import handleStandardResponse from "../../utils/handleStandardResponse";
import log4js from "log4js";

// Create a logger for admin events, those will be saved in a separate log file.
const ael = log4js.getLogger("adminEvent.v2");

export class Controller {
availableADGroups(req, res) {
ActiveDirectoryService.getAvailableADGroups().then((data) =>
handleStandardResponse(res, data)
);
}

findCommonADGroupsForUsers(req, res) {
ActiveDirectoryService.findCommonADGroupsForUsers(req.query.users).then(
(data) => handleStandardResponse(res, data)
);
}

getStore(req, res) {
// Extract the store name from request's path
const store = req.route.path.substring(1);
ActiveDirectoryService.getStore(store).then((data) => {
handleStandardResponse(res, data);
// If data doesn't contain the error property, we're good - print event to admin log
!data.error &&
ael.info(
`${res.locals.authUser} viewed contents of AD store "${store}"`
);
});
}

flushStores(req, res) {
ActiveDirectoryService.flushStores().then((data) => {
handleStandardResponse(res, data);
// If data doesn't contain the error property, we're good - print event to admin log
!data.error && ael.info(`${res.locals.authUser} flushed all AD stores`);
});
}
}

export default new Controller();
13 changes: 13 additions & 0 deletions new-backend/server/apis/v2/controllers/ad/router.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import * as express from "express";
import controller from "./controller";
import restrictAdmin from "../../middlewares/restrict.admin";

export default express
.Router()
.use(restrictAdmin) // We will not allow any of the following routes unless user is admin
.get("/availableadgroups", controller.availableADGroups)
.get("/findcommonadgroupsforusers", controller.findCommonADGroupsForUsers)
.get("/users", controller.getStore)
.get("/groups", controller.getStore)
.get("/groupsPerUser", controller.getStore)
.put("/flushStores", controller.flushStores);
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import ConfigServiceV2 from "../../services/config.service.v2";
import ConfigService from "../../services/config.service";
import ad from "../../services/activedirectory.service";
import handleStandardResponse from "../../utils/handleStandardResponse";

Expand All @@ -12,7 +12,7 @@ export class Controller {
* @memberof Controller
*/
byMap(req, res) {
ConfigServiceV2.getMapWithLayers(
ConfigService.getMapWithLayers(
req.params.map,
ad.getUserFromRequestHeader(req)
).then((data) => handleStandardResponse(res, data));
Expand Down
62 changes: 62 additions & 0 deletions new-backend/server/apis/v2/controllers/informative/controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import InformativeService from "../../services/informative.service";
import log4js from "log4js";

// Create a logger for admin events, those will be saved in a separate log file.
const ael = log4js.getLogger("adminEvent.v2");

export class Controller {
create(req, res) {
const { documentName, mapName } = JSON.parse(req.body);
InformativeService.create(documentName, mapName).then((r) => {
// FIXME: The buggy admin expects 200 and this string on success,
// but I think that we'd do better with a meaningful JSON response.
if (r && !r.error) {
res.status(200).send("Document created");
ael.info(
`${res.locals.authUser} created a new document, ${documentName}.json, and connected it to map ${mapName}.json`
);
} else res.status(500).send(r.error.message);
});
}

getByName(req, res) {
InformativeService.getByName(req.params.name).then((r) => {
if (r && !r.error) res.json(r);
else {
res
.status(404)
.send(`Document "${req.params.name}" could not be found`);
}
});
}

saveByName(req, res) {
InformativeService.saveByName(req.params.name, req.body).then((r) => {
if (r && !r.error) {
res.status(200).send("File saved");
ael.info(
`${res.locals.authUser} saved document ${req.params.name}.json`
);
} else res.status(500).send(r.error.message);
});
}

deleteByName(req, res) {
InformativeService.deleteByName(req.params.name).then((r) => {
if (r && !r.error) {
res.status(200).send("File saved");
ael.info(
`${res.locals.authUser} deleted document ${req.params.name}.json`
);
} else res.status(500).send(r.error.message);
});
}

list(req, res) {
InformativeService.getAvailableDocuments().then((r) => {
if (r && !r.error) res.json(r);
else res.status(500).send(r.error.message);
});
}
}
export default new Controller();
13 changes: 13 additions & 0 deletions new-backend/server/apis/v2/controllers/informative/router.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import * as express from "express";
import controller from "./controller";
import restrictAdmin from "../../middlewares/restrict.admin";

export default express
.Router()
.get("/load/:name", controller.getByName)
.use(restrictAdmin) // All routes that follow are admin-only!
.put("/create", controller.create) // PUT is correct here, as this operation is idempotent
.delete("/delete/:name", controller.deleteByName)
.get("/list", controller.list)
.get("/list/:name", controller.list) // FIXME: For now, the name paramter is ignored - should list only documents connected to specified map
.put("/save/:name", controller.saveByName); // PUT is correct here, as this operation is idempotent
Loading