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
|
# Configuration SMS Factor
|
||||||
SMS_FACTOR_TOKEN=
|
SMS_FACTOR_TOKEN=
|
||||||
|
|
||||||
#Configuration Mailchimp
|
# Configuration Mailchimp
|
||||||
MAILCHIMP_API_KEY=
|
MAILCHIMP_API_KEY=
|
||||||
MAILCHIMP_KEY=
|
MAILCHIMP_KEY=
|
||||||
MAILCHIMP_LIST_ID=
|
MAILCHIMP_LIST_ID=
|
||||||
|
|
||||||
|
# Configuration Stripe
|
||||||
#Configuration Stripe
|
|
||||||
STRIPE_SECRET_KEY=
|
STRIPE_SECRET_KEY=
|
||||||
STRIPE_WEBHOOK_SECRET=
|
STRIPE_WEBHOOK_SECRET=
|
||||||
STRIPE_STANDARD_SUBSCRIPTION_PRICE_ID=
|
STRIPE_STANDARD_SUBSCRIPTION_PRICE_ID=
|
||||||
@ -21,10 +20,20 @@ STRIPE_STANDARD_ANNUAL_SUBSCRIPTION_PRICE_ID=
|
|||||||
STRIPE_UNLIMITED_SUBSCRIPTION_PRICE_ID=
|
STRIPE_UNLIMITED_SUBSCRIPTION_PRICE_ID=
|
||||||
STRIPE_UNLIMITED_ANNUAL_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
|
SUCCES= 4242 4242 4242 4242 #Paiement réussi
|
||||||
DECLINED= 4000 0025 0000 3155 #Paiement refusé
|
DECLINED= 4000 0025 0000 3155 #Paiement refusé
|
||||||
|
|
||||||
# Configuration serveur
|
# Configuration serveur
|
||||||
APP_HOST=
|
APP_HOST=
|
||||||
PORT=
|
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:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ main ]
|
branches: [ dev ]
|
||||||
pull_request:
|
|
||||||
types: [closed]
|
|
||||||
branches: [ main ]
|
|
||||||
|
|
||||||
env:
|
env:
|
||||||
REGISTRY: git.4nkweb.com
|
REGISTRY: git.4nkweb.com
|
||||||
@ -14,11 +11,15 @@ env:
|
|||||||
jobs:
|
jobs:
|
||||||
build-and-push:
|
build-and-push:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.event.pull_request.merged == true || github.event_name == 'push'
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
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
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
@ -34,6 +35,8 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: true
|
push: true
|
||||||
|
ssh: default
|
||||||
tags: |
|
tags: |
|
||||||
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
||||||
|
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:dev
|
||||||
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ gitea.sha }}
|
${{ 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
|
**/*wallet*
|
||||||
.next
|
**/*keys*
|
||||||
|
|
||||||
# dependencies
|
**/*node_modules*
|
||||||
/node_modules
|
**/*cursor*
|
||||||
package-lock.json
|
**/*pid*
|
||||||
|
**/*next*
|
||||||
|
|
||||||
# envs
|
# Dossiers de logs communs
|
||||||
.env
|
log/
|
||||||
|
logs/
|
||||||
# misc
|
**/log/
|
||||||
.DS_Store
|
**/logs/
|
||||||
*.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
|
|
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
|
WORKDIR /app
|
||||||
|
|
||||||
# Installation des dépendances
|
# Outils nécessaires pour cloner le dépôt privé
|
||||||
COPY package*.json ./
|
RUN apk add --no-cache git openssh-client
|
||||||
RUN npm install --production
|
|
||||||
|
|
||||||
# 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
|
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
|
# Création d'un utilisateur non-root
|
||||||
RUN adduser -D appuser --uid 10000 && \
|
RUN adduser -D appuser --uid 10000 && \
|
||||||
chown -R appuser /app
|
chown -R appuser /app
|
||||||
USER appuser
|
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
|
EXPOSE 8080
|
||||||
CMD ["npm", "start"]
|
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
|
- http://localhost:3000/api/ping
|
||||||
|
|
||||||
Cette route renvoie un objet JSON avec le message "Hello World".
|
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",
|
"name": "lecoffre-back-mini",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "Mini serveur avec une route /api/ping",
|
"description": "Mini serveur avec une route /api/ping",
|
||||||
"main": "src/server.js",
|
"main": "dist/server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node src/server.js",
|
"build": "tsc",
|
||||||
"dev": "nodemon src/server.js"
|
"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": {
|
"dependencies": {
|
||||||
"@mailchimp/mailchimp_transactional": "^1.0.59",
|
"@mailchimp/mailchimp_transactional": "^1.0.59",
|
||||||
@ -14,10 +21,20 @@
|
|||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"node-fetch": "^2.6.7",
|
"node-fetch": "^2.6.7",
|
||||||
"ovh": "^2.0.3",
|
"ovh": "^2.0.3",
|
||||||
|
"pg": "^8.11.3",
|
||||||
|
"sdk-signer-client": "file:../sdk-signer-client",
|
||||||
"stripe": "^18.3.0",
|
"stripe": "^18.3.0",
|
||||||
"uuid": "^11.1.0"
|
"uuid": "^11.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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