diff --git a/eslint.config.mjs b/eslint.config.mjs index d05d65918..0bb9839fc 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -17,7 +17,7 @@ export default defineConfig([ ...love, languageOptions: { parserOptions: { - project: './tsconfig.dev.json', + project: ['./tsconfig.dev.json', './tsconfig.openapi.json'], }, }, }, @@ -42,7 +42,7 @@ export default defineConfig([ }, parserOptions: { - project: './tsconfig.dev.json', + project: ['./tsconfig.dev.json', './tsconfig.openapi.json'], }, }, @@ -87,7 +87,7 @@ export default defineConfig([ 'import/resolver': { typescript: { alwaysTryTypes: true, - project: './tsconfig.dev.json', + project: ['./tsconfig.dev.json', './tsconfig.openapi.json'], }, node: true, }, diff --git a/package.json b/package.json index 1232e88f7..bbfb45ca8 100644 --- a/package.json +++ b/package.json @@ -3,20 +3,20 @@ "description": "The service backend for the Philanthropy Data Commons", "main": "dist/index.js", "scripts": { - "build": "npm run cleanup && npm run build:node && npm run build:assets && npm run build:openapi", + "build": "npm run cleanup && npm run build:openapi && npm run build:node && npm run build:assets", "build:node": "tsc -p tsconfig.json", "build:assets": "copyfiles -u 1 \"src/**/*.sql\" \"src/**/.keep\" \"src/public/**\" dist", - "build:openapi": "redocly bundle \"src/openapi/api.json\" -o \"dist/openapi/api.json\"", + "build:openapi": "tsc -p tsconfig.openapi.json && redocly bundle \"src/openapi/api.json\" -o \"dist/openapi/api.json\"", "cleanup": "rimraf dist", "format": "npm run format:eslint && npm run format:prettier && npm run format:sqlfluff", "format:eslint": "eslint ./src --fix || true", "format:prettier": "prettier . --write", "format:sqlfluff": "./venv/bin/sqlfluff fix src/database/", - "lint": "npm run lint:eslint && npm run lint:prettier && npm run lint:tsc && npm run lint:openapi && npm run lint:sqlfluff && npm run lint:docker-compose", + "lint": "npm run lint:eslint && npm run lint:prettier && npm run lint:openapi && npm run lint:tsc && npm run lint:sqlfluff && npm run lint:docker-compose", "lint:docker-compose": "dclint ./compose-ci.yml", "lint:eslint": "eslint ./src --max-warnings=0", - "lint:openapi": "npx @redocly/cli lint src/openapi/api.json", - "lint:tsc": "tsc --noEmit -p tsconfig.dev.json", + "lint:openapi": "npm run build:openapi && npx @redocly/cli lint src/openapi/api.json", + "lint:tsc": "npm run build:openapi && tsc --noEmit -p tsconfig.dev.json", "lint:prettier": "prettier . --check", "lint:sqlfluff": "./venv/bin/sqlfluff lint src/database/", "migrate": "ts-node src/scripts/migrate.ts | pino-pretty", diff --git a/src/openapi/components/securitySchemes/auth.json b/src/openapi/components/securitySchemes/auth.json index 6fff2ffce..8d3f67c24 100644 --- a/src/openapi/components/securitySchemes/auth.json +++ b/src/openapi/components/securitySchemes/auth.json @@ -1,6 +1,7 @@ { "type": "oauth2", "description": "The OAuth 2.0 (and OpenID Connect) Authorization Code flow.", + "x-logoutUrl": "{{AUTH_ISSUER}}/protocol/openid-connect/logout", "flows": { "authorizationCode": { "authorizationUrl": "{{AUTH_ISSUER}}/protocol/openid-connect/auth", diff --git a/src/openapi/plugins/logout.ts b/src/openapi/plugins/logout.ts new file mode 100644 index 000000000..74e62cc57 --- /dev/null +++ b/src/openapi/plugins/logout.ts @@ -0,0 +1,59 @@ +type NestedMap = Map>>>; +type DeepNestedMap = Map< + string, + Map>>> +>; +interface System { + getState: () => DeepNestedMap; + specSelectors: { + specJson: () => NestedMap; + }; +} + +const logout = ( + system: System, +): { + statePlugins: { + auth: { + wrapActions: { + logout: ( + originalFunction: (keys: string[]) => Map, + ) => (keys: string[]) => Map; + }; + }; + }; +} => ({ + statePlugins: { + auth: { + wrapActions: { + logout: + (originalFunction: (keys: string[]) => Map) => + (keys: string[]) => { + const logoutUrl = system.specSelectors + .specJson() + .get('components') + ?.get('securitySchemes') + ?.get('auth') + ?.get('x-logoutUrl'); + const idToken = system + .getState() + .get('auth') + ?.get('authorized') + ?.get('auth') + ?.get('token') + ?.get('id_token'); + const { + location: { href }, + } = window; + const result = originalFunction(keys); + if (logoutUrl !== undefined) { + location.href = `${logoutUrl}?&id_token_hint=${idToken}&post_logout_redirect_uri=${href}`; + } + return result; + }, + }, + }, + }, +}); + +export { logout }; diff --git a/src/routers/documentationRouter.ts b/src/routers/documentationRouter.ts index 049b08f5b..db12134d3 100644 --- a/src/routers/documentationRouter.ts +++ b/src/routers/documentationRouter.ts @@ -3,6 +3,7 @@ import express from 'express'; import { requireEnv } from 'require-env-variable'; import swaggerUi from 'swagger-ui-express'; import { documentationHandlers } from '../handlers/documentationHandlers'; +import { logout } from '../../dist/openapi/plugins/logout'; import type { SwaggerUiOptions } from 'swagger-ui-express'; const { OPENAPI_DOCS_AUTH_CLIENT_ID } = requireEnv( @@ -20,6 +21,7 @@ const options: SwaggerUiOptions = { scopes: ['openid', 'roles', 'profile'], usePkceWithAuthorizationCodeGrant: true, }, + plugins: [logout], }, swaggerUrl: '/openapi/api.json', }; diff --git a/tsconfig.dev.json b/tsconfig.dev.json index 73db699f4..751446a17 100644 --- a/tsconfig.dev.json +++ b/tsconfig.dev.json @@ -6,5 +6,6 @@ "noUncheckedIndexedAccess": true, "isolatedModules": true }, - "include": ["src"] + "include": ["src"], + "exclude": ["src/openapi/plugins"] } diff --git a/tsconfig.json b/tsconfig.json index f8f61f45a..1737fc8b7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,5 +8,5 @@ "noUncheckedIndexedAccess": true }, "include": ["src"], - "exclude": ["**/*.test.ts", "**/src/test"] + "exclude": ["**/*.test.ts", "**/src/test", "src/openapi/plugins/logout.ts"] } diff --git a/tsconfig.openapi.json b/tsconfig.openapi.json new file mode 100644 index 000000000..15ba625e1 --- /dev/null +++ b/tsconfig.openapi.json @@ -0,0 +1,13 @@ +{ + "extends": "@tsconfig/node24/tsconfig.json", + "compilerOptions": { + "lib": ["dom"], + "outDir": "dist/openapi/plugins", + "declaration": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "noUncheckedIndexedAccess": true + }, + "files": ["src/openapi/plugins/logout.ts"], + "include": ["src/openapi/plugins/logout.ts"] +}