diff --git a/.env-example b/.env-example new file mode 100644 index 0000000..5f4827f --- /dev/null +++ b/.env-example @@ -0,0 +1,10 @@ +# connection details for this service +FRONTEND_URL=http://localhost:3000 +BACKEND_URL=http://localhost:3001 + +SESSION_SECRET= +JWT_SECRET= + +# rate limiting +RATE_LIMIT_WINDOW_MS=15*60*1000 +RATE_LIMIT_MAX_REQ=100 diff --git a/.eslintrc b/.eslintrc index 6874c4a..c811b02 100644 --- a/.eslintrc +++ b/.eslintrc @@ -21,7 +21,8 @@ "rules": { "prettier/prettier": 2, "no-console": 0, - "no-process-env": 0 + "no-process-env": 0, + "line-comment-position": 0 }, "globals": { "NodeJS": true diff --git a/README.md b/README.md index 4f3de26..ace00fe 100644 --- a/README.md +++ b/README.md @@ -8,18 +8,9 @@ - Node 20+ - An Instance of the StatsWales backend Service -## To get going +## Configuration -You'll need to define the following environment variables either in to the environment or in to a `.env` file: - -```env -BACKEND_SERVER -BACKEND_PROTOCOL -BACKEND_PORT -SESSION_SECRET -GOOGLE_CLIENT_ID -GOOGLE_CLIENT_SECRET -``` +Copy the [.env-example](.env-example) file to `.env` and provide the missing values. To run the app should be as simple as: diff --git a/package-lock.json b/package-lock.json index ec05565..cfe2a7f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,9 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "@types/express-session": "^1.18.0", - "@types/passport": "^1.0.16", - "@types/passport-local": "^1.0.38", + "cookie-parser": "^1.4.6", "dotenv": "^16.4.4", "ejs": "^3.1.9", "express": "^4.19.2", @@ -22,25 +20,32 @@ "i18next": "^23.10.1", "i18next-fs-backend": "^2.3.1", "i18next-http-middleware": "^3.5.0", + "jsonwebtoken": "^9.0.2", + "lodash": "^4.17.21", "multer": "^1.4.5-lts.1", "passport": "^0.7.0", - "passport-google-oauth20": "^2.0.0", - "passport-local": "^1.0.0", - "pino": "^8.19.0", - "ts-node-dev": "^2.0.0" + "pino": "^8.21.0", + "pino-http": "^10.2.0", + "ts-node-dev": "^2.0.0", + "uuid": "^10.0.0" }, "devDependencies": { "@shopify/eslint-plugin": "^44.0.0", "@types/better-sqlite3": "^7.6.9", + "@types/cookie-parser": "^1.4.7", "@types/express": "^4.17.21", + "@types/express-session": "^1.18.0", "@types/fs-extra": "^9.0.13", "@types/jest": "^29.5.12", + "@types/jsonwebtoken": "^9.0.6", + "@types/lodash": "^4.17.7", "@types/multer": "^1.4.11", "@types/node": "^20.11.27", - "@types/passport-google-oauth20": "^2.0.16", + "@types/passport": "^1.0.16", "@types/pino": "^7.0.5", "@types/shelljs": "^0.8.15", "@types/supertest": "^6.0.2", + "@types/uuid": "^10.0.0", "@typescript-eslint/eslint-plugin": "^7.0.1", "@typescript-eslint/parser": "^7.0.1", "eslint": "^8.56.0", @@ -53,6 +58,7 @@ "msw": "^2.3.1", "nodemon": "^3.1.0", "npm-run-all": "^4.1.5", + "pino-colada": "^2.2.2", "prettier": "^3.2.5", "rimraf": "^3.0.2", "shelljs": "^0.8.5", @@ -2246,6 +2252,7 @@ "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, "dependencies": { "@types/connect": "*", "@types/node": "*" @@ -2255,6 +2262,7 @@ "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, "dependencies": { "@types/node": "*" } @@ -2265,6 +2273,16 @@ "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", "dev": true }, + "node_modules/@types/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-Fvuyi354Z+uayxzIGCwYTayFKocfV7TuDYZClCdIP9ckhvAu/ixDtCB6qx2TT0FKjPLf1f3P/J1rgf6lPs64mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/cookiejar": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", @@ -2275,6 +2293,7 @@ "version": "4.17.21", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "dev": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", @@ -2286,6 +2305,7 @@ "version": "4.17.43", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.43.tgz", "integrity": "sha512-oaYtiBirUOPQGSWNGPWnzyAFJ0BP3cwvN4oWZQY+zUBwpVIGsKUkpBpSztp74drYcjavs7SKFZ4DX1V2QeN8rg==", + "dev": true, "dependencies": { "@types/node": "*", "@types/qs": "*", @@ -2297,6 +2317,8 @@ "version": "1.18.0", "resolved": "https://registry.npmjs.org/@types/express-session/-/express-session-1.18.0.tgz", "integrity": "sha512-27JdDRgor6PoYlURY+Y5kCakqp5ulC0kmf7y+QwaY+hv9jEFuQOThgkjyA53RP3jmKuBsH5GR6qEfFmvb8mwOA==", + "dev": true, + "license": "MIT", "dependencies": { "@types/express": "*" } @@ -2332,7 +2354,8 @@ "node_modules/@types/http-errors": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", - "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==" + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", @@ -2380,6 +2403,23 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.6.tgz", + "integrity": "sha512-/5hndP5dCjloafCXns6SZyESp3Ldq7YjH3zwzwczYnjxIT0Fqzk5ROSYVGfFyczIue7IUEj8hkvLbPoLQ18vQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/lodash": { + "version": "4.17.7", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz", + "integrity": "sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/methods": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", @@ -2389,7 +2429,8 @@ "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true }, "node_modules/@types/minimatch": { "version": "5.1.2", @@ -2423,62 +2464,14 @@ "undici-types": "~5.26.4" } }, - "node_modules/@types/oauth": { - "version": "0.9.5", - "resolved": "https://registry.npmjs.org/@types/oauth/-/oauth-0.9.5.tgz", - "integrity": "sha512-+oQ3C2Zx6ambINOcdIARF5Z3Tu3x//HipE889/fqo3sgpQZbe9c6ExdQFtN6qlhpR7p83lTZfPJt0tCAW29dog==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/passport": { "version": "1.0.16", "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.16.tgz", "integrity": "sha512-FD0qD5hbPWQzaM0wHUnJ/T0BBCJBxCeemtnCwc/ThhTg3x9jfrAcRUmj5Dopza+MfFS9acTe3wk7rcVnRIp/0A==", - "dependencies": { - "@types/express": "*" - } - }, - "node_modules/@types/passport-google-oauth20": { - "version": "2.0.16", - "resolved": "https://registry.npmjs.org/@types/passport-google-oauth20/-/passport-google-oauth20-2.0.16.tgz", - "integrity": "sha512-ayXK2CJ7uVieqhYOc6k/pIr5pcQxOLB6kBev+QUGS7oEZeTgIs1odDobXRqgfBPvXzl0wXCQHftV5220czZCPA==", - "dev": true, - "dependencies": { - "@types/express": "*", - "@types/passport": "*", - "@types/passport-oauth2": "*" - } - }, - "node_modules/@types/passport-local": { - "version": "1.0.38", - "resolved": "https://registry.npmjs.org/@types/passport-local/-/passport-local-1.0.38.tgz", - "integrity": "sha512-nsrW4A963lYE7lNTv9cr5WmiUD1ibYJvWrpE13oxApFsRt77b0RdtZvKbCdNIY4v/QZ6TRQWaDDEwV1kCTmcXg==", - "dependencies": { - "@types/express": "*", - "@types/passport": "*", - "@types/passport-strategy": "*" - } - }, - "node_modules/@types/passport-oauth2": { - "version": "1.4.17", - "resolved": "https://registry.npmjs.org/@types/passport-oauth2/-/passport-oauth2-1.4.17.tgz", - "integrity": "sha512-ODiAHvso6JcWJ6ZkHHroVp05EHGhqQN533PtFNBkg8Fy5mERDqsr030AX81M0D69ZcaMvhF92SRckEk2B0HYYg==", "dev": true, + "license": "MIT", "dependencies": { - "@types/express": "*", - "@types/oauth": "*", - "@types/passport": "*" - } - }, - "node_modules/@types/passport-strategy": { - "version": "0.2.38", - "resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.38.tgz", - "integrity": "sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==", - "dependencies": { - "@types/express": "*", - "@types/passport": "*" + "@types/express": "*" } }, "node_modules/@types/pino": { @@ -2494,12 +2487,14 @@ "node_modules/@types/qs": { "version": "6.9.14", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.14.tgz", - "integrity": "sha512-5khscbd3SwWMhFqylJBLQ0zIu7c1K6Vz0uBIt915BI3zV0q1nfjRQD3RqSBcPaO6PHEF4ov/t9y89fSiyThlPA==" + "integrity": "sha512-5khscbd3SwWMhFqylJBLQ0zIu7c1K6Vz0uBIt915BI3zV0q1nfjRQD3RqSBcPaO6PHEF4ov/t9y89fSiyThlPA==", + "dev": true }, "node_modules/@types/range-parser": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==" + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true }, "node_modules/@types/semver": { "version": "7.5.8", @@ -2511,6 +2506,7 @@ "version": "0.17.4", "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, "dependencies": { "@types/mime": "^1", "@types/node": "*" @@ -2520,6 +2516,7 @@ "version": "1.15.5", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.5.tgz", "integrity": "sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==", + "dev": true, "dependencies": { "@types/http-errors": "*", "@types/mime": "*", @@ -2579,6 +2576,13 @@ "@types/superagent": "^8.1.0" } }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/wrap-ansi": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz", @@ -2601,22 +2605,21 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.5.0.tgz", - "integrity": "sha512-HpqNTH8Du34nLxbKgVMGljZMG0rJd2O9ecvr2QLYp+7512ty1j42KnsFwspPXg1Vh8an9YImf6CokUBltisZFQ==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz", + "integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==", "dev": true, + "license": "MIT", "dependencies": { - "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "7.5.0", - "@typescript-eslint/type-utils": "7.5.0", - "@typescript-eslint/utils": "7.5.0", - "@typescript-eslint/visitor-keys": "7.5.0", - "debug": "^4.3.4", + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/type-utils": "7.18.0", + "@typescript-eslint/utils": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", "graphemer": "^1.4.0", - "ignore": "^5.2.4", + "ignore": "^5.3.1", "natural-compare": "^1.4.0", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" + "ts-api-utils": "^1.3.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -2635,49 +2638,17 @@ } } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/@typescript-eslint/parser": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.5.0.tgz", - "integrity": "sha512-cj+XGhNujfD2/wzR1tabNsidnYRaFfEkcULdcIyVBYcXjBvBKOes+mpMBP7hMpOyk+gBcfXsrg4NBGAStQyxjQ==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz", + "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/scope-manager": "7.5.0", - "@typescript-eslint/types": "7.5.0", - "@typescript-eslint/typescript-estree": "7.5.0", - "@typescript-eslint/visitor-keys": "7.5.0", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", "debug": "^4.3.4" }, "engines": { @@ -2697,13 +2668,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.5.0.tgz", - "integrity": "sha512-Z1r7uJY0MDeUlql9XJ6kRVgk/sP11sr3HKXn268HZyqL7i4cEfrdFuSSY/0tUqT37l5zT0tJOsuDP16kio85iA==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz", + "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.5.0", - "@typescript-eslint/visitor-keys": "7.5.0" + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -2714,15 +2686,16 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.5.0.tgz", - "integrity": "sha512-A021Rj33+G8mx2Dqh0nMO9GyjjIBK3MqgVgZ2qlKf6CJy51wY/lkkFqq3TqqnH34XyAHUkq27IjlUkWlQRpLHw==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz", + "integrity": "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "7.5.0", - "@typescript-eslint/utils": "7.5.0", + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/utils": "7.18.0", "debug": "^4.3.4", - "ts-api-utils": "^1.0.1" + "ts-api-utils": "^1.3.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -2741,10 +2714,11 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.5.0.tgz", - "integrity": "sha512-tv5B4IHeAdhR7uS4+bf8Ov3k793VEVHd45viRRkehIUZxm0WF82VPiLgHzA/Xl4TGPg1ZD49vfxBKFPecD5/mg==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz", + "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", "dev": true, + "license": "MIT", "engines": { "node": "^18.18.0 || >=20.0.0" }, @@ -2754,19 +2728,20 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.5.0.tgz", - "integrity": "sha512-YklQQfe0Rv2PZEueLTUffiQGKQneiIEKKnfIqPIOxgM9lKSZFCjT5Ad4VqRKj/U4+kQE3fa8YQpskViL7WjdPQ==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz", + "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/types": "7.5.0", - "@typescript-eslint/visitor-keys": "7.5.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", - "minimatch": "9.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -2781,26 +2756,28 @@ } } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, + "license": "ISC", "dependencies": { - "yallist": "^4.0.0" + "brace-expansion": "^2.0.1" }, "engines": { - "node": ">=10" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -2808,25 +2785,17 @@ "node": ">=10" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/@typescript-eslint/utils": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.5.0.tgz", - "integrity": "sha512-3vZl9u0R+/FLQcpy2EHyRGNqAS/ofJ3Ji8aebilfJe+fobK8+LbIFmrHciLVDxjDoONmufDcnVSF38KwMEOjzw==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz", + "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@types/json-schema": "^7.0.12", - "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "7.5.0", - "@typescript-eslint/types": "7.5.0", - "@typescript-eslint/typescript-estree": "7.5.0", - "semver": "^7.5.4" + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -2839,47 +2808,15 @@ "eslint": "^8.56.0" } }, - "node_modules/@typescript-eslint/utils/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.5.0.tgz", - "integrity": "sha512-mcuHM/QircmA6O7fy6nn2w/3ditQkj+SgtOc8DW3uQ10Yfj42amm2i+6F2K4YAOPNNTmE6iM1ynM6lrSwdendA==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz", + "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.5.0", - "eslint-visitor-keys": "^3.4.1" + "@typescript-eslint/types": "7.18.0", + "eslint-visitor-keys": "^3.4.3" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -2894,6 +2831,7 @@ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -3491,14 +3429,6 @@ } ] }, - "node_modules/base64url": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", - "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -3641,6 +3571,12 @@ "ieee754": "^1.2.1" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -3999,6 +3935,28 @@ "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==", + "license": "MIT", + "dependencies": { + "cookie": "0.4.1", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-parser/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==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", @@ -4385,6 +4343,15 @@ "xtend": "^4.0.0" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -5793,6 +5760,13 @@ "node": ">= 6" } }, + "node_modules/fast-json-parse": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/fast-json-parse/-/fast-json-parse-1.0.3.tgz", + "integrity": "sha512-FRWsaZRWEJ1ESVNbDWmsAlqDk96gPQezzLghafp5J4GUKjbCz3OkAHuZs5TuPEtkbVQERysLp9xv6c24fBm8Aw==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -6078,7 +6052,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, "engines": { "node": "6.* || 8.* || >= 10.*" } @@ -7899,6 +7872,16 @@ "xml": "^1.0.1" } }, + "node_modules/jest-junit/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/jest-leak-detector": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", @@ -8967,6 +8950,40 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/jsx-ast-utils": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", @@ -8982,6 +8999,27 @@ "node": ">=4.0" } }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -9098,6 +9136,48 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -9110,6 +9190,12 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -9252,6 +9338,7 @@ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, + "license": "MIT", "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" @@ -9336,8 +9423,7 @@ "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/msw": { "version": "2.3.1", @@ -9802,11 +9888,6 @@ "node": ">=8" } }, - "node_modules/oauth": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.0.tgz", - "integrity": "sha512-1orQ9MT1vHFGQxhuy7E/0gECD3fd2fCC+PIX+/jgmU/gI3EpRocXtmtvxCO5x3WZ443FLTLFWNDjl5MPJf9u+Q==" - }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -10082,6 +10163,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse-ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-2.1.0.tgz", + "integrity": "sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -10104,6 +10195,7 @@ "version": "0.7.0", "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", + "license": "MIT", "dependencies": { "passport-strategy": "1.x.x", "pause": "0.0.1", @@ -10117,47 +10209,6 @@ "url": "https://github.com/sponsors/jaredhanson" } }, - "node_modules/passport-google-oauth20": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz", - "integrity": "sha512-KSk6IJ15RoxuGq7D1UKK/8qKhNfzbLeLrG3gkLZ7p4A6DBCcv7xpyQwuXtWdpyR0+E0mwkpjY1VfPOhxQrKzdQ==", - "dependencies": { - "passport-oauth2": "1.x.x" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/passport-local": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz", - "integrity": "sha512-9wCE6qKznvf9mQYYbgJ3sVOHmCWoUNMVFoZzNoznmISbhnNNPhN9xfY3sLmScHMetEJeoY7CXwfhCe7argfQow==", - "dependencies": { - "passport-strategy": "1.x.x" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/passport-oauth2": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.8.0.tgz", - "integrity": "sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA==", - "dependencies": { - "base64url": "3.x.x", - "oauth": "0.10.x", - "passport-strategy": "1.x.x", - "uid2": "0.0.x", - "utils-merge": "1.x.x" - }, - "engines": { - "node": ">= 0.4.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/jaredhanson" - } - }, "node_modules/passport-strategy": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", @@ -10265,30 +10316,32 @@ } }, "node_modules/pino": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/pino/-/pino-8.19.0.tgz", - "integrity": "sha512-oswmokxkav9bADfJ2ifrvfHUwad6MLp73Uat0IkQWY3iAw5xTRoznXbXksZs8oaOUMpmhVWD+PZogNzllWpJaA==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-8.21.0.tgz", + "integrity": "sha512-ip4qdzjkAyDDZklUaZkcRFb2iA118H9SgRh8yzTkSQK8HilsOJF7rSY8HoW5+I0M46AZgX/pxbprf2vvzQCE0Q==", + "license": "MIT", "dependencies": { "atomic-sleep": "^1.0.0", "fast-redact": "^3.1.1", "on-exit-leak-free": "^2.1.0", - "pino-abstract-transport": "v1.1.0", + "pino-abstract-transport": "^1.2.0", "pino-std-serializers": "^6.0.0", "process-warning": "^3.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^3.7.0", - "thread-stream": "^2.0.0" + "thread-stream": "^2.6.0" }, "bin": { "pino": "bin.js" } }, "node_modules/pino-abstract-transport": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.1.0.tgz", - "integrity": "sha512-lsleG3/2a/JIWUtf9Q5gUNErBqwIu1tUKTT3dUzaf5DySw9ra1wcqKjJjLX1VTY64Wk1eEOYsVGSaGfCK85ekA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.2.0.tgz", + "integrity": "sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==", + "license": "MIT", "dependencies": { "readable-stream": "^4.0.0", "split2": "^4.0.0" @@ -10317,6 +10370,185 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/pino-colada": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/pino-colada/-/pino-colada-2.2.2.tgz", + "integrity": "sha512-tzZl6j4D2v9WSQ4vEa7s8j15v16U6+z6M0fAWJOu5gBKxpy+XnOAgFBzeZGkxm6W3AQ04WvdSpqVLnDBdJlvOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^3.0.0", + "fast-json-parse": "^1.0.2", + "prettier-bytes": "^1.0.3", + "pretty-ms": "^5.0.0", + "split2": "^3.0.0" + }, + "bin": { + "pino-colada": "bin.js" + } + }, + "node_modules/pino-colada/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pino-colada/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pino-colada/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/pino-colada/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/pino-colada/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pino-colada/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pino-colada/node_modules/split2": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", + "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==", + "dev": true, + "license": "ISC", + "dependencies": { + "readable-stream": "^3.0.0" + } + }, + "node_modules/pino-colada/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pino-http": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/pino-http/-/pino-http-10.2.0.tgz", + "integrity": "sha512-am03BxnV3Ckx68OkbH0iZs3indsrH78wncQ6w1w51KroIbvJZNImBKX2X1wjdY8lSyaJ0UrX/dnO2DY3cTeCRw==", + "license": "MIT", + "dependencies": { + "get-caller-file": "^2.0.5", + "pino": "^9.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^3.0.0" + } + }, + "node_modules/pino-http/node_modules/pino": { + "version": "9.3.2", + "resolved": "https://registry.npmjs.org/pino/-/pino-9.3.2.tgz", + "integrity": "sha512-WtARBjgZ7LNEkrGWxMBN/jvlFiE17LTbBoH0konmBU684Kd0uIiDwBXlcTCW7iJnA6HfIKwUssS/2AC6cDEanw==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.1.1", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^1.2.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^4.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-http/node_modules/pino-std-serializers": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz", + "integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==", + "license": "MIT" + }, + "node_modules/pino-http/node_modules/pino/node_modules/process-warning": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.0.tgz", + "integrity": "sha512-/MyYDxttz7DfGMMHiysAsFE4qF+pQYAA8ziO/3NcRVrQ5fSk+Mns4QZA/oRPFzvcqNoVJXQNWNAsdwBXLUkQKw==", + "license": "MIT" + }, + "node_modules/pino-http/node_modules/sonic-boom": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.0.1.tgz", + "integrity": "sha512-hTSD/6JMLyT4r9zeof6UtuBDpjJ9sO08/nmS5djaA9eozT9oOlNdpXSnzcgj4FTqpk3nkLrs61l4gip9r1HCrQ==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/pino-http/node_modules/thread-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", + "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + } + }, "node_modules/pino-std-serializers": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz", @@ -10385,6 +10617,13 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/prettier-bytes": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/prettier-bytes/-/prettier-bytes-1.0.4.tgz", + "integrity": "sha512-dLbWOa4xBn+qeWeIF60qRoB6Pk2jX5P3DIVgOQyMyvBpu931Q+8dXz8X0snJiFkQdohDDLnZQECjzsAj75hgZQ==", + "dev": true, + "license": "ISC" + }, "node_modules/prettier-linter-helpers": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", @@ -10423,6 +10662,22 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/pretty-ms": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-5.1.0.tgz", + "integrity": "sha512-4gaK1skD2gwscCfkswYQRmddUb2GJZtzDGRjHWadVHtK/DIKFufa12MvES6/xu1tVbUYeia5bmLcwJtZJQUqnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse-ms": "^2.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -11593,9 +11848,10 @@ "dev": true }, "node_modules/thread-stream": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.4.1.tgz", - "integrity": "sha512-d/Ex2iWd1whipbT681JmTINKw0ZwOUBZm7+Gjs64DHuX34mmw8vJL2bFAaNacaW72zYiTJxSHi5abUuOi5nsfg==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.7.0.tgz", + "integrity": "sha512-qQiRWsU/wvNolI6tbbCKd9iKaTnCXsTwVxhhKM6nctPdujTyztjlbUkUTUymidWcMnZ5pWR0ej4a0tjsW021vw==", + "license": "MIT", "dependencies": { "real-require": "^0.2.0" } @@ -11667,12 +11923,14 @@ } }, "node_modules/ts-jest": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.2.tgz", - "integrity": "sha512-br6GJoH/WUX4pu7FbZXuWGKGNDuU7b8Uj77g/Sp7puZV6EXzuByl6JrECvm0MzVzSTkSHWTihsXt+5XYER5b+g==", + "version": "29.2.4", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.4.tgz", + "integrity": "sha512-3d6tgDyhCI29HlpwIq87sNuI+3Q6GLTTCeYRHCs7vDz+/3GCMwEtV9jezLyl4ZtnBgx00I7hm8PCP8cTksMGrw==", "dev": true, + "license": "MIT", "dependencies": { "bs-logger": "0.x", + "ejs": "^3.1.10", "fast-json-stable-stringify": "2.x", "jest-util": "^29.0.0", "json5": "^2.2.3", @@ -11685,10 +11943,11 @@ "ts-jest": "cli.js" }, "engines": { - "node": "^16.10.0 || ^18.0.0 || >=20.0.0" + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" }, "peerDependencies": { "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0", "@jest/types": "^29.0.0", "babel-jest": "^29.0.0", "jest": "^29.0.0", @@ -11698,6 +11957,9 @@ "@babel/core": { "optional": true }, + "@jest/transform": { + "optional": true + }, "@jest/types": { "optional": true }, @@ -12062,11 +12324,6 @@ "node": ">= 0.8" } }, - "node_modules/uid2": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz", - "integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==" - }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", @@ -12181,10 +12438,14 @@ } }, "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true, + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", "bin": { "uuid": "dist/bin/uuid" } diff --git a/package.json b/package.json index ef845b0..5605ce2 100644 --- a/package.json +++ b/package.json @@ -7,17 +7,18 @@ "clean": "rimraf dist/*", "copy-assets": "ts-node tools/copyAssets", "tsc": "tsc", - "prettier-format": "prettier --config .prettierrc 'src/**/*.ts' --write", - "lint-and-fix": "eslint . --ext .ts --fix", - "lint": "eslint . --ext .ts", + "prettier:ci": "prettier --config .prettierrc 'src/**/*.ts' --check", + "prettier:fix": "prettier --config .prettierrc 'src/**/*.ts' --write", + "lint:ci": "eslint . --ext .ts", + "lint:fix": "eslint . --ext .ts --fix", + "build": "npm-run-all clean tsc copy-assets", "test": "jest --coverage", "test:ci": "jest --ci --coverage --config=jest.config.ts", - "build": "npm-run-all clean lint test tsc copy-assets", - "dev": "nodemon --watch src -e ts,ejs --exec npm run start", - "start": "npm run build && node dist/server.js", - "start:container": "node dist/server.js", - "typeorm": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js", - "migration": "npm run typeorm migration:run" + "check": "npm-run-all prettier:fix lint:fix test build", + "dev:check": "npm-run-all check dev", + "dev": "NODE_ENV=dev nodemon --watch src -e ts,ejs --exec npm run start", + "start": "npm run build && node dist/server.js | pino-colada", + "start:container": "node dist/server.js" }, "keywords": [], "author": "", @@ -25,15 +26,20 @@ "devDependencies": { "@shopify/eslint-plugin": "^44.0.0", "@types/better-sqlite3": "^7.6.9", + "@types/cookie-parser": "^1.4.7", "@types/express": "^4.17.21", + "@types/express-session": "^1.18.0", "@types/fs-extra": "^9.0.13", "@types/jest": "^29.5.12", + "@types/jsonwebtoken": "^9.0.6", + "@types/lodash": "^4.17.7", "@types/multer": "^1.4.11", "@types/node": "^20.11.27", - "@types/passport-google-oauth20": "^2.0.16", + "@types/passport": "^1.0.16", "@types/pino": "^7.0.5", "@types/shelljs": "^0.8.15", "@types/supertest": "^6.0.2", + "@types/uuid": "^10.0.0", "@typescript-eslint/eslint-plugin": "^7.0.1", "@typescript-eslint/parser": "^7.0.1", "eslint": "^8.56.0", @@ -46,6 +52,7 @@ "msw": "^2.3.1", "nodemon": "^3.1.0", "npm-run-all": "^4.1.5", + "pino-colada": "^2.2.2", "prettier": "^3.2.5", "rimraf": "^3.0.2", "shelljs": "^0.8.5", @@ -56,9 +63,7 @@ "typescript": "^5.5.2" }, "dependencies": { - "@types/express-session": "^1.18.0", - "@types/passport": "^1.0.16", - "@types/passport-local": "^1.0.38", + "cookie-parser": "^1.4.6", "dotenv": "^16.4.4", "ejs": "^3.1.9", "express": "^4.19.2", @@ -69,11 +74,13 @@ "i18next": "^23.10.1", "i18next-fs-backend": "^2.3.1", "i18next-http-middleware": "^3.5.0", + "jsonwebtoken": "^9.0.2", + "lodash": "^4.17.21", "multer": "^1.4.5-lts.1", "passport": "^0.7.0", - "passport-google-oauth20": "^2.0.0", - "passport-local": "^1.0.0", - "pino": "^8.19.0", - "ts-node-dev": "^2.0.0" + "pino": "^8.21.0", + "pino-http": "^10.2.0", + "ts-node-dev": "^2.0.0", + "uuid": "^10.0.0" } } diff --git a/src/app.ts b/src/app.ts index bcdab50..93a6dcd 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,102 +1,52 @@ -import path from 'path'; +import path from 'node:path'; -import i18next from 'i18next'; -import Backend from 'i18next-fs-backend'; -import i18nextMiddleware from 'i18next-http-middleware'; -import rateLimit from 'express-rate-limit'; import express, { Application, Request, Response } from 'express'; -import session from 'express-session'; +import cookieParser from 'cookie-parser'; -import './config/i18next'; -import passport, { auth } from './config/auth_config'; -import { ensureAuthenticated } from './config/authenticate'; -import { healthcheck } from './route/healthcheck'; -import { publish } from './route/publish'; -import { view } from './route/view'; +import { checkConfig } from './utils/check-config'; +import { httpLogger } from './utils/logger'; +import session from './middleware/session'; +import { ensureAuthenticated } from './middleware/ensure-authenticated'; +import { rateLimiter } from './middleware/rate-limiter'; +import { i18next, i18nextMiddleware } from './middleware/translation'; +import { auth } from './routes/auth'; +import { healthcheck } from './routes/healthcheck'; +import { publish } from './routes/publish'; +import { view } from './routes/view'; -if (process.env.NODE_ENV !== 'test') { - const variables = [ - 'BACKEND_SERVER', - 'BACKEND_PORT', - 'BACKEND_PROTOCOL', - 'SESSION_SECRET', - 'GOOGLE_CLIENT_ID', - 'GOOGLE_CLIENT_SECRET' - ]; - - variables.forEach((variable) => { - if (!process.env[variable]) { - throw new Error(`Environment variable ${variable} is missing`); - } - }); -} +checkConfig(); const app: Application = express(); -i18next - .use(Backend) - .use(i18nextMiddleware.LanguageDetector) - .init({ - detection: { - order: ['path', 'header'], - lookupHeader: 'accept-language', - caches: false, - ignoreRoutes: ['/healthcheck', '/public', '/css', '/assets'] - }, - backend: { - loadPath: `${__dirname}/resources/locales/{{lng}}.json` - }, - fallbackLng: 'en-GB', - preload: ['en-GB', 'cy-GB'], - debug: false - }); - -const apiLimiter = rateLimit({ - windowMs: 15 * 60 * 1000, - max: 100, - standardHeaders: true, - legacyHeaders: false, - handler(req, res) { - res.status(429).json({ - message: 'Too many requests, please try again later.' - }); - } -}); - -const sessionConfig = { - secret: process.env.SESSION_SECRET === undefined ? 'default' : process.env.SESSION_SECRET, - resave: false, - saveUninitialized: false, - cookie: { - secure: false - } -}; +app.disable('x-powered-by'); if (app.get('env') === 'production') { app.set('trust proxy', 1); - sessionConfig.cookie.secure = true; } -// Middleware Config +// enable middleware app.use(express.urlencoded({ extended: true })); -app.use(session(sessionConfig)); -app.use(passport.initialize()); -app.use(passport.session()); +app.use(express.json()); +app.use(httpLogger); app.use(i18nextMiddleware.handle(i18next)); +app.use(session); +app.use(cookieParser()); + +// configure the view engine app.set('views', path.join(__dirname, 'views')); app.set('view engine', 'ejs'); // Load Routes -app.use('/auth', auth); -app.use('/:lang/publish', publish, apiLimiter, ensureAuthenticated); -app.use('/:lang/dataset', view, apiLimiter, ensureAuthenticated); -app.use('/:lang/healthcheck', healthcheck); -app.use('/healthcheck', healthcheck); app.use('/public', express.static(`${__dirname}/public`)); app.use('/css', express.static(`${__dirname}/css`)); app.use('/assets', express.static(`${__dirname}/assets`)); +app.use('/auth', rateLimiter, auth); +app.use('/healthcheck', rateLimiter, healthcheck); + +app.use('/:lang/publish', rateLimiter, ensureAuthenticated, publish); +app.use('/:lang/dataset', rateLimiter, ensureAuthenticated, view); +app.use('/:lang/healthcheck', rateLimiter, healthcheck); -// App Root Routes app.get('/', (req: Request, res: Response) => { const lang = req.headers['accept-language'] || req.headers['Accept-Language'] || req.i18n.language || 'en-GB'; if (lang.includes('cy')) { @@ -106,7 +56,7 @@ app.get('/', (req: Request, res: Response) => { } }); -app.get('/:lang/', apiLimiter, ensureAuthenticated, (req: Request, res: Response) => { +app.get('/:lang/', rateLimiter, ensureAuthenticated, (req: Request, res: Response) => { res.render('index'); }); diff --git a/src/config/auth_config.ts b/src/config/auth_config.ts deleted file mode 100644 index a0a1432..0000000 --- a/src/config/auth_config.ts +++ /dev/null @@ -1,63 +0,0 @@ -import dotenv from 'dotenv'; -import { Router, Request, Response, NextFunction } from 'express'; -import passport from 'passport'; -import { Strategy as GoogleStrategy } from 'passport-google-oauth20'; - -export interface User { - id: string; - displayName: string; -} - -dotenv.config(); -const users: User[] = []; - -passport.use( - new GoogleStrategy( - { - clientID: process.env.GOOGLE_CLIENT_ID || 'client_id', - clientSecret: process.env.GOOGLE_CLIENT_SECRET || 'client_secret', - callbackURL: '/auth/google/callback' - }, - (accessToken, refreshToken, profile, done) => { - let user = users.find((usr) => usr.id === profile.id); - if (!user) { - user = { id: profile.id, displayName: profile.displayName }; - users.push(user); - } - return done(null, user); - } - ) -); - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -passport.serializeUser((user: any, done) => { - done(null, user.id); -}); - -passport.deserializeUser((id: string, done) => { - const user = users.find((usr) => usr.id === id); - done(null, user); -}); - -export default passport; -export const auth = Router(); - -auth.get('/login', (req, res) => { - res.render('login'); -}); - -auth.get('/logout', (req: Request, res: Response, next: NextFunction) => { - // eslint-disable-next-line consistent-return - req.logout((err): void => { - if (err) { - return next(err); - } - res.redirect('/'); - }); -}); - -auth.get('/google', passport.authenticate('google', { scope: ['profile'] })); - -auth.get('/google/callback', passport.authenticate('google', { failureRedirect: '/login' }), (req, res) => { - res.redirect('/'); -}); diff --git a/src/config/authenticate.ts b/src/config/authenticate.ts deleted file mode 100644 index 2af814c..0000000 --- a/src/config/authenticate.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Request, Response, NextFunction } from 'express'; - -// eslint-disable-next-line consistent-return -export function ensureAuthenticated(req: Request, res: Response, next: NextFunction) { - if (req.isAuthenticated()) { - return next(); - } - // If not authenticated, redirect to login page - res.redirect('/auth/login'); -} diff --git a/src/interfaces/authed-request.ts b/src/interfaces/authed-request.ts new file mode 100644 index 0000000..5a2e696 --- /dev/null +++ b/src/interfaces/authed-request.ts @@ -0,0 +1,5 @@ +import { Request } from 'express'; + +export interface AuthedRequest extends Request { + jwt?: string; +} diff --git a/src/interfaces/jwt-payload-with-user.ts b/src/interfaces/jwt-payload-with-user.ts new file mode 100644 index 0000000..2f554c5 --- /dev/null +++ b/src/interfaces/jwt-payload-with-user.ts @@ -0,0 +1,7 @@ +import { JwtPayload } from 'jsonwebtoken'; + +import { User } from './user.interface'; + +export interface JWTPayloadWithUser extends JwtPayload { + user?: User; +} diff --git a/src/interfaces/user.interface.ts b/src/interfaces/user.interface.ts new file mode 100644 index 0000000..2c8ea60 --- /dev/null +++ b/src/interfaces/user.interface.ts @@ -0,0 +1,6 @@ +export interface User { + id: string; + email: string; + firstName: string; + lastName: string; +} diff --git a/src/middleware/ensure-authenticated.ts b/src/middleware/ensure-authenticated.ts new file mode 100644 index 0000000..c9c9555 --- /dev/null +++ b/src/middleware/ensure-authenticated.ts @@ -0,0 +1,43 @@ +import { RequestHandler } from 'express'; +import JWT from 'jsonwebtoken'; + +import { AuthedRequest } from '../interfaces/authed-request'; +import { JWTPayloadWithUser } from '../interfaces/jwt-payload-with-user'; +import { logger } from '../utils/logger'; + +export const ensureAuthenticated: RequestHandler = (req: AuthedRequest, res, next) => { + logger.debug('checking if user is authenticated...'); + + try { + if (!req.cookies.jwt) { + throw new Error('JWT cookie not found'); + } + + // JWT_SECRET must be the same as the backend or the token will fail verification + const secret = process.env.JWT_SECRET || ''; + const token = req.cookies.jwt; + + // verify the JWT token was signed by us + const decoded = JWT.verify(token, secret) as JWTPayloadWithUser; + + if (decoded.exp && decoded.exp <= Date.now() / 1000) { + logger.error('JWT token has expired'); + res.status(401); + return res.redirect('/auth/login?error=expired'); + } + + // store the token string in the request as we need it for Authorization header in API requests + req.jwt = token; + logger.debug(`JWT: ${token}`); + + // store the user object in the request for use in the frontend + req.user = decoded.user; + logger.info('user is authenticated'); + } catch (err) { + logger.error(`authentication failed: ${err}`); + res.status(401); + return res.redirect('/auth/login'); + } + + return next(); +}; diff --git a/src/middleware/rate-limiter.ts b/src/middleware/rate-limiter.ts new file mode 100644 index 0000000..149e6fa --- /dev/null +++ b/src/middleware/rate-limiter.ts @@ -0,0 +1,13 @@ +import rateLimit from 'express-rate-limit'; + +export const rateLimiter = rateLimit({ + windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS || '60000', 10), + max: parseInt(process.env.RATE_LIMIT_MAX_REQ || '100', 10), + standardHeaders: true, + legacyHeaders: false, + handler: (req, res) => { + res.status(429).json({ + message: 'Too many requests, please try again later.' + }); + } +}); diff --git a/src/middleware/session.ts b/src/middleware/session.ts new file mode 100644 index 0000000..2831b5a --- /dev/null +++ b/src/middleware/session.ts @@ -0,0 +1,10 @@ +import session from 'express-session'; + +export default session({ + secret: process.env.SESSION_SECRET || '', + resave: false, + saveUninitialized: false, + cookie: { + secure: process.env.NODE_ENV !== 'dev' + } +}); diff --git a/src/config/i18next.ts b/src/middleware/translation.ts similarity index 67% rename from src/config/i18next.ts rename to src/middleware/translation.ts index 032b2db..2c98454 100644 --- a/src/config/i18next.ts +++ b/src/middleware/translation.ts @@ -2,6 +2,9 @@ import i18next from 'i18next'; import Backend from 'i18next-fs-backend'; import i18nextMiddleware from 'i18next-http-middleware'; +const ENGLISH = 'en-GB'; +const WELSH = 'cy-GB'; + i18next .use(Backend) .use(i18nextMiddleware.LanguageDetector) @@ -13,13 +16,11 @@ i18next ignoreRoutes: ['/healthcheck', '/public', '/css', '/assets'] }, backend: { - loadPath: `${__dirname}/resources/locales/{{lng}}.json` + loadPath: `${__dirname}/../resources/locales/{{lng}}.json` }, - fallbackLng: 'en-GB', - preload: ['en-GB', 'cy-GB'], + fallbackLng: ENGLISH, + preload: [ENGLISH, WELSH], debug: false }); -export const ENGLISH = 'en-GB'; -export const WELSH = 'cy-GB'; -export const t = i18next.t; +export { i18next, i18nextMiddleware, ENGLISH, WELSH }; diff --git a/src/resources/locales/en-GB.json b/src/resources/locales/en-GB.json index abfc947..cb0a77d 100644 --- a/src/resources/locales/en-GB.json +++ b/src/resources/locales/en-GB.json @@ -23,6 +23,18 @@ "next": "Next", "update": "Update" }, + "login": { + "heading": "Login", + "buttons": { + "onelogin": "Login with OneLogin", + "google": "Login with Google" + }, + "error": { + "summary-title": "There is a problem", + "generic": "You could not be logged in. Please try again later.", + "expired": "Your session has expired. Please log in again." + } + }, "homepage": { "title": "Welcome to StatsWales Beta", "welcome": "We're going to use the GovUK Design system until we're able to get hold of the Welsh Government GEL", diff --git a/src/routes/auth.ts b/src/routes/auth.ts new file mode 100644 index 0000000..29d0317 --- /dev/null +++ b/src/routes/auth.ts @@ -0,0 +1,59 @@ +import { Router, Request, Response } from 'express'; +import JWT from 'jsonwebtoken'; + +import { logger } from '../utils/logger'; +import { JWTPayloadWithUser } from '../interfaces/jwt-payload-with-user'; + +export const auth = Router(); + +auth.get('/login', (req: Request, res: Response) => { + if (req.query.error && req.query.error === 'expired') { + logger.error(`Authentication token has expired`); + res.status(400); + res.render('auth/login', { errors: ['login.error.expired'] }); + return; + } + res.render('auth/login'); +}); + +auth.get('/google', (req: Request, res: Response) => { + logger.debug('Sending user to backend for google authentication'); + res.redirect(`${process.env.BACKEND_URL}/auth/google`); +}); + +auth.get('/onelogin', (req: Request, res: Response) => { + logger.debug('Sending user to backend for onelogin authentication'); + res.redirect(`${process.env.BACKEND_URL}/auth/onelogin`); +}); + +auth.get('/callback', (req: Request, res: Response) => { + logger.debug('returning from auth backend'); + + try { + if (req.query.error) { + throw new Error(`auth backend returned an error: ${req.query.error}`); + } + + // the backend stores the JWT token in a cookie before retuning the user to the frontend + if (!req.cookies.jwt) { + throw new Error('JWT cookie not found'); + } + + const secret = process.env.JWT_SECRET || ''; + const decoded = JWT.verify(req.cookies.jwt, secret) as JWTPayloadWithUser; + req.user = decoded.user; + } catch (err) { + logger.error(`problem authenticating user ${err}`); + res.status(400); + res.render('auth/login', { errors: ['login.error.generic'] }); + return; + } + + logger.debug('User successfully logged in'); + res.redirect('/'); +}); + +auth.get('/logout', (req: Request, res: Response) => { + res.clearCookie('jwt'); + res.redirect('/auth/login'); +}); diff --git a/src/route/healthcheck.ts b/src/routes/healthcheck.ts similarity index 70% rename from src/route/healthcheck.ts rename to src/routes/healthcheck.ts index 0ef1a38..751041b 100644 --- a/src/route/healthcheck.ts +++ b/src/routes/healthcheck.ts @@ -1,22 +1,18 @@ import { Router } from 'express'; -import pino from 'pino'; -import { API } from '../controllers/api'; - -export const logger = pino({ - name: 'StatsWales-Alpha-App: Healthcheck', - level: 'debug' -}); - -const APIInstance = new API(); +import { StatsWalesApi } from '../services/stats-wales-api'; +import { logger } from '../utils/logger'; export const healthcheck = Router(); healthcheck.get('/', async (req, res) => { const lang = req.i18n.language || 'en-GB'; + const APIInstance = new StatsWalesApi(lang); logger.info(`Healthcheck requested in ${lang}`); + const statusMsg = req.t('app-running'); const beConnected = await APIInstance.ping(); + res.json({ status: statusMsg, notes: req.t('health-notes'), diff --git a/src/route/publish.ts b/src/routes/publish.ts similarity index 86% rename from src/route/publish.ts rename to src/routes/publish.ts index c32aa0c..699db12 100644 --- a/src/route/publish.ts +++ b/src/routes/publish.ts @@ -1,21 +1,17 @@ import { Blob } from 'buffer'; import { Request, Response, Router } from 'express'; -import pino from 'pino'; import multer from 'multer'; -import { t } from '../config/i18next'; -import { API } from '../controllers/api'; +import { logger } from '../utils/logger'; +import { StatsWalesApi } from '../services/stats-wales-api'; import { ViewDTO, ViewErrDTO } from '../dtos2/view-dto'; +import { i18next } from '../middleware/translation'; +import { AuthedRequest } from '../interfaces/authed-request'; +const t = i18next.t; const storage = multer.memoryStorage(); const upload = multer({ storage }); -const APIInstance = new API(); - -const logger = pino({ - name: 'StatsWales-Alpha-App: Publish', - level: 'debug' -}); export const publish = Router(); @@ -58,10 +54,12 @@ publish.post('/title', upload.none(), (req: Request, res: Response) => { res.render('publish/upload', { title }); }); -publish.post('/upload', upload.single('csv'), async (req: Request, res: Response) => { +publish.post('/upload', upload.single('csv'), async (req: AuthedRequest, res: Response) => { const lang = req.i18n.language; + const statsWalesApi = new StatsWalesApi(lang, req.jwt); + if (!req.body?.title) { - logger.debug('title was missing on request'); + logger.debug('Internal name was missing on request'); const err: ViewErrDTO = { success: false, status: 400, @@ -71,7 +69,7 @@ publish.post('/upload', upload.single('csv'), async (req: Request, res: Response field: 'title', message: [ { - lang: req.i18n.language, + lang, message: t('errors.title.missing') } ], @@ -86,8 +84,10 @@ publish.post('/upload', upload.single('csv'), async (req: Request, res: Response res.render('publish/title', err); return; } - logger.debug(`Title: ${req.body.title}`); + + logger.debug(`Title name: ${req.body.title}`); const title: string = req.body.title; + if (!req.file) { logger.debug('Attached file was missing on this request'); const err: ViewErrDTO = { @@ -99,7 +99,7 @@ publish.post('/upload', upload.single('csv'), async (req: Request, res: Response field: 'csv', message: [ { - lang: req.i18n.language, + lang, message: t('errors.upload.no-csv-data') } ], @@ -118,12 +118,13 @@ publish.post('/upload', upload.single('csv'), async (req: Request, res: Response const fileName = req.file?.originalname; const fileData = new Blob([req.file?.buffer], { type: req.file?.mimetype }); - const processedCSV = await APIInstance.uploadCSV(lang, fileData, fileName, title); + const processedCSV = await statsWalesApi.uploadCSV(fileData, fileName, title); + if (processedCSV.success) { // eslint-disable-next-line require-atomic-updates req.session.currentDataset = processedCSV.dataset; req.session.save(); - res.redirect(`/${req.i18n.language}/publish/preview`); + res.redirect(`/${lang}/publish/preview`); } else { res.status(400); res.render('publish/upload', processedCSV); @@ -131,17 +132,18 @@ publish.post('/upload', upload.single('csv'), async (req: Request, res: Response }); publish.get('/preview', async (req: Request, res: Response) => { + const lang = req.i18n.language; + const statsWalesApi = new StatsWalesApi(lang); + const dataset = req.session.currentDataset; if (!dataset) { logger.debug('No dataset in session'); res.redirect(`/${req.i18n.language}/publish/title`); return; } - const lang = req.i18n.language; const page_number: number = Number.parseInt(req.query.page_number as string, 10) || 1; const page_size: number = Number.parseInt(req.query.page_size as string, 10) || 10; - const previewData = await APIInstance.getDatasetDatafilePreview( - lang, + const previewData = await statsWalesApi.getDatasetDatafilePreview( dataset.id, dataset.revisions[0].id, dataset.revisions[0].imports[0].id, @@ -164,7 +166,7 @@ publish.post('/confirm', upload.none(), (req: Request, res: Response) => { res.redirect(`/${req.i18n.language}/publish/title`); return; } - // const lang = req.i18n.language; + const lang = req.i18n.language; const confirmData = req.body?.confirm; if (!confirmData) { logger.debug('No confirmation data was provided'); @@ -177,7 +179,7 @@ publish.post('/confirm', upload.none(), (req: Request, res: Response) => { field: 'confirm', message: [ { - lang: req.i18n.language, + lang, message: t('errors.confirm.missing') } ], diff --git a/src/route/view.ts b/src/routes/view.ts similarity index 57% rename from src/route/view.ts rename to src/routes/view.ts index c42d4e9..8f2dc9f 100644 --- a/src/route/view.ts +++ b/src/routes/view.ts @@ -1,26 +1,34 @@ -import { Router, Request, Response } from 'express'; +import { Router, Response } from 'express'; +import { validate as validateUUID } from 'uuid'; -import { t } from '../config/i18next'; -import { API } from '../controllers/api'; +import { StatsWalesApi } from '../services/stats-wales-api'; import { FileList } from '../dtos2/filelist'; import { ViewErrDTO } from '../dtos2/view-dto'; +import { i18next } from '../middleware/translation'; +import { logger } from '../utils/logger'; +import { AuthedRequest } from '../interfaces/authed-request'; -const APIInstance = new API(); +const t = i18next.t; export const view = Router(); -view.get('/', async (req: Request, res: Response) => { +const statsWalesApi = (req: AuthedRequest) => { const lang = req.i18n.language; - const fileList: FileList = await APIInstance.getFileList(lang); - console.log(`FileList from server = ${JSON.stringify(fileList)}`); + const token = req.jwt; + return new StatsWalesApi(lang, token); +}; + +view.get('/', async (req: AuthedRequest, res: Response) => { + const fileList: FileList = await statsWalesApi(req).getFileList(); + logger.debug(`FileList from server = ${JSON.stringify(fileList)}`); res.render('list', fileList); }); -view.get('/:datasetId', async (req: Request, res: Response) => { +view.get('/:datasetId', async (req: AuthedRequest, res: Response) => { const lang = req.i18n.language; const page_number: number = Number.parseInt(req.query.page_number as string, 10) || 1; const page_size: number = Number.parseInt(req.query.page_size as string, 10) || 100; - if (!req.params.datasetId) { + if (!req.params.datasetId || !validateUUID(req.params.datasetId)) { const err: ViewErrDTO = { success: false, status: 400, @@ -30,7 +38,7 @@ view.get('/:datasetId', async (req: Request, res: Response) => { field: 'file', message: [ { - lang: req.i18n.language, + lang, message: t('errors.dataset_missing') } ], @@ -45,9 +53,10 @@ view.get('/:datasetId', async (req: Request, res: Response) => { res.render('data', err); return; } + const datasetId = req.params.datasetId; - const file = await APIInstance.getDatasetView(lang, datasetId, page_number, page_size); + const file = await statsWalesApi(req).getDatasetView(datasetId, page_number, page_size); if (!file.success) { const error = file as ViewErrDTO; res.status(error.status); diff --git a/src/server.ts b/src/server.ts index f3b7e40..69bb172 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,9 +1,7 @@ -import dotenv from 'dotenv'; +import 'dotenv/config'; import app from './app'; -dotenv.config(); - const PORT = process.env.PORT || 3000; app.listen(PORT, () => { diff --git a/src/controllers/api.ts b/src/services/stats-wales-api.ts similarity index 72% rename from src/controllers/api.ts rename to src/services/stats-wales-api.ts index b1e6bb4..5a7aba3 100644 --- a/src/controllers/api.ts +++ b/src/services/stats-wales-api.ts @@ -1,17 +1,9 @@ -import { env } from 'process'; - -import pino from 'pino'; - import { FileListError, FileList } from '../dtos2/filelist'; import { ViewDTO, ViewErrDTO } from '../dtos2/view-dto'; import { Healthcheck } from '../dtos2/healthcehck'; import { UploadDTO, UploadErrDTO } from '../dtos2/upload-dto'; import { DatasetDTO } from '../dtos2/dataset-dto'; - -const logger = pino({ - name: 'StatsWales-Alpha-App: API', - level: 'debug' -}); +import { logger } from '../utils/logger'; class HttpError extends Error { public status: number; @@ -27,25 +19,22 @@ class HttpError extends Error { } } -export class API { - private readonly backend_server: string | undefined; - private readonly backend_port: string | undefined; - private readonly backend_protocol: string; +export class StatsWalesApi { + private readonly backendUrl = process.env.BACKEND_URL || ''; + private readonly authHeader: Record; - constructor() { - this.backend_server = env.BACKEND_SERVER; - this.backend_port = env.BACKEND_PORT; - if (env.BACKEND_PROTOCOL === 'https') { - this.backend_protocol = 'https'; - } else { - this.backend_protocol = 'http'; - } + constructor( + private lang: string, + private token?: string + ) { + this.lang = lang; + this.authHeader = token ? { Authorization: `Bearer ${token}` } : {}; } - public async getFileList(lang: string) { - const filelist: FileList = await fetch( - `${this.backend_protocol}://${this.backend_server}:${this.backend_port}/${lang}/dataset` - ) + public async getFileList() { + logger.debug(`Fetching file list from ${this.backendUrl}/${this.lang}/dataset`); + + const filelist: FileList = await fetch(`${this.backendUrl}/${this.lang}/dataset`, { headers: this.authHeader }) .then((response) => { if (response.ok) { return response.json(); @@ -64,12 +53,13 @@ export class API { return filelist; } - public async getDatasetView(lang: string, datasetId: string, page_number: number, page_size: number) { + public async getDatasetView(datasetId: string, pageNumber: number, pageSize: number) { logger.info( - `Fetching dataset view from ${this.backend_protocol}://${this.backend_server}:${this.backend_port}/${lang}/dataset/${datasetId}/view?page_number=${page_number}&page_size=${page_size}` + `Fetching dataset view from ${this.backendUrl}/${this.lang}/dataset/${datasetId}/view?page_number=${pageNumber}&page_size=${pageSize}` ); const file = await fetch( - `${this.backend_protocol}://${this.backend_server}:${this.backend_port}/${lang}/dataset/${datasetId}/view?page_number=${page_number}&page_size=${page_size}` + `${this.backendUrl}/${this.lang}/dataset/${datasetId}/view?page_number=${pageNumber}&page_size=${pageSize}`, + { headers: this.authHeader } ) .then((response) => { if (response.ok) { @@ -92,7 +82,7 @@ export class API { field: 'file', message: [ { - lang, + lang: this.lang, message: 'errors.dataset_missing' } ], @@ -109,15 +99,15 @@ export class API { } public async getDatasetDatafilePreview( - lang: string, datasetId: string, revisionId: string, importId: string, - page_number: number, - page_size: number + pageNumber: number, + pageSize: number ) { const file = await fetch( - `${this.backend_protocol}://${this.backend_server}:${this.backend_port}/${lang}/dataset/${datasetId}/revision/by-id/${revisionId}/import/by-id/${importId}/preview?page_number=${page_number}&page_size=${page_size}` + `${this.backendUrl}/${this.lang}/dataset/${datasetId}/revision/by-id/${revisionId}/import/by-id/${importId}/preview?page_number=${pageNumber}&page_size=${pageSize}`, + { headers: this.authHeader } ) .then((response) => { if (response.ok) { @@ -140,7 +130,7 @@ export class API { field: 'file', message: [ { - lang, + lang: this.lang, message: 'errors.dataset_missing' } ], @@ -156,18 +146,16 @@ export class API { return file; } - public async uploadCSV(lang: string, file: Blob, filename: string, title: string) { + public async uploadCSV(file: Blob, filename: string, title: string) { const formData = new FormData(); formData.set('csv', file, filename); formData.set('title', title); - const processedCSV = await fetch( - `${this.backend_protocol}://${this.backend_server}:${this.backend_port}/${lang}/dataset/`, - { - method: 'POST', - body: formData - } - ) + const processedCSV = await fetch(`${this.backendUrl}/${this.lang}/dataset/`, { + method: 'POST', + body: formData, + headers: this.authHeader + }) .then((response) => { if (response.ok) { return response.json(); @@ -193,7 +181,7 @@ export class API { field: 'csv', message: [ { - lang, + lang: this.lang, message: 'errors.upload.no-csv-data' } ], @@ -210,7 +198,7 @@ export class API { } public async ping() { - const health = await fetch(`${this.backend_protocol}://${this.backend_server}:${this.backend_port}/healthcheck`) + const health = await fetch(`${this.backendUrl}/healthcheck`) .then((api_res) => api_res.json()) .then((api_res) => { return api_res as Healthcheck; diff --git a/src/utils/check-config.ts b/src/utils/check-config.ts new file mode 100644 index 0000000..22bc1ca --- /dev/null +++ b/src/utils/check-config.ts @@ -0,0 +1,11 @@ +export const checkConfig = () => { + if (process.env.NODE_ENV === 'test') return; + + const requiredEnvVars = ['FRONTEND_URL', 'BACKEND_URL', 'SESSION_SECRET', 'JWT_SECRET']; + + requiredEnvVars.forEach((variable) => { + if (!process.env[variable]) { + throw new Error(`Environment variable ${variable} is missing`); + } + }); +}; diff --git a/src/utils/logger.ts b/src/utils/logger.ts new file mode 100644 index 0000000..8e4f234 --- /dev/null +++ b/src/utils/logger.ts @@ -0,0 +1,33 @@ +import pino from 'pino'; +import pinoHttp from 'pino-http'; +import pick from 'lodash/pick'; + +export const logger = pino({ + level: process.env.LOG_LEVEL || 'debug' +}); + +export const httpLogger = pinoHttp({ + logger, + autoLogging: { + ignore: (req) => { + const ignorePathsRx = /^\/css|\/public|\/assets|\/favicon/; + return ignorePathsRx.test(req.url || ''); + } + }, + customLogLevel(req, res, err) { + if (res.statusCode >= 400 && res.statusCode < 500) { + return 'warn'; + } else if (res.statusCode >= 500 || err) { + return 'error'; + } + return 'info'; + }, + serializers: { + req(req) { + return pick(req, ['method', 'url', 'query', 'params']); + }, + res(res) { + return pick(res, ['statusCode']); + } + } +}); diff --git a/src/views/auth/login.ejs b/src/views/auth/login.ejs new file mode 100644 index 0000000..6df191b --- /dev/null +++ b/src/views/auth/login.ejs @@ -0,0 +1,32 @@ +<%- include("../partials/top"); %> + +
+
+

<%= t('login.heading') %>

+ + <% if (locals?.errors) { %> +
+
+

+ <%= t('errors.problem') %> +

+
+ +
+
+
+ <% } %> + + +
+
+ +<%- include("../partials/bottom"); %> diff --git a/src/views/login.ejs b/src/views/login.ejs deleted file mode 100644 index 7bffb8f..0000000 --- a/src/views/login.ejs +++ /dev/null @@ -1,14 +0,0 @@ -<%- include("partials/top"); %> - -
- -
-

Login

- -
-
- -<%- include("partials/bottom"); %> \ No newline at end of file diff --git a/test/.jest/setEnvVars.ts b/test/.jest/setEnvVars.ts index ac2d7b3..8fca3aa 100644 --- a/test/.jest/setEnvVars.ts +++ b/test/.jest/setEnvVars.ts @@ -1,3 +1,4 @@ -process.env.BACKEND_SERVER = 'somehost.com'; -process.env.BACKEND_PORT = '3001'; -process.env.BACKEND_PROTOCOL = 'http'; +process.env.FRONTEND_URL = 'http://example.com:3000'; +process.env.BACKEND_URL = 'http://example.com:3001'; +process.env.SESSION_SECRET = 'mysecret'; +process.env.JWT_SECRET = 'mysecret'; diff --git a/test/app.test.ts b/test/app.test.ts index c302ebc..33e10d3 100644 --- a/test/app.test.ts +++ b/test/app.test.ts @@ -1,11 +1,11 @@ -import path from 'path'; +import path from 'node:path'; import request from 'supertest'; import { Request, Response, NextFunction } from 'express'; import { http, HttpResponse } from 'msw'; import { setupServer } from 'msw/node'; -import { ENGLISH, WELSH, t } from '../src/config/i18next'; +import { ENGLISH, WELSH, i18next } from '../src/middleware/translation'; import app from '../src/app'; import { DatasetDTO } from '../src/dtos2/dataset-dto'; @@ -15,12 +15,14 @@ declare module 'express-session' { } } -jest.mock('../src/config/authenticate', () => ({ +const t = i18next.t; + +jest.mock('../src/middleware/ensure-authenticated', () => ({ ensureAuthenticated: (req: Request, res: Response, next: NextFunction) => next() })); const server = setupServer( - http.get('http://somehost.com:3001/en-GB/dataset', () => { + http.get('http://example.com:3001/en-GB/dataset', () => { return HttpResponse.json({ filelist: [ { @@ -44,13 +46,13 @@ const server = setupServer( ] }); }), - http.get('http://somehost.com:3001/en-GB/dataset/missing-id/view', () => { + http.get('http://example.com:3001/en-GB/dataset/missing-id/view', () => { return new HttpResponse(null, { status: 404, statusText: '{}' }); }), - http.get('http://somehost.com:3001/en-GB/dataset/5caeb8ed-ea64-4a58-8cf0-b728308833e5', () => { + http.get('http://example.com:3001/en-GB/dataset/5caeb8ed-ea64-4a58-8cf0-b728308833e5', () => { return HttpResponse.json({ id: '5caeb8ed-ea64-4a58-8cf0-b728308833e5', creation_date: '2024-09-05T10:05:03.871Z', @@ -92,7 +94,7 @@ const server = setupServer( ] }); }), - http.get('http://somehost.com:3001/en-GB/dataset/5caeb8ed-ea64-4a58-8cf0-b728308833e5/view', () => { + http.get('http://example.com:3001/en-GB/dataset/5caeb8ed-ea64-4a58-8cf0-b728308833e5/view', () => { return HttpResponse.json({ success: true, dataset: { @@ -138,7 +140,7 @@ const server = setupServer( ] }); }), - http.post('http://somehost.com:3001/en-GB/dataset/', async (req) => { + http.post('http://example.com:3001/en-GB/dataset/', async (req) => { const data = await req.request.formData(); const title = data.get('title') as string; if (title === 'test-data-3.csv fail test') { @@ -201,7 +203,7 @@ const server = setupServer( ] }); }), - http.get('http://somehost.com:3001/healthcheck', () => { + http.get('http://example.com:3001/healthcheck', () => { return HttpResponse.json({ status: 'App is running', notes: 'Expand endpoint to check for database connection and other services.'