Compare commits
48 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
a1670cfc35 | ||
![]() |
cdf91d453f | ||
![]() |
26c33f00f2 | ||
ba2c36c014 | |||
7c5c4ab334 | |||
dd45e99a80 | |||
fd093aec65 | |||
125e9ac923 | |||
ea83bc759a | |||
47f3da354a | |||
7bb24d42ad | |||
5e3fb8f07f | |||
bde308037b | |||
11a77054b0 | |||
51ff0a00c9 | |||
235be939af | |||
f084e8a6fc | |||
61a6b166f7 | |||
f79b3f8f26 | |||
0069d53946 | |||
8283185022 | |||
efe9dbde1b | |||
02820b2862 | |||
e6fd9192b1 | |||
7d102772d9 | |||
8ed2ffb16c | |||
![]() |
7d47eec1f2 | ||
![]() |
b04679ba34 | ||
![]() |
cfa9514be9 | ||
![]() |
221ac17831 | ||
![]() |
6c6f49417e | ||
![]() |
fbdbc0a699 | ||
![]() |
180c06dc3b | ||
![]() |
164fdfb4f0 | ||
![]() |
de7f1a306d | ||
![]() |
02f1cbb885 | ||
![]() |
e8cb97b6c6 | ||
![]() |
c07591a97a | ||
![]() |
888ca48712 | ||
![]() |
06a6b5c7aa | ||
![]() |
8462b99586 | ||
![]() |
1465c9dfd2 | ||
![]() |
7e679ae33f | ||
![]() |
7b9d58545b | ||
![]() |
9bfcb47293 | ||
4f446cad5d | |||
ed7edef021 | |||
bbb5b5ae36 |
17
.env.exemple
17
.env.exemple
@ -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=
|
||||
|
@ -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
143
.gitignore
vendored
@ -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/
|
43
Dockerfile
43
Dockerfile
@ -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"]
|
10
README.md
10
README.md
@ -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)
|
||||
|
BIN
backups/nginx/20250924-223552.tar.gz
Normal file
BIN
backups/nginx/20250924-223552.tar.gz
Normal file
Binary file not shown.
1
backups/nginx/20250925-091648/TIMESTAMP
Normal file
1
backups/nginx/20250925-091648/TIMESTAMP
Normal file
@ -0,0 +1 @@
|
||||
20250925-091648
|
14
backups/nginx/20250925-091648/http_flows.md
Normal file
14
backups/nginx/20250925-091648/http_flows.md
Normal 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);
|
32
backups/nginx/20250925-091648/open_ports.txt
Normal file
32
backups/nginx/20250925-091648/open_ports.txt
Normal 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 <->
|
175
backups/nginx/20250925-091648/project/dev3.4nkweb.com.conf
Normal file
175
backups/nginx/20250925-091648/project/dev3.4nkweb.com.conf
Normal 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;
|
||||
|
||||
|
||||
}
|
176
backups/nginx/20250925-091648/project/dev3.4nkweb.com.fixed.conf
Normal file
176
backups/nginx/20250925-091648/project/dev3.4nkweb.com.fixed.conf
Normal 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;
|
||||
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
8
confs/nginx/_archive_20250924-184550/local.4nkweb.com-3000
Executable file
8
confs/nginx/_archive_20250924-184550/local.4nkweb.com-3000
Executable 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;
|
||||
}
|
||||
|
11
confs/nginx/_archive_20250924-184550/local.4nkweb.com-3000-ssl
Executable file
11
confs/nginx/_archive_20250924-184550/local.4nkweb.com-3000-ssl
Executable 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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
20
confs/nginx/_archive_20250924-184550/local.4nkweb.com-3000-ssl.bak-20250923
Executable file
20
confs/nginx/_archive_20250924-184550/local.4nkweb.com-3000-ssl.bak-20250923
Executable 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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
7
confs/nginx/_archive_20250924-184550/local.4nkweb.com-3000.bak
Executable file
7
confs/nginx/_archive_20250924-184550/local.4nkweb.com-3000.bak
Executable 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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
13
confs/nginx/_archive_20250924-184550/local.4nkweb.com-3000.bak-20250923
Executable file
13
confs/nginx/_archive_20250924-184550/local.4nkweb.com-3000.bak-20250923
Executable 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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
7
confs/nginx/_archive_20250924-184550/local.lecoffreio.4nkweb.com
Executable file
7
confs/nginx/_archive_20250924-184550/local.lecoffreio.4nkweb.com
Executable 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;
|
||||
}
|
7
confs/nginx/_archive_20250924-184550/local.lecoffreio.4nkweb.com.bak
Executable file
7
confs/nginx/_archive_20250924-184550/local.lecoffreio.4nkweb.com.bak
Executable 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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
44
confs/nginx/_effective_20250924-190412/conf.d/default.conf
Executable file
44
confs/nginx/_effective_20250924-190412/conf.d/default.conf
Executable 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;
|
||||
#}
|
||||
}
|
||||
|
34
confs/nginx/_effective_20250924-190412/nginx.conf
Executable file
34
confs/nginx/_effective_20250924-190412/nginx.conf
Executable 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/*;
|
||||
}
|
@ -0,0 +1 @@
|
||||
/etc/nginx/sites-available/demo.4nkweb.com
|
@ -0,0 +1 @@
|
||||
/etc/nginx/sites-available/dev1.4nkweb.com
|
@ -0,0 +1 @@
|
||||
/etc/nginx/sites-available/dev2.4nkweb.com
|
162
confs/nginx/_effective_20250924-190412/sites-enabled/dev3.4nkweb.com
Executable file
162
confs/nginx/_effective_20250924-190412/sites-enabled/dev3.4nkweb.com
Executable 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;
|
||||
|
||||
|
||||
}
|
@ -0,0 +1 @@
|
||||
/etc/nginx/sites-available/lecoffreio-dev2.4nkweb.com
|
@ -0,0 +1 @@
|
||||
/etc/nginx/sites-available/lecoffreio.4nkweb.com
|
@ -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;
|
||||
}
|
@ -0,0 +1 @@
|
||||
/etc/nginx/sites-available/relay235.4nkweb.com
|
@ -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;
|
||||
}
|
||||
}
|
215
confs/nginx/dev3.4nkweb.com.conf
Normal file
215
confs/nginx/dev3.4nkweb.com.conf
Normal 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;
|
||||
|
||||
|
||||
}
|
176
confs/nginx/dev3.4nkweb.com.fixed.conf
Normal file
176
confs/nginx/dev3.4nkweb.com.fixed.conf
Normal 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;
|
||||
|
||||
|
||||
}
|
45
confs/nginx/dev3.4nkweb.com.fixed.conf.b64
Normal file
45
confs/nginx/dev3.4nkweb.com.fixed.conf.b64
Normal 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,
|
7
confs/nginx/local.lecoffreio.4nkweb.com.conf
Normal file
7
confs/nginx/local.lecoffreio.4nkweb.com.conf
Normal 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
337
docs/data_account_test.md
Normal 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
165
docs/doc_api/.cursorrules
Normal 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 d’environnement 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 d’environnement.
|
||||
- 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 l’existence d’un 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 l’existant 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 d’expé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 d’adresse.
|
||||
* 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 d’abstraction pour les services de données.
|
||||
* Aller systématiquement au bout d’une 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 l’utilisateur root, privilégier un utilisateur dédié.
|
||||
- Limiter les capacités des conteneurs (option `--cap-drop`) pour réduire la surface d’attaque.
|
||||
- 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 l’historique (`git log`, `git filter-repo`) pour s’assurer qu’aucune information sensible n’a été poussée.
|
||||
- Signer les commits avec GPG pour garantir l’authenticité.
|
||||
- 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 l’utilisation 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.
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
docs/doc_api/API Annuaire - V2 - Documentation Utilisateur.pdf
Normal file
BIN
docs/doc_api/API Annuaire - V2 - Documentation Utilisateur.pdf
Normal file
Binary file not shown.
BIN
docs/doc_api/ID.NOT - Document d'intégration OpenIDConnect.pdf
Normal file
BIN
docs/doc_api/ID.NOT - Document d'intégration OpenIDConnect.pdf
Normal file
Binary file not shown.
BIN
docs/doc_api/ID.NOT - Présentation et guide d'intégration.pdf
Normal file
BIN
docs/doc_api/ID.NOT - Présentation et guide d'intégration.pdf
Normal file
Binary file not shown.
BIN
docs/doc_api/Portail des raccordements - Guide de démarrage.pdf
Normal file
BIN
docs/doc_api/Portail des raccordements - Guide de démarrage.pdf
Normal file
Binary file not shown.
37
docs/hmr-idnot-dev3-state-2025-09-24.md
Normal file
37
docs/hmr-idnot-dev3-state-2025-09-24.md
Normal 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 d’auth **idnot** en s’appuyant 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 l’utilisateur 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 jusqu’au callback.
|
||||
- CORS permet au front HMR d’appeler le back sans friction.
|
||||
|
||||
## Rôle de dev3
|
||||
- Point d’entré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.
|
||||
|
43
docs/idnot-callback-fix-2025-09-24.md
Normal file
43
docs/idnot-callback-fix-2025-09-24.md
Normal 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
|
||||
|
39
docs/idnot-callback-issue-2025-09-24.md
Normal file
39
docs/idnot-callback-issue-2025-09-24.md
Normal 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
119
docs/logs-backend.md
Normal 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, s’assurer qu’aucun 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 d’erreur proxy, on peut voir `No context` (non-JSON).
|
||||
- **Erreur applicative agrégée**:
|
||||
- `IdNot authentication failed` ou un objet d’erreur avec `code: 'IDNOT_SERVICE_ERROR'`.
|
||||
|
||||
### Interprétation rapide
|
||||
- **Si `No context` apparaît**:
|
||||
- Vérifier l’envoi 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 d’environnement utiles (rappel)
|
||||
- **Bases d’URL**:
|
||||
- `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 l’investigation 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 d’un identifiant de requête: `X-Request-Id: $request_id` vers l’upstream.
|
||||
- Ajout d’un 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 s’il est présent, sinon corrélez via timestamp, méthode et chemin).
|
||||
|
||||
### À surveiller côté Nginx
|
||||
- Codes `4xx/5xx` dans l’access log sur `/idnot/callback`, `/api/...`, `/ws/`.
|
||||
- Délai ou timeouts (`upstream timed out`) dans l’error log.
|
||||
- Cohérence du schéma (`X-Forwarded-Proto`) et du host (`Host`) vers l’upstream.
|
||||
|
||||
## 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`.
|
46
docs/nginx-audit-2025-09-24.md
Normal file
46
docs/nginx-audit-2025-09-24.md
Normal 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 n’a effectué aucune modification. Toute action doit être validée avant exécution.
|
36
docs/ports-2025-09-24.md
Normal file
36
docs/ports-2025-09-24.md
Normal 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
39
global.d.ts
vendored
Normal 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
22
logs/backend.out
Normal 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)
|
2134
logs/backend_20250925_085618.out
Normal file
2134
logs/backend_20250925_085618.out
Normal file
File diff suppressed because it is too large
Load Diff
1
logs/build_20250925_085623.out
Normal file
1
logs/build_20250925_085623.out
Normal file
@ -0,0 +1 @@
|
||||
bash: ligne 1: npm : commande introuvable
|
1715
logs/restart.out
Normal file
1715
logs/restart.out
Normal file
File diff suppressed because it is too large
Load Diff
1
logs/server.pid
Normal file
1
logs/server.pid
Normal file
@ -0,0 +1 @@
|
||||
4104714
|
25
package.json
25
package.json
@ -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
159
quick-test-rattachements.js
Executable 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
26
scripts/backup_nginx_confs.sh
Executable 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
43
scripts/launch_check.sh
Executable 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."
|
22
scripts/nginx_cleanup_local4nk.sh
Executable file
22
scripts/nginx_cleanup_local4nk.sh
Executable 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"
|
21
scripts/nginx_scrub_local_comment.sh
Executable file
21
scripts/nginx_scrub_local_comment.sh
Executable 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
8
src/config/email.ts
Normal 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
62
src/config/index.ts
Normal 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
9
src/config/signer.ts
Normal 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
12
src/config/sms.ts
Normal 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
5
src/config/stripe.ts
Normal 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',
|
||||
};
|
105
src/controllers/email.controller.ts
Normal file
105
src/controllers/email.controller.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
7
src/controllers/health.controller.ts
Normal file
7
src/controllers/health.controller.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { Request, Response } from 'express';
|
||||
|
||||
export class HealthController {
|
||||
static getHealth(req: Request, res: Response) {
|
||||
res.json({ message: 'OK' });
|
||||
}
|
||||
}
|
279
src/controllers/idnot.controller.ts
Normal file
279
src/controllers/idnot.controller.ts
Normal 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
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
550
src/controllers/process.controller.ts
Normal file
550
src/controllers/process.controller.ts
Normal 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()
|
||||
});
|
||||
});
|
||||
}
|
182
src/controllers/sms-improved.controller.ts
Normal file
182
src/controllers/sms-improved.controller.ts
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
126
src/controllers/sms.controller.ts
Normal file
126
src/controllers/sms.controller.ts
Normal 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'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
113
src/controllers/stripe.controller.ts
Normal file
113
src/controllers/stripe.controller.ts
Normal 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'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
61
src/handlers/idnot-callback.handlers.ts
Normal file
61
src/handlers/idnot-callback.handlers.ts
Normal 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);
|
||||
});
|
||||
}
|
135
src/handlers/idnot.handlers.ts
Normal file
135
src/handlers/idnot.handlers.ts
Normal 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);
|
||||
});
|
||||
}
|
24
src/handlers/state.handlers.ts
Normal file
24
src/handlers/state.handlers.ts
Normal 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
46
src/middleware/auth.ts
Normal 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();
|
||||
};
|
147
src/middleware/error-handler.ts
Normal file
147
src/middleware/error-handler.ts
Normal 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
25
src/middleware/session.ts
Normal 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();
|
||||
};
|
60
src/middleware/validation.ts
Normal file
60
src/middleware/validation.ts
Normal 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();
|
||||
};
|
11
src/routes/email.routes.ts
Normal file
11
src/routes/email.routes.ts
Normal 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 };
|
8
src/routes/health.routes.ts
Normal file
8
src/routes/health.routes.ts
Normal 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 };
|
15
src/routes/idnot.routes.ts
Normal file
15
src/routes/idnot.routes.ts
Normal 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
26
src/routes/index.ts
Normal 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
23
src/routes/index.ts.bak
Normal 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 };
|
||||
|
||||
|
||||
|
||||
|
20
src/routes/process.routes.ts
Normal file
20
src/routes/process.routes.ts
Normal 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
10
src/routes/sms.routes.ts
Normal 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 };
|
18
src/routes/stripe.routes.ts
Normal file
18
src/routes/stripe.routes.ts
Normal 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 };
|
940
src/server.js
940
src/server.js
@ -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
142
src/server.ts
Normal 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
Loading…
x
Reference in New Issue
Block a user