13 Commits

Author SHA1 Message Date
julien 36c1fe805e refactor(routes): extract user and application routes into auth domain
Move /api/cms/user and /api/skydive/applications to a new /api/auth domain.
Users and API key applications are identity/access concerns, not CMS or
skydive content. The auth domain provides a clear boundary for future
middleware hardening (rate limiting, audit logging, etc).

docs: add ADR 0014
2026-05-01 20:43:13 +02:00
julien 96da68e18e chore(deps): upgrade to Express 5 and remove redundant middleware
- Express 4 → 5.2.1
- Remove method-override (unused with Angular HttpClient) and errorhandler (superseded by exceptions.handler.js)
- Remove asyncHandler wrapper from all controllers — Express 5 propagates async rejections natively
- Remove body-parser (redundant, Express 5 includes it internally)
- Patch/minor updates: cors, dotenv, ejs, express-jwt, express-session, jsonwebtoken, morgan, mysql2, sequelize, underscore
- Dev updates: eslint 9.0→9.39, nodemon 2→3, globals, sequelize-cli
- Fix getUser endpoint to include SkydiverProfile data in response
- docs: add ADR 0013, update README
2026-05-01 20:42:39 +02:00
julien 59401b3c85 docs: README update 2026-04-27 23:10:49 +02:00
julien 74f19b669b docs(gitea): add SSH key setup step and fix SSH username to gitea@ 2026-04-27 21:14:16 +02:00
julien 461d66a90f docs: add Gitea installation guide for Debian 11 with ISPConfig 2026-04-27 19:54:44 +02:00
julien ad9009afa1 fix(auth): replace normalizeEmail with toLowerCase to prevent Gmail dot-stripping
normalizeEmail() transforms e.g. user.name@gmail.comusername@gmail.com
(removes dots per Gmail aliasing rules), causing a mismatch between the
stored email and the value Passport searches for at login. Replaced with
toLowerCase() which is the only normalization applied consistently by
create-user.js and the rest of the app.
2026-04-27 01:26:38 +02:00
julien d82dc0578c fix(scripts): suppress readline echo during password input in create-user 2026-04-27 01:17:39 +02:00
julien ec61ae6721 fix(scripts): use mysql.js registry in create-user instead of deleted barrel 2026-04-27 01:10:29 +02:00
julien 29a29a5074 docs: add complete production deployment guide 2026-04-27 00:20:50 +02:00
julien 6641da9a3d feat(validation): replace fieldValidation with express-validator on products and tags
Add products.validators.js and tags.validators.js following the same
pattern as articles/users. Wire createProductRules, updateProductRules,
and createTagRules into their respective routes via the validate middleware.
Remove the local fieldValidation helper from both controllers and fix
missing return statements on next() calls in products.controller.js.
2026-04-27 00:04:50 +02:00
julien 80cd96e006 chore(deps): add helmet, express-rate-limit, express-validator
Add production dependencies introduced in recent security commits.
Add docs/ENVIRONMENT_VARIABLES.md documenting TRUST_PROXY, SECRET,
SESSION_SECRET, and ALLOWED_ORIGINS with configuration guidance.
2026-04-26 23:52:09 +02:00
julien 54b343b2fb chore: remove legacy root-level documentation files 2026-04-26 23:51:58 +02:00
julien 7c16e5d8cb docs(adr): add ADR 0012 — express-validator for input validation 2026-04-26 22:07:55 +02:00
41 changed files with 2318 additions and 3032 deletions
-210
View File
@@ -1,210 +0,0 @@
# Méthodes disponibles sur l'objet `Article` (Sequelize)
Ce document liste toutes les méthodes automatiquement générées par Sequelize pour le modèle `Article` en fonction des associations définies dans `src/database/relationships/index.js`.
---
## Associations Many-to-Many (belongsToMany)
### 1. **Tags** via `TagList`
Association alias: `articleTags`
Clés: `foreignKey: "articleId"`, `otherKey: "tagName"`
```javascript
article.getArticleTags() // Récupère tous les tags
article.setArticleTags([...]) // Définit les tags
article.addArticleTag(tag) // Ajoute un tag
article.addArticleTags([...]) // Ajoute plusieurs tags
article.removeArticleTag(tag) // Retire un tag
article.removeArticleTags([...]) // Retire plusieurs tags
article.countArticleTags() // Compte les tags
article.hasArticleTag(tag) // Vérifie si le tag est lié
article.hasArticleTags([...]) // Vérifie si les tags sont liés
// Alias généré par Sequelize (correspond à belongsToMany):
// Nom de la clé étrangère singularisée + pluralisé = "ArticleTag(s)" => "addTagList", "getTagList", etc.
article.addTagList(tag) // Ajoute un tag via l'alias
article.addTagLists([...]) // Ajoute plusieurs tags via l'alias
article.getTagLists() // Récupère les tags via l'alias
article.setTagLists([...]) // Définit les tags via l'alias
article.removeTagList(tag) // Retire un tag via l'alias
article.removeTagLists([...]) // Retire plusieurs tags via l'alias
article.countTagLists() // Compte les tags via l'alias
article.hasTagList(tag) // Vérifie si le tag est lié via l'alias
article.hasTagLists([...]) // Vérifie si les tags sont liés via l'alias
```
### 2. **Users (Favoris)** via `Favorite`
Association alias: `articleFavoriteUsers`
Clés: `foreignKey: "articleId"`, `otherKey: "userId"`
```javascript
article.getArticleFavoriteUsers() // Récupère les utilisateurs qui ont favorisé
article.setArticleFavoriteUsers([...]) // Définit les utilisateurs
article.addUserIdUser(user) // Ajoute un utilisateur
article.addArticleFavoriteUsers([...]) // Ajoute plusieurs utilisateurs
article.removeUserIdUser(user) // Retire un utilisateur
article.removeArticleFavoriteUsers([...]) // Retire plusieurs utilisateurs
article.countArticleFavoriteUsers() // Compte les utilisateurs
article.hasUserIdUser(user) // Vérifie si l'utilisateur a favorisé
article.hasArticleFavoriteUsers([...]) // Vérifie si les utilisateurs ont favorisé
```
---
## Associations One-to-Many (hasMany)
### 3. **Comments**
Association alias: `comments`
Clé étrangère: `articleId`
```javascript
article.getComments() // Récupère tous les commentaires
article.setComments([...]) // Définit les commentaires
article.addComment(comment) // Crée/ajoute un commentaire
article.addComments([...]) // Ajoute plusieurs commentaires
article.removeComment(comment) // Retire un commentaire
article.removeComments([...]) // Retire plusieurs commentaires
article.countComments() // Compte les commentaires
article.hasComment(comment) // Vérifie si le commentaire existe
article.hasComments([...]) // Vérifie si les commentaires existent
article.createComment({...}) // Crée un nouveau commentaire
```
### 4. **Favorites** (enregistrements de join)
Association alias: `favorites`
Clé étrangère: `articleId`
```javascript
article.getFavorites() // Récupère les enregistrements Favorite
article.setFavorites([...]) // Définit les favoris
article.addFavorite(favorite) // Ajoute un enregistrement Favorite
article.addFavorites([...]) // Ajoute plusieurs enregistrements
article.removeFavorite(favorite) // Retire un enregistrement Favorite
article.removeFavorites([...]) // Retire plusieurs enregistrements
article.countFavorites() // Compte les enregistrements Favorite
article.hasFavorite(favorite) // Vérifie si le Favorite existe
article.hasFavorites([...]) // Vérifie si les Favorites existent
article.createFavorite({...}) // Crée un nouveau Favorite
```
### 5. **TagLists** (enregistrements de join)
Association alias: `tagLists`
Clé étrangère: `articleId`
```javascript
article.getTagLists() // Récupère les enregistrements TagList
article.setTagLists([...]) // Définit les TagList
article.addTagList(tagList) // Ajoute un enregistrement TagList
article.addTagLists([...]) // Ajoute plusieurs enregistrements
article.removeTagList(tagList) // Retire un enregistrement TagList
article.removeTagLists([...]) // Retire plusieurs enregistrements
article.countTagLists() // Compte les enregistrements TagList
article.hasTagList(tagList) // Vérifie si le TagList existe
article.hasTagLists([...]) // Vérifie si les TagLists existent
article.createTagList({...}) // Crée un nouveau TagList
```
---
## Associations Belongs-To (hasOne inverse)
### 6. **Author (User)**
Association alias: `author`
Clé étrangère: `authorId`
```javascript
article.getAuthor() // Récupère l'auteur
article.setAuthor(user) // Définit l'auteur
article.createAuthor({...}) // Crée un nouvel auteur
```
---
## Méthodes personnalisées du modèle
### Méthodes d'instance définies dans `Article.js` :
```javascript
article.toJSONFor() // Retourne un objet JSON formaté
// Format: { id, slug, title, description, body }
```
---
## Hooks (Triggers)
```javascript
Article.beforeValidate() // Déclenché avant la validation
// Génère automatiquement un slug basé sur le titre
// Format: slug(`${article.title}`) + '-' + random
```
---
## Exemples d'utilisation concrets
### Ajouter des tags à un article
```javascript
const article = await Article.findByPk(1);
const tag = await Tag.findByPk('javascript');
await article.addTagList(tag); // Via alias
// ou
await article.addArticleTag(tag); // Via alias généré automatiquement
```
### Récupérer les commentaires d'un article
```javascript
const article = await Article.findByPk(1);
const comments = await article.getComments();
```
### Obtenir les utilisateurs qui ont favorisé cet article
```javascript
const article = await Article.findByPk(1);
const fans = await article.getArticleFavoriteUsers();
```
### Créer un commentaire pour un article
```javascript
const article = await Article.findByPk(1);
const comment = await article.createComment({
body: "Great article!",
authorId: userId
});
```
### Compter les tags
```javascript
const article = await Article.findByPk(1);
const tagCount = await article.countTagLists();
```
---
## Notes importantes
1. **Noms des méthodes générées** : Sequelize génère les noms en fonction du nom de l'association (alias) ou du modèle.
- Si alias est `articleTags``getArticleTags()`, `addArticleTag()`, etc.
- Si alias est `tagLists``getTagLists()`, `addTagList()`, etc.
2. **Singulier/Pluriel** : Les méthodes `add*` acceptent soit un seul élément (singulier) soit un tableau (pluriel).
3. **Paramètres optionnels** : La plupart des méthodes `get*` acceptent un paramètre `options` pour personnaliser la requête :
```javascript
article.getComments({
where: { createdAt: { [Op.gte]: someDate } },
limit: 10,
offset: 0,
order: [['createdAt', 'DESC']]
});
```
4. **Transactions** : Toutes les opérations CRUD peuvent être encapsulées dans une transaction :
```javascript
const transaction = await sequelize.transaction();
await article.addTagList(tag, { transaction });
await transaction.commit();
```
5. **Alias vs Nom généré** : L'alias défini dans la relation détermine le nom des méthodes générées. Dans ce projet, les alias comme `articleTags`, `tagLists` permettent d'appeler `getTagLists()`, `addTagList()`, etc.
-328
View File
@@ -1,328 +0,0 @@
╔═══════════════════════════════════════════════════════════════════════════════╗
║ ║
║ ✅ MIGRATION AUTOMATIQUE RÉUSSIE - Structure Basée Domaines ║
║ ║
║ De: /api/v1, /api/v2, /api/v3 (confus) ║
║ Vers: /api/skydive, /api/cms, /api/herowars (clair) ║
║ ║
╚═══════════════════════════════════════════════════════════════════════════════╝
📊 STATISTIQUES DE LA MIGRATION
═══════════════════════════════════════════════════════════════════════════════
✓ Fichiers de routes migrés: 19 fichiers
✓ Domaines créés: 3 nouveaux domaines
✓ Fichiers index.js: 4 créés/mis à jour
✓ Documentation: 5 fichiers de rapport
✓ Scripts de migration: 3 scripts npm
✓ Commits Git: 2 commits
✓ Fichiers Swagger mis à jour: 4 fichiers YAML
🎯 STRUCTURE NOUVELLE CRÉÉE
═══════════════════════════════════════════════════════════════════════════════
src/routes/api/
├── skydive/ ← Parachutisme
│ ├── aeronefs.js
│ ├── applications.js
│ ├── canopies.js
│ ├── dropzones.js
│ ├── jumps.js
│ ├── pages.js
│ ├── profiles.js
│ ├── qcm.js
│ └── index.js
├── cms/ ← Gestion de contenu
│ ├── articles.routes.js
│ ├── comments.routes.js
│ ├── product.routes.js
│ ├── products.routes.js
│ ├── profiles.routes.js
│ ├── tags.routes.js
│ ├── user.routes.js
│ └── index.js
└── herowars/ ← Jeu HeroWars
├── clans.routes.js
├── members.routes.js
├── herowars.routes.js
└── index.js
📝 FICHIERS DE DOCUMENTATION CRÉÉS
═══════════════════════════════════════════════════════════════════════════════
1. MIGRATION_REPORT.md
└─ Rapport détaillé: structure ancienne → nouvelle, fichiers migrés
2. MIGRATION_COMPLETE_SUMMARY.md
└─ Résumé exécutif avec instructions de test et frontend
3. POST_MIGRATION_CHECKLIST.md
└─ Liste de vérification manuelle pour validation
4. SCRIPTS_MIGRATION_GUIDE.md
└─ Guide complet d'utilisation des scripts (CE FICHIER)
5. MIGRATION_SWAGGER_TODO.md
└─ Notes sur les mises à jour Swagger/OpenAPI
⚙️ SCRIPTS DE MIGRATION CRÉÉS
═══════════════════════════════════════════════════════════════════════════════
1. scripts/migrate-to-domain-structure.js
├─ Phase 1: Crée les répertoires de domaines
├─ Phase 2: Migre tous les fichiers de routes
├─ Phase 3: Génère les fichiers index.js
├─ Phase 4: Met à jour src/routes/api/index.js
├─ Phase 5: Crée les notes Swagger
├─ Phase 6: Gère les fichiers Swagger
├─ Phase 7: Effectue les opérations Git
├─ Phase 8: Valide la migration
└─ Phase 9: Génère un rapport
[✅ EXÉCUTÉ: commit 9f73ed0]
2. scripts/update-references-post-migration.js
├─ Phase 1: Met à jour les fichiers YAML Swagger
├─ Phase 2: Met à jour la collection/env Postman
├─ Phase 3: Scanne les références v1/v2/v3 restantes
└─ Phase 4: Génère une liste de vérification
[✅ EXÉCUTÉ]
3. scripts/finalize-migration.js
├─ Phase 1: Valide la structure des domaines
├─ Phase 2: Liste les anciens répertoires
├─ Phase 3: Optionnel - Nettoie les v1/v2/v3 (--cleanup)
├─ Phase 4: Optionnel - Teste les endpoints (--test)
└─ Phase 5: Génère un résumé final
[PRÊT À EXÉCUTER - voir instructions ci-dessous]
🚀 PROCHAINES ÉTAPES IMMÉDIATES
═══════════════════════════════════════════════════════════════════════════════
ÉTAPE 1️⃣: VALIDER LA STRUCTURE
────────────────────────────────────────────────────────────────────────────────
$ node scripts/finalize-migration.js
✓ Affichera: "skydive/: ✓ (8 route files)"
✓ Affichera: "cms/: ✓ (7 route files)"
✓ Affichera: "herowars/: ✓ (3 route files)"
ÉTAPE 2️⃣: NETTOYER LES ANCIENS RÉPERTOIRES (OPTIONNEL)
────────────────────────────────────────────────────────────────────────────────
OPTION A - Via script:
$ node scripts/finalize-migration.js --cleanup
OPTION B - Manuellement:
$ rm -rf src/routes/api/v1 src/routes/api/v2 src/routes/api/v3
$ git add -A && git commit -m "chore: remove old v1/v2/v3 directories"
ÉTAPE 3️⃣: TESTER LE SERVEUR
────────────────────────────────────────────────────────────────────────────────
Terminal 1 - Démarrer l'API:
$ npm run local
Terminal 2 - Tester les endpoints:
$ curl http://localhost:3200/api-docs # Swagger UI
$ curl http://localhost:3200/api/skydive/jumps # Skydive
$ curl http://localhost:3200/api/cms/articles # CMS
$ curl http://localhost:3200/api/herowars/clans # HeroWars
ÉTAPE 4️⃣: METTRE À JOUR LE FRONTEND (IMPORTANT!) 🔴
────────────────────────────────────────────────────────────────────────────────
A. Mettre à jour les environnements Angular:
Fichiers à modifier:
- src/environments/environment.ts
- src/environments/environment.development.ts
- src/environments/environment.local.ts
AVANT:
export const environment = {
apiUrl: '/api/v1',
cmsUrl: '/api/v2',
herowarsUrl: '/api/v3',
};
APRÈS:
export const environment = {
apiUrl: '/api/skydive',
cmsUrl: '/api/cms',
herowarsUrl: '/api/herowars',
};
B. Mettre à jour les services Angular:
Cherchez et remplacez les chemins en dur:
sed -i '' "s|'/api/v1/|'/api/skydive/|g" src/**/*.ts
sed -i '' "s|'/api/v2/|'/api/cms/|g" src/**/*.ts
sed -i '' "s|'/api/v3/|'/api/herowars/|g" src/**/*.ts
C. Tester le frontend:
$ npm start
Vérifier dans DevTools que les appels API vont vers /api/skydive/*, /api/cms/*, etc.
ÉTAPE 5️⃣: POUSSER LES CHANGEMENTS
────────────────────────────────────────────────────────────────────────────────
$ git push origin develop
ou
$ git push --all --follow-tags
📋 RÉSUMÉ DES COMMITS GIT
═══════════════════════════════════════════════════════════════════════════════
Commit 1: 9f73ed0
├─ Message: "refactor: restructure API routes from v1/v2/v3 to domain-based..."
├─ Actions:
│ ├─ Créé: src/routes/api/skydive/, cms/, herowars/
│ ├─ Copié: 19 fichiers de routes
│ └─ Mis à jour: src/routes/api/index.js
└─ État: ✅ MERGED
Commit 2: 74df96c
├─ Message: "docs: add migration documentation, finalization scripts, and update Swagger paths"
├─ Actions:
│ ├─ Créé: 4 fichiers de documentation
│ ├─ Créé: 2 scripts de finalisation
│ └─ Mis à jour: 4 fichiers Swagger YAML
└─ État: ✅ CURRENT HEAD
🔄 AVANT/APRÈS - COMPARAISON DES API
═══════════════════════════════════════════════════════════════════════════════
SAUT/PARACHUTISME:
AVANT: GET /api/v1/jumps
APRÈS: GET /api/skydive/jumps
──────────────────────
GET /api/v1/aeronefs → GET /api/skydive/aeronefs
POST /api/v1/aeronefs → POST /api/skydive/aeronefs
CONTENU:
AVANT: GET /api/v2/articles
APRÈS: GET /api/cms/articles
────────────────────────
GET /api/v2/articles/123 → GET /api/cms/articles/123
POST /api/v2/articles → POST /api/cms/articles
GET /api/v2/comments → GET /api/cms/comments
JEU:
AVANT: GET /api/v3/clans
APRÈS: GET /api/herowars/clans
────────────────────────
GET /api/v3/members → GET /api/herowars/members
POST /api/v3/members/123 → POST /api/herowars/members/123
⚠️ IMPORTANT: ROLLBACK EN CAS DE PROBLÈME
═══════════════════════════════════════════════════════════════════════════════
Si vous rencontrez des problèmes:
Option 1 - Peu de changements (rollback 2 commits):
$ git reset --hard HEAD~2
Option 2 - Si vous avez poussé, utiliser revert:
$ git revert 74df96c
$ git revert 9f73ed0
Option 3 - Restaurer depuis le remote:
$ git fetch origin
$ git reset --hard origin/develop
✅ CHECKLIST DE VALIDATION POST-MIGRATION
═══════════════════════════════════════════════════════════════════════════════
Backend (adastra_api):
☐ npm run local - serveur démarre sans erreur
☐ curl http://localhost:3200/api-docs - Swagger UI accessible
☐ Endpoints skydive fonctionnent: GET /api/skydive/jumps
☐ Endpoints CMS fonctionnent: GET /api/cms/articles
☐ Endpoints HeroWars fonctionnent: GET /api/herowars/clans
Frontend (adastra_app):
☐ npm start - app démarre sans erreur
☐ Appels API vont vers les nouveaux chemins
☐ Authentification toujours fonctionnelle
☐ Pages affichent les données correctement
☐ Aucune erreur 404 sur les endpoints
Git:
☐ Commits créés et visibles: git log --oneline -5
☐ Prêt pour: git push origin develop
Documentation:
☐ README.md mis à jour (optionnel)
☐ Équipe notifiée du changement
☐ Documentation technique mise à jour
📚 RESSOURCES SUPPLÉMENTAIRES
═══════════════════════════════════════════════════════════════════════════════
Consultez ces fichiers pour plus d'informations:
1. SCRIPTS_MIGRATION_GUIDE.md
└─ Guide détaillé de chaque script et options
2. POST_MIGRATION_CHECKLIST.md
└─ Liste complète de vérification manuelle
3. MIGRATION_COMPLETE_SUMMARY.md
└─ Résumé exécutif avec instructions
4. docs/*.md (depuis l'audit architectural)
└─ Architecture complète et ADRs
🎓 POINTS CLÉS À RETENIR
═══════════════════════════════════════════════════════════════════════════════
✓ NON-DESTRUCTIF: Les anciens répertoires restent, vous pouvez les nettoyer
progressivement après vérification
✓ AUTOMATISÉ: Scripts Python/Node qui gèrent la plupart des tâches
Swagger, Postman, routes mis à jour automatiquement
⚠️ MANUEL: Frontend DOIT être mis à jour (recherche/remplace)
Tests doivent être revérifiés
Documentation peut nécessiter mise à jour
✓ GIT-SAFE: Chaque étape crée un commit
Rollback facile si problème
Toutes les modifications sont tracées
✓ BÉNÉFICES: Chemins d'API clairs et explicites
Séparation nette des domaines métier
Meilleure architecture et maintenabilité
Préparation pour futures versions d'API
═══════════════════════════════════════════════════════════════════════════════
✨ Migration complétée avec succès! ✨
Prêt à continuer avec les 20 ADRs d'architecture?
(QUICK_START_WEEK1.md pour la Phase 1)
═══════════════════════════════════════════════════════════════════════════════
Date: 24 avril 2026
Statut: ✅ COMPLETE
Prochaine étape: Mise à jour du frontend + tests
-111
View File
@@ -1,111 +0,0 @@
╔════════════════════════════════════════════════════════════════════════════╗
║ MIGRATION SUCCESSFULLY COMPLETED ║
╚════════════════════════════════════════════════════════════════════════════╝
NEW API STRUCTURE:
├── /api/skydive/* ← Skydiving features (jumps, aeronefs, dropzones, etc.)
├── /api/cms/* ← Content management (articles, tags, comments, etc.)
└── /api/herowars/* ← Game features (clans, members, raids, etc.)
WHAT WAS CHANGED:
✓ Routes directory: v1/v2/v3 → domain-based structure
✓ Route definitions: Updated all index.js files
✓ Swagger YAML: Updated path prefixes to new domains
✓ Postman collection: Updated endpoint URLs
FILES MIGRATED:
✓ Skydive domain (8 route files):
- aeronefs.js, applications.js, canopies.js, dropzones.js
- jumps.js, pages.js, profiles.js, qcm.js
✓ CMS domain (8 route files):
- articles.routes.js, comments.routes.js, product.routes.js
- products.routes.js, profiles.routes.js, tags.routes.js, user.routes.js
✓ Hero Wars domain (3 route files):
- clans.routes.js, members.routes.js, herowars.routes.js
TESTING INSTRUCTIONS:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. Start the API server:
npm run local
2. In another terminal, verify Swagger is updated:
curl http://localhost:3200/api-docs
3. Test a sample endpoint:
curl http://localhost:3200/api/skydive/jumps
curl http://localhost:3200/api/cms/articles
curl http://localhost:3200/api/herowars/clans
FRONTEND UPDATES REQUIRED:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
In your Angular frontend (adastra_app):
1. Update environment files:
src/environments/environment.ts
src/environments/environment.development.ts
src/environments/environment.local.ts
Replace:
- apiUrl: '/api/v1' → apiUrl: '/api/skydive'
- cmUrl: '/api/v2' → cmUrl: '/api/cms'
- hwUrl: '/api/v3' → hwUrl: '/api/herowars'
2. Update API service calls:
// Before:
http.get('/api/v1/jumps')
http.get('/api/v2/articles')
http.get('/api/v3/clans')
// After:
http.get('/api/skydive/jumps')
http.get('/api/cms/articles')
http.get('/api/herowars/clans')
3. Search & replace in your services:
sed -i 's|/api/v1/|/api/skydive/|g' src/**/*.ts
sed -i 's|/api/v2/|/api/cms/|g' src/**/*.ts
sed -i 's|/api/v3/|/api/herowars/|g' src/**/*.ts
GIT OPERATIONS:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
View migration commits:
git log --oneline -5
If needed, view detailed changes:
git show HEAD
Push to remote:
git push origin develop
ROLLBACK (if issues):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
If you need to rollback:
git reset --hard HEAD~2
(Adjust the number of commits based on history)
ADDITIONAL RESOURCES:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Documentation files created:
- MIGRATION_REPORT.md: Detailed migration information
- MIGRATION_SWAGGER_TODO.md: Swagger update notes
- POST_MIGRATION_CHECKLIST.md: Verification checklist
Scripts created:
- scripts/migrate-to-domain-structure.js: Main migration script
- scripts/update-references-post-migration.js: Post-migration updates
- scripts/finalize-migration.js: Cleanup and testing
═════════════════════════════════════════════════════════════════════════════
✓ Migration is complete! Follow the testing instructions above.
═════════════════════════════════════════════════════════════════════════════
-67
View File
@@ -1,67 +0,0 @@
═══════════════════════════════════════════════════════════════════════════════
API STRUCTURE MIGRATION REPORT
═══════════════════════════════════════════════════════════════════════════════
MIGRATION: v1/v2/v3 → Domain-Based (skydive/cms/herowars)
OLD STRUCTURE:
└── routes/api/
├── v1/ (Skydive routes)
├── v2/ (CMS + misc routes)
└── v3/ (Hero Wars routes)
NEW STRUCTURE:
└── routes/api/
├── skydive/
├── cms/
└── herowars/
ROUTE MAPPINGS:
✓ /api/v1/* → /api/skydive/*
- applications, aeronefs, canopies, dropzones, jumps, pages, profiles, qcm
✓ /api/v2/* → /api/cms/*
- user, articles, comments, product, products, profiles, tags
✓ /api/v3/* → /api/herowars/*
- clans, members, herowars (moved from v2)
FILES CREATED/UPDATED:
✓ src/routes/api/skydive/index.js
✓ src/routes/api/cms/index.js
✓ src/routes/api/herowars/index.js
✓ src/routes/api/index.js
✓ All route files copied to appropriate domains
MANUAL STEPS REQUIRED:
1. Update Swagger/OpenAPI documentation:
- Review MIGRATION_SWAGGER_TODO.md
- Update path prefixes in src/swagger/*.yaml files
- Update paths in src/config/swagger.js if needed
2. Update app.js (if direct version references exist):
- Search for 'v1', 'v2', 'v3' strings
- Replace with domain names where appropriate
3. Update tests and client calls:
- Replace /api/v1/ with /api/skydive/
- Replace /api/v2/ with /api/cms/
- Replace /api/v3/ with /api/herowars/
4. Update any documentation:
- README.md API endpoints section
- API documentation and guides
5. Test all endpoints:
- Verify each domain routes work correctly
- Check authentication/authorization still functions
- Validate error handling
OPTIONAL: Old v1/v2/v3 directories can be removed after verification:
rm -rf src/routes/api/v1
rm -rf src/routes/api/v2
rm -rf src/routes/api/v3
═══════════════════════════════════════════════════════════════════════════════
-19
View File
@@ -1,19 +0,0 @@
# Swagger Path Updates Required
The following Swagger YAML files need path updates:
## skydive.yaml
- Change: /api/v1/ → /api/skydive/
- Affected routes: /jumps, /aeronefs, /dropzones, /canopies, /pages, /qcm, /applications
## cms.yaml
- Change: /api/v2/ → /api/cms/
- Affected routes: /articles, /comments, /products, /tags, /profiles, /user
## herowars.yaml
- Change: /api/v3/ → /api/herowars/
- Affected routes: /clans, /members
- Note: Add herowars endpoint from former /api/v2/herowars
Update in: src/config/swagger.js if paths are hardcoded.
-80
View File
@@ -1,80 +0,0 @@
# Post-Migration Update Checklist
## Files Updated by Script ✓
- [x] Swagger YAML files (/api/v1 → /api/skydive, etc.)
- [x] Postman collection and environment files
- [x] API routes structure (new domains created)
## Manual Verification Required
### 1. Test API Endpoints
```bash
npm run local # Start the server
# In another terminal:
curl http://localhost:3200/api-docs # Check Swagger UI
```
### 2. Verify Routes
- [ ] GET /api/skydive/jumps
- [ ] GET /api/cms/articles
- [ ] GET /api/herowars/clans
- [ ] Other domain-specific endpoints
### 3. Frontend Updates Required (if applicable)
- [ ] Update Angular environment files (environment.ts, environment.development.ts, etc.)
- [ ] Replace all API calls from /api/v1, /api/v2, /api/v3 to new domains
- [ ] Update service layer URLs
- [ ] Update HTTP interceptors if needed
### 4. Documentation Updates
- [ ] Update README.md with new API structure
- [ ] Update API documentation (if any)
- [ ] Update deployment/infrastructure docs
- [ ] Update team wiki/documentation
### 5. CI/CD Pipeline (if applicable)
- [ ] Update build/deployment scripts referencing v1/v2/v3
- [ ] Update any health checks pointing to old paths
- [ ] Update monitoring/alerting rules
### 6. Client Applications
- [ ] Update all client libraries/SDKs
- [ ] Update mobile app API endpoints
- [ ] Update third-party integrations
### 7. Database/Caching (if applicable)
- [ ] Clear any cached references to old endpoints
- [ ] Update API documentation in databases
- [ ] Check for hardcoded URLs in configuration
## Git Operations
```bash
# View the migration commits
git log --oneline -5
# If rollback needed:
git revert <commit-hash>
# Push changes
git push origin develop
```
## Optional: Cleanup Old Directories
```bash
# After thorough testing, remove old v1/v2/v3 directories:
rm -rf src/routes/api/v1
rm -rf src/routes/api/v2
rm -rf src/routes/api/v3
git add -A
git commit -m "chore: remove old v1/v2/v3 route directories"
```
## Rollback Procedure
If issues arise, rollback with:
```bash
git reset --hard HEAD~2 # Adjust commit count if needed
```
---
Document generated: 2026-04-23T22:48:52.497Z
+126 -40
View File
@@ -1,67 +1,153 @@
# AdAstra_API # AdAstra API
API pour l'application Ad Astra Backend of the AdAstra platform — an Express REST API serving four feature domains: CMS, e-commerce, skydive club management, and Hero Wars guild analytics.
## Tech stack
| Layer | Choice |
|---|---|
| Runtime | Node.js `^16.20.2 \|\| ^18.19.1 \|\| ^20.11.1` |
| Framework | Express 4 |
| MySQL ORM | Sequelize 6 |
| MongoDB ODM | Mongoose 6 |
| Auth | Passport (local strategy) + JWT (express-jwt / jsonwebtoken) |
| Validation | express-validator |
| Security | Helmet, express-rate-limit, CORS |
| Tests | Jest + Supertest, Postman/Newman |
| Dev server | Nodemon |
## Related repository
The frontend lives in [`adastra_app`](../adastra_app) — an Angular 21 standalone app. It calls this API exclusively through relative paths prepended with `environment.apiBaseUrl`.
## Getting started
**Installation rapide**
```bash ```bash
# Initialisation (applique les migrations, seed roles → création admin interactive → seed articles) npm install
# La commande suivante applique d'abord les migrations nécessaires.
# Copy the example env file for your environment
cp config/env/.env.example config/env/.env.local
# Edit .env.local: MYSQL_DBNAME, MYSQL_DBUSER, MYSQL_DBPASS, SECRET, …
# Initialize the database (migrate + seed roles + create admin user + seed articles)
npm run setup:api npm run setup:api
# Start the dev server
npm run local
``` ```
**Installation manuelle** The server listens on `SERVER_PORT` (default **3200**).
## Commands
| Command | Description |
|---|---|
| `npm start` | Production server (`APP_ENV=production`) |
| `npm run dev` | Development server, uses `.env.development` |
| `npm run local` | Local server with Nodemon, uses `.env.local` |
| `npm test` | Jest unit + integration tests |
| `npm run test:postman` | Newman / Postman collection run |
| `npm run create:user` | Interactive admin user creation script |
| `npm run stop` | Kill the process on port 3200 |
## Environment
Environment files live in `config/env/` and are loaded by `dotenv` at startup based on `APP_ENV`:
| File | Used when |
|---|---|
| `.env.local` | `npm run local` |
| `.env.development` | `npm run dev` |
| `.env.production` | `npm start` |
Copy `.env.example` to create a new environment file. Key variables:
```
SERVER_PORT=3200
SECRET= # JWT signing secret
SESSION_SECRET=
MYSQL_DBNAME=
MYSQL_DBUSER=
MYSQL_DBPASS=
MONGODB_URI=
SENDGRID_API_KEY= # optional — email sending
SKYDIVERID_API_URL= # optional — skydiver ID external API
# TRUST_PROXY=1 # uncomment when behind a reverse proxy
```
## Architecture
### Route domains
All routes are mounted under `/api` and split into four domains:
| Domain | Prefix | Resources |
|---|---|---|
| CMS | `/api/cms` | users, profiles, articles, comments, tags |
| E-commerce | `/api/ecommerce` | products |
| Skydive | `/api/skydive` | aeronefs, canopies, dropzones, jumps, pages, QCM, applications |
| Hero Wars | `/api/herowars` | clans, members |
### Middleware pipeline
Requests flow through:
1. **Helmet** — security headers
2. **CORS** — origin whitelist from `ALLOWED_ORIGINS`
3. **Rate limiter**`express-rate-limit`
4. **Body parser** — JSON + URL-encoded
5. **Passport** — initializes local + header-API-key strategies
6. **Route handlers** — validators → `auth` middleware → controller → `asyncHandler`
7. **Error handlers** — 404 catcher, log handler, final error handler
### Auth
- **Session-less JWT**: tokens are signed with `SECRET` and attached by the client as `Authorization: Token <jwt>`.
- `auth.js` middleware verifies the token via `express-jwt` and populates `req.auth`.
- Password storage: PBKDF2-SHA512 with a random 16-byte salt (no bcrypt dependency on the auth path).
### Databases
- **MySQL** (Sequelize): users, roles, skydive entities, e-commerce, CMS content.
- **MongoDB** (Mongoose): Hero Wars guild/clan data and analytics snapshots.
## Database — migrations & seeders
**One-command setup:**
```bash
npm run setup:api
# Runs: db:migrate → seed roles → create:user → seed articles
```
**Manual steps:**
```bash ```bash
# Appliquer les migrations
npx sequelize-cli db:migrate npx sequelize-cli db:migrate
# Exécuter le seeder des articles
npx sequelize-cli db:seed --seed 20251126-add-roles.js npx sequelize-cli db:seed --seed 20251126-add-roles.js
# Créer un utilisateur Admin
npm run create:user npm run create:user
# Exécuter le seeder des articles
npx sequelize-cli db:seed --seed 20251127-add-articles.js npx sequelize-cli db:seed --seed 20251127-add-articles.js
``` ```
**Migration** **Migration helpers:**
```bash ```bash
# Appliquer les migrations
npx sequelize-cli db:migrate
# Voir le statut des migrations
npx sequelize-cli db:migrate:status npx sequelize-cli db:migrate:status
# Annuler les migrations
npx sequelize-cli db:migrate:undo:all npx sequelize-cli db:migrate:undo:all
``` ```
**Seeder** **Seeder helpers:**
```bash ```bash
# Exécuter tous les seeders
npx sequelize-cli db:seed:all npx sequelize-cli db:seed:all
# Exécuter un seeder spécifique
npx sequelize-cli db:seed --seed 20251127-add-articles.js npx sequelize-cli db:seed --seed 20251127-add-articles.js
# Annuler tous les seeders
npx sequelize-cli db:seed:undo:all npx sequelize-cli db:seed:undo:all
# Annuler un seeder spécifique
npx sequelize-cli db:seed:undo --seed 20251127-add-articles.js npx sequelize-cli db:seed:undo --seed 20251127-add-articles.js
``` ```
**Start 'local' API server** ## Documentation
```bash
# Appliquer les migrations
npm run local
# Exécuter le seeder des articles Extended references are in [`docs/`](docs/):
npx sequelize-cli db:seed --seed 20251126-add-roles.js
# Créer un utilisateur Admin | File | Content |
npm run create:user |---|---|
| [DEPLOYMENT.md](docs/DEPLOYMENT.md) | Full VPS production deployment guide (Apache, PM2, SSL) |
# Exécuter le seeder des articles | [ENVIRONMENT_VARIABLES.md](docs/ENVIRONMENT_VARIABLES.md) | All env variables documented |
npx sequelize-cli db:seed --seed 20251127-add-articles.js | [SWAGGER_GUIDE.md](docs/SWAGGER_GUIDE.md) | API documentation via Swagger UI |
``` | [GITEA_INSTALL.md](docs/GITEA_INSTALL.md) | Self-hosted Gitea on Debian 11 + ISPConfig |
-329
View File
@@ -1,329 +0,0 @@
# Migration Scripts - Guide d'utilisation
## Vue d'ensemble
Cette migration restructure votre API Express de `/api/v1` `/api/v2` `/api/v3` (qui représentaient des domaines métier, pas des versions API) vers une architecture basée sur les domaines avec des chemins clairs et explicites.
### Ancien structure (confuse)
```
/api/v1/* → Skydive features
/api/v2/* → CMS features
/api/v3/* → Hero Wars features
```
### Nouvelle structure (claire)
```
/api/skydive/* ← Toutes les fonctionnalités de parachutisme
/api/cms/* ← Gestion de contenu (articles, tags, etc.)
/api/herowars/* ← Gestion du jeu HeroWars (clans, membres, raids)
```
---
## Scripts de migration
### 1. `migrate-to-domain-structure.js` ✓ EXÉCUTÉ
**Rôle:** Crée la nouvelle structure et migre les fichiers
**Statut:** ✅ Completed
- Créé les répertoires `/api/skydive`, `/api/cms`, `/api/herowars`
- Copié tous les fichiers de routes vers les nouveaux emplacements
- Généré les nouveaux fichiers `index.js` pour chaque domaine
- Mis à jour `src/routes/api/index.js` pour utiliser les nouveaux chemins
- Créé un commit Git
**Fichiers modifiés:**
```
✓ src/routes/api/skydive/ (8 fichiers)
✓ src/routes/api/cms/ (8 fichiers)
✓ src/routes/api/herowars/ (3 fichiers)
✓ src/routes/api/index.js (mis à jour)
```
**Rapports générés:**
- `MIGRATION_REPORT.md` - Détails complets de la migration
- `MIGRATION_SWAGGER_TODO.md` - Tâches pour Swagger
---
### 2. `update-references-post-migration.js` ✓ EXÉCUTÉ
**Rôle:** Met à jour les références aux chemins dans la documentation
**Statut:** ✅ Completed
- Mis à jour les fichiers YAML Swagger (`/api/v1/``/api/skydive/`, etc.)
- Mis à jour la collection et l'environnement Postman
- Scanné les fichiers clés pour les références restantes
**Rapports générés:**
- `POST_MIGRATION_CHECKLIST.md` - Liste de vérification manuelle
---
### 3. `finalize-migration.js`
**Rôle:** Valide, nettoie, et teste la migration
**Options disponibles:**
```bash
# Valider la structure sans rien faire
node scripts/finalize-migration.js
# Valider et nettoyer les anciens répertoires
node scripts/finalize-migration.js --cleanup
# Valider et tester les endpoints (nécessite le serveur en cours d'exécution)
node scripts/finalize-migration.js --test
# Mode complet: validation, nettoyage et résumé
node scripts/finalize-migration.js --full
```
**Statut:** ✅ Ready to run
**Rapport généré:**
- `MIGRATION_COMPLETE_SUMMARY.md` - Résumé final avec instructions
---
## Guide étape par étape
### Étape 1: Vérifier la migration ✅
```bash
ls -la src/routes/api/
# Devrait afficher: cms/, herowars/, skydive/, v1/, v2/, v3/, index.js
```
### Étape 2: Valider la structure
```bash
node scripts/finalize-migration.js
```
**Résultat attendu:**
```
✓ skydive/: ✓ (8 route files)
✓ cms/: ✓ (7 route files)
✓ herowars/: ✓ (3 route files)
```
### Étape 3: Nettoyer les anciens répertoires (OPTIONNEL)
```bash
# Option A: Via le script
node scripts/finalize-migration.js --cleanup
# Option B: Manuellement
rm -rf src/routes/api/v1 src/routes/api/v2 src/routes/api/v3
git add -A
git commit -m "chore: remove old v1/v2/v3 route directories"
```
### Étape 4: Mettre à jour le frontend (IMPORTANT)
**Fichiers à modifier dans `adastra_app/src/environments/`:**
#### `environment.ts`
```typescript
// AVANT:
export const environment = {
production: false,
apiUrl: '/api/v1',
cmsUrl: '/api/v2',
herowarsUrl: '/api/v3',
};
// APRÈS:
export const environment = {
production: false,
apiUrl: '/api/skydive',
cmsUrl: '/api/cms',
herowarsUrl: '/api/herowars',
};
```
#### `environment.development.ts` et `environment.local.ts`
Applique les mêmes changements
**Mise à jour des services Angular:**
```typescript
// Dans vos services (par exemple, src/app/services/*.service.ts)
// AVANT:
this.http.get(environment.apiUrl + '/jumps')
this.http.get(environment.cmsUrl + '/articles')
this.http.get(environment.herowarsUrl + '/clans')
// APRÈS:
this.http.get(environment.apiUrl + '/jumps')
this.http.get(environment.cmsUrl + '/articles')
this.http.get(environment.herowarsUrl + '/clans')
// Pas de changement si vous utilisez les variables d'environnement!
// Sinon, cherchez/remplacez les chemins en dur
```
**Automatiser la mise à jour du frontend (macOS/Linux):**
```bash
cd adastra_app
sed -i '' "s|/api/v1|/api/skydive|g" src/**/*.ts
sed -i '' "s|/api/v2|/api/cms|g" src/**/*.ts
sed -i '' "s|/api/v3|/api/herowars|g" src/**/*.ts
```
### Étape 5: Tester les endpoints
**Démarrer le serveur API:**
```bash
cd adastra_api
npm run local
```
**Dans un autre terminal, tester les nouveaux chemins:**
```bash
# Tester Swagger UI
curl http://localhost:3200/api-docs
# Tester les endpoints des domaines
curl http://localhost:3200/api/skydive/jumps
curl http://localhost:3200/api/cms/articles
curl http://localhost:3200/api/herowars/clans
# Avec authentification si nécessaire
curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:3200/api/skydive/jumps
```
### Étape 6: Tester le frontend
```bash
cd adastra_app
npm start
# Ou avec configuration spécifique
npm run local # ng serve --configuration local
```
Vérifier dans les DevTools que les appels API vont vers les nouveaux chemins.
---
## Statistiques de la migration
| Métrique | Valeur |
|----------|--------|
| Nouveaux domaines créés | 3 (skydive, cms, herowars) |
| Fichiers de routes migrés | 19 |
| Fichiers index.js créés/mis à jour | 4 |
| Fichiers Swagger mis à jour | Oui |
| Fichiers Postman mis à jour | Oui |
| Commits Git créés | 1 (structure) + opt. 1 (nettoyage) |
---
## Fichiers de rapport créés
| Fichier | Contenu |
|---------|---------|
| `MIGRATION_REPORT.md` | Rapport détaillé de la migration |
| `MIGRATION_SWAGGER_TODO.md` | Notes pour les mises à jour Swagger |
| `POST_MIGRATION_CHECKLIST.md` | Liste de vérification manuelle |
| `MIGRATION_COMPLETE_SUMMARY.md` | Résumé final et instructions |
| `SCRIPTS_MIGRATION_GUIDE.md` | Ce fichier |
---
## Procédure de rollback
Si vous devez annuler la migration:
```bash
# Voir l'historique
git log --oneline -10
# Annuler les changements (ajuster le nombre de commits)
git reset --hard HEAD~2
# Ou restaurer une branche spécifique
git reset --hard origin/develop
```
---
## Points clés à retenir
**La migration est non-destructive:**
- Les anciens répertoires v1/v2/v3 existent toujours
- Vous pouvez les supprimer après vérification
**Mises à jour automatisées:**
- Scripts: Oui ✓
- Swagger: Oui ✓
- Postman: Oui ✓
⚠️ **Mises à jour manuelles requises:**
- Frontend Angular (environnement + services)
- Tous les appels API en dur dans le code cliente
- Documentation externe
- Tests d'intégration
**Git-safe:**
- Chaque étape crée un commit
- Rollback facile si problème
---
## Troubleshooting
### Les endpoints retournent 404
- ✓ Vérifier que le serveur a redémarré après la migration
- ✓ Vérifier les chemins dans Swagger UI (http://localhost:3200/api-docs)
- ✓ Vérifier que le backend a bien recompilé (avec nodemon)
### Les routes ne sont pas trouvées
- ✓ Vérifier que `src/routes/api/index.js` a les bonnes références
- ✓ Vérifier que chaque domaine a un `index.js`
- ✓ Vérifier l'ordre des middlewares dans `app.js`
### Erreurs d'authentification
- ✓ Les middlewares d'auth ne sont pas affectés
- ✓ Les tokens JWT restent valides
- ✓ Les headers restent les mêmes
### Le frontend ne peut pas accéder à l'API
- ✓ Vérifier CORS dans `app.js`
- ✓ Vérifier les URLs dans les environnements Angular
- ✓ Vérifier les appels http dans les services
---
## Prochaines étapes
1. ✅ Tester tous les endpoints du backend
2. ✅ Mettre à jour le frontend (environnement + services)
3. ✅ Tester le frontend avec le backend modifié
4. ✅ Mettre à jour la documentation
5. ✅ Nettoyer les anciens répertoires v1/v2/v3 (optionnel)
6. ✅ Déployer en staging/production
---
## Ressources supplémentaires
- Documentation architecture: `ARCHITECTURE_DIAGRAMS.md`
- ADR (Architecture Decision Records): `ADR_INDEX.md`
- Configuration Swagger: `src/config/swagger.js`
- Routes configuration: `src/routes/api/index.js`
---
## Support
Pour des questions ou problèmes:
1. ✓ Consulter los fichiers de rapport (`POST_MIGRATION_CHECKLIST.md`, etc.)
2. ✓ Vérifier les logs du serveur: `npm run local` (output)
3. ✓ Vérifier Swagger UI: http://localhost:3200/api-docs
4. ✓ Exécuter la validation: `node scripts/finalize-migration.js`
---
**Migration effectuée le:** 24 avril 2026
**Version API:** v1/v2/v3 → domain-based
**Status:** ✅ Complete
-762
View File
@@ -1,762 +0,0 @@
# Méthodes disponibles sur les modèles MySQL (Sequelize)
Ce document liste toutes les méthodes automatiquement générées par Sequelize pour chaque modèle en fonction des associations définies dans `src/database/relationships/index.js`.
---
## Table des matières
1. [Article](#article)
2. [Tag](#tag)
3. [User](#user)
4. [Comment](#comment)
5. [Favorite](#favorite)
6. [Follower](#follower)
7. [Product](#product)
8. [Brand](#brand)
9. [Category](#category)
10. [Packaging](#packaging)
11. [TagList](#taglist)
12. [Role](#role)
13. [UserRoleXref](#userroleexref)
14. [ProductCategoryXref](#productcategoryxref)
15. [ProductTagXref](#producttagxref)
---
## Article
### Many-to-Many (belongsToMany)
#### Tags via `TagList`
Alias: `articleTags`
```javascript
article.getArticleTags() // Récupère tous les tags
article.setArticleTags([...]) // Définit les tags
article.addArticleTag(tag) // Ajoute un tag
article.removeArticleTag(tag) // Retire un tag
article.countArticleTags() // Compte les tags
article.hasArticleTag(tag) // Vérifie si le tag est lié
```
#### Users (Favoris) via `Favorite`
Alias: `articleFavoriteUsers` (cascade delete activé)
```javascript
article.getArticleFavoriteUsers() // Récupère les utilisateurs qui ont favorisé
article.setArticleFavoriteUsers([...]) // Définit les utilisateurs
article.addArticleFavoriteUser(user) // Ajoute un utilisateur
article.removeArticleFavoriteUser(user) // Retire un utilisateur
article.countArticleFavoriteUsers() // Compte les utilisateurs
article.hasArticleFavoriteUser(user) // Vérifie si l'utilisateur a favorisé
```
### One-to-Many (hasMany)
#### Comments (cascade delete activé)
```javascript
article.getComments() // Récupère tous les commentaires
article.setComments([...]) // Définit les commentaires
article.addComment(comment) // Ajoute un commentaire
article.removeComment(comment) // Retire un commentaire
article.countComments() // Compte les commentaires
article.hasComment(comment) // Vérifie si le commentaire existe
article.createComment({...}) // Crée un commentaire
```
#### Favorites (cascade delete activé)
```javascript
article.getFavorites() // Récupère les enregistrements Favorite
article.setFavorites([...]) // Définit les favoris
article.addFavorite(favorite) // Ajoute un Favorite
article.removeFavorite(favorite) // Retire un Favorite
article.countFavorites() // Compte les Favorites
article.hasFavorite(favorite) // Vérifie si le Favorite existe
article.createFavorite({...}) // Crée un Favorite
```
#### TagLists (cascade delete activé)
```javascript
article.getTagLists() // Récupère les enregistrements TagList
article.setTagLists([...]) // Définit les TagLists
article.addTagList(tagList) // Ajoute un TagList
article.removeTagList(tagList) // Retire un TagList
article.countTagLists() // Compte les TagLists
article.hasTagList(tagList) // Vérifie si le TagList existe
article.createTagList({...}) // Crée un TagList
```
### Belongs-To
#### Author (User)
```javascript
article.getAuthor() // Récupère l'auteur
article.setAuthor(user) // Définit l'auteur
article.createAuthor({...}) // Crée un auteur
```
### Instance Methods
```javascript
article.toJSONFor() // Formate en JSON
```
---
## Tag
### Many-to-Many (belongsToMany)
#### Articles via `TagList`
Alias: `articleIdArticleTagLists`
```javascript
tag.getArticleIdArticleTagLists() // Récupère les articles
tag.setArticleIdArticleTagLists([...])
tag.addArticleIdArticleTagList(article)
tag.removeArticleIdArticleTagList(article)
tag.countArticleIdArticleTagLists()
tag.hasArticleIdArticleTagList(article)
```
#### Products via `ProductTagXref`
Alias: `productIdProductProductTagXrefs`
```javascript
tag.getProductIdProductProductTagXrefs()
tag.setProductIdProductProductTagXrefs([...])
tag.addProductIdProductProductTagXref(product)
tag.removeProductIdProductProductTagXref(product)
tag.countProductIdProductProductTagXrefs()
tag.hasProductIdProductProductTagXref(product)
```
### One-to-Many (hasMany)
#### TagLists
```javascript
tag.getTagLists() // Récupère les enregistrements TagList
tag.setTagLists([...])
tag.addTagList(tagList)
tag.removeTagList(tagList)
tag.countTagLists()
tag.hasTagList(tagList)
tag.createTagList({...})
```
#### ProductTagXrefs
```javascript
tag.getProductTagXrefs() // Récupère les ProductTagXrefs
tag.setProductTagXrefs([...])
tag.addProductTagXref(xref)
tag.removeProductTagXref(xref)
tag.countProductTagXrefs()
tag.hasProductTagXref(xref)
tag.createProductTagXref({...})
```
### Instance Methods
```javascript
tag.toJSONFor() // Formate en JSON
```
---
## User
### Many-to-Many (belongsToMany)
#### Articles (Favoris) via `Favorite`
Alias: `articleIdArticles`
```javascript
user.getArticleIdArticles() // Récupère les articles favorisés
user.setArticleIdArticles([...])
user.addArticleIdArticle(article)
user.removeArticleIdArticle(article)
user.countArticleIdArticles()
user.hasArticleIdArticle(article)
```
#### Followers (ceux qui vous suivent) via `Follower`
Alias: `followerIdUsers`
```javascript
user.getFollowerIdUsers() // Récupère les followers
user.setFollowerIdUsers([...])
user.addFollowerIdUser(user)
user.removeFollowerIdUser(user)
user.countFollowerIdUsers()
user.hasFollowerIdUser(user)
```
#### Following (que vous suivez) via `Follower`
Alias: `userIdUserFollowers`
```javascript
user.getUserIdUserFollowers() // Récupère ceux que vous suivez
user.setUserIdUserFollowers([...])
user.addUserIdUserFollower(user)
user.removeUserIdUserFollower(user)
user.countUserIdUserFollowers()
user.hasUserIdUserFollower(user)
```
#### Roles via `UserRoleXref`
Alias: `roleIdRoles`
```javascript
user.getRoleIdRoles() // Récupère les rôles
user.setRoleIdRoles([...])
user.addRoleIdRole(role)
user.removeRoleIdRole(role)
user.countRoleIdRoles()
user.hasRoleIdRole(role)
```
### One-to-Many (hasMany)
#### Articles (créés par l'utilisateur)
```javascript
user.getArticles() // Récupère les articles créés
user.setArticles([...])
user.addArticle(article)
user.removeArticle(article)
user.countArticles()
user.hasArticle(article)
user.createArticle({...})
```
#### Comments (créés par l'utilisateur)
```javascript
user.getComments() // Récupère les commentaires créés
user.setComments([...])
user.addComment(comment)
user.removeComment(comment)
user.countComments()
user.hasComment(comment)
user.createComment({...})
```
#### Favorites (enregistrements)
```javascript
user.getFavorites() // Récupère les Favorites
user.setFavorites([...])
user.addFavorite(favorite)
user.removeFavorite(favorite)
user.countFavorites()
user.hasFavorite(favorite)
user.createFavorite({...})
```
#### Followers (enregistrements)
```javascript
user.getFollowers() // Récupère les Followers
user.setFollowers([...])
user.addFollower(follower)
user.removeFollower(follower)
user.countFollowers()
user.hasFollower(follower)
user.createFollower({...})
```
#### FollowerFollowers (ceux qui vous suivent via enregistrement)
```javascript
user.getFollowerFollowers() // Récupère les FollowerFollowers
user.setFollowerFollowers([...])
user.addFollowerFollower(follower)
user.removeFollowerFollower(follower)
user.countFollowerFollowers()
user.hasFollowerFollower(follower)
user.createFollowerFollower({...})
```
#### Products (créés/possédés)
```javascript
user.getProducts() // Récupère les produits
user.setProducts([...])
user.addProduct(product)
user.removeProduct(product)
user.countProducts()
user.hasProduct(product)
user.createProduct({...})
```
#### UserRoleXrefs
```javascript
user.getUserRoleXrefs() // Récupère les UserRoleXrefs
user.setUserRoleXrefs([...])
user.addUserRoleXref(xref)
user.removeUserRoleXref(xref)
user.countUserRoleXrefs()
user.hasUserRoleXref(xref)
user.createUserRoleXref({...})
```
### Instance Methods
```javascript
user.validPassword(password) // Valide le mot de passe
user.setPassword(password) // Définit le mot de passe (hash)
user.generateJWT() // Génère un JWT
user.toAuthJSON() // Retourne objet auth
user.toJSONFor() // Formate en JSON
user.toProfileJSONFor() // Formate profil en JSON
```
---
## Comment
### Belongs-To
#### Article
```javascript
comment.getArticle() // Récupère l'article
comment.setArticle(article) // Définit l'article
comment.createArticle({...}) // Crée un article
```
#### Author (User)
```javascript
comment.getAuthor() // Récupère l'auteur
comment.setAuthor(user) // Définit l'auteur
comment.createAuthor({...}) // Crée un auteur
```
### Instance Methods
```javascript
comment.toJSONFor() // Formate en JSON
```
---
## Favorite
### Belongs-To
#### User
```javascript
favorite.getUser() // Récupère l'utilisateur
favorite.setUser(user) // Définit l'utilisateur
favorite.createUser({...}) // Crée un utilisateur
```
#### Article
```javascript
favorite.getArticle() // Récupère l'article
favorite.setArticle(article) // Définit l'article
favorite.createArticle({...}) // Crée un article
```
### Instance Methods
```javascript
favorite.toJSONFor() // Formate en JSON
```
---
## Follower
### Belongs-To
#### User (l'utilisateur suivi)
```javascript
follower.getUser() // Récupère l'utilisateur suivi
follower.setUser(user) // Définit l'utilisateur suivi
follower.createUser({...}) // Crée un utilisateur
```
#### Follower (l'utilisateur qui suit) - alias 'follower'
```javascript
follower.getFollower() // Récupère celui qui suit
follower.setFollower(user) // Définit celui qui suit
follower.createFollower({...}) // Crée un utilisateur qui suit
```
### Instance Methods
```javascript
follower.toJSONFor() // Formate en JSON
```
---
## Product
### Many-to-Many (belongsToMany)
#### Categories via `ProductCategoryXref`
Alias: `productCategories`
```javascript
product.getProductCategories() // Récupère les catégories
product.setProductCategories([...])
product.addProductCategory(category)
product.removeProductCategory(category)
product.countProductCategories()
product.hasProductCategory(category)
```
#### Tags via `ProductTagXref`
Alias: `productTags`
```javascript
product.getProductTags() // Récupère les tags
product.setProductTags([...])
product.addProductTag(tag)
product.removeProductTag(tag)
product.countProductTags()
product.hasProductTag(tag)
```
### One-to-Many (hasMany)
#### ProductCategoryXrefs
```javascript
product.getProductCategoryXrefs() // Récupère les liens catégories
product.setProductCategoryXrefs([...])
product.addProductCategoryXref(xref)
product.removeProductCategoryXref(xref)
product.countProductCategoryXrefs()
product.hasProductCategoryXref(xref)
product.createProductCategoryXref({...})
```
#### ProductTagXrefs
```javascript
product.getProductTagXrefs() // Récupère les liens tags
product.setProductTagXrefs([...])
product.addProductTagXref(xref)
product.removeProductTagXref(xref)
product.countProductTagXrefs()
product.hasProductTagXref(xref)
product.createProductTagXref({...})
```
### Belongs-To
#### Brand
```javascript
product.getBrand() // Récupère la marque
product.setBrand(brand) // Définit la marque
product.createBrand({...}) // Crée une marque
```
#### Owner (User)
```javascript
product.getOwner() // Récupère le propriétaire
product.setOwner(user) // Définit le propriétaire
product.createOwner({...}) // Crée un propriétaire
```
#### Packaging
```javascript
product.getPackaging() // Récupère l'emballage
product.setPackaging(packaging) // Définit l'emballage
product.createPackaging({...}) // Crée un emballage
```
---
## Brand
### One-to-Many (hasMany)
#### Products
```javascript
brand.getProducts() // Récupère les produits
brand.setProducts([...])
brand.addProduct(product)
brand.removeProduct(product)
brand.countProducts()
brand.hasProduct(product)
brand.createProduct({...})
```
---
## Category
### Many-to-Many (belongsToMany)
#### Products via `ProductCategoryXref`
Alias: `productIdProducts`
```javascript
category.getProductIdProducts() // Récupère les produits
category.setProductIdProducts([...])
category.addProductIdProduct(product)
category.removeProductIdProduct(product)
category.countProductIdProducts()
category.hasProductIdProduct(product)
```
### One-to-Many (hasMany)
#### ProductCategoryXrefs
```javascript
category.getProductCategoryXrefs() // Récupère les liens produits
category.setProductCategoryXrefs([...])
category.addProductCategoryXref(xref)
category.removeProductCategoryXref(xref)
category.countProductCategoryXrefs()
category.hasProductCategoryXref(xref)
category.createProductCategoryXref({...})
```
---
## Packaging
### One-to-Many (hasMany)
#### Products
```javascript
packaging.getProducts() // Récupère les produits
packaging.setProducts([...])
packaging.addProduct(product)
packaging.removeProduct(product)
packaging.countProducts()
packaging.hasProduct(product)
packaging.createProduct({...})
```
---
## TagList
### Belongs-To
#### Article
```javascript
tagList.getArticle() // Récupère l'article
tagList.setArticle(article) // Définit l'article
tagList.createArticle({...}) // Crée un article
```
#### Tag
Alias: `tagNameTag`
```javascript
tagList.getTagNameTag() // Récupère le tag
tagList.setTagNameTag(tag) // Définit le tag
tagList.createTagNameTag({...}) // Crée un tag
```
---
## Role
### Many-to-Many (belongsToMany)
#### Users via `UserRoleXref`
Alias: `userIdUserUserRoleXrefs`
```javascript
role.getUserIdUserUserRoleXrefs() // Récupère les utilisateurs
role.setUserIdUserUserRoleXrefs([...])
role.addUserIdUserUserRoleXref(user)
role.removeUserIdUserUserRoleXref(user)
role.countUserIdUserUserRoleXrefs()
role.hasUserIdUserUserRoleXref(user)
```
### One-to-Many (hasMany)
#### UserRoleXrefs
```javascript
role.getUserRoleXrefs() // Récupère les UserRoleXrefs
role.setUserRoleXrefs([...])
role.addUserRoleXref(xref)
role.removeUserRoleXref(xref)
role.countUserRoleXrefs()
role.hasUserRoleXref(xref)
role.createUserRoleXref({...})
```
---
## UserRoleXref
### Belongs-To
#### User
```javascript
xref.getUser() // Récupère l'utilisateur
xref.setUser(user) // Définit l'utilisateur
xref.createUser({...}) // Crée un utilisateur
```
#### Role
```javascript
xref.getRole() // Récupère le rôle
xref.setRole(role) // Définit le rôle
xref.createRole({...}) // Crée un rôle
```
---
## ProductCategoryXref
### Belongs-To
#### Product
```javascript
xref.getProduct() // Récupère le produit
xref.setProduct(product) // Définit le produit
xref.createProduct({...}) // Crée un produit
```
#### Category
```javascript
xref.getCategory() // Récupère la catégorie
xref.setCategory(category) // Définit la catégorie
xref.createCategory({...}) // Crée une catégorie
```
---
## ProductTagXref
### Belongs-To
#### Product
```javascript
xref.getProduct() // Récupère le produit
xref.setProduct(product) // Définit le produit
xref.createProduct({...}) // Crée un produit
```
#### Tag
Alias: `tagNameTag`
```javascript
xref.getTagNameTag() // Récupère le tag
xref.setTagNameTag(tag) // Définit le tag
xref.createTagNameTag({...}) // Crée un tag
```
---
## Notes générales
1. **Paramètres `options`** : Presque toutes les méthodes `get*()` acceptent un objet `options` pour personnaliser la requête :
```javascript
model.getAssociated({
where: { createdAt: { [Op.gte]: someDate } },
limit: 10,
offset: 0,
order: [['createdAt', 'DESC']]
});
```
2. **Transactions** : Encapsulez les opérations dans une transaction :
```javascript
const transaction = await sequelize.transaction();
await model.addAssociated(associated, { transaction });
await transaction.commit();
```
3. **Eager Loading** : Utilisez `include` pour charger les associations :
```javascript
const article = await Article.findByPk(1, {
include: [
{ model: Tag, as: 'articleTags' },
{ model: User, as: 'author' },
{ model: Comment, as: 'comments' }
]
});
```
4. **Alias vs Nom généré** : Le nom des méthodes dépend de l'alias défini. Exemple :
- Alias `articleTags` → `getArticleTags()`, `addArticleTag()`
- Alias `tagLists` → `getTagLists()`, `addTagList()`
5. **Toutes les méthodes de manipulation** acceptent un paramètre `options` final :
```javascript
await article.addTagList(tag, { transaction, validate: true });
```
---
## Seeders
### Seeder Articles (20251127-add-articles.js)
Ce seeder ajoute automatiquement 3 articles avec du contenu Lorem Ipsum et les associe à des tags d'animaux.
#### Exécution
```bash
# Exécuter tous les seeders
npx sequelize-cli db:seed:all
# Annuler un seeder spécifique
npx sequelize-cli db:seed:undo --seed 20251127-add-articles.js
# Annuler tous les seeders
npx sequelize-cli db:seed:undo:all
```
#### Articles créés
| Titre | Tags | Slug |
|-------|------|------|
| Introduction aux écosystèmes marins | Dolphin, Eagle, Wolf | introduction-aux-ecosystemes-marins |
| Les prédateurs de la savane africaine | Lion, Tiger, Wolf | les-predateurs-de-la-savane-africaine |
| L'intelligence animale au-delà de nos attentes | Eagle, Dolphin, Lion | lintelligence-animale-au-dela-de-nos-attentes |
#### Tags disponibles
- Lion
- Eagle
- Dolphin
- Tiger
- Wolf
#### Fonctionnalités du seeder
- ✅ Crée automatiquement les 5 tags s'ils n'existent pas
- ✅ Génère des slugs uniques pour chaque article
- ✅ Associe 3 tags à chaque article via la table `tagList`
- ✅ Utilise les transactions pour l'intégrité des données
- ✅ Support du rollback via `db:seed:undo`
---
## Migrations et Schéma
### Migration initiale (20251127000000-initial-schema.js)
Crée tous les 15 tables avec les contraintes CASCADE appropriées :
**Tables avec CASCADE delete activé :**
- `article` → `comment` (foreignKey: `articleId`)
- `article` → `favorite` (foreignKey: `articleId`)
- `article` → `tagList` (foreignKey: `articleId`)
- `user` → `article` (foreignKey: `authorId`)
- `user` → `comment` (foreignKey: `authorId`)
- `tag` → `tagList` (foreignKey: `tagName`)
**Exécution**
```bash
# Appliquer les migrations
npx sequelize-cli db:migrate
# Voir le statut des migrations
npx sequelize-cli db:migrate:status
# Annuler les migrations
npx sequelize-cli db:migrate:undo:all
```
---
## Configuration de la base de données
### Variables d'environnement
Le fichier `.env.development` doit contenir :
```
DB_HOST=localhost
DB_USER=root
DB_PASS=password
DB_NAME=adastra
DB_DIALECT=mysql
```
### Validation du schéma au démarrage
L'application valide automatiquement la présence du schéma au démarrage :
- ✅ Si le schéma est absent, l'app échoue avec un message d'erreur
- ✅ Si le schéma est présent, les associations sont créées
- ✅ Message d'aide fourni pour exécuter les migrations manquantes
```bash
# Démarrer l'app (validera le schéma)
npm run local # Développement avec nodemon
npm run dev # Développement
npm start # Production
```
-210
View File
@@ -1,210 +0,0 @@
# Guide Swagger - Documentation OpenAPI pour Adastra API
## Accès à la documentation
Une fois le serveur lancé, accédez à : **http://localhost:3200/api-docs**
## Exemple de documentation pour une route
Voici un exemple de comment documenter une GET route avec des paramètres de query :
```javascript
/**
* @swagger
* /api/v1/articles:
* get:
* summary: Récupère la liste des articles
* description: Retourne une liste paginée de tous les articles
* tags:
* - Articles
* parameters:
* - in: query
* name: page
* schema:
* type: integer
* default: 1
* description: Numéro de page
* - in: query
* name: limit
* schema:
* type: integer
* default: 10
* description: Nombre d'articles par page
* responses:
* 200:
* description: Liste des articles récupérée avec succès
* content:
* application/json:
* schema:
* type: object
* properties:
* data:
* type: array
* items:
* type: object
* properties:
* id:
* type: integer
* title:
* type: string
* content:
* type: string
* total:
* type: integer
* 400:
* description: Erreur de validation
* 401:
* description: Non authentifié
* 500:
* description: Erreur serveur
*/
router.get('/', async (req, res) => {
// ... logique du contrôleur
});
```
## Exemple avec POST et authentification
```javascript
/**
* @swagger
* /api/v1/articles:
* post:
* summary: Crée un nouvel article
* description: Crée un nouvel article avec les données fournies
* tags:
* - Articles
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - title
* - content
* properties:
* title:
* type: string
* example: "Mon Article"
* content:
* type: string
* example: "Contenu de l'article"
* tags:
* type: array
* items:
* type: string
* example: ["tag1", "tag2"]
* responses:
* 201:
* description: Article créé avec succès
* content:
* application/json:
* schema:
* type: object
* properties:
* id:
* type: integer
* title:
* type: string
* content:
* type: string
* 400:
* description: Données invalides
* 401:
* description: Non authentifié
*/
router.post('/', authenticate, async (req, res) => {
// ... logique du contrôleur
});
```
## Exemple avec paramètres URL
```javascript
/**
* @swagger
* /api/v1/articles/{id}:
* get:
* summary: Récupère un article par ID
* tags:
* - Articles
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: ID de l'article
* responses:
* 200:
* description: Article récupéré avec succès
* 404:
* description: Article non trouvé
* 500:
* description: Erreur serveur
*/
router.get('/:id', async (req, res) => {
// ... logique du contrôleur
});
```
## Types de schémas courants
### Objets simples
```javascript
schema: {
type: object
properties:
id: { type: integer }
name: { type: string }
email: { type: string, format: email }
age: { type: integer, minimum: 0 }
isActive: { type: boolean }
}
```
### Arrays
```javascript
schema: {
type: array
items:
type: object
properties:
id: { type: integer }
name: { type: string }
}
```
### Enums
```javascript
schema: {
type: string
enum: ["pending", "approved", "rejected"]
}
```
## Tags disponibles
Les tags disponibles dans la config Swagger sont :
- **Articles** - Gestion des articles
- **Users** - Gestion des utilisateurs
- **Products** - Gestion des produits
- **Clans** - Gestion des clans
- **Hero Wars** - Données Hero Wars
## Instructions générales
1. **Placez les commentaires JSDoc directement au-dessus de la définition de la route**
2. **Utilisez le tag `@swagger`** pour indiquer que c'est de la documentation OpenAPI
3. **Commencez par le chemin d'accès** : `/api/v1/articles`, `/api/v1/articles/{id}`, etc.
4. **Spécifiez la méthode HTTP** : `get`, `post`, `put`, `delete`, `patch`
5. **Incluez toujours** : `summary`, `description`, `tags`, `responses`
6. **Pour les routes protégées**, utilisez `security: [{ bearerAuth: [] }]` ou `security: [{ apiKeyAuth: [] }]`
## Pour plus de détails
Consultez la documentation officielle : https://swagger.io/specification/
---
**Note** : Une fois que vous aurez ajouté les commentaires Swagger à vos routes, redémarrez le serveur et accédez à `/api-docs` pour voir la documentation mise à jour.
+387
View File
@@ -0,0 +1,387 @@
# Guide de mise en production — Adastra
Serveur cible : VPS Debian 11, Apache, MySQL déjà installés.
Stack : Node.js/Express (API) + Angular 18 (frontend).
---
## Table des matières
1. [Prérequis serveur](#1-prérequis-serveur)
2. [Base de données MySQL](#2-base-de-données-mysql)
3. [Base de données MongoDB](#3-base-de-données-mongodb)
4. [Déploiement de l'API](#4-déploiement-de-lapi)
5. [Déploiement du frontend](#5-déploiement-du-frontend)
6. [Configuration Apache](#6-configuration-apache)
7. [HTTPS avec Let's Encrypt](#7-https-avec-lets-encrypt)
8. [PM2 — démarrage automatique](#8-pm2--démarrage-automatique)
9. [Procédure de mise à jour](#9-procédure-de-mise-à-jour)
---
## 1. Prérequis serveur
### Node.js (via nvm — recommandé)
```bash
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
source ~/.bashrc
nvm install 20
nvm use 20
nvm alias default 20
node -v # doit afficher v20.x.x
```
### PM2 (gestionnaire de process Node)
```bash
npm install -g pm2
```
### MongoDB
L'API utilise à la fois MySQL et MongoDB. MongoDB n'est pas fourni par les dépôts Debian 11 par défaut.
```bash
# Clé GPG et dépôt MongoDB 7
curl -fsSL https://www.mongodb.org/static/pgp/server-7.0.asc | \
gpg -o /usr/share/keyrings/mongodb-server-7.0.gpg --dearmor
echo "deb [ signed-by=/usr/share/keyrings/mongodb-server-7.0.gpg ] https://repo.mongodb.org/apt/debian bullseye/mongodb-org/7.0 main" \
| tee /etc/apt/sources.list.d/mongodb-org-7.0.list
apt update && apt install -y mongodb-org
systemctl enable --now mongod
mongod --version
```
### Modules Apache nécessaires
```bash
a2enmod proxy proxy_http rewrite headers ssl
systemctl restart apache2
```
---
## 2. Base de données MySQL
MySQL est déjà installé. Créer la base et l'utilisateur dédié.
```sql
-- En tant que root MySQL
CREATE DATABASE adastra_prod CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'adastra'@'localhost' IDENTIFIED BY 'MOT_DE_PASSE_FORT';
GRANT ALL PRIVILEGES ON adastra_prod.* TO 'adastra'@'localhost';
FLUSH PRIVILEGES;
```
Les migrations Sequelize (voir [section 4](#4-déploiement-de-lapi)) créeront le schéma.
---
## 3. Base de données MongoDB
```bash
# Créer la base et l'utilisateur applicatif
mongosh <<'EOF'
use adastradb
db.createUser({
user: "adastra",
pwd: "MOT_DE_PASSE_FORT",
roles: [{ role: "readWrite", db: "adastradb" }]
})
EOF
```
Activer l'authentification MongoDB si elle n'est pas encore activée :
```bash
# /etc/mongod.conf
security:
authorization: enabled
```
```bash
systemctl restart mongod
```
La `MONGODB_URI` dans le `.env` devient alors :
```
MONGODB_URI=mongodb://adastra:MOT_DE_PASSE_FORT@localhost:27017/adastradb
```
---
## 4. Déploiement de l'API
### Récupérer le code
```bash
mkdir -p /var/www
cd /var/www
git clone <url-du-repo> adastra_api
cd adastra_api
git checkout master # ou la branche de production
npm install --omit=dev
```
### Fichier d'environnement
```bash
cp config/env/.env.example config/env/.env.production
nano config/env/.env.production
```
Valeurs à renseigner obligatoirement :
```dotenv
APP_ENV=production
NODE_ENV=production
SERVER_PORT=3200
# Secrets — générer avec :
# node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"
SECRET=<64-octets-hex>
SESSION_SECRET=<64-octets-hex>
# MySQL
MYSQL_DBNAME=adastra_prod
MYSQL_DBUSER=adastra
MYSQL_DBPASS=MOT_DE_PASSE_FORT
MYSQL_DBHOST=localhost
MYSQL_DBPORT=3306
MYSQL_DBTYPE=mysql
# MongoDB
MONGODB_URI=mongodb://adastra:MOT_DE_PASSE_FORT@localhost:27017/adastradb
# CORS — domaine exact du frontend (sans slash final)
ALLOWED_ORIGINS=https://adastra.example.com
# Proxy Apache → Node = 1 saut
TRUST_PROXY=1
# Sendgrid (si les emails sont utilisés)
SENDGRID_API_KEY=
SENDGRID_FROM_MAIL=
SENDGRID_TO_MAIL=
```
### Migrations et seeds initiaux
À exécuter **une seule fois** lors du premier déploiement :
```bash
cd /var/www/adastra_api
APP_ENV=production npx sequelize-cli db:migrate
APP_ENV=production npx sequelize-cli db:seed --seed 20251126-add-roles.js
APP_ENV=production npm run create:user # crée le premier compte admin
APP_ENV=production npx sequelize-cli db:seed --seed 20251127-add-articles.js
```
### Démarrer avec PM2
```bash
cd /var/www/adastra_api
pm2 start app.js --name adastra-api
pm2 save
```
Vérifier que l'API répond :
```bash
curl http://localhost:3200/api/health # ou tout endpoint public
```
---
## 5. Déploiement du frontend
Le build se fait **en local** (ou sur un serveur de CI), puis le répertoire `dist/` est transféré sur le serveur.
### Build en local
```bash
cd /chemin/local/adastra_app
npm install
npm run build # production build → dist/adastra_angular/browser/
```
> **Important :** vérifier que `src/environments/environment.ts` pointe vers l'URL de l'API de production avant de builder.
> ```typescript
> apiBaseUrl: 'https://api.adastra.example.com/api'
> ```
### Transfert sur le serveur
```bash
# Depuis la machine locale
rsync -avz --delete dist/adastra_angular/browser/ \
user@serveur:/var/www/adastra_app/
```
Ou avec scp :
```bash
scp -r dist/adastra_angular/browser/* user@serveur:/var/www/adastra_app/
```
### Permissions
```bash
# Sur le serveur
chown -R www-data:www-data /var/www/adastra_app
chmod -R 755 /var/www/adastra_app
```
---
## 6. Configuration Apache
Architecture recommandée : deux sous-domaines séparés.
| Sous-domaine | Rôle |
|---|---|
| `adastra.example.com` | Frontend Angular (fichiers statiques) |
| `api.adastra.example.com` | API Node.js (reverse proxy) |
### VirtualHost frontend — `/etc/apache2/sites-available/adastra-app.conf`
```apache
<VirtualHost *:80>
ServerName adastra.example.com
DocumentRoot /var/www/adastra_app
<Directory /var/www/adastra_app>
Options -Indexes
AllowOverride None
Require all granted
# Angular HTML5 routing — renvoie tout vers index.html
FallbackResource /index.html
</Directory>
# Compression et cache navigateur
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/css application/javascript application/json
</IfModule>
<FilesMatch "\.(js|css|woff2|png|jpg|svg)$">
Header set Cache-Control "max-age=31536000, public, immutable"
</FilesMatch>
<FilesMatch "index\.html$">
Header set Cache-Control "no-cache, no-store, must-revalidate"
</FilesMatch>
ErrorLog ${APACHE_LOG_DIR}/adastra-app-error.log
CustomLog ${APACHE_LOG_DIR}/adastra-app-access.log combined
</VirtualHost>
```
### VirtualHost API — `/etc/apache2/sites-available/adastra-api.conf`
```apache
<VirtualHost *:80>
ServerName api.adastra.example.com
ProxyPreserveHost On
ProxyPass / http://127.0.0.1:3200/
ProxyPassReverse / http://127.0.0.1:3200/
# Transmet l'IP réelle du client à Node (requis pour TRUST_PROXY=1)
RequestHeader set X-Forwarded-For "%{REMOTE_ADDR}s"
RequestHeader set X-Forwarded-Proto "http"
ErrorLog ${APACHE_LOG_DIR}/adastra-api-error.log
CustomLog ${APACHE_LOG_DIR}/adastra-api-access.log combined
</VirtualHost>
```
### Activer les vhosts
```bash
a2ensite adastra-app.conf
a2ensite adastra-api.conf
apache2ctl configtest # doit afficher "Syntax OK"
systemctl reload apache2
```
---
## 7. HTTPS avec Let's Encrypt
```bash
apt install -y certbot python3-certbot-apache
certbot --apache -d adastra.example.com -d api.adastra.example.com
```
Certbot modifie automatiquement les vhosts pour le HTTPS et programme le renouvellement.
Vérifier le renouvellement automatique :
```bash
certbot renew --dry-run
```
Après activation du HTTPS, penser à mettre à jour dans le `.env.production` :
```dotenv
ALLOWED_ORIGINS=https://adastra.example.com
```
Et dans `environment.ts` avant de rebuilder le frontend :
```typescript
apiBaseUrl: 'https://api.adastra.example.com/api'
```
---
## 8. PM2 — démarrage automatique
Pour que l'API redémarre automatiquement après un reboot :
```bash
pm2 startup # affiche une commande à exécuter en root — la copier/coller
pm2 save
```
Commandes PM2 utiles :
```bash
pm2 list # état des process
pm2 logs adastra-api # logs en temps réel
pm2 restart adastra-api # redémarrage manuel
pm2 stop adastra-api # arrêt
```
---
## 9. Procédure de mise à jour
### Mettre à jour l'API
```bash
cd /var/www/adastra_api
git pull origin master
npm install --omit=dev
# Si des migrations ont été ajoutées
APP_ENV=production npx sequelize-cli db:migrate
pm2 restart adastra-api
```
### Mettre à jour le frontend
En local :
```bash
cd /chemin/local/adastra_app
git pull origin master
npm install
npm run build
rsync -avz --delete dist/adastra_angular/browser/ user@serveur:/var/www/adastra_app/
```
Aucun redémarrage Apache nécessaire — les fichiers statiques sont servis directement.
+92
View File
@@ -0,0 +1,92 @@
# Variables d'environnement — Adastra API
Référence pour les variables sensibles ou non-triviales. Pour les valeurs par défaut et la liste exhaustive, voir `config/env/.env.example`.
---
## TRUST_PROXY
**Fichier concerné :** `src/createApp.js`
**Utilisé par :** `express-rate-limit` (IP réelle du client), `req.ip`
### Principe
Indique à Express combien de proxies se trouvent entre internet et le process Node. Sans cette valeur, `req.ip` retourne l'IP du proxy (`127.0.0.1`) au lieu de l'IP du client réel, ce qui rend le rate limiting inefficace (toutes les requêtes semblent venir de la même IP).
### Valeurs selon la topologie
| Environnement | Topologie | Valeur |
|---|---|---|
| Dev local | Node exposé directement, pas de proxy | ne pas setter (variable absente) |
| Prod — serveur seul | nginx → Node sur le même serveur | `1` |
| Prod — cloud avec LB | nginx → load balancer → Node | `2` |
| Confiance totale | Tous les proxies de la chaîne `X-Forwarded-For` | `true` (déconseillé) |
> **Règle générale :** `TRUST_PROXY` = nombre de proxies entre internet et Node.
### Vérification empirique
Ajouter temporairement cette route et appeler `/debug-ip` depuis l'extérieur :
```js
app.get('/debug-ip', (req, res) => {
res.json({
ip: req.ip,
ips: req.ips,
xForwardedFor: req.headers['x-forwarded-for']
});
});
```
- `req.ip` retourne l'IP réelle du client → valeur correcte
- `req.ip` retourne `127.0.0.1` ou l'IP du proxy → augmenter de 1
- `req.ips` est vide malgré un proxy → idem
**Supprimer la route après vérification.**
### Implémentation actuelle
```js
if (process.env.TRUST_PROXY) {
app.set('trust proxy', parseInt(process.env.TRUST_PROXY, 10) || process.env.TRUST_PROXY);
}
```
Le cast `parseInt(...) || process.env.TRUST_PROXY` permet de passer soit un entier (`"1"``1`) soit la chaîne `"true"`.
---
## SECRET
**Fichier concerné :** `src/config/index.js`
**Utilisé par :** signature JWT, HMAC-SHA256 des API keys, secret de session Express
Valeur obligatoire en production — le process refuse de démarrer si elle est absente. Générer avec :
```bash
node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"
```
---
## SESSION_SECRET
**Fichier concerné :** `src/createApp.js`
**Utilisé par :** `express-session`
Indépendant de `SECRET` pour limiter la surface d'impact si l'un des deux est compromis. Même commande de génération que `SECRET`. Fallback sur `SECRET` si absent (rétrocompatibilité).
---
## ALLOWED_ORIGINS
**Fichier concerné :** `src/createApp.js`
**Utilisé par :** CORS
Liste d'origines autorisées, séparées par des virgules. Si absente ou vide, CORS refuse toutes les origines cross-domain.
```
ALLOWED_ORIGINS=http://localhost:4200,http://localhost:4300
```
En production, indiquer uniquement le domaine du frontend (ex: `https://adastra.example.com`).
+338
View File
@@ -0,0 +1,338 @@
# Installation de Gitea sur Debian 11 avec Apache/ISPConfig
Gitea sera accessible sur `https://git.unespace.com` en reverse proxy devant Apache.
Les repos existants dans `/opt/git/` seront migrés à la fin.
---
## Table des matières
1. [Prérequis Apache](#1-prérequis-apache)
2. [Créer l'utilisateur système Gitea](#2-créer-lutilisateur-système-gitea)
3. [Télécharger et installer le binaire](#3-télécharger-et-installer-le-binaire)
4. [Créer les répertoires et la configuration](#4-créer-les-répertoires-et-la-configuration)
5. [Créer le service systemd](#5-créer-le-service-systemd)
6. [Démarrer Gitea et finaliser l'installation](#6-démarrer-gitea-et-finaliser-linstallation)
7. [Configurer le VHost dans ISPConfig](#7-configurer-le-vhost-dans-ispconfig)
8. [Migrer les repos existants](#8-migrer-les-repos-existants)
9. [Mettre à jour les remotes locaux](#9-mettre-à-jour-les-remotes-locaux)
---
## 1. Prérequis Apache
Activer les modules proxy si ce n'est pas déjà fait :
```bash
a2enmod proxy proxy_http headers
systemctl reload apache2
```
---
## 2. Créer l'utilisateur système Gitea
Gitea a besoin de son propre utilisateur Unix. On le crée **sans home** et **sans shell** pour limiter la surface d'attaque, mais on lui donne accès au répertoire des repos existants :
```bash
adduser --system --shell /bin/bash --gecos 'Gitea' --group --home /opt/gitea gitea
```
Pour que Gitea puisse lire les repos appartenant à l'utilisateur `git`, on ajoute `gitea` au groupe `git` :
```bash
usermod -aG git gitea
```
> Si tes repos dans `/opt/git/` appartiennent à `git:git`, cette étape est nécessaire pour la migration.
---
## 3. Télécharger et installer le binaire
Récupérer la dernière version stable depuis la page des releases Gitea. À adapter avec le numéro de version actuel :
```bash
# Vérifier la dernière version sur https://github.com/go-gitea/gitea/releases
GITEA_VERSION="1.26.1"
wget -O /tmp/gitea https://dl.gitea.com/gitea/${GITEA_VERSION}/gitea-${GITEA_VERSION}-linux-amd64
chmod +x /tmp/gitea
mv /tmp/gitea /usr/local/bin/gitea
# Vérifier
gitea --version
```
---
## 4. Créer les répertoires et la configuration
```bash
# Répertoires de données
mkdir -p /opt/gitea/{custom,data,log,repositories}
chown -R gitea:gitea /opt/gitea
chmod -R 750 /opt/gitea
# Répertoire de configuration
mkdir -p /etc/gitea
chown root:gitea /etc/gitea
chmod 770 /etc/gitea
```
Créer le fichier de configuration `/etc/gitea/app.ini` :
```bash
cat > /etc/gitea/app.ini << 'EOF'
APP_NAME = Adastra Git
RUN_USER = gitea
RUN_MODE = prod
WORK_PATH = /opt/gitea
[server]
PROTOCOL = http
HTTP_ADDR = 127.0.0.1
HTTP_PORT = 3030
DOMAIN = git.unespace.com
ROOT_URL = https://git.unespace.com/
DISABLE_SSH = false
SSH_PORT = 22
SSH_DOMAIN = git.unespace.com
START_SSH_SERVER = false
[database]
DB_TYPE = sqlite3
PATH = /opt/gitea/data/gitea.db
[repository]
ROOT = /opt/gitea/repositories
[security]
INSTALL_LOCK = false
SECRET_KEY =
INTERNAL_TOKEN =
[log]
ROOT_PATH = /opt/gitea/log
MODE = file
LEVEL = warn
[service]
DISABLE_REGISTRATION = false
REQUIRE_SIGNIN_VIEW = false
EOF
chown root:gitea /etc/gitea/app.ini
chmod 660 /etc/gitea/app.ini
```
> **Note :** `INSTALL_LOCK = false` permet à l'installeur web de fonctionner au premier démarrage. Il passera à `true` automatiquement après.
> Le port `3030` est choisi pour éviter tout conflit avec l'API Node.js (3200) et d'autres services éventuels.
---
## 5. Créer le service systemd
```bash
cat > /etc/systemd/system/gitea.service << 'EOF'
[Unit]
Description=Gitea (Git with a cup of tea)
After=network.target
[Service]
Type=simple
User=gitea
Group=gitea
WorkingDirectory=/opt/gitea
ExecStart=/usr/local/bin/gitea web --config /etc/gitea/app.ini
Restart=always
RestartSec=2s
Environment=USER=gitea HOME=/opt/gitea GITEA_WORK_DIR=/opt/gitea
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable gitea
systemctl start gitea
systemctl status gitea
```
Vérifier que Gitea écoute bien sur le bon port :
```bash
ss -tlnp | grep 3030
```
---
## 6. Démarrer Gitea et finaliser l'installation
À ce stade Gitea tourne mais l'interface d'installation web n'est pas encore accessible publiquement (le VHost n'est pas créé). On peut tester depuis le serveur lui-même :
```bash
curl -s http://127.0.0.1:3030 | head -5
# doit retourner du HTML
```
L'installation web sera finalisée **après** la configuration du VHost (étape 7).
---
## 7. Configurer le VHost dans ISPConfig
### 7.1 Créer le site web
Dans ISPConfig → **Sites → Ajouter un site web** :
| Champ | Valeur |
|---|---|
| Domain | `git.unespace.com` |
| Document Root | `/var/www/gitea-placeholder` (peu importe, ne sera pas utilisé) |
| Auto-subdomain | Aucun |
| Redirect type | Laisser vide |
Sauvegarder.
### 7.2 Activer le SSL Let's Encrypt
Dans l'onglet **SSL** du site que tu viens de créer :
- Cocher **SSL**
- Cocher **Let's Encrypt**
- Sauvegarder
ISPConfig demande le certificat automatiquement. Attendre que le statut passe à "actif" (peut prendre 1-2 minutes).
### 7.3 Ajouter les directives reverse proxy
Dans l'onglet **Options** (ou **Directives Apache** selon ta version d'ISPConfig), dans le champ **Apache Directives**, coller :
```apache
ProxyPreserveHost On
ProxyPass / http://127.0.0.1:3030/
ProxyPassReverse / http://127.0.0.1:3030/
RequestHeader set X-Forwarded-Proto "https"
RequestHeader set X-Real-IP %{REMOTE_ADDR}s
```
Répéter l'opération dans le champ **Apache Directives SSL** (le champ équivalent pour le VHost HTTPS) avec les mêmes lignes.
Sauvegarder. ISPConfig recharge Apache automatiquement.
### 7.4 Vérifier
```bash
curl -I https://git.unespace.com
# HTTP/2 200 ou redirection vers l'installeur Gitea
```
### 7.5 Finaliser l'installation Gitea via le navigateur
Ouvre `https://git.unespace.com` dans ton navigateur. L'installeur Gitea s'affiche.
Paramètres recommandés :
| Champ | Valeur |
|---|---|
| Base URL | `https://git.unespace.com/` |
| SSH Server Domain | `git.unespace.com` |
| SSH Port | `22` |
| HTTP Port | `3030` |
| Database | SQLite3 (déjà configuré) |
| Compte administrateur | Remplir nom/email/mot de passe |
Cliquer **Installer Gitea**. L'installeur met à jour `app.ini` et redirige vers la page d'accueil.
### 7.6 Ajouter ta clé SSH publique
Pour pouvoir pousser via SSH, ta clé publique doit être enregistrée dans Gitea.
Depuis ta machine locale, récupère ta clé publique :
```bash
cat ~/.ssh/id_rsa.pub
# ou si tu utilises ed25519 :
cat ~/.ssh/id_ed25519.pub
```
Dans Gitea : **ton avatar → Paramètres → Clés SSH/GPG → Ajouter une clé** — colle le contenu et valide.
---
## 8. Migrer les repos existants
Tes repos actuels sont dans `/opt/git/` sous forme de repos bare (ex. `/opt/git/adastra_api.git`).
### Option A — Via l'interface Gitea (recommandé)
Pour chaque repo :
1. Dans Gitea, créer un nouveau dépôt vide (même nom, sans initialisation)
2. Depuis ta machine locale, ajouter le nouveau remote et pousser :
```bash
cd /chemin/local/adastra_api
git remote add gitea gitea@git.unespace.com:TON_USERNAME/adastra_api.git
git push gitea --all
git push gitea --tags
```
> **Note :** l'utilisateur SSH est `gitea` (le user système sous lequel tourne Gitea), pas `git`.
### Option B — Copie directe côté serveur
Si tu veux éviter de tout repousser depuis le local, tu peux cloner les repos bare existants directement dans le répertoire de Gitea :
```bash
# Pour chaque repo — adapter le nom d'utilisateur Gitea
su - gitea -s /bin/bash
cd /opt/gitea/repositories/TON_USERNAME/
# Clone bare du repo existant
git clone --mirror /opt/git/adastra_api.git adastra_api.git
git clone --mirror /opt/git/adastra_app.git adastra_app.git
```
Puis dans Gitea → Admin panel → **Git Repositories****Git Resync** pour que Gitea indexe les repos copiés.
---
## 9. Mettre à jour les remotes locaux
Une fois les repos migrés, mettre à jour les remotes dans tes clones locaux :
```bash
# Vérifier les remotes actuels
git remote -v
# Remplacer l'ancien remote origin
git remote set-url origin gitea@git.unespace.com:TON_USERNAME/adastra_api.git
git remote set-url origin gitea@git.unespace.com:TON_USERNAME/adastra_app.git
# Vérifier
git remote -v
git fetch origin
```
> L'ancien serveur Git continue de fonctionner en parallèle jusqu'à ce que tu sois certain que tout est migré.
---
## Commandes d'administration utiles
```bash
# Statut du service
systemctl status gitea
# Logs en temps réel
journalctl -u gitea -f
# Mise à jour de Gitea (remplacer le binaire puis redémarrer)
systemctl stop gitea
wget -O /usr/local/bin/gitea https://dl.gitea.com/gitea/NEW_VERSION/gitea-NEW_VERSION-linux-amd64
chmod +x /usr/local/bin/gitea
systemctl start gitea
```
@@ -0,0 +1,64 @@
# Use express-validator for input validation at the route level
* Status: accepted
* Date: 2026-04-26
## Context and Problem Statement
API endpoints accepted user input without validating format, length, or type. A `fieldValidation` helper existed in several controllers but only checked for empty values, was duplicated across three controllers (users, articles, products), and had a critical bug: it called `next(error)` without `return`, so execution continued even when a field was invalid.
## Decision Drivers
* Input validation must happen before business logic and must halt execution on failure.
* Validation rules should be declared at the route level, not buried inside controller logic.
* The solution must be consistent and reusable across the codebase without adding a framework wrapper.
## Considered Options
* Keep and fix the `fieldValidation` helper
* Joi with a validation middleware wrapper
* express-validator with route-level chains
## Decision Outcome
Chosen option: "express-validator", because it integrates natively as Express middleware, allows validation rules to be declared directly in route definitions, and requires no wrapper function. A shared `validate.js` middleware reads `validationResult` and returns HTTP 422 with a structured error body if any rule fails.
Structure:
- `src/middlewares/validate.js` — shared middleware that checks `validationResult` and short-circuits on errors
- `src/middlewares/validators/users.validators.js` — rule sets for user endpoints
- `src/middlewares/validators/articles.validators.js` — rule sets for article endpoints
- Rules applied at route level: `router.post('/', rules, validate, controller)`
Initial coverage: CMS user endpoints (register, login, update profile/email/username/role) and article endpoints (create, update). Products and tags endpoints retain the old `fieldValidation` pattern pending a separate ecommerce validation pass.
### Positive Consequences
* Validation always runs before the controller — no risk of continuing on invalid input.
* Rules are co-located with routes, making the contract of each endpoint visible at a glance.
* Reusable rule sets can be composed and shared across routes.
* `fieldValidation` removed from users and articles controllers — no more duplicated logic.
### Negative Consequences
* Products and tags controllers still use the old `fieldValidation` pattern — inconsistency until the ecommerce pass is done.
* Validation rules must be maintained in sync with model constraints (e.g. if a column length changes, the validator must be updated manually).
## Pros and Cons of the Options
### Keep and fix fieldValidation
* Good, because no new dependency.
* Bad, because it is duplicated in every controller that needs it.
* Bad, because it belongs in controllers, not at the boundary where input arrives.
### Joi
* Good, because schema-based validation with rich type coercion.
* Bad, because requires a wrapper middleware to integrate with Express — adds boilerplate not present in the existing codebase.
### express-validator
* Good, because native Express middleware — no wrapper needed.
* Good, because rule sets are plain arrays, easy to compose and test independently.
* Good, because actively maintained with wide adoption in Express projects.
* Bad, because rules are imperative chains rather than declarative schemas, which can be verbose for complex objects.
+61
View File
@@ -0,0 +1,61 @@
# Upgrade Express from v4 to v5 and remove redundant middleware
* Status: accepted
* Date: 2026-04-28
## Context and Problem Statement
The API ran on Express 4.18, with two additional middleware packages (`method-override`, `errorhandler`) and an `asyncHandler` wrapper used across all controllers to forward async errors to Express. Express 5 was released as stable in September 2024 and addresses the async error handling gap natively.
## Decision Drivers
* Express 5 propagates promise rejections in route handlers to error middleware automatically — the `asyncHandler` wrapper is redundant noise.
* `method-override` allows clients to tunnel `PUT`/`DELETE` via `POST` using a `_method` parameter. The API is consumed exclusively by Angular's `HttpClient`, which sends correct HTTP verbs directly. The middleware has never been exercised.
* `errorhandler` provides verbose error pages in development, but a custom `exceptions.handler.js` already handles error formatting for all environments.
* Removing unused middleware reduces the attack surface and the number of dependencies to keep up to date.
## Considered Options
* Stay on Express 4, keep existing middleware
* Upgrade to Express 5, keep `method-override` and `errorhandler` for safety
* Upgrade to Express 5 and remove all three redundant pieces
## Decision Outcome
Chosen option: "upgrade to Express 5 and remove all three redundant pieces", because the migration has zero breaking changes for this codebase (no wildcard routes, no removed APIs in use) and each removed package was demonstrably unused or superseded.
Changes made:
- `express` bumped to `^5` (installed 5.2.1)
- `method-override` removed from `package.json` and `createApp.js`
- `errorhandler` removed from `package.json` and `createApp.js`
- `asyncHandler` wrapper removed from all 8 controllers and its middleware file deleted
- Controller exports changed from `asyncHandler(async (req, res, next) => { ... })` to `async (req, res, next) => { ... }`
### Positive Consequences
* Unhandled async errors now reach the error handler without any wrapper — less boilerplate in every controller.
* Two dependencies removed from the production bundle (`method-override`, `errorhandler`).
* Codebase is on a maintained major version with long-term support.
### Negative Consequences
* Express 5 is a major version bump — future middleware additions must be verified for Express 5 compatibility before adoption.
## Pros and Cons of the Options
### Stay on Express 4
* Good, because no migration effort.
* Bad, because the async error gap requires the `asyncHandler` wrapper indefinitely.
* Bad, because `method-override` and `errorhandler` remain as unused surface area.
### Upgrade to Express 5, keep existing middleware
* Good, because conservative — no behavioural changes beyond async propagation.
* Bad, because `method-override` and `errorhandler` are still dead weight.
### Upgrade to Express 5 and remove all three
* Good, because every removed package is one fewer dependency to audit and update.
* Good, because controller code is simpler without the wrapper.
* Bad, because a future Express 4-only middleware would need to be replaced — acceptable trade-off given the ecosystem has largely moved to v5.
@@ -0,0 +1,60 @@
# Extract user and application routes into a dedicated auth domain
* Status: accepted
* Date: 2026-05-01
## Context and Problem Statement
User authentication/registration routes (`/api/cms/user`) were grouped under the CMS domain, and API key application routes (`/api/skydive/applications`) were grouped under the Skydive domain. Neither resource is domain-specific content: users and applications are identity and access management concerns shared across all domains.
## Decision Drivers
* The domain prefix should reflect what a resource *is*, not which feature first needed it.
* `/api/cms/user` implies users are CMS content; `/api/skydive/applications` implies API keys are skydive data — both are misleading.
* A dedicated `auth` domain makes the security boundary explicit and easier to apply targeted middleware (rate limiting, stricter CORS, etc.) in the future.
## Considered Options
* Keep routes in their current domains
* Move only users out of CMS, leave applications in skydive
* Create a dedicated `auth` domain for both
## Decision Outcome
Chosen option: "dedicated `auth` domain", because both resources are identity/access concerns and grouping them together makes the API surface self-documenting.
Changes made:
- `src/routes/api/auth/` created with `users.routes.js`, `applications.routes.js`, and `index.js`
- `/user` removed from `src/routes/api/cms/index.js`
- `/applications` removed from `src/routes/api/skydive/index.js`
- `/auth` domain registered in `src/routes/api/index.js`
- Frontend `user.service.ts` and `applications.service.ts` updated: `_apiDomain` changed from `/cms` and `/skydive` to `/auth`
Resulting routes: `/api/auth/user/*` and `/api/auth/applications/*`.
### Positive Consequences
* API domain structure matches resource semantics — auth concerns are isolated from content and feature domains.
* A single place to tighten auth-specific middleware (rate limiting, IP allowlists, audit logging) without touching other domains.
### Negative Consequences
* Breaking change on the API surface — any client other than the Angular frontend calling the old paths must be updated.
## Pros and Cons of the Options
### Keep routes in current domains
* Good, because no migration effort.
* Bad, because the domain prefix actively misleads — users are not CMS content, applications are not skydive data.
### Move only users out of CMS
* Good, because smaller change.
* Bad, because applications in skydive remains wrong, and two related resources end up in different places.
### Dedicated auth domain
* Good, because both resources land where their semantics say they belong.
* Good, because the auth boundary is explicit and ready for future hardening.
* Bad, because it is a breaking change requiring a coordinated update of all consumers.
+997 -680
View File
File diff suppressed because it is too large Load Diff
+19 -19
View File
@@ -27,43 +27,43 @@
"dependencies": { "dependencies": {
"@sendgrid/mail": "^7.7.0", "@sendgrid/mail": "^7.7.0",
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
"body-parser": "^1.20.2", "cors": "^2.8.6",
"cors": "^2.8.5", "dotenv": "^16.6.1",
"dotenv": "^16.4.5", "ejs": "^3.1.10",
"ejs": "^3.1.9", "express": "^5",
"errorhandler": "^1.5.1", "express-jwt": "^8.5.1",
"express": "^4.18.2", "express-rate-limit": "^8.4.1",
"express-jwt": "^8.4.1", "express-session": "^1.19.0",
"express-session": "^1.18.0", "express-validator": "^7.3.2",
"helmet": "^8.1.0",
"js-yaml": "^4.1.1", "js-yaml": "^4.1.1",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.3",
"method-override": "3.0.0",
"methods": "1.1.2", "methods": "1.1.2",
"mongoose": "^6.12.6", "mongoose": "^6.12.6",
"mongoose-unique-validator": "^3.1.0", "mongoose-unique-validator": "^3.1.0",
"morgan": "^1.10.0", "morgan": "^1.10.1",
"mysql2": "^3.14.3", "mysql2": "^3.22.3",
"node-fetch": "^3.3.2", "node-fetch": "^3.3.2",
"passport": "^0.6.0", "passport": "^0.6.0",
"passport-headerapikey": "^1.2.2", "passport-headerapikey": "^1.2.2",
"passport-local": "^1.0.0", "passport-local": "^1.0.0",
"request": "^2.88.2", "request": "^2.88.2",
"sequelize": "^6.37.3", "sequelize": "^6.37.8",
"sequelize-auto": "^0.8.8", "sequelize-auto": "^0.8.8",
"slug": "^8.2.3", "slug": "^8.2.3",
"swagger-jsdoc": "^6.2.8", "swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.1", "swagger-ui-express": "^5.0.1",
"underscore": "^1.13.6", "underscore": "^1.13.8",
"uuid": "^9.0.1" "uuid": "^9.0.1"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.0.0", "@eslint/js": "^9.39.4",
"eslint": "^9.0.0", "eslint": "^9.39.4",
"globals": "^15.0.0", "globals": "^15.15.0",
"jest": "^30.3.0", "jest": "^30.3.0",
"newman": "^5.3.2", "newman": "^5.3.2",
"nodemon": "^2.0.22", "nodemon": "^3.1.14",
"sequelize-cli": "^6.6.2", "sequelize-cli": "^6.6.5",
"supertest": "^7.2.2" "supertest": "^7.2.2"
} }
} }
+6 -6
View File
@@ -180,7 +180,7 @@
}, },
{ {
"num": 21, "num": 21,
"libelle": "La vitesse de largage dun Cessna (206 ; 207) est d environ :", "libelle": "La vitesse de largage dun Cessna (206 ; 207) est denviron :",
"answers": [ "answers": [
{"index": "A", "libelle": "50 kts.", "correct": false}, {"index": "A", "libelle": "50 kts.", "correct": false},
{"index": "B", "libelle": "80 kts.", "correct": true}, {"index": "B", "libelle": "80 kts.", "correct": true},
@@ -189,7 +189,7 @@
}, },
{ {
"num": 22, "num": 22,
"libelle": "La vitesse de largage dun Cessna Caravan est d environ :", "libelle": "La vitesse de largage dun Cessna Caravan est denviron :",
"answers": [ "answers": [
{"index": "A", "libelle": "100 kts.", "correct": false}, {"index": "A", "libelle": "100 kts.", "correct": false},
{"index": "B", "libelle": "80 kts.", "correct": true}, {"index": "B", "libelle": "80 kts.", "correct": true},
@@ -839,7 +839,7 @@
}, },
{ {
"num": 94, "num": 94,
"libelle": "Au moment du largage, sassurer que lespace aérien est dégagé en dessous :", "libelle": "Au moment du largage, sassurer que lespace aérien est dégagé en dessous :",
"answers": [ "answers": [
{"index": "A", "libelle": "Est de la responsabilité du pilote, du directeur de séance et du largueur.", "correct": true}, {"index": "A", "libelle": "Est de la responsabilité du pilote, du directeur de séance et du largueur.", "correct": true},
{"index": "B", "libelle": "Est de la responsabilité du directeur de séance uniquement.", "correct": false}, {"index": "B", "libelle": "Est de la responsabilité du directeur de séance uniquement.", "correct": false},
@@ -1519,7 +1519,7 @@
}, },
{ {
"num": 167, "num": 167,
"libelle": "Pour faire un virage ’à plat :", "libelle": "Pour faire un virage ’à plat :",
"answers": [ "answers": [
{"index": "A", "libelle": "Il faut manœuvrer très rapidement à pleine vitesse.", "correct": false}, {"index": "A", "libelle": "Il faut manœuvrer très rapidement à pleine vitesse.", "correct": false},
{"index": "B", "libelle": "Il faut manœuvrer doucement et en ½ frein.", "correct": true} {"index": "B", "libelle": "Il faut manœuvrer doucement et en ½ frein.", "correct": true}
@@ -1738,7 +1738,7 @@
}, },
{ {
"num": 191, "num": 191,
"libelle": "Quappelle-t-on angle dincidence ?", "libelle": "Quappelle-t-on angle dincidence ?",
"answers": [ "answers": [
{"index": "A", "libelle": "Langle entre laile et laxe du cône de suspension.", "correct": false}, {"index": "A", "libelle": "Langle entre laile et laxe du cône de suspension.", "correct": false},
{"index": "B", "libelle": "Langle entre la trajectoire et lhorizontale.", "correct": false}, {"index": "B", "libelle": "Langle entre la trajectoire et lhorizontale.", "correct": false},
@@ -3260,7 +3260,7 @@
"libelle": "La présence dune couche nuageuse en altitude (cirrocumulus ou cirrostratus) :", "libelle": "La présence dune couche nuageuse en altitude (cirrocumulus ou cirrostratus) :",
"answers": [ "answers": [
{"index": "A", "libelle": "Indique que le mauvais temps est passé.", "correct": false}, {"index": "A", "libelle": "Indique que le mauvais temps est passé.", "correct": false},
{"index": "B", "libelle": "Annonce l arrivée dune perturbation.", "correct": true} {"index": "B", "libelle": "Annonce larrivée dune perturbation.", "correct": true}
] ]
}, },
{ {
+10 -27
View File
@@ -34,6 +34,11 @@ const question = (prompt) => {
const questionHidden = (prompt) => { const questionHidden = (prompt) => {
return new Promise((resolve) => { return new Promise((resolve) => {
process.stdout.write(prompt); process.stdout.write(prompt);
// Suppress readline's own software echo so only '*' appears
const originalWrite = rl._writeToOutput;
rl._writeToOutput = () => {};
process.stdin.setRawMode(true); process.stdin.setRawMode(true);
process.stdin.resume(); process.stdin.resume();
@@ -45,17 +50,18 @@ const questionHidden = (prompt) => {
switch (char) { switch (char) {
case '\n': case '\n':
case '\r': case '\r':
case '\u0004': case '':
process.stdin.setRawMode(false); process.stdin.setRawMode(false);
process.stdin.pause(); process.stdin.pause();
process.stdout.write('\n'); process.stdout.write('\n');
process.stdin.removeListener('data', dataListener); process.stdin.removeListener('data', dataListener);
rl._writeToOutput = originalWrite;
resolve(password); resolve(password);
break; break;
case '\u0003': case '':
process.exit(); process.exit();
break; break;
case '\u007F': case '':
case '\b': case '\b':
password = password.slice(0, -1); password = password.slice(0, -1);
process.stdout.write('\b \b'); process.stdout.write('\b \b');
@@ -116,32 +122,9 @@ const createUser = async () => {
await sequelize.authenticate(); await sequelize.authenticate();
console.log('✅ Connexion établie\n'); console.log('✅ Connexion établie\n');
// Initialize all models by calling their functions const DB = require('../src/database/mysql');
const rawDB = require('../src/database/models/mysql');
// Call each model function to initialize it
rawDB.User();
rawDB.Role();
rawDB.Article();
rawDB.Tag();
rawDB.TagList();
rawDB.UserRoleXref();
rawDB.Follower();
rawDB.Favorite();
rawDB.Comment();
rawDB.Brand();
rawDB.Category();
rawDB.Packaging();
rawDB.Product();
rawDB.ProductCategoryXref();
rawDB.ProductTagXref();
// Also initialize relationships
require('../src/database/relationships')(); require('../src/database/relationships')();
// Get the initialized models from sequelize
const DB = sequelize.models;
// Hash password with same algorithm as UserService // Hash password with same algorithm as UserService
const { salt, hash } = hashPassword(password); const { salt, hash } = hashPassword(password);
+16 -17
View File
@@ -1,10 +1,9 @@
const { ArticleService, TagService, UserService } = require('../services'); const { ArticleService, TagService, UserService } = require('../services');
const asyncHandler = require("../middlewares/asyncHandler");
const ErrorResponse = require("../utils/errorResponse"); const ErrorResponse = require("../utils/errorResponse");
const includeOptions = ArticleService.getArticleIncludeAssoc(); const includeOptions = ArticleService.getArticleIncludeAssoc();
module.exports.createArticle = asyncHandler(async (req, res, next) => { module.exports.createArticle = async (req, res, next) => {
try { try {
const loggedUser = await UserService.getLoggedUserById(req.payload.id); const loggedUser = await UserService.getLoggedUserById(req.payload.id);
@@ -47,9 +46,9 @@ module.exports.createArticle = asyncHandler(async (req, res, next) => {
} catch (err) { } catch (err) {
return next(new ErrorResponse('Something went wrong while creating article.', 500, err.message)); return next(new ErrorResponse('Something went wrong while creating article.', 500, err.message));
} }
}); };
module.exports.deleteArticle = asyncHandler(async (req, res, next) => { module.exports.deleteArticle = async (req, res, next) => {
try { try {
const { slug } = req.params; const { slug } = req.params;
const loggedUser = await UserService.getLoggedUserById(req.payload.id); const loggedUser = await UserService.getLoggedUserById(req.payload.id);
@@ -72,9 +71,9 @@ module.exports.deleteArticle = asyncHandler(async (req, res, next) => {
} catch (err) { } catch (err) {
return next(new ErrorResponse('Something went wrong while deleting article.', 500, err.message)); return next(new ErrorResponse('Something went wrong while deleting article.', 500, err.message));
} }
}); };
module.exports.getArticle = asyncHandler(async (req, res, next) => { module.exports.getArticle = async (req, res, next) => {
try { try {
const { slug } = req.params; const { slug } = req.params;
@@ -93,9 +92,9 @@ module.exports.getArticle = asyncHandler(async (req, res, next) => {
} catch (err) { } catch (err) {
return next(new ErrorResponse('Something went wrong while fetching article.', 500, err.message)); return next(new ErrorResponse('Something went wrong while fetching article.', 500, err.message));
} }
}); };
module.exports.getArticles = asyncHandler(async (req, res, next) => { module.exports.getArticles = async (req, res, next) => {
try { try {
let articles = { rows: [], count: 0 }; let articles = { rows: [], count: 0 };
const { tag, author, favorited, limit = 20, offset = 0 } = req.query; const { tag, author, favorited, limit = 20, offset = 0 } = req.query;
@@ -131,9 +130,9 @@ module.exports.getArticles = asyncHandler(async (req, res, next) => {
} catch (err) { } catch (err) {
return next(new ErrorResponse('Something went wrong while fetching articles.', 500, err.message)); return next(new ErrorResponse('Something went wrong while fetching articles.', 500, err.message));
} }
}); };
module.exports.updateArticle = asyncHandler(async (req, res, next) => { module.exports.updateArticle = async (req, res, next) => {
try { try {
const { slug } = req.params; const { slug } = req.params;
const loggedUser = await UserService.getLoggedUserById(req.payload.id); const loggedUser = await UserService.getLoggedUserById(req.payload.id);
@@ -174,9 +173,9 @@ module.exports.updateArticle = asyncHandler(async (req, res, next) => {
} catch (err) { } catch (err) {
return next(new ErrorResponse('Something went wrong while updating article.', 500, err.message)); return next(new ErrorResponse('Something went wrong while updating article.', 500, err.message));
} }
}); };
module.exports.articlesFeed = asyncHandler(async (req, res, next) => { module.exports.articlesFeed = async (req, res, next) => {
try { try {
const { limit = 3, offset = 0 } = req.query; const { limit = 3, offset = 0 } = req.query;
const loggedUser = await UserService.getLoggedUserById(req.payload.id); const loggedUser = await UserService.getLoggedUserById(req.payload.id);
@@ -207,9 +206,9 @@ module.exports.articlesFeed = asyncHandler(async (req, res, next) => {
} catch (err) { } catch (err) {
return next(new ErrorResponse('Something went wrong while fetching articles feed.', 500, err.message)); return next(new ErrorResponse('Something went wrong while fetching articles feed.', 500, err.message));
} }
}); };
module.exports.addFavoriteArticle = asyncHandler(async (req, res, next) => { module.exports.addFavoriteArticle = async (req, res, next) => {
try { try {
const { slug } = req.params; const { slug } = req.params;
const loggedUser = await UserService.getLoggedUserById(req.payload.id); const loggedUser = await UserService.getLoggedUserById(req.payload.id);
@@ -227,9 +226,9 @@ module.exports.addFavoriteArticle = asyncHandler(async (req, res, next) => {
} catch (err) { } catch (err) {
return next(new ErrorResponse('Something went wrong while adding a favorite.', 500, err.message)); return next(new ErrorResponse('Something went wrong while adding a favorite.', 500, err.message));
} }
}); };
module.exports.deleteFavoriteArticle = asyncHandler(async (req, res, next) => { module.exports.deleteFavoriteArticle = async (req, res, next) => {
try { try {
const { slug } = req.params; const { slug } = req.params;
const loggedUser = await UserService.getLoggedUserById(req.payload.id); const loggedUser = await UserService.getLoggedUserById(req.payload.id);
@@ -248,5 +247,5 @@ module.exports.deleteFavoriteArticle = asyncHandler(async (req, res, next) => {
} catch (err) { } catch (err) {
return next(new ErrorResponse('Something went wrong while removing a favorite.', 500, err.message)); return next(new ErrorResponse('Something went wrong while removing a favorite.', 500, err.message));
} }
}); };
+10 -11
View File
@@ -1,8 +1,7 @@
const { ClanService, UserService } = require('../services'); const { ClanService, UserService } = require('../services');
const asyncHandler = require("../middlewares/asyncHandler");
const ErrorResponse = require("../utils/errorResponse"); const ErrorResponse = require("../utils/errorResponse");
module.exports.createClan = asyncHandler(async (req, res, next) => { module.exports.createClan = async (req, res, next) => {
try { try {
const user = await UserService.getUserById(req.payload.id); const user = await UserService.getUserById(req.payload.id);
if (!user) { if (!user) {
@@ -27,9 +26,9 @@ module.exports.createClan = asyncHandler(async (req, res, next) => {
} catch (err) { } catch (err) {
return next(new ErrorResponse(`Something went wrong while creating clan.`, 500, err.message)); return next(new ErrorResponse(`Something went wrong while creating clan.`, 500, err.message));
} }
}); };
module.exports.deleteClan = asyncHandler(async (req, res, next) => { module.exports.deleteClan = async (req, res, next) => {
try { try {
const { clanId } = req.params; const { clanId } = req.params;
let clan = await ClanService.getClanById(clanId); let clan = await ClanService.getClanById(clanId);
@@ -45,9 +44,9 @@ module.exports.deleteClan = asyncHandler(async (req, res, next) => {
} catch (err) { } catch (err) {
return next(new ErrorResponse(`Something went wrong while deleting clan.`, 500, err.message)); return next(new ErrorResponse(`Something went wrong while deleting clan.`, 500, err.message));
} }
}); };
module.exports.getAllClans = asyncHandler(async (req, res, next) => { module.exports.getAllClans = async (req, res, next) => {
try { try {
let query = {}; let query = {};
let { limit = 25, offset = 0 } = req.query; let { limit = 25, offset = 0 } = req.query;
@@ -73,9 +72,9 @@ module.exports.getAllClans = asyncHandler(async (req, res, next) => {
} catch (err) { } catch (err) {
return next(new ErrorResponse(`Something went wrong while fetching clans.`, 500, err.message)); return next(new ErrorResponse(`Something went wrong while fetching clans.`, 500, err.message));
} }
}); };
module.exports.getClan = asyncHandler(async (req, res, next) => { module.exports.getClan = async (req, res, next) => {
try { try {
const { clanId } = req.params; const { clanId } = req.params;
let clan = await ClanService.getClanById(clanId); let clan = await ClanService.getClanById(clanId);
@@ -87,9 +86,9 @@ module.exports.getClan = asyncHandler(async (req, res, next) => {
} catch (err) { } catch (err) {
return next(new ErrorResponse(`Something went wrong while fetching clan.`, 500, err.message)); return next(new ErrorResponse(`Something went wrong while fetching clan.`, 500, err.message));
} }
}); };
module.exports.updateClan = asyncHandler(async (req, res, next) => { module.exports.updateClan = async (req, res, next) => {
try { try {
const { clanId } = req.params; const { clanId } = req.params;
let clan = await ClanService.getClanById(clanId); let clan = await ClanService.getClanById(clanId);
@@ -110,4 +109,4 @@ module.exports.updateClan = asyncHandler(async (req, res, next) => {
} catch (err) { } catch (err) {
return next(new ErrorResponse(`Something went wrong while updating clan.`, 500, err.message)); return next(new ErrorResponse(`Something went wrong while updating clan.`, 500, err.message));
} }
}); };
+4 -5
View File
@@ -1,8 +1,7 @@
const { CommentService } = require('../services'); const { CommentService } = require('../services');
const asyncHandler = require("../middlewares/asyncHandler");
const ErrorResponse = require("../utils/errorResponse"); const ErrorResponse = require("../utils/errorResponse");
module.exports.createComment = asyncHandler(async (req, res, next) => { module.exports.createComment = async (req, res, next) => {
try { try {
const { slug, title, description, body } = req.body; const { slug, title, description, body } = req.body;
const comment = await CommentService.createComment({ const comment = await CommentService.createComment({
@@ -20,9 +19,9 @@ module.exports.createComment = asyncHandler(async (req, res, next) => {
} catch (err) { } catch (err) {
return next(new ErrorResponse('Something went wrong while creating comment.', 500, err.message)); return next(new ErrorResponse('Something went wrong while creating comment.', 500, err.message));
} }
}); };
module.exports.getAllComments = asyncHandler(async (req, res, next) => { module.exports.getAllComments = async (req, res, next) => {
try { try {
const comments = await CommentService.getAllComments(); const comments = await CommentService.getAllComments();
@@ -32,4 +31,4 @@ module.exports.getAllComments = asyncHandler(async (req, res, next) => {
} catch (err) { } catch (err) {
return next(new ErrorResponse('Something went wrong while fetching comments.', 500, err.message)); return next(new ErrorResponse('Something went wrong while fetching comments.', 500, err.message));
} }
}); };
+10 -11
View File
@@ -1,8 +1,7 @@
const { MemberService, UserService } = require('../services'); const { MemberService, UserService } = require('../services');
const asyncHandler = require("../middlewares/asyncHandler");
const ErrorResponse = require("../utils/errorResponse"); const ErrorResponse = require("../utils/errorResponse");
module.exports.createMember = asyncHandler(async (req, res, next) => { module.exports.createMember = async (req, res, next) => {
try { try {
const user = await UserService.getUserById(req.payload.id); const user = await UserService.getUserById(req.payload.id);
if (!user) { if (!user) {
@@ -26,9 +25,9 @@ module.exports.createMember = asyncHandler(async (req, res, next) => {
} catch (err) { } catch (err) {
return next(new ErrorResponse(`Something went wrong while creating member.`, 500, err.message)); return next(new ErrorResponse(`Something went wrong while creating member.`, 500, err.message));
} }
}); };
module.exports.deleteMember = asyncHandler(async (req, res, next) => { module.exports.deleteMember = async (req, res, next) => {
try { try {
const { memberId } = req.params; const { memberId } = req.params;
let member = await MemberService.getMemberById(memberId); let member = await MemberService.getMemberById(memberId);
@@ -44,9 +43,9 @@ module.exports.deleteMember = asyncHandler(async (req, res, next) => {
} catch (err) { } catch (err) {
return next(new ErrorResponse(`Something went wrong while deleting member.`, 500, err.message)); return next(new ErrorResponse(`Something went wrong while deleting member.`, 500, err.message));
} }
}); };
module.exports.getAllMembers = asyncHandler(async (req, res, next) => { module.exports.getAllMembers = async (req, res, next) => {
try { try {
let query = {}; let query = {};
let { limit = 25, offset = 0 } = req.query; let { limit = 25, offset = 0 } = req.query;
@@ -72,9 +71,9 @@ module.exports.getAllMembers = asyncHandler(async (req, res, next) => {
} catch (err) { } catch (err) {
return next(new ErrorResponse(`Something went wrong while fetching members.`, 500, err.message)); return next(new ErrorResponse(`Something went wrong while fetching members.`, 500, err.message));
} }
}); };
module.exports.getMember = asyncHandler(async (req, res, next) => { module.exports.getMember = async (req, res, next) => {
try { try {
const { memberId } = req.params; const { memberId } = req.params;
let member = await MemberService.getMemberById(memberId); let member = await MemberService.getMemberById(memberId);
@@ -86,9 +85,9 @@ module.exports.getMember = asyncHandler(async (req, res, next) => {
} catch (err) { } catch (err) {
return next(new ErrorResponse(`Something went wrong while updating member.`, 500, err.message)); return next(new ErrorResponse(`Something went wrong while updating member.`, 500, err.message));
} }
}); };
module.exports.updateMember = asyncHandler(async (req, res, next) => { module.exports.updateMember = async (req, res, next) => {
try { try {
const { memberId } = req.params; const { memberId } = req.params;
let member = await MemberService.getMemberById(memberId); let member = await MemberService.getMemberById(memberId);
@@ -109,4 +108,4 @@ module.exports.updateMember = asyncHandler(async (req, res, next) => {
} catch (err) { } catch (err) {
return next(new ErrorResponse(`Something went wrong while updating member.`, 500, err.message)); return next(new ErrorResponse(`Something went wrong while updating member.`, 500, err.message));
} }
}); };
+21 -31
View File
@@ -1,6 +1,5 @@
const { ProductService } = require('../services'); const { ProductService } = require('../services');
//const { where } = require("sequelize"); //const { where } = require("sequelize");
const asyncHandler = require("../middlewares/asyncHandler");
const Product = require("../database/models/mysql/Product"); const Product = require("../database/models/mysql/Product");
const Tag = require("../database/models/mysql/Tag"); const Tag = require("../database/models/mysql/Tag");
const User = require("../database/models/mysql/User"); const User = require("../database/models/mysql/User");
@@ -23,7 +22,7 @@ const includeOptions = [
{ model: User, as: "author", attributes: { exclude: ["email", "password"] } }, { model: User, as: "author", attributes: { exclude: ["email", "password"] } },
]; ];
module.exports.getProducts = asyncHandler(async (req, res) => { module.exports.getProducts = async (req, res) => {
const { tag, author, favorited, limit = 20, offset = 0 } = req.query; const { tag, author, favorited, limit = 20, offset = 0 } = req.query;
const { loggedUser } = req; const { loggedUser } = req;
@@ -75,9 +74,9 @@ module.exports.getProducts = asyncHandler(async (req, res) => {
res res
.status(200) .status(200)
.json({ products: products.rows, productsCount: products.count }); .json({ products: products.rows, productsCount: products.count });
}); };
module.exports.getProductsByCategory = asyncHandler(async (req, res, next) => { module.exports.getProductsByCategory = async (req, res, next) => {
const { category } = req.params; const { category } = req.params;
//console.log(category); //console.log(category);
@@ -88,9 +87,9 @@ module.exports.getProductsByCategory = asyncHandler(async (req, res, next) => {
return next(new ErrorResponse("Products not found", 404)); return next(new ErrorResponse("Products not found", 404));
} }
res.status(200).json({ category: category, count: products.count, products: products.rows }); res.status(200).json({ category: category, count: products.count, products: products.rows });
}); };
module.exports.productsFeed = asyncHandler(async (req, res) => { module.exports.productsFeed = async (req, res) => {
const { loggedUser } = req; const { loggedUser } = req;
const { limit = 3, offset = 0 } = req.query; const { limit = 3, offset = 0 } = req.query;
@@ -114,9 +113,9 @@ module.exports.productsFeed = asyncHandler(async (req, res) => {
} }
res.json({ products: products.rows, productsCount: products.count }); res.json({ products: products.rows, productsCount: products.count });
}); };
module.exports.getProduct = asyncHandler(async (req, res, next) => { module.exports.getProduct = async (req, res, next) => {
const { slug: productSlug } = req.params; const { slug: productSlug } = req.params;
let product = await ProductService.getProductBySlug(productSlug); let product = await ProductService.getProductBySlug(productSlug);
@@ -129,19 +128,15 @@ module.exports.getProduct = asyncHandler(async (req, res, next) => {
product.tags = tags; product.tags = tags;
res.status(200).json({ product: product }); res.status(200).json({ product: product });
}); };
module.exports.createProduct = asyncHandler(async (req, res, next) => { module.exports.createProduct = async (req, res, next) => {
const { loggedUser } = req; const { loggedUser } = req;
fieldValidation(req.body.product.title, next);
fieldValidation(req.body.product.description, next);
fieldValidation(req.body.product.body, next);
const { title, description, body, tagList } = req.body.product; const { title, description, body, tagList } = req.body.product;
const slugInDB = await Product.findOne({ where: { slug: slug(`${title}`) } }); const slugInDB = await Product.findOne({ where: { slug: slug(`${title}`) } });
if (slugInDB) { if (slugInDB) {
next(new ErrorResponse("Title already exists", 400)); return next(new ErrorResponse("Title already exists", 400));
} }
const product = await Product.create({ const product = await Product.create({
@@ -169,9 +164,9 @@ module.exports.createProduct = asyncHandler(async (req, res, next) => {
await appendFavorites(loggedUser, product); await appendFavorites(loggedUser, product);
res.status(201).json({ product }); res.status(201).json({ product });
}); };
module.exports.deleteProduct = asyncHandler(async (req, res, next) => { module.exports.deleteProduct = async (req, res, next) => {
const { slug: productSlug } = req.params; const { slug: productSlug } = req.params;
const { loggedUser } = req; const { loggedUser } = req;
@@ -181,7 +176,7 @@ module.exports.deleteProduct = asyncHandler(async (req, res, next) => {
}); });
if (!product) { if (!product) {
next(new ErrorResponse("Product not found", 404)); return next(new ErrorResponse("Product not found", 404));
} }
if (product.authorId !== loggedUser.id) { if (product.authorId !== loggedUser.id) {
@@ -191,9 +186,9 @@ module.exports.deleteProduct = asyncHandler(async (req, res, next) => {
await product.destroy(); await product.destroy();
res.status(200).json({ product }); res.status(200).json({ product });
}); };
module.exports.updateProduct = asyncHandler(async (req, res, next) => { module.exports.updateProduct = async (req, res, next) => {
const { slug: productSlug } = req.params; const { slug: productSlug } = req.params;
const { loggedUser } = req; const { loggedUser } = req;
@@ -203,7 +198,7 @@ module.exports.updateProduct = asyncHandler(async (req, res, next) => {
}); });
if (!product) { if (!product) {
next(new ErrorResponse("Product not found", 404)); return next(new ErrorResponse("Product not found", 404));
} }
if (product.authorId !== loggedUser.id) { if (product.authorId !== loggedUser.id) {
return next(new ErrorResponse("Unauthorized", 401, "Authentication required.")); return next(new ErrorResponse("Unauthorized", 401, "Authentication required."));
@@ -229,9 +224,9 @@ module.exports.updateProduct = asyncHandler(async (req, res, next) => {
await appendFavorites(loggedUser, product); await appendFavorites(loggedUser, product);
res.status(200).json({ product }); res.status(200).json({ product });
}); };
module.exports.addFavoriteProduct = asyncHandler(async (req, res, next) => { module.exports.addFavoriteProduct = async (req, res, next) => {
const { loggedUser } = req; const { loggedUser } = req;
const { slug: productSlug } = req.params; const { slug: productSlug } = req.params;
@@ -252,9 +247,9 @@ module.exports.addFavoriteProduct = asyncHandler(async (req, res, next) => {
await appendFavorites(loggedUser, product); await appendFavorites(loggedUser, product);
res.status(200).json({ product }); res.status(200).json({ product });
}); };
module.exports.deleteFavoriteProduct = asyncHandler(async (req, res, next) => { module.exports.deleteFavoriteProduct = async (req, res, next) => {
const { loggedUser } = req; const { loggedUser } = req;
const { slug: productSlug } = req.params; const { slug: productSlug } = req.params;
@@ -275,10 +270,5 @@ module.exports.deleteFavoriteProduct = asyncHandler(async (req, res, next) => {
await appendFavorites(loggedUser, product); await appendFavorites(loggedUser, product);
res.status(200).json({ product }); res.status(200).json({ product });
});
const fieldValidation = (field, next) => {
if (!field) {
return next(new ErrorResponse(`Missing fields`, 400));
}
}; };
+6 -7
View File
@@ -1,8 +1,7 @@
const { UserService } = require('../services'); const { UserService } = require('../services');
const asyncHandler = require("../middlewares/asyncHandler");
const ErrorResponse = require("../utils/errorResponse"); const ErrorResponse = require("../utils/errorResponse");
module.exports.addFollowUser = asyncHandler(async (req, res, next) => { module.exports.addFollowUser = async (req, res, next) => {
try { try {
return res.status(200).json({ return res.status(200).json({
message: 'The user is now being followed.' message: 'The user is now being followed.'
@@ -10,9 +9,9 @@ module.exports.addFollowUser = asyncHandler(async (req, res, next) => {
} catch (err) { } catch (err) {
return next(new ErrorResponse('Something went wrong while following a user.', 500, err.message)); return next(new ErrorResponse('Something went wrong while following a user.', 500, err.message));
} }
}); };
module.exports.deleteFollowUser = asyncHandler(async (req, res, next) => { module.exports.deleteFollowUser = async (req, res, next) => {
try { try {
return res.status(200).json({ return res.status(200).json({
message: 'The user is no longer being followed.', message: 'The user is no longer being followed.',
@@ -21,9 +20,9 @@ module.exports.deleteFollowUser = asyncHandler(async (req, res, next) => {
} catch (err) { } catch (err) {
return next(new ErrorResponse('Something went wrong while unfollowing a user.', 500, err.message)); return next(new ErrorResponse('Something went wrong while unfollowing a user.', 500, err.message));
} }
}); };
module.exports.getProfile = asyncHandler(async (req, res, next) => { module.exports.getProfile = async (req, res, next) => {
try { try {
/*const user = await UserService.getUserById(req.payload.id); /*const user = await UserService.getUserById(req.payload.id);
if (!user) { if (!user) {
@@ -35,7 +34,7 @@ module.exports.getProfile = asyncHandler(async (req, res, next) => {
} catch (err) { } catch (err) {
return next(new ErrorResponse('Something went wrong while fetching user.', 500, err.message)); return next(new ErrorResponse('Something went wrong while fetching user.', 500, err.message));
} }
}); };
module.exports.getParamUsername = async (req, res, next, value) => { module.exports.getParamUsername = async (req, res, next, value) => {
try { try {
+6 -14
View File
@@ -1,11 +1,8 @@
const { TagService, UserService } = require('../services'); const { TagService, UserService } = require('../services');
const asyncHandler = require("../middlewares/asyncHandler");
const ErrorResponse = require("../utils/errorResponse"); const ErrorResponse = require("../utils/errorResponse");
module.exports.createTag = asyncHandler(async (req, res, next) => { module.exports.createTag = async (req, res, next) => {
try { try {
fieldValidation(req.body.tag.name, next);
const { name } = req.body.tag; const { name } = req.body.tag;
const tagInDB = await TagService.tagIsInDB(name); const tagInDB = await TagService.tagIsInDB(name);
if (tagInDB) { if (tagInDB) {
@@ -21,9 +18,9 @@ module.exports.createTag = asyncHandler(async (req, res, next) => {
} catch (err) { } catch (err) {
return next(new ErrorResponse('Something went wrong while creating tag.', 500, err.message)); return next(new ErrorResponse('Something went wrong while creating tag.', 500, err.message));
} }
}); };
module.exports.deleteTag = asyncHandler(async (req, res, next) => { module.exports.deleteTag = async (req, res, next) => {
try { try {
const { name } = req.params; const { name } = req.params;
const tag = await TagService.getTagByName(name); const tag = await TagService.getTagByName(name);
@@ -43,9 +40,9 @@ module.exports.deleteTag = asyncHandler(async (req, res, next) => {
} catch (err) { } catch (err) {
return next(new ErrorResponse('Something went wrong while fetching tags.', 500, err.message)); return next(new ErrorResponse('Something went wrong while fetching tags.', 500, err.message));
} }
}); };
module.exports.getAllTags = asyncHandler(async (req, res, next) => { module.exports.getAllTags = async (req, res, next) => {
try { try {
const tags = await TagService.getAllTags(); const tags = await TagService.getAllTags();
@@ -58,10 +55,5 @@ module.exports.getAllTags = asyncHandler(async (req, res, next) => {
} catch (err) { } catch (err) {
return next(new ErrorResponse('Something went wrong while fetching tags.', 500, err.message)); return next(new ErrorResponse('Something went wrong while fetching tags.', 500, err.message));
} }
});
const fieldValidation = (field, next) => {
if (!field) {
return next(new ErrorResponse(`Missing fields`, 400));
}
}; };
+26 -22
View File
@@ -1,10 +1,9 @@
const { UserService, SkydiverProfileService } = require('../services'); const { UserService, SkydiverProfileService } = require('../services');
const asyncHandler = require("../middlewares/asyncHandler");
const ErrorResponse = require("../utils/errorResponse"); const ErrorResponse = require("../utils/errorResponse");
var crypto = require('crypto'); var crypto = require('crypto');
const passport = require('passport'); const passport = require('passport');
module.exports.authenticate = asyncHandler(async (req, res, next) => { module.exports.authenticate = async (req, res, next) => {
try { try {
passport.authenticate('headerapikey', { session: false }, function (err, application, info) { passport.authenticate('headerapikey', { session: false }, function (err, application, info) {
if (err) { if (err) {
@@ -28,9 +27,9 @@ module.exports.authenticate = asyncHandler(async (req, res, next) => {
//res.status(500).json({ message: 'Something went wrong while authenticating.', error: err.message }); //res.status(500).json({ message: 'Something went wrong while authenticating.', error: err.message });
return next(new ErrorResponse('Something went wrong while authenticating.', 500, err.message)); return next(new ErrorResponse('Something went wrong while authenticating.', 500, err.message));
} }
}); };
module.exports.createUser = asyncHandler(async (req, res, next) => { module.exports.createUser = async (req, res, next) => {
try { try {
const { username, firstname, lastname, phone, email, password } = req.body.user; const { username, firstname, lastname, phone, email, password } = req.body.user;
const userSlug = UserService.slugify(username); const userSlug = UserService.slugify(username);
@@ -57,9 +56,9 @@ module.exports.createUser = asyncHandler(async (req, res, next) => {
} catch (err) { } catch (err) {
return next(new ErrorResponse('Something went wrong while creating user.', 500, err.message)); return next(new ErrorResponse('Something went wrong while creating user.', 500, err.message));
} }
}); };
module.exports.getAllUsers = asyncHandler(async (req, res, next) => { module.exports.getAllUsers = async (req, res, next) => {
try { try {
const users = await UserService.getAllUsers(); const users = await UserService.getAllUsers();
@@ -69,24 +68,29 @@ module.exports.getAllUsers = asyncHandler(async (req, res, next) => {
} catch (err) { } catch (err) {
return next(new ErrorResponse('Something went wrong while fetching user.', 500, err.message)); return next(new ErrorResponse('Something went wrong while fetching user.', 500, err.message));
} }
}); };
module.exports.getUser = asyncHandler(async (req, res, next) => { module.exports.getUser = async (req, res, next) => {
try { try {
const user = await UserService.getUserById(req.payload.id); const user = await UserService.getUserById(req.payload.id);
if (!user) { if (!user) {
return next(new ErrorResponse("Unauthorized", 401, "Authentication required.")); return next(new ErrorResponse("Unauthorized", 401, "Authentication required."));
} }
const profile = await SkydiverProfileService.getByUserId(req.payload.id);
res.status(200).json({ res.status(200).json({
user: user.toAuthJSON() user: {
...user.toAuthJSON(),
...(profile ? profile.toJSONFor() : { poids: null, licence: null, bg_image: null }),
}
}); });
} catch (err) { } catch (err) {
return next(new ErrorResponse('Something went wrong while fetching user.', 500, err.message)); return next(new ErrorResponse('Something went wrong while fetching user.', 500, err.message));
} }
}); };
module.exports.loginAsUser = asyncHandler(async (req, res, next) => { module.exports.loginAsUser = async (req, res, next) => {
try { try {
passport.authenticate('local', { session: false }, function (err, user, info) { passport.authenticate('local', { session: false }, function (err, user, info) {
if (err) { if (err) {
@@ -102,9 +106,9 @@ module.exports.loginAsUser = asyncHandler(async (req, res, next) => {
} catch (err) { } catch (err) {
return next(new ErrorResponse('Something went wrong while processing login.', 500, err.message)); return next(new ErrorResponse('Something went wrong while processing login.', 500, err.message));
} }
}); };
module.exports.updateUser = asyncHandler(async (req, res, next) => { module.exports.updateUser = async (req, res, next) => {
try { try {
let user = await UserService.getUserById(req.payload.id); let user = await UserService.getUserById(req.payload.id);
if (!user) { if (!user) {
@@ -142,9 +146,9 @@ module.exports.updateUser = asyncHandler(async (req, res, next) => {
} catch (err) { } catch (err) {
return next(new ErrorResponse('Something went wrong while updating user.', 500, err.message)); return next(new ErrorResponse('Something went wrong while updating user.', 500, err.message));
} }
}); };
module.exports.updateUserEmail = asyncHandler(async (req, res, next) => { module.exports.updateUserEmail = async (req, res, next) => {
try { try {
let user = await UserService.getUserById(req.payload.id); let user = await UserService.getUserById(req.payload.id);
if (!user) { if (!user) {
@@ -162,9 +166,9 @@ module.exports.updateUserEmail = asyncHandler(async (req, res, next) => {
} catch (err) { } catch (err) {
return next(new ErrorResponse('Something went wrong while updating user.', 500, err.message)); return next(new ErrorResponse('Something went wrong while updating user.', 500, err.message));
} }
}); };
module.exports.updateUserPassword = asyncHandler(async (req, res, next) => { module.exports.updateUserPassword = async (req, res, next) => {
try { try {
let user = await UserService.getUserById(req.payload.id); let user = await UserService.getUserById(req.payload.id);
if (!user) { if (!user) {
@@ -182,9 +186,9 @@ module.exports.updateUserPassword = asyncHandler(async (req, res, next) => {
} catch (err) { } catch (err) {
return next(new ErrorResponse('Something went wrong while updating user.', 500, err.message)); return next(new ErrorResponse('Something went wrong while updating user.', 500, err.message));
} }
}); };
module.exports.updateUserRole = asyncHandler(async (req, res, next) => { module.exports.updateUserRole = async (req, res, next) => {
try { try {
let user = await UserService.getUserById(req.payload.id); let user = await UserService.getUserById(req.payload.id);
if (!user) { if (!user) {
@@ -205,9 +209,9 @@ module.exports.updateUserRole = asyncHandler(async (req, res, next) => {
} catch (err) { } catch (err) {
return next(new ErrorResponse('Something went wrong while updating user.', 500, err.message)); return next(new ErrorResponse('Something went wrong while updating user.', 500, err.message));
} }
}); };
module.exports.updateUserUsername = asyncHandler(async (req, res, next) => { module.exports.updateUserUsername = async (req, res, next) => {
try { try {
let user = await UserService.getUserById(req.payload.id); let user = await UserService.getUserById(req.payload.id);
if (!user) { if (!user) {
@@ -225,5 +229,5 @@ module.exports.updateUserUsername = asyncHandler(async (req, res, next) => {
} catch (err) { } catch (err) {
return next(new ErrorResponse('Something went wrong while updating user.', 500, err.message)); return next(new ErrorResponse('Something went wrong while updating user.', 500, err.message));
} }
}); };
+1 -7
View File
@@ -1,8 +1,7 @@
var express = require('express'), var express = require('express'),
session = require('express-session'), session = require('express-session'),
cors = require('cors'), cors = require('cors'),
helmet = require('helmet'), helmet = require('helmet');
errorhandler = require('errorhandler');
const config = require('./config'), const config = require('./config'),
swaggerUi = require('swagger-ui-express'), swaggerUi = require('swagger-ui-express'),
@@ -23,7 +22,6 @@ function createApp() {
})); }));
app.use(express.json()); app.use(express.json());
app.use(express.urlencoded({ extended: true })); app.use(express.urlencoded({ extended: true }));
app.use(require('method-override')());
app.use(express.static(__dirname + '/../public')); app.use(express.static(__dirname + '/../public'));
app.use(session({ app.use(session({
secret: process.env.SESSION_SECRET || config.secret, secret: process.env.SESSION_SECRET || config.secret,
@@ -32,10 +30,6 @@ function createApp() {
saveUninitialized: false saveUninitialized: false
})); }));
if (process.env.APP_ENV !== 'production') {
app.use(errorhandler());
}
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec, { app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec, {
swaggerOptions: { persistAuthorization: true }, swaggerOptions: { persistAuthorization: true },
})); }));
-7
View File
@@ -1,7 +0,0 @@
const asyncHandler = (fn) => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch((err) => {
next(err);
});
};
module.exports = asyncHandler;
@@ -0,0 +1,27 @@
const { body } = require('express-validator');
const createProductRules = [
body('product.title')
.notEmpty().withMessage("can't be blank")
.isLength({ max: 255 }).withMessage('must be at most 255 characters')
.trim(),
body('product.description')
.notEmpty().withMessage("can't be blank")
.isLength({ max: 1000 }).withMessage('must be at most 1000 characters')
.trim(),
body('product.body')
.notEmpty().withMessage("can't be blank"),
];
const updateProductRules = [
body('product.title')
.optional()
.isLength({ max: 255 }).withMessage('must be at most 255 characters')
.trim(),
body('product.description')
.optional()
.isLength({ max: 1000 }).withMessage('must be at most 1000 characters')
.trim(),
];
module.exports = { createProductRules, updateProductRules };
@@ -0,0 +1,10 @@
const { body } = require('express-validator');
const createTagRules = [
body('tag.name')
.notEmpty().withMessage("can't be blank")
.isLength({ max: 50 }).withMessage('must be at most 50 characters')
.trim(),
];
module.exports = { createTagRules };
@@ -4,7 +4,7 @@ const createUserRules = [
body('user.email') body('user.email')
.notEmpty().withMessage("can't be blank") .notEmpty().withMessage("can't be blank")
.isEmail().withMessage('must be a valid email address') .isEmail().withMessage('must be a valid email address')
.normalizeEmail(), .toLowerCase(),
body('user.username') body('user.username')
.notEmpty().withMessage("can't be blank") .notEmpty().withMessage("can't be blank")
.isLength({ min: 3, max: 50 }).withMessage('must be between 3 and 50 characters') .isLength({ min: 3, max: 50 }).withMessage('must be between 3 and 50 characters')
@@ -18,7 +18,7 @@ const loginRules = [
body('user.email') body('user.email')
.notEmpty().withMessage("can't be blank") .notEmpty().withMessage("can't be blank")
.isEmail().withMessage('must be a valid email address') .isEmail().withMessage('must be a valid email address')
.normalizeEmail(), .toLowerCase(),
body('user.password') body('user.password')
.notEmpty().withMessage("can't be blank"), .notEmpty().withMessage("can't be blank"),
]; ];
@@ -44,7 +44,7 @@ const updateEmailRules = [
body('user.email') body('user.email')
.notEmpty().withMessage("can't be blank") .notEmpty().withMessage("can't be blank")
.isEmail().withMessage('must be a valid email address') .isEmail().withMessage('must be a valid email address')
.normalizeEmail(), .toLowerCase(),
]; ];
const updateUsernameRules = [ const updateUsernameRules = [
+6
View File
@@ -0,0 +1,6 @@
var routes = require('express').Router();
routes.use('/user', require('./users.routes'));
routes.use('/applications', require('./applications.routes'));
module.exports = routes;
-1
View File
@@ -1,6 +1,5 @@
var routes = require('express').Router(); var routes = require('express').Router();
routes.use('/user', require('./users.routes'));
routes.use('/articles', require('./articles.routes')); routes.use('/articles', require('./articles.routes'));
routes.use('/comments', require('./comments.routes')); routes.use('/comments', require('./comments.routes'));
routes.use('/profiles', require('./profiles.routes')); routes.use('/profiles', require('./profiles.routes'));
+3 -1
View File
@@ -1,10 +1,12 @@
const express = require('express'); const express = require('express');
const { createTag, deleteTag, getAllTags } = require('../../../controllers/tags.controller'); const { createTag, deleteTag, getAllTags } = require('../../../controllers/tags.controller');
const auth = require('../../../middlewares/auth'); const auth = require('../../../middlewares/auth');
const validate = require('../../../middlewares/validate');
const { createTagRules } = require('../../../middlewares/validators/tags.validators');
const tagRouter = express.Router(); const tagRouter = express.Router();
tagRouter.post('/', auth.required, createTag); tagRouter.post('/', auth.required, createTagRules, validate, createTag);
tagRouter.get('/', auth.optional, getAllTags); tagRouter.get('/', auth.optional, getAllTags);
tagRouter.delete('/:name', auth.required, deleteTag); tagRouter.delete('/:name', auth.required, deleteTag);
+4 -2
View File
@@ -9,13 +9,15 @@ const {
updateProduct, updateProduct,
} = require('../../../controllers/products.controller'); } = require('../../../controllers/products.controller');
const auth = require('../../../middlewares/auth'); const auth = require('../../../middlewares/auth');
const validate = require('../../../middlewares/validate');
const { createProductRules, updateProductRules } = require('../../../middlewares/validators/products.validators');
const productRouter = express.Router(); const productRouter = express.Router();
productRouter.get('/', auth.optional, getProducts); productRouter.get('/', auth.optional, getProducts);
productRouter.post('/', auth.required, createProduct); productRouter.post('/', auth.required, createProductRules, validate, createProduct);
productRouter.get('/:slug', auth.optional, getProduct); productRouter.get('/:slug', auth.optional, getProduct);
productRouter.put('/:slug', auth.required, updateProduct); productRouter.put('/:slug', auth.required, updateProductRules, validate, updateProduct);
productRouter.delete('/:slug', auth.required, deleteProduct); productRouter.delete('/:slug', auth.required, deleteProduct);
productRouter.post('/:slug/favorite', auth.required, addFavoriteProduct); productRouter.post('/:slug/favorite', auth.required, addFavoriteProduct);
productRouter.delete('/:slug/favorite', auth.required, deleteFavoriteProduct); productRouter.delete('/:slug/favorite', auth.required, deleteFavoriteProduct);
+1
View File
@@ -1,5 +1,6 @@
var routes = require('express').Router(); var routes = require('express').Router();
routes.use('/auth', require('./auth'));
routes.use('/skydive', require('./skydive')); routes.use('/skydive', require('./skydive'));
routes.use('/cms', require('./cms')); routes.use('/cms', require('./cms'));
routes.use('/ecommerce', require('./ecommerce')); routes.use('/ecommerce', require('./ecommerce'));
-1
View File
@@ -1,6 +1,5 @@
var routes = require('express').Router(); var routes = require('express').Router();
routes.use('/applications', require('./applications.routes'));
routes.use('/aeronefs', require('./aeronefs.routes')); routes.use('/aeronefs', require('./aeronefs.routes'));
routes.use('/canopies', require('./canopies.routes')); routes.use('/canopies', require('./canopies.routes'));
routes.use('/dropzones', require('./dropzones.routes')); routes.use('/dropzones', require('./dropzones.routes'));