Skip to content

Commit ea09b9b

Browse files
Merge pull request #540 from dave-ok/ft-settings-middleware
PR - [feature]: Add middlware to retrieve settings
2 parents ea8c4dc + c5ffedf commit ea09b9b

9 files changed

Lines changed: 689 additions & 12 deletions

File tree

package-lock.json

Lines changed: 22 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44
"description": "A single-/multi-tenant authentication microservice",
55
"main": "index.js",
66
"scripts": {
7-
"test": "cross-env NODE_ENV=test jest",
8-
"test:watch": "cross-env NODE_ENV=test jest --watch",
9-
"test:cover": "cross-env NODE_ENV=test jest --coverage",
10-
"test:ci": "cross-env NODE_ENV=test jest --coverage && shx cat ./coverage/lcov.info",
7+
"test": "cross-env NODE_ENV=test jest --verbose",
8+
"test:watch": "cross-env NODE_ENV=test jest --watch --verbose",
9+
"test:ci": "cross-env NODE_ENV=test jest --coverage --verbose && shx cat ./coverage/lcov.info",
10+
"test:cover": "cross-env NODE_ENV=test jest --coverage --verbose",
1111
"lint": "eslint \"src/**/*.js\"",
1212
"lint:fix": "eslint --fix \"src/**/*.js\"",
1313
"build": "babel src --out-dir dist --delete-dir-on-start --ignore '**/*.test.js'",
@@ -38,6 +38,7 @@
3838
"dotenv": "^8.2.0",
3939
"express": "^4.17.1",
4040
"express-jwt": "^6.0.0",
41+
"express-validator": "^6.6.1",
4142
"jsonwebtoken": "^8.5.1",
4243
"mongoose": "^5.9.27",
4344
"swagger-jsdoc": "^4.0.0",
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import express from "express";
2+
import request from "supertest";
3+
import errorHandler from "../../utils/errorhandler";
4+
import jwt from "jsonwebtoken";
5+
import * as mocks from "../../utils/mocks/settings";
6+
import settingsMiddleware from "../settings";
7+
8+
describe("Settings middleware", () => {
9+
let app;
10+
beforeAll(() => {
11+
process.env.API_SECRET = "secret";
12+
app = express();
13+
app.use(settingsMiddleware);
14+
app.get("/", (req, res) => {
15+
res.json({ projectId: req.projectId });
16+
});
17+
app.use((err, req, res, next) => {
18+
errorHandler(err, req, res, next);
19+
});
20+
});
21+
22+
it("should throw an error if no API key is present", async () => {
23+
const res = await request(app).get("/");
24+
expect(res.status).toBe(401);
25+
});
26+
it("should throw an error if invalid API key is present", async () => {
27+
const res = await request(app)
28+
.get("/")
29+
.set("X-MicroAPI-ProjectKey", "crapKey");
30+
expect(res.status).toBe(401);
31+
expect(res.body.error).toBe("Invalid API key found");
32+
});
33+
34+
it("should throw invalid settings error", async () => {
35+
const mockSettings = jest
36+
.spyOn(mocks, "mockSettings")
37+
.mockImplementation(() => {
38+
return mocks.errorMockSettings;
39+
});
40+
41+
const apiKey = jwt.sign(
42+
{ projectId: 123 },
43+
Buffer.from(process.env.API_SECRET, "base64")
44+
);
45+
const res = await request(app)
46+
.get("/")
47+
.set("X-MicroAPI-ProjectKey", apiKey);
48+
expect(mockSettings).toHaveBeenCalled();
49+
expect(res.status).toBe(400);
50+
expect(res.body.error).toBe("Invalid settings found");
51+
52+
mockSettings.mockRestore();
53+
});
54+
55+
it("should return status code 200 and decoded projectId", async () => {
56+
const apiKey = jwt.sign(
57+
{ projectId: 123 },
58+
Buffer.from(process.env.API_SECRET, "base64")
59+
);
60+
const res = await request(app)
61+
.get("/")
62+
.set("X-MicroAPI-ProjectKey", apiKey);
63+
expect(res.status).toBe(200);
64+
expect(res.body.projectId).toBe(123);
65+
});
66+
});

src/middleware/settings.js

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
require("dotenv").config();
2+
import jwt from "jsonwebtoken";
3+
import CustomError from "../utils/customError";
4+
import { parseSettings } from "../utils/settingsParser";
5+
import { mockSettings as fetchSettings } from "../utils/mocks/settings";
6+
const log = require("debug")("log");
7+
8+
export const getProjectId = (apiKey) => {
9+
//verify/decode projectID from API key
10+
const decoded = jwt.verify(
11+
apiKey,
12+
Buffer.from(process.env.API_SECRET, "base64")
13+
);
14+
return decoded.projectId;
15+
};
16+
17+
//get settings from external DB or endpoint
18+
//function might be modified to accomodate both sources
19+
export const getSettings = async (apiKey) => {
20+
//fool linter
21+
log(apiKey);
22+
23+
//mock the request for now with mocksettings
24+
//settings need to come from source
25+
//validate the settings by matching against predefined schema
26+
const settings = parseSettings(fetchSettings(), true);
27+
return settings;
28+
};
29+
30+
const settingsMiddleware = async (req, res, next) => {
31+
/* In multi-tenant app, projectID is retreived from API key in a custom HTTP header
32+
** For now we stick with multi-tenant and we will customize this to cater for **
33+
** single tenancy architecture in time where projectIDs are irrelevant **
34+
** In retrospect, expiry of API keys should be from MicroAPI, **
35+
** so making a request for settings with an invalid API key will be rejected **
36+
*/
37+
try {
38+
// we are calling our custom HTTP header X-MicroAPI-ProjectKey
39+
const apiKey = req.headers["x-microapi-projectkey"];
40+
if (!apiKey) throw new CustomError(401, "No API key found");
41+
42+
//validate apiKey
43+
let projectId;
44+
try {
45+
projectId = getProjectId(apiKey);
46+
} catch (error) {
47+
throw new CustomError(401, "Invalid API key found");
48+
}
49+
50+
// get settings from parent DB/source
51+
const settings = await getSettings(apiKey);
52+
if (settings.errors) {
53+
throw new CustomError(400, "Invalid settings found", settings.errors);
54+
}
55+
56+
//attach setting to request body
57+
req.settings = settings;
58+
req.projectId = projectId;
59+
60+
//pass to next middleware
61+
next();
62+
} catch (error) {
63+
next(error);
64+
}
65+
};
66+
67+
export default settingsMiddleware;
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { parseSettings } from "../settingsParser";
2+
3+
describe("Settings Parser test", () => {
4+
it("should return two settings", () => {
5+
const dummySetting = [
6+
{
7+
setting_name: "Setting 1",
8+
setting_type: "String",
9+
setting_key: "setting1",
10+
setting_value: null,
11+
setting_required: false,
12+
},
13+
{
14+
setting_name: "Setting 2",
15+
setting_type: "Number",
16+
setting_key: "setting2",
17+
setting_value: null,
18+
setting_required: false,
19+
},
20+
];
21+
const settings = parseSettings(dummySetting);
22+
expect(Object.keys(settings).length).toBe(2);
23+
});
24+
25+
it("should return two settings and one required error", () => {
26+
const dummySetting = [
27+
{
28+
setting_name: "Setting 1",
29+
setting_type: "String",
30+
setting_key: "setting1",
31+
setting_value: null,
32+
setting_required: true,
33+
},
34+
{
35+
setting_name: "Setting 2",
36+
setting_type: "Number",
37+
setting_key: "setting2",
38+
setting_value: 5,
39+
setting_required: true,
40+
},
41+
];
42+
const settings = parseSettings(dummySetting, true);
43+
44+
expect(Object.keys(settings).length).toBe(3);
45+
expect(settings.errors.length).toBe(1);
46+
});
47+
48+
it("should return two settings, one nested setting and one required error", () => {
49+
const dummySetting = [
50+
{
51+
setting_name: "Setting 1",
52+
setting_type: "String",
53+
setting_key: "setting1",
54+
setting_value: null,
55+
setting_required: true,
56+
},
57+
{
58+
setting_name: "Setting 2",
59+
setting_type: "Number",
60+
setting_key: "setting2",
61+
setting_value: 5,
62+
setting_required: true,
63+
},
64+
{
65+
setting_name: "Setting 3",
66+
setting_type: "Array",
67+
setting_key: "setting3",
68+
setting_required: true,
69+
setting_value: [
70+
{
71+
setting_name: "Setting 4",
72+
setting_type: "Number",
73+
setting_key: "setting4",
74+
setting_value: null,
75+
setting_required: true,
76+
},
77+
{
78+
setting_name: "Setting 5",
79+
setting_type: "String",
80+
setting_key: "setting5",
81+
setting_value: 5,
82+
setting_required: true,
83+
},
84+
],
85+
},
86+
];
87+
const settings = parseSettings(dummySetting, true);
88+
89+
expect(Object.keys(settings.setting3).length).toBe(2);
90+
expect(settings.errors.length).toBe(3);
91+
});
92+
93+
it("should return two settings and one type error one required error", () => {
94+
const dummySetting = [
95+
{
96+
setting_name: "Setting 1",
97+
setting_type: "String",
98+
setting_key: "setting1",
99+
setting_value: 5,
100+
setting_required: true,
101+
},
102+
{
103+
setting_name: "Setting 2",
104+
setting_type: "Number",
105+
setting_key: "setting2",
106+
setting_value: null,
107+
setting_required: true,
108+
},
109+
];
110+
const settings = parseSettings(dummySetting, true);
111+
expect(Object.keys(settings).length).toBe(3);
112+
expect(settings.errors.length).toBe(2);
113+
});
114+
});

src/utils/customError.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
export default class CustomError extends Error {
2-
constructor(statusCode, message) {
2+
constructor(statusCode, message, errors) {
33
super();
44
this.statusCode = statusCode;
55
this.message = message;
6+
this.errors = errors;
67
}
78
}

src/utils/errorhandler.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ const errorHandler = (err, req, res) => {
33
res.status(err.statusCode).json({
44
status: "error",
55
error: err.message,
6+
errors: err.errors,
67
});
78
} else if (err.status) {
89
res.status(err.status).json({
910
status: "error",
1011
error: err.message,
12+
errors: err.errors,
1113
});
1214
} else {
1315
res.status(500).json({

0 commit comments

Comments
 (0)