diff --git a/.circleci/config.yml b/.circleci/config.yml index 40b32058..c2796b26 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -14,7 +14,7 @@ parameters: node-version: type: string - default: 20.15-browsers + default: 22.12-browsers executors: integration-tests: diff --git a/Dockerfile b/Dockerfile index 81dfc6dc..ab771ec5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:20.15-bookworm-slim as builder +FROM node:22.12-bookworm-slim as builder ARG BUILD_NUMBER ARG GIT_REF @@ -22,7 +22,7 @@ RUN CYPRESS_INSTALL_BINARY=0 npm ci --no-audit && \ RUN npm prune --production -FROM node:20.15-bookworm-slim +FROM node:22.12-bookworm-slim LABEL maintainer="HMPPS Digital Studio " # Cache breaking diff --git a/job/sendReminders.ts b/job/sendReminders.ts index 9f2518c4..8b65c3bc 100644 --- a/job/sendReminders.ts +++ b/job/sendReminders.ts @@ -29,7 +29,7 @@ const statementClient = new StatementsClient(db.query) const reportLogClient = new ReportLogClient() const incidentClient = new IncidentClient(db.query, db.inTransaction, reportLogClient) -const systemToken = systemTokenBuilder(new TokenStore(createRedisClient({ legacyMode: false }))) +const systemToken = systemTokenBuilder(new TokenStore(createRedisClient())) const emailResolver = new EmailResolver(token => new AuthClient(token), systemToken, statementClient) const notificationService = notificationServiceFactory(eventPublisher) diff --git a/package-lock.json b/package-lock.json index e196d945..e29aa7bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,7 +7,6 @@ "": { "name": "use-of-force", "version": "0.0.1", - "hasInstallScript": true, "license": "MIT", "dependencies": { "@hapi/joi": "^17.1.1", @@ -21,8 +20,8 @@ "bunyan-format": "^0.2.1", "compression": "^1.7.4", "connect-flash": "^0.1.1", - "connect-redis": "^6.1.3", - "cookie-parser": "^1.4.6", + "connect-redis": "^8.0.1", + "cookie-parser": "^1.4.7", "cookie-session": "^2.1.0", "csurf": "^1.11.0", "cypress-axe": "^1.5.0", @@ -59,7 +58,7 @@ "@types/hapi__joi": "^17.1.9", "@types/http-errors": "^2.0.1", "@types/jest": "^29.5.12", - "@types/node": "^18.16.12", + "@types/node": "^22.10.2", "@types/nunjucks": "^3.2.2", "@types/pg": "^8.6.6", "@types/superagent": "^4.1.17", @@ -94,7 +93,7 @@ "typescript": "^4.9.5" }, "engines": { - "node": "^20", + "node": "^22", "npm": "^10" } }, @@ -1935,12 +1934,12 @@ "dev": true }, "node_modules/@types/node": { - "version": "18.19.50", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.50.tgz", - "integrity": "sha512-xonK+NRrMBRtkL1hVCc3G+uXtjh1Al4opBLjqVmipe5ZAaBYWW6cNAiBVZ1BvmkBhep698rP3UM3aRAdSALuhg==", + "version": "22.10.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", + "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==", "devOptional": true, "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.20.0" } }, "node_modules/@types/normalize-package-data": { @@ -3748,11 +3747,14 @@ } }, "node_modules/connect-redis": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/connect-redis/-/connect-redis-6.1.3.tgz", - "integrity": "sha512-aaNluLlAn/3JPxRwdzw7lhvEoU6Enb+d83xnokUNhC9dktqBoawKWL+WuxinxvBLTz6q9vReTnUDnUslaz74aw==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/connect-redis/-/connect-redis-8.0.1.tgz", + "integrity": "sha512-7iOI214/r15ahvu0rqKCHhsgpMdOgyLwqlw/icSTnnAR75xFvMyfxAE+je4M87rZLjDlKzKcTc48XxQXYFsMgA==", "engines": { - "node": ">=12" + "node": ">=18" + }, + "peerDependencies": { + "express-session": ">=1" } }, "node_modules/console-control-strings": { @@ -3814,19 +3816,21 @@ "dev": true }, "node_modules/cookie": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", - "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/cookie-parser": { - "version": "1.4.6", - "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz", - "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==", + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", "dependencies": { - "cookie": "0.4.1", + "cookie": "0.7.2", "cookie-signature": "1.0.6" }, "engines": { @@ -3923,9 +3927,10 @@ } }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -5296,16 +5301,17 @@ } }, "node_modules/express": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", - "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.6.0", + "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", @@ -5319,7 +5325,7 @@ "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.10", + "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", @@ -5334,14 +5340,19 @@ }, "engines": { "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/express-session": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.0.tgz", - "integrity": "sha512-m93QLWr0ju+rOwApSsyso838LQwgfs44QtOP/WBiwtAgPIo/SAh1a5c6nn2BR6mFNZehTpqKDESzP+fRHVbxwQ==", + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.1.tgz", + "integrity": "sha512-a5mtTqEaZvBCL9A9aqkrtfz+3SMDhOVUnjafjo+s7A9Txkq+SVX2DLvSp1Zrv4uCXa3lMSK3viWnh9Gg07PBUA==", + "license": "MIT", "dependencies": { - "cookie": "0.6.0", + "cookie": "0.7.2", "cookie-signature": "1.0.7", "debug": "2.6.9", "depd": "~2.0.0", @@ -5354,14 +5365,6 @@ "node": ">= 0.8.0" } }, - "node_modules/express-session/node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/express-session/node_modules/cookie-signature": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", @@ -5400,9 +5403,10 @@ ] }, "node_modules/express/node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -5823,7 +5827,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "optional": true, "os": [ @@ -10330,9 +10333,10 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "node_modules/path-to-regexp": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", - "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" }, "node_modules/path-type": { "version": "4.0.0", @@ -12710,9 +12714,9 @@ "dev": true }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", "devOptional": true }, "node_modules/unique-filename": { diff --git a/package.json b/package.json index 5363c5bd..3b71c4dc 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "security_audit": "npx audit-ci --config audit-ci.json" }, "engines": { - "node": "^20", + "node": "^22", "npm": "^10" }, "jest": { @@ -108,8 +108,8 @@ "bunyan-format": "^0.2.1", "compression": "^1.7.4", "connect-flash": "^0.1.1", - "connect-redis": "^6.1.3", - "cookie-parser": "^1.4.6", + "connect-redis": "^8.0.1", + "cookie-parser": "^1.4.7", "cookie-session": "^2.1.0", "csurf": "^1.11.0", "cypress-axe": "^1.5.0", @@ -146,7 +146,7 @@ "@types/hapi__joi": "^17.1.9", "@types/http-errors": "^2.0.1", "@types/jest": "^29.5.12", - "@types/node": "^18.16.12", + "@types/node": "^22.10.2", "@types/nunjucks": "^3.2.2", "@types/pg": "^8.6.6", "@types/superagent": "^4.1.17", diff --git a/server/app.ts b/server/app.ts index ef992068..14bf20c6 100755 --- a/server/app.ts +++ b/server/app.ts @@ -9,9 +9,10 @@ import compression from 'compression' import passport from 'passport' import crypto from 'crypto' import createError from 'http-errors' -import session from 'express-session' -import ConnectRedis from 'connect-redis' -import { createRedisClient } from './data/redisClient' +import session, { MemoryStore, Store } from 'express-session' +import { RedisStore } from 'connect-redis' +import { redisClient } from './data/redisClient' + import RequestLogger from './middleware/requestLogger' import createRouter from './routes' @@ -124,13 +125,17 @@ export default function createApp(services: Services): Express { next() }) - const RedisStore = ConnectRedis(session) - const client = createRedisClient({ legacyMode: true }) - client.connect() - + let store: Store + if (config.redis.enabled) { + const client = redisClient + client.connect().catch((err: Error) => logger.error(`Error connecting to Redis`, err)) + store = new RedisStore({ client }) + } else { + store = new MemoryStore() + } app.use( session({ - store: new RedisStore({ client }), + store, cookie: { secure: config.https, sameSite: 'lax', maxAge: config.session.expiryMinutes * 60 * 1000 }, secret: config.session.secret, resave: false, // redis implements touch so shouldn't need this diff --git a/server/data/index.ts b/server/data/index.ts index 2376060e..c6180939 100755 --- a/server/data/index.ts +++ b/server/data/index.ts @@ -41,7 +41,7 @@ export const dataAccess = { telemetryClient, draftReportClient, reportLogClient, - systemToken: systemTokenBuilder(new TokenStore(createRedisClient({ legacyMode: false }))), + systemToken: systemTokenBuilder(new TokenStore(createRedisClient())), authClientBuilder: ((token: string) => new AuthClient(token)) as RestClientBuilder, prisonClientBuilder: restClientBuilder('prisonApi', config.apis.prison, PrisonClient), locationClientBuilder: restClientBuilder('locationApi', config.apis.location, LocationClient), diff --git a/server/data/prisonClient.ts b/server/data/prisonClient.ts index de1e0b38..1dd38b16 100644 --- a/server/data/prisonClient.ts +++ b/server/data/prisonClient.ts @@ -9,7 +9,6 @@ import type { PrisonerDetail, CaseLoad, Prison, - PrisonLocation, } from './prisonClientTypes' export default class PrisonClient { diff --git a/server/data/redisClient.ts b/server/data/redisClient.ts index 5660ce52..5f6ce2bc 100644 --- a/server/data/redisClient.ts +++ b/server/data/redisClient.ts @@ -9,11 +9,10 @@ const url = ? `rediss://${config.redis.host}:${config.redis.port}` : `redis://${config.redis.host}:${config.redis.port}` -export const createRedisClient = ({ legacyMode }: { legacyMode: boolean }): RedisClient => { +export const createRedisClient = (): RedisClient => { const client = createClient({ url, password: config.redis.password, - legacyMode, socket: { reconnectStrategy: (attempts: number) => { // Exponential back off: 20ms, 40ms, 80ms..., capped to retry every 30 seconds @@ -27,3 +26,5 @@ export const createRedisClient = ({ legacyMode }: { legacyMode: boolean }): Redi client.on('error', (e: Error) => logger.error('Redis client error', e)) return client } + +export const redisClient = config.redis.enabled ? createRedisClient() : null diff --git a/server/services/reportSummary.ts b/server/services/reportSummary.ts index 409c6fb9..c531f2ab 100644 --- a/server/services/reportSummary.ts +++ b/server/services/reportSummary.ts @@ -155,7 +155,9 @@ const getRestraintPositions = positions => { const toParentChild = postions => { const positionObjects = postions.map(p => findEnum(ControlAndRestraintPosition, p)) + // eslint-disable-next-line @typescript-eslint/no-explicit-any const parents: any[] = [] + // eslint-disable-next-line @typescript-eslint/no-explicit-any const children: any[] = [] positionObjects.forEach(obj => { if (obj.parent == null) { @@ -165,6 +167,7 @@ const toParentChild = postions => { } }) const parentChild: string[] = [] + // eslint-disable-next-line func-names parents.forEach(function (p) { const thesechildren = children .filter(pos => pos.parent === p.value)