diff --git a/.env b/.env new file mode 100644 index 0000000..948fdc4 --- /dev/null +++ b/.env @@ -0,0 +1,15 @@ +APP_ENV="development" +NODE_ENV="development" +DEBUG=false +SERVER_PORT="3200" +SECRET="$2b$10$2.JkQCRUa3fXESy/WyKVWO" +SESSION_LIFETIME="3600" +SENDGRID_API_KEY="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" +SENDGRID_FROM_MAIL="contact@rampeur.fr" +SENDGRID_TO_MAIL="rampeur@gmail.com" +MYSQL_DBNAME="c4adastracomdb1" +MYSQL_DBUSER="c4adastracomu1" +MYSQL_DBPASS="kbrnrt5jPK12" +MYSQL_DBHOST="ns388640.ip-176-31-255.eu" +MYSQL_DBPORT="3306" +MYSQL_DBTYPE="mysql" \ No newline at end of file diff --git a/.env.development b/.env.development new file mode 100644 index 0000000..cebbad5 --- /dev/null +++ b/.env.development @@ -0,0 +1,15 @@ +APP_ENV="development" +NODE_ENV="development" +DEBUG=false +SERVER_PORT="3200" +SECRET="$2b$10$2.JkQCRUa3fXESy/WyKVWO" +SESSION_LIFETIME=3600 +SENDGRID_API_KEY="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" +SENDGRID_FROM_MAIL="contact@rampeur.fr" +SENDGRID_TO_MAIL="rampeur@gmail.com" +MYSQL_DBNAME="c4adastracomdb1" +MYSQL_DBUSER="c4adastracomu1" +MYSQL_DBPASS="kbrnrt5jPK12" +MYSQL_DBHOST="ns388640.ip-176-31-255.eu" +MYSQL_DBPORT="3306" +MYSQL_DBTYPE="mysql" diff --git a/.env.local b/.env.local new file mode 100644 index 0000000..791da64 --- /dev/null +++ b/.env.local @@ -0,0 +1,15 @@ +APP_ENV="local" +NODE_ENV="development" +DEBUG=false +SERVER_PORT="3200" +SECRET="$2b$10$2.JkQCRUa3fXESy/WyKVWO" +SESSION_LIFETIME=3600 +SENDGRID_API_KEY="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" +SENDGRID_FROM_MAIL="contact@rampeur.fr" +SENDGRID_TO_MAIL="rampeur@gmail.com" +MYSQL_DBNAME="c4adastracomdb1" +MYSQL_DBUSER="c4adastracomu1" +MYSQL_DBPASS="kbrnrt5jPK12" +MYSQL_DBHOST="ns388640.ip-176-31-255.eu" +MYSQL_DBPORT="3306" +MYSQL_DBTYPE="mysql" diff --git a/.env.production b/.env.production new file mode 100644 index 0000000..aaca62e --- /dev/null +++ b/.env.production @@ -0,0 +1,15 @@ +APP_ENV="production" +NODE_ENV="production" +DEBUG=false +SERVER_PORT="3200" +SECRET="$2b$10$2.JkQCRUa3fXESy/WyKVWO" +SESSION_LIFETIME=3600 +SENDGRID_API_KEY="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" +SENDGRID_FROM_MAIL="contact@rampeur.fr" +SENDGRID_TO_MAIL="rampeur@gmail.com" +MYSQL_DBNAME="c4adastracomdb1" +MYSQL_DBUSER="c4adastracomu1" +MYSQL_DBPASS="kbrnrt5jPK12" +MYSQL_DBHOST="ns388640.ip-176-31-255.eu" +MYSQL_DBPORT="3306" +MYSQL_DBTYPE="mysql" diff --git a/controllers/articles.js b/controllers/articles.js new file mode 100644 index 0000000..0e84af7 --- /dev/null +++ b/controllers/articles.js @@ -0,0 +1,265 @@ +const { where } = require("sequelize"); +const asyncHandler = require("../middlewares/asyncHandler"); +const Article = require("../models/Article"); +const Tag = require("../models/Tag"); +const User = require("../models/User"); +const ErrorResponse = require("../util/errorResponse"); +const slugify = require("slugify"); + +const { + appendFollowers, + appendFavorites, + appendTagList, +} = require("../util/helpers"); + +const includeOptions = [ + { + model: Tag, + as: "tagLists", + attributes: ["name"], + through: { attributes: [] }, + }, + { model: User, as: "author", attributes: { exclude: ["email", "password"] } }, +]; + +module.exports.getArticles = asyncHandler(async (req, res, next) => { + const { tag, author, favorited, limit = 20, offset = 0 } = req.query; + const { loggedUser } = req; + + const searchOptions = { + include: [ + { + model: Tag, + as: "tagLists", + attributes: ["name"], + through: { attributes: [] }, // ? this will remove the rows from the join table + // where: tag ? { name: tag } : {}, + ...(tag && { where: { name: tag } }), + }, + { + model: User, + as: "author", + attributes: { exclude: ["password", "email"] }, + // where: author ? { username: author } : {}, + ...(author && { where: { username: author } }), + }, + ], + limit: parseInt(limit), + offset: parseInt(offset), + order: [["createdAt", "DESC"]], + distinct: true, + }; + + let articles = { rows: [], count: 0 }; + + if (favorited) { + const user = await User.findOne({ where: { username: favorited } }); + + articles.rows = await user.getFavorites(searchOptions); + articles.count = await user.countFavorites(); + } else { + articles = await Article.findAndCountAll(searchOptions); + } + + for (let article of articles.rows) { + const articleTags = await article.getTagLists(); + // const articleTags = article.tagLists; + appendTagList(articleTags, article); + await appendFollowers(loggedUser, article); + await appendFavorites(loggedUser, article); + + delete article.dataValues.Favorites; + } + + res + .status(200) + .json({ articles: articles.rows, articlesCount: articles.count }); +}); + +module.exports.createArticle = asyncHandler(async (req, res, next) => { + const { loggedUser } = req; + + fieldValidation(req.body.article.title, next); + fieldValidation(req.body.article.description, next); + fieldValidation(req.body.article.body, next); + + const { title, description, body, tagList } = req.body.article; + const slug = slugify(title, { lower: true }); + const slugInDB = await Article.findOne({ where: { slug: slug } }); + if (slugInDB) next(new ErrorResponse("Title already exists", 400)); + + const article = await Article.create({ + title: title, + description: description, + body: body, + }); + + for (const tag of tagList) { + const tagInDB = await Tag.findByPk(tag.trim()); + + if (tagInDB) { + await article.addTagList(tagInDB); + } else if (tag.length > 2) { + const newTag = await Tag.create({ name: tag.trim() }); + await article.addTagList(newTag); + } + } + delete loggedUser.dataValues.token; + + article.dataValues.tagList = tagList; + article.setAuthor(loggedUser); + article.dataValues.author = loggedUser; + await appendFollowers(loggedUser, loggedUser); + await appendFavorites(loggedUser, article); + + res.status(201).json({ article }); +}); + +module.exports.deleteArticle = asyncHandler(async (req, res, next) => { + const { slug } = req.params; + const { loggedUser } = req; + + const article = await Article.findOne({ + where: { slug: slug }, + include: includeOptions, + }); + + if (!article) next(new ErrorResponse("Article not found", 404)); + + if (article.authorId !== loggedUser.id) + return next(new ErrorResponse("Unauthorized", 401)); + + await article.destroy(); + + res.status(200).json({ article }); +}); + +module.exports.updateArticle = asyncHandler(async (req, res, next) => { + const { slug } = req.params; + const { loggedUser } = req; + + const article = await Article.findOne({ + where: { slug: slug }, + include: includeOptions, + }); + + if (!article) next(new ErrorResponse("Article not found", 404)); + + if (article.authorId !== loggedUser.id) + return next(new ErrorResponse("Unauthorized", 401)); + + const { title, description, body } = req.body.article; + + const slugInDB = await Article.findOne({ + where: { slug: slugify(title ? title : article.title, { lower: true }) }, + }); + + if (slugInDB && slugInDB.slug !== slug) + return next(new ErrorResponse("Title already exists", 400)); + + await article.update({ + title: title ? title : article.title, + description: description ? description : article.description, + body: body ? body : article.body, + }); + + const articleTags = await article.getTagLists(); + appendTagList(articleTags, article); + await appendFollowers(loggedUser, article); + await appendFavorites(loggedUser, article); + + res.status(200).json({ article }); +}); + +module.exports.articlesFeed = asyncHandler(async (req, res, next) => { + const { loggedUser } = req; + + const { limit = 3, offset = 0 } = req.query; + const authors = await loggedUser.getFollowing(); + + const articles = await Article.findAndCountAll({ + include: includeOptions, + limit: parseInt(limit), + offset: offset * limit, + order: [["createdAt", "DESC"]], + where: { authorId: authors.map((author) => author.id) }, + distinct: true, + }); + + for (const article of articles.rows) { + const articleTags = await article.getTagLists(); + + appendTagList(articleTags, article); + await appendFollowers(loggedUser, article); + await appendFavorites(loggedUser, article); + } + + res.json({ articles: articles.rows, articlesCount: articles.count }); +}); + +module.exports.getArticle = asyncHandler(async (req, res, next) => { + const { loggedUser } = req; + const { slug } = req.params; + + const article = await Article.findOne({ + where: { slug: slug }, + include: includeOptions, + }); + + if (!article) return next(new ErrorResponse("Article not found", 404)); + + const articleTags = await article.getTagLists(); + appendTagList(articleTags, article); + await appendFollowers(loggedUser, article); + await appendFavorites(loggedUser, article); + + res.status(200).json({ article }); +}); + +module.exports.addFavoriteArticle = asyncHandler(async (req, res, next) => { + const { loggedUser } = req; + const { slug } = req.params; + + const article = await Article.findOne({ + where: { slug: slug }, + include: includeOptions, + }); + + if (!article) return next(new ErrorResponse("Article not found", 404)); + + await loggedUser.addFavorite(article); + + const articleTags = await article.getTagLists(); + appendTagList(articleTags, article); + await appendFollowers(loggedUser, article); + await appendFavorites(loggedUser, article); + + res.status(200).json({ article }); +}); + +module.exports.deleteFavoriteArticle = asyncHandler(async (req, res, next) => { + const { loggedUser } = req; + const { slug } = req.params; + + const article = await Article.findOne({ + where: { slug: slug }, + include: includeOptions, + }); + + if (!article) return next(new ErrorResponse("Article not found", 404)); + + await loggedUser.removeFavorite(article); + + const articleTags = await article.getTagLists(); + appendTagList(articleTags, article); + await appendFollowers(loggedUser, article); + await appendFavorites(loggedUser, article); + + res.status(200).json({ article }); +}); + +const fieldValidation = (field, next) => { + if (!field) { + return next(new ErrorResponse(`Missing fields`, 400)); + } +}; diff --git a/controllers/comments.js b/controllers/comments.js new file mode 100644 index 0000000..b378ccc --- /dev/null +++ b/controllers/comments.js @@ -0,0 +1,120 @@ +const Comment = require("../models/Comment"); +const Article = require("../models/Article"); +const User = require("../models/User"); +const asyncHandler = require("../middlewares/asyncHandler"); +const { appendFollowers } = require("../util/helpers"); +const ErrorResponse = require("../util/errorResponse"); + +module.exports.getComments = asyncHandler(async (req, res, next) => { + const { slug } = req.params; + const { loggedUser } = req; + + const article = await Article.findOne({ + where: { slug: slug }, + }); + + if (!article) return next(new ErrorResponse("Article not found", 404)); + + const comments = await article.getComments({ + include: [ + { + model: User, + as: "author", + attributes: ["username", "bio", "image"], + }, + ], + }); + + for (let comment of comments) { + await appendFollowers(loggedUser, comment); + } + + res.status(200).json({ comments }); +}); + +module.exports.getComment = asyncHandler(async (req, res, next) => { + const { slug, id } = req.params; + const { loggedUser } = req; + + const article = await Article.findOne({ + where: { slug: slug }, + }); + + if (!article) return next(new ErrorResponse("Article not found", 404)); + + const comment = await Comment.findOne({ + where: { id: id }, + include: [ + { + model: User, + as: "author", + attributes: ["username", "bio", "image"], + }, + ], + }); + if (!comment) return next(new ErrorResponse("Comment not found", 404)); + + await appendFollowers(loggedUser, comment); + + res.status(200).json({ comment }); +}); + +module.exports.createComment = asyncHandler(async (req, res, next) => { + const { body } = req.body.comment; + const { slug } = req.params; + const { loggedUser } = req; + + fieldValidation(body, next); + + const article = await Article.findOne({ + where: { slug: slug }, + include: [ + { model: User, as: "author", attributes: ["username", "bio", "image"] }, + ], + }); + + if (!article) return next(new ErrorResponse("Article not found", 404)); + + const comment = await Comment.create({ + body: body, + articleId: article.id, + authorId: loggedUser.id, + }); + + delete loggedUser.dataValues.token; + await appendFollowers(loggedUser, loggedUser); + + comment.dataValues.author = loggedUser; + + res.status(201).json({ comment }); +}); + +module.exports.deleteComment = asyncHandler(async (req, res, next) => { + const { slug, id } = req.params; + const { loggedUser } = req; + + const article = await Article.findOne({ + where: { slug: slug }, + }); + + if (!article) return next(new ErrorResponse("Article not found", 404)); + + const comment = await Comment.findOne({ + where: { id: id }, + }); + + if (!comment) return next(new ErrorResponse("Comment not found", 404)); + + if (comment.authorId !== loggedUser.id) + return next(new ErrorResponse("Unauthorized", 401)); + + await comment.destroy(); + + res.status(200).json({ comment }); +}); + +const fieldValidation = (field, next) => { + if (!field) { + return next(new ErrorResponse(`Missing fields`, 400)); + } +}; diff --git a/controllers/profiles.js b/controllers/profiles.js new file mode 100644 index 0000000..8c21a04 --- /dev/null +++ b/controllers/profiles.js @@ -0,0 +1,74 @@ +const User = require("../models/User"); +const asyncHandler = require("../middlewares/asyncHandler"); +const ErrorResponse = require("../util/errorResponse"); + +module.exports.getProfile = asyncHandler(async (req, res, next) => { + const username = req.params.username; + const { loggedUser } = req; + const user = await User.findOne({ where: { username: username } }); + + if (!user) { + return next(new ErrorResponse(`User not found`, 404)); + } + + let isFollowing = false; + + if (loggedUser) { + const followers = await user.getFollowers(); + isFollowing = followers.some((user) => user.id === loggedUser.id); + } + + const profile = { + username, + bio: user.bio, + image: user.image, + following: isFollowing, + }; + res.status(200).json({ profile }); +}); + +module.exports.followUser = asyncHandler(async (req, res, next) => { + const username = req.params.username; + const { loggedUser } = req; + const userToFollow = await User.findOne({ where: { username: username } }); + + if (!userToFollow) { + return next(new ErrorResponse(`User not found`, 404)); + } + + const currentUser = await User.findByPk(loggedUser.id); + + await userToFollow.addFollower(currentUser); + + const profile = { + username: username, + bio: userToFollow.dataValues.bio, + image: userToFollow.dataValues.image, + following: true, + }; + + res.status(200).json({ profile }); +}); + +module.exports.unfollowUser = asyncHandler(async (req, res, next) => { + const username = req.params.username; + const { loggedUser } = req; + const userToUnfollow = await User.findOne({ where: { username: username } }); + + if (!userToUnfollow) { + return next(new ErrorResponse(`User not found`, 404)); + } + + const currentUser = await User.findByPk(loggedUser.id); + + await userToUnfollow.removeFollower(currentUser); + + const profile = { + username: username, + bio: userToUnfollow.dataValues.bio, + image: userToUnfollow.dataValues.image, + following: false, + }; + + res.status(200).json({ profile }); +}); diff --git a/controllers/tags.js b/controllers/tags.js new file mode 100644 index 0000000..637eaa7 --- /dev/null +++ b/controllers/tags.js @@ -0,0 +1,12 @@ +const Tag = require("../models/Tag"); +const { appendTagList } = require("../util/helpers"); + +module.exports.getTags = async (req, res, next) => { + let tags = await Tag.findAll({ + attributes: ["name"], + }); + + tags = tags.map((tag) => tag.name); + + res.status(200).json({ tags }); +}; diff --git a/controllers/users.js b/controllers/users.js new file mode 100644 index 0000000..6a788e6 --- /dev/null +++ b/controllers/users.js @@ -0,0 +1,93 @@ +const asyncHandler = require("../middlewares/asyncHandler"); +const User = require("../models/User"); +const ErrorResponse = require("../util/errorResponse"); +const { sign } = require("../util/jwt"); + +module.exports.createUser = asyncHandler(async (req, res, next) => { + const { email, password, username } = req.body.user; + + fieldValidation(email, next); + fieldValidation(password, next); + fieldValidation(username, next); + + const user = await User.create({ + email: email, + password: password, + username: username, + }); + + if (user.dataValues.password) { + delete user.dataValues.password; + } + + user.dataValues.token = await sign(user); + + user.dataValues.bio = null; + user.dataValues.image = null; + + res.status(201).json({ user }); +}); + +module.exports.loginUser = asyncHandler(async (req, res, next) => { + const { email, password } = req.body.user; + + fieldValidation(email, next); + fieldValidation(password, next); + + const user = await User.findOne({ + where: { + email: email, + }, + }); + + if (!user) { + return next(new ErrorResponse(`User not found`, 404)); + } + + const isMatch = user.matchPassword(password); + + if (!isMatch) { + return next(new ErrorResponse("Wrong password", 401)); + } + + delete user.dataValues.password; + + user.dataValues.token = await sign(user); + + user.dataValues.bio = null; + user.dataValues.image = null; + + res.status(200).json({ user }); +}); + +module.exports.getCurrentUser = asyncHandler(async (req, res, next) => { + const { loggedUser } = req; + const user = await User.findByPk(loggedUser.id); + + if (!user) { + return next(new ErrorResponse(`User not found`, 404)); + } + + user.dataValues.token = req.headers.authorization.split(" ")[1]; + + res.status(200).json({ user }); +}); + +module.exports.updateUser = asyncHandler(async (req, res, next) => { + await User.update(req.body.user, { + where: { + id: req.user.id, + }, + }); + + const user = await User.findByPk(req.user.id); + user.dataValues.token = req.headers.authorization.split(" ")[1]; + + res.status(200).json({ user }); +}); + +const fieldValidation = (field, next) => { + if (!field) { + return next(new ErrorResponse(`Missing fields`, 400)); + } +}; diff --git a/models copy/Article.js b/models copy/Article.js new file mode 100644 index 0000000..236f6ba --- /dev/null +++ b/models copy/Article.js @@ -0,0 +1,27 @@ +const { DataTypes, Model } = require("sequelize"); +const sequelize = require("../util/database"); +const slugify = require("slugify"); + +const Article = sequelize.define("Article", { + slug: { + type: DataTypes.STRING, + allowNull: false, + }, + title: { + type: DataTypes.STRING, + allowNull: false, + }, + description: { + type: DataTypes.TEXT, + }, + body: { + type: DataTypes.TEXT, + allowNull: false, + }, +}); + +Article.beforeValidate((article) => { + article.slug = slugify(article.title, { lower: true }); +}); + +module.exports = Article; diff --git a/models copy/Comment.js b/models copy/Comment.js new file mode 100644 index 0000000..42e731a --- /dev/null +++ b/models copy/Comment.js @@ -0,0 +1,11 @@ +const { DataTypes, Model } = require("sequelize"); +const sequelize = require("../util/database"); + +const Comment = sequelize.define("Comment", { + body: { + type: DataTypes.TEXT, + allowNull: false, + }, +}); + +module.exports = Comment; diff --git a/models copy/Tag.js b/models copy/Tag.js new file mode 100644 index 0000000..89f54c4 --- /dev/null +++ b/models copy/Tag.js @@ -0,0 +1,16 @@ +const { DataTypes, Model } = require("sequelize"); +const sequelize = require("../util/database"); + +const Tag = sequelize.define( + "Tag", + { + name: { + type: DataTypes.STRING, + allowNull: false, + primaryKey: true, + }, + }, + { timestamps: false } +); + +module.exports = Tag; diff --git a/models copy/User.js b/models copy/User.js new file mode 100644 index 0000000..94bd322 --- /dev/null +++ b/models copy/User.js @@ -0,0 +1,58 @@ +const { DataTypes, Model } = require("sequelize"); +const sequelize = require("../util/database"); +const bcrypt = require("bcryptjs"); + +/** + * "email": "jake@jake.jake", + "token": "jwt.token.here", + "username": "jake", + "bio": "I work at statefarm", + "image": null + */ + +const User = sequelize.define( + "User", + { + email: { + type: DataTypes.STRING, + allowNull: false, + unique: true, + }, + + username: { + type: DataTypes.STRING, + allowNull: false, + unique: true, + }, + password: { + type: DataTypes.STRING, + allowNull: false, + }, + + bio: { + type: DataTypes.TEXT, + allowNull: true, + }, + image: { + type: DataTypes.TEXT, + allowNull: true, + }, + }, + { timestamps: false } +); + +Model.prototype.matchPassword = async function (enteredPassword) { + return await bcrypt.compare(enteredPassword, this.password); +}; + +const DEFAULT_SALT_ROUNDS = 10; + +User.addHook("beforeCreate", async (user) => { + const encryptedPassword = await bcrypt.hash( + user.password, + DEFAULT_SALT_ROUNDS + ); + user.password = encryptedPassword; +}); + +module.exports = User; diff --git a/models/mysql/Article.js b/models/mysql/Article.js new file mode 100644 index 0000000..2027c4d --- /dev/null +++ b/models/mysql/Article.js @@ -0,0 +1,48 @@ + +const slug = require("slug"); + +const { + Model +} = require('sequelize'); +module.exports = (sequelize, DataTypes) => { + class Article extends Model { + /** + * Helper method for defining associations. + * This method is not a part of Sequelize lifecycle. + * The `models/index` file will call this method automatically. + */ + static associate(models) { + /* define association here */ + } + } + Article.init({ + article_id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true, allowNull: false }, + article_slug: { type: DataTypes.STRING, allowNull: false }, + article_title: { type: DataTypes.STRING, allowNull: false }, + article_description: { type: DataTypes.TEXT }, + article_body: { type: DataTypes.TEXT, allowNull: false } + }, { + sequelize, + timestamps: false, + modelName: 'Article', + tableName: 'article', + createdAt: true, + updatedAt: true + }); + + Article.beforeValidate((article) => { + article.slug = slug(article.title) + '-' + (Math.random() * Math.pow(36, 6) | 0).toString(36); + }); + + Article.prototype.toJSONFor = function () { + return { + id: this.article_id, + slug: this.article_slug, + title: this.article_title, + description: this.article_description, + body: this.article_body + }; + }; + + return Article; +}; diff --git a/models/mysql/Comment.js b/models/mysql/Comment.js new file mode 100644 index 0000000..437faa3 --- /dev/null +++ b/models/mysql/Comment.js @@ -0,0 +1,35 @@ +const { + Model +} = require('sequelize'); +module.exports = (sequelize, DataTypes) => { + class Comment extends Model { + /** + * Helper method for defining associations. + * This method is not a part of Sequelize lifecycle. + * The `models/index` file will call this method automatically. + */ + static associate(models) { + /* define association here */ + } + } + Comment.init({ + comment_id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true, allowNull: false }, + comment_body: { type: DataTypes.TEXT, allowNull: false } + }, { + sequelize, + timestamps: false, + modelName: 'Comment', + tableName: 'comment', + createdAt: true, + updatedAt: true + }); + + Comment.prototype.toJSONFor = function () { + return { + id: this.comment_id, + body: this.comment_body + }; + }; + + return Comment; +}; \ No newline at end of file diff --git a/models/mysql/Membre.js b/models/mysql/Membre.js new file mode 100644 index 0000000..e3ce729 --- /dev/null +++ b/models/mysql/Membre.js @@ -0,0 +1,57 @@ +'use strict'; +const { + Model +} = require('sequelize'); +module.exports = (sequelize, DataTypes) => { + class Membre extends Model { + /** + * Helper method for defining associations. + * This method is not a part of Sequelize lifecycle. + * The `models/index` file will call this method automatically. + */ + static associate(models) { + /* define association here */ + } + } + Membre.init({ + mbr_id: {type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true, allowNull: false}, + mbr_pseudo: {type: DataTypes.STRING, allowNull: false}, + mbr_nom: {type: DataTypes.STRING}, + mbr_prenom: {type: DataTypes.STRING}, + mbr_email: {type: DataTypes.STRING}, + mbr_adresse: {type: DataTypes.STRING}, + mbr_ville: {type: DataTypes.STRING}, + mbr_region: {type: DataTypes.STRING}, + mbr_cp: {type: DataTypes.STRING}, + mbr_pays: {type: DataTypes.STRING}, + mbr_tel: {type: DataTypes.STRING}, + mbr_mobile: {type: DataTypes.STRING}, + mbr_statut: {type: DataTypes.INTEGER} + }, { + sequelize, + timestamps: false, + modelName: 'Membre', + tableName: 'membre', + createdAt: false, + updatedAt: false + }); + + Membre.prototype.toJSONFor = function () { + return { + id: this.mbr_id, + pseudo: this.mbr_pseudo, + nom: this.mbr_nom, + prenom: this.mbr_prenom, + email: this.mbr_email, + adresse: this.mbr_adresse, + ville: this.mbr_ville, + region: this.mbr_region, + cp: this.mbr_cp, + pays: this.mbr_pays, + tel: this.mbr_tel, + mobile: this.mbr_mobile, + statut: this.mbr_statut + }; + }; + return Membre; +}; diff --git a/models/mysql/Tag.js b/models/mysql/Tag.js new file mode 100644 index 0000000..882f01f --- /dev/null +++ b/models/mysql/Tag.js @@ -0,0 +1,34 @@ +const { + Model +} = require('sequelize'); +module.exports = (sequelize, DataTypes) => { + class Tag extends Model { + /** + * Helper method for defining associations. + * This method is not a part of Sequelize lifecycle. + * The `models/index` file will call this method automatically. + */ + static associate(models) { + /* define association here */ + } + } + Tag.init({ + tag_name: { type: DataTypes.STRING, allowNull: false, primaryKey: true } + }, { + sequelize, + timestamps: false, + modelName: 'Tag', + tableName: 'tag', + createdAt: true, + updatedAt: true + }); + + Tag.prototype.toJSONFor = function () { + return { + id: this.tag_id, + name: this.tag_name + }; + }; + + return Tag; +}; \ No newline at end of file diff --git a/models/mysql/User.js b/models/mysql/User.js new file mode 100644 index 0000000..fd9d64e --- /dev/null +++ b/models/mysql/User.js @@ -0,0 +1,74 @@ +'use strict'; + +const bcrypt = require('bcrypt'); + +const { + Model +} = require('sequelize'); +module.exports = (sequelize, DataTypes) => { + class User extends Model { + /** + * Helper method for defining associations. + * This method is not a part of Sequelize lifecycle. + * The `models/index` file will call this method automatically. + */ + static associate(models) { + /* define association here */ + } + } + User.init({ + user_id: {type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true, allowNull: false}, + user_username: {type: DataTypes.STRING, allowNull: false}, + user_lastname: {type: DataTypes.STRING}, + user_firstname: {type: DataTypes.STRING}, + user_email: {type: DataTypes.STRING}, + user_adresse: {type: DataTypes.STRING}, + user_ville: {type: DataTypes.STRING}, + user_region: {type: DataTypes.STRING}, + user_cp: {type: DataTypes.STRING}, + user_pays: {type: DataTypes.STRING}, + user_tel: {type: DataTypes.STRING}, + user_mobile: {type: DataTypes.STRING}, + user_role: {type: DataTypes.INTEGER} + }, { + sequelize, + timestamps: false, + modelName: 'User', + tableName: 'user', + createdAt: true, + updatedAt: true + }); + + User.prototype.matchPassword = async function (enteredPassword) { + return await bcrypt.compare(enteredPassword, this.password); + }; + + const DEFAULT_SALT_ROUNDS = 10; + + User.addHook("beforeCreate", async (user) => { + const encryptedPassword = await bcrypt.hash( + user.password, + DEFAULT_SALT_ROUNDS + ); + user.password = encryptedPassword; + }); + + User.prototype.toJSONFor = function () { + return { + id: this.mbr_id, + username: this.mbr_pseudo, + lastname: this.mbr_nom, + firstname: this.mbr_prenom, + email: this.mbr_email, + adresse: this.mbr_adresse, + ville: this.mbr_ville, + region: this.mbr_region, + cp: this.mbr_cp, + pays: this.mbr_pays, + tel: this.mbr_tel, + mobile: this.mbr_mobile, + statut: this.mbr_statut + }; + }; + return User; +}; diff --git a/models/mysql/index.js b/models/mysql/index.js new file mode 100644 index 0000000..f524654 --- /dev/null +++ b/models/mysql/index.js @@ -0,0 +1,50 @@ +const fs = require('fs'), + path = require('path'), + basename = path.basename(__filename), + chalk = require('chalk'), + utils = require('../../routes/utils'); + +const { Sequelize } = require('sequelize'); +const db = {}; +const Op = Sequelize.Op; +const sequelize = new Sequelize( + process.env.MYSQL_DBNAME, + process.env.MYSQL_DBUSER, + process.env.MYSQL_DBPASS, + { + host: process.env.MYSQL_DBHOST, + port: process.env.MYSQL_DBPORT, + dialect: process.env.MYSQL_DBTYPE, + $like: Op.like, + $not: Op.not + } +); + +sequelize.authenticate().then(() => { + if (process.env.DEBUG) { + console.log(`${utils.getDateTimeISOString()}`, chalk.green('MySQL connection has been established successfully')); + } +}).catch((error) => { + console.log(`${utils.getDateTimeISOString()}`, chalk.red('Unable to connect to the MySQL database')); + console.error(error); +}); + +fs + .readdirSync(__dirname) + .filter(file => { + return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js'); + }) + .forEach(file => { + const model = require(path.join(__dirname, file))(sequelize, Sequelize.DataTypes); + db[model.name] = model; + }); + +Object.keys(db).forEach(modelName => { + if (db[modelName].associate) { + db[modelName].associate(db); + } +}); + +db.sequelize = sequelize; + +module.exports = db; \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..8260738 --- /dev/null +++ b/package.json @@ -0,0 +1,26 @@ +{ + "name": "adastra-api", + "version": "1.0.0", + "private": true, + "author": "adastra", + "main": "server.js", + "scripts": { + "dev": "nodemon server.js" + }, + "dependencies": { + "bcryptjs": "^2.4.3", + "colors": "^1.4.0", + "dotenv": "^16.0.3", + "express": "^4.18.2", + "jsonwebtoken": "^8.5.1", + "morgan": "^1.10.0", + "mysql2": "^2.3.3", + "nodemon": "^2.0.20", + "sequelize": "^6.26.0", + "slugify": "^1.6.5" + }, + "devDependencies": { + "eslint": "^8.29.0", + "prettier": "^2.8.0" + } +} diff --git a/routes copy/articles.js b/routes copy/articles.js new file mode 100644 index 0000000..e5c04c4 --- /dev/null +++ b/routes copy/articles.js @@ -0,0 +1,30 @@ +const express = require("express"); +const router = express.Router(); +const { + getArticle, + getArticles, + createArticle, + articlesFeed, + addFavoriteArticle, + deleteFavoriteArticle, + deleteArticle, + updateArticle, +} = require("../controllers/articles"); +const { protect } = require("../middlewares/auth"); + +router + .route("/articles") + .get(protect, getArticles) + .post(protect, createArticle); +router.route("/articles/feed").get(protect, articlesFeed); +router + .route("/articles/:slug") + .get(protect, getArticle) + .put(protect, updateArticle) + .delete(protect, deleteArticle); +router + .route("/articles/:slug/favorite") + .post(protect, addFavoriteArticle) + .delete(protect, deleteFavoriteArticle); + +module.exports = router; diff --git a/routes copy/comments.js b/routes copy/comments.js new file mode 100644 index 0000000..322bea3 --- /dev/null +++ b/routes copy/comments.js @@ -0,0 +1,23 @@ +const express = require("express"); +const router = express.Router(); + +const { + getComment, + getComments, + createComment, + deleteComment, +} = require("../controllers/comments"); + +const { protect } = require("../middlewares/auth"); + +router + .route("/articles/:slug/comments") + .get(protect, getComments) + .post(protect, createComment); + +router + .route("/articles/:slug/comments/:id") + .get(protect, getComment) + .delete(protect, deleteComment); + +module.exports = router; diff --git a/routes copy/profiles.js b/routes copy/profiles.js new file mode 100644 index 0000000..d6a558b --- /dev/null +++ b/routes copy/profiles.js @@ -0,0 +1,16 @@ +const express = require("express"); +const router = express.Router(); +const { + getProfile, + followUser, + unfollowUser, +} = require("../controllers/profiles"); +const { protect } = require("../middlewares/auth"); + +router.route("/profiles/:username").get(protect, getProfile); +router + .route("/profiles/:username/follow") + .post(protect, followUser) + .delete(protect, unfollowUser); + +module.exports = router; diff --git a/routes copy/tags.js b/routes copy/tags.js new file mode 100644 index 0000000..5157a6d --- /dev/null +++ b/routes copy/tags.js @@ -0,0 +1,10 @@ +const express = require("express"); +const router = express.Router(); + +const { getTags } = require("../controllers/tags"); + +const { protect } = require("../middlewares/auth"); + +router.route("/tags/").get(getTags); + +module.exports = router; diff --git a/routes copy/users.js b/routes copy/users.js new file mode 100644 index 0000000..9bf832e --- /dev/null +++ b/routes copy/users.js @@ -0,0 +1,16 @@ +const express = require("express"); +const router = express.Router(); +const { + createUser, + loginUser, + getCurrentUser, + updateUser, +} = require("../controllers/users"); +const { protect } = require("../middlewares/auth"); + +router.post("/users", createUser); +router.post("/users/login", loginUser); + +router.route("/user").get(protect, getCurrentUser).put(protect, updateUser); + +module.exports = router; diff --git a/server.js b/server.js new file mode 100644 index 0000000..7d37ec2 --- /dev/null +++ b/server.js @@ -0,0 +1,138 @@ +var appEnv = process.env.APP_ENV; +if (appEnv == undefined) { + appEnv = 'development'; +} +const express = require("express"); +const sequelize = require("./util/database"); +const dotenv = require("dotenv"); +const morgan = require("morgan"); +const colors = require("colors"); +const { errorHandler } = require("./middlewares/errorHandler"); + +// Import Models +const User = require("./models/User"); +const Article = require("./models/Article"); +const Tag = require("./models/Tag"); +const Comment = require("./models/Comment"); + +dotenv.config({ path: `.env.${appEnv}` }); + +const app = express(); + +// Body parser +app.use(express.json()); + +if (process.env.NODE_ENV === "development") { + app.use(morgan("dev")); +} else { + app.use(morgan('[:date[iso]] ":method :url HTTP/:http-version" :statusColor :res[content-length] ":referrer" ":user-agent" :remote-addr')); +} + +// CORS +app.use((req, res, next) => { + res.header("Access-Control-Allow-Origin", "*"); + res.header( + "Access-Control-Allow-Headers", + "Origin, X-Requested-With, Content-Type, Accept, Authorization" + ); + if (req.method === "OPTIONS") { + res.header("Access-Control-Allow-Methods", "PUT, POST, PATCH, DELETE, GET"); + return res.status(200).json({}); + } + next(); +}); + +// Route files +const users = require("./routes/users"); +const profiles = require("./routes/profiles"); +const articles = require("./routes/articles"); +const comments = require("./routes/comments"); +const tags = require("./routes/tags"); + +// Mount routers +app.use(users); +app.use(profiles); +app.use(articles); +app.use(comments); +app.use(tags); + +const PORT = process.env.PORT || process.env.SERVER_PORT; + +app.use(errorHandler); + +// Relations +User.belongsToMany(User, { + as: "followers", + through: "Followers", + foreignKey: "userId", + timestamps: false, +}); +User.belongsToMany(User, { + as: "following", + through: "Followers", + foreignKey: "followerId", + timestamps: false, +}); + +User.hasMany(Article, { + foreignKey: "authorId", + onDelete: "CASCADE", +}); +Article.belongsTo(User, { as: "author", foreignKey: "authorId" }); + +User.hasMany(Comment, { + foreignKey: "authorId", + onDelete: "CASCADE", +}); +Comment.belongsTo(User, { as: "author", foreignKey: "authorId" }); + +Article.hasMany(Comment, { + foreignKey: "articleId", + onDelete: "CASCADE", +}); +Comment.belongsTo(Article, { foreignKey: "articleId" }); + +User.belongsToMany(Article, { + as: "favorites", + through: "Favorites", + timestamps: false, +}); +Article.belongsToMany(User, { + through: "Favorites", + foreignKey: "articleId", + timestamps: false, +}); + +Article.belongsToMany(Tag, { + through: "TagLists", + as: "tagLists", + foreignKey: "articleId", + timestamps: false, + onDelete: "CASCADE", +}); +Tag.belongsToMany(Article, { + through: "ArticleTags", + uniqueKey: false, + timestamps: false, +}); + +const sync = async () => await sequelize.sync({ force: true }); +sync().then(() => { + User.create({ + email: "test@test.com", + password: "123456", + username: "neo", + }); + User.create({ + email: "test2@test.com", + password: "123456", + username: "celeb_neo", + }); +}); + +const server = app.listen( + PORT, + console.log( + `Server running in ${process.env.NODE_ENV} mode on port ${PORT}`.yellow.bold + ) +); diff --git a/util/database.js b/util/database.js new file mode 100644 index 0000000..b0fc3e5 --- /dev/null +++ b/util/database.js @@ -0,0 +1,28 @@ +const { Sequelize } = require('sequelize'); + +const Op = Sequelize.Op; +const sequelize = new Sequelize( + process.env.MYSQL_DBNAME, + process.env.MYSQL_DBUSER, + process.env.MYSQL_DBPASS, + { + host: process.env.MYSQL_DBHOST, + port: process.env.MYSQL_DBPORT, + dialect: process.env.MYSQL_DBTYPE, + $like: Op.like, + $not: Op.not + } +); + +const checkConnection = async () => { + try { + await sequelize.authenticate(); + console.log(`DB Connected`.cyan.underline.bold); + } catch (error) { + console.error("Unable to connect to the database:".red.bold, error); + } +}; + +checkConnection(); + +module.exports = sequelize; diff --git a/util/errorResponse.js b/util/errorResponse.js new file mode 100644 index 0000000..29d4ac9 --- /dev/null +++ b/util/errorResponse.js @@ -0,0 +1,8 @@ +class ErrorResponse extends Error { + constructor(message, statusCode) { + super(message); + this.statusCode = statusCode; + } +} + +module.exports = ErrorResponse; diff --git a/util/helpers.js b/util/helpers.js new file mode 100644 index 0000000..567d917 --- /dev/null +++ b/util/helpers.js @@ -0,0 +1,38 @@ +const appendTagList = (articleTags, article) => { + const tagList = articleTags.map((tag) => tag.name); + + if (!article) return tagList; + delete article.dataValues.tagLists; + article.dataValues.tagList = tagList; +}; + +const appendFavorites = async (loggedUser, article) => { + const favorited = await article.hasUser(loggedUser ? loggedUser : null); + article.dataValues.favorited = loggedUser ? favorited : false; + + const favoritesCount = await article.countUsers(); + article.dataValues.favoritesCount = favoritesCount; +}; + +const appendFollowers = async (loggedUser, toAppend) => { + // + if (toAppend?.author) { + const author = await toAppend.getAuthor(); + const following = await author.hasFollower(loggedUser ? loggedUser : null); + toAppend.author.dataValues.following = loggedUser ? following : false; + + const followersCount = await author.countFollowers(); + toAppend.author.dataValues.followersCount = followersCount; + // + } else { + const following = await toAppend.hasFollower( + loggedUser ? loggedUser : null + ); + toAppend.dataValues.following = loggedUser ? following : false; + + const followersCount = await toAppend.countFollowers(); + toAppend.dataValues.followersCount = followersCount; + } +}; + +module.exports = { appendTagList, appendFavorites, appendFollowers }; diff --git a/util/jwt.js b/util/jwt.js new file mode 100644 index 0000000..4bfed3b --- /dev/null +++ b/util/jwt.js @@ -0,0 +1,14 @@ +const jwt = require("jsonwebtoken"); + +module.exports.sign = async (user) => { + const token = await jwt.sign( + { id: user.id, username: user.username, email: user.email }, + process.env.JWT_SECRET + ); + return token; +}; + +module.exports.verify = async (token) => { + const decoded = await jwt.verify(token, process.env.JWT_SECRET); + return decoded; +};