diff --git a/Jenkinsfile b/Jenkinsfile
index c313036..84fc8af 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -90,17 +90,17 @@ pipeline {
parallel (
"Unit" : {
colourText("info","Running unit tests...")
- sh 'npm run-script test-unit'
+ sh 'npm run test:unit'
},
"Stress" : {
colourText("info","Running stress tests...")
- sh 'ENV=local node server/ & HOST=http://localhost:3001 REQUEST=5000 REQ_PER_SECOND=50 npm run-script test-load'
+ sh 'ENV=local node server/ & HOST=http://localhost:3001 REQUEST=5000 REQ_PER_SECOND=50 npm run test:load'
// sh 'killall node'
// The above command will leave node running, will this be closed along with the workspace?
},
"Server" : {
colourText("info","Running server tests...")
- sh "npm run-script test-server"
+ sh "npm run test:server"
}
)
}
@@ -124,11 +124,11 @@ pipeline {
parallel (
"Coverage Report" : {
colourText("info","Generating coverage report...")
- sh "npm run-script cover"
+ sh "npm run cover"
},
"Style Report" : {
colourText("info","Generating style report...")
- sh 'npm run-script lint-report-xml'
+ sh 'npm run lint-report-xml'
}
)
}
diff --git a/README.md b/README.md
index cd7c044..da3733d 100644
--- a/README.md
+++ b/README.md
@@ -29,32 +29,29 @@ export BI_UI_TEST_ADMIN_USERNAME=admin
export BI_UI_TEST_ADMIN_PASSWORD=admin
export BI_UI_TEST_USER_USERNAME=test
export BI_UI_TEST_USER_PASSWORD=test
-export JWT_SECRET=SECRET
```
## Running the UI:
-1. Clone this repo, install dependencies and start NPM
+1. Clone this repo and install dependencies
```shell
git clone https://github.com/ONSdigital/bi-ui.git
cd bi-ui
npm install
-npm start
```
-The NPM start command uses the following commands:
+2. Start the `Node.js` server
```shell
-npm run build
-SERVE_HTML=true ENV=local node server
+npm run start:server
```
-This will run Node and React on localhost:3001, since Node is serving
-`index.html`, hot-reloading will not work.
+3. Start the `React.js` development server (with hot reloading)
-To use hot-reloading, use `npm restart` which runs `react-scripts start`, this
-will start React on port 3000. To start the server, use `ENV=local node server/index.js`.
+```shell
+npm run start:react
+```
## Running the API
diff --git a/package.json b/package.json
index ee1bac5..d543007 100644
--- a/package.json
+++ b/package.json
@@ -15,71 +15,47 @@
"babel-preset-react": "^6.24.1",
"babel-preset-stage-0": "^6.24.1",
"chai": "^3.5.0",
- "enzyme": "^2.9.0",
- "enzyme-redux": "^0.1.6",
"eslint": "^3.0.0",
"eslint-config-airbnb": "^15.0.1",
"eslint-plugin-flowtype": "^2.34.1",
"eslint-plugin-import": "^2.3.0",
"eslint-plugin-jsx-a11y": "^4.0.0",
"eslint-plugin-react": "^7.1.0",
- "flow-bin": "^0.48.0",
+ "eslint-html-reporter": "^0.5.2",
"jasmine": "^2.5.3",
- "jasmine-enzyme": "^3.3.0",
- "jasmine-es6": "^0.4.0",
"jasmine-node": "^1.14.5",
- "jsdom": "11.0.0",
- "jsdom-global": "3.0.2",
+ "jasmine-es6": "^0.4.0",
+ "coveralls": "^2.13.1",
+ "flow-bin": "^0.48.0",
"mocha": "^3.0.2",
+ "istanbul": "^0.4.5",
+ "morgan": "^1.7.0",
"mz": "^2.4.0",
"react-scripts": "0.2.3",
"react-test-renderer": "^15.6.1",
- "redux-mock-store": "^1.2.3",
- "redux-test-utils": "^0.1.2",
"selenium-webdriver": "^3.3.0",
+ "loadtest": "^2.3.0",
"supertest": "^3.0.0",
"supertest-as-promised": "^4.0.0"
},
"dependencies": {
"base-64": "^0.1.0",
- "bcryptjs": "^2.4.3",
"body-parser": "^1.17.2",
"compression": "^1.7.0",
- "coveralls": "^2.13.1",
- "d3": "^4.10.0",
- "eslint-html-reporter": "^0.5.2",
"express": "^4.14.0",
"halogen": "^0.2.0",
- "ie-version": "^0.1.0",
- "istanbul": "^0.4.5",
- "jasmine": "^2.5.3",
- "jasmine-node": "^1.14.5",
- "jquery": "^3.2.1",
- "loadtest": "^2.3.0",
"memory-cache": "^0.2.0",
- "morgan": "^1.7.0",
- "npm-run-script": "0.0.4",
"object.assign": "^4.0.4",
"prop-types": "^15.5.10",
- "rc-pagination": "^1.7.3",
"react": "^15.6.2",
"react-a11y": "^0.3.4",
- "react-bootstrap": "^0.30.7",
- "react-bootstrap-button-loader": "1.0.8",
- "react-bootstrap-table": "^3.4.5",
"react-confetti": "^2.0.1",
- "react-copy-to-clipboard": "^5.0.1",
- "react-d3-tree": "^1.4.0",
"react-dom": "^15.5.4",
"react-modal-dialog": "^4.0.7",
"react-redux": "^5.0.5",
"react-router": "^2.6.1",
- "react-router-bootstrap": "^0.23.1",
"react-select": "^1.0.0-rc.10",
- "react-stepper-horizontal": "^1.0.9",
"react-table": "^6.5.3",
- "react-toggle": "^4.0.1",
- "react-tooltip": "^3.3.0",
"redux": "^3.6.0",
"redux-thunk": "^2.2.0",
"registers-react-library": "^1.0.0",
@@ -93,18 +69,17 @@
"npm": "3.8.3"
},
"scripts": {
- "start:server": "ENV=local SERVER_AUTH_URL=http://localhost:3002/auth SERVER_API_GW_URL=http://localhost:3002 node server",
- "start": "npm run build; SERVE_HTML=true ENV=local SERVER_AUTH_URL=http://localhost:3002/auth SERVER_API_GW_URL=http://localhost:3002 node server",
- "restart": "REACT_APP_ENV=local REACT_APP_AUTH_URL=http://localhost:3001 REACT_APP_API_URL=http://localhost:3001/api react-scripts start",
+ "start": "REACT_APP_ENV=local REACT_APP_AUTH_URL=http://localhost:3001 REACT_APP_API_URL=http://localhost:3001/api npm run build; SERVE_HTML=true NODE_ENV=development SERVER_AUTH_URL=http://localhost:3002/auth SERVER_API_GW_URL=http://localhost:3002 node server",
+ "start:server": "NODE_ENV=development SERVER_AUTH_URL=http://localhost:3002/auth SERVER_API_GW_URL=http://localhost:3002 node server",
+ "start:react": "REACT_APP_ENV=local REACT_APP_AUTH_URL=http://localhost:3001 REACT_APP_API_URL=http://localhost:3001/api react-scripts start",
"build": "react-scripts build",
"eject": "react-scripts eject",
"cover": "NODE_ENV=test SERVE_HTML=true ENV=local ./node_modules/istanbul/lib/cli.js cover --report cobertura ./node_modules/mocha/bin/_mocha -- -R spec test/server.test.js",
- "test": "npm run-script test-unit; npm run-script test-components; npm run-script test-server; npm run-script test-server",
- "test-components": "./node_modules/babel-cli/bin/babel-node.js test/component-tests.js",
- "test-unit": "./node_modules/babel-cli/bin/babel-node.js test/utils-unit-tests.js",
- "test-load": "./node_modules/babel-cli/bin/babel-node.js test/loadtest-unit-tests.js",
- "test-server": "ENV=local SERVE_HTML=true ./node_modules/mocha/bin/mocha test/server.test.js",
- "test-selenium": "NODE_ENV=test UI_URL=http://localhost:3000 ./node_modules/jasmine/bin/jasmine.js test/integration-test.js",
+ "test": "npm run test:unit; npm run test:server; npm run test:server",
+ "test:unit": "./node_modules/babel-cli/bin/babel-node.js test/utils-unit-tests.js",
+ "test:load": "./node_modules/babel-cli/bin/babel-node.js test/loadtest-unit-tests.js",
+ "test:server": "./node_modules/mocha/bin/mocha test/server.test.js; ./node_modules/mocha/bin/mocha test/server-test-spec --recursive",
+ "test:selenium": "NODE_ENV=test UI_URL=http://localhost:3000 ./node_modules/jasmine/bin/jasmine.js test/integration-test.js",
"lint": "./node_modules/eslint/bin/eslint.js **/*.js --ignore-pattern /test/*.js",
"lint-report-xml": "./node_modules/eslint/bin/eslint.js . -f checkstyle -o ./coverage/eslint-report-checkstyle.xml; exit 0",
"flow": "flow"
diff --git a/server/app.js b/server/app.js
deleted file mode 100644
index a8f66b6..0000000
--- a/server/app.js
+++ /dev/null
@@ -1,236 +0,0 @@
-'use strict';
-
-// Rule exceptions:
-/* eslint strict: "off" */
-/* eslint comma-dangle: ["error", "never"] */
-
-const express = require('express');
-const morgan = require('morgan');
-const path = require('path');
-const uuidv4 = require('uuid/v4');
-const myParser = require('body-parser');
-const urls = require('./config/urls');
-const timeouts = require('./config/timeouts');
-const version = require('./package.json').version;
-const rp = require('request-promise');
-const compression = require('compression');
-const cache = require('./helpers/cache');
-const formatDate = require('./helpers/formatDate.js');
-const logger = require('./logger')(module);
-
-// To allow hot-reloading, the node server only serves the React.js index.html
-// in the /build file if SERVE_HTML is true
-const ENV = process.env.ENV;
-const SERVE_HTML = (process.env.SERVE_HTML === 'true');
-
-const sessions = {}; // For the user sessions
-const startTime = formatDate(new Date());
-
-const app = express();
-
-app.use(compression()); // gzip all responses
-morgan(':remote-addr - :remote-user [:date[clf]] ":method :url HTTP/:http-version" :status :res[content-length] :response-time ms');
-app.use(morgan('combined', { stream: logger.stream }));
-app.use(myParser.json()); // For parsing body of POSTs
-
-// Serve static assets (static js files for React from 'npm run build')
-if (SERVE_HTML) {
- logger.info('Serving static html in build dir');
- app.use(express.static(path.resolve(__dirname, '..', 'build')));
-}
-
-// Below is for CORS, CORS is only needed when React/Node are on different ports
-// e.g. when testing locally and React is on 3000 and Node is on 3001
-if (ENV === 'local') {
- logger.info('Using Access-Control-Allow-Origin CORS headers');
- app.use((req, res, next) => {
- res.header('Access-Control-Allow-Origin', '*');
- res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');
- next();
- });
-}
-
-// This method needs to be above the serve React code
-// If it's below, the get('*') will point all GETs to the React
-app.get('/info', cache(), (req, res) => {
- logger.info('Returning /info');
- res.send(JSON.stringify({
- version,
- lastUpdate: startTime
- }));
-});
-
-// Always return the main index.html, so react-router renders the route in the client
-if (SERVE_HTML) {
- logger.info('Serving /build dir static files');
- app.get('*', cache(), (req, res) => {
- res.sendFile(path.resolve(__dirname, '..', 'build', 'index.html'));
- });
-}
-
-app.post('/login', (req, res) => {
- logger.info('Logging user in');
- // Get the username from the body of the POST
- const username = req.body.username;
-
- const basicAuth = req.get('Authorization');
- let options = {
- method: 'POST',
- uri: urls.AUTH_URL,
- timeout: timeouts.API_GW,
- headers: {
- 'Content-Type': 'application/json',
- 'Authorization': `${basicAuth}`
- },
- json: true,
- body: { username }
- };
- if (ENV === 'prod') {
- options = {
- method: 'POST',
- uri: urls.AUTH_URL,
- timeout: timeouts.API_GW,
- headers: {
- 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
- 'Authorization': `${basicAuth}`
- },
- json: true
- };
- }
-
- rp(options)
- .then((gatewayJson) => {
- // Create user session
- const accessToken = uuidv4();
- // We get the showConfetti env var on every login, as it can dynamically change in CF
- const showConfetti = (process.env.SHOW_CONFETTI === 'true');
- sessions[accessToken] = {
- key: gatewayJson.key,
- role: gatewayJson.role,
- username
- };
-
- logger.info('Successful login');
- res.setHeader('Content-Type', 'application/json');
- return res.send(JSON.stringify({
- username,
- accessToken,
- role: gatewayJson.role,
- showConfetti
- }));
- })
- .catch((err) => {
- logger.error('Unable to login, timeout or server error');
- if (err.statusCode) return res.sendStatus(err.statusCode);
- return res.sendStatus(504); // Timeout
- });
-});
-
-app.post('/checkToken', (req, res) => {
- logger.info('Checking token');
- const accessToken = req.body.accessToken;
-
- if (sessions[accessToken]) {
- logger.info('Valid token');
- res.setHeader('Content-Type', 'application/json');
- res.send(JSON.stringify({
- username: sessions[accessToken].username,
- accessToken,
- role: sessions[accessToken].role
- }));
- } else {
- logger.info('Invalid token');
- res.sendStatus(401);
- }
-});
-
-app.post('/logout', (req, res) => {
- const token = req.body.token;
- // Remove user from storage
- delete sessions[token];
- logger.info('Logging user out');
- res.sendStatus(200);
-});
-
-app.post('/logout', (req, res) => {
- logger.info('Logging user out');
- const token = req.body.token;
- try {
- // Remove user from storage
- delete sessions[token];
- logger.info('Successful logout');
- res.sendStatus(200);
- } catch (e) {
- logger.error(`Unable to log user out: ${e}`);
- res.sendStatus(500);
- }
-});
-
-app.post('/api', (req, res) => {
- // re route api requests with API key
- const method = req.body.method;
- const endpoint = req.body.endpoint;
- const accessToken = req.get('Authorization');
-
- if (sessions[accessToken]) {
- const key = sessions[accessToken].key;
- if (method === 'GET') {
- getApiEndpoint(`${urls.API_GW}/bi/${endpoint}`, key)
- .then((response) => {
- logger.info('Returning GET response from API Gateway');
- return res.send(response);
- })
- .catch((err) => {
- logger.info('Error in API Gateway for GET request');
- return res.status(err.statusCode).send(err);
- });
- } else if (method === 'POST') {
- const postBody = req.body.postBody;
- postApiEndpoint(`${urls.API_GW}/bi/${endpoint}`, postBody, key)
- .then((response) => {
- logger.info('Returning POST response from API Gateway');
- return res.send(response);
- })
- .catch((err) => {
- logger.info('Error in API Gateway for POST request');
- return res.status(err.statusCode).send(err);
- });
- }
- } else {
- logger.info('Unable to use /api endpoint, not authenticated');
- return res.sendStatus(401);
- }
-});
-
-function getApiEndpoint(url, apiKey) {
- logger.debug(`GET API endpoint for url: ${url}`);
- const options = {
- method: 'GET',
- headers: {
- 'Authorization': apiKey
- },
- uri: url,
- timeout: timeouts.API_GET
- };
-
- return rp(options);
-}
-
-function postApiEndpoint(url, postBody, apiKey) {
- logger.debug(`POST API endpoint for url: ${url}`);
- const options = {
- method: 'POST',
- uri: url,
- timeout: timeouts.API_POST,
- headers: {
- 'Authorization': apiKey,
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify(postBody),
- json: false
- };
-
- return rp(options);
-}
-
-module.exports = app;
diff --git a/server/config/sessions.js b/server/config/sessions.js
new file mode 100644
index 0000000..d929ce1
--- /dev/null
+++ b/server/config/sessions.js
@@ -0,0 +1,6 @@
+const sessions = {
+ SESSION_EXPIRE: 60 * 60 * 8,
+ SESSION_EXPIRE_HOURS: 8,
+};
+
+module.exports = sessions;
diff --git a/server/index.js b/server/index.js
index d5e3316..ccd4e60 100644
--- a/server/index.js
+++ b/server/index.js
@@ -1,21 +1,85 @@
'use strict';
-/* eslint strict: "off" */
-/* eslint no-console: "off" */
-
+const express = require('express');
const fork = require('child_process').fork;
-const app = require('./app');
const logger = require('./logger')(module);
+const compression = require('compression');
+const morgan = require('morgan');
+const myParser = require('body-parser');
+const path = require('path');
+const JsonSession = require('./sessions/JsonSession');
+// Environment Variables
+const SERVE_HTML = (process.env.SERVE_HTML === 'true'); // To server the React /build
const PORT = process.env.PORT || 3001;
+const LOG_LEVEL = (process.env.NODE_ENV === 'production') ? 'info' : 'debug';
+const SESSION_DB = process.env.SESSION_DB || 'json';
+const ENV = process.env.NODE_ENV;
-// On a local environment, we mock the API Gateway with the a node script on localhost:3002
-const child = (process.env.ENV === 'local') ? fork('./server/apiGateway') : null;
+// On a local environment, we mock the API Gateway with a node script on localhost:3002
+const child = (ENV === 'development') ? fork('./server/apiGateway') : null;
-logger.level = 'debug';
+logger.level = LOG_LEVEL;
logger.info('Started Winston logger & created log file');
+logger.info(`NODE_ENV: ${process.env.NODE_ENV}`);
+logger.info(`LOG_LEVEL: ${LOG_LEVEL}`);
+
+// Choose which session type to use
+const session = ((db) => {
+ switch (db) {
+ case 'json':
+ logger.debug('Creating new JsonSession');
+ return new JsonSession();
+ default:
+ logger.debug('Creating new JsonSession');
+ return new JsonSession();
+ }
+})(SESSION_DB);
+logger.info(`Using session type: ${session.name}`);
+
+// https://stackoverflow.com/questions/10090414/express-how-to-pass-app-instance-to-routes-from-a-different-file
+const app = module.exports = express();
+
+// Attach the cache function to the app
+app.cache = require('./helpers/cache');
+
+// Gzip all responses
+app.use(compression());
+
+// Logging configuration
+morgan(':remote-addr - :remote-user [:date[clf]] ":method :url HTTP/:http-version" :status :res[content-length] :response-time ms');
+app.use(morgan('combined', { stream: logger.stream })); // Send Morgan logs to Winston
+
+// For parsing the body of POSTs
+app.use(myParser.json());
+
+// Serve static assets (static js files for React from 'npm run build')
+if (SERVE_HTML) {
+ logger.info('Serving static html in build dir');
+ app.use(express.static(path.resolve(__dirname, '..', 'build')));
+}
+
+// Below is for CORS, CORS is only needed when React/Node are on different ports
+// e.g. when testing locally and React is on 3000 and Node is on 3001
+if (ENV === 'development') {
+ logger.info('Using Access-Control-Allow-Origin CORS headers');
+ app.use((req, res, next) => {
+ res.header('Access-Control-Allow-Origin', '*');
+ res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');
+ res.header('Access-Control-Allow-Methods', 'POST, PUT, GET, OPTIONS');
+ next();
+ });
+}
+
+// Attach the session class to the app
+app.session = session;
+app.env = ENV;
+
+// Routes
+app.use(require('./routes/auth'));
+app.use(require('./routes/api'));
+if (SERVE_HTML) app.use(require('./routes/staticFiles'));
-app.maxSockets = 500;
app.listen(PORT, () => {
logger.info(`bi-ui-node-server listening on port ${PORT}!`);
});
@@ -26,12 +90,11 @@ process.stdin.resume(); // so the program will not close instantly
function exitHandler(options, err) {
if (options.cleanup) {
if (process.env.ENV === 'local') {
- console.log('Killing child process (bi-ui-mock-api-gateway)...');
logger.info('Killing child process (bi-ui-mock-api-gateway)...');
child.kill('SIGINT');
}
}
- if (err) console.log(err.stack);
+ if (err) logger.error(err.stack);
if (options.exit) process.exit();
}
diff --git a/server/package.json b/server/package.json
index 44f31a0..980247c 100644
--- a/server/package.json
+++ b/server/package.json
@@ -26,6 +26,6 @@
"npm": "3.8.3"
},
"scripts": {
- "start": "SERVE_HTML=true ENV=local node index.js"
+ "start": "NODE_ENV=development SERVER_AUTH_URL=http://localhost:3002/auth SERVER_API_GW_URL=http://localhost:3002 node index.js"
}
}
diff --git a/server/routes/api.js b/server/routes/api.js
new file mode 100644
index 0000000..e796234
--- /dev/null
+++ b/server/routes/api.js
@@ -0,0 +1,103 @@
+'use strict';
+
+const app = require('../index');
+const express = require('express');
+const logger = require('../logger')(module);
+const formatDate = require('../helpers/formatDate');
+const version = require('../package.json').version;
+const urls = require('../config/urls');
+const timeouts = require('../config/timeouts');
+const rp = require('request-promise');
+
+const router = express.Router();
+
+const startTime = formatDate(new Date());
+
+const authMiddleware = function (req, res, next) {
+ // This middleware will be used in every /api/ method to
+ // validate the user provided accessToken
+ const accessToken = req.get('Authorization');
+
+ app.session.getSession(accessToken)
+ .then((json) => {
+ logger.info('Valid token');
+ req.username = json.username;
+ req.apiKey = json.apiKey;
+ next();
+ })
+ .catch(() => {
+ logger.info('Invalid token');
+ res.sendStatus(401).end();
+ });
+};
+
+router.get('/api/info', authMiddleware, (req, res) => {
+ logger.info('Returning /info');
+ res.send(JSON.stringify({
+ version,
+ lastUpdate: startTime,
+ }));
+});
+
+router.post('/api', authMiddleware, (req, res) => {
+ // re route api requests with API key
+ const method = req.body.method;
+ const endpoint = req.body.endpoint;
+
+ const key = req.apiKey;
+ if (method === 'GET') {
+ getApiEndpoint(`${urls.API_GW}/bi/${endpoint}`, key)
+ .then((response) => {
+ logger.info('Returning GET response from API Gateway');
+ return res.send(response);
+ })
+ .catch((err) => {
+ logger.info('Error in API Gateway for GET request');
+ return res.sendStatus(err.statusCode);
+ });
+ } else if (method === 'POST') {
+ const postBody = req.body.postBody;
+ postApiEndpoint(`${urls.API_GW}/bi/${endpoint}`, postBody, key)
+ .then((response) => {
+ logger.info('Returning POST response from API Gateway');
+ return res.send(response);
+ })
+ .catch((err) => {
+ logger.info('Error in API Gateway for POST request');
+ return res.sendStatus(err.statusCode);
+ });
+ }
+});
+
+function getApiEndpoint(url, apiKey) {
+ logger.debug(`GET API endpoint for url: ${url}`);
+ const options = {
+ method: 'GET',
+ headers: {
+ 'Authorization': apiKey,
+ },
+ uri: url,
+ timeout: timeouts.API_GET,
+ };
+
+ return rp(options);
+}
+
+function postApiEndpoint(url, postBody, apiKey) {
+ logger.debug(`POST API endpoint for url: ${url}`);
+ const options = {
+ method: 'POST',
+ uri: url,
+ timeout: timeouts.API_POST,
+ headers: {
+ 'Authorization': apiKey,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(postBody),
+ json: false,
+ };
+
+ return rp(options);
+}
+
+module.exports = router;
diff --git a/server/routes/auth.js b/server/routes/auth.js
new file mode 100644
index 0000000..ee1129d
--- /dev/null
+++ b/server/routes/auth.js
@@ -0,0 +1,121 @@
+'use strict';
+
+const app = require('../index');
+const express = require('express');
+const rp = require('request-promise');
+const uuidv4 = require('uuid/v4');
+const logger = require('../logger')(module);
+const urls = require('../config/urls');
+const timeouts = require('../config/timeouts');
+
+const router = express.Router();
+
+const createSessionMiddleware = (req, res) => {
+ app.session.createSession(req.username, req.connection.remoteAddress, req.role, req.key)
+ .then((sessionJson) => {
+ logger.info('Successful login');
+ res.send(JSON.stringify({
+ accessToken: sessionJson.accessToken,
+ username: req.body.username,
+ role: req.role,
+ showConfetti: req.showConfetti,
+ })).end();
+ })
+ .catch((error) => {
+ logger.error(`Login 500 server error: ${error}`);
+ res.sendStatus(500).end();
+ });
+};
+
+const loginMiddleware = (req, res, next) => {
+ const username = req.body.username;
+ const basicAuth = req.get('Authorization');
+
+ let options = {
+ method: 'POST',
+ uri: urls.AUTH_URL,
+ timeout: timeouts.API_GW,
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `${basicAuth}`,
+ },
+ json: true,
+ body: { username },
+ };
+ if (app.ENV === 'prod') {
+ options = {
+ method: 'POST',
+ uri: urls.AUTH_URL,
+ timeout: timeouts.API_GW,
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
+ 'Authorization': `${basicAuth}`,
+ },
+ json: true,
+ };
+ }
+
+ rp(options)
+ .then((gatewayJson) => {
+ // Create user session
+ const accessToken = uuidv4();
+ // We get the showConfetti env var on every login, as it can dynamically change in CF
+ const showConfetti = (process.env.SHOW_CONFETTI === 'true');
+
+ // Attach variables to the request for later use
+ req.showConfetti = showConfetti;
+ req.role = gatewayJson.role;
+ req.key = gatewayJson.key;
+ req.accessToken = accessToken;
+ req.username = username;
+
+ logger.info('Successful login');
+ next();
+ })
+ .catch((err) => {
+ logger.error(`Unable to login, timeout or server error: ${err}`);
+ if (err.statusCode) return res.sendStatus(err.statusCode).end();
+ return res.sendStatus(504).end(); // Timeout
+ });
+};
+
+const checkTokenMiddleware = (req, res) => {
+ logger.info('Checking token');
+ const accessToken = req.get('Authorization');
+
+ app.session.getSession(accessToken)
+ .then((json) => {
+ logger.info('Valid token');
+ res.setHeader('Content-Type', 'application/json');
+ res.send(JSON.stringify({
+ username: json.username,
+ role: json.role,
+ accessToken,
+ })).end();
+ })
+ .catch(() => {
+ logger.info('Invalid token');
+ res.sendStatus(401).end();
+ });
+};
+
+const logoutMiddleware = (req, res) => {
+ logger.info('Logging user out');
+ const accessToken = req.get('Authorization');
+
+ app.session.killSession(accessToken)
+ .then(() => {
+ logger.info('Successful user log out');
+ res.sendStatus(200).end();
+ })
+ .catch((err) => {
+ logger.info(`Unable to log user out: ${err}`);
+ res.sendStatus(401).end();
+ });
+};
+
+router.post('/auth/login', [loginMiddleware, createSessionMiddleware]);
+router.post('/auth/checkToken', [checkTokenMiddleware]);
+router.post('/auth/logout', [logoutMiddleware]);
+
+module.exports = router;
diff --git a/server/routes/staticFiles.js b/server/routes/staticFiles.js
new file mode 100644
index 0000000..bf625c6
--- /dev/null
+++ b/server/routes/staticFiles.js
@@ -0,0 +1,14 @@
+'use strict';
+
+const app = require('../index');
+const express = require('express');
+const path = require('path');
+
+const router = express.Router();
+
+// Always return the main index.html, so react-router renders the route in the client
+router.get('*', app.cache(), (req, res) => {
+ res.sendFile(path.resolve(__dirname, '../..', 'build', 'index.html'));
+});
+
+module.exports = router;
diff --git a/server/sessions/JsonSession.js b/server/sessions/JsonSession.js
new file mode 100644
index 0000000..5244573
--- /dev/null
+++ b/server/sessions/JsonSession.js
@@ -0,0 +1,79 @@
+'use strict';
+
+const uuidv4 = require('uuid/v4');
+const logger = require('../logger')(module);
+const config = require('../config/sessions');
+
+class JsonSession {
+ constructor() {
+ this.name = 'json';
+ this.sessionExpireHours = config.SESSION_EXPIRE_HOURS;
+ this.session = {};
+ }
+
+ createSession(username, remoteAddress, role, apiKey) {
+ logger.debug('Creating new JSON session');
+
+ return new Promise((resolve, reject) => {
+ const accessToken = uuidv4();
+ try {
+ const sessionExpire = new Date();
+ sessionExpire.setHours(sessionExpire.getHours() + this.sessionExpireHours);
+ this.session[accessToken] = {
+ username,
+ role,
+ apiKey,
+ remoteAddress,
+ sessionExpire,
+ };
+ logger.debug('Create JSON session was successful');
+ resolve({ accessToken });
+ } catch (error) {
+ logger.error(`Unable to create JSON session: ${error}`);
+ reject({ error });
+ }
+ });
+ }
+
+ getSession(accessToken) {
+ logger.debug('Getting JSON session');
+
+ return new Promise((resolve, reject) => {
+ try {
+ const userSession = this.session[accessToken];
+ const username = userSession.username;
+ const role = userSession.role;
+ const apiKey = userSession.apiKey;
+ const sessionExpire = userSession.sessionExpire;
+ if (new Date() > sessionExpire) {
+ logger.debug('JSON session has timed out');
+ delete this.userSession[accessToken];
+ reject({ error: 'JSON session has timed out' });
+ } else {
+ logger.debug('Get JSON session was successful');
+ resolve({ username, accessToken, role, apiKey });
+ }
+ } catch (error) {
+ logger.error(`Unable to get JSON session: ${error}`);
+ reject({ error });
+ }
+ });
+ }
+
+ killSession(accessToken) {
+ logger.debug('Killing JSON session');
+
+ return new Promise((resolve, reject) => {
+ try {
+ delete this.session[accessToken];
+ logger.debug('JSON session was successfully killed');
+ resolve();
+ } catch (error) {
+ logger.error(`Unable to kill JSON session: ${error}`);
+ reject({ error });
+ }
+ });
+ }
+}
+
+module.exports = JsonSession;
diff --git a/src/utils/apiInfo.js b/src/utils/apiInfo.js
index 8b6c161..d53f072 100644
--- a/src/utils/apiInfo.js
+++ b/src/utils/apiInfo.js
@@ -2,7 +2,7 @@
import config from '../config/api-urls';
-const { AUTH_URL, API_URL } = config;
+const { AUTH_URL, REROUTE_URL, API_VERSION } = config;
/**
* API lib for getting info (version/last updated etc.)
@@ -14,10 +14,11 @@ const apiInfo = {
* @param {Function} callback Called with returned data.
*/
getUiInfo(callback: (success: boolean, data: {}) => void) {
- fetch(`${AUTH_URL}/info`, {
+ fetch(`${AUTH_URL}/api/info`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
+ 'Authorization': sessionStorage.accessToken,
},
}).then((response) => {
if (response.status === 200) {
@@ -37,8 +38,16 @@ const apiInfo = {
* @param {Function} callback Called with returned data.
*/
getApiInfo(callback: (success: boolean, data: {}) => void) {
- fetch(`${API_URL}/version`, {
- method: 'GET',
+ fetch(`${REROUTE_URL}`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': sessionStorage.getItem('accessToken'),
+ },
+ body: JSON.stringify({
+ method: 'GET',
+ endpoint: 'version',
+ }),
}).then((response) => {
if (response.status === 200) {
return response.json().then((json) => {
diff --git a/src/utils/auth.js b/src/utils/auth.js
index 63a7f63..83ca1ef 100644
--- a/src/utils/auth.js
+++ b/src/utils/auth.js
@@ -20,7 +20,7 @@ const auth = {
// routes.js before this method is called
// POST to the backend with username/password
- fetch(`${AUTH_URL}/login`, {
+ fetch(`${AUTH_URL}/auth/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -48,12 +48,12 @@ const auth = {
});
},
checkToken(accessToken: string, callback: (success: boolean, data: ?{}) => void) {
- fetch(`${AUTH_URL}/checkToken`, {
+ fetch(`${AUTH_URL}/auth/checkToken`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
+ 'Authorization': sessionStorage.accessToken,
},
- body: JSON.stringify({ accessToken }),
}).then((response) => {
if (response.status === 200) {
return response.json().then((json) => {
@@ -75,12 +75,12 @@ const auth = {
*/
logout(accessToken: string, callback: (success: boolean) => void) {
// const token: string = sessionStorage.token;
- fetch(`${AUTH_URL}/logout`, {
+ fetch(`${AUTH_URL}/auth/logout`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
+ 'Authorization': sessionStorage.accessToken,
},
- body: JSON.stringify({ accessToken }),
}).then(() => {
// Whatever the response, log the user out.
sessionStorage.clear();
diff --git a/test/component-tests.js b/test/component-tests.js
deleted file mode 100644
index 987a284..0000000
--- a/test/component-tests.js
+++ /dev/null
@@ -1,17 +0,0 @@
-import Jasmine from 'jasmine'
-
-var jasmine = new Jasmine()
-
-// Load all files in the /spec dir ending in spec.js
-jasmine.loadConfig({
- "spec_dir": "test/component-tests",
- "spec_files": [
- "*.js"
- ],
- "helpers": [
- "../node_modules/jasmine-es6/lib/install.js",
- "helpers/**/*.js"
- ]
-});
-
-jasmine.execute()
diff --git a/test/component-tests/LoginErrorMessage.test.js b/test/component-tests/LoginErrorMessage.test.js
deleted file mode 100644
index 7e44f4e..0000000
--- a/test/component-tests/LoginErrorMessage.test.js
+++ /dev/null
@@ -1,28 +0,0 @@
-import 'jsdom-global/register';
-import React from 'react';
-import { expect } from 'chai';
-import { mount, shallow } from 'enzyme';
-import jasmineEnzyme from 'jasmine-enzyme';
-import { connect } from 'react-redux';
-import { shallowWithStore } from 'enzyme-redux';
-import { createMockStore } from 'redux-test-utils';
-import configureStore from 'redux-mock-store';
-import LoginErrorMessage from '../../src/components/LoginErrorMessage';
-
-const mockStore = configureStore();
-
-describe('