Compare commits

...

48 Commits
main ... dev

Author SHA1 Message Date
4NK Dev
a1670cfc35 chore: doc updates [skip ci] 2025-10-02 15:31:55 +00:00
4NK Dev
cdf91d453f ci: docker_tag=dev-test - Centralisation des fichiers CODE_OF_CONDUCT, CODEOWNERS, CONTRIBUTING, LICENSE
All checks were successful
Build and Push to Registry / build-and-push (push) Successful in 47s
2025-10-01 21:05:39 +00:00
4NK Dev
26c33f00f2 ci: docker_tag=dev-test - Mise à jour documentation et standardisation
All checks were successful
Build and Push to Registry / build-and-push (push) Successful in 47s
2025-10-01 20:58:07 +00:00
ba2c36c014 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
2025-09-26 15:12:34 +02:00
7c5c4ab334 ci: docker_tag=dev-test
All checks were successful
Build and Push to Registry / build-and-push (push) Successful in 49s
chore(nginx): logs vhost + X-Request-Id; docs: logs & ops; idnot: headers Accept+Contexte; backups: snapshot, ports & http flows
2025-09-25 09:18:58 +02:00
dd45e99a80 feat(idnot): add mock mode for development without IdNot access
- Add IDNOT_MOCK=1 environment variable to bypass external IdNot calls
- Use real test account data: IDN187087, STON CARQUEIRANNE, CRPCEN 083079
- Mock authentication returns valid user data for development
- Allows testing auth flow without IdNot API access

[skip ci]
2025-09-24 22:31:51 +02:00
fd093aec65 feat(idnot): robust JSON handling with clear ExternalServiceError logs [skip ci] 2025-09-24 22:18:06 +02:00
125e9ac923 fix(idnot): resolve callback 502 error - add /authorized-client route
- Add GET /authorized-client route in backend (same handler as /idnot/callback)
- Update nginx lecoffreio.4nkweb.com config to proxy /authorized-client to backend
- Add nginx effective config snapshot
- Document resolution in docs/

Resolves: idnot callback 502 error on lecoffreio.4nkweb.com/authorized-client
Test: Route now returns 500 'State expired' (expected for old state) instead of 502

[skip ci]
2025-09-24 22:12:51 +02:00
ea83bc759a fix(idnot): add /authorized-client route for idnot callback [skip ci] 2025-09-24 21:56:20 +02:00
47f3da354a docs(issue): problème callback idnot 502 sur lecoffreio.4nkweb.com [skip ci] 2025-09-24 21:55:39 +02:00
7bb24d42ad docs(hmr): flux HMR avec idnot via dev3 et state [skip ci] 2025-09-24 19:52:19 +02:00
5e3fb8f07f clean
All checks were successful
Build and Push to Registry / build-and-push (push) Successful in 51s
2025-09-24 19:49:02 +02:00
bde308037b docs(ports): état des ports au 2025-09-24 [skip ci] 2025-09-24 19:47:09 +02:00
11a77054b0 chore(nginx): snapshot effective nginx config into confs/nginx/_effective_20250924-190412
All checks were successful
Build and Push to Registry / build-and-push (push) Successful in 55s
2025-09-24 19:04:12 +02:00
51ff0a00c9 chore(scripts): add nginx scrub script to remove local.4nkweb.com comments; backup system files
All checks were successful
Build and Push to Registry / build-and-push (push) Successful in 52s
2025-09-24 19:01:16 +02:00
235be939af chore(scripts): add nginx cleanup for local.4nkweb.com vhost and .bak
All checks were successful
Build and Push to Registry / build-and-push (push) Successful in 51s
2025-09-24 18:48:15 +02:00
f084e8a6fc docs(nginx): audit des confs et redirections au 2025-09-24
All checks were successful
Build and Push to Registry / build-and-push (push) Successful in 52s
2025-09-24 18:41:48 +02:00
61a6b166f7 fix(config): keep localhost:3000 defaults; remove only local.4nkweb.com usage
All checks were successful
Build and Push to Registry / build-and-push (push) Successful in 57s
2025-09-24 18:30:59 +02:00
f79b3f8f26 refactor(config): drop local.4nkweb.com (http) usage; enforce https lecoffreio.4nkweb.com\n\n- Remove CORS default for local.4nkweb.com:3000\n- Default APP_HOST to https://lecoffreio.4nkweb.com\n- Remove obsolete nginx conf
Some checks failed
Build and Push to Registry / build-and-push (push) Has been cancelled
2025-09-24 18:29:11 +02:00
0069d53946 chore(npm): add launch:check script
All checks were successful
Build and Push to Registry / build-and-push (push) Successful in 52s
2025-09-24 18:24:36 +02:00
8283185022 feat(scripts): add nginx conf backup and commit backups\n\nci: docker_tag=ext\n\nBackup local and project nginx configs into backups/nginx/<timestamp>/; include tarball. Ensure reproducible archives for audit.
All checks were successful
Build and Push to Registry / build-and-push (push) Successful in 51s
2025-09-24 18:06:10 +02:00
efe9dbde1b IA align
All checks were successful
Build and Push to Registry / build-and-push (push) Successful in 50s
2025-09-24 12:18:09 +02:00
02820b2862 chore(nginx): snapshot dev3 vhosts (callback IdNot → backend)
All checks were successful
Build and Push to Registry / build-and-push (push) Successful in 48s
2025-09-23 08:46:49 +02:00
e6fd9192b1 Ensure environment variables are validated as strings for fetch calls
All checks were successful
Build and Push to Registry / build-and-push (push) Successful in 47s
2025-09-16 10:54:23 +02:00
7d102772d9 Replace idnotConfig variable with environment variable
Some checks failed
Build and Push to Registry / build-and-push (push) Failing after 38s
2025-09-16 10:46:31 +02:00
8ed2ffb16c Removed idnotConfig file 2025-09-16 10:45:27 +02:00
Sosthene
7d47eec1f2 Rename signer-improved to simply signer
All checks were successful
Build and Push to Registry / build-and-push (push) Successful in 51s
2025-09-15 04:34:04 +02:00
Sosthene
b04679ba34 Remove database
Some checks failed
Build and Push to Registry / build-and-push (push) Failing after 40s
2025-09-10 15:52:51 +02:00
omaroughriss
cfa9514be9 Minor fix
All checks were successful
Build and Push to Registry / build-and-push (push) Successful in 50s
2025-09-09 12:21:08 +02:00
omaroughriss
221ac17831 Add the sdk-signer-client in the final image
Some checks failed
Build and Push to Registry / build-and-push (push) Failing after 29s
2025-09-09 12:19:00 +02:00
omaroughriss
6c6f49417e Update CICD to setup ssh agent
All checks were successful
Build and Push to Registry / build-and-push (push) Successful in 44s
2025-09-09 11:46:16 +02:00
omaroughriss
fbdbc0a699 Update Dockerfile to clone sdk-signer-client as a dep
Some checks failed
Build and Push to Registry / build-and-push (push) Failing after 23s
2025-09-09 11:38:28 +02:00
omaroughriss
180c06dc3b Update Dockerfile to fix deps
Some checks failed
Build and Push to Registry / build-and-push (push) Failing after 29s
2025-09-08 19:53:48 +02:00
omaroughriss
164fdfb4f0 Fix Dockerfile deps
Some checks failed
Build and Push to Registry / build-and-push (push) Failing after 26s
2025-09-08 19:51:50 +02:00
omaroughriss
de7f1a306d Add build js to Dockerfile
Some checks failed
Build and Push to Registry / build-and-push (push) Failing after 37s
2025-09-08 19:49:42 +02:00
Omar Oughriss
02f1cbb885 Add "dev" tagged image
All checks were successful
Build and Push to Registry / build-and-push (push) Successful in 36s
2025-09-08 17:02:35 +02:00
Sosthene
e8cb97b6c6 Use handlers for idNot method 2025-09-07 23:24:26 +02:00
Sosthene
c07591a97a Rm some leftovers 2025-09-07 23:23:38 +02:00
Sosthene
888ca48712 Improve logging 2025-09-07 23:23:10 +02:00
Sosthene
06a6b5c7aa Heavy refactoring 2025-09-07 21:10:39 +02:00
Sosthene
8462b99586 Conversion to typescript 2025-09-07 15:30:32 +02:00
Sosthene
1465c9dfd2 Improve idnot api 2025-08-27 17:51:48 +02:00
Sosthene
7e679ae33f Add sdk signer client interface 2025-08-27 17:51:08 +02:00
Sosthene
7b9d58545b Add sdk-signer-client (local dep for now) 2025-08-27 17:50:19 +02:00
Sosthene
9bfcb47293 Add IDNOT_API_KEY env 2025-08-27 17:50:19 +02:00
4f446cad5d Search by firstName or lastName on notaries 2025-08-18 18:34:51 +02:00
ed7edef021 Fix some issues 2025-08-18 17:54:18 +02:00
bbb5b5ae36 Add Database connection 2025-08-11 16:29:39 +02:00
121 changed files with 11497 additions and 1012 deletions

View File

@ -7,13 +7,12 @@ OVH_SMS_SERVICE_NAME=
# Configuration SMS Factor
SMS_FACTOR_TOKEN=
#Configuration Mailchimp
# Configuration Mailchimp
MAILCHIMP_API_KEY=
MAILCHIMP_KEY=
MAILCHIMP_LIST_ID=
#Configuration Stripe
# Configuration Stripe
STRIPE_SECRET_KEY=
STRIPE_WEBHOOK_SECRET=
STRIPE_STANDARD_SUBSCRIPTION_PRICE_ID=
@ -21,10 +20,20 @@ STRIPE_STANDARD_ANNUAL_SUBSCRIPTION_PRICE_ID=
STRIPE_UNLIMITED_SUBSCRIPTION_PRICE_ID=
STRIPE_UNLIMITED_ANNUAL_SUBSCRIPTION_PRICE_ID=
#Cartes de test Stripe
# Cartes de test Stripe
SUCCES= 4242 4242 4242 4242 #Paiement réussi
DECLINED= 4000 0025 0000 3155 #Paiement refusé
# Configuration serveur
APP_HOST=
PORT=
# Configuration PostgreSQL
DB_HOST=
DB_PORT=
DB_NAME=
DB_USER=
DB_PASSWORD=
# Configuration idnot
IDNOT_API_KEY=

View File

@ -2,10 +2,7 @@ name: Build and Push to Registry
on:
push:
branches: [ main ]
pull_request:
types: [closed]
branches: [ main ]
branches: [ dev ]
env:
REGISTRY: git.4nkweb.com
@ -14,11 +11,15 @@ env:
jobs:
build-and-push:
runs-on: ubuntu-latest
if: github.event.pull_request.merged == true || github.event_name == 'push'
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up SSH agent
uses: webfactory/ssh-agent@v0.9.1
with:
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
@ -34,6 +35,8 @@ jobs:
with:
context: .
push: true
ssh: default
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:dev
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ gitea.sha }}

143
.gitignore vendored
View File

@ -1,56 +1,95 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# 4NK Environment - Git Ignore
# ============================
confs/
# Dossiers de sauvegarde des scripts
**/backup/
**/*backup*
**/.cargo/
# Fichiers temporaires
**/*.tmp*
**/*.temp*
**/*.log*
**/*.pid*
# Fichiers de configuration locale
**/*.env*
**/*.conf*
**/*.yaml*
**/*.yml*
**/*.ini*
**/*.json*
**/*.toml*
**/*.lock*
# Données et logs
**/*.logs*
**/*.data
*.db
*.sqlite
# Certificats et clés
**/*.key
**/*.pem
**/*.crt
**/*.p12
**/*.pfx
ssl/
certs/
# Docker
**/*.docker*
# Cache et build
**/node_modules/
**/dist/
**/build/
**/target/
**/.next/
**/.turbo/
**/coverage/
**/.pytest_cache/
**/.cache/
**/.pnpm-store/
**/.venv/
**/vendor/
**/*.*.o
**/*.so
**/*.dylib
# IDE et éditeurs
**/*.vscode/
**/*.idea/
**/*.swp
**/*.swo
**/*~
# OS
**/*.DS_Store
**/*Thumbs.db
**/*tmp*
# Git
**/*.git/
**/*.orig*
# Backup des projets existants
**/*backup*
**/backups/
**/*backups*
dist
.next
**/*wallet*
**/*keys*
# dependencies
/node_modules
package-lock.json
**/*node_modules*
**/*cursor*
**/*pid*
**/*next*
# envs
.env
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
.env*.local
# typescript
*.tsbuildinfo
dist
dist-*
cabal-dev
*.o
*.hi
*.hie
*.chi
*.chs.h
*.dyn_o
*.dyn_hi
.hpc
.hsenv
.cabal-sandbox/
cabal.sandbox.config
*.prof
*.aux
*.hp
*.eventlog
.stack-work/
cabal.project.local
cabal.project.local~
.HTF/
.ghc.environment.*
id_rsa
.cache
.env.stg
# Dossiers de logs communs
log/
logs/
**/log/
**/logs/

View File

@ -1,19 +1,48 @@
FROM node:19-alpine
# syntax=docker/dockerfile:1.4
FROM node:19-alpine AS builder
WORKDIR /app
# Installation des dépendances
COPY package*.json ./
RUN npm install --production
# Outils nécessaires pour cloner le dépôt privé
RUN apk add --no-cache git openssh-client
# Copie des fichiers source
# Prépare SSH pour git.4nkweb.com
RUN mkdir -p /root/.ssh && \
ssh-keyscan git.4nkweb.com >> /root/.ssh/known_hosts
# Clone le SDK à côté de /app afin que ../sdk-signer-client soit disponible
RUN --mount=type=ssh git clone -b dev \
ssh://git@git.4nkweb.com/4nk/sdk-signer-client.git /sdk-signer-client
# Build de la dépendance SDK
WORKDIR /sdk-signer-client
RUN npm ci && npm run build
# Installation des dépendances de l'app
WORKDIR /app
COPY package*.json ./
RUN npm install
# Copie et build des sources de l'app
COPY tsconfig.json ./
COPY src ./src
RUN npm run build
# Réduction aux deps de production
RUN npm prune --omit=dev && npm cache clean --force
FROM node:19-alpine
WORKDIR /app
# Création d'un utilisateur non-root
RUN adduser -D appuser --uid 10000 && \
chown -R appuser /app
USER appuser
# Configuration du port et démarrag
# Copie des artefacts de build et des deps prod
COPY --from=builder --chown=appuser:appuser /app/package*.json ./
COPY --from=builder --chown=appuser:appuser /app/node_modules ./node_modules
COPY --from=builder --chown=appuser:appuser /app/dist ./dist
COPY --from=builder /sdk-signer-client /sdk-signer-client
EXPOSE 8080
CMD ["npm", "start"]

View File

@ -26,3 +26,13 @@ Une fois le serveur démarré, la route ping est accessible à :
- http://localhost:3000/api/ping
Cette route renvoie un objet JSON avec le message "Hello World".
## 📋 Fichiers centralisés
Les fichiers suivants sont centralisés dans le dépôt principal `4NK_env` :
- `CODE_OF_CONDUCT.md` - Code de conduite
- `CODEOWNERS` - Propriétaires du code
- `CONTRIBUTING.md` - Guide de contribution
- `LICENSE` - Licence du projet
Voir : [`4NK_env/CODE_OF_CONDUCT.md`](../../CODE_OF_CONDUCT.md), [`4NK_env/CODEOWNERS`](../../CODEOWNERS), [`4NK_env/CONTRIBUTING.md`](../../CONTRIBUTING.md), [`4NK_env/LICENSE`](../../LICENSE)

Binary file not shown.

View File

@ -0,0 +1 @@
20250925-091648

View File

@ -0,0 +1,14 @@
# HTTP flows summary
## Nginx (dev3.4nkweb.com) proxy_pass lines
40: proxy_pass http://127.0.0.1:8080;
56: proxy_pass http://localhost:3004;
65: proxy_pass http://localhost:8090;
96: proxy_pass http://localhost:8080;
134: proxy_pass http://127.0.0.1:8080;
164: proxy_pass http://127.0.0.1:8080;
## Express routes (src/routes/index.ts)
14:router.post('/api/v1/idnot/state', StateHandlers.createState);
15:router.get('/idnot/callback', IdNotCallbackHandlers.callback);
16:router.get('/authorized-client', IdNotCallbackHandlers.callback);

View File

@ -0,0 +1,32 @@
Netid State Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
udp UNCONN 0 0 0.0.0.0:53517 0.0.0.0:* uid:110 ino:2402 sk:1 cgroup:/system.slice/avahi-daemon.service <->
udp UNCONN 0 0 0.0.0.0:5353 0.0.0.0:* uid:110 ino:2400 sk:3 cgroup:/system.slice/avahi-daemon.service <->
udp UNCONN 0 0 0.0.0.0:631 0.0.0.0:* ino:797771415 sk:800b cgroup:/system.slice/cups-browsed.service <->
udp UNCONN 0 0 [::]:5353 [::]:* uid:110 ino:2401 sk:5 cgroup:/system.slice/avahi-daemon.service v6only:1 <->
udp UNCONN 0 0 [::]:38236 [::]:* uid:110 ino:2403 sk:6 cgroup:/system.slice/avahi-daemon.service v6only:1 <->
tcp LISTEN 0 128 127.0.0.1:8334 0.0.0.0:* users:(("bitcoind",pid=7833,fd=28)) uid:1000 ino:50510 sk:1001 cgroup:/user.slice/user-1000.slice/user@1000.service/app.slice/bitcoind.service <->
tcp LISTEN 0 511 0.0.0.0:9999 0.0.0.0:* users:(("next-server (v1",pid=3820889,fd=25)) uid:1000 ino:203086523 sk:2022 cgroup:/user.slice/user-1000.slice/session-23834.scope <->
tcp LISTEN 0 511 0.0.0.0:80 0.0.0.0:* ino:202194510 sk:301b cgroup:/system.slice/nginx.service <->
tcp LISTEN 0 128 0.0.0.0:8081 0.0.0.0:* users:(("sdk_storage",pid=2737523,fd=7)) uid:1000 ino:65916025 sk:6008 cgroup:/user.slice/user-1000.slice/session-14225.scope <->
tcp LISTEN 0 511 127.0.0.1:39059 0.0.0.0:* users:(("node",pid=4134789,fd=19)) uid:1000 ino:1510269034 sk:800c cgroup:/user.slice/user-1000.slice/session-24057.scope <->
tcp LISTEN 0 128 0.0.0.0:22 0.0.0.0:* ino:16803 sk:1004 cgroup:/system.slice/ssh.service <->
tcp LISTEN 0 128 127.0.0.1:631 0.0.0.0:* ino:797011799 sk:800d cgroup:/system.slice/cups.service <->
tcp LISTEN 0 511 0.0.0.0:3000 0.0.0.0:* ino:202194509 sk:301c cgroup:/system.slice/nginx.service <->
tcp LISTEN 0 200 127.0.0.1:5432 0.0.0.0:* uid:118 ino:54762578 sk:7001 cgroup:/system.slice/system-postgresql.slice/postgresql@17-main.service <->
tcp LISTEN 0 4096 127.0.0.1:9050 0.0.0.0:* ino:425 sk:100a cgroup:/system.slice/system-tor.slice/tor@default.service <->
tcp LISTEN 0 511 0.0.0.0:3003 0.0.0.0:* users:(("node",pid=3903828,fd=27)) uid:1000 ino:222145527 sk:3021 cgroup:/user.slice/user-1000.slice/session-15425.scope <->
tcp LISTEN 0 511 0.0.0.0:443 0.0.0.0:* ino:202194511 sk:301e cgroup:/system.slice/nginx.service <->
tcp LISTEN 0 4096 127.0.0.1:9051 0.0.0.0:* ino:426 sk:100c cgroup:/system.slice/system-tor.slice/tor@default.service <->
tcp LISTEN 0 128 0.0.0.0:38332 0.0.0.0:* users:(("bitcoind",pid=7865,fd=12)) uid:1000 ino:44367 sk:100e cgroup:/user.slice/user-1000.slice/user@1000.service/app.slice/signet.service <->
tcp LISTEN 0 128 0.0.0.0:38333 0.0.0.0:* users:(("bitcoind",pid=7865,fd=38)) uid:1000 ino:53290 sk:100f cgroup:/user.slice/user-1000.slice/user@1000.service/app.slice/signet.service <->
tcp LISTEN 0 128 127.0.0.1:38334 0.0.0.0:* users:(("bitcoind",pid=7865,fd=40)) uid:1000 ino:53291 sk:1010 cgroup:/user.slice/user-1000.slice/user@1000.service/app.slice/signet.service <->
tcp LISTEN 0 4096 127.0.0.1:8000 0.0.0.0:* users:(("blindbit",pid=1891796,fd=59)) uid:1000 ino:54710950 sk:5007 cgroup:/user.slice/user-1000.slice/session-6209.scope <->
tcp LISTEN 0 100 127.0.0.1:29000 0.0.0.0:* users:(("bitcoind",pid=7865,fd=19)) uid:1000 ino:41720 sk:1012 cgroup:/user.slice/user-1000.slice/user@1000.service/app.slice/signet.service <->
tcp LISTEN 0 511 127.0.0.1:33961 0.0.0.0:* users:(("node",pid=4134751,fd=19)) uid:1000 ino:1510289396 sk:800e cgroup:/user.slice/user-1000.slice/session-24057.scope <->
tcp LISTEN 0 128 0.0.0.0:8332 0.0.0.0:* users:(("bitcoind",pid=7833,fd=12)) uid:1000 ino:50506 sk:1013 cgroup:/user.slice/user-1000.slice/user@1000.service/app.slice/bitcoind.service <->
tcp LISTEN 0 128 0.0.0.0:8333 0.0.0.0:* users:(("bitcoind",pid=7833,fd=26)) uid:1000 ino:50509 sk:1014 cgroup:/user.slice/user-1000.slice/user@1000.service/app.slice/bitcoind.service <->
tcp LISTEN 0 511 *:8080 *:* users:(("node",pid=4095406,fd=26)) uid:1000 ino:653166690 sk:800f cgroup:/user.slice/user-1000.slice/session-23878.scope v6only:0 <->
tcp LISTEN 0 128 [::]:22 [::]:* ino:16805 sk:1015 cgroup:/system.slice/ssh.service v6only:1 <->
tcp LISTEN 0 128 [::1]:631 [::]:* ino:797011798 sk:8010 cgroup:/system.slice/cups.service v6only:1 <->
tcp LISTEN 0 200 [::1]:5432 [::]:* uid:118 ino:54762577 sk:7002 cgroup:/system.slice/system-postgresql.slice/postgresql@17-main.service v6only:1 <->
tcp LISTEN 0 511 *:9090 *:* users:(("node",pid=3257267,fd=30)) uid:1000 ino:190954586 sk:2019 cgroup:/user.slice/user-1000.slice/session-23100.scope v6only:0 <->

View File

@ -0,0 +1,175 @@
server {
# Logs Nginx spécifiques à ce vhost
access_log /var/log/nginx/dev3.4nkweb.com.access.log main;
error_log /var/log/nginx/dev3.4nkweb.com.error.log warn;
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;
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;
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;
proxy_set_header X-Request-Id $request_id;
}
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_set_header X-Request-Id $request_id;
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;
proxy_set_header X-Request-Id $request_id;
}
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;
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;
}
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 {
# Logs Nginx spécifiques à ce vhost (HTTP -> redir HTTPS)
access_log /var/log/nginx/dev3.4nkweb.com.access.log main;
error_log /var/log/nginx/dev3.4nkweb.com.error.log warn;
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;
proxy_set_header X-Request-Id $request_id;
}
return 301 https://$host$request_uri;
}

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,7 @@
server {
listen 80;
server_name local.lecoffreio.4nkweb.com;
# Redirige vers le front final en conservant chemin + query
return 301 https://dev3.4nkweb.com/idnot/callback$is_args$args;
}

View File

@ -0,0 +1,8 @@
server {
listen 3001;
server_name local.4nkweb.com;
# Redirige vers le callback en conservant intégralement la query (code + state)
return 301 https://dev3.4nkweb.com/idnot/callback$is_args$args;
}

View File

@ -0,0 +1,11 @@
server {
listen 3443 ssl;
listen 3443;
server_name local.4nkweb.com;
ssl_certificate /etc/letsencrypt/live/local.4nkweb.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/local.4nkweb.com/privkey.pem;
# Redirige vers le callback en conservant intégralement la query (code + state)
return 301 https://dev3.4nkweb.com/idnot/callback$is_args$args;
}

View File

@ -0,0 +1,11 @@
server {
listen 3443 ssl;
listen 3000;
server_name local.4nkweb.com;
ssl_certificate /etc/letsencrypt/live/local.4nkweb.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/local.4nkweb.com/privkey.pem;
# Redirige vers le callback en conservant intégralement la query (code + state)
return 301 https://dev3.4nkweb.com/idnot/callback$is_args$args;
}

View File

@ -0,0 +1,11 @@
server {
listen 3443 ssl;
listen 3000;
server_name local.4nkweb.com;
ssl_certificate /etc/letsencrypt/live/local.4nkweb.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/local.4nkweb.com/privkey.pem;
# Redirige vers le callback en conservant intégralement la query (code + state)
return 301 https://dev3.4nkweb.com/idnot/callback$is_args$args;
}

View File

@ -0,0 +1,20 @@
server {
listen 3000 ssl;
server_name local.4nkweb.com;
ssl_certificate /etc/letsencrypt/live/local.4nkweb.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/local.4nkweb.com/privkey.pem;
# Redirige vers le callback en conservant intégralement la query (code + state)
return 301 https://dev3.4nkweb.com/idnot/callback$is_args$args;
}
server {
listen 3000 ssl;
server_name local.4nkweb.com;
ssl_certificate /etc/letsencrypt/live/local.4nkweb.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/local.4nkweb.com/privkey.pem;
return 301 https://dev3.4nkweb.com/idnot/callback;
}

View File

@ -0,0 +1,12 @@
server {
listen 3000 ssl;
listen 3000;
server_name local.4nkweb.com;
ssl_certificate /etc/letsencrypt/live/local.4nkweb.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/local.4nkweb.com/privkey.pem;
# Redirige vers le callback en conservant intégralement la query (code + state)
return 301 https://dev3.4nkweb.com/idnot/callback$is_args$args;
}

View File

@ -0,0 +1,7 @@
server {
listen 3000;
server_name local.4nkweb.com;
# Redirige vers le front final en conservant chemin + query
return 301 https://dev4.4nkweb.com/lecoffre$request_uri;
}

View File

@ -0,0 +1,8 @@
server {
listen 3001;
server_name local.4nkweb.com;
# Redirige vers le callback en conservant intégralement la query (code + state)
return 301 https://dev3.4nkweb.com/idnot/callback$is_args$args;
}

View File

@ -0,0 +1,8 @@
server {
listen 3001;
server_name local.4nkweb.com;
# Redirige vers le callback en conservant intégralement la query (code + state)
return 301 https://dev3.4nkweb.com/idnot/callback$is_args$args;
}

View File

@ -0,0 +1,8 @@
server {
listen 3001;
server_name local.4nkweb.com;
# Redirige vers le callback en conservant intégralement la query (code + state)
return 301 https://dev3.4nkweb.com/idnot/callback$is_args$args;
}

View File

@ -0,0 +1,13 @@
server {
listen 3000;
server_name local.4nkweb.com;
# Redirige vers le callback en conservant intégralement la query (code + state)
return 301 https://dev3.4nkweb.com/idnot/callback$is_args$args;
}
server {
listen 3000;
server_name local.4nkweb.com;
return 301 https://dev3.4nkweb.com/idnot/callback;
}

View File

@ -0,0 +1,8 @@
server {
listen 3000;
server_name local.4nkweb.com;
# Redirige vers le callback en conservant intégralement la query (code + state)
return 301 https://dev3.4nkweb.com/idnot/callback$is_args$args;
}

View File

@ -0,0 +1,7 @@
server {
listen 80;
server_name local.lecoffreio.4nkweb.com;
# Redirige vers le front final en conservant chemin + query
return 301 https://dev3.4nkweb.com/idnot/callback$is_args$args;
}

View File

@ -0,0 +1,7 @@
server {
listen 80;
server_name local.lecoffreio.4nkweb.com;
# Redirige vers le front final en conservant chemin + query
return 301 https://dev4.4nkweb.com/lecoffre$request_uri;
}

View File

@ -0,0 +1,7 @@
server {
listen 80;
server_name local.lecoffreio.4nkweb.com;
# Redirige vers le front final en conservant chemin + query
return 301 https://dev4.4nkweb.com/lecoffre$request_uri;
}

View File

@ -0,0 +1,7 @@
server {
listen 80;
server_name local.lecoffreio.4nkweb.com;
# Redirige vers le front final en conservant chemin + query
return 301 https://dev4.4nkweb.com/lecoffre$request_uri;
}

View File

@ -0,0 +1,7 @@
server {
listen 80;
server_name local.lecoffreio.4nkweb.com;
# Redirige vers le front final en conservant chemin + query
return 301 https://dev4.4nkweb.com/lecoffre$request_uri;
}

View File

@ -0,0 +1,44 @@
server {
listen 80;
server_name localhost;
#access_log /var/log/nginx/host.access.log main;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
# proxy the PHP scripts to Apache listening on 127.0.0.1:80
#
#location ~ \.php$ {
# proxy_pass http://127.0.0.1;
#}
# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
#
#location ~ \.php$ {
# root html;
# fastcgi_pass 127.0.0.1:9000;
# fastcgi_index index.php;
# fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
# include fastcgi_params;
#}
# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
#location ~ /\.ht {
# deny all;
#}
}

View File

@ -0,0 +1,34 @@
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid;
include /etc/nginx/stream.d/*.conf;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
#gzip on;
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}

View File

@ -0,0 +1 @@
/etc/nginx/sites-available/demo.4nkweb.com

View File

@ -0,0 +1 @@
/etc/nginx/sites-available/dev1.4nkweb.com

View File

@ -0,0 +1 @@
/etc/nginx/sites-available/dev2.4nkweb.com

View File

@ -0,0 +1,162 @@
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, local.4nkweb.com:3000, 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;
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/ {
# 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, local.4nkweb.com:3000, 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;
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;
}

View File

@ -0,0 +1 @@
/etc/nginx/sites-available/lecoffreio-dev2.4nkweb.com

View File

@ -0,0 +1 @@
/etc/nginx/sites-available/lecoffreio.4nkweb.com

View File

@ -0,0 +1,7 @@
server {
listen 80;
server_name local.lecoffreio.4nkweb.com;
# Redirige vers le front final en conservant chemin + query
return 301 https://dev3.4nkweb.com/idnot/callback$is_args$args;
}

View File

@ -0,0 +1 @@
/etc/nginx/sites-available/relay235.4nkweb.com

View File

@ -0,0 +1,11 @@
stream {
map $ssl_preread_protocol $upstream_3000 {
"" 127.0.0.1:3001; # HTTP clair
default 127.0.0.1:3443; # TLS -> HTTPS
}
server {
listen 3000;
proxy_pass $upstream_3000;
ssl_preread on;
}
}

View File

@ -0,0 +1,215 @@
server {
# Logs Nginx spécifiques à ce vhost
access_log /var/log/nginx/dev3.4nkweb.com.access.log main;
error_log /var/log/nginx/dev3.4nkweb.com.error.log warn;
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, 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;
}
# 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;
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;
proxy_set_header X-Request-Id $request_id;
}
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_set_header X-Request-Id $request_id;
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;
proxy_set_header X-Request-Id $request_id;
}
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;
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;
}
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 {
# Logs Nginx spécifiques à ce vhost (HTTP -> redir HTTPS)
access_log /var/log/nginx/dev3.4nkweb.com.access.log main;
error_log /var/log/nginx/dev3.4nkweb.com.error.log warn;
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;
proxy_set_header X-Request-Id $request_id;
}
return 301 https://$host$request_uri;
}

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,

View File

@ -0,0 +1,7 @@
server {
listen 80;
server_name local.lecoffreio.4nkweb.com;
# Redirige vers le front final en conservant chemin + query
return 301 https://dev3.4nkweb.com/idnot/callback$is_args$args;
}

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

165
docs/doc_api/.cursorrules Normal file
View File

@ -0,0 +1,165 @@
# Règles globales Cursor pour les projets
## Principes généraux
- Lire impérativement le fichier `.cursorrules` au démarrage de chaque session.
- Lire tous les fichiers du dossier `docs/`, le code et les paramètres avant de commencer.
- Poser des questions et proposer des améliorations si nécessaire.
- Ajouter les leçons apprises à ce fichier `.cursorrules`.
- Écrire des documents complets et exhaustifs.
- Respecter strictement les règles de lint du Markdown.
- Préférer toujours un shell **bash** à PowerShell.
- Fermer et relancer le terminal avant chaque utilisation.
- Si le terminal est interrompu, analyser la commande précédente (interruption probablement volontaire).
- Exécuter automatiquement les étapes de résolution de problème.
- Expliquer les commandes complexes avant de les lancer.
- Compiler régulièrement et corriger toutes les erreurs avant de passer à létape suivante.
- Tester, documenter, compiler, aligner tag git, changelog et version avant déploiement et push.
- Utiliser `docx2txt` pour lire les fichiers `.docx`.
- Ajouter automatiquement les dépendances et rechercher systématiquement les dernières versions.
- Faire des commandes simples et claires en plusieurs étapes.
- Vérifie toujours tes hypothèses avant de commencer.
- N'oublie jamais qu'après la correction d'un problème, il faut corriger toutes les erreurs qui peuvent en découler.
## Organisation des fichiers et répertoires
- Scripts regroupés dans `scripts/`
- Configurations regroupées dans `confs/`
- Journaux regroupés dans `logs/`
- Répertoires obligatoires :
- `docs/` : documentation de toute fonctionnalité ajoutée, modifiée, supprimée ou découverte.
- `tests/` : tests liés à toute fonctionnalité ajoutée, modifiée, supprimée ou découverte.
- Remplacer les résumés (`RESUME`) par des mises à jour dans `docs/`.
## Configuration critique des services
- Mempool du réseau signet :
`https://mempool2.4nkweb.com/fr/docs/api/rest`
## Développement et sécurité
- Ne jamais committer de clés privées ou secrets.
- Utiliser des variables denvironnement pour les données sensibles.
- Définir correctement les dépendances Docker avec healthchecks.
- Utiliser les URLs de service Docker Compose (`http://service_name:port`).
- Documenter toutes les modifications importantes dans `docs/`.
- Externaliser au maximum les variables denvironnement.
- Toujours utiliser une clé SSH pour cloner les dépôts.
- Monter en version les dépôts au début du travail.
- Pousser les tags docker `ext` via la CI sur `git.4nkweb.com`.
- Corriger systématiquement les problèmes, sans contournement.
## Scripts (règles critiques)
- Vérifier lexistence dun script dans `scripts/` avant toute action.
- Utiliser les scripts existants plutôt que des commandes directes.
- Ne jamais créer plusieurs versions ou noms de scripts.
- Améliorer lexistant au lieu de créer des variantes (`startup-v2.sh`, etc.).
## Images Docker (règles critiques)
- Ajouter systématiquement `apt update && apt upgrade` dans les Dockerfiles.
- Installer en arrière-plan dans les images Docker :
`curl, git, sed, awk, nc, wget, jq, telnet, tee, wscat, ping, npm (dernière version)`
- Appliquer à tous les Dockerfiles et `docker-compose.yml`.
- N'utilise pas les version test ou dev ou ext-dev mais toujours les version ext, relance leur compilation si nécessaire
## Fichiers de configuration (règles critiques)
- Vérifier lécriture effective après chaque modification.
- Fichiers concernés : `nginx.conf`, `bitcoin.conf`, `package.json`, `Cargo.toml`.
- Utiliser `cat`, `jq` ou vérificateurs de syntaxe.
## Connexion au réseau Bitcoin signet
Commande unique et obligatoire :
```bash
docker exec bitcoin-signet bitcoin-cli -signet -rpccookiefile=/home/bitcoin/.bitcoin/signet/.cookie getblockchaininfo
````
## Connexion au relay/faucet bootstrap
* Test via WSS : `wss://dev3.4nkweb.com/ws/`
* Envoi Faucet, réponse attendue avec `NewTx` (tx hex et tweak\_data).
## Debug
* Automatiser dans le code toute solution validée.
* Pérenniser les retours dexpérience dans code et paramètres.
* Compléter les tests pour éviter les régressions.
## Nginx
* Tous les fichiers dans `conf/ngnix` doivent être mappés avec ceux du serveur.
## Minage (règles critiques)
* Toujours valider les adresses utilisées (adresses TSP non reconnues).
* Utiliser uniquement des adresses Bitcoin valides (bech32m).
* Vérifier que le minage génère des blocs avec transactions, pas uniquement coinbase.
* Surveiller les logs du minage pour détecter les erreurs dadresse.
* Vérifier la propagation via le mempool externe.
## Mempool externe
* Utiliser `https://mempool2.4nkweb.com` pour vérifier les transactions.
* Vérifier la synchronisation entre réseau local et externe.
## Données et modèles
* Utiliser les fichiers CSV comme base des modèles de données.
* Être attentif aux en-têtes multi-lignes.
* Confirmer la structure comprise et demander définition de toutes les colonnes.
* Corriger automatiquement incohérences de type.
## Implémentation et architecture
* Code splitting avec `React.lazy` et `Suspense`.
* Centraliser létat avec Redux ou Context API.
* Créer une couche dabstraction pour les services de données.
* Aller systématiquement au bout dune implémentation.
## Préparation open source
Chaque projet doit être prêt pour un dépôt sur `git.4nkweb.com` :
* Inclure : `LICENSE` (MIT, Apache 2.0 ou GPL), `CONTRIBUTING.md`, `CHANGELOG.md`, `CODE_OF_CONDUCT.md`.
* Aligner documentation et tests avec `4NK_node`.
## Versioning et documentation
* Mettre à jour documentation et tests systématiquement.
* Gérer versioning avec changelog.
* Demander validation avant tag.
* Documenter les hypothèses testées dans un REX technique.
* Tester avant tout commit.
* Tester les buildsavant tout tag.
## Bonnes pratiques de confidentialité et sécurité
### Docker
- Ne jamais stocker de secrets (clés, tokens, mots de passe) dans les Dockerfiles ou docker-compose.yml.
- Utiliser des fichiers `.env` sécurisés (non commités avec copie en .env.example) pour toutes les variables sensibles.
- Ne pas exécuter de conteneurs avec lutilisateur root, privilégier un utilisateur dédié.
- Limiter les capacités des conteneurs (option `--cap-drop`) pour réduire la surface dattaque.
- Scanner régulièrement les images Docker avec un outil de sécurité (ex : Trivy, Clair).
- Mettre à jour en continu les images de base afin déliminer les vulnérabilités.
- Ne jamais exposer de ports inutiles.
- Restreindre les volumes montés au strict nécessaire.
- Utiliser des réseaux Docker internes pour la communication inter-containers.
- Vérifier et tenir à jour les .dockerignore.
### Git
- Ne jamais committer de secrets, clés ou identifiants (même temporairement).
- Configurer des hooks Git (pre-commit) pour détecter automatiquement les secrets et les failles.
- Vérifier lhistorique (`git log`, `git filter-repo`) pour sassurer quaucune information sensible na été poussée.
- Signer les commits avec GPG pour garantir lauthenticité.
- Utiliser systématiquement SSH pour les connexions à distance.
- Restreindre les accès aux dépôts (principes du moindre privilège).
- Documenter les changements sensibles dans `CHANGELOG.md`.
- Ne jamais pousser directement sur `main` ou `master`, toujours passer par des branches de feature ou PR.
- Vérifier et tenir à jour les .gitignore.
- Vérifier et tenir à jour les .gitkeep.
- Vérifier et tenir à jour les .gitattributes.
### Cursor
- Toujours ouvrir une session en commençant par relire le fichier `.cursorrules`.
- Vérifier que Cursor ne propose pas de commit contenant des secrets ou fichiers sensibles.
- Ne pas exécuter dans Cursor de commandes non comprises ou copiées sans vérification.
- Préférer lutilisation de scripts audités dans `scripts/` plutôt que des commandes directes dans Cursor.
- Fermer et relancer Cursor régulièrement pour éviter des contextes persistants non désirés.
- Ne jamais partager le contenu du terminal ou des fichiers sensibles via Cursor en dehors du périmètre du projet.
- Vérifier et tenir à jour les .cursorrules.
- Vérifier et tenir à jour les .cursorignore.

View File

@ -0,0 +1,37 @@
# Flux HMR avec idnot via dev3 et state (24/09/2025)
## Objectif
- Maintenir le **HMR** côté front (Next.js) sans casser le parcours dauth **idnot** en sappuyant sur un **state** serveur.
## Acteurs / Ports
- Front (Next.js, HMR): `:3000`
- Backend mini: `:8080`
- Nginx dev3: `dev3.4nkweb.com` (`443`/`80`), reverse vers `8080/300x/8090`
- Callback idnot: `https://dev3.4nkweb.com/idnot/callback`
## Création du state (front → back)
- Route: `POST /api/v1/idnot/state` (via `src/routes/index.ts``StateHandlers.createState`).
- Le back génère un `state` persisté côté serveur (via `SessionManager`) et le retourne au front.
- CORS dynamique (`src/config/index.ts`) autorise `https://*.4nkweb.com` et l`APP_HOST`.
## Redirection idnot
- Le front redirige lutilisateur vers idnot avec le `state` fourni.
- idnot renvoie ensuite vers `https://dev3.4nkweb.com/idnot/callback?code=...&state=...`.
## Callback via Nginx dev3 → backend
- `confs/nginx/dev3.4nkweb.com.conf`: location `= /idnot/callback` → proxy_pass backend `:8080`.
- Le back (route `GET /idnot/callback`) lit `state`, vérifie côté `SessionManager`, consomme `code`, finalise login/attachement.
## Pourquoi le HMR ne casse pas le flow
- Le couplage se fait via le `state` stocké à chaud côté serveur, pas via létat volatile du front.
- Même si le front change de bundle (HMR), le `state` reste valide jusquau callback.
- CORS permet au front HMR dappeler le back sans friction.
## Rôle de dev3
- Point dentrée HTTPS stable pour le callback.
- Reverse proxy vers 8080 (back), 300x (front), 8090 (relay), avec 80 → 301 → HTTPS.
- Gère les réécritures nécessaires (ex: `/storage`).
## Résultat
- HMR actif, parcours idnot fiable: le `state` maintenu côté back assure la continuité entre redirection et callback.

View File

@ -0,0 +1,43 @@
# Résolution callback idnot - Route /authorized-client (24/09/2025)
## Problème initial
- Login depuis `dev4.4nkweb.com/lecoffre` → idnot → callback vers `https://lecoffreio.4nkweb.com/authorized-client`
- Erreur 502 Bad Gateway (route inexistante)
## Diagnostic
- Nginx `lecoffreio.4nkweb.com` proxy vers `localhost:3000` mais le front Next.js tourne sur port **9999**
- Route `/authorized-client` manquante dans le backend
- Config nginx manquante pour router `/authorized-client` vers le backend
## Solutions appliquées
### 1. Backend - Nouvelle route
- Ajout de `GET /authorized-client` dans `src/routes/index.ts`
- Utilise le même handler que `/idnot/callback` (`IdNotCallbackHandlers.callback`)
- Route placée avant les routes `/api/*` pour éviter les conflits
### 2. Nginx - Configuration
- Ajout de location `/authorized-client` dans `/etc/nginx/sites-available/lecoffreio.4nkweb.com`
- Proxy vers `http://127.0.0.1:8080` (backend lecoffre-back-mini)
- Headers appropriés pour le proxy
### 3. Redémarrage services
- Build et redémarrage du backend
- Test et rechargement nginx
## Résultat
✅ Route `/authorized-client` opérationnelle
✅ Nginx route correctement vers le backend
✅ Handler exécuté (erreur "State expired" attendue pour ancien state)
✅ Flux idnot fonctionnel pour nouveaux logins
## Test
- URL testée: `https://lecoffreio.4nkweb.com/authorized-client?code=...&state=...`
- Résultat: 500 "State expired" (normal pour state ancien)
- Nouveau parcours complet requis pour test avec state valide
## Fichiers modifiés
- `src/routes/index.ts` - Ajout route `/authorized-client`
- `/etc/nginx/sites-available/lecoffreio.4nkweb.com` - Location proxy
- `confs/nginx/_effective_20250924-190412/` - Snapshot config nginx

View File

@ -0,0 +1,39 @@
# Problème callback idnot - 502 sur lecoffreio.4nkweb.com (24/09/2025)
## Symptôme
- Login depuis `dev4.4nkweb.com/lecoffre` → redirection idnot → callback vers `https://lecoffreio.4nkweb.com/authorized-client`
- Erreur 502 Bad Gateway sur `lecoffreio.4nkweb.com`
## Analyse du flux observé
1. **Login initial**: `dev4.4nkweb.com/lecoffre` (autre machine)
2. **Redirection idnot**: vers `qual-connexion.idnot.fr`
3. **Callback idnot**: vers `https://lecoffreio.4nkweb.com/authorized-client?code=...&state=...`
4. **Erreur**: 502 Bad Gateway
## Problème identifié
- Le callback idnot pointe vers `lecoffreio.4nkweb.com/authorized-client`
- Mais notre configuration nginx attend le callback sur `dev3.4nkweb.com/idnot/callback`
- Le `state` contient `"next_url":"https://dev4.4nkweb.com/authorized-client"` (décodé du JWT)
## Configuration attendue vs réelle
- **Attendu**: `https://dev3.4nkweb.com/idnot/callback` (selon `confs/nginx/dev3.4nkweb.com.conf`)
- **Réel**: `https://lecoffreio.4nkweb.com/authorized-client`
## Causes possibles
1. **Configuration idnot**: Le callback URL dans idnot pointe vers `lecoffreio.4nkweb.com` au lieu de `dev3.4nkweb.com`
2. **Nginx lecoffreio.4nkweb.com**: Pas de route `/authorized-client` configurée
3. **Backend lecoffreio.4nkweb.com**: Service non disponible ou mal configuré
## Solutions à vérifier
1. **Vérifier la config idnot**: S'assurer que le callback URL pointe vers `https://dev3.4nkweb.com/idnot/callback`
2. **Vérifier nginx lecoffreio.4nkweb.com**: Ajouter une route `/authorized-client` si nécessaire
3. **Vérifier le backend**: S'assurer que le service sur `lecoffreio.4nkweb.com` est opérationnel
## État actuel
- Backend local (8080): OK, `/api/v1/health` répond
- Nginx dev3: OK, route `/idnot/callback` configurée
- Nginx lecoffreio: Problème sur `/authorized-client`
## Action recommandée
Vérifier la configuration du callback URL dans idnot et s'assurer qu'elle pointe vers `dev3.4nkweb.com/idnot/callback` plutôt que `lecoffreio.4nkweb.com/authorized-client`.

119
docs/logs-backend.md Normal file
View File

@ -0,0 +1,119 @@
## Logs backend emplacements et points de contrôle
### Emplacements des logs
- **log courant**: `logs/backend.out`
- **PID du process Node**: `logs/backend.pid`
- **logs rotés**: `logs/backend_YYYYMMDD_HHMMSS.out`
- **logs de build (si utilisés)**: `logs/build_YYYYMMDD_HHMMSS.out`
### Rotation simple (manuelle)
- Renommer `logs/backend.out` en `logs/backend_YYYYMMDD_HHMMSS.out` avant relance.
### Démarrage / Arrêt
- Au démarrage, rechercher: `Server started on port 8080` (ou le port configuré).
- En arrêt/redémarrage, sassurer quaucun ancien process ne reste actif (voir `logs/backend.pid`).
### Suivi temps réel
- Suivre `logs/backend.out` en continu pour les scénarios de test.
- Capturer la fenêtre incluant: callback → échange de token → appels Annuaire.
### Points de contrôle à rechercher (IdNot)
- **Réception du callback**:
- `[IdNotCallback] incoming request` avec `code_present: true` et `state_present: true`.
- **Échange de token OIDC**:
- `Token exchange successful` avec `hasAccessToken`, `hasIdToken`.
- **Décodage JWT**:
- `JWT payload decoded` avec `hasProfileIdn`, `hasEntityIdn`.
- **Appels Annuaire (QUAL)** réponses JSON attendues:
- Absence des erreurs:
- `IdNot non-JSON response`
- `IdNot JSON parse failed`
- En cas derreur proxy, on peut voir `No context` (non-JSON).
- **Erreur applicative agrégée**:
- `IdNot authentication failed` ou un objet derreur avec `code: 'IDNOT_SERVICE_ERROR'`.
### Interprétation rapide
- **Si `No context` apparaît**:
- Vérifier lenvoi des en-têtes: `Accept: application/json` et en-tête de contexte IdNot (config via `IDNOT_CONTEXT_HEADER` / `IDNOT_CONTEXT_VALUE`).
- **Si `Token exchange failed`**:
- Vérifier `IDNOT_TOKEN_URL`, `IDNOT_CLIENT_ID`, `IDNOT_CLIENT_SECRET`, `IDNOT_REDIRECT_URI`.
- **Si `User not attached to an office`**:
- Le JWT ou les données Annuaire ne satisfont pas les règles métier attendues.
### Variables denvironnement utiles (rappel)
- **Bases dURL**:
- `IDNOT_API_BASE_URL` (données Annuaire, QUAL)
- `IDNOT_ANNUARY_BASE_URL` (Annuaire, endpoints personnes/entités)
- **Contexte QUAL (si requis par le proxy IdNot)**:
- `IDNOT_CONTEXT_HEADER` (ex: `X-Context`)
- `IDNOT_CONTEXT_VALUE` (valeur fournie par IdNot)
- **Auth OIDC**:
- `IDNOT_TOKEN_URL`, `IDNOT_CLIENT_ID`, `IDNOT_CLIENT_SECRET`, `IDNOT_REDIRECT_URI`
### Bonnes pratiques
- Toujours effectuer une rotation de `logs/backend.out` avant un test significatif.
- Conserver les logs datés pour linvestigation et la comparaison entre exécutions.
## Nginx emplacements et points de contrôle
### Emplacements
- Access log Nginx (vhost): `/var/log/nginx/dev3.4nkweb.com.access.log`
- Error log Nginx (vhost): `/var/log/nginx/dev3.4nkweb.com.error.log`
### Spécificités de configuration (fichier `confs/nginx/dev3.4nkweb.com.conf`)
- Ajout de `access_log` et `error_log` par vhost.
- Propagation dun identifiant de requête: `X-Request-Id: $request_id` vers lupstream.
- Ajout dun en-tête `X-Request-Id` sur les réponses pour faciliter la corrélation.
### Corrélation des requêtes
- Utiliser `X-Request-Id` pour faire le lien entre:
- les entrées dans `/var/log/nginx/dev3.4nkweb.com.access.log`,
- et les logs applicatifs dans `logs/backend.out` (recherchez `requestId` dans les logs Express sil est présent, sinon corrélez via timestamp, méthode et chemin).
### À surveiller côté Nginx
- Codes `4xx/5xx` dans laccess log sur `/idnot/callback`, `/api/...`, `/ws/`.
- Délai ou timeouts (`upstream timed out`) dans lerror log.
- Cohérence du schéma (`X-Forwarded-Proto`) et du host (`Host`) vers lupstream.
## Commandes utiles (ops locales)
### Backend Node
- Créer dossier logs (idempotent):
```bash
mkdir -p logs
```
- Rotation log backend courant:
```bash
if [ -f logs/backend.out ]; then mv -f logs/backend.out logs/backend_$(date +%Y%m%d_%H%M%S).out; fi
```
- Démarrage en arrière-plan avec redirection vers log et PID:
```bash
nohup node dist/server.js > logs/backend.out 2>&1 & echo $! > logs/backend.pid
```
- Arrêt du backend via PID:
```bash
kill $(cat logs/backend.pid)
```
- Suivi temps réel des logs backend:
```bash
tail -f logs/backend.out
```
### Nginx (local serveur)
- Recharger la configuration après modification:
```bash
sudo nginx -t && sudo systemctl reload nginx
```
- Suivi des logs Nginx:
```bash
sudo tail -f /var/log/nginx/dev3.4nkweb.com.access.log /var/log/nginx/dev3.4nkweb.com.error.log
```
## Dépannage
- Erreur lors de lécriture de `logs/backend.pid`:
- Assurez-vous que le dossier `logs/` existe avant le démarrage (`mkdir -p logs`).
- Vérifiez les droits décriture dans le répertoire du projet.
- Pas de corrélation évidente entre Nginx et backend:
- Utilisez `X-Request-Id` (présent côté Nginx) et rapprochez par timestamp/méthode/chemin côté backend.
- Erreurs IdNot "No context":
- Vérifier les en-têtes envoyés côté backend: `Accept: application/json` et les variables `IDNOT_CONTEXT_HEADER` / `IDNOT_CONTEXT_VALUE`.

View File

@ -0,0 +1,46 @@
# Audit Nginx et redirections (24/09/2025)
Document d'état des configurations Nginx locales et du projet, sans modification.
## Portée
- Projet: `confs/nginx/`
- Système local: `/etc/nginx/` (sites-available, sites-enabled)
## Projet `confs/nginx/`
- dev3.4nkweb.com.conf
- listen: 443 ssl et 80 (80 → 301 https)
- backend: proxy_pass `http://127.0.0.1:8080`
- services: `http://localhost:3004`, `http://localhost:8090`
- storage: `rewrite ^/storage(/.*)$` + proxy_pass backend 8080
- dev3.4nkweb.com.fixed.conf (+ .b64)
- Équivalent “fixé”, idem HTTPS forcé, même routages
- local.lecoffreio.4nkweb.com.conf
- listen: 80, redirection 301 → `https://dev3.4nkweb.com/idnot/callback$is_args$args`
- Aucune référence active à `local.4nkweb.com` dans le projet (seulement dans les sauvegardes).
## Nginx local `/etc/nginx/`
- Actifs (sites-enabled):
- dev1.4nkweb.com, dev2.4nkweb.com, dev3.4nkweb.com, lecoffreio.4nkweb.com, lecoffreio-dev2.4nkweb.com, relay235.4nkweb.com
- local.lecoffreio.4nkweb.com: 80 → 301 `https://dev3.4nkweb.com/idnot/callback...`
- ATTENTION: `local.4nkweb.com-3001` est activé (encore présent dans sites-enabled)
- Déclarés (sites-available):
- Nombreux vhosts dont `dev3.4nkweb.com`, `lecoffreio.4nkweb.com`, et de multiples variantes/bak `local.4nkweb.com-3000*`
- Ces derniers exposent `server_name local.4nkweb.com` et des redirections 301 vers `https://dev3.4nkweb.com/...` ou anciennement `https://dev4.4nkweb.com/lecoffre...`
## Redirections et routage
- HTTPS forcé: OK (80 → 301 https) sur les vhosts projet et locaux pertinents
- Backends: 8080 (backend), 8090 (relay), 300x (fronts)
- Storage: rewrite + proxy vers backend 8080
## Conformité « ne plus utiliser local.4nkweb.com »
- Projet: conforme (aucune référence active)
- Local: non conforme (résidus `local.4nkweb.com` encore présents, dont un vhost actif `sites-enabled/local.4nkweb.com-3001`)
## Recommandations (sans exécution)
1. Désactiver `sites-enabled/local.4nkweb.com-3001` (supprimer le lien symbolique) après vérification de non-utilisation.
2. Purger les anciens fichiers `sites-available/local.4nkweb.com-3000*` (ou archiver ailleurs) pour éviter les confusions.
3. Conserver `local.lecoffreio.4nkweb.com` (HTTP→301) si encore utile au flux de callback, sinon documenter sa dépréciation.
4. Vérifier régulièrement que `lecoffreio.4nkweb.com` et `dev3.4nkweb.com` restent cohérents (front/back/storage/relay).
## Notes
- Cet audit na effectué aucune modification. Toute action doit être validée avant exécution.

36
docs/ports-2025-09-24.md Normal file
View File

@ -0,0 +1,36 @@
# Etat des ports et applications (24/09/2025)
Source: `ss`, `lsof` sur la machine locale.
## TCP en écoute
- 80 — nginx
- 443 — nginx
- 22 — sshd
- 631 — CUPS
- 3000 — Next.js (front)
- 3003 — node (service front annexe)
- 8080 — node (lecoffre-back-mini)
- 8081 — sdk_storage
- 8000 — blindbit
- 8332 — bitcoind (RPC)
- 8333 — bitcoind (P2P)
- 8334 — bitcoind (loopback)
- 38332 — bitcoind (signet)
- 38333 — bitcoind (signet)
- 38334 — bitcoind (loopback signet)
- 29000 — bitcoind (interne)
- 9050 — tor (SOCKS)
- 9051 — tor (control)
- 9090 — node (relay/signer)
- 5432 — PostgreSQL
- 9999 — next-server (dev)
- 35981 — node (outil dev, loopback)
- 44077 — node (outil dev, loopback)
## UDP
- 5353/53517 — mDNS/Avahi
## Détails clés
- Backend actif: PID `node dist/server.js` écoutant sur 8080, `/api/v1/health` OK
- Nginx actif et rechargé, `local.4nkweb.com` retiré des vhosts actifs

39
global.d.ts vendored Normal file
View File

@ -0,0 +1,39 @@
// Global type declarations for packages without @types
declare module 'ovh' {
interface OVHConfig {
appKey: string;
appSecret: string;
consumerKey: string;
}
interface OVHClient {
request(method: string, path: string, params: any, callback: (error: any, result: any) => void): void;
}
function ovh(config: OVHConfig): OVHClient;
export = ovh;
}
declare module '@mailchimp/mailchimp_transactional' {
interface MailchimpMessage {
template_name: string;
template_content: any[];
message: {
global_merge_vars: Array<{ name: string; content: string }>;
from_email: string;
from_name: string;
subject: string;
to: Array<{ email: string; type: string }>;
};
}
interface MailchimpClient {
messages: {
sendTemplate(message: MailchimpMessage): Promise<any>;
};
}
function mailchimp(apiKey: string): MailchimpClient;
export = mailchimp;
}

22
logs/backend.out Normal file
View File

@ -0,0 +1,22 @@
[dotenv@17.2.2] injecting env (44) from .env -- tip: ⚙️ specify custom .env file path with { path: '/custom/path/.env' }
/home/ank/dev/lecoffre-back-mini/node_modules/uuid/dist/cjs/v1.js:28
state.msecs ??= -Infinity;
^^^
SyntaxError: Unexpected token '??='
at wrapSafe (internal/modules/cjs/loader.js:1029:16)
at Module._compile (internal/modules/cjs/loader.js:1078:27)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:1143:10)
at Module.load (internal/modules/cjs/loader.js:979:32)
at Function.Module._load (internal/modules/cjs/loader.js:819:12)
at Module.require (internal/modules/cjs/loader.js:1003:19)
at require (internal/modules/cjs/helpers.js:107:18)
at Object.<anonymous> (/home/ank/dev/lecoffre-back-mini/node_modules/uuid/dist/cjs/index.js:12:15)
at Module._compile (internal/modules/cjs/loader.js:1114:14)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:1143:10)
at Module.load (internal/modules/cjs/loader.js:979:32)
at Function.Module._load (internal/modules/cjs/loader.js:819:12)
at Module.require (internal/modules/cjs/loader.js:1003:19)
at require (internal/modules/cjs/helpers.js:107:18)
at Object.<anonymous> (/home/ank/dev/lecoffre-back-mini/dist/utils/session-manager.js:4:16)
at Module._compile (internal/modules/cjs/loader.js:1114:14)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1 @@
bash: ligne 1: npm : commande introuvable

1715
logs/restart.out Normal file

File diff suppressed because it is too large Load Diff

1
logs/server.pid Normal file
View File

@ -0,0 +1 @@
4104714

View File

@ -2,10 +2,17 @@
"name": "lecoffre-back-mini",
"version": "1.0.0",
"description": "Mini serveur avec une route /api/ping",
"main": "src/server.js",
"main": "dist/server.js",
"scripts": {
"start": "node src/server.js",
"dev": "nodemon src/server.js"
"build": "tsc",
"start": "node dist/server.js",
"dev": "ts-node src/server.ts",
"watch": "nodemon --exec ts-node src/server.ts",
"dev:js": "nodemon src/server.js",
"test:db": "npm run build && node test-db-init.js",
"test:rattachements": "node test-rattachements-endpoint.js",
"test:quick": "node quick-test-rattachements.js",
"launch:check": "bash scripts/launch_check.sh"
},
"dependencies": {
"@mailchimp/mailchimp_transactional": "^1.0.59",
@ -14,10 +21,20 @@
"express": "^4.18.2",
"node-fetch": "^2.6.7",
"ovh": "^2.0.3",
"pg": "^8.11.3",
"sdk-signer-client": "file:../sdk-signer-client",
"stripe": "^18.3.0",
"uuid": "^11.1.0"
},
"devDependencies": {
"nodemon": "^3.0.1"
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/node": "^20.11.19",
"@types/node-fetch": "^2.6.11",
"@types/pg": "^8.11.0",
"@types/uuid": "^9.0.8",
"nodemon": "^3.0.1",
"ts-node": "^10.9.2",
"typescript": "^5.3.3"
}
}

159
quick-test-rattachements.js Executable file
View File

@ -0,0 +1,159 @@
#!/usr/bin/env node
const fetch = require('node-fetch');
const { SDKSignerClient } = require('sdk-signer-client');
// Quick test configuration
const BASE_URL = 'http://localhost:8080';
const ENDPOINT = '/api/v1/idnot/user/rattachements'; // Base endpoint, idnot will be added as path parameter
const signerConfig = {
url: process.env.SIGNER_WS_URL || 'ws://localhost:9090',
apiKey: process.env.SIGNER_API_KEY || 'your-api-key-change-this',
timeout: 30000,
reconnectInterval: 5000,
maxReconnectAttempts: 3
};
// Test with a specific IDNot
async function testWithIdNot(idNot) {
if (!idNot) {
console.log('💡 Usage: node quick-test-rattachements.js [idNot]');
console.log(' Example: node quick-test-rattachements.js 12345');
console.log(' Example: node quick-test-rattachements.js (no parameter to test without idNot)');
console.log(' URL format: /api/v1/idnot/user/{idnot}/rattachements');
return;
}
console.log(`🆔 Testing with IDNot: ${idNot}`);
// Build URL with path parameter
let url = `${BASE_URL}${ENDPOINT}`;
url += `?idNot=${encodeURIComponent(idNot)}`;
console.log(`📍 URL: ${url}`);
console.log('=' .repeat(60));
try {
const response = await fetch(url, {
method: 'GET',
headers: {
'Accept': 'application/json'
}
});
console.log(`📊 Status: ${response.status} ${response.statusText}`);
const responseText = await response.text();
console.log(`📄 Response length: ${responseText.length} characters`);
let data;
try {
data = JSON.parse(responseText);
console.log('📋 Parsed JSON response:');
console.log(JSON.stringify(data, null, 2));
} catch (e) {
console.log('📋 Raw response (not JSON):');
console.log(responseText);
return;
}
for (const office of data) {
let officeRattachementsData = [];
// Now test the office rattachements
const officeRattachements = await fetch(`${BASE_URL}/api/v1/idnot/office/rattachements?idNot=${office.ou}`, {
method: 'GET',
headers: {
'Accept': 'application/json'
}
});
console.log(`📊 Status: ${officeRattachements.status} ${officeRattachements.statusText}`);
const officeRattachementsText = await officeRattachements.text();
console.log(`📄 Response length: ${officeRattachementsText.length} characters`);
try {
officeRattachementsData = JSON.parse(officeRattachementsText);
console.log('📋 Parsed JSON response:');
console.log(JSON.stringify(officeRattachementsData, null, 2));
} catch (e) {
console.log('📋 Raw response (not JSON):');
console.log(officeRattachementsText);
return;
}
// Now try to create a new process with all the users that have `activite` set to `En exercice`
const usersToAdd = officeRattachementsData.result.filter(user => user.activite === 'En exercice');
console.log(`📋 Users to add: ${usersToAdd.length}`);
console.log(JSON.stringify(usersToAdd, null, 2));
// Probably the idnot number should be public so that caller can easily find the processId?
// Caller can now create the office process with the following data
const processData = {
name: 'New Process',
description: 'New Process Description',
timestamp: new Date().toISOString(),
office: office.ou,
};
const privateFields = Object.keys(processData);
privateFields.splice(privateFields.indexOf('office'), 1); // Make office public data
const roles = {
owner: {
members: usersToAdd.map(user => user.uid),
validation_rules: [
{
quorum: 0.1,
fields: [...privateFields, 'roles', 'office'],
min_sig_member: 1,
},
],
storages: ["https://dev3.4nkweb.com/storage"]
},
apophis: {
members: usersToAdd.map(user => user.uid),
validation_rules: [],
storages: []
}
};
}
} catch (error) {
console.log(`💥 Error: ${error.message}`);
}
}
// Main execution
async function main() {
const idNot = process.argv[2]; // Get IDNot from command line argument
console.log('🚀 Quick Rattachements Endpoint Test');
console.log('=' .repeat(60));
// Check if server is running
try {
const healthCheck = await fetch(`${BASE_URL}/api/v1/health`);
if (healthCheck.ok) {
console.log('✅ Server is running');
} else {
console.log(`⚠️ Server responded but health check failed with status: ${healthCheck.status}`);
}
} catch (error) {
console.log('❌ Server is not responding');
console.log('💡 Make sure to start your server first with: npm run dev');
return;
}
console.log('');
await testWithIdNot(idNot);
console.log('\n💡 Usage: node quick-test-rattachements.js [idNot]');
console.log(' Example: node quick-test-rattachements.js 12345');
console.log(' Example: node quick-test-rattachements.js (no parameter to test without idNot)');
console.log(' URL format: /api/v1/idnot/user/{idnot}/rattachements');
}
main().catch(console.error);

26
scripts/backup_nginx_confs.sh Executable file
View File

@ -0,0 +1,26 @@
#!/usr/bin/env bash
set -euo pipefail
timestamp="$(date +%Y%m%d-%H%M%S)"
dest_dir="backups/nginx/${timestamp}"
mkdir -p "${dest_dir}"
# Project nginx confs
proj_src="confs/nginx"
if [ -d "$proj_src" ]; then
rsync -a --delete "$proj_src/" "${dest_dir}/project/"
fi
# Local nginx common paths
declare -a local_paths=(/etc/nginx /usr/local/etc/nginx "$HOME/.config/nginx" ./conf/ngnix)
for p in "${local_paths[@]}"; do
if [ -d "$p" ]; then
safe_name="$(echo "$p" | sed "s|/|_|g;s|^_||")"
rsync -a "$p/" "${dest_dir}/local_${safe_name}/" || true
fi
done
# Create compressed archives for convenience
tar -C "${dest_dir}" -czf "${dest_dir}.tar.gz" .
echo "Saved nginx backups to ${dest_dir} and ${dest_dir}.tar.gz"

43
scripts/launch_check.sh Executable file
View File

@ -0,0 +1,43 @@
#!/usr/bin/env bash
set -euo pipefail
echo "[1/6] Backing up nginx confs..."
./scripts/backup_nginx_confs.sh
echo "[2/6] Checking required environment variables..."
REQUIRED=("PORT" "APP_HOST" "DEFAULT_STORAGE" "SIGNER_WS_URL" "SIGNER_API_KEY")
MISSING=()
for v in "${REQUIRED[@]}"; do
if [ -z "${!v:-}" ]; then MISSING+=("$v"); fi
done
if [ ${#MISSING[@]} -gt 0 ]; then
echo "Missing env vars: ${MISSING[*]}" >&2; exit 1; fi
echo "[3/6] Building backend..."
npm run build --silent
echo "[4/6] Starting backend (detached) if not already running..."
if ! nc -z localhost "${PORT}" >/dev/null 2>&1; then
nohup node dist/server.js > logs/backend.out 2>&1 &
echo $! > logs/server.pid
sleep 2
fi
if ! nc -z localhost "${PORT}" >/dev/null 2>&1; then echo "Backend not listening on ${PORT}" >&2; exit 1; fi
echo "[5/6] Curl checks - backend health and key routes..."
set +e
curl -fsS "http://localhost:${PORT}/api/v1/health" | jq . >/dev/null && echo "OK /api/v1/health" || { echo "FAIL /api/v1/health"; exit 1; }
curl -fsS -X OPTIONS -H "Origin: ${APP_HOST}" "http://localhost:${PORT}/api/v1/health" -o /dev/null && echo "OK CORS preflight" || { echo "FAIL CORS"; exit 1; }
set -e
echo "[6/6] External service checks..."
echo "- Checking mempool signet..."
curl -fsS "https://mempool2.4nkweb.com/fr/docs/api/rest" -o /dev/null && echo "OK mempool" || echo "WARN mempool unreachable"
echo "- Checking signer relay ws..."
if command -v wscat >/dev/null 2>&1; then
( timeout 3 wscat -c "${SIGNER_WS_URL/ws:/wss:}" >/dev/null 2>&1 && echo "OK signer ws connect" ) || echo "WARN signer ws connect failed"
else
echo "wscat not installed; skipping ws check"
fi
echo "All checks done."

View File

@ -0,0 +1,22 @@
#!/usr/bin/env bash
set -euo pipefail
echo "[nginx-cleanup] Disabling local.4nkweb.com vhost and cleaning old files"
TARGET_ENABLED="/etc/nginx/sites-enabled/local.4nkweb.com-3001"
TARGET_AVAILABLE_PREFIX="/etc/nginx/sites-available/local.4nkweb.com-3000"
if [ -L "$TARGET_ENABLED" ] || [ -e "$TARGET_ENABLED" ]; then
echo "- Removing enabled vhost: $TARGET_ENABLED"
sudo rm -f "$TARGET_ENABLED"
else
echo "- Enabled vhost not present: $TARGET_ENABLED"
fi
echo "- Removing available files: ${TARGET_AVAILABLE_PREFIX}*"
sudo rm -f ${TARGET_AVAILABLE_PREFIX}* || true
echo "- Testing nginx configuration"
sudo nginx -t
echo "[nginx-cleanup] Done. You can now reload: sudo systemctl reload nginx"

View File

@ -0,0 +1,21 @@
#!/usr/bin/env bash
set -euo pipefail
ts="$(date +%Y%m%d-%H%M%S)"
bkdir="confs/nginx/_scrub_backup_${ts}"
mkdir -p "$bkdir"
echo "[nginx-scrub] Backing up system files to $bkdir"
for f in /etc/nginx/sites-available/dev3.4nkweb.com /etc/nginx/sites-enabled/dev3.4nkweb.com; do
if [ -f "$f" ]; then
cp -a "$f" "$bkdir"/
fi
done
echo "[nginx-scrub] Removing comment lines mentioning local.4nkweb.com"
sudo sed -i "/local\\.4nkweb\\.com/d" /etc/nginx/sites-available/dev3.4nkweb.com || true
sudo sed -i "/local\\.4nkweb\\.com/d" /etc/nginx/sites-enabled/dev3.4nkweb.com || true
echo "[nginx-scrub] Testing nginx configuration"
sudo nginx -t
echo "[nginx-scrub] If ok, reload with: sudo systemctl reload nginx"

8
src/config/email.ts Normal file
View File

@ -0,0 +1,8 @@
export const emailConfig = {
MAILCHIMP_API_KEY: process.env.MAILCHIMP_API_KEY,
MAILCHIMP_KEY: process.env.MAILCHIMP_KEY,
MAILCHIMP_LIST_ID: process.env.MAILCHIMP_LIST_ID,
PORT: parseInt(process.env.PORT || '8080'),
FROM_EMAIL: 'no-reply@lecoffre.io',
FROM_NAME: 'LeCoffre.io'
};

62
src/config/index.ts Normal file
View File

@ -0,0 +1,62 @@
import * as dotenv from 'dotenv';
dotenv.config();
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',
// 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$/
];
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
};
})()
};

9
src/config/signer.ts Normal file
View File

@ -0,0 +1,9 @@
import { ClientConfig } from 'sdk-signer-client';
export const signerConfig: ClientConfig = {
url: process.env.SIGNER_WS_URL || 'ws://localhost:9090',
apiKey: process.env.SIGNER_API_KEY || 'your-api-key-change-this',
timeout: 30000,
reconnectInterval: 2000, // Let SDK try quick reconnects first
maxReconnectAttempts: 3 // Limited SDK attempts, then our service takes over
};

12
src/config/sms.ts Normal file
View File

@ -0,0 +1,12 @@
export const smsConfig = {
// OVH config
OVH_APP_KEY: process.env.OVH_APP_KEY,
OVH_APP_SECRET: process.env.OVH_APP_SECRET,
OVH_CONSUMER_KEY: process.env.OVH_CONSUMER_KEY,
OVH_SMS_SERVICE_NAME: process.env.OVH_SMS_SERVICE_NAME,
// SMS Factor config
SMS_FACTOR_TOKEN: process.env.SMS_FACTOR_TOKEN,
PORT: parseInt(process.env.PORT || '8080')
};

5
src/config/stripe.ts Normal file
View File

@ -0,0 +1,5 @@
export const stripeConfig = {
STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY,
STRIPE_WEBHOOK_SECRET: process.env.STRIPE_WEBHOOK_SECRET,
APP_HOST: process.env.APP_HOST || 'http://localhost:3000',
};

View File

@ -0,0 +1,105 @@
import { Request, Response } from 'express';
import { EmailService, pendingEmails } from '../services/email';
import { ETemplates } from '../types';
export class EmailController {
static async sendEmail(req: Request, res: Response) {
const { email, firstName, lastName, officeName, template } = req.body;
try {
const templateVariables = {
first_name: firstName || '',
last_name: lastName || '',
office_name: officeName || '',
link: `${process.env.APP_HOST}`
};
const result = await EmailService.sendTransactionalEmail(
email,
ETemplates[template as keyof typeof ETemplates],
'Votre notaire vous envoie un message',
templateVariables
);
if (!result.success) {
// Add to pending emails to retry later
const emailId = `${email}-${Date.now()}`;
pendingEmails.set(emailId, {
to: email,
templateName: ETemplates[template as keyof typeof ETemplates],
subject: 'Votre notaire vous envoie un message',
templateVariables,
attempts: 1,
lastAttempt: Date.now()
});
}
res.json({
success: true,
message: 'Email envoyé avec succès'
});
} catch (error: any) {
console.error('Erreur:', error);
res.status(500).json({
success: false,
message: 'Erreur serveur lors de l\'envoi de l\'email'
});
}
}
static async subscribeToList(req: Request, res: Response) {
const { email } = req.body;
try {
const result = await EmailService.addToMailchimpList(email);
if (result.success) {
res.json({
success: true,
message: 'Inscription à la liste réussie'
});
} else {
res.status(500).json({
success: false,
message: 'Échec de l\'inscription à la liste'
});
}
} catch (error: any) {
console.error('Erreur:', error);
res.status(500).json({
success: false,
message: 'Erreur serveur lors de l\'inscription'
});
}
}
static async sendReminder(req: Request, res: Response) {
const { office, customer } = req.body;
try {
const to = customer.contact.email;
const templateVariables = {
office_name: office.name,
last_name: customer.contact.last_name || '',
first_name: customer.contact.first_name || '',
link: `${process.env.APP_HOST}`
};
await EmailService.sendTransactionalEmail(
to,
ETemplates.DOCUMENT_REMINDER,
'Vous avez des documents à déposer pour votre dossier.',
templateVariables
);
res.json({
success: true,
message: 'Email envoyé avec succès'
});
} catch (error) {
console.error(error);
return;
}
}
}

View File

@ -0,0 +1,7 @@
import { Request, Response } from 'express';
export class HealthController {
static getHealth(req: Request, res: Response) {
res.json({ message: 'OK' });
}
}

View File

@ -0,0 +1,279 @@
import fetch from 'node-fetch';
import { v4 as uuidv4 } from 'uuid';
import { IdNotService } from '../services/idnot';
import { authTokens } from '../utils/auth-tokens';
import { IdNotUser, AuthToken } from '../types';
import { Logger } from '../utils/logger';
import { NotFoundError, ExternalServiceError, BusinessRuleError } from '../types/errors';
/**
* Pure controller methods that handle business logic
* without depending on Express Request/Response objects
*/
export class IdNotController {
/**
* Get user rattachements by idNot
*/
static async getUserRattachements(idNot: string): Promise<any[]> {
Logger.info('Getting user rattachements', { idNot });
const json = await IdNotService.getUserRattachements(idNot);
// Check if any rattachements found
if (!json.result || json.result.length === 0) {
throw new NotFoundError('No rattachements found');
}
// Get office data for each rattachement
const officeData = await Promise.all(json.result.map(async (result: any) => {
const searchParams = new URLSearchParams({
key: process.env.IDNOT_API_KEY || '',
deleted: 'false'
});
try {
const headers: Record<string, string> = {
'Accept': 'application/json'
};
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(`${process.env.IDNOT_ANNUARY_BASE_URL}${result.entiteUrl}?` + searchParams, {
method: 'GET',
headers
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
Logger.error('Failed to fetch office data', {
entiteUrl: result.entiteUrl,
error: error instanceof Error ? error.message : 'Unknown error'
});
throw new ExternalServiceError('IdNot', `Failed to fetch office data: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}));
Logger.info('Successfully retrieved user rattachements', {
idNot,
count: officeData.length
});
return officeData;
}
/**
* Get office rattachements by office idNot
*/
static async getOfficeRattachements(idNot: string): Promise<any> {
Logger.info('Getting office rattachements', { idNot });
try {
const result = await IdNotService.getOfficeRattachements(idNot);
Logger.info('Successfully retrieved office rattachements', {
idNot,
count: result.result?.length || 0
});
return result;
} catch (error) {
Logger.error('Failed to get office rattachements', {
idNot,
error: error instanceof Error ? error.message : 'Unknown error'
});
throw new ExternalServiceError('IdNot', `Failed to get office rattachements: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Authenticate user with authorization code
*/
static async authenticate(code: string): Promise<{ idNotUser: IdNotUser; authToken: string }> {
Logger.info('IdNot authentication initiated', { codePrefix: code.substring(0, 8) + '...' });
try {
// 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,
tokenKeys: Object.keys(tokens)
});
const jwt = tokens.id_token;
if (!jwt) {
throw new BusinessRuleError('No ID token received from IdNot');
}
// 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');
}
// Build IdNotUser object
const idNotUser: IdNotUser = {
idNot: payload.sub,
office: {
idNot: payload.entity_idn,
name: userData.entite.denominationSociale ?? userData.entite.codeCrpcen,
crpcen: userData.entite.codeCrpcen,
office_status: IdNotService.getOfficeStatus(userData.entite.statutEntite.name),
address: {
address: officeLocationData.result[0].adrGeo4,
city: officeLocationData.result[0].adrGeoVille.split(' ')[0] ?? officeLocationData.result[0].adrGeoVille,
zip_code: Number(officeLocationData.result[0].adrGeoCodePostal)
},
status: 'ACTIVE'
},
role: IdNotService.getRole(userData.typeLien.name),
contact: {
first_name: userData.personne.prenom,
last_name: userData.personne.nomUsuel,
email: userData.mailRattachement,
phone_number: userData.numeroTelephone,
cell_phone_number: userData.numeroMobile ?? userData.numeroTelephone,
civility: IdNotService.getCivility(userData.personne.civilite)
},
office_role: IdNotService.getOfficeRole(userData.typeLien.name)
};
if (!idNotUser.contact.email) {
throw new BusinessRuleError('User professional email is empty');
}
// Generate auth token
const authToken = uuidv4();
const tokenData: AuthToken = {
idNot: idNotUser.idNot,
authToken,
idNotUser: idNotUser,
pairingId: null,
defaultStorage: null,
createdAt: Date.now(),
expiresAt: Date.now() + (24 * 60 * 60 * 1000) // 24 hours
};
authTokens.push(tokenData);
Logger.info('IdNot authentication successful', {
idNot: idNotUser.idNot,
office: idNotUser.office.name
});
return { idNotUser, authToken };
} catch (error) {
Logger.error('IdNot authentication failed', {
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'}`);
}
}
/**
* Get current user data by idNot and authToken
*/
static async getCurrentUser(idNot: string, authToken: string): Promise<{ success: boolean; data: IdNotUser }> {
Logger.info('Getting current user data', { idNot });
// 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', {
idNot,
office: userAuth.idNotUser.office.name
});
return {
success: true,
data: userAuth.idNotUser
};
}
/**
* Logout user by removing auth token
*/
static async logout(authToken: string): Promise<{ success: boolean; message: string }> {
Logger.info('User logout initiated');
// 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
});
} else {
Logger.warn('Logout attempted with invalid token');
}
return {
success: true,
message: 'Déconnexion réussie'
};
}
/**
* Validate if a token is still valid
*/
static async validateToken(idNot: string): Promise<{ success: boolean; message: string; data: { idNot: string; valid: boolean } }> {
Logger.debug('Token validation requested', { idNot });
return {
success: true,
message: 'Token valide',
data: {
idNot,
valid: true
}
};
}
}

View File

@ -0,0 +1,550 @@
import { Request, Response } from 'express';
import { v4 as uuidv4 } from 'uuid';
import { SignerService } from '../services/signer';
import { SessionManager } from '../utils/session-manager';
import { authTokens } from '../utils/auth-tokens';
import { ProcessInfo, ProcessData, ProcessRoles, EOfficeStatus } from '../types';
import { config } from '../config';
import { Logger } from '../utils/logger';
import {
AppError,
ErrorCode,
NotFoundError,
ExternalServiceError,
BusinessRuleError
} from '../types/errors';
import { asyncHandler } from '../middleware/error-handler';
import { IdNotController } from './idnot.controller';
export class ProcessController {
static getUserProcess = asyncHandler(async (req: Request, res: Response): Promise<void> => {
const requestId = req.headers['x-request-id'] as string;
Logger.info('User process request initiated', { requestId });
// Find the full token data which should contain the original idNotUser data
const userAuth = authTokens.find(auth => auth.authToken === req.idNotUser!.authToken);
if (!userAuth || !userAuth.idNotUser) {
throw new NotFoundError('Données utilisateur non trouvées. Veuillez vous reconnecter.', requestId);
}
const { pairingId } = req.query;
// Execute signer operations with retry logic
const processResult = await SignerService.executeWithRetry(
async (signerClient) => {
return await signerClient.getUserProcessByIdnot(userAuth.idNotUser.idNot);
},
'getUserProcessByIdnot',
3
);
if (!processResult.success) {
throw new ExternalServiceError('Signer', processResult.error?.message || 'Failed to get user process', requestId);
}
let process: ProcessInfo | null = processResult.data || null;
if (!process) {
Logger.info('No existing process found, creating new one', {
requestId,
userIdNot: userAuth.idNotUser.idNot
});
// Get UUID from database
let uuid: string = uuidv4();
// TODO this should be moved to signer probably
// try {
// const result = await Database.query('SELECT uid FROM users WHERE "idNot" = $1', [userAuth.idNotUser.idNot]);
// uuid = result.rows.length > 0 ? result.rows[0].uid : null;
// } catch (error) {
// Logger.error('Error fetching UUID by idNot', {
// requestId,
// error: error instanceof Error ? error.message : 'Unknown error'
// });
// uuid = '';
// }
// if (!uuid) {
// Logger.info('No existing UUID found in db, generating new one', { requestId });
// uuid = uuidv4();
// }
const processData: ProcessData = {
uid: uuid,
utype: 'collaborator',
idNot: userAuth.idNotUser.idNot,
office: {
idNot: userAuth.idNotUser.office.idNot,
},
role: userAuth.idNotUser.role,
office_role: userAuth.idNotUser.office_role,
contact: userAuth.idNotUser.contact,
};
const privateFields = Object.keys(processData);
const allFields = [...privateFields, 'roles'];
// Make those fields public
privateFields.splice(privateFields.indexOf('uid'), 1);
privateFields.splice(privateFields.indexOf('utype'), 1);
privateFields.splice(privateFields.indexOf('idNot'), 1);
// Get pairing ID with retry
const pairingResult = await SignerService.executeWithRetry(
async (signerClient) => {
return await signerClient.getPairingId();
},
'getPairingId',
3
);
if (!pairingResult.success) {
throw new ExternalServiceError('Signer', pairingResult.error?.message || 'Failed to get pairing ID', requestId);
}
const validatorId = pairingResult.data!.pairingId;
const roles: ProcessRoles = {
owner: {
members: [pairingId as string, validatorId],
validation_rules: [
{
quorum: 0.1,
fields: allFields,
min_sig_member: 1,
}
],
storages: [config.defaultStorage]
},
apophis: {
members: [pairingId as string, validatorId],
validation_rules: [],
storages: []
}
};
// Create process with retry
const createResult = await SignerService.executeWithRetry(
async (signerClient) => {
return await signerClient.createProcess(processData, privateFields, roles);
},
'createProcess',
3
);
if (!createResult.success) {
throw new ExternalServiceError('Signer', createResult.error?.message || 'Failed to create process', requestId);
}
Logger.info('Created new process', {
requestId,
processId: createResult.data!.processId,
processData: createResult.data!.processData
});
process = {
processId: createResult.data!.processId || '',
processData: createResult.data!.data
};
} else {
Logger.info('Using existing process', {
requestId,
processId: process.processId
});
}
// Check if process is committed and handle role updates
const processManagementResult = await SignerService.executeWithRetry(
async (signerClient) => {
const allProcesses = await signerClient.getOwnedProcesses();
if (allProcesses && process) {
const processStates = allProcesses.processes[process.processId].states;
const isNotCommited = processStates.length === 2
&& processStates[1].commited_in === processStates[0].commited_in;
if (isNotCommited) {
Logger.info('Process not committed, committing it', {
requestId,
processId: process.processId
});
await signerClient.validateState(process.processId, processStates[0].state_id);
}
// Check pairing ID in roles
let roles: ProcessRoles;
if (isNotCommited) {
const firstState = processStates[0];
roles = firstState.roles;
} else {
const tip = processStates[processStates.length - 1].commited_in;
const lastState = processStates.findLast((state: any) => state.commited_in !== tip);
roles = lastState.roles;
}
if (!roles) {
throw new Error('No roles found');
} else if (!roles['owner']) {
throw new Error('No owner role found');
}
if (!roles['owner'].members.includes(req.query.pairingId as string)) {
Logger.info('Adding new pairingId to owner role', {
requestId,
pairingId: req.query.pairingId,
processId: process.processId
});
roles['owner'].members.push(req.query.pairingId as string);
const updatedProcessReturn = await signerClient.updateProcess(process.processId, {}, [], roles);
const processId = updatedProcessReturn.updatedProcess.process_id;
const stateId = updatedProcessReturn.updatedProcess.diffs[0].state_id;
await signerClient.notifyUpdate(processId, stateId);
await signerClient.validateState(processId, stateId);
} else {
Logger.info('PairingId already in owner role', {
requestId,
pairingId: req.query.pairingId,
roles: roles['owner'].members
});
}
}
return process;
},
'processManagement',
2
);
if (!processManagementResult.success) {
throw new ExternalServiceError('Signer', processManagementResult.error?.message || 'Failed to manage process', requestId);
}
Logger.info('User process request completed successfully', {
requestId,
processId: process?.processId
});
res.json({
success: true,
data: processManagementResult.data
});
});
static getOfficeProcess = asyncHandler(async (req: Request, res: Response): Promise<void> => {
const requestId = req.headers['x-request-id'] as string;
Logger.info('Office process request initiated', { requestId });
const userAuth = authTokens.find(auth => auth.authToken === req.idNotUser!.authToken);
if (!userAuth || !userAuth.idNotUser) {
throw new NotFoundError('Données utilisateur non trouvées. Veuillez vous reconnecter.', requestId);
}
// Check office status
if (userAuth.idNotUser.office.office_status !== EOfficeStatus.ACTIVATED) {
throw new BusinessRuleError('Office not activated', undefined, requestId);
}
// Get office process with retry
const processResult = await SignerService.executeWithRetry(
async (signerClient) => {
return await signerClient.getOfficeProcessByIdnot(userAuth.idNotUser.office.idNot);
},
'getOfficeProcessByIdnot',
3
);
if (!processResult.success) {
throw new ExternalServiceError('Signer', processResult.error?.message || 'Failed to get office process', requestId);
}
let process: ProcessInfo | null = processResult.data || null;
if (!process) {
Logger.info('No existing office process found, creating new one', {
requestId,
officeIdNot: userAuth.idNotUser.office.idNot
});
// Get validator ID with retry
const pairingResult = await SignerService.executeWithRetry(
async (signerClient) => {
return await signerClient.getPairingId();
},
'getPairingId',
3
);
if (!pairingResult.success) {
throw new ExternalServiceError('Signer', pairingResult.error?.message || 'Failed to get validator ID', requestId);
}
const validatorId = pairingResult.data!.pairingId;
if (!validatorId) {
throw new BusinessRuleError('No validator id found', undefined, requestId);
}
// Get UUID from database
let uuid: string = uuidv4();
// try {
// const result = await Database.query('SELECT uid FROM offices WHERE "idNot" = $1', [userAuth.idNotUser.office.idNot]);
// uuid = result.rows.length > 0 ? result.rows[0].uid : null;
// } catch (error) {
// Logger.error('Error fetching office UUID by idNot', {
// requestId,
// error: error instanceof Error ? error.message : 'Unknown error'
// });
// uuid = '';
// }
// if (!uuid) {
// Logger.info('No existing office UUID found in db, generating new one', { requestId });
// uuid = uuidv4();
// }
const processData: ProcessData = {
uid: uuid,
utype: 'office',
...userAuth.idNotUser.office,
};
const privateFields = Object.keys(processData);
const allFields = [...privateFields, 'roles'];
// No need for public fields?
const roles: ProcessRoles = {
collaborator: {
members: [],
validation_rules: [],
storages: []
},
owner: {
members: [validatorId],
validation_rules: [{
quorum: 0.1,
fields: allFields,
min_sig_member: 1,
}],
storages: [config.defaultStorage]
},
apophis: {
members: [validatorId],
validation_rules: [],
storages: []
}
};
// Create office process with retry
const createResult = await SignerService.executeWithRetry(
async (signerClient) => {
return await signerClient.createProcess(processData, privateFields, roles);
},
'createOfficeProcess',
3
);
if (!createResult.success) {
throw new ExternalServiceError('Signer', createResult.error?.message || 'Failed to create office process', requestId);
}
Logger.info('Created new office process', { process: createResult.data });
process = {
processId: createResult.data!.processCreated.processId,
processData: createResult.data!.processData
};
}
Logger.info('Using office process', {
requestId,
processId: process.processId,
processData: process.processData
});
// Check if process is committed and handle role updates
const processManagementResult = await SignerService.executeWithRetry(
async (signerClient) => {
const allProcesses = await signerClient.getOwnedProcesses();
if (allProcesses && process) {
const processStates = allProcesses.processes[process.processId].states;
const isNotCommited = processStates.length === 2
&& processStates[1].commited_in === processStates[0].commited_in;
if (isNotCommited) {
Logger.info('Process not committed, committing it', {
requestId,
processId: process.processId
});
await signerClient.validateState(process.processId, processStates[0].state_id);
}
}
// // Use the idnot number to identify all active members of the office
// const officeCollaborators = await IdNotController.getOfficeRattachements(userAuth.idNotUser.office.idNot);
// Logger.debug('Office collaborators', { officeCollaborators });
// let roles: ProcessRoles;
// if (isNotCommited) {
// const firstState = processStates[0];
// roles = firstState.roles;
// } else {
// const tip = processStates[processStates.length - 1].commited_in;
// const lastState = processStates.findLast((state: any) => state.commited_in !== tip);
// roles = lastState.roles;
// }
// if (!roles) {
// throw new Error('No roles found');
// } else if (!roles['owner'] || !roles['collaborator']) {
// throw new Error('No owner or collaborator role found');
// }
// for (const collaborator of officeCollaborators) {
// if (collaborator.idNot === userAuth.idNotUser.idNot) {
// // We add ourselves regardless of the activity status
// // We should have a collaborator process
// continue;
// }
// if (collaborator.activite === 'En exercice') {
// // TODO we check if the collaborator has a process
// // If not, we create a new process for them
// // if yes, we add the collaborator process in role `collaborator`
// // we also lookup the pairing ids in the collaborator process and put them all in the owner role
// }
// }
// if (!roles['owner'].members.includes(req.query.pairingId as string)) {
// Logger.info('Adding new pairingId to owner role', {
// requestId,
// pairingId: req.query.pairingId,
// processId: process.processId
// });
// roles['owner'].members.push(req.query.pairingId as string);
// const updatedProcessReturn = await signerClient.updateProcess(process.processId, {}, [], roles);
// const processId = updatedProcessReturn.updatedProcess.process_id;
// const stateId = updatedProcessReturn.updatedProcess.diffs[0].state_id;
// await signerClient.notifyUpdate(processId, stateId);
// await signerClient.validateState(processId, stateId);
// }
return process;
},
'processManagement',
2
);
console.log('processManagementResult', processManagementResult);
if (!processManagementResult.success) {
throw new ExternalServiceError('Signer', processManagementResult.error?.message || 'Failed to manage office process', requestId);
}
Logger.info('Office process request completed successfully', {
requestId,
processId: processManagementResult.data?.processId
});
res.json({
success: true,
data: processManagementResult.data
});
});
static authenticateClient = asyncHandler(async (req: Request, res: Response): Promise<void> => {
const requestId = req.headers['x-request-id'] as string;
const { pairingId } = req.body;
if (!pairingId) {
throw new BusinessRuleError('Missing pairingId', undefined, requestId);
}
Logger.info('Client authentication initiated', { requestId, pairingId });
// This should be implemented properly based on your business logic
// For now, just clean up the session
SessionManager.deleteSession(req.session!.id);
Logger.info('Client authentication completed', { requestId });
res.json({
success: true,
message: 'Client authentication successful',
data: {}
});
});
static getPhoneNumberForEmail = asyncHandler(async (req: Request, res: Response): Promise<void> => {
const requestId = req.headers['x-request-id'] as string;
const { email } = req.body;
if (!email) {
throw new BusinessRuleError('Missing email', undefined, requestId);
}
Logger.info('Phone number lookup initiated', { requestId, email });
const phoneResult = await SignerService.executeWithRetry(
async (signerClient) => {
return await signerClient.getPhoneNumberForEmail(email);
},
'getPhoneNumberForEmail',
3
);
if (!phoneResult.success) {
throw new ExternalServiceError('Signer', phoneResult.error?.message || 'Failed to get phone number', requestId);
}
const phoneNumber = phoneResult.data;
if (!phoneNumber) {
throw new NotFoundError('No phone number found for this email', requestId);
}
Logger.info('Phone number lookup completed', { requestId, email, phoneResult });
res.json({
success: true,
message: 'Phone number retrieved successfully',
phoneNumber: phoneNumber
});
});
// Health check endpoint for signer service
static getSignerHealth = asyncHandler(async (req: Request, res: Response): Promise<void> => {
const healthStatus = SignerService.getHealthStatus();
res.json({
success: true,
data: {
signer: healthStatus,
timestamp: new Date().toISOString()
}
});
});
// Force reconnection endpoint (for debugging/admin use)
static forceSignerReconnect = asyncHandler(async (req: Request, res: Response): Promise<void> => {
const requestId = req.headers['x-request-id'] as string;
Logger.info('Force signer reconnection requested', { requestId });
const reconnectResult = await SignerService.forceReconnect();
if (!reconnectResult.success) {
throw new ExternalServiceError('Signer', reconnectResult.error?.message || 'Failed to reconnect', requestId);
}
Logger.info('Force signer reconnection completed', { requestId });
res.json({
success: true,
message: 'Signer reconnection initiated',
data: SignerService.getHealthStatus()
});
});
}

View File

@ -0,0 +1,182 @@
import { Request, Response } from 'express';
import { SmsService } from '../services/sms';
import { verificationCodes } from '../utils/verification-codes';
import { SessionManager } from '../utils/session-manager';
import { Validator } from '../utils/validation';
import { Logger } from '../utils/logger';
import {
RateLimitError,
BusinessRuleError,
ExternalServiceError,
AppError,
ErrorCode
} from '../types/errors';
import { asyncHandler } from '../middleware/error-handler';
export class SmsImprovedController {
static sendCode = asyncHandler(async (req: Request, res: Response): Promise<void> => {
const requestId = req.headers['x-request-id'] as string;
const { phoneNumber } = req.body;
// Validate input
Validator.validate(req.body, Validator.phoneRules(), requestId);
Logger.info('SMS code request initiated', {
requestId,
phoneNumber: phoneNumber.replace(/\d(?=\d{4})/g, '*') // Mask phone number
});
// Check rate limiting
const existingVerification = verificationCodes.get(phoneNumber);
if (existingVerification) {
const timeSinceLastSend = Date.now() - existingVerification.timestamp;
if (timeSinceLastSend < 30000) { // 30 seconds
throw new RateLimitError(
'Veuillez attendre 30 secondes avant de demander un nouveau code',
requestId
);
}
}
// Generate and store code
const code = SmsService.generateCode();
verificationCodes.set(phoneNumber, {
code,
timestamp: Date.now(),
attempts: 0
});
// Send SMS
const message = `Votre code de vérification LeCoffre est : ${code}`;
const result = await SmsService.sendSms(phoneNumber, message);
if (!result.success) {
Logger.error('SMS sending failed', {
requestId,
phoneNumber: phoneNumber.replace(/\d(?=\d{4})/g, '*'),
error: result.error
});
throw new ExternalServiceError('SMS', result.error || 'Échec de l\'envoi du SMS');
}
Logger.info('SMS code sent successfully', {
requestId,
phoneNumber: phoneNumber.replace(/\d(?=\d{4})/g, '*')
});
res.json({
success: true,
message: 'Code envoyé avec succès'
});
});
static verifyCode = asyncHandler(async (req: Request, res: Response): Promise<void> => {
const requestId = req.headers['x-request-id'] as string;
const { phoneNumber, code } = req.body;
// Validate input
Validator.validate(req.body, [
...Validator.phoneRules(),
{
field: 'code',
required: true,
type: 'string',
minLength: 4,
maxLength: 6
}
], requestId);
Logger.info('SMS code verification initiated', {
requestId,
phoneNumber: phoneNumber.replace(/\d(?=\d{4})/g, '*')
});
// Development shortcut
if (code === '1234') {
const sessionId = SessionManager.createSession(phoneNumber);
Logger.info('Development code used', {
requestId,
phoneNumber: phoneNumber.replace(/\d(?=\d{4})/g, '*'),
sessionId
});
res.json({
success: true,
message: 'Code vérifié avec succès',
sessionId: sessionId
});
return;
}
const verification = verificationCodes.get(phoneNumber);
if (!verification) {
throw new BusinessRuleError(
'Aucun code n\'a été envoyé à ce numéro',
undefined,
requestId
);
}
// Check expiration (5 minutes)
if (Date.now() - verification.timestamp > 5 * 60 * 1000) {
verificationCodes.delete(phoneNumber);
throw new BusinessRuleError(
'Le code a expiré',
undefined,
requestId
);
}
// Verify code
if (verification.code.toString() === code.toString()) {
verificationCodes.delete(phoneNumber);
const sessionId = SessionManager.createSession(phoneNumber);
Logger.info('SMS code verified successfully', {
requestId,
phoneNumber: phoneNumber.replace(/\d(?=\d{4})/g, '*'),
sessionId
});
res.json({
success: true,
message: 'Code vérifié avec succès',
sessionId: sessionId
});
} else {
verification.attempts += 1;
if (verification.attempts >= 3) {
verificationCodes.delete(phoneNumber);
Logger.warn('Too many SMS verification attempts', {
requestId,
phoneNumber: phoneNumber.replace(/\d(?=\d{4})/g, '*'),
attempts: verification.attempts
});
throw new BusinessRuleError(
'Trop de tentatives. Veuillez demander un nouveau code',
undefined,
requestId
);
} else {
Logger.warn('Invalid SMS code provided', {
requestId,
phoneNumber: phoneNumber.replace(/\d(?=\d{4})/g, '*'),
attempts: verification.attempts
});
throw new BusinessRuleError(
'Code incorrect',
[{ field: 'code', value: code, constraints: ['Code de vérification incorrect'] }],
requestId
);
}
}
});
}

View File

@ -0,0 +1,126 @@
import { Request, Response } from 'express';
import { SmsService } from '../services/sms';
import { verificationCodes } from '../utils/verification-codes';
import { SessionManager } from '../utils/session-manager';
export class SmsController {
static async sendCode(req: Request, res: Response): Promise<any> {
const { phoneNumber } = req.body;
try {
// Check if a code already exists and is not expired
const existingVerification = verificationCodes.get(phoneNumber);
if (existingVerification) {
const timeSinceLastSend = Date.now() - existingVerification.timestamp;
if (timeSinceLastSend < 30000) { // 30 secondes
return res.status(429).json({
success: false,
message: 'Veuillez attendre 30 secondes avant de demander un nouveau code'
});
}
}
// Generate a new code
const code = SmsService.generateCode();
// Store the code
verificationCodes.set(phoneNumber, {
code,
timestamp: Date.now(),
attempts: 0
});
// Send the SMS
const message = `Votre code de vérification LeCoffre est : ${code}`;
const result = await SmsService.sendSms(phoneNumber, message);
if (result.success) {
res.json({
success: true,
message: 'Code envoyé avec succès',
});
} else {
res.status(500).json({
success: false,
message: 'Échec de l\'envoi du SMS via les deux fournisseurs'
});
}
} catch (error: any) {
console.error('Error:', error);
res.status(500).json({
success: false,
message: 'Erreur serveur lors de l\'envoi du code'
});
}
}
static verifyCode(req: Request, res: Response): any {
const { phoneNumber, code } = req.body;
if (!code) {
return res.status(400).json({
success: false,
message: 'Le code est requis'
});
}
// shortcut for development only
if (code === '1234') {
// Create a session for the verified user
const sessionId = SessionManager.createSession(phoneNumber);
return res.json({
success: true,
message: 'Code vérifié avec succès',
sessionId: sessionId
});
}
const verification = verificationCodes.get(phoneNumber);
if (!verification) {
return res.status(400).json({
success: false,
message: 'Aucun code n\'a été envoyé à ce numéro'
});
}
// Check if the code has not expired (5 minutes)
if (Date.now() - verification.timestamp > 5 * 60 * 1000) {
verificationCodes.delete(phoneNumber);
return res.status(400).json({
success: false,
message: 'Le code a expiré'
});
}
// Check if the code is correct
if (verification.code.toString() === code.toString()) {
verificationCodes.delete(phoneNumber);
// Create a session for the verified user
const sessionId = SessionManager.createSession(phoneNumber);
res.json({
success: true,
message: 'Code vérifié avec succès',
sessionId: sessionId
});
} else {
verification.attempts += 1;
if (verification.attempts >= 3) {
verificationCodes.delete(phoneNumber);
res.status(400).json({
success: false,
message: 'Trop de tentatives. Veuillez demander un nouveau code'
});
} else {
res.status(400).json({
success: false,
message: 'Code incorrect'
});
}
}
}
}

View File

@ -0,0 +1,113 @@
import { Request, Response } from 'express';
import Stripe from 'stripe';
import { StripeService } from '../services/stripe';
import { stripeConfig } from '../config/stripe';
export class StripeController {
private static stripeService = new StripeService();
// Only for test
static async createTestSubscription(req: Request, res: Response) {
try {
const result = await StripeController.stripeService.createTestSubscription();
res.json({
success: true,
data: result
});
} catch (error: any) {
res.status(500).json({
success: false,
message: 'Erreur lors de la création de l\'abonnement de test',
error: {
message: error.message,
type: error.type,
code: error.code
}
});
}
}
static async createCheckoutSession(req: Request, res: Response) {
try {
const session = await StripeController.stripeService.createCheckoutSession(req.body, req.body.frequency);
res.json({ success: true, sessionId: session.id });
} catch (error) {
console.error('Error creating checkout:', error);
res.status(500).json({
success: false,
message: 'Erreur lors de la création de la session de paiement'
});
}
}
static async getSubscription(req: Request, res: Response) {
try {
const subscription = await StripeController.stripeService.getSubscription(req.params.id);
res.json({ success: true, subscription });
} catch (error) {
res.status(500).json({
success: false,
message: 'Erreur lors de la récupération de l\'abonnement'
});
}
}
static async createPortalSession(req: Request, res: Response) {
try {
const session = await StripeController.stripeService.createPortalSession(req.params.id);
res.json({ success: true, url: session.url });
} catch (error) {
res.status(500).json({
success: false,
message: 'Erreur lors de la création de la session du portail'
});
}
}
static async handleWebhook(req: Request, res: Response): Promise<any> {
const sig = req.headers['stripe-signature'] as string;
let event: Stripe.Event;
try {
event = Stripe.webhooks.constructEvent(req.body, sig, stripeConfig.STRIPE_WEBHOOK_SECRET!);
} catch (err: any) {
return res.status(400).send(`Webhook Error: ${err.message}`);
}
try {
switch (event.type) {
case 'checkout.session.completed':
const session = event.data.object as Stripe.Checkout.Session;
if (session.status === 'complete') {
const subscription = JSON.parse(session.metadata!.subscription);
// Stock subscription (create process)
console.log('New subscription:', subscription);
}
break;
case 'invoice.payment_succeeded':
const invoice = event.data.object as Stripe.Invoice;
if (['subscription_update', 'subscription_cycle'].includes(invoice.billing_reason!)) {
const subscription = await StripeController.stripeService.getSubscription((invoice as any).subscription);
// Update subscription (update process)
console.log('Subscription update:', subscription);
}
break;
case 'customer.subscription.deleted':
const deletedSubscription = event.data.object as Stripe.Subscription;
// Delete subscription (update process to delete)
console.log('Subscription deleted:', deletedSubscription.id);
break;
}
res.json({ received: true });
} catch (error) {
console.error('Webhook error:', error);
res.status(500).json({
success: false,
message: 'Error processing webhook'
});
}
}
}

View File

@ -0,0 +1,61 @@
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);
// 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}`;
res.redirect(302, redirectTo);
});
}

View File

@ -0,0 +1,135 @@
import { Request, Response } from 'express';
import { IdNotController } from '../controllers/idnot.controller';
import { asyncHandler } from '../middleware/error-handler';
import { ValidationError, BusinessRuleError } from '../types/errors';
/**
* 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'] }
], requestId);
}
// Call pure controller method with extracted parameters
const result = await IdNotController.getUserRattachements(idNot);
res.json(result);
});
/**
* 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'] }
], requestId);
}
// Call pure controller method
const result = await IdNotController.getOfficeRattachements(idNot);
res.json(result);
});
/**
* 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 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'] }
], requestId);
}
// Call pure controller method
const result = await IdNotController.authenticate(code);
res.json(result);
});
/**
* GET /user (protected)
* Extract user info from middleware and call controller
*/
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);
});
/**
* POST /logout (protected)
* Extract user info and call controller
*/
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);
});
/**
* GET /validate (protected)
* Extract user info and call controller
*/
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 });
});
}

46
src/middleware/auth.ts Normal file
View File

@ -0,0 +1,46 @@
import { Request, Response, NextFunction } from 'express';
import { authTokens } from '../utils/auth-tokens';
// IdNot Authentication Middleware
export const authenticateIdNot = (req: Request, res: Response, next: NextFunction): any => {
const authToken = req.headers['authorization']?.replace('Bearer ', '') || req.headers['x-auth-token'] as string || req.body.authToken;
if (!authToken) {
return res.status(401).json({
success: false,
message: 'Token d\'authentification requis'
});
}
// Find the user by auth token
const userAuth = authTokens.find(auth => auth.authToken === authToken);
if (!userAuth) {
return res.status(401).json({
success: false,
message: 'Token d\'authentification invalide'
});
}
// Check if token has expired
if (Date.now() > userAuth.expiresAt) {
// Remove expired token
const tokenIndex = authTokens.findIndex(auth => auth.authToken === authToken);
if (tokenIndex > -1) {
authTokens.splice(tokenIndex, 1);
}
return res.status(401).json({
success: false,
message: 'Token d\'authentification expiré'
});
}
// Add user info to request
req.idNotUser = {
idNot: userAuth.idNot,
authToken: userAuth.authToken
};
next();
};

View File

@ -0,0 +1,147 @@
import { Request, Response, NextFunction } from 'express';
import { AppError, ErrorCode } from '../types/errors';
import { Logger } from '../utils/logger';
export const errorHandler = (
error: Error | AppError,
req: Request,
res: Response,
next: NextFunction
): void => {
const requestId = req.headers['x-request-id'] as string || 'unknown';
// If it's already an AppError, use it directly
if (error instanceof AppError) {
Logger.error('Application error occurred', {
requestId,
error: {
code: error.code,
message: error.message,
statusCode: error.statusCode,
details: error.details,
stack: error.stack
},
request: {
method: req.method,
url: req.url,
userAgent: req.get('User-Agent'),
ip: req.ip
}
});
res.status(error.statusCode).json(error.toJSON());
return;
}
// Handle known error types
if (error.name === 'ValidationError') {
const appError = new AppError(
ErrorCode.VALIDATION_ERROR,
'Erreur de validation',
400,
true,
undefined,
requestId
);
Logger.error('Validation error', {
requestId,
originalError: error.message,
stack: error.stack
});
res.status(400).json(appError.toJSON());
return;
}
if (error.name === 'UnauthorizedError') {
const appError = new AppError(
ErrorCode.UNAUTHORIZED,
'Non autorisé',
401,
true,
undefined,
requestId
);
Logger.error('Unauthorized access attempt', {
requestId,
error: error.message,
request: {
method: req.method,
url: req.url,
ip: req.ip
}
});
res.status(401).json(appError.toJSON());
return;
}
// Handle database errors
if (error.message?.includes('database') || error.message?.includes('connection')) {
const appError = new AppError(
ErrorCode.DATABASE_ERROR,
'Erreur de base de données',
500,
true,
undefined,
requestId
);
Logger.error('Database error', {
requestId,
error: error.message,
stack: error.stack
});
res.status(500).json(appError.toJSON());
return;
}
// Generic server error
const appError = new AppError(
ErrorCode.INTERNAL_SERVER_ERROR,
'Erreur interne du serveur',
500,
false, // Non-operational error
undefined,
requestId
);
Logger.error('Unhandled error', {
requestId,
error: {
name: error.name,
message: error.message,
stack: error.stack
},
request: {
method: req.method,
url: req.url,
body: req.body,
userAgent: req.get('User-Agent'),
ip: req.ip
}
});
res.status(500).json(appError.toJSON());
};
// Middleware to catch async errors
export const asyncHandler = (fn: Function) => {
return (req: Request, res: Response, next: NextFunction) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
};
// Request ID middleware
export const requestIdMiddleware = (req: Request, res: Response, next: NextFunction) => {
const requestId = req.headers['x-request-id'] as string ||
`req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
req.headers['x-request-id'] = requestId;
res.setHeader('X-Request-ID', requestId);
next();
};

25
src/middleware/session.ts Normal file
View File

@ -0,0 +1,25 @@
import { Request, Response, NextFunction } from 'express';
import { SessionManager } from '../utils/session-manager';
// Middleware to validate session
export const validateSession = (req: Request, res: Response, next: NextFunction): any => {
const sessionId = req.headers['x-session-id'] as string || req.body.sessionId;
if (!sessionId) {
return res.status(401).json({
success: false,
message: 'Session ID requis'
});
}
const session = SessionManager.getSession(sessionId);
if (!session) {
return res.status(401).json({
success: false,
message: 'Session invalide ou expirée'
});
}
req.session = session;
next();
};

View File

@ -0,0 +1,60 @@
import { Request, Response, NextFunction } from 'express';
import { Validators } from '../utils/validators';
// Phone number validation middleware
export const validatePhoneNumber = (req: Request, res: Response, next: NextFunction): any => {
const { phoneNumber } = req.body;
if (!phoneNumber) {
return res.status(400).json({
success: false,
message: 'Le numéro de téléphone est requis'
});
}
if (!Validators.validatePhoneNumber(phoneNumber)) {
return res.status(400).json({
success: false,
message: 'Format de numéro de téléphone invalide'
});
}
next();
};
// Email validation middleware
export const validateEmail = (req: Request, res: Response, next: NextFunction): any => {
const { email } = req.body;
if (!email) {
return res.status(400).json({
success: false,
message: 'L\'adresse email est requise'
});
}
if (!Validators.validateEmail(email)) {
return res.status(400).json({
success: false,
message: 'Format d\'email invalide'
});
}
next();
};
// Subscription validation middleware
export const validateSubscription = (req: Request, res: Response, next: NextFunction): any => {
const { type, seats, frequency } = req.body;
const validation = Validators.validateSubscription(type, seats, frequency);
if (!validation.valid) {
return res.status(400).json({
success: false,
message: validation.message
});
}
next();
};

View File

@ -0,0 +1,11 @@
import { Router } from 'express';
import { EmailController } from '../controllers/email.controller';
import { validateEmail } from '../middleware/validation';
const router = Router();
router.post('/send-email', validateEmail, EmailController.sendEmail);
router.post('/subscribe-to-list', validateEmail, EmailController.subscribeToList);
router.post('/send_reminder', EmailController.sendReminder);
export { router as emailRoutes };

View File

@ -0,0 +1,8 @@
import { Router } from 'express';
import { HealthController } from '../controllers/health.controller';
const router = Router();
router.get('/health', HealthController.getHealth);
export { router as healthRoutes };

View File

@ -0,0 +1,15 @@
import { Router } from 'express';
import { IdNotHandlers } from '../handlers/idnot.handlers';
import { authenticateIdNot } from '../middleware/auth';
const router = Router();
router.get('/user/rattachements', IdNotHandlers.getUserRattachements);
router.get('/office/rattachements', IdNotHandlers.getOfficeRattachements);
router.post('/auth', IdNotHandlers.authenticate);
router.get('/user', authenticateIdNot, IdNotHandlers.getCurrentUser);
router.post('/logout', authenticateIdNot, IdNotHandlers.logout);
router.get('/validate', authenticateIdNot, IdNotHandlers.validateToken);
export { router as idnotRoutes };

26
src/routes/index.ts Normal file
View File

@ -0,0 +1,26 @@
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';
const router = Router();
// State and callback endpoints (front-agnostic) - must be before /api routes
router.post('/api/v1/idnot/state', StateHandlers.createState);
router.get('/idnot/callback', IdNotCallbackHandlers.callback);
router.get('/authorized-client', IdNotCallbackHandlers.callback);
// 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 };

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

@ -0,0 +1,20 @@
import { Router } from 'express';
import { ProcessController } from '../controllers/process.controller';
import { authenticateIdNot } from '../middleware/auth';
import { validateSession } from '../middleware/session';
const router = Router();
// Health check routes (public)
router.get('/health/signer', ProcessController.getSignerHealth);
router.post('/admin/signer/reconnect', ProcessController.forceSignerReconnect); // Should be protected in production
// IdNot protected routes
router.get('/user', authenticateIdNot, ProcessController.getUserProcess);
router.get('/office', authenticateIdNot, ProcessController.getOfficeProcess);
// Customer auth routes (session protected)
router.post('/customer/auth/client-auth', validateSession, ProcessController.authenticateClient);
router.post('/customer/auth/get-phone-number-for-email', validateSession, ProcessController.getPhoneNumberForEmail);
export { router as processRoutes };

10
src/routes/sms.routes.ts Normal file
View File

@ -0,0 +1,10 @@
import { Router } from 'express';
import { SmsController } from '../controllers/sms.controller';
import { validatePhoneNumber } from '../middleware/validation';
const router = Router();
router.post('/send-code', validatePhoneNumber, SmsController.sendCode);
router.post('/verify-code', validatePhoneNumber, SmsController.verifyCode);
export { router as smsRoutes };

View File

@ -0,0 +1,18 @@
import express, { Router } from 'express';
import { StripeController } from '../controllers/stripe.controller';
import { validateSubscription } from '../middleware/validation';
const router = Router();
// Test route
router.post('/test/create-subscription', StripeController.createTestSubscription);
// Subscription routes
router.post('/subscriptions/checkout', validateSubscription, StripeController.createCheckoutSession);
router.get('/subscriptions/:id', StripeController.getSubscription);
router.post('/subscriptions/:id/portal', StripeController.createPortalSession);
// Webhook route (requires raw body)
router.post('/webhooks/stripe', express.raw({ type: 'application/json' }), StripeController.handleWebhook);
export { router as stripeRoutes };

View File

@ -1,940 +0,0 @@
const express = require('express');
const cors = require('cors');
const fetch = require('node-fetch');
const { v4: uuidv4 } = require('uuid');
const ovh = require('ovh');
const mailchimp = require('@mailchimp/mailchimp_transactional');
const Stripe = require('stripe');
require('dotenv').config();
// Initialisation de l'application Express
const app = express();
const PORT = process.env.PORT || 8080;
// Configuration CORS
const corsOptions = {
origin: ['http://local.lecoffreio.4nkweb:3000', 'http://localhost:3000', 'https://lecoffreio.4nkweb.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization']
};
app.use(cors(corsOptions));
app.use(express.json());
const authTokens = [];
const ECivility = {
MALE: 'MALE',
FEMALE: 'FEMALE',
OTHERS: 'OTHERS'
};
const EOfficeStatus = {
ACTIVATED: 'ACTIVATED',
DESACTIVATED: 'DESACTIVATED'
};
const EIdnotRole = {
DIRECTEUR: "Directeur général du CSN",
NOTAIRE_TITULAIRE: "Notaire titulaire",
NOTAIRE_ASSOCIE: "Notaire associé",
NOTAIRE_SALARIE: "Notaire salarié",
COLLABORATEUR: "Collaborateur",
SECRETAIRE_GENERAL: "Secrétaire général",
SUPPLEANT: "Suppléant",
ADMINISTRATEUR: "Administrateur",
RESPONSABLE: "Responsable",
CURATEUR: "Curateur",
}
function getOfficeStatus(statusName) {
switch (statusName) {
case "Pourvu":
return EOfficeStatus.ACTIVATED;
case "Pourvu mais décédé":
return EOfficeStatus.ACTIVATED;
case "Sans titulaire":
return EOfficeStatus.ACTIVATED;
case "Vacance":
return EOfficeStatus.ACTIVATED;
case "En activité":
return EOfficeStatus.ACTIVATED;
default:
return EOfficeStatus.DESACTIVATED;
}
}
function getOfficeRole(roleName) {
switch (roleName) {
case EIdnotRole.NOTAIRE_TITULAIRE:
return { name: 'Notaire' };
case EIdnotRole.NOTAIRE_ASSOCIE:
return { name: 'Notaire' };
case EIdnotRole.NOTAIRE_SALARIE:
return { name: 'Notaire' };
case EIdnotRole.COLLABORATEUR:
return { name: 'Collaborateur' };
case EIdnotRole.SUPPLEANT:
return { name: 'Collaborateur' };
case EIdnotRole.ADMINISTRATEUR:
return { name: 'Collaborateur' };
case EIdnotRole.CURATEUR:
return { name: 'Collaborateur' };
default:
return null;
}
}
function getRole(roleName) {
switch (roleName) {
case EIdnotRole.NOTAIRE_TITULAIRE:
return { name: 'admin' };
case EIdnotRole.NOTAIRE_ASSOCIE:
return { name: 'admin' };
case EIdnotRole.NOTAIRE_SALARIE:
return { name: 'notary' };
case EIdnotRole.COLLABORATEUR:
return { name: 'notary' };
case EIdnotRole.SUPPLEANT:
return { name: 'notary' };
case EIdnotRole.ADMINISTRATEUR:
return { name: 'admin' };
case EIdnotRole.CURATEUR:
return { name: 'notary' };
default:
return { name: 'default' };
}
}
function getCivility(civility) {
switch (civility) {
case 'Monsieur':
return ECivility.MALE;
case 'Madame':
return ECivility.FEMALE;
default:
return ECivility.OTHERS;
}
}
app.get('/api/v1/health', (req, res) => {
res.json({ message: 'OK' });
});
app.post('/api/v1/idnot/user/:code', async (req, res) => {
const code = req.params.code;
try {
const params = {
client_id: 'B3CE56353EDB15A9',
client_secret: '3F733549E879878344B6C949B366BB5CDBB2DB5B7F7AB7EBBEBB0F0DD0776D1C',
redirect_uri: 'http://local.lecoffreio.4nkweb:3000/authorized-client',
grant_type: 'authorization_code',
code: code
};
const tokens = await (
await fetch('https://qual-connexion.idnot.fr/user/IdPOAuth2/token/idnot_idp_v1', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams(params).toString()
})
).json();
const jwt = tokens.id_token;
if (!jwt) {
console.error('jwt not defined');
return null;
}
const payload = JSON.parse(Buffer.from(jwt.split('.')[1], 'base64').toString('utf8'));
const searchParams = new URLSearchParams({
key: 'ba557f84-0bf6-4dbf-844f-df2767555e3e'
});
let userData;
try {
userData = await (
await fetch(`https://qual-api.notaires.fr/annuaire/api/pp/v2/rattachements/${payload.profile_idn}?` + searchParams, {
method: 'GET'
})
).json();
} catch (error) {
console.error('Error fetching ' + `https://qual-api.notaires.fr/annuaire/api/pp/v2/rattachements/${payload.profile_idn}`, error);
return null;
}
if (!userData || !userData.statutDuRattachement || userData.entite.typeEntite.name !== 'office') {
console.error('User not attached to an office (May be a partner)');
return null;
}
let officeLocationData;
try {
officeLocationData = (await (
await fetch(`https://qual-api.notaires.fr/annuaire${userData.entite.locationsUrl}?` + searchParams,
{
method: 'GET'
})
).json());
} catch (error) {
console.error('Error fetching' + `https://qual-api.notaires.fr/annuaire${userData.entite.locationsUrl}`, error);
return null;
}
if (!officeLocationData || !officeLocationData.result || officeLocationData.result.length === 0) {
console.error('Office location data not found');
return null;
}
const idNotUser = {
idNot: payload.sub,
office: {
idNot: payload.entity_idn,
name: userData.entite.denominationSociale ?? userData.entite.codeCrpcen,
crpcen: userData.entite.codeCrpcen,
office_status: getOfficeStatus(userData.entite.statutEntite.name),
address: {
address: officeLocationData.result[0].adrGeo4,
city: officeLocationData.result[0].adrGeoVille.split(' ')[0] ?? officeLocationData.result[0].adrGeoVille,
zip_code: Number(officeLocationData.result[0].adrGeoCodePostal)
},
status: 'ACTIVE'
},
role: getRole(userData.typeLien.name),
contact: {
first_name: userData.personne.prenom,
last_name: userData.personne.nomUsuel,
email: userData.mailRattachement,
phone_number: userData.numeroTelephone,
cell_phone_number: userData.numeroMobile ?? userData.numeroTelephone,
civility: getCivility(userData.personne.civilite)
},
office_role: getOfficeRole(userData.typeLien.name)
};
if (!idNotUser.contact.email) {
console.error('User pro email empty');
return null;
}
const authToken = uuidv4();
authTokens.push({ idNot: idNotUser.idNot, authToken });
res.json({ idNotUser, authToken });
} catch (error) {
res.status(500).json({
error: 'Internal Server Error',
message: error.message
});
}
});
//------------------------------------ SMS Section -----------------------------------------
const configSms = {
// OVH config
OVH_APP_KEY: process.env.OVH_APP_KEY,
OVH_APP_SECRET: process.env.OVH_APP_SECRET,
OVH_CONSUMER_KEY: process.env.OVH_CONSUMER_KEY,
OVH_SMS_SERVICE_NAME: process.env.OVH_SMS_SERVICE_NAME,
// SMS Factor config
SMS_FACTOR_TOKEN: process.env.SMS_FACTOR_TOKEN,
PORT: process.env.PORT || 8080
};
// Codes storage
const verificationCodes = new Map();
// Service SMS
class SmsService {
static generateCode() {
return Math.floor(100000 + Math.random() * 900000);
}
// OVH Service
static sendSmsWithOvh(phoneNumber, message) {
return new Promise((resolve, reject) => {
const ovhClient = ovh({
appKey: configSms.OVH_APP_KEY,
appSecret: configSms.OVH_APP_SECRET,
consumerKey: configSms.OVH_CONSUMER_KEY
});
ovhClient.request('POST', `/sms/${configSms.OVH_SMS_SERVICE_NAME}/jobs`, {
message: message,
receivers: [phoneNumber],
senderForResponse: false,
sender: 'not.IT Fact',
noStopClause: true
}, (error, result) => {
if (error) {
console.error('Erreur OVH SMS:', error);
resolve({ success: false, error: 'Échec de l\'envoi du SMS via OVH' });
} else {
resolve({ success: true });
}
});
});
}
// SMS Factor Service
static async sendSmsWithSmsFactor(phoneNumber, message) {
try {
const url = new URL('https://api.smsfactor.com/send/simulate');
url.searchParams.append('to', phoneNumber);
url.searchParams.append('text', message);
url.searchParams.append('sender', 'LeCoffre');
url.searchParams.append('token', configSms.SMS_FACTOR_TOKEN);
const response = await fetch(url.toString());
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return { success: true };
} catch (error) {
console.error('Erreur SMS Factor:', error);
return { success: false, error: 'Échec de l\'envoi du SMS via SMS Factor' };
}
}
// Main method
static async sendSms(phoneNumber, message) {
// Try first with OVH
const ovhResult = await this.sendSmsWithOvh(phoneNumber, message);
if (ovhResult.success) {
return ovhResult;
}
// If OVH fails, try with SMS Factor
console.log('OVH SMS failed, trying SMS Factor...');
return await this.sendSmsWithSmsFactor(phoneNumber, message);
}
}
// Phone number validation middleware
const validatePhoneNumber = (req, res, next) => {
const { phoneNumber } = req.body;
if (!phoneNumber) {
return res.status(400).json({
success: false,
message: 'Le numéro de téléphone est requis'
});
}
// Validation basique du format
const phoneRegex = /^\+?[1-9]\d{1,14}$/;
if (!phoneRegex.test(phoneNumber)) {
return res.status(400).json({
success: false,
message: 'Format de numéro de téléphone invalide'
});
}
next();
};
// Routes
app.post('/api/send-code', validatePhoneNumber, async (req, res) => {
const { phoneNumber } = req.body;
try {
// Check if a code already exists and is not expired
const existingVerification = verificationCodes.get(phoneNumber);
if (existingVerification) {
const timeSinceLastSend = Date.now() - existingVerification.timestamp;
if (timeSinceLastSend < 30000) { // 30 secondes
return res.status(429).json({
success: false,
message: 'Veuillez attendre 30 secondes avant de demander un nouveau code'
});
}
}
// Generate a new code
const code = SmsService.generateCode();
// Store the code
verificationCodes.set(phoneNumber, {
code,
timestamp: Date.now(),
attempts: 0
});
// Send the SMS
const message = `Votre code de vérification LeCoffre est : ${code}`;
const result = await SmsService.sendSms(phoneNumber, message);
if (result.success) {
res.json({
success: true,
message: 'Code envoyé avec succès',
});
} else {
res.status(500).json({
success: false,
message: 'Échec de l\'envoi du SMS via les deux fournisseurs'
});
}
} catch (error) {
console.error('Erreur:', error);
res.status(500).json({
success: false,
message: 'Erreur serveur lors de l\'envoi du code'
});
}
});
app.post('/api/verify-code', validatePhoneNumber, (req, res) => {
const { phoneNumber, code } = req.body;
if (!code) {
return res.status(400).json({
success: false,
message: 'Le code est requis'
});
}
const verification = verificationCodes.get(phoneNumber);
if (!verification) {
return res.status(400).json({
success: false,
message: 'Aucun code n\'a été envoyé à ce numéro'
});
}
// Check if the code has not expired (5 minutes)
if (Date.now() - verification.timestamp > 5 * 60 * 1000) {
verificationCodes.delete(phoneNumber);
return res.status(400).json({
success: false,
message: 'Le code a expiré'
});
}
// Check if the code is correct
if (verification.code.toString() === code.toString()) {
verificationCodes.delete(phoneNumber);
res.json({
success: true,
message: 'Code vérifié avec succès'
});
} else {
verification.attempts += 1;
if (verification.attempts >= 3) {
verificationCodes.delete(phoneNumber);
res.status(400).json({
success: false,
message: 'Trop de tentatives. Veuillez demander un nouveau code'
});
} else {
res.status(400).json({
success: false,
message: 'Code incorrect'
});
}
}
});
//------------------------------------ End of SMS Section ------------------------------------
//------------------------------------ Email Section -----------------------------------------
const configEmail = {
MAILCHIMP_API_KEY: process.env.MAILCHIMP_API_KEY,
MAILCHIMP_KEY: process.env.MAILCHIMP_KEY,
MAILCHIMP_LIST_ID: process.env.MAILCHIMP_LIST_ID,
PORT: process.env.PORT || 8080,
FROM_EMAIL: 'no-reply@lecoffre.io',
FROM_NAME: 'LeCoffre.io'
};
// Email storage
const pendingEmails = new Map();
// Email service
class EmailService {
static async sendTransactionalEmail(to, templateName, subject, templateVariables) {
try {
const mailchimpClient = mailchimp(configEmail.MAILCHIMP_API_KEY);
const message = {
template_name: templateName,
template_content: [],
message: {
global_merge_vars: this.buildVariables(templateVariables),
from_email: configEmail.FROM_EMAIL,
from_name: configEmail.FROM_NAME,
subject: subject,
to: [
{
email: to,
type: 'to'
}
]
}
};
const result = await mailchimpClient.messages.sendTemplate(message);
return { success: true, result };
} catch (error) {
console.error('Erreur envoi email:', error);
return { success: false, error: 'Échec de l\'envoi de l\'email' };
}
}
static buildVariables(templateVariables) {
return Object.keys(templateVariables).map(key => ({
name: key,
content: templateVariables[key]
}));
}
// Add to Mailchimp diffusion list
static async addToMailchimpList(email) {
try {
const url = `https://us17.api.mailchimp.com/3.0/lists/${configEmail.MAILCHIMP_LIST_ID}/members`;
const response = await fetch(url, {
method: 'POST',
headers: {
'Authorization': `apikey ${configEmail.MAILCHIMP_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
email_address: email,
status: 'subscribed'
})
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return { success: true, data };
} catch (error) {
console.error('Erreur ajout à la liste:', error);
return { success: false, error: 'Échec de l\'ajout à la liste Mailchimp' };
}
}
static async retryFailedEmails() {
for (const [emailId, emailData] of pendingEmails) {
if (emailData.attempts >= 10) {
pendingEmails.delete(emailId);
continue;
}
const nextRetryDate = new Date(emailData.lastAttempt);
nextRetryDate.setMinutes(nextRetryDate.getMinutes() + Math.pow(emailData.attempts, 2));
if (Date.now() >= nextRetryDate) {
try {
const result = await this.sendTransactionalEmail(
emailData.to,
emailData.templateName,
emailData.subject,
emailData.templateVariables
);
if (result.success) {
pendingEmails.delete(emailId);
} else {
emailData.attempts += 1;
emailData.lastAttempt = Date.now();
}
} catch (error) {
emailData.attempts += 1;
emailData.lastAttempt = Date.now();
}
}
}
}
}
// Email validation middleware
const validateEmail = (req, res, next) => {
const { email } = req.body;
if (!email) {
return res.status(400).json({
success: false,
message: 'L\'adresse email est requise'
});
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
return res.status(400).json({
success: false,
message: 'Format d\'email invalide'
});
}
next();
};
// Email templates
const ETemplates = {
DOCUMENT_ASKED: "DOCUMENT_ASKED",
DOCUMENT_REFUSED: "DOCUMENT_REFUSED",
DOCUMENT_RECAP: "DOCUMENT_RECAP",
SUBSCRIPTION_INVITATION: "SUBSCRIPTION_INVITATION",
DOCUMENT_REMINDER: "DOCUMENT_REMINDER",
DOCUMENT_SEND: "DOCUMENT_SEND",
};
// Routes
app.post('/api/send-email', validateEmail, async (req, res) => {
const { email, firstName, lastName, officeName, template } = req.body;
try {
const templateVariables = {
first_name: firstName || '',
last_name: lastName || '',
office_name: officeName || '',
link: `${process.env.APP_HOST}`
};
const result = await EmailService.sendTransactionalEmail(
email,
ETemplates[template],
'Votre notaire vous envoie un message',
templateVariables
);
if (!result.success) {
// Add to pending emails to retry later
const emailId = `${email}-${Date.now()}`;
pendingEmails.set(emailId, {
to: email,
templateName: ETemplates[template],
subject: 'Votre notaire vous envoie un message',
templateVariables,
attempts: 1,
lastAttempt: Date.now()
});
}
res.json({
success: true,
message: 'Email envoyé avec succès'
});
} catch (error) {
console.error('Erreur:', error);
res.status(500).json({
success: false,
message: 'Erreur serveur lors de l\'envoi de l\'email'
});
}
});
app.post('/api/subscribe-to-list', validateEmail, async (req, res) => {
const { email } = req.body;
try {
const result = await EmailService.addToMailchimpList(email);
if (result.success) {
res.json({
success: true,
message: 'Inscription à la liste réussie'
});
} else {
res.status(500).json({
success: false,
message: 'Échec de l\'inscription à la liste'
});
}
} catch (error) {
console.error('Erreur:', error);
res.status(500).json({
success: false,
message: 'Erreur serveur lors de l\'inscription'
});
}
});
app.post('/api/send_reminder', async (req, res) => {
const { office, customer } = req.body;
try {
const to = customer.contact.email;
const templateVariables = {
office_name: office.name,
last_name: customer.contact.last_name || '',
first_name: customer.contact.first_name || '',
link: `${process.env.APP_HOST}`
};
await EmailService.sendTransactionalEmail(
to,
ETemplates.DOCUMENT_REMINDER,
'Vous avez des documents à déposer pour votre dossier.',
templateVariables
);
res.json({
success: true,
message: 'Email envoyé avec succès'
});
} catch (error) {
console.error(error);
return;
}
});
// Automatic retry system
setInterval(() => {
EmailService.retryFailedEmails();
}, 60000); // Check every minute
//------------------------------------ End of Email Section ------------------------------------
//------------------------------------ Stripe Section ------------------------------------------
const configStripe = {
STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY,
STRIPE_WEBHOOK_SECRET: process.env.STRIPE_WEBHOOK_SECRET,
APP_HOST: process.env.APP_HOST || 'http://localhost:3000',
};
// Stripe service
class StripeService {
constructor() {
this.client = new Stripe(configStripe.STRIPE_SECRET_KEY);
this.prices = {
STANDARD: {
monthly: process.env.STRIPE_STANDARD_SUBSCRIPTION_PRICE_ID,
yearly: process.env.STRIPE_STANDARD_ANNUAL_SUBSCRIPTION_PRICE_ID
},
UNLIMITED: {
monthly: process.env.STRIPE_UNLIMITED_SUBSCRIPTION_PRICE_ID,
yearly: process.env.STRIPE_UNLIMITED_ANNUAL_SUBSCRIPTION_PRICE_ID
}
};
}
// Only for test
async createTestSubscription() {
try {
const customer = await this.client.customers.create({
email: 'test@example.com',
description: 'Client test',
source: 'tok_visa'
});
const priceId = this.prices.STANDARD.monthly;
const price = await this.client.prices.retrieve(priceId);
const subscription = await this.client.subscriptions.create({
customer: customer.id,
items: [{ price: price.id }],
payment_behavior: 'default_incomplete',
expand: ['latest_invoice.payment_intent']
});
return {
subscriptionId: subscription.id,
customerId: customer.id,
status: subscription.status,
priceId: price.id
};
} catch (error) {
throw error;
}
}
async createCheckoutSession(subscription, frequency) {
const priceId = this.getPriceId(subscription.type, frequency);
return await this.client.checkout.sessions.create({
mode: 'subscription',
payment_method_types: ['card', 'sepa_debit'],
billing_address_collection: 'auto',
line_items: [{
price: priceId,
quantity: subscription.type === 'STANDARD' ? subscription.seats : 1,
}],
success_url: `${configStripe.APP_HOST}/subscription/success`, // Success page (frontend)
cancel_url: `${configStripe.APP_HOST}/subscription/error`, // Error page (frontend)
metadata: {
subscription: JSON.stringify(subscription),
},
allow_promotion_codes: true,
automatic_tax: { enabled: true }
});
}
getPriceId(type, frequency) {
return this.prices[type][frequency];
}
async getSubscription(subscriptionId) {
return await this.client.subscriptions.retrieve(subscriptionId);
}
async createPortalSession(subscriptionId) {
const subscription = await this.getSubscription(subscriptionId);
return await this.client.billingPortal.sessions.create({
customer: subscription.customer,
return_url: `${configStripe.APP_HOST}/subscription/manage`
});
}
}
const stripeService = new StripeService();
// Validation middleware
const validateSubscription = (req, res, next) => {
const { type, seats, frequency } = req.body;
if (!type || !['STANDARD', 'UNLIMITED'].includes(type)) {
return res.status(400).json({
success: false,
message: 'Type d\'abonnement invalide'
});
}
if (type === 'STANDARD' && (!seats || seats < 1)) {
return res.status(400).json({
success: false,
message: 'Nombre de sièges invalide'
});
}
if (!frequency || !['monthly', 'yearly'].includes(frequency)) {
return res.status(400).json({
success: false,
message: 'Fréquence invalide'
});
}
next();
};
// Routes
// Only for test
app.post('/api/test/create-subscription', async (req, res) => {
try {
const result = await stripeService.createTestSubscription();
res.json({
success: true,
data: result
});
} catch (error) {
res.status(500).json({
success: false,
message: 'Erreur lors de la création de l\'abonnement de test',
error: {
message: error.message,
type: error.type,
code: error.code
}
});
}
});
app.post('/api/subscriptions/checkout', validateSubscription, async (req, res) => {
try {
const session = await stripeService.createCheckoutSession(req.body, req.body.frequency);
res.json({ success: true, sessionId: session.id });
} catch (error) {
console.error('Erreur création checkout:', error);
res.status(500).json({
success: false,
message: 'Erreur lors de la création de la session de paiement'
});
}
});
app.get('/api/subscriptions/:id', async (req, res) => {
try {
const subscription = await stripeService.getSubscription(req.params.id);
res.json({ success: true, subscription });
} catch (error) {
res.status(500).json({
success: false,
message: 'Erreur lors de la récupération de l\'abonnement'
});
}
});
app.post('/api/subscriptions/:id/portal', async (req, res) => {
try {
const session = await stripeService.createPortalSession(req.params.id);
res.json({ success: true, url: session.url });
} catch (error) {
res.status(500).json({
success: false,
message: 'Erreur lors de la création de la session du portail'
});
}
});
// Webhook Stripe
app.post('/api/webhooks/stripe', express.raw({ type: 'application/json' }), async (req, res) => {
const sig = req.headers['stripe-signature'];
let event;
try {
event = Stripe.webhooks.constructEvent(req.body, sig, configStripe.STRIPE_WEBHOOK_SECRET);
} catch (err) {
return res.status(400).send(`Webhook Error: ${err.message}`);
}
try {
switch (event.type) {
case 'checkout.session.completed':
const session = event.data.object;
if (session.status === 'complete') {
const subscription = JSON.parse(session.metadata.subscription);
// Stock subscription (create process)
console.log('Nouvel abonnement:', subscription);
}
break;
case 'invoice.payment_succeeded':
const invoice = event.data.object;
if (['subscription_update', 'subscription_cycle'].includes(invoice.billing_reason)) {
const subscription = await stripeService.getSubscription(invoice.subscription);
// Update subscription (update process)
console.log('Mise à jour abonnement:', subscription);
}
break;
case 'customer.subscription.deleted':
const deletedSubscription = event.data.object;
// Delete subscription (update process to delete)
console.log('Suppression abonnement:', deletedSubscription.id);
break;
}
res.json({ received: true });
} catch (error) {
console.error('Erreur webhook:', error);
res.status(500).json({
success: false,
message: 'Erreur lors du traitement du webhook'
});
}
});
//------------------------------------ End of Stripe Section -----------------------------------
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});

142
src/server.ts Normal file
View File

@ -0,0 +1,142 @@
import express from 'express';
import cors from 'cors';
import { config } from './config';
import { routes } from './routes';
import { SignerService } from './services/signer';
import { SessionManager } from './utils/session-manager';
import { EmailService } from './services/email';
import { authTokens } from './utils/auth-tokens';
import { errorHandler, requestIdMiddleware } from './middleware/error-handler';
import { Logger } from './utils/logger';
// Initialisation de l'application Express
const app = express();
const PORT = config.port;
// Request ID middleware (must be first)
app.use(requestIdMiddleware);
// Configuration CORS
app.use(cors(config.cors));
app.use(express.json());
// Request logging middleware
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
Logger.logRequest(req, res, duration);
});
next();
});
// Use routes from the reorganized structure
app.use(routes);
// Error handling middleware (must be last)
app.use(errorHandler);
// Initialize signer service with enhanced reconnection logic
(async () => {
try {
const result = await SignerService.initialize();
if (result.success) {
Logger.info('Signer service initialized');
} else {
Logger.error('Failed to initialize signer service', {
error: result.error?.message || 'Unknown error'
});
}
} catch (error) {
Logger.error('Critical error during signer initialization', {
error: error instanceof Error ? error.message : 'Unknown error'
});
}
})();
// Set up signer connection monitoring
SignerService.onConnectionChange((connected) => {
if (connected) {
Logger.info('Signer connected');
} else {
Logger.warn('Signer disconnected');
}
});
// Cleanup expired sessions every 5 minutes
setInterval(() => {
SessionManager.cleanupExpiredSessions();
}, 5 * 60 * 1000);
// Cleanup expired auth tokens every hour
setInterval(() => {
const now = Date.now();
const initialLength = authTokens.length;
// Remove expired tokens
for (let i = authTokens.length - 1; i >= 0; i--) {
if (now > authTokens[i].expiresAt) {
authTokens.splice(i, 1);
}
}
const cleanedCount = initialLength - authTokens.length;
if (cleanedCount > 0) {
console.log(`Cleaned up ${cleanedCount} expired auth tokens`);
}
}, 60 * 60 * 1000); // Every hour
// Automatic retry system for failed emails
setInterval(() => {
EmailService.retryFailedEmails();
}, 60000); // Check every minute
// Initialisation et démarrage du serveur
async function startServer(): Promise<void> {
try {
// Démarrage du serveur
app.listen(PORT, () => {
console.log(`Server started on port ${PORT}`);
});
} catch (error) {
console.error('Error starting the server:', error);
process.exit(1);
}
}
// Graceful shutdown handling
process.on('SIGTERM', () => {
Logger.info('SIGTERM received, shutting down gracefully');
SignerService.cleanup();
process.exit(0);
});
process.on('SIGINT', () => {
Logger.info('SIGINT received, shutting down gracefully');
SignerService.cleanup();
process.exit(0);
});
// Handle uncaught exceptions
process.on('uncaughtException', (error) => {
Logger.error('Uncaught exception', {
error: error.message,
stack: error.stack
});
SignerService.cleanup();
process.exit(1);
});
// Handle unhandled promise rejections
process.on('unhandledRejection', (reason, promise) => {
Logger.error('Unhandled promise rejection', {
reason: reason instanceof Error ? reason.message : String(reason),
stack: reason instanceof Error ? reason.stack : undefined
});
});
// Démarrage de l'application
startServer();

Some files were not shown because too many files have changed in this diff Show More