IA align
All checks were successful
Build and Push to Registry / build-and-push (push) Successful in 50s

This commit is contained in:
NicolasCantu 2025-09-24 12:18:09 +02:00
parent 02820b2862
commit efe9dbde1b
18 changed files with 10676 additions and 56 deletions

48
.env.backup Normal file
View File

@ -0,0 +1,48 @@
# Configuration OVH
OVH_APP_KEY=5ab0709bbb65ef26
OVH_APP_SECRET=de1fac1779d707d263a611a557cd5766
OVH_CONSUMER_KEY=5fe817829b8a9c780cfa2354f8312ece
OVH_SMS_SERVICE_NAME=sms-tt802880-1
# Configuration SMS Factor
SMS_FACTOR_TOKEN=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI4NzgzNiIsImlhdCI6MTcwMTMzOTY1Mi45NDUzOH0.GNoqLb5MDBWuniNlQjbr1PKolwxGqBZe_tf4IMObvHw
#Configuration Mailchimp
MAILCHIMP_API_KEY=md-VVfaml-ApIV4nsGgaJKl0A
MAILCHIMP_KEY=3fa54304bc766dfd0b8043a827b28a3a-us17
MAILCHIMP_LIST_ID=a48d9ad852
#Configuration Stripe
STRIPE_SECRET_KEY=sk_test_51OwKmMP5xh1u9BqSeFpqw0Yr15hHtFsh0pvRGaE0VERhlYtvw33ND1qiGA6Dy1DPmmV61B6BqIimlhuv7bwElhjF00PLQwD60n
STRIPE_WEBHOOK_SECRET=
STRIPE_STANDARD_SUBSCRIPTION_PRICE_ID=price_1P66fuP5xh1u9BqSHj0O6Uy3
STRIPE_STANDARD_ANNUAL_SUBSCRIPTION_PRICE_ID=price_1P9NsRP5xh1u9BqSFgkUDbQY
STRIPE_UNLIMITED_SUBSCRIPTION_PRICE_ID=price_1P66RqP5xh1u9BqSuUzkQNac
STRIPE_UNLIMITED_ANNUAL_SUBSCRIPTION_PRICE_ID=price_1P9NpKP5xh1u9BqSApFogvUB
# Configuration serveur
APP_HOST=https://demo.4nkweb.com
PORT=
# Configuration front-end
NEXT_PUBLIC_4NK_URL=https://demo.4nkweb.com
NEXT_PUBLIC_FRONT_APP_HOST=https://demo.4nkweb.com
NEXT_PUBLIC_IDNOT_BASE_URL=https://qual-connexion.idnot.fr
NEXT_PUBLIC_IDNOT_AUTHORIZE_ENDPOINT=/IdPOAuth2/authorize/idnot_idp_v1
NEXT_PUBLIC_IDNOT_CLIENT_ID=4501646203F3EF67
NEXT_PUBLIC_BACK_API_PROTOCOL=http
NEXT_PUBLIC_BACK_API_HOST=localhost
BACK_API_PORT=8081
BACK_API_ROOT_URL=/api
BACK_API_VERSION=/v1
# Configuration idnot
IDNOT_ANNUARY_BASE_URL='https://qual-api.notaires.fr/annuaire'
IDNOT_API_KEY='ba557f84-0bf6-4dbf-844f-df2767555e3e'
# Configuration PostgreSQL
DB_HOST=
DB_PORT=
DB_NAME=
DB_USER=
DB_PASSWORD=

58
.env.backup2 Normal file
View File

@ -0,0 +1,58 @@
# Configuration OVH
OVH_APP_KEY=5ab0709bbb65ef26
OVH_APP_SECRET=de1fac1779d707d263a611a557cd5766
OVH_CONSUMER_KEY=5fe817829b8a9c780cfa2354f8312ece
OVH_SMS_SERVICE_NAME=sms-tt802880-1
# Configuration SMS Factor
SMS_FACTOR_TOKEN=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI4NzgzNiIsImlhdCI6MTcwMTMzOTY1Mi45NDUzOH0.GNoqLb5MDBWuniNlQjbr1PKolwxGqBZe_tf4IMObvHw
#Configuration Mailchimp
MAILCHIMP_API_KEY=md-VVfaml-ApIV4nsGgaJKl0A
MAILCHIMP_KEY=3fa54304bc766dfd0b8043a827b28a3a-us17
MAILCHIMP_LIST_ID=a48d9ad852
#Configuration Stripe
STRIPE_SECRET_KEY=sk_test_51OwKmMP5xh1u9BqSeFpqw0Yr15hHtFsh0pvRGaE0VERhlYtvw33ND1qiGA6Dy1DPmmV61B6BqIimlhuv7bwElhjF00PLQwD60n
STRIPE_WEBHOOK_SECRET=
STRIPE_STANDARD_SUBSCRIPTION_PRICE_ID=price_1P66fuP5xh1u9BqSHj0O6Uy3
STRIPE_STANDARD_ANNUAL_SUBSCRIPTION_PRICE_ID=price_1P9NsRP5xh1u9BqSFgkUDbQY
STRIPE_UNLIMITED_SUBSCRIPTION_PRICE_ID=price_1P66RqP5xh1u9BqSuUzkQNac
STRIPE_UNLIMITED_ANNUAL_SUBSCRIPTION_PRICE_ID=price_1P9NpKP5xh1u9BqSApFogvUB
# Configuration serveur
APP_HOST=https://demo.4nkweb.com
PORT=
# Configuration front-end
NEXT_PUBLIC_4NK_URL=https://demo.4nkweb.com
NEXT_PUBLIC_FRONT_APP_HOST=https://demo.4nkweb.com
NEXT_PUBLIC_IDNOT_BASE_URL=https://qual-connexion.idnot.fr
NEXT_PUBLIC_IDNOT_AUTHORIZE_ENDPOINT=/IdPOAuth2/authorize/idnot_idp_v1
NEXT_PUBLIC_IDNOT_CLIENT_ID=4501646203F3EF67
NEXT_PUBLIC_BACK_API_PROTOCOL=http
NEXT_PUBLIC_BACK_API_HOST=localhost
BACK_API_PORT=8081
BACK_API_ROOT_URL=/api
BACK_API_VERSION=/v1
# Configuration idnot
IDNOT_ANNUARY_BASE_URL='https://qual-api.notaires.fr/annuaire'
IDNOT_API_KEY='ba557f84-0bf6-4dbf-844f-df2767555e3e'
# Configuration PostgreSQL
DB_HOST=
DB_PORT=
DB_NAME=
DB_USER=
DB_PASSWORD=
# Variables IdNot manquantes pour l'authentification
IDNOT_API_BASE_URL='https://qual-api.notaires.fr'
IDNOT_REDIRECT_URI='https://dev4.4nkweb.com/lecoffre/authorized-client'
IDNOT_TOKEN_URL='https://qual-connexion.idnot.fr/IdPOAuth2/token/idnot_idp_v1'
IDNOT_API_KEY=ba557f84-0bf6-4dbf-844f-df2767555e3e
IDNOT_CLIENT_ID=B3CE56353EDB15A9
IDNOT_CLIENT_SECRET=3F733549E879878344B6C949B366BB5CDBB2DB5B7F7AB7EBBEBB0F0DD0776D1C
NEXT_PUBLIC_IDNOT_CLIENT_ID=B3CE56353EDB15A9

57
.env.bak Normal file
View File

@ -0,0 +1,57 @@
# Configuration OVH
OVH_APP_KEY=5ab0709bbb65ef26
OVH_APP_SECRET=de1fac1779d707d263a611a557cd5766
OVH_CONSUMER_KEY=5fe817829b8a9c780cfa2354f8312ece
OVH_SMS_SERVICE_NAME=sms-tt802880-1
# Configuration SMS Factor
SMS_FACTOR_TOKEN=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI4NzgzNiIsImlhdCI6MTcwMTMzOTY1Mi45NDUzOH0.GNoqLb5MDBWuniNlQjbr1PKolwxGqBZe_tf4IMObvHw
#Configuration Mailchimp
MAILCHIMP_API_KEY=md-VVfaml-ApIV4nsGgaJKl0A
MAILCHIMP_KEY=3fa54304bc766dfd0b8043a827b28a3a-us17
MAILCHIMP_LIST_ID=a48d9ad852
#Configuration Stripe
STRIPE_SECRET_KEY=sk_test_51OwKmMP5xh1u9BqSeFpqw0Yr15hHtFsh0pvRGaE0VERhlYtvw33ND1qiGA6Dy1DPmmV61B6BqIimlhuv7bwElhjF00PLQwD60n
STRIPE_WEBHOOK_SECRET=
STRIPE_STANDARD_SUBSCRIPTION_PRICE_ID=price_1P66fuP5xh1u9BqSHj0O6Uy3
STRIPE_STANDARD_ANNUAL_SUBSCRIPTION_PRICE_ID=price_1P9NsRP5xh1u9BqSFgkUDbQY
STRIPE_UNLIMITED_SUBSCRIPTION_PRICE_ID=price_1P66RqP5xh1u9BqSuUzkQNac
STRIPE_UNLIMITED_ANNUAL_SUBSCRIPTION_PRICE_ID=price_1P9NpKP5xh1u9BqSApFogvUB
# Configuration serveur
APP_HOST=https://demo.4nkweb.com
PORT=
# Configuration front-end
NEXT_PUBLIC_4NK_URL=https://demo.4nkweb.com
NEXT_PUBLIC_FRONT_APP_HOST=https://demo.4nkweb.com
NEXT_PUBLIC_IDNOT_BASE_URL=https://qual-connexion.idnot.fr
NEXT_PUBLIC_IDNOT_AUTHORIZE_ENDPOINT=/IdPOAuth2/authorize/idnot_idp_v1
NEXT_PUBLIC_BACK_API_PROTOCOL=http
NEXT_PUBLIC_BACK_API_HOST=localhost
BACK_API_PORT=8081
BACK_API_ROOT_URL=/api
BACK_API_VERSION=/v1
# Configuration idnot
IDNOT_ANNUARY_BASE_URL='https://qual-api.notaires.fr/annuaire'
IDNOT_API_KEY='ba557f84-0bf6-4dbf-844f-df2767555e3e'
# Configuration PostgreSQL
DB_HOST=
DB_PORT=
DB_NAME=
DB_USER=
DB_PASSWORD=
# Variables IdNot manquantes pour l'authentification
IDNOT_API_BASE_URL='https://qual-api.notaires.fr'
IDNOT_REDIRECT_URI='https://lecoffreio.4nkweb.com/authorized-client'
IDNOT_TOKEN_URL='https://qual-connexion.idnot.fr/user/IdPOAuth2/token/idnot_idp_v1'
IDNOT_API_KEY=ba557f84-0bf6-4dbf-844f-df2767555e3e
IDNOT_CLIENT_ID=B3CE56353EDB15A9
IDNOT_CLIENT_SECRET=3F733549E879878344B6C949B366BB5CDBB2DB5B7F7AB7EBBEBB0F0DD0776D1C
NEXT_PUBLIC_IDNOT_CLIENT_ID=B3CE56353EDB15A9

63
.env.bak.1758708401 Normal file
View File

@ -0,0 +1,63 @@
# Configuration OVH
OVH_APP_KEY=5ab0709bbb65ef26
OVH_APP_SECRET=de1fac1779d707d263a611a557cd5766
OVH_CONSUMER_KEY=5fe817829b8a9c780cfa2354f8312ece
OVH_SMS_SERVICE_NAME=sms-tt802880-1
# Configuration SMS Factor
SMS_FACTOR_TOKEN=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI4NzgzNiIsImlhdCI6MTcwMTMzOTY1Mi45NDUzOH0.GNoqLb5MDBWuniNlQjbr1PKolwxGqBZe_tf4IMObvHw
#Configuration Mailchimp
MAILCHIMP_API_KEY=md-VVfaml-ApIV4nsGgaJKl0A
MAILCHIMP_KEY=3fa54304bc766dfd0b8043a827b28a3a-us17
MAILCHIMP_LIST_ID=a48d9ad852
#Configuration Stripe
STRIPE_SECRET_KEY=sk_test_51OwKmMP5xh1u9BqSeFpqw0Yr15hHtFsh0pvRGaE0VERhlYtvw33ND1qiGA6Dy1DPmmV61B6BqIimlhuv7bwElhjF00PLQwD60n
STRIPE_WEBHOOK_SECRET=
STRIPE_STANDARD_SUBSCRIPTION_PRICE_ID=price_1P66fuP5xh1u9BqSHj0O6Uy3
STRIPE_STANDARD_ANNUAL_SUBSCRIPTION_PRICE_ID=price_1P9NsRP5xh1u9BqSFgkUDbQY
STRIPE_UNLIMITED_SUBSCRIPTION_PRICE_ID=price_1P66RqP5xh1u9BqSuUzkQNac
STRIPE_UNLIMITED_ANNUAL_SUBSCRIPTION_PRICE_ID=price_1P9NpKP5xh1u9BqSApFogvUB
# Configuration serveur
APP_HOST=https://demo.4nkweb.com
PORT=
# Configuration front-end
NEXT_PUBLIC_4NK_URL=https://demo.4nkweb.com
NEXT_PUBLIC_FRONT_APP_HOST=https://demo.4nkweb.com
NEXT_PUBLIC_IDNOT_BASE_URL=https://qual-connexion.idnot.fr
NEXT_PUBLIC_IDNOT_AUTHORIZE_ENDPOINT=/IdPOAuth2/authorize/idnot_idp_v1
NEXT_PUBLIC_BACK_API_PROTOCOL=http
NEXT_PUBLIC_BACK_API_HOST=localhost
BACK_API_PORT=8081
BACK_API_ROOT_URL=/api
BACK_API_VERSION=/v1
# Configuration idnot
IDNOT_ANNUARY_BASE_URL='https://qual-api.notaires.fr/annuaire'
IDNOT_API_KEY='ba557f84-0bf6-4dbf-844f-df2767555e3e'
# Configuration PostgreSQL
DB_HOST=
DB_PORT=
DB_NAME=
DB_USER=
DB_PASSWORD=
# Variables IdNot manquantes pour l'authentification
IDNOT_API_BASE_URL='https://qual-api.notaires.fr'
IDNOT_REDIRECT_URI='https://lecoffreio.4nkweb.com/authorized-client'
IDNOT_TOKEN_URL='https://qual-connexion.idnot.fr/user/IdPOAuth2/token/idnot_idp_v1'
IDNOT_API_KEY=ba557f84-0bf6-4dbf-844f-df2767555e3e
IDNOT_CLIENT_ID=B3CE56353EDB15A9
IDNOT_CLIENT_SECRET=3F733549E879878344B6C949B366BB5CDBB2DB5B7F7AB7EBBEBB0F0DD0776D1C
NEXT_PUBLIC_IDNOT_CLIENT_ID=B3CE56353EDB15A9
STATE_TTL_SECONDS=180
ALLOW_LOCALHOST_REDIRECTS=true
ALLOWED_REDIRECT_HOST_PATTERNS=^lecoffreio.4nkweb.com$
BACK_HMAC_SECRET=7e0f4a8b7c9d3e2fb6c1a5d4e8f09b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f70

View File

@ -1,7 +1,38 @@
server {
listen 443 ssl;
server_name dev3.4nkweb.com;
# Callback IdNot -> backend, avec CORS dynamique et masquage des en-têtes upstream
location = /idnot/callback {
# 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: autorise dev4, lecoffreio.4nkweb.com, localhost:3000 et sous-domaines *.4nkweb.com
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" 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" 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;
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
@ -35,7 +66,7 @@ server {
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-NginX-Proxy true;
proxy_read_timeout 86400;
proxy_read_timeout 86400;
proxy_set_header Connection "Upgrade";
}
@ -63,6 +94,35 @@ server {
}
location ^~ /api/ {
# Masquer les en-têtes CORS de 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: autorise dev4, lecoffreio.4nkweb.com, localhost:3000 et sous-domaines *.4nkweb.com
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" 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" 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;
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
@ -100,5 +160,3 @@ server {
}

View File

@ -0,0 +1,176 @@
server {
listen 443 ssl;
server_name dev3.4nkweb.com;
location = /idnot/callback {
# Hide upstream CORS headers
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;
# Dynamic CORS allowlist
set $cors_origin "";
if ($http_origin ~* ^(https://dev4\.4nkweb\.com|http://local\.(4nkweb|4nkdev)\.com:3000|http://localhost:3000|https://.*\.4nkweb\.com)$) {
set $cors_origin $http_origin;
}
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-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
add_header Vary "Origin, Access-Control-Request-Method, Access-Control-Request-Headers" always;
return 204;
}
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-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
add_header Vary "Origin, Access-Control-Request-Method, Access-Control-Request-Headers" 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;
}
ssl_certificate /etc/letsencrypt/live/dev3.4nkweb.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/dev3.4nkweb.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers HIGH:!aNULL:!MD5;
# Redirection des requêtes HTTP normales vers Vite
location / {
proxy_pass http://localhost:3004;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
}
location /ws/ {
proxy_pass http://localhost:8090;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
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-NginX-Proxy true;
proxy_read_timeout 86400;
proxy_set_header Connection "Upgrade";
}
location /storage/ {
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization, X-Requested-With' always;
add_header 'Access-Control-Max-Age' 86400;
add_header 'Content-Length' 0;
add_header 'Content-Type' 'text/plain';
return 204;
}
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization, X-Requested-With' always;
rewrite ^/storage(/.*)$ $1 break;
proxy_pass http://localhost:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
}
location ^~ /api/ {
# Hide upstream CORS headers
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;
# Dynamic CORS allowlist
set $cors_origin "";
if ($http_origin ~* ^(https://dev4\.4nkweb\.com|http://local\.(4nkweb|4nkdev)\.com:3000|http://localhost:3000|https://.*\.4nkweb\.com)$) {
set $cors_origin $http_origin;
}
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-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
add_header Vary "Origin, Access-Control-Request-Method, Access-Control-Request-Headers" always;
return 204;
}
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-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
add_header Vary "Origin, Access-Control-Request-Method, Access-Control-Request-Headers" 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;
}
location @handle_502 {
internal;
add_header Access-Control-Allow-Origin "*" always;
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS, PUT, DELETE" always;
add_header Access-Control-Allow-Headers "Authorization, Content-Type, Accept, X-Requested-With" always;
return 502;
}
}
server {
if ($host = dev3.4nkweb.com) {
return 301 https://$host$request_uri;
} # managed by Certbot
listen 80;
server_name dev3.4nkweb.com;
location = /idnot/callback {
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;
}
return 301 https://$host$request_uri;
}
server {
if ($host = dev3.4nkweb.com) {
return 301 https://$host$request_uri;
} # managed by Certbot
listen 80;
server_name dev3.4nkweb.com;
location = /idnot/callback {
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;
}
return 301 https://$host$request_uri;
}

View File

@ -0,0 +1,45 @@
server {
listen 443 ssl;
server_name dev3.4nkweb.com;
location = /idnot/callback {
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;
}
ssl_certificate /etc/letsencrypt/live/dev3.4nkweb.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/dev3.4nkweb.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers HIGH:!aNULL:!MD5;
# Redirection des requêtes HTTP normales vers Vite
location / {
proxy_pass http://localhost:3004;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
}
location /ws/ {
proxy_pass http://localhost:8090;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
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-NginX-Proxy true;
proxy_read_timeout 86400;
proxy_set_header Connection "Upgrade";
}
location /storage/ {
if ($request_method = OPTIONS) {
add_header Access-Control-Allow-Origin * always;
add_header Access-Control-Allow-Methods GET,

9813
logs/backend.out Normal file

File diff suppressed because it is too large Load Diff

View File

@ -6,11 +6,58 @@ export const config = {
port: process.env.PORT || 8080,
defaultStorage: process.env.DEFAULT_STORAGE || 'https://dev3.4nkweb.com/storage',
appHost: process.env.APP_HOST || 'http://localhost:3000',
cors: {
origin: 'http://localhost:3000',
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'x-session-id', 'Authorization'],
credentials: true
}
// Déterminer dynamiquement l'origine CORS à partir d'APP_HOST si possible
cors: (() => {
// Build a whitelist of allowed origins
const envList = (process.env.CORS_ORIGINS || process.env.CORS_ORIGIN || '')
.split(',')
.map(s => s.trim())
.filter(Boolean);
// Include APP_HOST origin if present
try {
const appHost = process.env.APP_HOST || 'http://localhost:3000';
const appOrigin = new URL(appHost).origin;
if (!envList.includes(appOrigin)) envList.push(appOrigin);
} catch {}
// Common dev frontends (dev4 and local redirection constraint)
const defaults = ['https://dev4.4nkweb.com', 'https://lecoffreio.4nkweb.com'];
for (const o of defaults) {
if (!envList.includes(o)) envList.push(o);
}
// Regex allow-list from env (comma-separated), fallback to *.4nkweb.com and lecoffreio.4nkweb.com
const regexFromEnv = (process.env.CORS_ORIGINS_REGEX || '')
.split(',')
.map(s => s.trim())
.filter(Boolean)
.map(pattern => {
try { return new RegExp(pattern); } catch { return null; }
})
.filter((r): r is RegExp => !!r);
const defaultRegexList: RegExp[] = [
/^https:\/\/.*\.4nkweb\.com$/,
/^http:\/\/local\.4nkweb\.com:3000$/
];
const regexList = regexFromEnv.length > 0 ? regexFromEnv : defaultRegexList;
// Always allow same-origin or non-browser clients (no Origin header)
const origin = (requestOrigin: string | undefined, callback: (err: Error | null, allow?: boolean | string | RegExp | (string | RegExp)[]) => void) => {
if (!requestOrigin) return callback(null, true);
if (envList.includes(requestOrigin)) return callback(null, requestOrigin);
if (regexList.some(re => re.test(requestOrigin))) return callback(null, requestOrigin);
return callback(null, false);
};
return {
origin,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'x-session-id', 'Authorization'],
credentials: true
};
})()
};

View File

@ -11,7 +11,7 @@ import { NotFoundError, ExternalServiceError, BusinessRuleError } from '../types
* without depending on Express Request/Response objects
*/
export class IdNotController {
/**
* Get user rattachements by idNot
*/
@ -51,9 +51,9 @@ export class IdNotController {
}
}));
Logger.info('Successfully retrieved user rattachements', {
Logger.info('Successfully retrieved user rattachements', {
idNot,
count: officeData.length
count: officeData.length
});
return officeData;
@ -67,12 +67,12 @@ export class IdNotController {
try {
const result = await IdNotService.getOfficeRattachements(idNot);
Logger.info('Successfully retrieved office rattachements', {
idNot,
count: result.result?.length || 0
Logger.info('Successfully retrieved office rattachements', {
idNot,
count: result.result?.length || 0
});
return result;
} catch (error) {
Logger.error('Failed to get office rattachements', {
@ -93,6 +93,12 @@ export class IdNotController {
// Exchange code for tokens
const tokens = await IdNotService.exchangeCodeForTokens(code);
Logger.info('Token exchange successful', {
hasAccessToken: !!tokens.access_token,
hasIdToken: !!tokens.id_token,
tokenKeys: Object.keys(tokens)
});
const jwt = tokens.id_token;
if (!jwt) {
throw new BusinessRuleError('No ID token received from IdNot');
@ -101,16 +107,23 @@ export class IdNotController {
// Decode JWT payload
const payload = JSON.parse(Buffer.from(jwt.split('.')[1], 'base64').toString('utf8'));
Logger.info('JWT payload decoded', {
payloadKeys: Object.keys(payload),
hasProfileIdn: !!payload.profile_idn,
hasSub: !!payload.sub,
hasEntityIdn: !!payload.entity_idn
});
// Get user data
const userData = await IdNotService.getUserData(payload.profile_idn);
if (!userData || !userData.statutDuRattachement || userData.entite.typeEntite.name !== 'office') {
throw new BusinessRuleError('User not attached to an office');
}
// Get office location data
const officeLocationData = await IdNotService.getOfficeLocationData(userData.entite.locationsUrl);
if (!officeLocationData || !officeLocationData.result || officeLocationData.result.length === 0) {
throw new BusinessRuleError('Office location data not found');
}
@ -157,10 +170,10 @@ export class IdNotController {
createdAt: Date.now(),
expiresAt: Date.now() + (24 * 60 * 60 * 1000) // 24 hours
};
authTokens.push(tokenData);
Logger.info('IdNot authentication successful', {
Logger.info('IdNot authentication successful', {
idNot: idNotUser.idNot,
office: idNotUser.office.name
});
@ -172,11 +185,11 @@ export class IdNotController {
codePrefix: code.substring(0, 8) + '...',
error: error instanceof Error ? error.message : 'Unknown error'
});
if (error instanceof BusinessRuleError || error instanceof ExternalServiceError) {
throw error;
}
throw new ExternalServiceError('IdNot', `Authentication failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
@ -189,12 +202,12 @@ export class IdNotController {
// Find the full token data
const userAuth = authTokens.find(auth => auth.authToken === authToken);
if (!userAuth || !userAuth.idNotUser) {
throw new NotFoundError('User data not found. Please log in again.');
}
Logger.info('Current user data retrieved', {
Logger.info('Current user data retrieved', {
idNot,
office: userAuth.idNotUser.office.name
});
@ -213,16 +226,16 @@ export class IdNotController {
// Remove the auth token from the array
const tokenIndex = authTokens.findIndex(auth => auth.authToken === authToken);
if (tokenIndex > -1) {
const removedToken = authTokens.splice(tokenIndex, 1)[0];
Logger.info('User logout successful', {
idNot: removedToken.idNot
Logger.info('User logout successful', {
idNot: removedToken.idNot
});
} else {
Logger.warn('Logout attempted with invalid token');
}
return {
success: true,
message: 'Déconnexion réussie'

View File

@ -0,0 +1,55 @@
import { Request, Response } from 'express';
import { asyncHandler } from '../middleware/error-handler';
import { StateService } from '../services/state.service';
import { IdNotController } from '../controllers/idnot.controller';
import { ExternalServiceError, ValidationError } from '../types/errors';
export class IdNotCallbackHandlers {
static callback = asyncHandler(async (req: Request, res: Response): Promise<void> => {
const code = req.query.code as string | undefined;
const state = req.query.state as string | undefined;
// Debug logging to inspect what reaches the backend via Nginx
// Note: keep logs concise to avoid leaking sensitive data in production
try {
// Only log presence/length of params to avoid full values in logs
const safeQuery = {
code_present: typeof code === 'string',
code_length: typeof code === 'string' ? code.length : undefined,
state_present: typeof state === 'string',
state_length: typeof state === 'string' ? state.length : undefined
};
console.info('[IdNotCallback] incoming request', {
originalUrl: req.originalUrl,
method: req.method,
query: safeQuery,
headers: {
host: req.headers.host,
'x-forwarded-for': req.headers['x-forwarded-for'],
'x-forwarded-proto': req.headers['x-forwarded-proto']
}
});
} catch {}
if (!code || !state) {
throw new ValidationError('Missing code or state', [
{ field: 'code', value: code, constraints: ['required'] },
{ field: 'state', value: state, constraints: ['required'] }
]);
}
const payload = StateService.verifyState(state);
// Exchange code using existing controller logic to build auth and user
const { authToken } = await IdNotController.authenticate(code);
const url = new URL(payload.next_url);
// 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}`;
res.redirect(302, redirectTo);
});
}

View File

@ -4,21 +4,21 @@ import { asyncHandler } from '../middleware/error-handler';
import { ValidationError, BusinessRuleError } from '../types/errors';
/**
* Route handlers that extract and validate HTTP request data
* Route handlers that extract and validate HTTP request data
* before calling pure controller methods
*/
export class IdNotHandlers {
/**
* GET /user/rattachements
* Extract idNot from query params and call controller
*/
static getUserRattachements = asyncHandler(async (req: Request, res: Response): Promise<void> => {
const requestId = req.headers['x-request-id'] as string;
// Extract and validate parameters
const { idNot } = req.query;
if (!idNot || typeof idNot !== 'string') {
throw new ValidationError('idNot parameter is required', [
{ field: 'idNot', value: idNot, constraints: ['Must be a valid string'] }
@ -27,20 +27,20 @@ export class IdNotHandlers {
// Call pure controller method with extracted parameters
const result = await IdNotController.getUserRattachements(idNot);
res.json(result);
});
/**
* GET /office/rattachements
* GET /office/rattachements
* Extract idNot from query params and call controller
*/
static getOfficeRattachements = asyncHandler(async (req: Request, res: Response): Promise<void> => {
const requestId = req.headers['x-request-id'] as string;
// Extract and validate parameters
const { idNot } = req.query;
if (!idNot || typeof idNot !== 'string') {
throw new ValidationError('idNot parameter is required', [
{ field: 'idNot', value: idNot, constraints: ['Must be a valid string'] }
@ -49,20 +49,20 @@ export class IdNotHandlers {
// Call pure controller method
const result = await IdNotController.getOfficeRattachements(idNot);
res.json(result);
});
/**
* POST /auth/:code
* Extract code from URL params and call controller
* POST /auth
* Extract code from request body and call controller
*/
static authenticate = asyncHandler(async (req: Request, res: Response): Promise<void> => {
const requestId = req.headers['x-request-id'] as string;
// Extract and validate parameters
const { code } = req.params;
// Extract and validate parameters from body
const { code } = req.body;
if (!code || typeof code !== 'string' || code.length < 10) {
throw new ValidationError('Invalid authentication code', [
{ field: 'code', value: code, constraints: ['Must be a valid authorization code'] }
@ -71,7 +71,7 @@ export class IdNotHandlers {
// Call pure controller method
const result = await IdNotController.authenticate(code);
res.json(result);
});
@ -81,17 +81,17 @@ export class IdNotHandlers {
*/
static getCurrentUser = asyncHandler(async (req: Request, res: Response): Promise<void> => {
const requestId = req.headers['x-request-id'] as string;
// Extract user info (set by authenticateIdNot middleware)
if (!req.idNotUser) {
throw new BusinessRuleError('User authentication required', undefined, requestId);
}
const { idNot, authToken } = req.idNotUser;
// Call pure controller method
const result = await IdNotController.getCurrentUser(idNot, authToken);
res.json(result);
});
@ -101,16 +101,16 @@ export class IdNotHandlers {
*/
static logout = asyncHandler(async (req: Request, res: Response): Promise<void> => {
const requestId = req.headers['x-request-id'] as string;
if (!req.idNotUser) {
throw new BusinessRuleError('User authentication required', undefined, requestId);
}
const { authToken } = req.idNotUser;
// Call pure controller method
const result = await IdNotController.logout(authToken);
res.json(result);
});
@ -120,16 +120,16 @@ export class IdNotHandlers {
*/
static validateToken = asyncHandler(async (req: Request, res: Response): Promise<void> => {
const requestId = req.headers['x-request-id'] as string;
if (!req.idNotUser) {
throw new BusinessRuleError('User authentication required', undefined, requestId);
}
const { idNot } = req.idNotUser;
// Call pure controller method
const result = await IdNotController.validateToken(idNot);
res.json(result);
});
}

View File

@ -0,0 +1,24 @@
import { Request, Response } from 'express';
import { asyncHandler } from '../middleware/error-handler';
import { StateService } from '../services/state.service';
import { ValidationError } from '../types/errors';
export class StateHandlers {
static createState = asyncHandler(async (req: Request, res: Response): Promise<void> => {
const requestId = req.headers['x-request-id'] as string;
const { next_url } = req.body || {};
if (!next_url || typeof next_url !== 'string') {
throw new ValidationError('next_url is required', [
{ field: 'next_url', value: next_url, constraints: ['Must be a valid string URL'] }
], requestId);
}
const state = StateService.signState(next_url);
res.json({ state });
});
}

View File

@ -6,7 +6,7 @@ const router = Router();
router.get('/user/rattachements', IdNotHandlers.getUserRattachements);
router.get('/office/rattachements', IdNotHandlers.getOfficeRattachements);
router.post('/auth/:code', IdNotHandlers.authenticate);
router.post('/auth', IdNotHandlers.authenticate);
router.get('/user', authenticateIdNot, IdNotHandlers.getCurrentUser);
router.post('/logout', authenticateIdNot, IdNotHandlers.logout);

View File

@ -2,6 +2,8 @@ import { Router } from 'express';
import { healthRoutes } from './health.routes';
import { smsRoutes } from './sms.routes';
import { idnotRoutes } from './idnot.routes';
import { StateHandlers } from '../handlers/state.handlers';
import { IdNotCallbackHandlers } from '../handlers/idnot-callback.handlers';
import { emailRoutes } from './email.routes';
import { stripeRoutes } from './stripe.routes';
import { processRoutes } from './process.routes';
@ -16,4 +18,8 @@ router.use('/api/v1/process', processRoutes);
router.use('/api', emailRoutes);
router.use('/api', stripeRoutes);
// State and callback endpoints (front-agnostic)
router.post('/api/v1/idnot/state', StateHandlers.createState);
router.get('/idnot/callback', IdNotCallbackHandlers.callback);
export { router as routes };

23
src/routes/index.ts.bak Normal file
View File

@ -0,0 +1,23 @@
import { Router } from 'express';
import { healthRoutes } from './health.routes';
import { smsRoutes } from './sms.routes';
import { idnotRoutes } from './idnot.routes';
import { emailRoutes } from './email.routes';
import { stripeRoutes } from './stripe.routes';
import { processRoutes } from './process.routes';
const router = Router();
// Mount routes
router.use('/api/v1', healthRoutes);
router.use('/api', smsRoutes);
router.use('/api/v1/idnot', idnotRoutes);
router.use('/api/v1/process', processRoutes);
router.use('/api', emailRoutes);
router.use('/api', stripeRoutes);
export { router as routes };

View File

@ -22,6 +22,15 @@ export class IdNotService {
code
};
console.log('🔍 IdNot Token Request Debug:', {
url: IDNOT_TOKEN_URL,
client_id: IDNOT_CLIENT_ID,
redirect_uri: IDNOT_REDIRECT_URI,
grant_type: 'authorization_code',
code_length: code.length,
code_prefix: code.substring(0, 8) + '...'
});
const response = await fetch(IDNOT_TOKEN_URL, {
method: 'POST',
headers: {
@ -30,6 +39,12 @@ export class IdNotService {
body: new URLSearchParams(params).toString()
});
console.log('🔍 IdNot Token Response Debug:', {
status: response.status,
statusText: response.statusText,
headers: Object.fromEntries(response.headers.entries())
});
if (!response.ok) {
throw new Error(`Token exchange failed: ${response.status} ${response.statusText}`);
}

View File

@ -0,0 +1,119 @@
import crypto from 'crypto';
type StatePayload = {
next_url: string;
nonce: string;
ts: number;
};
const nonceCache = new Map<string, number>();
function base64url(input: Buffer | string): string {
const buf = Buffer.isBuffer(input) ? input : Buffer.from(input);
return buf.toString('base64').replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
}
function hmacSha256(data: string, secret: string): string {
return base64url(crypto.createHmac('sha256', secret).update(data).digest());
}
function isLocalhostHost(host: string): boolean {
return host === 'localhost' || host === '127.0.0.1';
}
function validateNextUrl(nextUrl: string): void {
const allowLocalhost = (process.env.ALLOW_LOCALHOST_REDIRECTS || 'false').toLowerCase() === 'true';
const patterns = (process.env.ALLOWED_REDIRECT_HOST_PATTERNS || '')
.split(',')
.map(s => s.trim())
.filter(Boolean)
.map(p => new RegExp(p));
let url: URL;
try {
url = new URL(nextUrl);
} catch {
throw new Error('Invalid next_url');
}
if (url.protocol !== 'https:' && !(allowLocalhost && url.protocol === 'http:' && isLocalhostHost(url.hostname))) {
throw new Error('next_url scheme not allowed');
}
if (patterns.length > 0) {
const ok = isLocalhostHost(url.hostname) || patterns.some(re => re.test(url.hostname));
if (!ok) {
throw new Error('next_url host not allowed');
}
}
if (nextUrl.length > 2048) {
throw new Error('next_url too long');
}
}
export const StateService = {
signState(nextUrl: string): string {
validateNextUrl(nextUrl);
const secret = process.env.BACK_HMAC_SECRET;
if (!secret) throw new Error('Missing BACK_HMAC_SECRET');
const payload: StatePayload = {
next_url: nextUrl,
nonce: crypto.randomBytes(16).toString('hex'),
ts: Date.now()
};
const payloadStr = JSON.stringify(payload);
const signature = hmacSha256(payloadStr, secret);
// store nonce for replay protection
const ttl = parseInt(process.env.STATE_TTL_SECONDS || '180', 10) * 1000;
nonceCache.set(payload.nonce, payload.ts + ttl);
return `${base64url(payloadStr)}.${signature}`;
},
verifyState(state: string): StatePayload {
const secret = process.env.BACK_HMAC_SECRET;
if (!secret) throw new Error('Missing BACK_HMAC_SECRET');
const parts = state.split('.');
if (parts.length !== 2) throw new Error('Invalid state format');
const [b64, sig] = parts;
const payloadJson = Buffer.from(b64.replace(/-/g, '+').replace(/_/g, '/'), 'base64').toString('utf8');
const expectedSig = hmacSha256(payloadJson, secret);
if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expectedSig))) {
throw new Error('Invalid state signature');
}
const payload = JSON.parse(payloadJson) as StatePayload;
// TTL
const ttlMs = parseInt(process.env.STATE_TTL_SECONDS || '180', 10) * 1000;
if (Date.now() - payload.ts > ttlMs) throw new Error('State expired');
// replay protection
const expiry = nonceCache.get(payload.nonce);
if (!expiry) throw new Error('Invalid nonce');
nonceCache.delete(payload.nonce);
// validate next_url again
validateNextUrl(payload.next_url);
return payload;
}
};
// periodic cleanup
setInterval(() => {
const now = Date.now();
for (const [nonce, expiry] of nonceCache.entries()) {
if (expiry <= now) nonceCache.delete(nonce);
}
}, 60000);