From ba2c36c0143fcf2b36c52903bd5dceec0cf42649 Mon Sep 17 00:00:00 2001 From: NicolasCantu Date: Fri, 26 Sep 2025 15:12:34 +0200 Subject: [PATCH] docs: add test data for login; feat: ID.Not dev flow tweaks; chore: nginx dev host adjustments --- confs/nginx/dev3.4nkweb.com.conf | 44 +++- docs/data_account_test.md | 337 ++++++++++++++++++++++++ src/controllers/idnot.controller.ts | 50 +--- src/handlers/idnot-callback.handlers.ts | 18 +- src/services/idnot/index.ts | 69 ++++- 5 files changed, 462 insertions(+), 56 deletions(-) create mode 100644 docs/data_account_test.md diff --git a/confs/nginx/dev3.4nkweb.com.conf b/confs/nginx/dev3.4nkweb.com.conf index cc3b1cf..5c46d37 100644 --- a/confs/nginx/dev3.4nkweb.com.conf +++ b/confs/nginx/dev3.4nkweb.com.conf @@ -23,7 +23,7 @@ server { if ($request_method = OPTIONS) { add_header Access-Control-Allow-Origin $cors_origin always; add_header Access-Control-Allow-Credentials "true" always; - add_header Access-Control-Allow-Headers "Content-Type, Authorization, x-session-id" always; + add_header Access-Control-Allow-Headers "Content-Type, Authorization, x-session-id, x-request-id" always; add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always; add_header Vary "Origin, Access-Control-Request-Method, Access-Control-Request-Headers" always; return 204; @@ -32,7 +32,7 @@ server { # En-têtes CORS pour les autres méthodes add_header Access-Control-Allow-Origin $cors_origin always; add_header Access-Control-Allow-Credentials "true" always; - add_header Access-Control-Allow-Headers "Content-Type, Authorization, x-session-id" always; + add_header Access-Control-Allow-Headers "Content-Type, Authorization, x-session-id, x-request-id" always; add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always; add_header Vary "Origin, Access-Control-Request-Method, Access-Control-Request-Headers" always; add_header X-Request-Id $request_id always; @@ -45,6 +45,46 @@ server { proxy_set_header X-Request-Id $request_id; } + # Redirection front (authorized-client) -> backend Express + location = /authorized-client { + # Masquer les en-têtes CORS envoyés par l'upstream (Express) + proxy_hide_header Access-Control-Allow-Origin; + proxy_hide_header Access-Control-Allow-Credentials; + proxy_hide_header Access-Control-Allow-Headers; + proxy_hide_header Access-Control-Allow-Methods; + + # CORS dynamique + set $cors_origin ""; + if ($http_origin ~* ^(https://dev4\.4nkweb\.com|http://local\.4nkweb\.com:3000|http://localhost:3000|https://.*\.4nkweb\.com)$) { + set $cors_origin $http_origin; + } + + # Préflight OPTIONS + if ($request_method = OPTIONS) { + add_header Access-Control-Allow-Origin $cors_origin always; + add_header Access-Control-Allow-Credentials "true" always; + add_header Access-Control-Allow-Headers "Content-Type, Authorization, x-session-id, x-request-id" always; + add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always; + add_header Vary "Origin, Access-Control-Request-Method, Access-Control-Request-Headers" always; + return 204; + } + + # En-têtes CORS pour les autres méthodes + add_header Access-Control-Allow-Origin $cors_origin always; + add_header Access-Control-Allow-Credentials "true" always; + add_header Access-Control-Allow-Headers "Content-Type, Authorization, x-session-id, x-request-id" always; + add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always; + add_header Vary "Origin, Access-Control-Request-Method, Access-Control-Request-Headers" always; + add_header X-Request-Id $request_id always; + + proxy_pass http://127.0.0.1:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Request-Id $request_id; + } + ssl_certificate /etc/letsencrypt/live/dev3.4nkweb.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/dev3.4nkweb.com/privkey.pem; diff --git a/docs/data_account_test.md b/docs/data_account_test.md new file mode 100644 index 0000000..37d3aea --- /dev/null +++ b/docs/data_account_test.md @@ -0,0 +1,337 @@ +# Data de test + +## Environnement + +- Environnement : DEV +- IDN : IDN96755310A +- Environnement de production : Non +- Code de l'environnement : DEV +- Description : Environnement de developpement +- URL : https://lecoffreio.4nkweb.com/* +- ID.not : OpenID +- API Annuaire : true + +```json +{ + "id": "5207116884324909574", + "idn": "APP14191728A", + "label": "LeCoffre", + "description": "A remplir par le propriétaire", + "code": "LECOFFRE", + "technologies": [ + "a préciser" + ], + "status": "ACCEPTED", + "environments": [ + { + "id": "5737646715224215506", + "idn": "IDN96755310A", + "description": "Environnement de developpement ", + "code": "DEV", + "isProduction": false, + "url": "https://lecoffreio.4nkweb.com/*", + "deploymentTarget": "2025-04-11", + "status": "OK", + "hasOpenId": true, + "hasSaml": false, + "hasDirectoryApi": true, + "access": "OPEN", + "hasPendingAccess": false + } + ], + "owner": { + "idn": "IDN369599", + "label": "Not.IT (Fonds de dotation technologique porté par les Notaires d'Ille-et-Vilaine)", + "intitule": "Not.IT (Fonds de dotation technologique porté par les Notaires d'Ille-et-Vilaine)" + } +} +``` + +openId: + +```json +{ + "openIdData": { + "idpClientLabel": "1.0", + "wellKnownUrl": "https://qual-connexion.idnot.fr/IdPOAuth2/idnot_idp_v1/.well-known/openid-configuration", + "logoutUrl": "https://qual-connexion.idnot.fr/user/auth/logout?sourceURL=VOTRE_URL", + "callbackUrls": [ + "https://lecoffreio.4nkweb.com/*", + "https://lecoffreio.4nkweb.com/folders", + "https://lecoffreio.4nkweb.com/authorized-client", + "https://oauth.pstmn.io/v1/browser-callback", + "http://local.lecoffreio.4nkweb:3000/*", + "https://oauth.pstmn.io/v1/callback", + "https://test.lecoffre.io/*", + "https://test.lecoffre.io/authorized-client", + "http://local.4nkweb.com:3000/authorized-client", + "http://local.lecoffreio.4nkweb" + ], + "clientId": ""******"", + "clientSecret": "******" + }, + "askedInfos": { + "firstname": "Admin", + "lastname": "KOGUS", + "date": "2025-04-10T14:00:55.458537Z" + }, + "validatedInfos": { + "firstname": "Haitam", + "lastname": "TANASSA", + "date": "2025-04-14T08:18:01.880555Z", + "justification": null + }, + "openIdScopes": { + "email": { + "name": "email", + "asked": false, + "justification": "" + } + }, + "scopeStatus": "ACCEPTED" +} +``` + +## Utilisateur + +- Identifiant code : IDN00082246I +- Identifiant : marie.curie.519 +- Nom : Marie Curie +- Administrateur @ ABBATE et associés +- login : marie.curie.519 +- pass: "******" + +Infos basiques: + +```json +{ + "status": "OK", + "success": true, + "idn": "IDN00082246I", + "civilite": "Madame", + "nomDeNaissance": "CURIE", + "nomUsuel": "CURIE", + "prenom": "Marie", + "jourDeNaissance": "08", + "moisDeNaissance": "04", + "anneeDeNaissance": "1965", + "paysDeNaissance": { + "nom": null, + "code": "France" + }, + "communeDeNaissance": "MONTÉLIMAR", + "photo": "", + "managedByFicen": true, + "completion": 0, + "interne": true, + "languages": [ + "FR" + ] +} +``` + +Infos détaillées: + +```json +{ + "firstName": "Marie", + "lastName": "CURIE", + "activated": true, + "langKey": "fr", + "authorities": [ + "ROLE_INTERNE" + ], + "entityAuthorities": [ + { + "oid": "IDN187087", + "role": "ROLE_GESTIONNAIRE_NATUREL", + "authority": "ROLE_GESTIONNAIRE_NATUREL - IDN187087" + } + ], + "entities": [ + { + "id": "IDN187087", + "name": "ABBATE et associés", + "logo": null, + "adresseGeographique": null, + "adressePostale": null, + "telephone": "04 94 00 52 90", + "email": "abbate.gabolde@notaires.fr", + "siteInternet": "www.carqueiranne-abbate-gabolde-servel.notaires.fr", + "identifiantNotaconnect": "IDN187087", + "nomAbrege": "ABBATE et associés", + "courDappel": null, + "departementsCouverts": [], + "crpcen": "083079", + "type": "STON", + "typeEntite": "office", + "statut": "Pourvu", + "residence": "CARQUEIRANNE (83034)", + "departementDeResidence": "083 - VAR", + "siren": "423762640", + "siret": "42376264000013", + "idnRattachement": null, + "ctmAdrGeoVille": "CARQUEIRANNE", + "ctmAdrGeoCodePostal": "83320", + "ctmAdrGeo1": null, + "ctmAdrGeo2": null, + "ctmAdrGeo3": null, + "ctmAdrGeo4": null, + "ctmAdrGeo5": null, + "ctmAdrPostaleCodePostal": "83320", + "ctmAdrPostaleVille": "CARQUEIRANNE", + "ctmAdrPostale1": null, + "ctmAdrPostale2": null, + "ctmAdrPostale3": null, + "ctmAdrPostale4": "1 AVENUE JEAN JAURES", + "ctmAdrPostale5": "BP 14", + "ctmDenominationSociale": "SCP Louis ABBATE, Gabriel GABOLDE et Laura SERVEL-SCHROEDER", + "ctmDenominationSocialeAbregee": "ABBATE et associés", + "ctmIntitule": "ABBATE Louis, GABOLDE Gabriel et SERVEL-SCHROEDER Laura", + "ctmFormeJuridique": "SCP", + "ctmLibelle": null, + "rattachement": { + "id": "IDN00082246I_IDN187087", + "email": "marie.curie.519@notaires.fr", + "blocked": false, + "phoneNumber": null, + "homePhoneNumber": null, + "entityType": "office", + "linkType": "Administrateur", + "subLinkType": null, + "activitiesDomain": [], + "mandats": [], + "manager": true, + "naturalManager": true + } + } + ], + "idn": "IDN00082246I", + "civilite": "Madame", + "photo": "", + "email": "personaIDN00082246I@portail.com", + "pseudo": "marie.curie.519", + "backupEmail": "nicolas.cantu@pm.me", + "emailValidated": "true" +} +``` + +### Informations de secours + +- Email de récupération : personaIDN00082246I@portail.com +- Email de récupération de secours : nicolas.cantu@pm.me + +## Office de rattachement + +```json +{ + "firstName": "Marie", + "lastName": "CURIE", + "activated": true, + "langKey": "fr", + "authorities": [ + "ROLE_INTERNE" + ], + "entityAuthorities": [ + { + "oid": "IDN187087", + "role": "ROLE_GESTIONNAIRE_NATUREL", + "authority": "ROLE_GESTIONNAIRE_NATUREL - IDN187087" + } + ], + "entities": [ + { + "id": "IDN187087", + "name": "ABBATE et associés", + "logo": null, + "adresseGeographique": null, + "adressePostale": null, + "telephone": "04 94 00 52 90", + "email": "abbate.gabolde@notaires.fr", + "siteInternet": "www.carqueiranne-abbate-gabolde-servel.notaires.fr", + "identifiantNotaconnect": "IDN187087", + "nomAbrege": "ABBATE et associés", + "courDappel": null, + "departementsCouverts": [], + "crpcen": "083079", + "type": "STON", + "typeEntite": "office", + "statut": "Pourvu", + "residence": "CARQUEIRANNE (83034)", + "departementDeResidence": "083 - VAR", + "siren": "423762640", + "siret": "42376264000013", + "idnRattachement": null, + "ctmAdrGeoVille": "CARQUEIRANNE", + "ctmAdrGeoCodePostal": "83320", + "ctmAdrGeo1": null, + "ctmAdrGeo2": null, + "ctmAdrGeo3": null, + "ctmAdrGeo4": null, + "ctmAdrGeo5": null, + "ctmAdrPostaleCodePostal": "83320", + "ctmAdrPostaleVille": "CARQUEIRANNE", + "ctmAdrPostale1": null, + "ctmAdrPostale2": null, + "ctmAdrPostale3": null, + "ctmAdrPostale4": "1 AVENUE JEAN JAURES", + "ctmAdrPostale5": "BP 14", + "ctmDenominationSociale": "SCP Louis ABBATE, Gabriel GABOLDE et Laura SERVEL-SCHROEDER", + "ctmDenominationSocialeAbregee": "ABBATE et associés", + "ctmIntitule": "ABBATE Louis, GABOLDE Gabriel et SERVEL-SCHROEDER Laura", + "ctmFormeJuridique": "SCP", + "ctmLibelle": null, + "rattachement": { + "id": "IDN00082246I_IDN187087", + "email": "marie.curie.519@notaires.fr", + "blocked": false, + "phoneNumber": null, + "homePhoneNumber": null, + "entityType": "office", + "linkType": "Administrateur", + "subLinkType": null, + "activitiesDomain": [], + "mandats": [], + "manager": true, + "naturalManager": true + } + } + ], + "idn": "IDN00082246I", + "civilite": "Madame", + "photo": "", + "email": "personaIDN00082246I@portail.com", + "pseudo": "marie.curie.519", + "backupEmail": "nicolas.cantu@pm.me", + "emailValidated": "true" +} +``` + +### Identifants + +- Identifiant : ID.NOT IDN187087 +- Type : STON +- CRPCEN : 083079 +- Forme juridique : SCP +- Statut : Pourvu +- Département de résidence : 083 - VAR +- Résidence : CARQUEIRANNE (83034) + +### Contact + +- Téléphone : 0494005290 +- Email : abbate.gabolde@notaires.fr +- Site internet : www.carqueiranne-abbate-gabolde-servel.notaires.fr + +### Adresse géographique + +- Numéro et libellé de la voie : 1 AVENUE JEAN JAURES +- Code postal : 83320 +- Ville : CARQUEIRANNE + +### Adresse postale + +- Numéro et libellé de la voie : 1 AVENUE JEAN JAURES +- Complément d'adresse : BP 14 +- Code postal : 83320 +- Ville : CARQUEIRANNE diff --git a/src/controllers/idnot.controller.ts b/src/controllers/idnot.controller.ts index efbcb8e..83f1211 100644 --- a/src/controllers/idnot.controller.ts +++ b/src/controllers/idnot.controller.ts @@ -97,49 +97,21 @@ export class IdNotController { Logger.info('IdNot authentication initiated', { codePrefix: code.substring(0, 8) + '...' }); try { - // Development fallback: allow authentication without contacting IdNot - if (process.env.IDNOT_MOCK === '1') { - Logger.warn('IDNOT_MOCK enabled - returning mocked IdNot user without external calls'); - const idNotUser: IdNotUser = { - idNot: 'IDN187087', - office: { - idNot: 'IDN187087', - name: 'STON - CARQUEIRANNE', - crpcen: '083079', - office_status: 'ACTIVATED' as any, - address: { address: 'CARQUEIRANNE', city: 'CARQUEIRANNE', zip_code: 83034 }, - status: 'ACTIVE' - }, - role: { name: 'admin' }, - contact: { - first_name: 'Test', - last_name: 'User', - email: 'test@lecoffre.io', - phone_number: '+33400000000', - cell_phone_number: '+33600000000', - civility: 'Monsieur' as any - }, - office_role: { name: 'Notaire' } - }; - - const authToken = uuidv4(); - const tokenData: AuthToken = { - idNot: idNotUser.idNot, - authToken, - idNotUser, - pairingId: null, - defaultStorage: null, - createdAt: Date.now(), - expiresAt: Date.now() + (24 * 60 * 60 * 1000) - }; - authTokens.push(tokenData); - - return { idNotUser, authToken }; - } + // Mock désactivé: suppression du bypass IDNOT_MOCK // Exchange code for tokens const tokens = await IdNotService.exchangeCodeForTokens(code); + // Optional diagnostic: fetch userinfo to validate access_token + try { + if (tokens.access_token) { + const userinfo = await IdNotService.getUserInfo(tokens.access_token); + Logger.info('Userinfo fetched', { userinfoKeys: Object.keys(userinfo || {}) }); + } + } catch (e) { + Logger.warn('Userinfo fetch failed (non-blocking)', { error: e instanceof Error ? e.message : String(e) }); + } + Logger.info('Token exchange successful', { hasAccessToken: !!tokens.access_token, hasIdToken: !!tokens.id_token, diff --git a/src/handlers/idnot-callback.handlers.ts b/src/handlers/idnot-callback.handlers.ts index 4fd8f16..db9860d 100644 --- a/src/handlers/idnot-callback.handlers.ts +++ b/src/handlers/idnot-callback.handlers.ts @@ -40,20 +40,18 @@ export class IdNotCallbackHandlers { const payload = StateService.verifyState(state); - // If external IdNot access is unavailable, allow mock bypass when enabled - const mockEnabled = process.env.IDNOT_MOCK === '1'; - if (mockEnabled) { - const { authToken } = await IdNotController.authenticate(code); - const url = new URL(payload.next_url); - const hash = url.hash ? url.hash.replace(/^#/, '') + `&authToken=${encodeURIComponent(authToken)}` : `authToken=${encodeURIComponent(authToken)}`; - const redirectTo = `${url.origin}${url.pathname}${url.search}#${hash}`; - return res.redirect(302, redirectTo); - } + // Mock désactivé: suppression du bypass IDNOT_MOCK // Exchange code using existing controller logic to build auth and user const { authToken } = await IdNotController.authenticate(code); const url = new URL(payload.next_url); + // Normalisation du chemin pour dev4: forcer le préfixe /lecoffre si absent + try { + if (url.hostname === 'dev4.4nkweb.com' && url.pathname === '/authorized-client') { + url.pathname = '/lecoffre/authorized-client'; + } + } catch {} // Prefer fragment to avoid leaking in server logs const hash = url.hash ? url.hash.replace(/^#/, '') + `&authToken=${encodeURIComponent(authToken)}` : `authToken=${encodeURIComponent(authToken)}`; const redirectTo = `${url.origin}${url.pathname}${url.search}#${hash}`; @@ -61,5 +59,3 @@ export class IdNotCallbackHandlers { res.redirect(302, redirectTo); }); } - - diff --git a/src/services/idnot/index.ts b/src/services/idnot/index.ts index 9f4a946..5176833 100644 --- a/src/services/idnot/index.ts +++ b/src/services/idnot/index.ts @@ -78,12 +78,57 @@ export class IdNotService { }); if (!response.ok) { + try { + const contentType = response.headers.get('content-type') || ''; + const bodyText = await (response as any).text().catch(() => undefined); + const snippet = bodyText ? bodyText.slice(0, 200) : undefined; + Logger.error('IdNot token exchange non-OK response', { + url: IDNOT_TOKEN_URL, + status: response.status, + statusText: response.statusText, + contentType, + bodySnippet: snippet + }); + } catch {} throw new Error(`Token exchange failed: ${response.status} ${response.statusText}`); } return response.json(); } + static async getUserInfo(accessToken: string) { + const { IDNOT_USERINFO_URL } = process.env; + if (!IDNOT_USERINFO_URL) { + throw new Error('Missing IDNOT_USERINFO_URL'); + } + + const response = await fetch(IDNOT_USERINFO_URL, { + method: 'GET', + headers: { + 'Accept': 'application/json', + 'Authorization': `Bearer ${accessToken}` + } + }); + + if (!response.ok) { + try { + const contentType = response.headers.get('content-type') || ''; + const bodyText = await (response as any).text().catch(() => undefined); + const snippet = bodyText ? bodyText.slice(0, 200) : undefined; + Logger.error('IdNot userinfo non-OK response', { + url: IDNOT_USERINFO_URL, + status: response.status, + statusText: response.statusText, + contentType, + bodySnippet: snippet + }); + } catch {} + throw new ExternalServiceError('IdNot', `Failed to fetch userinfo: ${response.status} ${response.statusText}`); + } + + return await IdNotService.parseJsonOrThrow(response as any, 'getUserInfo'); + } + static async getUserRattachements(idNot: string) { const { IDNOT_API_KEY, IDNOT_ANNUARY_BASE_URL } = process.env; @@ -139,10 +184,10 @@ export class IdNotService { } static async getUserData(profileIdn: string) { - const { IDNOT_API_KEY, IDNOT_API_BASE_URL } = process.env; + const { IDNOT_API_KEY, IDNOT_ANNUARY_BASE_URL } = process.env; - if (!IDNOT_API_KEY || !IDNOT_API_BASE_URL) { - throw new Error('Missing IDnot API key or base URL'); + if (!IDNOT_API_KEY || !IDNOT_ANNUARY_BASE_URL) { + throw new Error('Missing IDnot API key or annuary base URL'); } const searchParams = new URLSearchParams({ @@ -155,8 +200,24 @@ export class IdNotService { if (process.env.IDNOT_CONTEXT_HEADER && process.env.IDNOT_CONTEXT_VALUE) { headers[process.env.IDNOT_CONTEXT_HEADER] = process.env.IDNOT_CONTEXT_VALUE; } - const response = await fetch(`${IDNOT_API_BASE_URL}/api/pp/v2/rattachements/${profileIdn}?` + searchParams, { method: 'GET', headers }); + const fullUrl = `${IDNOT_ANNUARY_BASE_URL}/api/pp/v2/rattachements/${profileIdn}?` + searchParams; + const response = await fetch(fullUrl, { method: 'GET', headers }); if (!response.ok) { + try { + const contentType = response.headers.get('content-type') || ''; + const bodyText = await (response as any).text().catch(() => undefined); + const snippet = bodyText ? bodyText.slice(0, 200) : undefined; + const safeUrl = fullUrl.replace(/(key=)[^&]+/i, '$1***'); + Logger.error('IdNot getUserData non-OK response', { + context: 'getUserData', + profileIdn, + url: safeUrl, + status: response.status, + statusText: response.statusText, + contentType, + bodySnippet: snippet + }); + } catch {} throw new ExternalServiceError('IdNot', `Failed to fetch user data: ${response.status} ${response.statusText}`); } return await IdNotService.parseJsonOrThrow(response as any, 'getUserData');