diff --git a/codegen.yml b/codegen.yml index cb27a44a..bf4f875b 100644 --- a/codegen.yml +++ b/codegen.yml @@ -2,6 +2,9 @@ overwrite: true schema: 'src/schema/**/*.ts': noRequire: true +hooks: + afterAllFileWrite: + - prettier --write generates: src/types/generated/schema-types.ts: plugins: diff --git a/package.json b/package.json index db158d44..b7a0a9c8 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "knex": "^3.2.10", "knex-postgis": "^0.14.3", "lodash": "^4.17.11", + "lusca": "^1.7.0", "moment": "^2.24.0", "moment-timezone": "^0.5.23", "node-fetch": "^2.3.0", @@ -95,6 +96,7 @@ "@types/ioredis": "^4.0.9", "@types/jest": "^30", "@types/lodash": "^4.14.122", + "@types/lusca": "^1.7.5", "@types/node": "^20", "@types/validator": "^13.15.4", "concurrently": "^4.1.0", diff --git a/src/admin/adminController.ts b/src/admin/adminController.ts index 05e8c5eb..2e4e06c5 100644 --- a/src/admin/adminController.ts +++ b/src/admin/adminController.ts @@ -1,4 +1,5 @@ import express from 'express' +import lusca from 'lusca' import { DomainGroup, getSettings, @@ -19,9 +20,20 @@ export const adminController = async (adminPath) => { const prefixedAdminPath = join(PATH_PREFIX, adminPath) const adminRouter = express.Router() + // Protect the state-changing admin form submissions (rendered as HTML forms + // and posted as application/x-www-form-urlencoded) against CSRF. lusca.csrf + // stores a secret in the existing session and exposes the per-session token + // as `res.locals._csrf`, which must be echoed back via the hidden `_csrf` + // field embedded in each form. + adminRouter.use(lusca.csrf()) + adminRouter.get('/', async (req, res) => { const currentState = await getSettings() - res.render('Admin', { adminPath: prefixedAdminPath, settings: currentState }) + res.render('Admin', { + adminPath: prefixedAdminPath, + settings: currentState, + csrfToken: res.locals._csrf, + }) }) function createDomainGroupsFromInput(input: string): DomainGroup[] { diff --git a/src/types/generated/resolver-types.ts b/src/types/generated/resolver-types.ts index 5a5a055c..a71ab680 100644 --- a/src/types/generated/resolver-types.ts +++ b/src/types/generated/resolver-types.ts @@ -1117,8 +1117,8 @@ export type ResolversTypes = { Mutation: ResolverTypeWrapper<{}> Feedback: ResolverTypeWrapper Upload: ResolverTypeWrapper - BBox: ResolverTypeWrapper TlpType: TlpType + BBox: ResolverTypeWrapper } /** Mapping between all available schema types and the resolvers parents */ @@ -1165,7 +1165,9 @@ export type ResolversParentTypes = { ObservedArrival: ObservedArrival ObservedDeparture: ObservedDeparture ExceptionDay: ExceptionDay - Journey: Omit & { events: ResolversParentTypes['JourneyEventType'][] } + Journey: Omit & { + events: ResolversParentTypes['JourneyEventType'][] + } VehiclePosition: VehiclePosition JourneyEventType: | ResolversParentTypes['JourneyEvent'] @@ -1193,8 +1195,8 @@ export type ResolversParentTypes = { Mutation: {} Feedback: Feedback Upload: Scalars['Upload'] - BBox: Scalars['BBox'] TlpType: TlpType + BBox: Scalars['BBox'] } export type AlertResolvers< diff --git a/src/views/Admin.js b/src/views/Admin.js index 5f562028..ef2aa596 100644 --- a/src/views/Admin.js +++ b/src/views/Admin.js @@ -4,14 +4,14 @@ import GroupAssignment from './components/GroupAssignment' import AutoDomainGroups from './components/AutoDomainGroups' import join from 'proper-url-join' -const AdminView = ({ adminPath, settings }) => { +const AdminView = ({ adminPath, settings, csrfToken }) => { return ( <>

Transitlog server admin


- - - + + +

Clear cache

@@ -19,6 +19,7 @@ const AdminView = ({ adminPath, settings }) => { will result in some queries taking longer due to being fetched from the DB.

+
diff --git a/src/views/components/AutoDomainGroups.js b/src/views/components/AutoDomainGroups.js index c24a526a..7e35795e 100644 --- a/src/views/components/AutoDomainGroups.js +++ b/src/views/components/AutoDomainGroups.js @@ -2,13 +2,14 @@ import React from 'react' import join from 'proper-url-join' import { get } from 'lodash' -const AutoDomainGroups = ({ adminPath, settings }) => { +const AutoDomainGroups = ({ adminPath, settings, csrfToken }) => { const assignedGroups = get(settings, 'auto_domain_groups', []) return ( <>

Create groups for emails

+
HSL ID group creation

diff --git a/src/views/components/GroupAssignment.js b/src/views/components/GroupAssignment.js index 331b733b..5869e4df 100644 --- a/src/views/components/GroupAssignment.js +++ b/src/views/components/GroupAssignment.js @@ -2,13 +2,14 @@ import React from 'react' import join from 'proper-url-join' import { get } from 'lodash' -const GroupAssignment = ({ adminPath, settings }) => { +const GroupAssignment = ({ adminPath, settings, csrfToken }) => { const assignedGroups = get(settings, 'domain_groups', []) return ( <>

Assign email domains to groups

+
HSL ID group assignment

diff --git a/src/views/components/UIMessage.js b/src/views/components/UIMessage.js index 26a4a6ce..069c1bff 100644 --- a/src/views/components/UIMessage.js +++ b/src/views/components/UIMessage.js @@ -2,13 +2,14 @@ import React from 'react' import join from 'proper-url-join' import { get } from 'lodash' -const UIMessageForm = ({ settings, adminPath }) => { +const UIMessageForm = ({ settings, adminPath, csrfToken }) => { const message = get(settings, 'ui_message') || { date: '', message: '' } return ( <>

Set UI message

+
UI message

Set a message that is shown in the UI.

diff --git a/yarn.lock b/yarn.lock index cd1c6178..c8ce501b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2608,9 +2608,21 @@ integrity sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ== "@types/long@^4.0.0": - version "4.0.2" - resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a" - integrity sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA== + version "4.0.1" + resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9" + integrity sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w== + +"@types/lusca@^1.7.5": + version "1.7.5" + resolved "https://registry.yarnpkg.com/@types/lusca/-/lusca-1.7.5.tgz#6fff257dc11176bd3150afba90790e626a12cd0f" + integrity sha512-l49gAf8pu2iMzbKejLcz6Pqj+51H2na6BgORv1ElnE8ByPFcBdh/eZ0WNR1Va/6ZuNSZa01Hoy1DTZ3IZ+y+kA== + dependencies: + "@types/express" "*" + +"@types/mime@^1": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a" + integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw== "@types/minimatch@*": version "6.0.0" @@ -6947,6 +6959,20 @@ lru-cache@^5.0.0, lru-cache@^5.1.1: dependencies: yallist "^3.0.2" +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + +lusca@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/lusca/-/lusca-1.7.0.tgz#a5d979f1b51776e60d41e0ca98f886f1b8b95502" + integrity sha512-msnrplCfY7zaqlZBDEloCIKld+RUeMZVeWzSPaGUKeRXFlruNSdKg2XxCyR+zj6BqzcXhXlRnvcvx6rAGgsvMA== + dependencies: + tsscmp "^1.0.5" + make-dir@^1.0.0: version "1.3.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c" @@ -9307,6 +9333,11 @@ tslint@^6.1.3: tslib "^1.13.0" tsutils "^2.29.0" +tsscmp@^1.0.5: + version "1.0.6" + resolved "https://registry.yarnpkg.com/tsscmp/-/tsscmp-1.0.6.tgz#85b99583ac3589ec4bfef825b5000aa911d605eb" + integrity sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA== + tsutils@^2.29.0: version "2.29.0" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.29.0.tgz#32b488501467acbedd4b85498673a0812aca0b99"