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

EUI-2976: ExUI strategic solution #276

Open
wants to merge 66 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 64 commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
b0a3edb
EUI-2976
paul-graham May 20, 2021
e294b18
Update server.js
paul-graham May 20, 2021
d3a0636
Update server.js
paul-graham May 20, 2021
36714d3
Update cors.js
paul-graham May 21, 2021
f190a45
Refactor
paul-graham May 21, 2021
c72f4bb
Redis instantiator
paul-graham May 24, 2021
10b3f97
Redis persistence
paul-graham May 24, 2021
329a8b7
Track what each socket is doing
paul-graham May 24, 2021
93ae2ae
Update activity-service.js
paul-graham May 24, 2021
f479ba1
Update package.json
paul-graham May 24, 2021
9396497
Update activity-service.js
paul-graham May 25, 2021
38f84ff
Update index.js
paul-graham May 25, 2021
4bcbf05
lint
paul-graham May 25, 2021
8d853af
Update instantiator.js
paul-graham May 25, 2021
569d85b
Refactoring
paul-graham May 25, 2021
7cd2c17
Update activity-service.js
paul-graham May 25, 2021
2e4e5bc
Tests
paul-graham May 25, 2021
8c30b43
Unit tests
paul-graham May 26, 2021
46dde6a
Unit tests
paul-graham May 26, 2021
cbc6c22
Key space and tests
paul-graham May 26, 2021
b2bf897
Update keys.js
paul-graham May 27, 2021
9a0a6f2
Unit tests
paul-graham May 27, 2021
b30efab
Fixed the subscriptions
paul-graham May 27, 2021
985bdc3
Unit tests
paul-graham May 27, 2021
caee0ae
Redis subscriber
paul-graham May 28, 2021
7f8a678
Unit tests
paul-graham May 28, 2021
2762b67
moment
paul-graham May 28, 2021
3288a68
Fixed underscore CVE issue
paul-graham May 28, 2021
a4000c9
Refactor for testing
paul-graham May 28, 2021
23abf9d
Create ttl-score-generator.spec.js
paul-graham May 28, 2021
9f5a85f
Update store-cleanup-job.js
paul-graham May 28, 2021
753d42f
Update store-cleanup-job.js
paul-graham May 28, 2021
52d0395
Update server.js
paul-graham May 28, 2021
af4a7d8
CVE issue
paul-graham Jun 9, 2021
a8a177e
Socket TTLs
paul-graham Jun 10, 2021
5558216
console.logs
paul-graham Jun 10, 2021
8b5881f
Update custom-environment-variables.yaml
paul-graham Jun 14, 2021
d37a5ee
No need for 'register' event
paul-graham Jun 15, 2021
67acec7
Update activity-service.js
paul-graham Jul 15, 2021
166cdfe
Update index.js
paul-graham Jul 15, 2021
3b20e7e
Update index.spec.js
paul-graham Jul 15, 2021
b2e40f1
Version
paul-graham Jul 19, 2021
745a45f
Update values.preview.template.yaml
paul-graham Jul 19, 2021
19b59f0
Update values.preview.template.yaml
paul-graham Jul 19, 2021
32c725d
User name for logging
paul-graham Jul 28, 2021
e14501f
Merge branch 'master' into EUI-2976-exui-strategic
paulhowes-HMCTS Jan 12, 2022
15350c3
Update packages
paulhowes-HMCTS Jan 14, 2022
4bf8c81
Yarn audit
paulhowes-HMCTS Jan 14, 2022
f48e972
Known issues temporary workaround
paulhowes-HMCTS Jan 25, 2022
ec39d6b
Merge branch 'master' into EUI-2976-exui-strategic
paulhowes-HMCTS Jan 25, 2022
ebecf5c
Merge branch 'master' into EUI-2976-exui-strategic
phillip-whitaker-hmcts Oct 7, 2022
0da814d
CVE test
phillip-whitaker-hmcts Oct 7, 2022
bf49b0c
Updated CVE audit known issues file
phillip-whitaker-hmcts Oct 7, 2022
2236909
Merge branch 'master' into EUI-2976-exui-strategic
phillip-whitaker-hmcts Oct 24, 2022
7fc3372
EUI-2976: Merge master into branch and fix conflicts
LucaDelBuonoHMCTS Mar 10, 2023
799da22
EUI-2976: Fix linting
LucaDelBuonoHMCTS Mar 10, 2023
61ba69f
EUI-2976: Force rebuild
LucaDelBuonoHMCTS Mar 14, 2023
b4f0845
Resolve preview issue
danlysiak Mar 15, 2023
f24fc6a
Merge branch 'master' into EUI-2976-exui-strategic
danlysiak Mar 20, 2023
2c4f094
EUI-2976: Whitelist everything
LucaDelBuonoHMCTS Mar 20, 2023
a9b29d4
Merge branch 'EUI-2976-exui-strategic' of github.com:hmcts/ccd-case-a…
LucaDelBuonoHMCTS Mar 20, 2023
685355f
EUI-2976: Whitelist everything
LucaDelBuonoHMCTS Mar 20, 2023
8ee832d
EUI-2976: Whitelist everything
LucaDelBuonoHMCTS Mar 20, 2023
ea379e7
EUI-2976: Update node-fetch
LucaDelBuonoHMCTS Mar 21, 2023
0a79c13
EUI-2976: Reduce ttl to 30 secs for testing
LucaDelBuonoHMCTS Mar 30, 2023
3f19129
Merge branch 'master' into EUI-2976-exui-strategic
LucaDelBuonoHMCTS Apr 26, 2023
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
13 changes: 13 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module.exports = {
"extends": "airbnb-base",
LucaDelBuonoHMCTS marked this conversation as resolved.
Show resolved Hide resolved
"env": {
"mocha": true,
"jasmine": true
},
"rules": {
"comma-dangle": 0,
"arrow-body-style": 0,
"no-param-reassign": [ 2, { props: false } ],
"linebreak-style": [ "error", process.platform === 'win32' ? 'windows' : 'unix' ]
}
}
4 changes: 0 additions & 4 deletions .eslintrc.yml

This file was deleted.

4 changes: 4 additions & 0 deletions RELEASE-NOTES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
## RELEASE NOTES

### Version 0.1.0-socket-alpha
**EUI-2976** Socket-based Activity Tracking.
7 changes: 3 additions & 4 deletions app.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
const healthcheck = require('@hmcts/nodejs-healthcheck');
const express = require('express');
const logger = require('morgan');
const bodyParser = require('body-parser');
const config = require('config');
const debug = require('debug')('ccd-case-activity-api:app');
const enableAppInsights = require('./app/app-insights/app-insights');
Expand Down Expand Up @@ -43,9 +42,9 @@ if (config.util.getEnv('NODE_ENV') === 'test') {
debug(`starting application with environment: ${config.util.getEnv('NODE_ENV')}`);

app.use(corsHandler);
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.text());
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(express.text());
app.use(authCheckerUserOnlyFilter);

app.use('/', activity);
Expand Down
1 change: 0 additions & 1 deletion app/health.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,4 @@ const activityHealth = healthcheck.configure({
.catch(() => healthcheck.down())),
},
});

module.exports = activityHealth;
21 changes: 12 additions & 9 deletions app/job/store-cleanup-job.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
const cron = require('node-cron');
const debug = require('debug')('ccd-case-activity-api:store-cleanup-job');
const moment = require('moment');
const config = require('config');
const redis = require('../redis/redis-client');

const { logPipelineFailures } = redis;
const now = () => moment().valueOf();
const REDIS_ACTIVITY_KEY_PREFIX = config.get('redis.keyPrefix');

const scanExistingCasesKeys = (f) => {
const scanExistingCasesKeys = (f, prefix) => {
const stream = redis.scanStream({
// only returns keys following the pattern
match: `${REDIS_ACTIVITY_KEY_PREFIX}case:*`,
match: `${REDIS_ACTIVITY_KEY_PREFIX}${prefix}:*`,
// returns approximately 100 elements per call
count: 100,
});
Expand All @@ -28,18 +26,17 @@ const scanExistingCasesKeys = (f) => {
});
};

const getCasesWithActivities = (f) => scanExistingCasesKeys(f);
const getCasesWithActivities = (f, prefix) => scanExistingCasesKeys(f, prefix);

const cleanupActivitiesCommand = (key) => ['zremrangebyscore', key, '-inf', now()];
const cleanupActivitiesCommand = (key) => ['zremrangebyscore', key, '-inf', Date.now()];

const pipeline = (cases) => {
const commands = cases.map((caseKey) => cleanupActivitiesCommand(caseKey));
debug(`created cleanup pipeline: ${commands}`);
return redis.pipeline(commands);
};

const storeCleanup = () => {
debug('store cleanup starting...');
const cleanCasesWithPrefix = (prefix) => {
getCasesWithActivities((cases) => {
// scan returns the prefixed keys. Remove them since the redis client will add it back
const casesWithoutPrefix = cases.map((k) => k.replace(REDIS_ACTIVITY_KEY_PREFIX, ''));
Expand All @@ -50,7 +47,13 @@ const storeCleanup = () => {
.catch((err) => {
debug('Error in getCasesWithActivities', err.message);
});
});
}, prefix);
};

const storeCleanup = () => {
debug('store cleanup starting...');
cleanCasesWithPrefix('case'); // Cases via RESTful interface.
cleanCasesWithPrefix('c'); // Cases via socket interface.
};

exports.start = (crontab) => {
Expand Down
46 changes: 46 additions & 0 deletions app/redis/instantiator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
const config = require('config');
const Redis = require('ioredis');

const ERROR = 0;
const RESULT = 1;
const ENV = config.util.getEnv('NODE_ENV');

module.exports = (debug) => {
const redis = new Redis({
port: config.get('redis.port'),
host: config.get('redis.host'),
password: config.get('secrets.ccd.activity-redis-password'),
tls: config.get('redis.ssl'),
keyPrefix: config.get('redis.keyPrefix'),
// log unhandled redis errors
showFriendlyErrorStack: ENV === 'test' || ENV === 'dev',
});

/* redis pipeline returns a reply of the form [[op1error, op1result], [op2error, op2result], ...].
error is null in case of success */
redis.logPipelineFailures = (plOutcome, message) => {
if (Array.isArray(plOutcome)) {
const operationsFailureOutcome = plOutcome.map((operationOutcome) => operationOutcome[ERROR]);
const failures = operationsFailureOutcome.filter((element) => element !== null);
failures.forEach((f) => debug(`${message}: ${f}`));
}
return plOutcome;
};

redis.extractPipelineResults = (pipelineOutcome) => {
const results = pipelineOutcome.map((operationOutcome) => operationOutcome[RESULT]);
debug(`pipeline results: ${results}`);
return results;
};

redis
.on('error', (err) => {
// eslint-disable-next-line no-console
debug(`Redis error: ${err.message}`);
}).on('connect', () => {
// eslint-disable-next-line no-console
debug('connected to Redis');
});

return redis;
};
46 changes: 1 addition & 45 deletions app/redis/redis-client.js
Original file line number Diff line number Diff line change
@@ -1,47 +1,3 @@
const config = require('config');
const debug = require('debug')('ccd-case-activity-api:redis-client');
const Redis = require('ioredis');

const ERROR = 0;
const RESULT = 1;
const ENV = config.util.getEnv('NODE_ENV');

const redis = new Redis({
port: config.get('redis.port'),
host: config.get('redis.host'),
password: config.get('secrets.ccd.activity-redis-password'),
tls: config.get('redis.ssl'),
keyPrefix: config.get('redis.keyPrefix'),
// log unhandled redis errors
showFriendlyErrorStack: ENV === 'test' || ENV === 'dev',
});

/* redis pipeline returns a reply of the form [[op1error, op1result], [op2error, op2result], ...].
error is null in case of success */
redis.logPipelineFailures = (plOutcome, message) => {
if (Array.isArray(plOutcome)) {
const operationsFailureOutcome = plOutcome.map((operationOutcome) => operationOutcome[ERROR]);
const failures = operationsFailureOutcome.filter((element) => element !== null);
failures.forEach((f) => debug(`${message}: ${f}`));
} else {
debug(`${plOutcome} is not an Array...`);
}
return plOutcome;
};

redis.extractPipelineResults = (pipelineOutcome) => {
const results = pipelineOutcome.map((operationOutcome) => operationOutcome[RESULT]);
debug(`pipeline results: ${results}`);
return results;
};

redis
.on('error', (err) => {
// eslint-disable-next-line no-console
console.log(`Redis error: ${err.message}`);
}).on('connect', () => {
// eslint-disable-next-line no-console
console.log('connected to Redis');
});

module.exports = redis;
module.exports = require('./instantiator')(debug);
4 changes: 3 additions & 1 deletion app/routes/validate-request.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
const debug = require('debug')('ccd-case-activity-api:validate-request');

const validateRequest = (schema, value) => (req, res, next) => {
const { error } = schema.validate(value);
const valid = error == null;
Expand All @@ -6,7 +8,7 @@ const validateRequest = (schema, value) => (req, res, next) => {
} else {
const { details } = error;
const message = details.map((i) => i.message).join(',');
console.log('error', message);
debug(`error ${message}`);
res.status(400).json({ error: message });
}
};
Expand Down
3 changes: 2 additions & 1 deletion app/security/cors.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ const config = require('config');
const sanitize = require('../util/sanitize');

const createWhitelistValidator = (val) => {
const whitelist = config.get('security.cors_origin_whitelist').split(',');
const configValue = config.get('security.cors_origin_whitelist') || '';
const whitelist = configValue.split(',');
for (let i = 0; i < whitelist.length; i += 1) {
if (val === whitelist[i]) {
return true;
Expand Down
3 changes: 1 addition & 2 deletions app/service/activity-service.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
const moment = require('moment');
const debug = require('debug')('ccd-case-activity-api:activity-service');

module.exports = (config, redis, ttlScoreGenerator) => {
Expand Down Expand Up @@ -31,7 +30,7 @@ module.exports = (config, redis, ttlScoreGenerator) => {
const uniqueUserIds = [];
let caseViewers = [];
let caseEditors = [];
const now = moment.now();
const now = Date.now();
const getUserDetails = () => redis.pipeline(uniqueUserIds.map((userId) => ['get', `user:${userId}`])).exec();
const extractUniqueUserIds = (result) => {
result.forEach((item) => {
Expand Down
8 changes: 4 additions & 4 deletions app/service/ttl-score-generator.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
const config = require('config');
const moment = require('moment');
const debug = require('debug')('ccd-case-activity-api:score-generator');

exports.getScore = () => {
const now = moment();
const score = now.add(config.get('redis.activityTtlSec'), 'seconds').valueOf();
debug(`generated score out of current timestamp '${now.valueOf()}' plus ${config.get('redis.activityTtlSec')} sec`);
const now = Date.now();
const ttl = parseInt(config.get('redis.activityTtlSec'), 10) || 0;
const score = now + (ttl * 1000);
debug(`generated score out of current timestamp '${now}' plus ${ttl} sec`);
return score;
};
37 changes: 37 additions & 0 deletions app/socket/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
const config = require('config');
const IORouter = require('socket.io-router-middleware');
const SocketIO = require('socket.io');

const ActivityService = require('./service/activity-service');
const Handlers = require('./service/handlers');
const pubSub = require('./redis/pub-sub')();
const router = require('./router');

/**
* Sets up a series of routes for a "socket" endpoint, that
* leverages socket.io and will more than likely use long polling
* instead of websockets as the latter isn't supported by Azure
* Front Door.
*
* The behaviour is the same, though.
*
* TODO:
* * Some sort of auth / get the credentials when the user connects.
*/
module.exports = (server, redis) => {
const activityService = ActivityService(config, redis);
const socketServer = SocketIO(server, {
allowEIO3: true,
cors: {
origin: '*',
methods: ['GET', 'POST'],
credentials: true
},
});
const handlers = Handlers(activityService, socketServer);
const watcher = redis.duplicate();
pubSub.init(watcher, handlers.notify);
router.init(socketServer, new IORouter(), handlers);

return { socketServer, activityService, handlers };
};
23 changes: 23 additions & 0 deletions app/socket/redis/keys.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
const keys = {
prefixes: {
case: 'c',
socket: 's',
user: 'u'
},
case: {
view: (caseId) => keys.compile('case', caseId, 'viewers'),
edit: (caseId) => keys.compile('case', caseId, 'editors'),
base: (caseId) => keys.compile('case', caseId),
},
user: (userId) => keys.compile('user', userId),
socket: (socketId) => keys.compile('socket', socketId),
compile: (prefix, value, suffix) => {
const key = `${keys.prefixes[prefix]}:${value}`;
if (suffix) {
return `${key}:${suffix}`;
}
return key;
}
};

module.exports = keys;
15 changes: 15 additions & 0 deletions app/socket/redis/pub-sub.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const keys = require('./keys');

module.exports = () => {
return {
init: (watcher, caseNotifier) => {
if (watcher && typeof caseNotifier === 'function') {
watcher.psubscribe(`${keys.prefixes.case}:*`);
watcher.on('pmessage', (_, room) => {
const caseId = room.replace(`${keys.prefixes.case}:`, '');
caseNotifier(caseId);
});
}
}
};
};
Loading