docs: add test data for login; feat: ID.Not dev flow tweaks; chore: nginx dev host adjustments
All checks were successful
Build and Push to Registry / build-and-push (push) Successful in 47s

This commit is contained in:
NicolasCantu 2025-09-26 15:12:34 +02:00
parent 7c5c4ab334
commit ba2c36c014
5 changed files with 462 additions and 56 deletions

View File

@ -23,7 +23,7 @@ server {
if ($request_method = OPTIONS) { if ($request_method = OPTIONS) {
add_header Access-Control-Allow-Origin $cors_origin always; add_header Access-Control-Allow-Origin $cors_origin always;
add_header Access-Control-Allow-Credentials "true" 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 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 Vary "Origin, Access-Control-Request-Method, Access-Control-Request-Headers" always;
return 204; return 204;
@ -32,7 +32,7 @@ server {
# En-têtes CORS pour les autres méthodes # En-têtes CORS pour les autres méthodes
add_header Access-Control-Allow-Origin $cors_origin always; add_header Access-Control-Allow-Origin $cors_origin always;
add_header Access-Control-Allow-Credentials "true" 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 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 Vary "Origin, Access-Control-Request-Method, Access-Control-Request-Headers" always;
add_header X-Request-Id $request_id always; add_header X-Request-Id $request_id always;
@ -45,6 +45,46 @@ server {
proxy_set_header X-Request-Id $request_id; 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 /etc/letsencrypt/live/dev3.4nkweb.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/dev3.4nkweb.com/privkey.pem; ssl_certificate_key /etc/letsencrypt/live/dev3.4nkweb.com/privkey.pem;

337
docs/data_account_test.md Normal file
View File

@ -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

View File

@ -97,49 +97,21 @@ export class IdNotController {
Logger.info('IdNot authentication initiated', { codePrefix: code.substring(0, 8) + '...' }); Logger.info('IdNot authentication initiated', { codePrefix: code.substring(0, 8) + '...' });
try { try {
// Development fallback: allow authentication without contacting IdNot // Mock désactivé: suppression du bypass IDNOT_MOCK
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 };
}
// Exchange code for tokens // Exchange code for tokens
const tokens = await IdNotService.exchangeCodeForTokens(code); 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', { Logger.info('Token exchange successful', {
hasAccessToken: !!tokens.access_token, hasAccessToken: !!tokens.access_token,
hasIdToken: !!tokens.id_token, hasIdToken: !!tokens.id_token,

View File

@ -40,20 +40,18 @@ export class IdNotCallbackHandlers {
const payload = StateService.verifyState(state); const payload = StateService.verifyState(state);
// If external IdNot access is unavailable, allow mock bypass when enabled // Mock désactivé: suppression du bypass IDNOT_MOCK
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);
}
// Exchange code using existing controller logic to build auth and user // Exchange code using existing controller logic to build auth and user
const { authToken } = await IdNotController.authenticate(code); const { authToken } = await IdNotController.authenticate(code);
const url = new URL(payload.next_url); 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 // Prefer fragment to avoid leaking in server logs
const hash = url.hash ? url.hash.replace(/^#/, '') + `&authToken=${encodeURIComponent(authToken)}` : `authToken=${encodeURIComponent(authToken)}`; const hash = url.hash ? url.hash.replace(/^#/, '') + `&authToken=${encodeURIComponent(authToken)}` : `authToken=${encodeURIComponent(authToken)}`;
const redirectTo = `${url.origin}${url.pathname}${url.search}#${hash}`; const redirectTo = `${url.origin}${url.pathname}${url.search}#${hash}`;
@ -61,5 +59,3 @@ export class IdNotCallbackHandlers {
res.redirect(302, redirectTo); res.redirect(302, redirectTo);
}); });
} }

View File

@ -78,12 +78,57 @@ export class IdNotService {
}); });
if (!response.ok) { 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}`); throw new Error(`Token exchange failed: ${response.status} ${response.statusText}`);
} }
return response.json(); 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) { static async getUserRattachements(idNot: string) {
const { IDNOT_API_KEY, IDNOT_ANNUARY_BASE_URL } = process.env; const { IDNOT_API_KEY, IDNOT_ANNUARY_BASE_URL } = process.env;
@ -139,10 +184,10 @@ export class IdNotService {
} }
static async getUserData(profileIdn: string) { 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) { if (!IDNOT_API_KEY || !IDNOT_ANNUARY_BASE_URL) {
throw new Error('Missing IDnot API key or base URL'); throw new Error('Missing IDnot API key or annuary base URL');
} }
const searchParams = new URLSearchParams({ const searchParams = new URLSearchParams({
@ -155,8 +200,24 @@ export class IdNotService {
if (process.env.IDNOT_CONTEXT_HEADER && process.env.IDNOT_CONTEXT_VALUE) { if (process.env.IDNOT_CONTEXT_HEADER && process.env.IDNOT_CONTEXT_VALUE) {
headers[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) { 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}`); throw new ExternalServiceError('IdNot', `Failed to fetch user data: ${response.status} ${response.statusText}`);
} }
return await IdNotService.parseJsonOrThrow(response as any, 'getUserData'); return await IdNotService.parseJsonOrThrow(response as any, 'getUserData');