diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6ba7e54..da346a9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,12 +12,13 @@ Bu döküman bu koda katkıda bulunmak için izlemeniz gereken adımları ve uym * Veritabanı tablo ve field, modellerin key isimleri: **snake_case** Tablo isimler **çoğul** olmalıdır(users). +Service isimleri **tekil** olmalıdır(user). -Migrasyon dosyaları **tarih damgası** ile başlamalıdır(yyyymmddhhmmss-migration-name.js). +Migrasyon ve seed dosyaları **tarih damgası** ile başlamalıdır(yyyymmddhhmmss-job-description.js). ## Katkı nasıl sağlanır? Projeye katkı yapmak istiyorsanız lütfen ilk önce yaptığınız işin halihazırda [issue](https://github.com/mavidurak/orientation-api/issues "issue'ları görüntülemek için tıklayın") olarak bulunup buşunmadığını kontrol edin.Eğer **issue** varsa ve issue üzerinde görevlendirilmiş birisi bulunuyorsa o kişi ile iletişime geçiniz.Eğer issue yoksa geliştirmenizi **pull request** açarak proje yetkililerine iletebilirsiniz. **Issue** oluştururken bulduğunuz bug ile ilgili detaylı bilgi yazmalısınız(gönderdiğiniz veri, gelen cevap, hata kodu, ...) ve mümkünse ekran görüntüsü eklemelisiniz. -Açtığınız **pull request** ismi ve açıklaması yaptığınız geliştirme ile alakalı olmalıdır..İşleyişsel değişiklikler yaptıysanız bunları açıklayıcı bir şekilde anlatmalısınız. \ No newline at end of file +Açtığınız **pull request**'in ismi ve açıklaması yaptığınız geliştirme ile alakalı olmalıdır..İşleyişsel değişiklikler yaptıysanız bunları açıklayıcı bir şekilde anlatmalısınız. Üzerinde hala geliştirme yapıyorsanız pull request **draft** olamalıdır ve incelenmek için hazır olduğunu düşündüğünüzde **waiting to review** etiketi ile işaretlenmelidir. diff --git a/README.md b/README.md index c099de7..6d031ca 100644 --- a/README.md +++ b/README.md @@ -8,16 +8,16 @@ ├── CONTRIBUTING.md ├── docs │   └── ... -├── hooks // Git hooks +├── hooks # Git hooks │   └── pre-commit ├── LICENSE -├── migrations -│   └── .js ├── nodemon.json # Nodemon config ├── package.json ├── package-lock.json ├── README.md ├── sample.env # Sample environment variables file +├── migrations +│   └── .js ├── seeders │   └── .js └── src @@ -25,19 +25,24 @@ │   └── sequelize.js # Database config ├── constants │   └── api.js - ├── joi.js # For Joi multiple error - ├── models - │   ├── index.js - │   └── .js ├── pre_handlers │   ├── authantication.js │   ├── complatePath.js │   └── index.js - ├── router.js +│   ├── exceptions +│   │   ├── errorHandler.js +│   │   └── HTTPError.js # Custom HTTP error + ├── models + │   ├── index.js + │   └── .js + ├── services + │   └── .js ├── routes │   ├── │   │   └── index.js │   └── index.js + ├── joi.js # For Joi multiple error + ├── router.js ├── sequelize.js # Database connection ├── server.js # Server start point └── utils diff --git a/src/exceptions/errorHandler.js b/src/exceptions/errorHandler.js index 78f1430..869fde4 100644 --- a/src/exceptions/errorHandler.js +++ b/src/exceptions/errorHandler.js @@ -1,3 +1,4 @@ +/* eslint-disable-next-line no-unused-vars */ export default (error, req, res, next) => { res.header('Content-Type', 'application/json'); res.status(error.statusCode).send(error.message); diff --git a/src/models/emailConfirmationTokens.js b/src/models/emailConfirmationTokens.js index b2f5054..83b54e4 100644 --- a/src/models/emailConfirmationTokens.js +++ b/src/models/emailConfirmationTokens.js @@ -1,7 +1,8 @@ import { DataTypes } from 'sequelize'; -import { EMAIL_TOKEN_STATUS } from '../constants/email'; - import Sequelize from '../sequelize'; +import UserService from '../services/user'; + +import { EMAIL_TOKEN_STATUS } from '../constants/email'; const email_confirmation_tokens = Sequelize.define('email_confirmation_tokens', { @@ -52,11 +53,7 @@ const initialize = (models) => { }; models.email_confirmation_tokens.prototype.confirmToken = async function () { - const user = await models.users.findOne({ - where: { - id: this.user_id, - }, - }); + const user = await UserService.getUser(this.user_id); if (!user) { return false; } diff --git a/src/routes/authentication/index.js b/src/routes/authentication/index.js index d5b3bfe..862d0d0 100644 --- a/src/routes/authentication/index.js +++ b/src/routes/authentication/index.js @@ -1,10 +1,11 @@ -import { Op } from 'sequelize'; -import Joi from '../../joi'; -import { EMAIL_TOKEN_STATUS, EMAIL_TYPES } from '../../constants/email'; +import models from '../../models'; +import UserService from '../../services/user'; import { sendEmail } from '../../utils/sendEmail'; +import { makeSha512, createSaltHashPassword } from '../../utils/encription'; +import Joi from '../../joi'; +import HTTPError from '../../exceptions/HTTPError'; -import models from '../../models'; -import { createSaltHashPassword, makeSha512 } from '../../utils/encription'; +import { EMAIL_TOKEN_STATUS, EMAIL_TYPES } from '../../constants/email'; const registerSchema = { body: Joi.object({ @@ -68,50 +69,37 @@ const resetPasswordSchema = { }), }; -const login = async (req, res) => { +const login = async (req, res, next) => { const { error } = loginSchema.body.validate(req.body); if (error) { return res.status(400).send({ errors: error.details, }); } - const { username, password } = req.body; - const user = await models.users.findOne({ - where: { - username, - }, - }); + try { + const user = await UserService.getUser(username); - if (user) { - const passwordHash = makeSha512(password, user.password_salt); - if (passwordHash === user.password_hash) { - if (!user.is_email_confirmed) { - return res.send(401, { - errors: [ - { - message: 'This account has not been confirmed yet.', - }, - ], - }); - } - const token = await user.createToken(req.headers['x-forwarded-for'] || req.connection.remoteAddress); + if (user) { + const passwordHash = makeSha512(password, user.password_salt); + if (passwordHash === user.password_hash) { + if (!user.is_email_confirmed) { + throw new HTTPError('This account has not been confirmed yet.', 401); + } - return res.send({ token }); + const token = await user.createToken(req.headers['x-forwarded-for'] || req.connection.remoteAddress); + return res.send({ token }); + } } - } - return res.status(401).send({ - errors: [ - { - message: 'Username or password is incorrect!', - }, - ], - }); + throw new HTTPError('Username or password is incorrect!', 401); + } catch (err) { + next(err); + } }; -const register = async (req, res) => { +const register = async (req, res, next) => { const { error } = registerSchema.body.validate(req.body); if (error) { return res.status(400).send({ @@ -122,131 +110,53 @@ const register = async (req, res) => { const { username, email, password, name, } = req.body; + try { + const user = await UserService.createUser(username, email, password, name); + + const value = await user.createEmailConfirmationToken(EMAIL_TYPES.EMAIL_VALIDATION); + if (!process.env.NODE_ENV || process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test') { + user.is_email_confirmed = true; + await user.save(); + } else { + await sendEmail(user, { + subject: 'Welcome to MaviDurak-IO', + emailType: EMAIL_TYPES.EMAIL_VALIDATION, + }, { + username: user.name, + href: `${process.env.API_PATH}/authentication/email-confirmation?token=${value}`, + }); + } - let user = await models.users.findOne({ - where: { - [Op.or]: { - username: username.trim(), - email: email.trim(), - }, - }, - }); - if (user) { - return res.send(400, { - errors: [ - { - message: 'E-mail or username is already used!', - }, - ], - }); - } - - const { - hash: password_hash, - salt: password_salt, - } = createSaltHashPassword(password); - - user = await models.users.create({ - username, - email, - name, - password_hash, - password_salt, - }); - - const value = await user.createEmailConfirmationToken(EMAIL_TYPES.EMAIL_VALIDATION); - if (!process.env.NODE_ENV || process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test') { - user.is_email_confirmed = true; - await user.save(); - } else { - await sendEmail(user, { - subject: 'Welcome to MaviDurak-IO', - emailType: EMAIL_TYPES.EMAIL_VALIDATION, - }, { - username: user.name, - href: `${process.env.API_PATH}/authentication/email-confirmation?token=${value}`, + return res.status(201).send({ + user: user.toJSON(), }); + } catch (err) { + next(err); } - - return res.status(201).send({ - user: user.toJSON(), - }); }; const userInfo = async (req, res) => { res.send(req.user); }; -const update = async (req, res) => { +const update = async (req, res, next) => { const { error } = updateSchema.body.validate(req.body); if (error) { return res.status(400).send({ errors: error.details, }); } - const { - password, username, email, name, friends_ids, new_password, new_password_again, - } = req.body; - const user = await models.users.findOne({ - where: { - username: req.user.username, - }, - }); - if (user) { - const passwordHash = makeSha512(password, user.password_salt); - if (passwordHash !== user.password_hash) { - return res.send(403, { - errors: [ - { - message: 'password is incorrect please try again ', - }, - ], - }); - } - let where = {}; - if (username || email) { - where = username ? { username } : { email }; - const isExist = await models.users.findOne({ - where, - }); - if (isExist) { - return res.send(403, { - errors: [ - { - message: 'E-mail or username is already used!', - }, - ], - }); - } - } - let passwordValdation = { hash: null, salt: null }; - if (new_password && new_password_again) { - if (new_password !== new_password_again) { - return res.send(403, { - errors: [ - { - message: 'Passwords must be same!', - }, - ], - }); - } - passwordValdation = createSaltHashPassword(new_password); - } - where = Object.entries({ - username, email, name, friends_ids, password_hash: passwordValdation.hash, password_salt: passwordValdation.salt, - }).reduce((a, [k, v]) => (v == null ? a : (a[k] = v, a)), {}); - const user2 = await user.update(where); - return res.send(user2.toJSON()); + try { + const user = await UserService.updateUser({ ...req.body }, req.user.id); + res.status(200).send(user); + } catch (err) { + next(err); } }; const deleteUser = async (req, res) => { - const isDeleted = await models.users.destroy({ - where: { - id: req.user.id, - }, - }); + const isDeleted = await UserService.deleteUser(req.user.id); if (isDeleted) { res.send(200, { message: 'Successfully deleted', @@ -270,78 +180,74 @@ const emailConfirmation = async (req, res) => { return res.redirect(`${process.env.FRONTEND_PATH}/login`); }; -const sendForgotPasswordEmail = async (req, res) => { - const user = await models.users.findOne({ - where: { - email: req.body.email, - }, - }); +const sendForgotPasswordEmail = async (req, res, next) => { + try { + const user = await models.users.findOne({ + where: { + email: req.body.email, + }, + }); - if (!user) { - return res.send(401, { - errors: [ - { - message: 'User not found!', - }, - ], + if (!user) { + throw new HTTPError('User not found!', 401); + } + + const value = await user.createEmailConfirmationToken(EMAIL_TYPES.FORGOT_PASSWORD); + await sendEmail(user, { + subject: 'Reset your password', + emailType: EMAIL_TYPES.FORGOT_PASSWORD, + }, { + username: user.name, + href: `${process.env.FRONTEND_PATH}/reset-password?token=${value}`, }); + + res.send(200); + } catch (error) { + next(error); } - const value = await user.createEmailConfirmationToken(EMAIL_TYPES.FORGOT_PASSWORD); - await sendEmail(user, { - subject: 'Reset your password', - emailType: EMAIL_TYPES.FORGOT_PASSWORD, - }, { - username: user.name, - href: `${process.env.FRONTEND_PATH}/reset-password?token=${value}`, - }); - res.send(200); }; -const resetPassword = async (req, res) => { +const resetPassword = async (req, res, next) => { const { error } = resetPasswordSchema.body.validate(req.body); if (error) { return res.status(400).send({ errors: error.details, }); } - const value = req.query.token; - const resetPasswordValue = await models.email_confirmation_tokens.findOne({ - where: { - value, - type: EMAIL_TYPES.FORGOT_PASSWORD, - status: EMAIL_TOKEN_STATUS.PENDING, - }, - }); - if (resetPasswordValue) { - const { password } = req.body; - const { - hash: password_hash, - salt: password_salt, - } = createSaltHashPassword(password); - - await models.users.update({ - password_hash, - password_salt, - }, { + const value = req.query.token; + const { password } = req.body; + try { + const resetPasswordToken = await models.email_confirmation_tokens.findOne({ where: { - id: resetPasswordValue.user_id, + value, + type: EMAIL_TYPES.FORGOT_PASSWORD, + status: EMAIL_TOKEN_STATUS.PENDING, }, }); - await resetPasswordValue.confirmToken(); + if (resetPasswordToken) { + const user = await UserService.getUser(resetPasswordToken.user_id); - return res.send(200, { - message: 'Your password has been successfully changed.', - }); + const { + hash: password_hash, + salt: password_salt, + } = createSaltHashPassword(password); + user.password_hash = password_hash; + user.password_salt = password_salt; + + await user.save(); + await resetPasswordToken.confirmToken(); + + return res.send(200, { + message: 'Your password has been successfully changed.', + }); + } + + throw new HTTPError('You do not have permission to change password!', 401); + } catch (err) { + next(err); } - return res.send(401, { - errors: [ - { - message: 'You do not have permission to change password!', - }, - ], - }); }; export default { diff --git a/src/services/user.js b/src/services/user.js new file mode 100644 index 0000000..24cf89a --- /dev/null +++ b/src/services/user.js @@ -0,0 +1,104 @@ +import { Op } from 'sequelize'; + +import models from '../models'; +import HTTPError from '../exceptions/HTTPError'; +import { createSaltHashPassword, makeSha512 } from '../utils/encription'; + +const getUser = async (usernameOrId) => { + const inputType = typeof usernameOrId === 'string' ? 'username' : 'id'; + const user = await models.users.findOne({ + where: { + [inputType]: usernameOrId, + }, + }); + return user; +}; + +const createUser = async (username, email, password, name) => { + let user = await models.users.findOne({ + where: { + [Op.or]: { + username: username.trim(), + email: email.trim(), + }, + }, + }); + + if (user) { + throw new HTTPError('E-mail or username is already used!', 400); + } + + const { + hash: password_hash, + salt: password_salt, + } = createSaltHashPassword(password); + + user = await models.users.create({ + username, + email, + name, + password_hash, + password_salt, + }); + + return user; +}; + +const updateUser = async ({ + password, username, email, name, friends_ids, new_password, new_password_again, +}, id) => { + const user = await getUser(id); + + if (user) { + const passwordHash = makeSha512(password, user.password_salt); + if (passwordHash !== user.password_hash) { + throw new HTTPError('Password is incorrect please try again', 403); + } + let where = {}; + if (username || email) { + where = username ? { username } : { email }; + const isExist = await models.users.findOne({ + where, + }); + if (isExist) { + throw new HTTPError('E-mail or username is already used!', 403); + } + } + let passwordValdation = { hash: null, salt: null }; + if (new_password && new_password_again) { + if (new_password !== new_password_again) { + throw new HTTPError('Passwords must be same!', 403); + } + passwordValdation = createSaltHashPassword(new_password); + } + /* eslint-disable no-return-assign */ + where = Object.entries({ + username, + email, + name, + friends_ids, + password_hash: passwordValdation.hash, + password_salt: passwordValdation.salt, + }).reduce((a, [k, v]) => (v == null ? a : (a[k] = v, a)), {}); + const user2 = await user.update(where); + return user2.toJSON(); + } +}; + +const deleteUser = async (id) => { + const isDeleted = await models.users.destroy({ + where: { + id, + }, + }); + return isDeleted; +}; + +const UserService = { + getUser, + createUser, + updateUser, + deleteUser, +}; + +export default UserService; diff --git a/src/utils/encription.js b/src/utils/encription.js index aa827dd..6e1dac9 100644 --- a/src/utils/encription.js +++ b/src/utils/encription.js @@ -21,7 +21,7 @@ export const b64Encode = (value) => Buffer.from((value).toString()).toString('ba export const b64Decode = (b64value) => Buffer.from(b64value, 'base64').toString(); export const makeSha512 = (password, salt) => { - const hash = crypto.createHmac('sha512', salt); /** Hashing algorithm sha512 */ + const hash = crypto.createHmac('sha512', `${salt}`); /** Hashing algorithm sha512 */ hash.update(password); return hash.digest('hex');