fix(user): persist updateUser correctly; add setup:api script and README update; seeders tweaks

This commit is contained in:
2025-12-05 01:33:38 +01:00
parent bd263c01b9
commit b710b34569
20 changed files with 1637 additions and 53 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ DEBUG=false
SERVER_PORT=3201 SERVER_PORT=3201
SECRET="$2b$10$2.JkQCRUa3fXESy/WyKVWO" SECRET="$2b$10$2.JkQCRUa3fXESy/WyKVWO"
SESSION_LIFETIME=3600 SESSION_LIFETIME=3600
MONGODB_URI="mongodb://localhost:27017/headupdb" MONGODB_URI="mongodb://localhost:27017/adastradb"
SENDGRID_API_KEY="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" SENDGRID_API_KEY="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"
SENDGRID_FROM_MAIL="contact@rampeur.fr" SENDGRID_FROM_MAIL="contact@rampeur.fr"
SENDGRID_TO_MAIL="rampeur@gmail.com" SENDGRID_TO_MAIL="rampeur@gmail.com"
+1 -1
View File
@@ -4,7 +4,7 @@ DEBUG=false
SERVER_PORT=3201 SERVER_PORT=3201
SECRET="$2b$10$2.JkQCRUa3fXESy/WyKVWO" SECRET="$2b$10$2.JkQCRUa3fXESy/WyKVWO"
SESSION_LIFETIME=86400 SESSION_LIFETIME=86400
MONGODB_URI="mongodb://localhost:27017/headupdb" MONGODB_URI="mongodb://localhost:27017/adastradb"
SENDGRID_API_KEY="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" SENDGRID_API_KEY="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"
SENDGRID_FROM_MAIL="contact@rampeur.fr" SENDGRID_FROM_MAIL="contact@rampeur.fr"
SENDGRID_TO_MAIL="rampeur@gmail.com" SENDGRID_TO_MAIL="rampeur@gmail.com"
+1 -1
View File
@@ -4,7 +4,7 @@ DEBUG=false
SERVER_PORT=3201 SERVER_PORT=3201
SECRET="$2b$10$2.JkQCRUa3fXESy/WyKVWO" SECRET="$2b$10$2.JkQCRUa3fXESy/WyKVWO"
SESSION_LIFETIME=3600 SESSION_LIFETIME=3600
MONGODB_URI="mongodb://localhost:27017/headupdb" MONGODB_URI="mongodb://localhost:27017/adastradb"
SENDGRID_API_KEY="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" SENDGRID_API_KEY="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"
SENDGRID_FROM_MAIL="contact@rampeur.fr" SENDGRID_FROM_MAIL="contact@rampeur.fr"
SENDGRID_TO_MAIL="rampeur@gmail.com" SENDGRID_TO_MAIL="rampeur@gmail.com"
+1 -1
View File
@@ -4,7 +4,7 @@ DEBUG=false
SERVER_PORT=3201 SERVER_PORT=3201
SECRET="$2b$10$2.JkQCRUa3fXESy/WyKVWO" SECRET="$2b$10$2.JkQCRUa3fXESy/WyKVWO"
SESSION_LIFETIME=3600 SESSION_LIFETIME=3600
MONGODB_URI="mongodb://localhost:27017/headupdb" MONGODB_URI="mongodb://localhost:27017/adastradb"
SENDGRID_API_KEY="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" SENDGRID_API_KEY="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"
SENDGRID_FROM_MAIL="contact@rampeur.fr" SENDGRID_FROM_MAIL="contact@rampeur.fr"
SENDGRID_TO_MAIL="rampeur@gmail.com" SENDGRID_TO_MAIL="rampeur@gmail.com"
+156
View File
@@ -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.)
+210
View File
@@ -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.
+50
View File
@@ -2,7 +2,42 @@
API pour l'application Ad Astra 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 # Exécuter tous les seeders
npx sequelize-cli db:seed:all npx sequelize-cli db:seed:all
@@ -14,4 +49,19 @@ npx sequelize-cli db:seed:undo:all
# Annuler un seeder spécifique # Annuler un seeder spécifique
npx sequelize-cli db:seed:undo --seed 20251127-add-articles.js npx sequelize-cli db:seed:undo --seed 20251127-add-articles.js
```
**Start 'local' API server**
```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
```
+762
View File
@@ -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
```
+1 -1
View File
@@ -6,7 +6,7 @@
"SERVER_PORT": 3201, "SERVER_PORT": 3201,
"SECRET": "$2b$10$aEsAUG7Dy8dcTORi1vaQle", "SECRET": "$2b$10$aEsAUG7Dy8dcTORi1vaQle",
"SESSION_LIFETIME": 3600, "SESSION_LIFETIME": 3600,
"MONGODB_URI": "mongodb://localhost:27017/headupdb", "MONGODB_URI": "mongodb://localhost:27017/adastradb",
"SENDGRID_API_KEY": "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", "SENDGRID_API_KEY": "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",
"SENDGRID_FROM_MAIL": "contact@rampeur.fr", "SENDGRID_FROM_MAIL": "contact@rampeur.fr",
"SENDGRID_TO_MAIL": "rampeur@gmail.com", "SENDGRID_TO_MAIL": "rampeur@gmail.com",
+3 -1
View File
@@ -14,7 +14,9 @@
"test": "newman run ./tests/adastra-api-tests.postman_collection.json -e ./tests/adastra-api-tests.postman_environment.json", "test": "newman run ./tests/adastra-api-tests.postman_collection.json -e ./tests/adastra-api-tests.postman_environment.json",
"stop": "lsof -ti :3200 | xargs kill", "stop": "lsof -ti :3200 | xargs kill",
"mongo:start": "docker run --name hu-mongo -p 27017:27017 mongo & sleep 5", "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": { "engines": {
"node": "^16.20.2 || ^18.19.1 || ^20.11.1" "node": "^16.20.2 || ^18.19.1 || ^20.11.1"
+154
View File
@@ -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();
+125 -26
View File
@@ -1,8 +1,21 @@
const { UserService } = require('../services'); const { UserService } = require('../services');
const asyncHandler = require("../middlewares/asyncHandler"); const asyncHandler = require("../middlewares/asyncHandler");
const ErrorResponse = require("../utils/errorResponse"); const ErrorResponse = require("../utils/errorResponse");
var crypto = require('crypto');
const passport = require('passport'); 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) => { module.exports.authenticate = asyncHandler(async (req, res, next) => {
try { try {
passport.authenticate('headerapikey', { session: false }, function (err, application, info) { 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) => { module.exports.createUser = asyncHandler(async (req, res, next) => {
try { 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({ const user = await UserService.createUser({
id: uuid,
username, username,
firstname, firstname,
lastname, lastname,
email phone,
email,
password
}); });
res.status(201).json({ 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) => { module.exports.getAllUsers = asyncHandler(async (req, res, next) => {
try { try {
const users = await UserService.getAllUsers(); const users = await UserService.getAllUsers();
@@ -69,6 +108,7 @@ module.exports.getUser = asyncHandler(async (req, res, next) => {
res.status(200).json({ res.status(200).json({
user: user.toAuthJSON(), user: user.toAuthJSON(),
uuid: crypto.randomUUID()
}); });
} catch (err) { } catch (err) {
return next(new ErrorResponse('Something went wrong while fetching user.', 500, err.message)); return next(new ErrorResponse('Something went wrong while fetching user.', 500, err.message));
@@ -105,12 +145,6 @@ module.exports.updateUser = asyncHandler(async (req, res, next) => {
if (!user) { if (!user) {
return next(new ErrorResponse("Unauthorized", 401, "You are not allowed to access this resource.")); 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') { if (typeof req.body.user.firstname !== 'undefined') {
user.firstname = req.body.user.firstname; 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') { if (typeof req.body.user.bg_image !== 'undefined') {
user.bg_image = req.body.user.bg_image; 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') { 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({ res.status(200).json({
user: user.toAuthJSON(), user: updatedUser.toAuthJSON()
});*/ });
return user.save().then(function () { /*return user.save().then(function () {
return res.json({ return res.json({
message: 'User updated successfully.', message: 'User updated successfully.',
user: user.toAuthJSON() 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) { } catch (err) {
return next(new ErrorResponse('Something went wrong while updating user.', 500, err.message)); return next(new ErrorResponse('Something went wrong while updating user.', 500, err.message));
} }
}); });
module.exports.addFollowUser = asyncHandler(async (req, res, next) => { module.exports.updateUserPassword = asyncHandler(async (req, res, next) => {
try { try {
return res.status(200).json({ let user = await UserService.getUserById(req.payload.id);
message: 'The user is now being followed.', if (!user) {
user: true 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) { } 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 { try {
return res.status(200).json({ let user = await UserService.getUserById(req.payload.id);
message: 'The user is no longer being followed.', if (!user) {
user: true 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) { } 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));
}
};
@@ -8,7 +8,7 @@ module.exports = {
// Create User table // Create User table
await queryInterface.createTable('user', { await queryInterface.createTable('user', {
id: { id: {
type: Sequelize.STRING(24), type: Sequelize.STRING(36),
allowNull: false, allowNull: false,
primaryKey: true primaryKey: true
}, },
@@ -202,7 +202,7 @@ module.exports = {
allowNull: false allowNull: false
}, },
authorId: { authorId: {
type: Sequelize.STRING(24), type: Sequelize.STRING(36),
allowNull: true, allowNull: true,
references: { references: {
model: 'user', model: 'user',
@@ -236,7 +236,7 @@ module.exports = {
allowNull: false allowNull: false
}, },
authorId: { authorId: {
type: Sequelize.STRING(24), type: Sequelize.STRING(36),
allowNull: true, allowNull: true,
references: { references: {
model: 'user', model: 'user',
@@ -306,7 +306,7 @@ module.exports = {
// Create Favorite table (junction table for User-Article) // Create Favorite table (junction table for User-Article)
await queryInterface.createTable('favorite', { await queryInterface.createTable('favorite', {
userId: { userId: {
type: Sequelize.STRING(24), type: Sequelize.STRING(36),
allowNull: false, allowNull: false,
primaryKey: true, primaryKey: true,
references: { references: {
@@ -342,7 +342,7 @@ module.exports = {
// Create Follower table (junction table for User-User) // Create Follower table (junction table for User-User)
await queryInterface.createTable('follower', { await queryInterface.createTable('follower', {
userId: { userId: {
type: Sequelize.STRING(24), type: Sequelize.STRING(36),
allowNull: false, allowNull: false,
primaryKey: true, primaryKey: true,
references: { references: {
@@ -353,7 +353,7 @@ module.exports = {
onUpdate: 'CASCADE' onUpdate: 'CASCADE'
}, },
followerId: { followerId: {
type: Sequelize.STRING(24), type: Sequelize.STRING(36),
allowNull: false, allowNull: false,
primaryKey: true, primaryKey: true,
references: { references: {
@@ -437,7 +437,7 @@ module.exports = {
onUpdate: 'CASCADE' onUpdate: 'CASCADE'
}, },
ownerId: { ownerId: {
type: Sequelize.STRING(24), type: Sequelize.STRING(36),
allowNull: true, allowNull: true,
references: { references: {
model: 'user', model: 'user',
@@ -471,7 +471,7 @@ module.exports = {
// Create UserRoleXref table (junction table for User-Role) // Create UserRoleXref table (junction table for User-Role)
await queryInterface.createTable('userRoleXref', { await queryInterface.createTable('userRoleXref', {
userId: { userId: {
type: Sequelize.STRING(24), type: Sequelize.STRING(36),
allowNull: false, allowNull: false,
primaryKey: true, primaryKey: true,
references: { references: {
+4 -4
View File
@@ -11,7 +11,7 @@ const UserModel = () => {
User.init( User.init(
{ {
id: { id: {
type: DataTypes.STRING(24), type: DataTypes.UUID(),
allowNull: false, allowNull: false,
primaryKey: true primaryKey: true
}, },
@@ -87,16 +87,16 @@ const UserModel = () => {
} }
); );
User.beforeCreate((user) => { /*User.beforeCreate((user) => {
if (typeof user.username === 'undefined') { if (typeof user.username === 'undefined') {
user.username = `${user.firstname}_${user.lastname}`; user.username = `${user.firstname}_${user.lastname}`;
} }
user.setPassword(user.password); user.setPassword(user.password);
user.role = 'User'; user.role = 'User';
}); });*/
User.prototype.validPassword = function (password) { 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; return this.hash === hash;
}; };
@@ -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;
}
}
};
+36 -2
View File
@@ -4,7 +4,7 @@ const slug = require('slug');
const ANIMAL_TAGS = ['Lion', 'Eagle', 'Dolphin', 'Tiger', 'Wolf']; 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 = [ const ARTICLES = [
{ {
@@ -46,6 +46,40 @@ module.exports = {
// Get transaction for consistency // Get transaction for consistency
const transaction = await queryInterface.sequelize.transaction(); 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 { try {
// Create articles // Create articles
for (const articleData of ARTICLES) { for (const articleData of ARTICLES) {
@@ -127,7 +161,7 @@ module.exports = {
} }
}, },
async down(queryInterface, Sequelize) { async down(queryInterface) {
try { try {
// Delete in correct order due to foreign keys // Delete in correct order due to foreign keys
await queryInterface.sequelize.query('DELETE FROM tagList WHERE 1=1'); await queryInterface.sequelize.query('DELETE FROM tagList WHERE 1=1');
@@ -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;
}
}
};
-2
View File
@@ -1,2 +0,0 @@
// Seeders managed by individual migration files
// See seeders directory for specific seeder implementations
+8 -1
View File
@@ -1,5 +1,8 @@
const express = require('express'); 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 auth = require('../../../middlewares/auth');
const userRouter = express.Router(); const userRouter = express.Router();
@@ -11,6 +14,10 @@ userRouter.get('/authenticate', auth.optional, authenticate);
userRouter.post('/login', auth.optional, loginAsUser); userRouter.post('/login', auth.optional, loginAsUser);
//userRouter.post('/register', auth.optional, createUser); //userRouter.post('/register', auth.optional, createUser);
//userRouter.get('/users', auth.required, getAllUsers); //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.post('/:username/follow', auth.required, addFollowUser);
userRouter.delete('/:username/follow', auth.required, deleteFollowUser); userRouter.delete('/:username/follow', auth.required, deleteFollowUser);
+40 -4
View File
@@ -1,12 +1,27 @@
const slug = require("slug");
const DB = require('../../database/mysql'); const DB = require('../../database/mysql');
var crypto = require('crypto');
const { User } = DB; const { User } = DB;
class UserService { class UserService {
static async createUser(data) { 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); 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() { static async getAllUsers() {
return User.findAll({ return User.findAll({
order: [['createdAt', 'DESC']], order: [['createdAt', 'DESC']],
@@ -36,14 +51,19 @@ class UserService {
return user.getArticleIdArticles(searchOptions); return user.getArticleIdArticles(searchOptions);
} }
static async getFavoritesCount(user) {
return user.countArticleIdArticles();
}
static async getUserFollowed(user) { static async getUserFollowed(user) {
// Sequelize generated method for "users this user follows" is named // Sequelize generated method for "users this user follows" is named
// `getUserIdUserFollowers()` (see `src/database/relationships/index.js`). // `getUserIdUserFollowers()` (see `src/database/relationships/index.js`).
return user.getUserIdUserFollowers(); return user.getUserIdUserFollowers();
} }
static async getFavoritesCount(user) { static async isUsernameUsed(username) {
return user.countArticleIdArticles(); const user = await User.findOne({ where: { username: username } });
return !!user;
} }
static async addFavoriteArticle(user, article) { static async addFavoriteArticle(user, article) {
@@ -54,8 +74,24 @@ class UserService {
return user.removeArticleIdArticles(article); return user.removeArticleIdArticles(article);
} }
static async updateUser(data) { static slugify(stringToSlug) {
return User.update(data); return slug(`${stringToSlug}`) + '-' + (Math.random() * Math.pow(36, 6) | 0).toString(36);
}
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);
} }
} }