Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 74f19b669b | |||
| 461d66a90f | |||
| ad9009afa1 | |||
| d82dc0578c | |||
| ec61ae6721 | |||
| 29a29a5074 | |||
| 6641da9a3d | |||
| 80cd96e006 | |||
| 54b343b2fb | |||
| 7c16e5d8cb |
@@ -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.
|
|
||||||
@@ -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
|
|
||||||
@@ -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.
|
|
||||||
|
|
||||||
═════════════════════════════════════════════════════════════════════════════
|
|
||||||
@@ -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
|
|
||||||
|
|
||||||
═══════════════════════════════════════════════════════════════════════════════
|
|
||||||
@@ -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.
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
```
|
|
||||||
@@ -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.
|
|
||||||
@@ -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.
|
||||||
@@ -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`).
|
||||||
@@ -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.
|
||||||
Generated
+62
-3
@@ -18,7 +18,10 @@
|
|||||||
"errorhandler": "^1.5.1",
|
"errorhandler": "^1.5.1",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"express-jwt": "^8.4.1",
|
"express-jwt": "^8.4.1",
|
||||||
|
"express-rate-limit": "^8.4.1",
|
||||||
"express-session": "^1.18.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.2",
|
||||||
"method-override": "3.0.0",
|
"method-override": "3.0.0",
|
||||||
@@ -5005,6 +5008,33 @@
|
|||||||
"node": ">= 8.0.0"
|
"node": ">= 8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/express-rate-limit": {
|
||||||
|
"version": "8.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.4.1.tgz",
|
||||||
|
"integrity": "sha512-NGVYwQSAyEQgzxX1iCM978PP9AdO/hW93gMcF6ZwQCm+rFvLsBH6w4xcXWTcliS8La5EPRN3p9wzItqBwJrfNw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ip-address": "10.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 16"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/express-rate-limit"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"express": ">= 4.11"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/express-rate-limit/node_modules/ip-address": {
|
||||||
|
"version": "10.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
|
||||||
|
"integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/express-session": {
|
"node_modules/express-session": {
|
||||||
"version": "1.18.0",
|
"version": "1.18.0",
|
||||||
"resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.0.tgz",
|
"resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.0.tgz",
|
||||||
@@ -5041,6 +5071,25 @@
|
|||||||
"resolved": "https://registry.npmjs.org/express-unless/-/express-unless-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/express-unless/-/express-unless-2.1.3.tgz",
|
||||||
"integrity": "sha512-wj4tLMyCVYuIIKHGt0FhCtIViBcwzWejX0EjNxveAa6dG+0XBCQhMbx+PnkLkFCxLC69qoFrxds4pIyL88inaQ=="
|
"integrity": "sha512-wj4tLMyCVYuIIKHGt0FhCtIViBcwzWejX0EjNxveAa6dG+0XBCQhMbx+PnkLkFCxLC69qoFrxds4pIyL88inaQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/express-validator": {
|
||||||
|
"version": "7.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.3.2.tgz",
|
||||||
|
"integrity": "sha512-ctLw1Vl6dXVH62dIQMDdTAQkrh480mkFuG6/SGXOaVlwPNukhRAe7EgJIMJ2TSAni8iwHBRp530zAZE5ZPF2IA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"lodash": "^4.18.1",
|
||||||
|
"validator": "~13.15.23"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/express-validator/node_modules/lodash": {
|
||||||
|
"version": "4.18.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
|
||||||
|
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/express/node_modules/body-parser": {
|
"node_modules/express/node_modules/body-parser": {
|
||||||
"version": "1.20.1",
|
"version": "1.20.1",
|
||||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
|
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
|
||||||
@@ -5750,6 +5799,15 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/helmet": {
|
||||||
|
"version": "8.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz",
|
||||||
|
"integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/html-escaper": {
|
"node_modules/html-escaper": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
|
||||||
@@ -10450,9 +10508,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/validator": {
|
"node_modules/validator": {
|
||||||
"version": "13.15.15",
|
"version": "13.15.35",
|
||||||
"resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz",
|
"resolved": "https://registry.npmjs.org/validator/-/validator-13.15.35.tgz",
|
||||||
"integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==",
|
"integrity": "sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==",
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.10"
|
"node": ">= 0.10"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,10 @@
|
|||||||
"errorhandler": "^1.5.1",
|
"errorhandler": "^1.5.1",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"express-jwt": "^8.4.1",
|
"express-jwt": "^8.4.1",
|
||||||
|
"express-rate-limit": "^8.4.1",
|
||||||
"express-session": "^1.18.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.2",
|
||||||
"method-override": "3.0.0",
|
"method-override": "3.0.0",
|
||||||
|
|||||||
+10
-27
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -134,14 +134,10 @@ module.exports.getProduct = asyncHandler(async (req, res, next) => {
|
|||||||
module.exports.createProduct = asyncHandler(async (req, res, next) => {
|
module.exports.createProduct = asyncHandler(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({
|
||||||
@@ -181,7 +177,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) {
|
||||||
@@ -203,7 +199,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."));
|
||||||
@@ -277,8 +273,3 @@ module.exports.deleteFavoriteProduct = asyncHandler(async (req, res, next) => {
|
|||||||
res.status(200).json({ product });
|
res.status(200).json({ product });
|
||||||
});
|
});
|
||||||
|
|
||||||
const fieldValidation = (field, next) => {
|
|
||||||
if (!field) {
|
|
||||||
return next(new ErrorResponse(`Missing fields`, 400));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ const ErrorResponse = require("../utils/errorResponse");
|
|||||||
|
|
||||||
module.exports.createTag = asyncHandler(async (req, res, next) => {
|
module.exports.createTag = asyncHandler(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) {
|
||||||
@@ -60,8 +58,3 @@ module.exports.getAllTags = asyncHandler(async (req, res, next) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const fieldValidation = (field, next) => {
|
|
||||||
if (!field) {
|
|
||||||
return next(new ErrorResponse(`Missing fields`, 400));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -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 = [
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user