diff --git a/keycloak/import/realm-dev.json b/keycloak/import/realm-dev.json index 369315ef64..60335cc55a 100644 --- a/keycloak/import/realm-dev.json +++ b/keycloak/import/realm-dev.json @@ -725,6 +725,22 @@ "realmRoles": ["default-roles-potentiel"], "notBefore": 0, "groups": ["/GestionnairesRéseau/17X100A100A0001A"] + }, + { + "id": "c16130dd-13be-4a22-8a50-293a2b3cdafc", + "username": "service-account-integration-grd-enedis", + "lastName": "Integration GRD Enedis", + "email": "integration-grd-enedis@clients", + "emailVerified": false, + "createdTimestamp": 1731675236457, + "enabled": true, + "totp": false, + "serviceAccountClientId": "integration-grd-enedis", + "disableableCredentialTypes": [], + "requiredActions": [], + "realmRoles": ["default-roles-potentiel"], + "notBefore": 0, + "groups": ["/GestionnairesRéseau/17X100A100A0001A"] } ], "scopeMappings": [ @@ -1162,6 +1178,108 @@ ], "defaultClientScopes": ["web-origins", "roles", "profile", "email"], "optionalClientScopes": ["address", "phone", "offline_access", "microprofile-jwt"] + }, + { + "id": "e6ef1124-c854-4c59-9089-5168c328560f", + "clientId": "integration-grd-enedis", + "name": "", + "description": "", + "rootUrl": "", + "adminUrl": "", + "baseUrl": "", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "aWLTypom26xaQEhRjWoraV6GJcuCQRMs", + "redirectUris": ["/*"], + "webOrigins": ["/*"], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": false, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": true, + "publicClient": false, + "frontchannelLogout": true, + "protocol": "openid-connect", + "attributes": { + "realm_client": "false", + "oidc.ciba.grant.enabled": "false", + "client.secret.creation.time": "1731675236", + "backchannel.logout.session.required": "true", + "oauth2.device.authorization.grant.enabled": "false", + "backchannel.logout.revoke.offline.tokens": "false" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, + "protocolMappers": [ + { + "id": "8a989462-50db-44e0-b3f6-a9a9b67adb6e", + "name": "user-groups", + "protocol": "openid-connect", + "protocolMapper": "oidc-group-membership-mapper", + "consentRequired": false, + "config": { + "full.path": "true", + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "id.token.claim": "true", + "lightweight.claim": "false", + "access.token.claim": "true", + "claim.name": "groups" + } + }, + { + "id": "533de63a-8683-48ff-b2fc-6b6ea56cfa9c", + "name": "Client IP Address", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientAddress", + "id.token.claim": "true", + "introspection.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientAddress", + "jsonType.label": "String" + } + }, + { + "id": "9c9e5ac6-5cd6-42ae-bc95-bdc1c06e04c4", + "name": "Client Host", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientHost", + "id.token.claim": "true", + "introspection.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientHost", + "jsonType.label": "String" + } + }, + { + "id": "b4c7d993-3d5f-499c-8f5b-fea7be1567fb", + "name": "Client ID", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "client_id", + "id.token.claim": "true", + "introspection.token.claim": "true", + "access.token.claim": "true", + "claim.name": "client_id", + "jsonType.label": "String" + } + } + ], + "defaultClientScopes": ["web-origins", "acr", "roles", "profile", "basic", "email"], + "optionalClientScopes": ["address", "phone", "offline_access", "microprofile-jwt"] } ], "clientScopes": [ diff --git a/package-lock.json b/package-lock.json index 9fc723681a..a1ba61f62f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8430,11 +8430,6 @@ "node": ">=14" } }, - "node_modules/@testim/chrome-version": { - "version": "1.1.4", - "license": "MIT", - "optional": true - }, "node_modules/@testing-library/dom": { "version": "10.4.0", "dev": true, @@ -8606,11 +8601,6 @@ "@testing-library/dom": ">=7.21.4" } }, - "node_modules/@tootallnate/quickjs-emscripten": { - "version": "0.23.0", - "license": "MIT", - "optional": true - }, "node_modules/@tsconfig/node10": { "version": "1.0.9", "dev": true, @@ -8757,14 +8747,6 @@ "@types/send": "*" } }, - "node_modules/@types/express-session": { - "version": "1.18.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/express": "*" - } - }, "node_modules/@types/graceful-fs": { "version": "4.1.6", "dev": true, @@ -9040,14 +9022,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/yauzl": { - "version": "2.10.3", - "license": "MIT", - "optional": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "7.18.0", "dev": true, @@ -10100,16 +10074,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/asn1.js": { - "version": "5.4.1", - "license": "MIT", - "dependencies": { - "bn.js": "^4.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0", - "safer-buffer": "^2.1.0" - } - }, "node_modules/assert": { "version": "2.1.0", "dev": true, @@ -10140,17 +10104,6 @@ "repeat-string": "^1.6.1" } }, - "node_modules/ast-types": { - "version": "0.13.4", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.0.1" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/async": { "version": "3.2.4", "license": "MIT" @@ -10620,14 +10573,6 @@ "node": ">= 0.8" } }, - "node_modules/basic-ftp": { - "version": "5.0.5", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/better-opn": { "version": "3.0.2", "dev": true, @@ -10675,6 +10620,7 @@ }, "node_modules/bn.js": { "version": "4.12.0", + "dev": true, "license": "MIT" }, "node_modules/body-parser": { @@ -10749,6 +10695,7 @@ }, "node_modules/brorand": { "version": "1.1.0", + "dev": true, "license": "MIT" }, "node_modules/brotli": { @@ -10968,14 +10915,6 @@ "ieee754": "^1.1.4" } }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "license": "MIT", - "optional": true, - "engines": { - "node": "*" - } - }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "license": "BSD-3-Clause" @@ -11247,27 +11186,6 @@ "node": ">=6.0" } }, - "node_modules/chromedriver": { - "version": "127.0.2", - "hasInstallScript": true, - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@testim/chrome-version": "^1.1.4", - "axios": "^1.6.7", - "compare-versions": "^6.1.0", - "extract-zip": "^2.0.1", - "proxy-agent": "^6.4.0", - "proxy-from-env": "^1.1.0", - "tcp-port-used": "^1.0.2" - }, - "bin": { - "chromedriver": "bin/chromedriver" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/ci-info": { "version": "3.8.0", "dev": true, @@ -11583,11 +11501,6 @@ "dev": true, "license": "MIT" }, - "node_modules/compare-versions": { - "version": "6.1.1", - "license": "MIT", - "optional": true - }, "node_modules/concat-map": { "version": "0.0.1", "license": "MIT" @@ -11677,19 +11590,6 @@ "proto-list": "~1.2.1" } }, - "node_modules/connect-session-sequelize": { - "version": "7.1.7", - "license": "MIT", - "dependencies": { - "debug": "^4.1.1" - }, - "engines": { - "node": ">= 10" - }, - "peerDependencies": { - "sequelize": ">= 6.1.0" - } - }, "node_modules/console-browserify": { "version": "1.2.0", "dev": true @@ -12105,14 +12005,6 @@ "type": "^1.0.1" } }, - "node_modules/data-uri-to-buffer": { - "version": "6.0.2", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 14" - } - }, "node_modules/data-urls": { "version": "5.0.0", "license": "MIT", @@ -12252,7 +12144,7 @@ }, "node_modules/deep-is": { "version": "0.1.4", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/deepmerge": { @@ -12310,19 +12202,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/degenerator": { - "version": "5.0.1", - "license": "MIT", - "optional": true, - "dependencies": { - "ast-types": "^0.13.4", - "escodegen": "^2.1.0", - "esprima": "^4.0.1" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/delayed-stream": { "version": "1.0.0", "license": "MIT", @@ -12757,6 +12636,7 @@ }, "node_modules/elliptic": { "version": "6.5.7", + "dev": true, "license": "MIT", "dependencies": { "bn.js": "^4.11.9", @@ -12802,14 +12682,6 @@ "node": ">= 0.8" } }, - "node_modules/end-of-stream": { - "version": "1.4.4", - "license": "MIT", - "optional": true, - "dependencies": { - "once": "^1.4.0" - } - }, "node_modules/endent": { "version": "2.1.0", "dev": true, @@ -13220,7 +13092,7 @@ }, "node_modules/escodegen": { "version": "2.1.0", - "devOptional": true, + "dev": true, "license": "BSD-2-Clause", "dependencies": { "esprima": "^4.0.1", @@ -13879,7 +13751,7 @@ }, "node_modules/esprima": { "version": "4.0.1", - "devOptional": true, + "dev": true, "license": "BSD-2-Clause", "bin": { "esparse": "bin/esparse.js", @@ -13913,7 +13785,7 @@ }, "node_modules/estraverse": { "version": "5.3.0", - "devOptional": true, + "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=4.0" @@ -13934,7 +13806,7 @@ }, "node_modules/esutils": { "version": "2.0.3", - "devOptional": true, + "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" @@ -14072,56 +13944,6 @@ "version": "1.2.0", "license": "MIT" }, - "node_modules/express-session": { - "version": "1.18.1", - "license": "MIT", - "dependencies": { - "cookie": "0.7.2", - "cookie-signature": "1.0.7", - "debug": "2.6.9", - "depd": "~2.0.0", - "on-headers": "~1.0.2", - "parseurl": "~1.3.3", - "safe-buffer": "5.2.1", - "uid-safe": "~2.1.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/express-session/node_modules/cookie-signature": { - "version": "1.0.7", - "license": "MIT" - }, - "node_modules/express-session/node_modules/debug": { - "version": "2.6.9", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/express-session/node_modules/ms": { - "version": "2.0.0", - "license": "MIT" - }, - "node_modules/express-session/node_modules/safe-buffer": { - "version": "5.2.1", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/express/node_modules/cookie": { "version": "0.7.1", "license": "MIT", @@ -14176,39 +13998,6 @@ "version": "2.7.2", "license": "ISC" }, - "node_modules/extract-zip": { - "version": "2.0.1", - "license": "BSD-2-Clause", - "optional": true, - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, - "node_modules/extract-zip/node_modules/get-stream": { - "version": "5.2.0", - "license": "MIT", - "optional": true, - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "license": "MIT" @@ -14305,14 +14094,6 @@ "bser": "2.1.1" } }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "license": "MIT", - "optional": true, - "dependencies": { - "pend": "~1.2.0" - } - }, "node_modules/fecha": { "version": "4.2.3", "license": "MIT" @@ -14887,33 +14668,6 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, - "node_modules/get-uri": { - "version": "6.0.3", - "license": "MIT", - "optional": true, - "dependencies": { - "basic-ftp": "^5.0.2", - "data-uri-to-buffer": "^6.0.2", - "debug": "^4.3.4", - "fs-extra": "^11.2.0" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/get-uri/node_modules/fs-extra": { - "version": "11.2.0", - "license": "MIT", - "optional": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, "node_modules/git-hooks-list": { "version": "3.1.0", "dev": true, @@ -15197,6 +14951,7 @@ }, "node_modules/hash.js": { "version": "1.1.7", + "dev": true, "license": "MIT", "dependencies": { "inherits": "^2.0.3", @@ -15275,6 +15030,7 @@ }, "node_modules/hmac-drbg": { "version": "1.0.1", + "dev": true, "license": "MIT", "dependencies": { "hash.js": "^1.0.3", @@ -15784,31 +15540,6 @@ "node": ">=6" } }, - "node_modules/ip-address": { - "version": "9.0.5", - "license": "MIT", - "optional": true, - "dependencies": { - "jsbn": "1.1.0", - "sprintf-js": "^1.1.3" - }, - "engines": { - "node": ">= 12" - } - }, - "node_modules/ip-address/node_modules/sprintf-js": { - "version": "1.1.3", - "license": "BSD-3-Clause", - "optional": true - }, - "node_modules/ip-regex": { - "version": "4.3.0", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=8" - } - }, "node_modules/ipaddr.js": { "version": "1.9.1", "license": "MIT", @@ -16333,19 +16064,6 @@ "node": ">=8" } }, - "node_modules/is2": { - "version": "2.0.9", - "license": "MIT", - "optional": true, - "dependencies": { - "deep-is": "^0.1.3", - "ip-regex": "^4.1.0", - "is-url": "^1.2.4" - }, - "engines": { - "node": ">=v0.10.0" - } - }, "node_modules/isarray": { "version": "2.0.5", "dev": true, @@ -17979,11 +17697,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/jsbn": { - "version": "1.1.0", - "license": "MIT", - "optional": true - }, "node_modules/jsdoc-type-pratt-parser": { "version": "4.1.0", "dev": true, @@ -18186,15 +17899,6 @@ "safe-buffer": "^5.0.1" } }, - "node_modules/jwk-to-pem": { - "version": "2.0.5", - "license": "Apache-2.0", - "dependencies": { - "asn1.js": "^5.3.0", - "elliptic": "^6.5.4", - "safe-buffer": "^5.0.1" - } - }, "node_modules/jws": { "version": "3.2.2", "license": "MIT", @@ -18203,19 +17907,6 @@ "safe-buffer": "^5.0.1" } }, - "node_modules/keycloak-connect": { - "version": "23.0.7", - "license": "Apache-2.0", - "dependencies": { - "jwk-to-pem": "^2.0.0" - }, - "engines": { - "node": ">=14" - }, - "optionalDependencies": { - "chromedriver": "latest" - } - }, "node_modules/keyv": { "version": "4.5.4", "dev": true, @@ -19095,10 +18786,12 @@ }, "node_modules/minimalistic-assert": { "version": "1.0.1", + "dev": true, "license": "ISC" }, "node_modules/minimalistic-crypto-utils": { "version": "1.0.1", + "dev": true, "license": "MIT" }, "node_modules/minimatch": { @@ -19320,14 +19013,6 @@ "dev": true, "license": "MIT" }, - "node_modules/netmask": { - "version": "2.0.2", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/neverthrow": { "version": "6.2.2", "license": "MIT" @@ -20184,36 +19869,6 @@ "node": ">=6" } }, - "node_modules/pac-proxy-agent": { - "version": "7.0.2", - "license": "MIT", - "optional": true, - "dependencies": { - "@tootallnate/quickjs-emscripten": "^0.23.0", - "agent-base": "^7.0.2", - "debug": "^4.3.4", - "get-uri": "^6.0.1", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.5", - "pac-resolver": "^7.0.1", - "socks-proxy-agent": "^8.0.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/pac-resolver": { - "version": "7.0.1", - "license": "MIT", - "optional": true, - "dependencies": { - "degenerator": "^5.0.0", - "netmask": "^2.0.2" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/package-json-from-dist": { "version": "1.0.0", "dev": true, @@ -20465,11 +20120,6 @@ "node": ">=0.12" } }, - "node_modules/pend": { - "version": "1.2.0", - "license": "MIT", - "optional": true - }, "node_modules/pg": { "version": "8.13.0", "license": "MIT", @@ -21274,32 +20924,6 @@ "node": ">= 0.10" } }, - "node_modules/proxy-agent": { - "version": "6.4.0", - "license": "MIT", - "optional": true, - "dependencies": { - "agent-base": "^7.0.2", - "debug": "^4.3.4", - "http-proxy-agent": "^7.0.1", - "https-proxy-agent": "^7.0.3", - "lru-cache": "^7.14.1", - "pac-proxy-agent": "^7.0.1", - "proxy-from-env": "^1.1.0", - "socks-proxy-agent": "^8.0.2" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/proxy-agent/node_modules/lru-cache": { - "version": "7.18.3", - "license": "ISC", - "optional": true, - "engines": { - "node": ">=12" - } - }, "node_modules/proxy-from-env": { "version": "1.1.0", "license": "MIT" @@ -21317,15 +20941,6 @@ "safe-buffer": "^5.1.2" } }, - "node_modules/pump": { - "version": "3.0.0", - "license": "MIT", - "optional": true, - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, "node_modules/punycode": { "version": "2.3.1", "license": "MIT", @@ -21426,13 +21041,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/random-bytes": { - "version": "1.0.0", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/randombytes": { "version": "2.1.0", "dev": true, @@ -22918,15 +22526,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, "node_modules/snake-case": { "version": "3.0.4", "dev": true, @@ -22936,32 +22535,6 @@ "tslib": "^2.0.3" } }, - "node_modules/socks": { - "version": "2.8.3", - "license": "MIT", - "optional": true, - "dependencies": { - "ip-address": "^9.0.5", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks-proxy-agent": { - "version": "8.0.4", - "license": "MIT", - "optional": true, - "dependencies": { - "agent-base": "^7.1.1", - "debug": "^4.3.4", - "socks": "^2.8.3" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/sort-object-keys": { "version": "1.1.3", "dev": true, @@ -23662,31 +23235,6 @@ "node": ">=6" } }, - "node_modules/tcp-port-used": { - "version": "1.0.2", - "license": "MIT", - "optional": true, - "dependencies": { - "debug": "4.3.1", - "is2": "^2.0.6" - } - }, - "node_modules/tcp-port-used/node_modules/debug": { - "version": "4.3.1", - "license": "MIT", - "optional": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/telejson": { "version": "7.2.0", "dev": true, @@ -24701,16 +24249,6 @@ } } }, - "node_modules/uid-safe": { - "version": "2.1.5", - "license": "MIT", - "dependencies": { - "random-bytes": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/umzug": { "version": "2.3.0", "license": "MIT", @@ -25690,15 +25228,6 @@ "node": ">=12" } }, - "node_modules/yauzl": { - "version": "2.10.0", - "license": "MIT", - "optional": true, - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - }, "node_modules/yn": { "version": "3.1.1", "dev": true, @@ -25829,7 +25358,6 @@ "@potentiel-infrastructure/pg-event-sourcing": "*", "@react-pdf/renderer": "^3.4.5", "@sentry/node": "^7.119.2", - "connect-session-sequelize": "^7.1.7", "cookie-parser": "^1.4.7", "csv-parse": "^5.5.6", "date-fns": "^2.30.0", @@ -25838,14 +25366,12 @@ "esbuild": "^0.20.1", "express": "^4.21.1", "express-async-handler": "^1.2.0", - "express-session": "^1.18.1", "helmet": "^6.2.0", "iconv-lite": "^0.6.3", "ioredis": "^4.28.5", "isemail": "^3.2.0", "json2csv": "^5.0.7", "jsonwebtoken": "^9.0.2", - "keycloak-connect": "^23.0.4", "mediateur": "^0.1.3", "mime-types": "^2.1.35", "moment": "^2.30.1", @@ -25875,7 +25401,6 @@ "@fullhuman/postcss-purgecss": "^5.0.0", "@jest/globals": "^29.6.4", "@types/express": "^4.17.17", - "@types/express-session": "^1.18.0", "@types/ioredis": "^4.28.10", "@types/mime-types": "^2.1.4", "@types/morgan": "^1.9.9", diff --git a/packages/applications/legacy/package.json b/packages/applications/legacy/package.json index f2be47acab..c0a3b1eb7b 100644 --- a/packages/applications/legacy/package.json +++ b/packages/applications/legacy/package.json @@ -33,7 +33,6 @@ "@potentiel-infrastructure/pg-event-sourcing": "*", "@react-pdf/renderer": "^3.4.5", "@sentry/node": "^7.119.2", - "connect-session-sequelize": "^7.1.7", "cookie-parser": "^1.4.7", "csv-parse": "^5.5.6", "date-fns": "^2.30.0", @@ -42,14 +41,12 @@ "esbuild": "^0.20.1", "express": "^4.21.1", "express-async-handler": "^1.2.0", - "express-session": "^1.18.1", "helmet": "^6.2.0", "iconv-lite": "^0.6.3", "ioredis": "^4.28.5", "isemail": "^3.2.0", "json2csv": "^5.0.7", "jsonwebtoken": "^9.0.2", - "keycloak-connect": "^23.0.4", "mediateur": "^0.1.3", "mime-types": "^2.1.35", "moment": "^2.30.1", @@ -79,7 +76,6 @@ "@fullhuman/postcss-purgecss": "^5.0.0", "@jest/globals": "^29.6.4", "@types/express": "^4.17.17", - "@types/express-session": "^1.18.0", "@types/ioredis": "^4.28.10", "@types/mime-types": "^2.1.4", "@types/morgan": "^1.9.9", diff --git a/packages/applications/legacy/src/config/authN.config.ts b/packages/applications/legacy/src/config/authN.config.ts index 954e5c6703..44c4cd2fad 100644 --- a/packages/applications/legacy/src/config/authN.config.ts +++ b/packages/applications/legacy/src/config/authN.config.ts @@ -1,26 +1,9 @@ import { makeKeycloakAuth } from '../infra/keycloak'; -import { sequelizeInstance } from '../sequelize.config'; import { getUserByEmail } from './queries.config'; import { createUser } from './useCases.config'; const getKeycloakAuth = () => { - const { - KEYCLOAK_SERVER, - KEYCLOAK_REALM, - KEYCLOAK_USER_CLIENT_ID, - KEYCLOAK_USER_CLIENT_SECRET, - NEXT_AUTH_SESSION_TOKEN_COOKIE_NAME, - } = process.env; - - console.log(`Authentication through Keycloak server ${KEYCLOAK_SERVER}`); - return makeKeycloakAuth({ - sequelizeInstance, - KEYCLOAK_SERVER, - KEYCLOAK_REALM, - KEYCLOAK_USER_CLIENT_ID, - KEYCLOAK_USER_CLIENT_SECRET, - NEXT_AUTH_SESSION_TOKEN_COOKIE_NAME, getUserByEmail, createUser, }); diff --git a/packages/applications/legacy/src/controllers/admin/inviterDgecValidateur/getInviterDgecValidateurPage.ts b/packages/applications/legacy/src/controllers/admin/inviterDgecValidateur/getInviterDgecValidateurPage.ts index 2fdc1a06de..5f700a57f4 100644 --- a/packages/applications/legacy/src/controllers/admin/inviterDgecValidateur/getInviterDgecValidateurPage.ts +++ b/packages/applications/legacy/src/controllers/admin/inviterDgecValidateur/getInviterDgecValidateurPage.ts @@ -4,22 +4,17 @@ import { v1Router } from '../../v1Router'; import { vérifierPermissionUtilisateur } from '../../helpers'; import { InviterDgecValidateurPage } from '../../../views'; import { PermissionInviterDgecValidateur } from '../../../modules/utilisateur'; -import { getApiResult } from '../../helpers/apiResult'; v1Router.get( routes.ADMIN_INVITATION_DGEC_VALIDATEUR, vérifierPermissionUtilisateur(PermissionInviterDgecValidateur), asyncHandler(async (request, response) => { - const result = getApiResult(request, routes.ADMIN_INVITATION_DGEC_VALIDATEUR_ACTION); + const error = request.query.error; return response.send( InviterDgecValidateurPage({ request, - inviationRéussi: result?.status === 'OK' ? true : undefined, - formErrors: - result?.status === 'BAD_REQUEST' - ? (result.formErrors as Record) - : undefined, + inviationRéussi: !error || undefined, }), ); }), diff --git a/packages/applications/legacy/src/controllers/admin/inviterDgecValidateur/postInviterDgecValidateur.ts b/packages/applications/legacy/src/controllers/admin/inviterDgecValidateur/postInviterDgecValidateur.ts index 0eb9a28ca5..d2ea017f3b 100644 --- a/packages/applications/legacy/src/controllers/admin/inviterDgecValidateur/postInviterDgecValidateur.ts +++ b/packages/applications/legacy/src/controllers/admin/inviterDgecValidateur/postInviterDgecValidateur.ts @@ -7,14 +7,9 @@ import { vérifierPermissionUtilisateur, } from '../../helpers'; import { inviterUtilisateur } from '../../../config'; -import { - InvitationUniqueParUtilisateurError, - InvitationUtilisateurExistantError, - PermissionInviterDgecValidateur, -} from '../../../modules/utilisateur'; +import { PermissionInviterDgecValidateur } from '../../../modules/utilisateur'; import { logger } from '../../../core/utils'; import asyncHandler from '../../helpers/asyncHandler'; -import { setApiResult } from '../../helpers/apiResult'; const schema = yup.object({ role: yup @@ -41,46 +36,23 @@ v1Router.post( ) .match( () => { - setApiResult(request, { - route: routes.ADMIN_INVITATION_DGEC_VALIDATEUR_ACTION, - status: 'OK', - }); return response.redirect(routes.ADMIN_INVITATION_DGEC_VALIDATEUR); }, (error: Error) => { if (error instanceof RequestValidationError) { - setApiResult(request, { - route: routes.ADMIN_INVITATION_DGEC_VALIDATEUR_ACTION, - status: 'BAD_REQUEST', - message: 'Le formulaire contient des erreurs', - formErrors: Object.entries(error.errors).reduce((prev, [key, value]) => { - return { - ...prev, - [key.replace('error-', '')]: value, - }; - }, {}), - }); - return response.redirect(routes.ADMIN_INVITATION_DGEC_VALIDATEUR); - } - if ( - error instanceof InvitationUniqueParUtilisateurError || - error instanceof InvitationUtilisateurExistantError - ) { - setApiResult(request, { - route: routes.ADMIN_INVITATION_DGEC_VALIDATEUR_ACTION, - status: 'BAD_REQUEST', - message: error.message, - }); - return response.redirect(routes.ADMIN_INVITATION_DGEC_VALIDATEUR); + return response.redirect( + routes.ADMIN_INVITATION_DGEC_VALIDATEUR + + '?' + + new URLSearchParams({ error: 'Le formulaire contient des erreurs' }).toString(), + ); } logger.error(error); - setApiResult(request, { - route: routes.ADMIN_INVITATION_DGEC_VALIDATEUR_ACTION, - status: 'BAD_REQUEST', - message: - 'Il y a eu une erreur lors de la soumission de votre demande. Merci de recommencer.', - }); - return response.redirect(routes.ADMIN_INVITATION_DGEC_VALIDATEUR); + + return response.redirect( + routes.ADMIN_INVITATION_DGEC_VALIDATEUR + + `?` + + new URLSearchParams({ error: "Impossible d'inviter l'utilisateur" }).toString(), + ); }, ); }), diff --git a/packages/applications/legacy/src/controllers/helpers/apiResult.ts b/packages/applications/legacy/src/controllers/helpers/apiResult.ts deleted file mode 100644 index fd03caaa20..0000000000 --- a/packages/applications/legacy/src/controllers/helpers/apiResult.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Request } from 'express'; - -export type FormErrors = Record; - -export type ApiResult = { - route: string; -} & ( - | { - status: 'OK'; - result?: TResult; - } - | { - status: 'BAD_REQUEST'; - message: string; - formErrors?: FormErrors; - } -); - -export const setApiResult = ( - request: Request, - { route, ...result }: ApiResult, -): void => { - request.session.apiResults = { - ...request.session.apiResults, - [route]: result, - }; -}; - -export const getApiResult = ( - request: Request, - route: string, -): ApiResult | undefined => { - const apiResults = request.session.apiResults; - - if (apiResults) { - const { [route]: apiResult, ...clearedApiResults } = apiResults; - - request.session.apiResults = clearedApiResults; - - return apiResult as ApiResult; - } -}; diff --git "a/packages/applications/legacy/src/controllers/helpers/v\303\251rifierPermissionUtilisateur.ts" "b/packages/applications/legacy/src/controllers/helpers/v\303\251rifierPermissionUtilisateur.ts" index 85b5dbdc22..738f7b9ccb 100644 --- "a/packages/applications/legacy/src/controllers/helpers/v\303\251rifierPermissionUtilisateur.ts" +++ "b/packages/applications/legacy/src/controllers/helpers/v\303\251rifierPermissionUtilisateur.ts" @@ -1,14 +1,14 @@ import { RequestHandler } from 'express'; import { Permission } from '../../modules/authN'; -import routes from '../../routes'; import { AccèsNonAutoriséPage } from '../../views'; +import { Routes } from '@potentiel-applications/routes'; export const vérifierPermissionUtilisateur = (permission: Permission): RequestHandler => (request, response, next) => { const { user } = request; if (!user) { - response.redirect(routes.LOGIN); + response.redirect(Routes.Auth.signIn()); return; } diff --git a/packages/applications/legacy/src/controllers/index.ts b/packages/applications/legacy/src/controllers/index.ts index 75d5e9f9c3..7bc2d54d4f 100644 --- a/packages/applications/legacy/src/controllers/index.ts +++ b/packages/applications/legacy/src/controllers/index.ts @@ -8,6 +8,5 @@ export * from './tableauxDeBord'; export * from './userAccount'; export * from './getDéclarationAccessibilitéPage'; export * from './getSuccèsPage'; -export * from './signout'; export * from './upload'; export * from './v1Router'; diff --git a/packages/applications/legacy/src/controllers/signout.ts b/packages/applications/legacy/src/controllers/signout.ts deleted file mode 100644 index a7723ff01b..0000000000 --- a/packages/applications/legacy/src/controllers/signout.ts +++ /dev/null @@ -1,10 +0,0 @@ -import asyncHandler from './helpers/asyncHandler'; -import routes from '../routes'; -import { v1Router } from './v1Router'; - -v1Router.get( - routes.LOGOUT_ACTION, - asyncHandler(async (request, response) => { - response.redirect('/auth/signOut'); - }), -); diff --git a/packages/applications/legacy/src/controllers/userAccount/getSignupPage.ts b/packages/applications/legacy/src/controllers/userAccount/getSignupPage.ts index aa24467530..8ed333154e 100644 --- a/packages/applications/legacy/src/controllers/userAccount/getSignupPage.ts +++ b/packages/applications/legacy/src/controllers/userAccount/getSignupPage.ts @@ -2,6 +2,7 @@ import asyncHandler from '../helpers/asyncHandler'; import routes from '../../routes'; import { v1Router } from '../v1Router'; import { SignupPage } from '../../views'; +import { Routes } from '@potentiel-applications/routes'; v1Router.get( routes.SIGNUP, @@ -9,7 +10,7 @@ v1Router.get( const { user, query } = request; if (user) { - return response.redirect(routes.REDIRECT_BASED_ON_ROLE); + return response.redirect(Routes.Auth.redirectToDashboard()); } const validationErrors: Array<{ [fieldName: string]: string }> = Object.entries(query).reduce( diff --git a/packages/applications/legacy/src/index.ts b/packages/applications/legacy/src/index.ts index 92cce7d7d5..bbd8c04e31 100644 --- a/packages/applications/legacy/src/index.ts +++ b/packages/applications/legacy/src/index.ts @@ -11,11 +11,5 @@ mandatoryVariables.forEach((variable) => { } }); -const sessionSecret = process.env.SESSION_SECRET; -if (!sessionSecret) { - console.error('Missing SESSION_SECRET environment variable'); - process.exit(1); -} - const port: number = Number(process.env.PORT) || 3000; -makeServer(port, sessionSecret); +makeServer(port); diff --git a/packages/applications/legacy/src/infra/keycloak/attachUserToRequestMiddleware.spec.ts b/packages/applications/legacy/src/infra/keycloak/attachUserToRequestMiddleware.spec.ts index b3baf2efbd..e647174c80 100644 --- a/packages/applications/legacy/src/infra/keycloak/attachUserToRequestMiddleware.spec.ts +++ b/packages/applications/legacy/src/infra/keycloak/attachUserToRequestMiddleware.spec.ts @@ -5,6 +5,8 @@ import { User } from '../../entities'; import { GetUserByEmail, UserRole } from '../../modules/users'; import { makeFakeCreateUser } from '../../__tests__/fakes'; import { makeAttachUserToRequestMiddleware } from './attachUserToRequestMiddleware'; +import { IdentifiantUtilisateur, Role, Utilisateur } from '@potentiel-domain/utilisateur'; +import { Option } from '@potentiel-libraries/monads'; describe(`attachUserToRequestMiddleware`, () => { const staticPaths = ['/fonts', '/css', '/images', '/scripts', '/main']; @@ -14,43 +16,43 @@ describe(`attachUserToRequestMiddleware`, () => { path, } as express.Request; const nextFunction = jest.fn(); + const getUtilisateur = jest.fn(() => Promise.resolve(undefined)); const middleware = makeAttachUserToRequestMiddleware({ getUserByEmail: jest.fn(), createUser: makeFakeCreateUser(), + getUtilisateur, }); middleware(request, {} as express.Response, nextFunction); it('should not attach the user to the request and execute the next function', () => { expect(request.user).toBeUndefined(); + expect(getUtilisateur).not.toHaveBeenCalled(); expect(nextFunction).toHaveBeenCalled(); }); }); }); describe(`when the path is not a static one`, () => { + const request = { path: '/a-protected-path' } as express.Request; + const makeFakeGetUtilisateur = (role: string, username: string) => async () => { + return Utilisateur.bind({ + groupe: Option.none, + identifiantUtilisateur: IdentifiantUtilisateur.convertirEnValueType(username), + nom: '', + role: Role.convertirEnValueType(role), + }); + }; describe(`when there is no user email in the keycloak access token`, () => { - const hasRealmRole = jest.fn(); - - const request = { - path: '/a-protected-path', - } as express.Request; - const token = { - content: {}, - hasRealmRole, - }; - request['kauth'] = { grant: { access_token: token } }; - - token.content['email'] = undefined; - const nextFunction = jest.fn(); - const middleware = makeAttachUserToRequestMiddleware({ getUserByEmail: jest.fn(), createUser: makeFakeCreateUser(), + getUtilisateur: makeFakeGetUtilisateur('admin', ''), }); - middleware(request, {} as express.Response, nextFunction); - it('should not attach the user to the request and execute the next function', () => { + it('should not attach the user to the request and execute the next function', async () => { + const nextFunction = jest.fn(); + await middleware(request, {} as express.Response, nextFunction); expect(request.user).toBeUndefined(); expect(nextFunction).toHaveBeenCalled(); }); @@ -59,25 +61,10 @@ describe(`attachUserToRequestMiddleware`, () => { describe(`when there is a user email in the keycloak access token`, () => { describe(`when the user exists in Potentiel`, () => { describe(`when no role in the keycloak access token`, () => { - const hasRealmRole = jest.fn((role) => false); - - const request = { - path: '/a-protected-path', - } as express.Request; - - const token = { - content: {}, - hasRealmRole, - }; - request['kauth'] = { grant: { access_token: token } }; - const userEmail = 'user@email.com'; - token.content['email'] = userEmail; - const user: User = { email: userEmail, - fullName: 'User', id: 'user-id', role: undefined as unknown as UserRole, }; @@ -91,39 +78,23 @@ describe(`attachUserToRequestMiddleware`, () => { const middleware = makeAttachUserToRequestMiddleware({ getUserByEmail, createUser: makeFakeCreateUser(), + getUtilisateur: makeFakeGetUtilisateur('', userEmail), }); - middleware(request, {} as express.Response, nextFunction); - it('should attach the user to the request with no role and execute the next function', () => { - expect(request.user).toMatchObject({ - ...user, - accountUrl: expect.any(String), - }); + it('should not attach the user to the request and execute the next function', async () => { + await middleware(request, {} as express.Response, nextFunction); + expect(request.user).toBeUndefined(); expect(nextFunction).toHaveBeenCalled(); }); }); describe(`when there is a role in the keycloak access token`, () => { const tokenUserRole = 'admin'; - const hasRealmRole = jest.fn((role) => (role === tokenUserRole ? true : false)); - - const request = { - path: '/a-protected-path', - } as express.Request; - - const token = { - content: {}, - hasRealmRole, - }; - request['kauth'] = { grant: { access_token: token } }; const userEmail = 'user@email.com'; - token.content['email'] = userEmail; - const user: User = { email: userEmail, - fullName: 'User', id: 'user-id', role: 'porteur-projet', }; @@ -137,19 +108,18 @@ describe(`attachUserToRequestMiddleware`, () => { const middleware = makeAttachUserToRequestMiddleware({ getUserByEmail, createUser: makeFakeCreateUser(), + getUtilisateur: makeFakeGetUtilisateur(tokenUserRole, userEmail), }); - middleware(request, {} as express.Response, nextFunction); - it('should attach the user to the request with role from token', () => { + it('should attach the user to the request with role from token', async () => { const expectedUser = { ...user, role: tokenUserRole, accountUrl: expect.any(String), }; + await middleware(request, {} as express.Response, nextFunction); expect(request.user).toMatchObject(expectedUser); - }); - it('should execute the next function', () => { expect(nextFunction).toHaveBeenCalled(); }); }); @@ -157,26 +127,8 @@ describe(`attachUserToRequestMiddleware`, () => { describe(`when the user does not exist in Potentiel`, () => { describe(`when no role in the keycloak access token`, () => { - const hasRealmRole = jest.fn(() => false); - - const request = { - path: '/a-protected-path', - session: {}, - } as express.Request; - //@ts-ignore - request.session.destroy = jest.fn(); - - const token = { - content: {}, - hasRealmRole, - }; - request['kauth'] = { grant: { access_token: token } }; - const userEmail = 'user@email.com'; - const userName = 'User'; - token.content['email'] = userEmail; - token.content['name'] = userName; const getUserByEmail: GetUserByEmail = jest.fn(() => okAsync(null)); const userId = 'user-id'; @@ -187,48 +139,28 @@ describe(`attachUserToRequestMiddleware`, () => { const middleware = makeAttachUserToRequestMiddleware({ getUserByEmail, createUser, + getUtilisateur: makeFakeGetUtilisateur('porteur-projet', userEmail), }); - middleware(request, {} as express.Response, nextFunction); - it('should attach a new user to the request', () => { + it('should attach a new user to the request', async () => { + await middleware(request, {} as express.Response, nextFunction); expect(request.user).toMatchObject({ email: userEmail, - fullName: userName, id: userId, role: 'porteur-projet', accountUrl: expect.any(String), permissions: expect.anything(), }); - }); - - it('should destroy the request session', () => { - expect(request.session.destroy).toHaveBeenCalled(); - }); - it('should execute the next function', () => { expect(nextFunction).toHaveBeenCalled(); }); }); describe(`when there is a role in the keycloak access token`, () => { const userRole = 'admin'; - const hasRealmRole = jest.fn((role) => (role === userRole ? true : false)); - - const request = { - path: '/a-protected-path', - } as express.Request; - - const token = { - content: {}, - hasRealmRole, - }; - request['kauth'] = { grant: { access_token: token } }; const userEmail = 'user@email.com'; - const userName = 'User'; - token.content['email'] = userEmail; - token.content['name'] = userName; const getUserByEmail: GetUserByEmail = jest.fn(() => okAsync(null)); const userId = 'user-id'; @@ -239,21 +171,19 @@ describe(`attachUserToRequestMiddleware`, () => { const middleware = makeAttachUserToRequestMiddleware({ getUserByEmail, createUser, + getUtilisateur: makeFakeGetUtilisateur(userRole, userEmail), }); - middleware(request, {} as express.Response, nextFunction); - it('should attach a new user to the request with the same role of the token', () => { + it('should attach a new user to the request with the same role of the token', async () => { + await middleware(request, {} as express.Response, nextFunction); expect(request.user).toMatchObject({ email: userEmail, - fullName: userName, id: userId, role: userRole, accountUrl: expect.any(String), permissions: expect.anything(), }); - }); - it('should execute the next function', () => { expect(nextFunction).toHaveBeenCalled(); }); }); diff --git a/packages/applications/legacy/src/infra/keycloak/attachUserToRequestMiddleware.ts b/packages/applications/legacy/src/infra/keycloak/attachUserToRequestMiddleware.ts index ae0ee85559..c84b6a9c36 100644 --- a/packages/applications/legacy/src/infra/keycloak/attachUserToRequestMiddleware.ts +++ b/packages/applications/legacy/src/infra/keycloak/attachUserToRequestMiddleware.ts @@ -1,15 +1,59 @@ import { NextFunction, Request, Response } from 'express'; -import { logger, ok } from '../../core/utils'; -import { CreateUser, GetUserByEmail, USER_ROLES } from '../../modules/users'; -import { getPermissions } from '../../modules/authN'; +import { logger, ResultAsync } from '../../core/utils'; +import { CreateUser, GetUserByEmail } from '../../modules/users'; +import { getPermissions, Permission } from '../../modules/authN'; +import { Role, Utilisateur } from '@potentiel-domain/utilisateur'; +import { getToken, GetTokenParams } from 'next-auth/jwt'; +import { PlainType } from '@potentiel-domain/core'; type AttachUserToRequestMiddlewareDependencies = { getUserByEmail: GetUserByEmail; createUser: CreateUser; + getUtilisateur?: (req: Request) => Promise; }; +// The token is generated by next-auth on the SSR app +declare module 'next-auth/jwt' { + interface JWT { + idToken?: string; + utilisateur?: PlainType; + } +} + +declare module 'express-serve-static-core' { + interface Request { + user: { + email: string; + role: Role.RawType; + fullName: string; + id: string; + accountUrl: string; + permissions: Permission[]; + }; + } +} + +const getNextAuthUtilisateur = async (req: Request) => { + try { + const token = await getToken({ + req: { cookies: req.cookies } as unknown as GetTokenParams['req'], + }); + return token?.utilisateur && Utilisateur.bind(token.utilisateur); + } catch (e) { + logger.error('getToken failed'); + logger.error(e); + } +}; + +const promisify = (resultAsync: ResultAsync) => + new Promise((resolve, reject) => resultAsync.match(resolve, reject)); + const makeAttachUserToRequestMiddleware = - ({ getUserByEmail, createUser }: AttachUserToRequestMiddlewareDependencies) => + ({ + getUserByEmail, + createUser, + getUtilisateur = getNextAuthUtilisateur, + }: AttachUserToRequestMiddlewareDependencies) => async (request: Request, response: Response, next: NextFunction) => { if ( // Theses paths should be prefixed with /static in the future @@ -17,52 +61,45 @@ const makeAttachUserToRequestMiddleware = request.path.startsWith('/css') || request.path.startsWith('/images') || request.path.startsWith('/scripts') || - request.path.startsWith('/main') + request.path.startsWith('/main') || + request.path.startsWith('/illustrations') ) { next(); return; } - const token = request.kauth?.grant?.access_token; - - const userEmail = token?.content?.email; - const kRole = USER_ROLES.find((role) => token?.hasRealmRole(role)); + try { + const utilisateur = await getUtilisateur(request); + if (!utilisateur) { + next(); + return; + } - if (userEmail) { - await getUserByEmail(userEmail) - .andThen((user) => { - if (user) { - return ok({ - ...user, - role: kRole!, - }); - } + const { + identifiantUtilisateur: { email }, + role: { nom: role }, + nom: fullName, + } = utilisateur; - const fullName = token?.content?.name; - const createUserArgs = { email: userEmail, role: kRole, fullName }; + const getOrCreateUser = async () => { + const createUserArgs = { email, role, fullName }; + const user = await promisify(getUserByEmail(email)); + if (user) return { id: user.id, ...createUserArgs }; - return createUser(createUserArgs).andThen(({ id, role }) => { - if (!kRole) { - request.session.destroy(() => {}); - } + const { id } = await promisify(createUser(createUserArgs)); + return { id, ...createUserArgs }; + }; + const user = await getOrCreateUser(); - return ok({ ...createUserArgs, id, role }); - }); - }) - .match( - (user) => { - request.user = { - ...user, - accountUrl: `${process.env.KEYCLOAK_SERVER}/realms/${process.env.KEYCLOAK_REALM}/account`, - permissions: getPermissions(user), - }; - }, - (e: Error) => { - logger.error(e); - }, - ); + request.user = { + ...user, + accountUrl: `${process.env.KEYCLOAK_SERVER}/realms/${process.env.KEYCLOAK_REALM}/account`, + permissions: getPermissions(user), + }; + } catch (e) { + logger.error('Auth failed'); + logger.error(e); } - next(); }; diff --git a/packages/applications/legacy/src/infra/keycloak/createUserCredentials.ts b/packages/applications/legacy/src/infra/keycloak/createUserCredentials.ts index 01da6978c8..d0fc499d74 100644 --- a/packages/applications/legacy/src/infra/keycloak/createUserCredentials.ts +++ b/packages/applications/legacy/src/infra/keycloak/createUserCredentials.ts @@ -3,8 +3,8 @@ import { authorizedTestEmails, isProdEnv } from '../../config'; import { logger, ResultAsync } from '../../core/utils'; import { CreateUserCredentials } from '../../modules/authN'; import { OtherError, UnauthorizedError } from '../../modules/shared'; -import routes from '../../routes'; import { makeKeycloakClient } from './keycloakClient'; +import { Routes } from '@potentiel-applications/routes'; const ONE_MONTH = 3600 * 24 * 30; @@ -61,7 +61,7 @@ export const createUserCredentials: CreateUserCredentials = (args) => { clientId: KEYCLOAK_USER_CLIENT_ID, actions, realm: KEYCLOAK_REALM, - redirectUri: BASE_URL + routes.REDIRECT_BASED_ON_ROLE, + redirectUri: BASE_URL + Routes.Auth.redirectToDashboard(), lifespan: ONE_MONTH, }); } else { diff --git a/packages/applications/legacy/src/infra/keycloak/makeKeycloakAuth.ts b/packages/applications/legacy/src/infra/keycloak/makeKeycloakAuth.ts index 1f1dfc7862..52f44b0dca 100644 --- a/packages/applications/legacy/src/infra/keycloak/makeKeycloakAuth.ts +++ b/packages/applications/legacy/src/infra/keycloak/makeKeycloakAuth.ts @@ -1,177 +1,53 @@ -import makeSequelizeStore from 'connect-session-sequelize'; -import session from 'express-session'; -import Keycloak from 'keycloak-connect'; -import QueryString from 'querystring'; -import { User } from '../../entities'; +import { Routes } from '@potentiel-applications/routes'; import { EnsureRole, RegisterAuth } from '../../modules/authN'; import { CreateUser, GetUserByEmail } from '../../modules/users'; -import routes from '../../routes'; + import { makeAttachUserToRequestMiddleware } from './attachUserToRequestMiddleware'; -import { miseAJourStatistiquesUtilisation } from '../../controllers/helpers'; -import { isLocalEnv } from '../../config'; import { getLogger } from '@potentiel-libraries/monitoring'; -import { Routes } from '@potentiel-applications/routes'; +import { RequestHandler } from 'express'; export interface KeycloakAuthDeps { - sequelizeInstance: any; - KEYCLOAK_SERVER: string | undefined; - KEYCLOAK_REALM: string | undefined; - KEYCLOAK_USER_CLIENT_ID: string | undefined; - KEYCLOAK_USER_CLIENT_SECRET: string | undefined; - NEXT_AUTH_SESSION_TOKEN_COOKIE_NAME: string | undefined; getUserByEmail: GetUserByEmail; createUser: CreateUser; } export const makeKeycloakAuth = (deps: KeycloakAuthDeps) => { - const { - sequelizeInstance, - KEYCLOAK_SERVER, - KEYCLOAK_REALM, - KEYCLOAK_USER_CLIENT_ID, - KEYCLOAK_USER_CLIENT_SECRET, - NEXT_AUTH_SESSION_TOKEN_COOKIE_NAME, - getUserByEmail, - createUser, - } = deps; - - if ( - !KEYCLOAK_SERVER || - !KEYCLOAK_REALM || - !KEYCLOAK_USER_CLIENT_ID || - !KEYCLOAK_USER_CLIENT_SECRET - ) { - console.error('Missing KEYCLOAK env vars'); - process.exit(1); - } + const { getUserByEmail, createUser } = deps; - if (!NEXT_AUTH_SESSION_TOKEN_COOKIE_NAME) { - console.error('Missing NEXT_AUTH_SESSION_TOKEN_COOKIE_NAME env var'); - process.exit(1); - } - const SequelizeStore = makeSequelizeStore(session.Store); - - const store = new SequelizeStore({ - db: sequelizeInstance, - tableName: 'sessions', - checkExpirationInterval: 15 * 60 * 1000, // 15 minutes - expiration: 24 * 60 * 60 * 1000, // 1 day - }); - - const keycloak = new Keycloak( - { - store, - }, - { - 'confidential-port': 0, - 'auth-server-url': KEYCLOAK_SERVER, - resource: KEYCLOAK_USER_CLIENT_ID, - 'ssl-required': 'external', - 'bearer-only': false, - realm: KEYCLOAK_REALM, - // @ts-ignore - credentials: { - secret: KEYCLOAK_USER_CLIENT_SECRET, - }, - }, - ); + const protectRoute: RequestHandler = async (req, res, next) => { + if (req.user) { + return next(); + } + return res.redirect(Routes.Auth.signIn(req.path)); + }; const ensureRole: EnsureRole = (roles) => { const roleList = Array.isArray(roles) ? roles : [roles]; - return keycloak.protect((token) => { - return roleList.some((role) => token.hasRealmRole(role)); - }); + const logger = getLogger('KeycloakAuthLegacy'); + return (req, res, next) => + protectRoute(req, res, () => { + if (!req.user) { + logger.warn('no user found'); + res.status(403); + res.end('Access denied'); + } + if (!roleList.includes(req.user.role)) { + logger.warn(`Role missing`, { user: req.user, roleList }); + res.status(403); + res.end('Access denied'); + } + return next(); + }); }; - const registerAuth: RegisterAuth = ({ app, sessionSecret, router }) => { - app.use( - session({ - secret: sessionSecret, - store, - resave: false, - proxy: true, - saveUninitialized: false, - ...(!isLocalEnv && { - cookie: { - secure: true, - }, - }), - }), - ); - - app.use(keycloak.middleware()); - + const registerAuth: RegisterAuth = ({ app }) => { app.use( makeAttachUserToRequestMiddleware({ getUserByEmail, createUser, }), ); - - router.get(routes.LOGIN, keycloak.protect(), (req, res) => { - res.redirect(routes.REDIRECT_BASED_ON_ROLE); - }); - - router.get(routes.REDIRECT_BASED_ON_ROLE, keycloak.protect(), async (req, res) => { - const user = req.user as User; - - if (!user) { - // Sometimes, the user session is not immediately available in the req object - // In that case, wait a bit and redirect to the same url - - // @ts-ignore - if (req.kauth && Object.keys(req.kauth).length) { - getLogger().error(new Error(`Got a valid auth token but no user associated !`), { - token: req.kauth?.grant?.access_token?.content, - }); - res.redirect(routes.LOGOUT_ACTION); - return; - } - - // Use a retry counter to avoid infinite loop - const retryCount = Number(req.query.retry || 0); - if (retryCount > 5) { - // Too many retries - return res.redirect('/'); - } - setTimeout(() => { - res.redirect(`${routes.REDIRECT_BASED_ON_ROLE}?retry=${retryCount + 1}`); - }, 200); - return; - } - - miseAJourStatistiquesUtilisation({ - type: 'connexionUtilisateur', - données: { - utilisateur: { - role: req.user.role, - }, - }, - }); - - // @ts-ignore - const queryString = QueryString.stringify(req.query); - - /** - * @todo Code à revoir quand on aura basculé toute l'app sur Next - * - * Le code ci-dessous fait les actiosn suivantes : - * - Si j'ai un cookie qui a le nom de la variable d'env NEXT_AUTH_SESSION_TOKEN_COOKIE_NAME, alors je suis déjà authentifié sur l'app next, - * alors je retourne directement la liste des projets - * - Sinon je m'authentifie déjà sur next, puis je suis redirigé sur la liste des projets - * - */ - const redirectTo = - req.user.role === 'grd' - ? Routes.Raccordement.lister - : `${routes.LISTE_PROJETS}?${queryString}`; - if (req.cookies[NEXT_AUTH_SESSION_TOKEN_COOKIE_NAME]) { - return res.redirect(redirectTo); - } - - return res.redirect(`auth/signIn?callbackUrl=${redirectTo}`); - }); }; return { diff --git a/packages/applications/legacy/src/infra/keycloak/resendInvitationEmail.ts b/packages/applications/legacy/src/infra/keycloak/resendInvitationEmail.ts index 6c5238e65e..7ce06ba73b 100644 --- a/packages/applications/legacy/src/infra/keycloak/resendInvitationEmail.ts +++ b/packages/applications/legacy/src/infra/keycloak/resendInvitationEmail.ts @@ -2,8 +2,8 @@ import { requiredAction } from '@potentiel-libraries/keycloak-cjs'; import { authorizedTestEmails, isProdEnv } from '../../config'; import { logger, ResultAsync } from '../../core/utils'; import { OtherError } from '../../modules/shared'; -import routes from '../../routes'; import { makeKeycloakClient } from './keycloakClient'; +import { Routes } from '@potentiel-applications/routes'; const { KEYCLOAK_ADMIN_CLIENT_ID, @@ -46,7 +46,7 @@ export const resendInvitationEmail = (email: string) => { clientId: KEYCLOAK_USER_CLIENT_ID, actions, realm: KEYCLOAK_REALM, - redirectUri: BASE_URL + routes.REDIRECT_BASED_ON_ROLE, + redirectUri: BASE_URL + Routes.Auth.redirectToDashboard(), lifespan: ONE_MONTH, }); } else { diff --git a/packages/applications/legacy/src/modules/authN/queries/RegisterAuth.ts b/packages/applications/legacy/src/modules/authN/queries/RegisterAuth.ts index d388e29be6..b1f3c8687a 100644 --- a/packages/applications/legacy/src/modules/authN/queries/RegisterAuth.ts +++ b/packages/applications/legacy/src/modules/authN/queries/RegisterAuth.ts @@ -1,5 +1,5 @@ -import type { Application, Router } from 'express'; +import type { Application } from 'express'; export interface RegisterAuth { - (args: { app: Application; sessionSecret: string; router: Router }): void; + (args: { app: Application }): void; } diff --git a/packages/applications/legacy/src/routes.ts b/packages/applications/legacy/src/routes.ts index 637db666dc..ba02c909a7 100644 --- a/packages/applications/legacy/src/routes.ts +++ b/packages/applications/legacy/src/routes.ts @@ -25,17 +25,13 @@ export { withParams }; class routes { static HOME = '/'; - static LOGIN = '/login.html'; - static LOGIN_ACTION = '/login.html'; static STATS = '/stats.html'; static ABONNEMENT_LETTRE_INFORMATION = '/abonnement-lettre-information.html'; static POST_SINSCRIRE_LETTRE_INFORMATION = '/s-inscrire-a-la-lettre-d-information'; static DECLARATION_ACCESSIBILITE = '/accessibilite.html'; - static LOGOUT_ACTION = '/signout'; static SIGNUP = '/signup.html'; static POST_SIGNUP = '/signup'; - static REDIRECT_BASED_ON_ROLE = '/go-to-user-dashboard'; static ADMIN_GARANTIES_FINANCIERES = '/admin/garanties-financieres.html'; static ADMIN_AO_PERIODE = '/admin/appels-offres.html'; diff --git a/packages/applications/legacy/src/server.ts b/packages/applications/legacy/src/server.ts index 5451b4443a..9ac097a654 100644 --- a/packages/applications/legacy/src/server.ts +++ b/packages/applications/legacy/src/server.ts @@ -21,7 +21,7 @@ import { MulterError } from 'multer'; setDefaultOptions({ locale: LOCALE.fr }); dotenv.config(); -export async function makeServer(port: number, sessionSecret: string) { +export async function makeServer(port: number) { try { await registerSagas(); @@ -107,7 +107,7 @@ export async function makeServer(port: number, sessionSecret: string) { app.use(express.json({ limit: FILE_SIZE_LIMIT_IN_MB + 'mb' })); - registerAuth({ app, sessionSecret, router: v1Router }); + registerAuth({ app }); app.use(v1Router); app.use(express.static(path.join(__dirname, 'public'))); diff --git a/packages/applications/legacy/src/types/express-custom.d.ts b/packages/applications/legacy/src/types/express-custom.d.ts deleted file mode 100644 index efb4e7d9e8..0000000000 --- a/packages/applications/legacy/src/types/express-custom.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { UtilisateurReadModel } from '../modules/utilisateur/récupérer/UtilisateurReadModel'; - -declare module 'express-serve-static-core' { - // eslint-disable-next-line - interface Request { - user: UtilisateurReadModel; - kauth: any; - } -} - -declare module 'express-session' { - interface SessionData { - apiResults?: Record; - } -} diff --git a/packages/applications/legacy/src/views/components/UI/molecules/dropdowns/DropdownMenu.tsx b/packages/applications/legacy/src/views/components/UI/molecules/dropdowns/DropdownMenu.tsx index a9c0635570..14e2760765 100644 --- a/packages/applications/legacy/src/views/components/UI/molecules/dropdowns/DropdownMenu.tsx +++ b/packages/applications/legacy/src/views/components/UI/molecules/dropdowns/DropdownMenu.tsx @@ -3,7 +3,7 @@ import { ArrowDownIcon } from '../../atoms/icons'; type DropdownMenuProps = ComponentProps<'li'> & { buttonChildren: React.ReactNode; - children?: (ReactElement | false)[]; + children?: ReactElement | (ReactElement | false)[]; }; export const DropdownMenu: React.FC & { DropdownItem: typeof DropdownItem } = ({ @@ -14,7 +14,9 @@ export const DropdownMenu: React.FC & { DropdownItem: typeof }: DropdownMenuProps) => { const [visible, setVisible] = useState(false); const isCurrent = children - ? children.some((subMenu) => subMenu && subMenu.props.isCurrent) + ? Array.isArray(children) + ? children.some((subMenu) => subMenu && subMenu.props.isCurrent) + : children.props.isCurrent : undefined; const ref = useRef(null); diff --git a/packages/applications/legacy/src/views/components/UI/organisms/Header.tsx b/packages/applications/legacy/src/views/components/UI/organisms/Header.tsx index 265be2d5cc..a3a161ae78 100644 --- a/packages/applications/legacy/src/views/components/UI/organisms/Header.tsx +++ b/packages/applications/legacy/src/views/components/UI/organisms/Header.tsx @@ -93,7 +93,7 @@ const Header: React.FC & { MenuItem: typeof MenuItem } = ({
- +
@@ -156,7 +156,7 @@ const QuickAccess = ({ user }: QuickAccessProps) => (
  • @@ -179,7 +179,7 @@ const QuickAccess = ({ user }: QuickAccessProps) => (
  • M'identifier diff --git a/packages/applications/legacy/src/views/components/UI/organisms/UserNavigation.tsx b/packages/applications/legacy/src/views/components/UI/organisms/UserNavigation.tsx index 7edea69a9b..51af333890 100644 --- a/packages/applications/legacy/src/views/components/UI/organisms/UserNavigation.tsx +++ b/packages/applications/legacy/src/views/components/UI/organisms/UserNavigation.tsx @@ -120,6 +120,9 @@ const MenuAdmin = (currentPage?: string) => ( Importer des dates de mise en service + + Corriger des références dossier + ( > Courriers historiques - - Corrections de références dossier - ( -
    +
    diff --git a/packages/applications/legacy/src/views/pages/Signup.tsx b/packages/applications/legacy/src/views/pages/Signup.tsx index 318fdb3369..23aa52d4ff 100644 --- a/packages/applications/legacy/src/views/pages/Signup.tsx +++ b/packages/applications/legacy/src/views/pages/Signup.tsx @@ -16,6 +16,7 @@ import { } from '../components'; import { hydrateOnClient } from '../helpers'; import { App } from '../App'; +import { Routes } from '@potentiel-applications/routes'; type SignupProps = { request: Request; @@ -155,7 +156,7 @@ const SignupFailed = ({ error }: SignupFailedProps) => (
    {error}
    - + M'identifier diff --git a/packages/applications/legacy/src/views/pages/homePage/Home.tsx b/packages/applications/legacy/src/views/pages/homePage/Home.tsx index e184a46acc..293de9065f 100644 --- a/packages/applications/legacy/src/views/pages/homePage/Home.tsx +++ b/packages/applications/legacy/src/views/pages/homePage/Home.tsx @@ -1,11 +1,11 @@ import type { Request } from 'express'; import React from 'react'; -import routes from '../../../routes'; import { Header, Footer, ArrowRightWithCircle } from '../../components'; import { hydrateOnClient } from '../../helpers/hydrateOnClient'; import { InscriptionConnexion, Benefices, PropositionDeValeur } from './components'; import { App } from '../..'; import { User } from '../../../entities'; +import { Routes } from '@potentiel-applications/routes'; type HomeProps = { request: Request; @@ -29,9 +29,9 @@ export const Home = (props: HomeProps) => { return ( -
    +
    {user && ( - +
    {getMenuText(user)} diff --git a/packages/applications/legacy/src/views/pages/homePage/components/InscriptionConnexion.tsx b/packages/applications/legacy/src/views/pages/homePage/components/InscriptionConnexion.tsx index d3999cb525..a7a5638110 100644 --- a/packages/applications/legacy/src/views/pages/homePage/components/InscriptionConnexion.tsx +++ b/packages/applications/legacy/src/views/pages/homePage/components/InscriptionConnexion.tsx @@ -1,5 +1,4 @@ import React, { useState } from 'react'; -import { User } from '../../../../entities'; import routes from '../../../../routes'; import { AccountIcon, @@ -11,6 +10,7 @@ import { LogoutBoxIcon, SecondaryLinkButton, } from '../../../components'; +import { Routes } from '@potentiel-applications/routes'; type InscriptionConnexionProps = | ({ connected: true } & BienvenueProps) @@ -51,14 +51,14 @@ const Bienvenue = ({ fullName, redirectText }: BienvenueProps) => (
    {redirectText} Me déconnecter @@ -103,7 +103,7 @@ const SignupBox = () => { )}

    - Vous avez déjà un compte ? + Vous avez déjà un compte ?

    ); @@ -144,7 +144,7 @@ const LoginBox = () => (

    Connectez-vous pour accéder aux projets.

    - + M'identifier diff --git a/packages/applications/routes/src/auth/auth.routes.ts b/packages/applications/routes/src/auth/auth.routes.ts new file mode 100644 index 0000000000..ef1b5c5fbe --- /dev/null +++ b/packages/applications/routes/src/auth/auth.routes.ts @@ -0,0 +1,19 @@ +export const signIn = (callbackUrl?: string) => { + const route = `/auth/signIn`; + if (!callbackUrl) return route; + const params = new URLSearchParams({ callbackUrl }); + return `${route}?${params}`; +}; + +// The signout page, where the user is redirected after federeated logout +export const signOut = (callbackUrl?: string) => { + const route = `/auth/signOut`; + if (!callbackUrl) return route; + const params = new URLSearchParams({ callbackUrl }); + return `${route}?${params}`; +}; + +// The route to call to initiate user logout +export const federatedLogout = () => `/api/auth/federated-logout`; + +export const redirectToDashboard = () => `/go-to-user-dashboard`; diff --git a/packages/applications/routes/src/auth/index.ts b/packages/applications/routes/src/auth/index.ts new file mode 100644 index 0000000000..047fb6f439 --- /dev/null +++ b/packages/applications/routes/src/auth/index.ts @@ -0,0 +1 @@ +export * as Auth from './auth.routes'; diff --git a/packages/applications/routes/src/index.ts b/packages/applications/routes/src/index.ts index 9f06175924..a73c3b2b11 100644 --- a/packages/applications/routes/src/index.ts +++ b/packages/applications/routes/src/index.ts @@ -7,6 +7,7 @@ import { Projet } from './projet'; import { Recours } from './éliminé'; import { Tache } from './tâche'; import { Période } from './période'; +import { Auth } from './auth'; export const Routes = { Abandon, @@ -20,4 +21,5 @@ export const Routes = { Recours, Tache, Période, + Auth, }; diff --git a/packages/applications/routes/src/projet/projet.routes.ts b/packages/applications/routes/src/projet/projet.routes.ts index 71be180c58..6308b8fc94 100644 --- a/packages/applications/routes/src/projet/projet.routes.ts +++ b/packages/applications/routes/src/projet/projet.routes.ts @@ -4,3 +4,5 @@ export const details = (identifiantProjet: string) => { const url = `/projet/${encodeParameter(identifiantProjet)}/details.html`; return url; }; + +export const lister = () => `/projets.html`; diff --git a/packages/applications/ssr/public/next.svg b/packages/applications/ssr/public/next.svg deleted file mode 100644 index 5174b28c56..0000000000 --- a/packages/applications/ssr/public/next.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/packages/applications/ssr/public/vercel.svg b/packages/applications/ssr/public/vercel.svg deleted file mode 100644 index d2f8422273..0000000000 --- a/packages/applications/ssr/public/vercel.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/packages/applications/ssr/src/app/api/auth/[...nextauth]/route.ts b/packages/applications/ssr/src/app/api/auth/[...nextauth]/route.ts index 95292344d4..d6726d1f31 100644 --- a/packages/applications/ssr/src/app/api/auth/[...nextauth]/route.ts +++ b/packages/applications/ssr/src/app/api/auth/[...nextauth]/route.ts @@ -1,30 +1,7 @@ import NextAuth from 'next-auth'; -import KeycloakProvider from 'next-auth/providers/keycloak'; -const FIFTEEN_MINUTES = 15 * 60; -const ONE_DAY = 24 * 60 * 60; +import { authOptions } from '@/auth'; -const handler = NextAuth({ - providers: [ - KeycloakProvider({ - clientId: process.env.KEYCLOAK_USER_CLIENT_ID ?? '', - clientSecret: process.env.KEYCLOAK_USER_CLIENT_SECRET ?? '', - issuer: `${process.env.KEYCLOAK_SERVER}/realms/${process.env.KEYCLOAK_REALM}`, - }), - ], - session: { - strategy: 'jwt', - maxAge: ONE_DAY, - updateAge: FIFTEEN_MINUTES, - }, - callbacks: { - jwt({ token, account }) { - if (account?.access_token) { - token.accessToken = account.access_token; - } - return token; - }, - }, -}); +const handler = NextAuth(authOptions); export { handler as GET, handler as POST }; diff --git a/packages/applications/ssr/src/app/api/auth/federated-logout/route.ts b/packages/applications/ssr/src/app/api/auth/federated-logout/route.ts new file mode 100644 index 0000000000..1ad9ca1d74 --- /dev/null +++ b/packages/applications/ssr/src/app/api/auth/federated-logout/route.ts @@ -0,0 +1,51 @@ +import { NextResponse } from 'next/server'; +import { getServerSession } from 'next-auth'; + +import { Routes } from '@potentiel-applications/routes'; +import { getLogger } from '@potentiel-libraries/monitoring'; + +import { issuerUrl } from '@/auth'; + +/** + * This route manages logout from the SSO (keycloak). + * Without this, logging out of the app only removes cookies, but the user is still logged to SSO + * @see https://github.com/nextauthjs/next-auth/discussions/3938 + */ +export const GET = async () => { + const { NEXTAUTH_URL = '' } = process.env; + const redirectUrl = new URL(Routes.Auth.signOut(), NEXTAUTH_URL); + + try { + // Gets the session, with idToken + const session = await getServerSession({ + callbacks: { + session({ session, token }) { + session.idToken = token.idToken; + return session; + }, + }, + }); + if (!session) { + return NextResponse.redirect(NEXTAUTH_URL); + } + + // after keycloak logout, redirect the user to this route to remove the session + const ssoLogoutUrl = new URL(`${issuerUrl}/protocol/openid-connect/logout`); + + if (session.idToken) { + // without this, Keycloak prompts the user for confirmation + ssoLogoutUrl.searchParams.set('post_logout_redirect_uri', redirectUrl.toString()); + ssoLogoutUrl.searchParams.set('id_token_hint', session.idToken); + return NextResponse.redirect(ssoLogoutUrl.toString()); + } + + getLogger().warn('A user logged out without an id token, the keycloak session is still active'); + return NextResponse.redirect(redirectUrl); + } catch (e) { + getLogger().error(new Error('Logout error', { cause: e })); + return NextResponse.redirect(redirectUrl); + } +}; + +// forces the route handler to be dynamic +export const dynamic = 'force-dynamic'; diff --git a/packages/applications/ssr/src/app/auth/signIn/page.tsx b/packages/applications/ssr/src/app/auth/signIn/page.tsx index 400ecfe091..36255bb53c 100644 --- a/packages/applications/ssr/src/app/auth/signIn/page.tsx +++ b/packages/applications/ssr/src/app/auth/signIn/page.tsx @@ -4,28 +4,30 @@ import { redirect, useSearchParams } from 'next/navigation'; import { signIn, useSession } from 'next-auth/react'; import { useEffect } from 'react'; +import { Routes } from '@potentiel-applications/routes'; + import { PageTemplate } from '@/components/templates/Page.template'; export default function SignIn() { const params = useSearchParams(); - const { status } = useSession(); + const { status, data } = useSession(); + const callbackUrl = params.get('callbackUrl') ?? Routes.Auth.redirectToDashboard(); useEffect(() => { - const autoSigning = async () => { - await delay(1500); - - const callbackUrl = params.get('callbackUrl') ?? '/'; - - if (status === 'unauthenticated') { - signIn('keycloak', { callbackUrl }); - } - - if (status === 'authenticated') { + switch (status) { + case 'authenticated': + // This checks that the session is up to date with the necessary requirements + // it's useful when changing what's inside the cookie for instance + if (!data.utilisateur) { + redirect(Routes.Auth.signOut(callbackUrl)); + break; + } redirect(callbackUrl); - } - }; - autoSigning(); - }, [status, params]); + break; + case 'unauthenticated': + signIn('keycloak', { callbackUrl }); + } + }, [status, callbackUrl, data]); return ( @@ -35,5 +37,3 @@ export default function SignIn() { ); } - -const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); diff --git a/packages/applications/ssr/src/app/auth/signOut/page.tsx b/packages/applications/ssr/src/app/auth/signOut/page.tsx index 795b3272e5..065aa8f687 100644 --- a/packages/applications/ssr/src/app/auth/signOut/page.tsx +++ b/packages/applications/ssr/src/app/auth/signOut/page.tsx @@ -5,16 +5,10 @@ import { useEffect } from 'react'; import { PageTemplate } from '@/components/templates/Page.template'; -export default function SignIn() { +export default function SignOut() { useEffect(() => { - const autoSignout = async () => { - await delay(1500); - - signOut({ callbackUrl: '/logout' }); - }; - - autoSignout(); - }); + signOut({ callbackUrl: '/' }); + }, []); return ( @@ -24,5 +18,3 @@ export default function SignIn() { ); } - -const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); diff --git a/packages/applications/ssr/src/app/go-to-user-dashboard/route.ts b/packages/applications/ssr/src/app/go-to-user-dashboard/route.ts new file mode 100644 index 0000000000..3627352528 --- /dev/null +++ b/packages/applications/ssr/src/app/go-to-user-dashboard/route.ts @@ -0,0 +1,17 @@ +import { redirect } from 'next/navigation'; + +import { Routes } from '@potentiel-applications/routes'; +import { Role } from '@potentiel-domain/utilisateur'; + +import { getOptionalAuthenticatedUser } from '@/utils/getAuthenticatedUser.handler'; + +export const GET = async () => { + const utilisateur = await getOptionalAuthenticatedUser(); + if (utilisateur) { + const redirectTo = utilisateur.role.estÉgaleÀ(Role.grd) + ? Routes.Raccordement.lister + : Routes.Projet.lister(); + redirect(redirectTo); + } + redirect(Routes.Auth.signIn()); +}; diff --git a/packages/applications/ssr/src/auth/authOptions.ts b/packages/applications/ssr/src/auth/authOptions.ts new file mode 100644 index 0000000000..23b4044b12 --- /dev/null +++ b/packages/applications/ssr/src/auth/authOptions.ts @@ -0,0 +1,58 @@ +import { AuthOptions } from 'next-auth'; +import KeycloakProvider from 'next-auth/providers/keycloak'; + +import { getLogger } from '@potentiel-libraries/monitoring'; + +import { convertToken } from './convertToken'; + +const ONE_HOUR = 60 * 60; + +export const issuerUrl = `${process.env.KEYCLOAK_SERVER}/realms/${process.env.KEYCLOAK_REALM}`; + +export const authOptions: AuthOptions = { + providers: [ + KeycloakProvider({ + clientId: process.env.KEYCLOAK_USER_CLIENT_ID ?? '', + clientSecret: process.env.KEYCLOAK_USER_CLIENT_SECRET ?? '', + issuer: issuerUrl, + }), + ], + session: { + strategy: 'jwt', + // This is the max age for the next-auth cookie + // It is renewed on each page refresh, so this represents inactivity time. + // Moreover, the user will not be disconnected after expiration (if their Keycloak session still exists), + // but there will be a redirection to keycloak. + maxAge: ONE_HOUR, + }, + callbacks: { + // Stores user data and idToken to the next-auth cookie + jwt({ token, account }) { + // NB `account` is defined only at login + if (account?.access_token) { + try { + const utilisateur = convertToken(account.access_token); + token.utilisateur = utilisateur; + } catch (e) { + getLogger('Auth').error( + new Error("Impossible de convertir l'accessToken en Utilisateur", { cause: e }), + ); + return token; + } + } + // Stores the id token as it is required to logout of Keycloak + if (account?.id_token) { + token.idToken = account.id_token; + } + return token; + }, + session({ session, token }) { + { + if (token.utilisateur) { + session.utilisateur = token.utilisateur; + } + return session; + } + }, + }, +}; diff --git a/packages/applications/ssr/src/auth/convertToken.ts b/packages/applications/ssr/src/auth/convertToken.ts new file mode 100644 index 0000000000..1f1857d091 --- /dev/null +++ b/packages/applications/ssr/src/auth/convertToken.ts @@ -0,0 +1,67 @@ +import { decodeJwt } from 'jose'; +import { z } from 'zod'; + +import { OperationRejectedError, PlainType } from '@potentiel-domain/core'; +import { Role, Groupe, IdentifiantUtilisateur, Utilisateur } from '@potentiel-domain/utilisateur'; +import { Option } from '@potentiel-libraries/monads'; + +export const convertToken = (token: string): PlainType => { + const { email, nom, roles, groupes } = parseToken(token); + + const role = roles.find((r) => Role.estUnRoleValide(r)); + const groupe = groupes.find((g) => Groupe.estUnGroupeValide(g)); + + return { + role: Role.convertirEnValueType(role ?? ''), + groupe: groupe ? Groupe.convertirEnValueType(groupe) : Option.none, + nom, + identifiantUtilisateur: IdentifiantUtilisateur.convertirEnValueType(email), + }; +}; + +const jwtSchema = z.object({ + name: z.string(), + email: z.string(), + realm_access: z.object({ + roles: z.array(z.string()), + }), + groups: z.array(z.string()).optional(), +}); + +const parseToken = (token: string) => { + try { + if (!token) { + throw new EmptyTokenError(); + } + + const decodedJwt = decodeJwt(token); + const { + name, + email, + realm_access: { roles }, + groups, + } = jwtSchema.parse(decodedJwt); + + return { + nom: name, + email, + roles, + groupes: groups ?? [], + }; + } catch (e) { + throw new TokenInvalideError(e as Error); + } +}; + +class TokenInvalideError extends OperationRejectedError { + constructor(cause: Error) { + super(`Le format du token utilisateur n'est pas valide.`); + this.cause = cause; + } +} + +class EmptyTokenError extends Error { + constructor() { + super(`Token vide`); + } +} diff --git a/packages/applications/ssr/src/auth/index.ts b/packages/applications/ssr/src/auth/index.ts new file mode 100644 index 0000000000..ca160157ab --- /dev/null +++ b/packages/applications/ssr/src/auth/index.ts @@ -0,0 +1,2 @@ +export { authOptions, issuerUrl } from './authOptions'; +export { convertToken } from './convertToken'; diff --git a/packages/applications/ssr/src/components/molecules/UserHeaderQuickAccessItem.tsx b/packages/applications/ssr/src/components/molecules/UserHeaderQuickAccessItem.tsx index 0e540aa6f7..cf762e4924 100644 --- a/packages/applications/ssr/src/components/molecules/UserHeaderQuickAccessItem.tsx +++ b/packages/applications/ssr/src/components/molecules/UserHeaderQuickAccessItem.tsx @@ -31,6 +31,7 @@ export async function UserHeaderQuickAccessItem() { iconId: 'ri-user-line', linkProps: { href: accountUrl, + prefetch: false, }, text: utilisateur.nom, }} @@ -41,7 +42,8 @@ export async function UserHeaderQuickAccessItem() { quickAccessItem={{ iconId: 'ri-logout-box-line', linkProps: { - href: '/auth/signOut', + href: Routes.Auth.federatedLogout(), + prefetch: false, }, text: 'Me déconnecter', }} @@ -57,6 +59,7 @@ export async function UserHeaderQuickAccessItem() { iconId: 'ri-account-circle-line', linkProps: { href: '/signup.html', + prefetch: false, }, text: "M'inscrire", }} @@ -65,7 +68,8 @@ export async function UserHeaderQuickAccessItem() { quickAccessItem={{ iconId: 'ri-lock-line', linkProps: { - href: '/auth/signIn', + href: Routes.Auth.signIn(), + prefetch: false, }, text: "M'identifier", }} @@ -104,6 +108,7 @@ async function getTâcheHeaderQuickAccessItem(utilisateur: Utilisateur.ValueType iconId: nombreTâches > 0 ? 'ri-mail-unread-line' : 'ri-mail-check-line', linkProps: { href: Routes.Tache.lister, + prefetch: false, }, text: `Tâches (${nombreTâches})`, }} diff --git a/packages/applications/ssr/src/middleware.ts b/packages/applications/ssr/src/middleware.ts index 970822e800..e56ced03bb 100644 --- a/packages/applications/ssr/src/middleware.ts +++ b/packages/applications/ssr/src/middleware.ts @@ -1,10 +1,13 @@ import { withAuth } from 'next-auth/middleware'; export default withAuth({ + // NB: importing Routes is not working in the middleware pages: { signIn: '/auth/signIn' }, }); export const config = { // do not run middleware for paths matching one of following - matcher: ['/((?!api|_next/static|_next/image|auth|favicon.ico|robots.txt|images|$).*)'], + matcher: [ + '/((?!api|_next/static|_next/image|auth|favicon.ico|robots.txt|images|illustrations|$).*)', + ], }; diff --git a/packages/applications/ssr/src/types/next-auth.d.ts b/packages/applications/ssr/src/types/next-auth.d.ts index ab9fe812aa..ebb129ebd6 100644 --- a/packages/applications/ssr/src/types/next-auth.d.ts +++ b/packages/applications/ssr/src/types/next-auth.d.ts @@ -1,7 +1,16 @@ -import NextAuthJwt from 'next-auth/jwt'; +import { Utilisateur } from '@potentiel-domain/utilisateur'; +import { PlainType } from '@potentiel-domain/core'; declare module 'next-auth/jwt' { - interface JWT extends NextAuthJwt.JWT { - accessToken?: string; + interface JWT { + idToken?: string; + utilisateur?: PlainType; + } +} + +declare module 'next-auth' { + interface Session { + idToken?: string; + utilisateur?: PlainType; } } diff --git a/packages/applications/ssr/src/utils/PageWithErrorHandling.tsx b/packages/applications/ssr/src/utils/PageWithErrorHandling.tsx index e8c1382d11..03b57848d2 100644 --- a/packages/applications/ssr/src/utils/PageWithErrorHandling.tsx +++ b/packages/applications/ssr/src/utils/PageWithErrorHandling.tsx @@ -1,11 +1,14 @@ 'use server'; +import { redirect } from 'next/navigation'; + import { AggregateNotFoundError, DomainError, InvalidOperationError, OperationRejectedError, } from '@potentiel-domain/core'; +import { Routes } from '@potentiel-applications/routes'; import { CustomErrorPage } from '@/components/pages/custom-error/CustomError.page'; @@ -13,7 +16,8 @@ import { withErrorHandling } from './withErrorHandling'; export const PageWithErrorHandling = async ( render: () => Promise, -): Promise => withErrorHandling(render, renderDomainError, renderUnknownError); +): Promise => + withErrorHandling(render, renderDomainError, redirectOnAuthenticationError, renderUnknownError); const renderDomainError = (e: DomainError) => { if (e instanceof AggregateNotFoundError) { @@ -29,6 +33,10 @@ const renderDomainError = (e: DomainError) => { return <>; }; -const renderUnknownError = () => { +const renderUnknownError = (_: Error) => { return ; }; + +const redirectOnAuthenticationError = () => { + redirect(Routes.Auth.signIn()); +}; diff --git a/packages/applications/ssr/src/utils/apiAction.ts b/packages/applications/ssr/src/utils/apiAction.ts index e8f8d0ece4..65d792b392 100644 --- a/packages/applications/ssr/src/utils/apiAction.ts +++ b/packages/applications/ssr/src/utils/apiAction.ts @@ -1,3 +1,5 @@ +import { STATUS_CODES } from 'node:http'; + import { AggregateNotFoundError, DomainError, @@ -8,14 +10,14 @@ import { import { withErrorHandling } from './withErrorHandling'; export const apiAction = async (action: () => Promise) => - withErrorHandling(action, mapDomainError, mapTo500); + withErrorHandling(action, mapDomainError, mapTo401, mapTo500); const mapDomainError = (e: DomainError) => { if (e instanceof InvalidOperationError) { return mapTo400(e); } if (e instanceof OperationRejectedError) { - return mapTo401(); + return mapTo403(); } if (e instanceof AggregateNotFoundError) { return mapTo404(e); @@ -24,59 +26,20 @@ const mapDomainError = (e: DomainError) => { return mapTo500(); }; -const mapTo404 = (e: Error) => { - return Response.json( - { - message: e.message, - }, - { - status: 404, - statusText: 'Not Found', - headers: { - 'content-type': 'text/plain', - }, - }, - ); -}; - -const mapTo400 = (e: Error) => { - return Response.json( - { - message: e.message, - }, - { - status: 400, - statusText: 'Bad Request', - }, - ); -}; - -const mapTo401 = () => { - return Response.json( - { - message: 'Opération rejetée', - }, +const mapToHttpError = (status: number, message: string) => + Response.json( + { message }, { - status: 401, - statusText: 'Unauthorized', + status, + statusText: STATUS_CODES[status], headers: { 'content-type': 'text/plain', }, }, ); -}; -const mapTo500 = () => { - return Response.json( - { - message: 'Une erreur est survenue', - }, - { - status: 500, - statusText: 'Internal Server Error', - headers: { - 'content-type': 'text/plain', - }, - }, - ); -}; +const mapTo400 = (e: Error) => mapToHttpError(400, e.message); +const mapTo401 = () => mapToHttpError(401, "L'authentification a échoué"); +const mapTo403 = () => mapToHttpError(403, 'Opération rejetée'); +const mapTo404 = (e: Error) => mapToHttpError(404, e.message); +const mapTo500 = () => mapToHttpError(500, 'Une erreur est survenue'); diff --git a/packages/applications/ssr/src/utils/getAuthenticatedUser.handler.ts b/packages/applications/ssr/src/utils/getAuthenticatedUser.handler.ts index 73b4b29a61..f64d35273b 100644 --- a/packages/applications/ssr/src/utils/getAuthenticatedUser.handler.ts +++ b/packages/applications/ssr/src/utils/getAuthenticatedUser.handler.ts @@ -1,44 +1,29 @@ import { Message, MessageHandler } from 'mediateur'; -import { cookies, headers } from 'next/headers'; -import { decode } from 'next-auth/jwt'; +import { getServerSession } from 'next-auth'; +import { headers } from 'next/headers'; // import * as Sentry from '@sentry/nextjs'; import { Utilisateur } from '@potentiel-domain/utilisateur'; +import { authOptions, convertToken } from '@/auth'; + export type GetAuthenticatedUserMessage = Message< 'System.Authorization.RécupérerUtilisateur', {}, Utilisateur.ValueType >; -const { NEXT_AUTH_SESSION_TOKEN_COOKIE_NAME = 'next-auth.session-token' } = process.env; - -/** - * Check for an access token - * - in the session (encrypted) - * - in Authorization header - **/ -const getAccessToken = async () => { - const cookiesContent = cookies(); - const sessionToken = cookiesContent.get(NEXT_AUTH_SESSION_TOKEN_COOKIE_NAME)?.value || ''; - if (sessionToken) { - const decoded = await decode({ - token: sessionToken, - secret: process.env.NEXTAUTH_SECRET ?? '', - }); - - return decoded?.accessToken; - } - const authorizationHeader = headers().get('authorization'); - return authorizationHeader?.replace(/Bearer /, ''); -}; - export const getOptionalAuthenticatedUser = async (): Promise< Utilisateur.ValueType | undefined > => { - const accessToken = await getAccessToken(); - if (accessToken) { - return Utilisateur.convertirEnValueType(accessToken); + const session = await getServerSession(authOptions); + if (session?.utilisateur) { + return Utilisateur.bind(session.utilisateur); + } + const authorizationHeader = headers().get('Authorization'); + if (authorizationHeader && authorizationHeader.toLowerCase().startsWith('bearer ')) { + const utilisateur = convertToken(authorizationHeader.slice(7)); + return utilisateur && Utilisateur.bind(utilisateur); } }; diff --git a/packages/applications/ssr/src/utils/withErrorHandling.ts b/packages/applications/ssr/src/utils/withErrorHandling.ts index be879c1115..280634d900 100644 --- a/packages/applications/ssr/src/utils/withErrorHandling.ts +++ b/packages/applications/ssr/src/utils/withErrorHandling.ts @@ -1,6 +1,5 @@ import { isNotFoundError } from 'next/dist/client/components/not-found'; import { isRedirectError } from 'next/dist/client/components/redirect'; -import { redirect } from 'next/navigation'; import { getLogger } from '@potentiel-libraries/monitoring'; import { DomainError } from '@potentiel-domain/core'; @@ -12,7 +11,8 @@ import { NoAuthenticatedUserError } from './getAuthenticatedUser.handler'; export async function withErrorHandling( action: () => Promise, onDomainError: (error: DomainError) => TResult, - onUnknowmError: (error: Error) => TResult, + onAuthenticationError: () => TResult, + onUnknownError: (error: Error) => TResult, ): Promise { try { await bootstrap({ middlewares: [permissionMiddleware] }); @@ -23,7 +23,7 @@ export async function withErrorHandling( } if (e instanceof NoAuthenticatedUserError) { - redirect('/auth/signIn'); + return onAuthenticationError(); } if (e instanceof DomainError) { @@ -32,6 +32,6 @@ export async function withErrorHandling( } getLogger().error(e as Error); - return onUnknowmError(e as Error); + return onUnknownError(e as Error); } } diff --git a/packages/domain/utilisateur/src/groupe.valueType.ts b/packages/domain/utilisateur/src/groupe.valueType.ts index 867c436229..3af1d776b2 100644 --- a/packages/domain/utilisateur/src/groupe.valueType.ts +++ b/packages/domain/utilisateur/src/groupe.valueType.ts @@ -30,7 +30,7 @@ export const convertirEnValueType = (value: string): ValueType => { }; export const bind = ({ nom, type }: PlainType) => { - return convertirEnValueType(`${type}/${nom}`); + return convertirEnValueType(`/${type}/${nom}`); }; export const estUnGroupeValide = (value: string) => { diff --git a/packages/domain/utilisateur/src/utilisateur.valueType.ts b/packages/domain/utilisateur/src/utilisateur.valueType.ts index 817b371e46..9322dc8302 100644 --- a/packages/domain/utilisateur/src/utilisateur.valueType.ts +++ b/packages/domain/utilisateur/src/utilisateur.valueType.ts @@ -1,4 +1,4 @@ -import { OperationRejectedError, ReadonlyValueType } from '@potentiel-domain/core'; +import { PlainType, ReadonlyValueType } from '@potentiel-domain/core'; import { Option } from '@potentiel-libraries/monads'; import * as Role from './role.valueType'; @@ -12,16 +12,21 @@ export type ValueType = ReadonlyValueType<{ groupe: Option.Type; }>; -export const convertirEnValueType = (value: string): ValueType => { - const { nom, identifiantUtilisateur, role, groupe } = convertToken(value); +export const bind = ({ + nom, + identifiantUtilisateur, + groupe, + role, +}: PlainType): ValueType => { + const _identifiantUtilisateur = IdentifiantUtilisateur.bind(identifiantUtilisateur); return { nom, - identifiantUtilisateur, - role, - groupe, + role: Role.bind(role), + identifiantUtilisateur: _identifiantUtilisateur, + groupe: Option.isSome(groupe) ? Groupe.bind(groupe) : Option.none, estÉgaleÀ(valueType) { return this.nom === valueType.nom && - this.identifiantUtilisateur.estÉgaleÀ(identifiantUtilisateur) && + this.identifiantUtilisateur.estÉgaleÀ(_identifiantUtilisateur) && this.role.estÉgaleÀ(valueType.role) && Option.isSome(this.groupe) ? Option.isSome(valueType.groupe) && this.groupe.estÉgaleÀ(valueType.groupe) @@ -29,60 +34,3 @@ export const convertirEnValueType = (value: string): ValueType => { }, }; }; - -const convertToken = (token: string) => { - const { email, nom, roles, groupes } = parseToken(token); - - const role = roles.find((r) => Role.estUnRoleValide(r)); - const groupe = groupes.find((g) => Groupe.estUnGroupeValide(g)); - - return { - role: Role.convertirEnValueType(role ?? ''), - groupe: groupe ? Groupe.convertirEnValueType(groupe) : Option.none, - nom, - identifiantUtilisateur: IdentifiantUtilisateur.convertirEnValueType(email), - }; -}; - -const parseToken = (token: string) => { - try { - if (!token) { - throw new EmptyTokenError(); - } - const { - name, - email, - realm_access: { roles }, - groups, - } = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString()) as { - name: string; - email: string; - realm_access: { - roles: Array; - }; - groups: string[]; - }; - - return { - nom: name, - email, - roles, - groupes: groups ?? [], - }; - } catch (e) { - throw new TokenInvalideError(e as Error); - } -}; - -class TokenInvalideError extends OperationRejectedError { - constructor(cause: Error) { - super(`Le format du token utilisateur n'est pas valide.`); - this.cause = cause; - } -} - -class EmptyTokenError extends Error { - constructor() { - super(`Token vide`); - } -}