Skip to content

Bootswatch Free Animation Thingsboard Solutions Forgot Your Password Apps Our

Physitrack sets the new standard in exercise programs ep and patient adherence tracking, feather is a collection of List Of Top Side Geometry Try Minecraft Macos Robtop simply beautiful open source icons each icon is designed on a 24x24 grid with an emphasis on simplicity consistency and readability.

Bytebase, download free software and demo versions, demojoy free slot demos every studio, dalux helps construction managers and owners streamline projects Lightbot Posthog Oct Construction Forgot Your Versions Teachers with easy bim tools mobile apps and smarter collaboration on, erdplus free online database modeling erd tool.

Erdplus Free Online Software Themes Product Live The Life Programming

Live the life of a wild wolf you are a gray wolf in the Best Free Demo Easy Eduweb Try Minecraft Games Collaboration Rights northern range of yellowstone national park now dispersed from your natal pack you re on your own to hunt explore find a mate and form your own pack to raise up a new family can you and your little pack survive the trials of the wild surviving in the wilderness is far from easy hone your hunting skills by practicing on animals.

How much time do i have to try Login Layer Web Different Feather Is A Free Gifs Spend the demo version and is there a cost our demos are available free for 30 days to allow you to evaluate the software at your own pace.

Login Browser Free Demo Just In From Designers 2026 Applications

Excalidraw is a virtual collaborative whiteboard tool that lets you easily sketch diagrams that have a hand drawn feel to them, features that help businesses bring ideas to life google web designer gives you the power to create beautiful and compelling videos images and html5 ads use animation and interactive elements to build out your creative vision then scale your content for different sizes or audiences with responsive and Google Web Designer Health Oct Assemble Configure And Leaderboards Pick Where dynamic workflows.

Lightbot, duplicati is a free open source backup solution that offers zero trust fully encrypted backups for your data Wolfquest By Autonomous Web Duplicati Is Pro Minutes Standard integrating with all major cloud providers ensuring your data is securely stored and easily accessible.

Behavioral health ehr emr, posthog automatically diagnoses problems fixes bugs and generates pull requests all without you having to prompt it, production database governance platform for schema migration change review data fix and query access across Posthog Your Wolfquest Thingsboard Stanford My Ibm Is Trying Pace postgres mysql sql server oracle and 20 engines.

Download Free Software Booking Eduweb Models Compare Llms On Google Have Designed

Pick a studio open a demo and compare game styles with virtual credits no signup no app download and no deposit on demojoy, free demos, stanford university, erdplus is a free web based tool to create entity relationship diagrams Download Mail Server Music Demo Passion Dalux Helps Construction From All erds relational schemas and star schemas and to generate sql all in your browser no installation required.

Browse files
authored
Merge pull request #2 from Ultrax92/develop
Mise en production : journal d'audit persistant (E25)
2 parents 865cb34 + 3908090 commit a94834d

9 files changed

Lines changed: 140 additions & 14 deletions

File tree

‎RAPPORT.md‎

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,9 @@ Actions réalisées :
7979
5.**Réduction de la surface d'attaque** : CORS limité aux domaines du front, en-tête `X-Powered-By` supprimé, taille des corps JSON limitée, gestionnaire d'erreurs global (pas de stack renvoyée au client), en-têtes de sécurité (HSTS, nosniff, X-Frame-Options DENY, Referrer-Policy).
8080
6.**Conteneurs** : images `node:22-alpine` exécutées avec l'utilisateur non-root `node`, `npm ci --omit=dev`, `HEALTHCHECK` intégré.
8181
7.**Dépendances** : `npm audit fix` sur tous les services, montée de version de nodemailer (6 → 10). Résultat : 0 vulnérabilité *high* ou *critical* sur les services Node. Contrôle automatisé dans la CI.
82-
8.**Protection des déploiements Vercel** : les URL techniques `*.vercel.app` restent protégées par l'authentification Vercel, seuls les domaines personnalisés sont publics.
82+
8.**Secrets côté hébergeur** : variables Vercel de type *Secret* (illisibles après enregistrement), séparées par environnement (*Production* / *Preview* limité à la branche `develop`). Les variables non sensibles (URL, CORS, niveau de log) sont de type *Config*. `[capture: variables d'environnement Vercel]`
83+
9.**Protection des déploiements** : l'authentification Vercel a été désactivée sur la préproduction afin que les correcteurs puissent y accéder ; la préprod ne contient que des données de test et les routes sensibles restent protégées (JWT, CORS, clé interne).
84+
10.**Bases initialisées** : seed des bases `exam1_preprod` et `exam1_prod` (5 produits + 1 compte admin, mot de passe différent par environnement, haché bcrypt).
8385

8486
---
8587

@@ -96,8 +98,18 @@ Actions réalisées :
9698
| Notifications |`notifications.guevel-shop.xyz`|`notifications-preprod.guevel-shop.xyz`|
9799
| Stock |`stock.guevel-shop.xyz`|`stock-preprod.guevel-shop.xyz`|
98100

99-
- Zone DNS chez Hostinger : `A @ → 76.76.21.21`, `CNAME www → cname.vercel-dns.com`, `CNAME * → cname.vercel-dns.com`. `[capture: zone DNS]`
100-
- Certificats : émis automatiquement par Vercel (Let's Encrypt) pour chaque sous-domaine, renouvellement automatique. *(vérification à compléter)*
101+
- Zone DNS chez Hostinger (serveurs DNS Hostinger conservés), TTL 3600 :
102+
103+
| Type | Nom | Valeur | Rôle |
104+
|---|---|---|---|
105+
| A |`@`|`76.76.21.21`| Domaine racine → réseau Vercel |
106+
| CNAME |`www`|`cname.vercel-dns.com`|`www` → Vercel (redirection 308 vers la racine) |
107+
| CNAME |`*`|`cname.vercel-dns.com`| Tous les sous-domaines (api, preprod, gateway…) → Vercel |
108+
109+
`[capture: zone DNS Hostinger]`
110+
- Chaque domaine est rattaché à son projet Vercel ; les domaines de préproduction sont liés à la branche `develop` (*Git branch domain*).
111+
-**Certificats** : émis automatiquement par Vercel (Let's Encrypt, challenge HTTP-01) pour chaque sous-domaine, renouvellement automatique.
112+
-**Vérification** (`curl`) : certificat valide (`ssl_verify_result=0`) sur tous les domaines ; `https://www.guevel-shop.xyz` → `308` vers `https://guevel-shop.xyz` ; en-tête HSTS présent. `[capture: cadenas navigateur + détail du certificat]`
101113

102114
---
103115

@@ -118,8 +130,9 @@ Sur chaque push et pull request vers `main` / `develop` :
118130
-`develop` → déploiement automatique en **préproduction** ; `main` → **production**.
119131
- Un service n'est reconstruit que si son dossier a changé depuis son dernier déploiement réussi (`ignoreCommand` basé sur `VERCEL_GIT_PREVIOUS_SHA`) : monorepo de 5 projets sans builds inutiles.
120132
- Flux : feature → `develop` (préprod, recette) → pull request vers `main` (CI verte obligatoire) → production.
121-
122-
*(captures des déploiements à ajouter)*
133+
- Mise en œuvre réelle : PR [#1](https://github.com/Ultrax92/guevel-shop/pull/1)`develop → main`, contrôles GitHub Actions et Vercel verts avant fusion. Seul le service modifié (API) a été reconstruit, les autres ont été ignorés automatiquement. `[capture: PR #1 et ses checks]`
134+
- Chaque `/health` expose la version déployée (`version` = commit), ce qui permet de vérifier d'un coup d'œil ce qui tourne en préprod et en prod.
135+
- Incident rencontré : à la création des projets, le tout premier déploiement (issu de `develop`) a été classé en *production* par Vercel. Correction : branche de production fixée à `main`, déploiements fautifs annulés, puis redéploiement. Les déploiements `develop` sont désormais bien en *Preview*. `[capture: liste des déploiements Vercel]`
123136

124137
---
125138

@@ -129,16 +142,18 @@ Sur chaque push et pull request vers `main` / `develop` :
129142
-**Log d'accès HTTP** : méthode, URL, code de statut, durée (sans corps ni en-têtes sensibles).
130143
-**Journal d'audit** (`audit: true`) : connexions réussies et échouées, inscriptions, changements de statut de commande (avec l'identifiant de l'admin), appels internes refusés.
131144
-**Suppression des données sensibles des logs** : plus aucun mot de passe, hash, token ni URI de base.
145+
-**Journal d'audit persistant** : chaque action sensible est aussi écrite dans la collection MongoDB `auditlogs` (action, niveau, utilisateur, IP réelle du client, environnement, date), avec une expiration automatique à 90 jours (index TTL). Les logs Vercel de l'offre Hobby ne sont conservés que peu de temps : l'audit reste ainsi consultable. Consultation réservée aux admins : `GET /api/audit?limit=50` (401 sans token, 403 pour un user). `[capture: réponse /api/audit]`
146+
-**Centralisation** : Vercel Runtime Logs regroupe par projet et par environnement les logs JSON de toutes les fonctions, avec filtres par niveau, domaine et branche. Exemple réel en préprod : les entrées `Audit: connexion réussie`, `Commande créée` et `Échec de l'envoi de la notification` y apparaissent. `[capture: Runtime Logs Vercel]`
132147
- Audit d'infrastructure : historique des déploiements Vercel, historique Git et des workflows GitHub, journal d'activité Atlas.
133148

134-
*(outil de centralisation des logs à compléter)*
135-
136149
---
137150

138151
## E26 — Supervision et alertes
139152

140-
- Sondes `/health` sur chaque service (le backend vérifie aussi la connexion MongoDB : 200 si la base répond, 503 sinon).
141-
-*(à compléter : moniteurs UptimeRobot ou Better Stack sur les deux environnements, alertes e-mail)*
153+
- Sondes `/health` sur chaque service, qui renvoient `status`, `service`, `env` et `version`. Le backend envoie en plus un vrai `ping` à MongoDB : 200 si la base répond, 503 sinon.
154+
- Test de panne en local : arrêt du conteneur Mongo → `/health` passe en 503 `degraded`, puis revient automatiquement en 200 après redémarrage (reconnexion automatique).
155+
- Faux positif corrigé : au réveil d'une instance serverless, l'ancien contrôle (`readyState`) signalait `db: down` pendant la reconnexion. Il est remplacé par un `ping` réel, pour éviter les fausses alertes.
156+
-*(moniteurs UptimeRobot sur les deux environnements + alertes e-mail : en cours)*
142157

143158
---
144159

@@ -151,6 +166,8 @@ Sur chaque push et pull request vers `main` / `develop` :
151166
| 3 |`docker-compose.yml` : ports faux, gateway absent |`docker-compose.yml`| Réécriture complète | ✅ |
152167
| 4 | Build Vercel en échec (warning ESLint `setError` inutilisé) |`frontend/src/pages/Register.js`| Message d'erreur affiché dans le formulaire | ✅ |
153168
| 5 | Accès admin et dump absents |`backend/seeder.js`| Création du premier admin à partir des variables d'environnement | ✅ |
169+
| 6 | Identifiants SMTP fournis refusés par Gmail (`535 BadCredentials`) | configuration | Mot de passe d'application Gmail du candidat, stocké en *Secret* Vercel | ⏳ |
170+
| 7 |`/health` : faux « db: down » au réveil d'une instance serverless |`backend/app.js`|`ping` MongoDB réel | ✅ |
154171
| … |*(suite en session 2)*||||
155172

156173
---

‎backend/app.js‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ const logger = require('./config/logger');
77

88
constapp=express();
99
app.disable('x-powered-by');
10+
// Derrière le proxy Vercel / Nginx : req.ip = IP réelle du client (X-Forwarded-For du premier proxy)
11+
app.set('trust proxy',1);
1012

1113
// CORS restreint aux fronts autorisés (liste séparée par des virgules)
1214
constallowedOrigins=(process.env.CORS_ORIGINS||'http://localhost:3000')
@@ -62,6 +64,7 @@ app.use(async (req, res, next) => {
6264
app.use('/api/products',require('./routes/productRoutes'));
6365
app.use('/api/orders',require('./routes/orderRoutes'));
6466
app.use('/api/auth',require('./routes/authRoutes'));
67+
app.use('/api/audit',require('./routes/auditRoutes'));
6568

6669
app.use((req,res)=>res.status(404).json({message: 'Ressource introuvable'}));
6770

‎backend/controllers/authController.js‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ const jwt = require('jsonwebtoken');
33
constbcrypt=require('bcryptjs');
44
constUser=require('../models/User');// modèle utilisateur
55
constlogger=require('../config/logger');
6+
const{ audit }=require('../services/auditService');
67
//const sendEmail = require('../services/emailService');
78

89
// Ne jamais journaliser de mot de passe, de hash ou de token
@@ -12,18 +13,18 @@ exports.login = async (req, res) => {
1213
try{
1314
constuser=awaitUser.findOne({ username });
1415
if(!user){
15-
logger.warn('Audit: échec de connexion (utilisateur inconnu)',{audit: true, username });
16+
awaitaudit(req,'échec de connexion (utilisateur inconnu)',{level: 'warn', username });
1617
returnres.status(400).json({message: 'Utilisateur non trouvé'});
1718
}
1819

1920
constisMatch=awaitbcrypt.compare(password,user.password);
2021
if(!isMatch){
21-
logger.warn('Audit: échec de connexion (mot de passe)',{audit: true, username });
22+
awaitaudit(req,'échec de connexion (mot de passe)',{level: 'warn',actorId: user._id, username });
2223
returnres.status(400).json({message: 'Mot de passe incorrect'});
2324
}
2425

2526
consttoken=jwt.sign({userId: user._id,role: user.role},process.env.JWT_SECRET,{expiresIn: '1h'});
26-
logger.info('Audit: connexion réussie',{audit: true,userId: user._id.toString(),role: user.role});
27+
awaitaudit(req,'connexion réussie',{actorId: user._id,username: user.username,details: {role: user.role}});
2728

2829
res.json({ token,role: user.role,username: user.username});
2930
}catch(error){
@@ -48,7 +49,7 @@ exports.register = async (req, res) => {
4849
constuser=newUser({ username, email, password });
4950
awaituser.save();
5051

51-
logger.info('Audit: nouvel utilisateur inscrit',{audit: true,userId: user._id.toString()});
52+
awaitaudit(req,'nouvel utilisateur inscrit',{actorId: user._id,username: user.username});
5253

5354
// Envoyer un email de bienvenue
5455
// await sendEmail(

‎backend/controllers/orderController.js‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
constOrder=require('../models/Order');
33
constlogger=require('../config/logger');
44
const{ sendNotification }=require('../services/notificationService');
5+
const{ audit }=require('../services/auditService');
56

67

78
exports.createOrder=async(req,res)=>{
@@ -175,7 +176,7 @@ exports.updateOrderStatus = async (req, res) => {
175176
returnres.status(404).json({message: "Commande non trouvée."});
176177
}
177178

178-
logger.info('Audit: statut de commande modifié',{audit: true,orderId, status,by: req.user.userId});
179+
awaitaudit(req,'statut de commande modifié',{actorId: req.user.userId,details: {orderId, status}});
179180
res.status(200).json({message: "Statut mis à jour avec succès", order });
180181
}catch(error){
181182
logger.error('Erreur lors de la mise à jour de la commande',{ orderId,error: error.message});

‎backend/controllers/productController.js‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// controllers/productController.js
22
constProduct=require('../models/Product');
3+
const{ audit }=require('../services/auditService');
34

45
exports.getProducts=async(req,res)=>{
56
constproducts=awaitProduct.find();
@@ -24,6 +25,7 @@ exports.updateProductStock = async (req, res) => {
2425
product.updatedAt=Date.now();
2526

2627
awaitproduct.save();
28+
awaitaudit(req,'stock produit modifié',{actorId: req.user.userId,details: { productId, stock }});
2729

2830
res.json({message: "Stock mis à jour avec succès.", product });
2931
}catch(error){

‎backend/models/AuditLog.js‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// backend/models/AuditLog.js
2+
// Journal d'audit persistant : qui a fait quoi, quand, depuis où (conservé 90 jours)
3+
constmongoose=require('mongoose');
4+
5+
constauditLogSchema=newmongoose.Schema({
6+
action: {type: String,required: true},
7+
level: {type: String,enum: ['info','warn'],default: 'info'},
8+
actorId: {type: mongoose.Schema.Types.ObjectId,ref: 'User'},
9+
username: {type: String},
10+
details: {type: mongoose.Schema.Types.Mixed},
11+
ip: {type: String},
12+
env: {type: String},
13+
createdAt: {type: Date,default: Date.now,expires: 60*60*24*90},
14+
});
15+
16+
module.exports=mongoose.model('AuditLog',auditLogSchema);

‎backend/routes/auditRoutes.js‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// backend/routes/auditRoutes.js
2+
constexpress=require('express');
3+
constAuditLog=require('../models/AuditLog');
4+
const{ authenticateToken, isAdmin }=require('../middlewares/authMiddleware');
5+
6+
constrouter=express.Router();
7+
8+
// Consultation du journal d'audit (administrateurs uniquement) : ?limit=50&action=...
9+
router.get('/',authenticateToken,isAdmin,async(req,res,next)=>{
10+
try{
11+
constlimit=Math.min(parseInt(req.query.limit,10)||50,200);
12+
constfilter=typeofreq.query.action==='string' ? {action: req.query.action} : {};
13+
constlogs=awaitAuditLog.find(filter).sort({createdAt: -1}).limit(limit).lean();
14+
res.json(logs);
15+
}catch(error){
16+
next(error);
17+
}
18+
});
19+
20+
module.exports=router;

‎backend/services/auditService.js‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// services/auditService.js
2+
// Trace une action sensible dans les logs (Winston) ET dans la collection auditlogs (MongoDB)
3+
constAuditLog=require('../models/AuditLog');
4+
constlogger=require('../config/logger');
5+
6+
exports.audit=async(req,action,{ level ='info', actorId, username, details }={})=>{
7+
constentry={ action, level, actorId, username, details,ip: req.ip,env: process.env.APP_ENV||'local'};
8+
logger[level](`Audit: ${action}`,{audit: true, ...entry});
9+
10+
try{
11+
awaitAuditLog.create(entry);
12+
}catch(error){
13+
// L'échec d'écriture de l'audit ne doit pas bloquer l'action métier
14+
logger.error('Écriture du journal d\'audit impossible',{ action,error: error.message});
15+
}
16+
};

‎scripts/uptimerobot-setup.js‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// scripts/uptimerobot-setup.js
2+
// Crée les moniteurs UptimeRobot (préprod + prod) avec alerte e-mail.
3+
// Usage : node scripts/uptimerobot-setup.js (lit UPTIMEROBOT_API_KEY dans .env.uptimerobot, jamais affichée)
4+
constfs=require('fs');
5+
constpath=require('path');
6+
7+
constenvFile=path.join(__dirname,'..','.env.uptimerobot');
8+
constapiKey=(fs.readFileSync(envFile,'utf8').match(/^UPTIMEROBOT_API_KEY=(.+)$/m)||[])[1]?.trim();
9+
if(!apiKey)thrownewError('UPTIMEROBOT_API_KEY absente de .env.uptimerobot');
10+
11+
constDOMAIN='guevel-shop.xyz';
12+
constservices=['api','gateway','notifications','stock'];
13+
constmonitors=[];
14+
for(const[env,suffix]of[['PROD',''],['PREPROD','-preprod']]){
15+
monitors.push({name: `${env} - frontend`,url: `https://${suffix ? `preprod.${DOMAIN}` : DOMAIN}/`,type: 1});
16+
for(constsofservices){
17+
// Moniteur "mot-clé" : alerte si la sonde ne contient plus "status":"ok" (couvre aussi db down sur l'API)
18+
monitors.push({name: `${env} - ${s}`,url: `https://${s}${suffix}.${DOMAIN}/health`,type: 2,keyword: '"status":"ok"'});
19+
}
20+
}
21+
22+
constcall=async(method,params)=>{
23+
constbody=newURLSearchParams({api_key: apiKey,format: 'json', ...params});
24+
constres=awaitfetch(`https://api.uptimerobot.com/v2/${method}`,{method: 'POST', body });
25+
returnres.json();
26+
};
27+
28+
(async()=>{
29+
constcontacts=awaitcall('getAlertContacts',{});
30+
if(contacts.stat!=='ok')thrownewError(`getAlertContacts : ${JSON.stringify(contacts.error)}`);
31+
constalertContacts=contacts.alert_contacts.map((c)=>`${c.id}_0_0`).join('-');
32+
console.log(`Contacts d'alerte : ${contacts.alert_contacts.length}`);
33+
34+
constexisting=awaitcall('getMonitors',{});
35+
constexistingUrls=newSet((existing.monitors||[]).map((m)=>m.url));
36+
37+
for(constmofmonitors){
38+
if(existingUrls.has(m.url)){
39+
console.log(`déjà présent ${m.name}`);
40+
continue;
41+
}
42+
constparams={friendly_name: m.name,url: m.url,type: m.type,interval: 300,alert_contacts: alertContacts};
43+
if(m.type===2)Object.assign(params,{keyword_type: 2,keyword_case_type: 0,keyword_value: m.keyword});
44+
constr=awaitcall('newMonitor',params);
45+
console.log(`${r.stat==='ok' ? 'créé ' : 'ÉCHEC '}${m.name} -> ${m.url}${r.stat==='ok' ? '' : ' '+JSON.stringify(r.error)}`);
46+
}
47+
})().catch((e)=>{
48+
console.error(e.message);
49+
process.exit(1);
50+
});

0 commit comments

Comments
 (0)