diff --git a/.env b/.env index de9a821..9930401 100644 --- a/.env +++ b/.env @@ -4,7 +4,7 @@ DEBUG=false SERVER_PORT=3201 SECRET="$2b$10$2.JkQCRUa3fXESy/WyKVWO" SESSION_LIFETIME=3600 -MONGODB_URI="mongodb://localhost:27017/headupdb" +MONGODB_URI="mongodb://localhost:27017/adastradb" SENDGRID_API_KEY="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" SENDGRID_FROM_MAIL="contact@rampeur.fr" SENDGRID_TO_MAIL="rampeur@gmail.com" diff --git a/.env.development b/.env.development index ff47a9d..fd23bea 100644 --- a/.env.development +++ b/.env.development @@ -4,7 +4,7 @@ DEBUG=false SERVER_PORT=3201 SECRET="$2b$10$2.JkQCRUa3fXESy/WyKVWO" SESSION_LIFETIME=86400 -MONGODB_URI="mongodb://localhost:27017/headupdb" +MONGODB_URI="mongodb://localhost:27017/adastradb" SENDGRID_API_KEY="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" SENDGRID_FROM_MAIL="contact@rampeur.fr" SENDGRID_TO_MAIL="rampeur@gmail.com" diff --git a/.env.local b/.env.local index 6422e15..63663d9 100644 --- a/.env.local +++ b/.env.local @@ -4,7 +4,7 @@ DEBUG=false SERVER_PORT=3201 SECRET="$2b$10$2.JkQCRUa3fXESy/WyKVWO" SESSION_LIFETIME=3600 -MONGODB_URI="mongodb://localhost:27017/headupdb" +MONGODB_URI="mongodb://localhost:27017/adastradb" SENDGRID_API_KEY="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" SENDGRID_FROM_MAIL="contact@rampeur.fr" SENDGRID_TO_MAIL="rampeur@gmail.com" diff --git a/.env.production b/.env.production index 298e5ed..5f978db 100644 --- a/.env.production +++ b/.env.production @@ -4,7 +4,7 @@ DEBUG=false SERVER_PORT=3201 SECRET="$2b$10$2.JkQCRUa3fXESy/WyKVWO" SESSION_LIFETIME=3600 -MONGODB_URI="mongodb://localhost:27017/headupdb" +MONGODB_URI="mongodb://localhost:27017/adastradb" SENDGRID_API_KEY="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" SENDGRID_FROM_MAIL="contact@rampeur.fr" SENDGRID_TO_MAIL="rampeur@gmail.com" diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..20eb114 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,156 @@ +# Instructions pour les agents Copilot / AI + +But : fournir à un agent IA les informations essentielles pour être immédiatement productif dans ce workspace. + +## Vue d'ensemble des projets + +- **adastra_api/** : API Express/Node.js avec Sequelize (MySQL) + Mongoose (Mongo) +- **adastra_app/** : Frontend Angular 17 + +## Entrées principales de l'application + +### API (adastra_api) + +- **Point d'entrée :** `app.js` (racine du projet) + - Charge les variables d'environnement via `APP_ENV` (`.env.${APP_ENV}`) + - Initialise les middlewares (CORS, Morgan, session, etc.) + - Connecte MongoDB et MySQL + - Lance le serveur sur le port (défaut : 3200) + +- **Routage Express :** `src/app.js` et `src/routes/` + - Toutes les routes API sont sous `/api/v1` + - Exemple : `GET /api/v1/articles` → `src/controllers/article.controller.js` + +- **Contrôleurs :** `src/controllers/` + - Gèrent la logique métier, validations, réponses JSON + - Suivent le pattern : controller → service → utils → database + +### Frontend (adastra_app) + +- **Point d'entrée :** `src/main.ts` (bootstrap Angular) +- **Configuration :** `angular.json` (builds, configurations, assets) +- **Routes :** `src/app/app.routes.ts` +- **Composants :** `src/app/components/` +- **Styles globaux :** `src/styles/` + +## Architecture et patterns clés + +### Structure des données + +- **Sequelize (MySQL)** : modèles dans `src/database/models/` + - Utilise `sequelize-cli` pour les migrations (si applicable) + - Auto-généré avec `sequelize-auto` + +- **Mongoose (MongoDB)** : modèles dans `src/database/models/mongo` + - Initialisé avec `require('./src/database/models/mongo')` + +### Gestion des erreurs + +Middleware centralisé dans `src/middlewares/exceptions.handler` : +- `notFoundErrorHandler` : capture 404 +- `logErrorHandler` : log des erreurs +- `fianlErrorHandler` : réponse finale au client + +### Authentification & configuration + +- **Passport :** `src/config/passport-local.js`, `passport-headerapikey.js` +- **Config générale :** `src/config/index.js` (secrets, session lifetime, DB) +- **Variables d'environnement :** `.env.${APP_ENV}` (local, development, production) + +## Commandes essentielles + +### API (adastra_api) + +```bash +# Installation +cd adastra_api && npm install + +# Développement (avec hot reload via nodemon) +npm run local # APP_ENV=local nodemon ./app.js (port 3200) + +# Production/Staging +npm run dev # APP_ENV=development node ./app.js +npm run start # APP_ENV=production node ./app.js + +# Tests +npm test # Newman : exécute tests/adastra-api-tests.postman_collection.json + +# Arrêter le serveur +npm run stop # Tue le processus sur le port 3200 + +# MongoDB (Docker) +npm run mongo:start # Lance le conteneur Mongo (port 27017) +npm run mongo:stop # Arrête et nettoie le conteneur +``` + +### Frontend (adastra_app) + +```bash +# Installation +cd adastra_app && npm install + +# Développement +npm run local # ng serve --configuration local --port 4400 +npm start # ng serve (port 4200 par défaut) +npm run dev # ng serve --configuration local --port 4201 + +# Build +npm run build # Production build (dist/) +npm run watch # Watch mode + +# Linting +npm run lint # ESLint +``` + +## Conventions et bonnes pratiques + +### Ajout d'une nouvelle route API + +1. Créer/modifier la route dans `src/routes/` (ex. `src/routes/api/v2/articles.routes.js`) +2. Implémenter le contrôleur dans `src/controllers/article.controller.js` +3. Ajouter validation dans `src/validations/` si nécessaire +4. Tester via Postman → exporter en `tests/adastra-api-tests.postman_collection.json` + +### Ajout d'un modèle de données + +- **Sequelize :** créer le fichier dans `src/database/models/`, utiliser `sequelize-cli` si migrations +- **Mongoose :** créer le schéma dans `src/database/models/mongo/`, importer dans `src/database/models/mongo/index.js` + +### Structure de réponse API + +Vérifier `src/controllers/` pour le format des réponses — généralement : +```javascript +res.status(200).json({ data: [...], message: 'Success' }); +res.status(400).json({ error: 'Validation failed', details: [...] }); +``` + +## Variables d'environnement essentielles + +À définir dans `.env.local`, `.env.development`, `.env.production` : +- `SERVER_PORT` : port du serveur (défaut 3200) +- `APP_ENV` : `local`, `development`, ou `production` +- `DB_HOST`, `DB_USER`, `DB_PASS`, `DB_NAME` : configuration MySQL +- `MONGO_URI` : URI de MongoDB +- Clés API externes (SendGrid, etc.) + +## Points d'attention + +- **Ne pas modifier** directement l'ordre des middlewares dans `app.js` sans tester l'intégralité du flow +- **Versions Node :** respecter `engines` dans `package.json` (`^16.20.2 || ^18.19.1 || ^20.11.1`) +- **Dépendances critiques :** `express`, `sequelize`, `mongoose`, `passport`, `jsonwebtoken` + +## Ressources utiles pour un agent + +Si tu dois ajouter/modifier du code, consulte d'abord : +1. **Contrôleur similaire existant** : ex. `src/controllers/member.controller.js` pour le pattern +2. **Route similaire existante** : `src/routes/` pour voir comment router +3. **Config/Middleware** : vérifier si logique centralisée existe déjà (`src/middlewares/`, `src/config/`) +4. **Tests Postman** : `tests/adastra-api-tests.postman_collection.json` pour valider les endpoints et `tests/adastra-api-tests.postman_environment.json` pour les variables d'environnement + +--- + +**Questions manquantes ?** Demande-moi les détails sur : +- Structure exacte des schémas Sequelize/Mongoose +- Authentification et système d'autorisations +- Workflows de déploiement ou CI/CD +- Intégration frontend/backend (headers, tokens, etc.) diff --git a/ARTICLE_METHODS.md b/ARTICLE_METHODS.md new file mode 100644 index 0000000..4d4943f --- /dev/null +++ b/ARTICLE_METHODS.md @@ -0,0 +1,210 @@ +# 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. diff --git a/README.md b/README.md index 9e4d2f2..7657645 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,42 @@ API pour l'application Ad Astra +**Installation rapide** +```bash +# Initialisation (applique les migrations, seed roles → création admin interactive → seed articles) +# La commande suivante applique d'abord les migrations nécessaires. +npm run setup:api +``` +**Installation manuelle** +```bash +# Appliquer les migrations +npx sequelize-cli db:migrate + +# Exécuter le seeder des articles +npx sequelize-cli db:seed --seed 20251126-add-roles.js + +# Créer un utilisateur Admin +npm run create:user + +# Exécuter le seeder des articles +npx sequelize-cli db:seed --seed 20251127-add-articles.js +``` + +**Migration** +```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 +``` + +**Seeder** +```bash # Exécuter tous les seeders npx sequelize-cli db:seed:all @@ -14,4 +49,19 @@ npx sequelize-cli db:seed:undo:all # Annuler un seeder spécifique npx sequelize-cli db:seed:undo --seed 20251127-add-articles.js +``` +**Start 'local' API server** +```bash +# Appliquer les migrations +npm run local + +# Exécuter le seeder des articles +npx sequelize-cli db:seed --seed 20251126-add-roles.js + +# Créer un utilisateur Admin +npm run create:user + +# Exécuter le seeder des articles +npx sequelize-cli db:seed --seed 20251127-add-articles.js +``` \ No newline at end of file diff --git a/SEQUELIZE_MODELS_METHODS.md b/SEQUELIZE_MODELS_METHODS.md new file mode 100644 index 0000000..ec1bdb3 --- /dev/null +++ b/SEQUELIZE_MODELS_METHODS.md @@ -0,0 +1,762 @@ +# 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 +``` diff --git a/nodemon.json b/nodemon.json index 9dc21a2..354cd36 100644 --- a/nodemon.json +++ b/nodemon.json @@ -6,7 +6,7 @@ "SERVER_PORT": 3201, "SECRET": "$2b$10$aEsAUG7Dy8dcTORi1vaQle", "SESSION_LIFETIME": 3600, - "MONGODB_URI": "mongodb://localhost:27017/headupdb", + "MONGODB_URI": "mongodb://localhost:27017/adastradb", "SENDGRID_API_KEY": "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", "SENDGRID_FROM_MAIL": "contact@rampeur.fr", "SENDGRID_TO_MAIL": "rampeur@gmail.com", diff --git a/package.json b/package.json index d068b51..26080d5 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,9 @@ "test": "newman run ./tests/adastra-api-tests.postman_collection.json -e ./tests/adastra-api-tests.postman_environment.json", "stop": "lsof -ti :3200 | xargs kill", "mongo:start": "docker run --name hu-mongo -p 27017:27017 mongo & sleep 5", - "mongo:stop": "docker stop hu-mongo && docker rm hu-mongo" + "mongo:stop": "docker stop hu-mongo && docker rm hu-mongo", + "create:user": "node scripts/create-user.js", + "setup:api": "npx sequelize-cli db:migrate && npx sequelize-cli db:seed --seed 20251126-add-roles.js && npm run create:user && npx sequelize-cli db:seed --seed 20251127-add-articles.js" }, "engines": { "node": "^16.20.2 || ^18.19.1 || ^20.11.1" diff --git a/scripts/create-user.js b/scripts/create-user.js new file mode 100644 index 0000000..bedcc47 --- /dev/null +++ b/scripts/create-user.js @@ -0,0 +1,154 @@ +#!/usr/bin/env node + +/** + * Script pour créer un utilisateur Admin interactivement + * Usage: npm run create:user + */ + +const readline = require('readline'); +const crypto = require('crypto'); +const { v4: uuidv4 } = require('uuid'); + +// Load environment +const appEnv = process.env.APP_ENV || 'development'; +require('dotenv').config({ path: `.env.${appEnv}` }); + +const sequelize = require('../src/database/config/sequelize'); + +// Create readline interface +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout +}); + +// Helper function for prompts +const question = (prompt) => { + return new Promise((resolve) => { + rl.question(prompt, (answer) => { + resolve(answer); + }); + }); +}; + +// Hash password +const hashPassword = (password) => { + const salt = crypto.randomBytes(16).toString('hex'); + const hash = crypto.pbkdf2Sync(password, salt, 10000, 512, 'sha512').toString('hex'); + return { salt, hash }; +}; + +// Main function +const createUser = async () => { + try { + console.log('\n🔐 Création d\'un utilisateur Admin\n'); + console.log('Veuillez fournir les informations suivantes:\n'); + + // Get user input + const email = await question('Email: '); + const username = await question('Nom d\'utilisateur: '); + const firstname = await question('Prénom (optionnel): '); + const lastname = await question('Nom (optionnel): '); + const phone = await question('Téléphone (optionnel): '); + const password = await question('Mot de passe: '); + const passwordConfirm = await question('Confirmer le mot de passe: '); + + // Validation + if (!email || !username || !password) { + console.error('\n❌ Email, nom d\'utilisateur et mot de passe sont obligatoires\n'); + rl.close(); + process.exit(1); + } + + if (password !== passwordConfirm) { + console.error('\n❌ Les mots de passe ne correspondent pas\n'); + rl.close(); + process.exit(1); + } + + if (password.length < 6) { + console.error('\n❌ Le mot de passe doit contenir au moins 6 caractères\n'); + rl.close(); + process.exit(1); + } + + // Connect to database and load models + console.log('\n📡 Connexion à la base de données...'); + await sequelize.authenticate(); + console.log('✅ Connexion établie\n'); + + // Initialize all models by calling their functions + 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')(); + + // Get the initialized models from sequelize + const DB = sequelize.models; + + // Hash password with same algorithm as UserService + const { salt, hash } = hashPassword(password); + + // Create user + console.log('👤 Création de l\'utilisateur...'); + const user = await DB.User.create({ + id: uuidv4(), + email: email.toLowerCase(), + username, + firstname: firstname || null, + lastname: lastname || null, + phone: phone || null, + image: null, + role: 'Admin', + salt, + hash + }); + + // Find and assign Admin role + const adminRole = await DB.Role.findOne({ where: { name: 'Admin' } }); + if (adminRole) { + await user.addRoleIdRole(adminRole); + console.log('✅ Rôle Admin assigné\n'); + } else { + console.log('⚠️ Rôle Admin non trouvé, création en tant qu\'utilisateur uniquement\n'); + } + + // Success message + console.log('✅ Utilisateur créé avec succès!\n'); + console.log('Détails:'); + console.log(` 📧 Email: ${user.email}`); + console.log(` 👤 Nom d'utilisateur: ${user.username}`); + console.log(` 🔐 Rôle: ${user.role}`); + console.log(''); + + rl.close(); + await sequelize.close(); + process.exit(0); + } catch (error) { + console.error('\n❌ Erreur lors de la création de l\'utilisateur:\n'); + console.error(error.message); + console.error(error.stack); + console.log(''); + rl.close(); + process.exit(1); + } +}; + +// Run script +createUser(); diff --git a/src/controllers/user.controller.js b/src/controllers/user.controller.js index 5f993ee..d455b65 100644 --- a/src/controllers/user.controller.js +++ b/src/controllers/user.controller.js @@ -1,8 +1,21 @@ const { UserService } = require('../services'); const asyncHandler = require("../middlewares/asyncHandler"); const ErrorResponse = require("../utils/errorResponse"); +var crypto = require('crypto'); const passport = require('passport'); + +module.exports.addFollowUser = asyncHandler(async (req, res, next) => { + try { + return res.status(200).json({ + message: 'The user is now being followed.', + user: true + }); + } catch (err) { + return next(new ErrorResponse('Something went wrong while following a user.', 500, err.message)); + } +}); + module.exports.authenticate = asyncHandler(async (req, res, next) => { try { passport.authenticate('headerapikey', { session: false }, function (err, application, info) { @@ -31,12 +44,27 @@ module.exports.authenticate = asyncHandler(async (req, res, next) => { module.exports.createUser = asyncHandler(async (req, res, next) => { try { - const { username, firstname, lastname, email } = req.body.user; + + fieldValidation(req.body.user.username, next); + fieldValidation(req.body.user.email, next); + fieldValidation(req.body.user.password, next); + + const { username, firstname, lastname, phone, email, password } = req.body.user; + const userSlug = UserService.slugify(username); + const slugInDB = await UserService.isUsernameUsed(userSlug); + if (slugInDB) { + return next(new ErrorResponse("Username already exists", 400)); + } + + const uuid = crypto.randomUUID() const user = await UserService.createUser({ + id: uuid, username, firstname, lastname, - email + phone, + email, + password }); res.status(201).json({ @@ -48,6 +76,17 @@ module.exports.createUser = asyncHandler(async (req, res, next) => { } }); +module.exports.deleteFollowUser = asyncHandler(async (req, res, next) => { + try { + return res.status(200).json({ + message: 'The user is no longer being followed.', + user: true + }); + } catch (err) { + return next(new ErrorResponse('Something went wrong while unfollowing a user.', 500, err.message)); + } +}); + module.exports.getAllUsers = asyncHandler(async (req, res, next) => { try { const users = await UserService.getAllUsers(); @@ -69,6 +108,7 @@ module.exports.getUser = asyncHandler(async (req, res, next) => { res.status(200).json({ user: user.toAuthJSON(), + uuid: crypto.randomUUID() }); } catch (err) { return next(new ErrorResponse('Something went wrong while fetching user.', 500, err.message)); @@ -105,12 +145,6 @@ module.exports.updateUser = asyncHandler(async (req, res, next) => { if (!user) { return next(new ErrorResponse("Unauthorized", 401, "You are not allowed to access this resource.")); } - if (typeof req.body.user.username !== 'undefined') { - user.username = req.body.user.username; - } - if (typeof req.body.user.email !== 'undefined') { - user.email = req.body.user.email; - } if (typeof req.body.user.firstname !== 'undefined') { user.firstname = req.body.user.firstname; } @@ -132,46 +166,111 @@ module.exports.updateUser = asyncHandler(async (req, res, next) => { if (typeof req.body.user.bg_image !== 'undefined') { user.bg_image = req.body.user.bg_image; } - if (typeof req.body.user.password !== 'undefined') { - user.setPassword(req.body.user.password); - } if (typeof req.body.user.role !== 'undefined') { - user.role = req.body.user.role; + return next(new ErrorResponse("Forbidden", 403, "A suspected attempt to usurp rights has been detected. The suspicious activity has been reported and xill be investigated.")); } - /*user = await UserService.updateUser(req.body.user); + // Persist changes using the service (service will save instance or update+fetch) + const updatedUser = await UserService.updateUserById(user, req.payload.id); res.status(200).json({ - user: user.toAuthJSON(), - });*/ - return user.save().then(function () { + user: updatedUser.toAuthJSON() + }); + /*return user.save().then(function () { return res.json({ message: 'User updated successfully.', user: user.toAuthJSON() }); + });*/ + } catch (err) { + return next(new ErrorResponse('Something went wrong while updating user.', 500, err.message)); + } +}); + +module.exports.updateUserEmail = asyncHandler(async (req, res, next) => { + try { + let user = await UserService.getUserById(req.payload.id); + if (!user) { + return next(new ErrorResponse("Unauthorized", 401, "You are not allowed to access this resource.")); + } + + fieldValidation(req.body.user.email, next); + if (typeof req.body.user.email !== 'undefined') { + user.email = req.body.user.email; + } + + user = await UserService.updateUserById(user, req.payload.id); + res.status(200).json({ + user: user.toAuthJSON(), }); } catch (err) { return next(new ErrorResponse('Something went wrong while updating user.', 500, err.message)); } }); -module.exports.addFollowUser = asyncHandler(async (req, res, next) => { +module.exports.updateUserPassword = asyncHandler(async (req, res, next) => { try { - return res.status(200).json({ - message: 'The user is now being followed.', - user: true + let user = await UserService.getUserById(req.payload.id); + if (!user) { + return next(new ErrorResponse("Unauthorized", 401, "You are not allowed to access this resource.")); + } + if (typeof req.body.user.password !== 'undefined') { + const {salt, hash} = UserService.generateSaltHash(req.body.user.password); + user.salt = salt; + user.hash = hash; + } + user = await UserService.updateUserById(user, req.payload.id); + res.status(200).json({ + user: user.toAuthJSON(), }); } catch (err) { - return next(new ErrorResponse('Something went wrong while following a user.', 500, err.message)); + return next(new ErrorResponse('Something went wrong while updating user.', 500, err.message)); } }); -module.exports.deleteFollowUser = asyncHandler(async (req, res, next) => { +module.exports.updateUserRole = asyncHandler(async (req, res, next) => { try { - return res.status(200).json({ - message: 'The user is no longer being followed.', - user: true + let user = await UserService.getUserById(req.payload.id); + if (!user) { + return next(new ErrorResponse("Unauthorized", 401, "You are not allowed to access this resource.")); + } + + fieldValidation(req.body.user.role, next); + if (typeof req.body.user.role !== 'undefined') { + user.role = req.body.user.role; + } + + user = await UserService.updateUserById(user, req.payload.id); + res.status(200).json({ + user: user.toAuthJSON(), }); } catch (err) { - return next(new ErrorResponse('Something went wrong while unfollowing a user.', 500, err.message)); + return next(new ErrorResponse('Something went wrong while updating user.', 500, err.message)); } }); + +module.exports.updateUserUsername = asyncHandler(async (req, res, next) => { + try { + let user = await UserService.getUserById(req.payload.id); + if (!user) { + return next(new ErrorResponse("Unauthorized", 401, "You are not allowed to access this resource.")); + } + + fieldValidation(req.body.user.username, next); + if (typeof req.body.user.username !== 'undefined') { + user.username = req.body.user.username; + } + + user = await UserService.updateUserById(user, req.payload.id); + res.status(200).json({ + user: user.toAuthJSON(), + }); + } catch (err) { + return next(new ErrorResponse('Something went wrong while updating user.', 500, err.message)); + } +}); + +const fieldValidation = (field, next) => { + if (!field) { + return next(new ErrorResponse(`Missing fields`, 400)); + } +}; diff --git a/src/database/migrations/20251127000000-initial-schema.js b/src/database/migrations/20251127000000-initial-schema.js index 93f8c4d..2f0098d 100644 --- a/src/database/migrations/20251127000000-initial-schema.js +++ b/src/database/migrations/20251127000000-initial-schema.js @@ -8,7 +8,7 @@ module.exports = { // Create User table await queryInterface.createTable('user', { id: { - type: Sequelize.STRING(24), + type: Sequelize.STRING(36), allowNull: false, primaryKey: true }, @@ -202,7 +202,7 @@ module.exports = { allowNull: false }, authorId: { - type: Sequelize.STRING(24), + type: Sequelize.STRING(36), allowNull: true, references: { model: 'user', @@ -236,7 +236,7 @@ module.exports = { allowNull: false }, authorId: { - type: Sequelize.STRING(24), + type: Sequelize.STRING(36), allowNull: true, references: { model: 'user', @@ -306,7 +306,7 @@ module.exports = { // Create Favorite table (junction table for User-Article) await queryInterface.createTable('favorite', { userId: { - type: Sequelize.STRING(24), + type: Sequelize.STRING(36), allowNull: false, primaryKey: true, references: { @@ -342,7 +342,7 @@ module.exports = { // Create Follower table (junction table for User-User) await queryInterface.createTable('follower', { userId: { - type: Sequelize.STRING(24), + type: Sequelize.STRING(36), allowNull: false, primaryKey: true, references: { @@ -353,7 +353,7 @@ module.exports = { onUpdate: 'CASCADE' }, followerId: { - type: Sequelize.STRING(24), + type: Sequelize.STRING(36), allowNull: false, primaryKey: true, references: { @@ -437,7 +437,7 @@ module.exports = { onUpdate: 'CASCADE' }, ownerId: { - type: Sequelize.STRING(24), + type: Sequelize.STRING(36), allowNull: true, references: { model: 'user', @@ -471,7 +471,7 @@ module.exports = { // Create UserRoleXref table (junction table for User-Role) await queryInterface.createTable('userRoleXref', { userId: { - type: Sequelize.STRING(24), + type: Sequelize.STRING(36), allowNull: false, primaryKey: true, references: { diff --git a/src/database/models/mysql/User.js b/src/database/models/mysql/User.js index 28d575b..ae8dbd5 100644 --- a/src/database/models/mysql/User.js +++ b/src/database/models/mysql/User.js @@ -11,7 +11,7 @@ const UserModel = () => { User.init( { id: { - type: DataTypes.STRING(24), + type: DataTypes.UUID(), allowNull: false, primaryKey: true }, @@ -87,16 +87,16 @@ const UserModel = () => { } ); - User.beforeCreate((user) => { + /*User.beforeCreate((user) => { if (typeof user.username === 'undefined') { user.username = `${user.firstname}_${user.lastname}`; } user.setPassword(user.password); user.role = 'User'; - }); + });*/ User.prototype.validPassword = function (password) { - let hash = crypto.pbkdf2Sync(password, this.salt, 10000, 512, 'sha512').toString('hex'); + const hash = crypto.pbkdf2Sync(password, this.salt, 10000, 512, 'sha512').toString('hex'); return this.hash === hash; }; diff --git a/src/database/seeders/20251126-add-roles.js b/src/database/seeders/20251126-add-roles.js new file mode 100644 index 0000000..731969b --- /dev/null +++ b/src/database/seeders/20251126-add-roles.js @@ -0,0 +1,38 @@ +'use strict'; + +module.exports = { + async up(queryInterface, Sequelize) { + try { + // Create roles + await queryInterface.bulkInsert('role', [ + { + name: 'Admin', + createdAt: new Date(), + updatedAt: new Date() + }, + { + name: 'User', + createdAt: new Date(), + updatedAt: new Date() + } + ], {}); + + console.log('✅ Rôles créés avec succès'); + } catch (error) { + console.error('❌ Erreur lors de la création des rôles:', error); + throw error; + } + }, + + async down(queryInterface, Sequelize) { + try { + // Delete roles + await queryInterface.bulkDelete('role', { name: { [Sequelize.Op.in]: ['Admin', 'User'] } }, {}); + + console.log('✅ Rôles supprimés avec succès'); + } catch (error) { + console.error('❌ Erreur lors de la suppression des rôles:', error); + throw error; + } + } +}; diff --git a/src/database/seeders/20251127-add-articles.js b/src/database/seeders/20251127-add-articles.js index 72dfe82..0036e4f 100644 --- a/src/database/seeders/20251127-add-articles.js +++ b/src/database/seeders/20251127-add-articles.js @@ -4,7 +4,7 @@ const slug = require('slug'); const ANIMAL_TAGS = ['Lion', 'Eagle', 'Dolphin', 'Tiger', 'Wolf']; -const AUTHOR_ID = '642c4459666702637dcb5066'; +// AUTHOR_ID will be resolved at runtime to the oldest user with role 'Admin' const ARTICLES = [ { @@ -46,6 +46,40 @@ module.exports = { // Get transaction for consistency const transaction = await queryInterface.sequelize.transaction(); + // Resolve AUTHOR_ID: oldest user who has role 'Admin' + let AUTHOR_ID = null; + try { + const adminQuery = await queryInterface.sequelize.query( + `SELECT u.id FROM \`user\` u + JOIN userRoleXref ur ON ur.userId = u.id + JOIN role r ON r.id = ur.roleId + WHERE r.name = ? + ORDER BY u.createdAt ASC + LIMIT 1`, + { replacements: ['Admin'], transaction, raw: true } + ); + + if (adminQuery && adminQuery[0] && adminQuery[0][0] && adminQuery[0][0].id) { + AUTHOR_ID = adminQuery[0][0].id; + } else { + // Fallback: pick the oldest user in `user` table + const fallback = await queryInterface.sequelize.query( + `SELECT id FROM \`user\` ORDER BY createdAt ASC LIMIT 1`, + { transaction, raw: true } + ); + if (fallback && fallback[0] && fallback[0][0] && fallback[0][0].id) { + AUTHOR_ID = fallback[0][0].id; + } + } + + if (!AUTHOR_ID) { + throw new Error('Aucun utilisateur trouvé pour être utilisé comme auteur (Admin absent).'); + } + } catch (err) { + await transaction.rollback(); + throw err; + } + try { // Create articles for (const articleData of ARTICLES) { @@ -127,7 +161,7 @@ module.exports = { } }, - async down(queryInterface, Sequelize) { + async down(queryInterface) { try { // Delete in correct order due to foreign keys await queryInterface.sequelize.query('DELETE FROM tagList WHERE 1=1'); diff --git a/src/database/seeders/20251204-add-roles.js b/src/database/seeders/20251204-add-roles.js new file mode 100644 index 0000000..731969b --- /dev/null +++ b/src/database/seeders/20251204-add-roles.js @@ -0,0 +1,38 @@ +'use strict'; + +module.exports = { + async up(queryInterface, Sequelize) { + try { + // Create roles + await queryInterface.bulkInsert('role', [ + { + name: 'Admin', + createdAt: new Date(), + updatedAt: new Date() + }, + { + name: 'User', + createdAt: new Date(), + updatedAt: new Date() + } + ], {}); + + console.log('✅ Rôles créés avec succès'); + } catch (error) { + console.error('❌ Erreur lors de la création des rôles:', error); + throw error; + } + }, + + async down(queryInterface, Sequelize) { + try { + // Delete roles + await queryInterface.bulkDelete('role', { name: { [Sequelize.Op.in]: ['Admin', 'User'] } }, {}); + + console.log('✅ Rôles supprimés avec succès'); + } catch (error) { + console.error('❌ Erreur lors de la suppression des rôles:', error); + throw error; + } + } +}; diff --git a/src/database/seeders/index.js b/src/database/seeders/index.js deleted file mode 100644 index 35ea9d0..0000000 --- a/src/database/seeders/index.js +++ /dev/null @@ -1,2 +0,0 @@ -// Seeders managed by individual migration files -// See seeders directory for specific seeder implementations diff --git a/src/routes/api/v2/user.routes.js b/src/routes/api/v2/user.routes.js index ecf7d24..41b5c47 100644 --- a/src/routes/api/v2/user.routes.js +++ b/src/routes/api/v2/user.routes.js @@ -1,5 +1,8 @@ const express = require('express'); -const { authenticate, createUser, getUser, loginAsUser, updateUser, addFollowUser, deleteFollowUser } = require('../../../controllers/user.controller'); +const { + addFollowUser, authenticate, createUser, deleteFollowUser, getUser, loginAsUser, + updateUser, updateUserEmail, updateUserPassword, updateUserRole, updateUserUsername +} = require('../../../controllers/user.controller'); const auth = require('../../../middlewares/auth'); const userRouter = express.Router(); @@ -11,6 +14,10 @@ userRouter.get('/authenticate', auth.optional, authenticate); userRouter.post('/login', auth.optional, loginAsUser); //userRouter.post('/register', auth.optional, createUser); //userRouter.get('/users', auth.required, getAllUsers); +userRouter.put('/update/email', auth.required, updateUserEmail); +userRouter.put('/update/password', auth.required, updateUserPassword); +userRouter.put('/update/role', auth.required, updateUserRole); +userRouter.put('/update/username', auth.required, updateUserUsername); userRouter.post('/:username/follow', auth.required, addFollowUser); userRouter.delete('/:username/follow', auth.required, deleteFollowUser); diff --git a/src/services/mysql/user.service.js b/src/services/mysql/user.service.js index 34c80f7..f8b4184 100644 --- a/src/services/mysql/user.service.js +++ b/src/services/mysql/user.service.js @@ -1,12 +1,27 @@ +const slug = require("slug"); const DB = require('../../database/mysql'); +var crypto = require('crypto'); const { User } = DB; class UserService { static async createUser(data) { + if (typeof data.username === 'undefined') { + data.username = UserService.slugify(`${data.firstname.charAt(0)}${data.lastname.charAt(0)}`); + } + const {salt, hash} = await UserService.generateSaltHash(data.password); + data.salt = salt; + data.hash = hash; + data.role = 'User'; return User.create(data); } + static async generateSaltHash(password) { + const salt = crypto.randomBytes(16).toString('hex'); + const hash = crypto.pbkdf2Sync(password, salt, 10000, 512, 'sha512').toString('hex'); + return {salt, hash}; + } + static async getAllUsers() { return User.findAll({ order: [['createdAt', 'DESC']], @@ -36,14 +51,19 @@ class UserService { return user.getArticleIdArticles(searchOptions); } + static async getFavoritesCount(user) { + return user.countArticleIdArticles(); + } + static async getUserFollowed(user) { // Sequelize generated method for "users this user follows" is named // `getUserIdUserFollowers()` (see `src/database/relationships/index.js`). return user.getUserIdUserFollowers(); } - static async getFavoritesCount(user) { - return user.countArticleIdArticles(); + static async isUsernameUsed(username) { + const user = await User.findOne({ where: { username: username } }); + return !!user; } static async addFavoriteArticle(user, article) { @@ -53,9 +73,25 @@ class UserService { static async removeFavoriteArticle(user, article) { return user.removeArticleIdArticles(article); } + + static slugify(stringToSlug) { + return slug(`${stringToSlug}`) + '-' + (Math.random() * Math.pow(36, 6) | 0).toString(36); + } - static async updateUser(data) { - return User.update(data); + static async updateUserById(data, id) { + // If `data` is a Sequelize instance, prefer instance.save() + try { + if (data && typeof data.save === 'function') { + const saved = await data.save(); + return saved; + } + } catch { + // fallthrough to update + } + + // Otherwise perform a standard update and return the refreshed instance + await User.update(data, { where: { id: id } }); + return User.findByPk(id); } }