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
+125 -26
View File
@@ -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));
}
};
@@ -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: {
+4 -4
View File
@@ -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;
};
@@ -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 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');
@@ -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 { 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);
+40 -4
View File
@@ -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);
}
}