diff --git a/config/environments/development.js b/config/environments/development.js index f9cf7a9..6e500c4 100644 --- a/config/environments/development.js +++ b/config/environments/development.js @@ -15,5 +15,10 @@ module.exports = { server: { hostname: process.env.HOSTNAME || '127.0.0.1', port: process.env.PORT || 1337 + }, + + paginate: { + limit: 10, + maxLimit: 50 } } diff --git a/core/controllers/find.js b/core/controllers/find.js new file mode 100644 index 0000000..f21b085 --- /dev/null +++ b/core/controllers/find.js @@ -0,0 +1,118 @@ +function extractFindOptions(req, res, targetAttributes) { + const result = {}; + + result.params = (req && req.params) ? req.params : null; + + if (res && res.locals) { + Object.keys(res.locals).forEach(key => { + result[key] = res.locals[key] + }) + } + + console.log(result); + return result; +} + +function parseFindOptions(findOptions, targetAttributes) { + const result = {}; + + // Where + if (findOptions.params) { + result.where = findOptions.params + } + + // Paginate + if (findOptions.paginate) { + result.page = findOptions.paginate.page; + result.limit = findOptions.paginate.limit; + } + + // Order + if (findOptions.sort) { + result.order = []; + Object.keys(findOptions.sort).forEach(key => { + let dir = findOptions.sort[key] ? 'ASC' : 'DESC'; + result.order.push([key, dir]) + }); + } + + // Attributes + if (findOptions.fields) { + if (findOptions.fields.validFields) { + result.attributes = findOptions.fields.validFields + } + if (findOptions.fields.invalidFields && Array.isArray(findOptions.fields.invalidFields)) { + result.attributes = targetAttributes.filter(attr => !findOptions.fields.invalidFields.includes(attr)); + } + } + + return result; +} + + +function checkIsModel(target) { + return !!target.getTableName; +} + +function checkIsAssociation(target) { + return !!target.associationType; +} + +function resultIsAList(params) { + const _params = Object.keys(params); + if (_params && Array.isArray(_params) && model.primaryKeyAttributes && Array.isArray(model.primaryKeyAttributes)) { + return !model.primaryKeyAttributes.every(field => _params.includes(field)); + } else { + return true; + } +} + +function _find(modelMaybeThunk, options = {}) { + /*if (typeof modelMaybeThunk != 'function' && !checkIsModel(modelMaybeThunk) && !checkIsAssociation(modelMaybeThunk)) { + throw new Error('resolverFactory should be called with a model, an association or a function (which resolves to a model or an association)'); + }*/ + + if (options.before === undefined) options.before = (options) => options; + if (options.after === undefined) options.after = (result) => result; + + return async function (req, res, next) { + let target = typeof modelMaybeThunk === 'function' && !checkIsModel(modelMaybeThunk) ? + await Promise.resolve(modelMaybeThunk(req, res, next)) : modelMaybeThunk; + + let isModel = checkIsModel(target), + isAssociation = checkIsAssociation(target), + association = isAssociation && target, + isList = options.list || resultIsAList(req.params), + model = isAssociation && target.target || isModel && target; + + let findOptions = extractFindOptions(req, res); + let targetAttributes = Object.keys(model.rawAttributes); + + findOptions = parseFindOptions(findOptions, targetAttributes); + + console.log(findOptions); + + return Promise.resolve(options.before(findOptions, req, res)) + .then(findOptions => { + return model[isList ? 'fetchAll' : 'fetch'](findOptions); + }) + .then(result => { + return options.after(result, req, res); + }); + } +} + + +const defaultOptions = { + //before: optimizeAttributes, +} + + +const find = (model, options = defaultOptions) => { + return async function (req, res, next) { + return _find(model, options)(req, res, next) + } +} + + +module.exports = find; \ No newline at end of file diff --git a/core/controllers/index.js b/core/controllers/index.js new file mode 100644 index 0000000..e842bc0 --- /dev/null +++ b/core/controllers/index.js @@ -0,0 +1,91 @@ +const _find = require('./find'); +const httpStatus = require('http-status'); +const { extractParamsFromRequest, handleErrorResponse, handleResultResponse } = require('../../helpers/controller.helper'); + +function buildContext(req, res) { + return { + user: req.user + } +} + +const defaultOptions = { + MODULE_NAME: 'default' +} + +const generateControllers = (service, extraControllers = {}, options = defaultOptions) => { + const defaultControllers = { + find: async (req, res, next) => { + const params = extractParamsFromRequest(req, res); + + try { + const result = await service.fetchAll(params, buildContext(req, res)); + return handleResultResponse(result.rows, result.count, params, res); + } catch (error) { + handleErrorResponse(options.MODULE_NAME, 'find', error, res) + } + }, + + findOne: async (req, res, next) => { + const params = extractParamsFromRequest(req, res); + + try { + const result = await service.fetchOne(params, buildContext(req, res)); + console.log(result); + return handleResultResponse(result, null, params, res, (result === null) ? httpStatus.NOT_FOUND : httpStatus.OK); + } catch (error) { + handleErrorResponse(options.MODULE_NAME, 'findOne', error, res) + } + }, + + count: async(req, res, next) => { + const params = extractParamsFromRequest(req, res); + + try { + const result = await service.count(params, buildContext(req, res)); + return handleResultResponse(result, null, params, res); + } catch (error) { + handleErrorResponse(options.MODULE_NAME, 'count', error, res) + } + }, + + create: async (req, res, next) => { + try { + const result = await service.create(req.body, buildContext(req, res)); + return handleResultResponse(result, null, null, res, httpStatus.CREATED) + } catch (error) { + handleErrorResponse(options.MODULE_NAME, 'create', error, res) + } + }, + + update: async (req, res, next) => { + try { + const result = await service.update(req.params, req.body, buildContext(req, res)); + return handleResultResponse(result, null, req.params, res, httpStatus.OK) + } catch (error) { + handleErrorResponse(options.MODULE_NAME, 'update', error, res) + } + }, + + delete: async (req, res, next) => { + try { + const result = await service.delete(req.params, buildContext(req, res)); + return handleResultResponse(result, null, req.params, res, httpStatus.NO_CONTENT); + } catch (error) { + handleErrorResponse(options.MODULE_NAME, 'delete', error, res) + } + }, + } + + //const associationControllers = generateAssociationControllers(model, options); + + //console.log(associationControllers); + + return { + ...defaultControllers, + //...associationControllers + ...extraControllers + } +} + + +module.exports = generateControllers; \ No newline at end of file diff --git a/core/express.js b/core/express.js index 612e84f..ba5b2df 100644 --- a/core/express.js +++ b/core/express.js @@ -10,10 +10,10 @@ const cors = require('cors'); const helmet = require('helmet'); const passport = require('passport'); +const config = require('../config'); const router = require('./router'); const error = require('../middlewares/error'); - /** * Express instance * @public @@ -75,7 +75,6 @@ app.use(cors({ app.use(passport.initialize()); require('./passport'); - // Set routes app.use('/api', router()); diff --git a/core/index.js b/core/index.js index 16d59d9..b651e02 100644 --- a/core/index.js +++ b/core/index.js @@ -5,24 +5,8 @@ const express = require('./express'); const logger = require('./logger'); const models = require('./models'); -//const modules = require('./modules'); - -//const middlewares = require('./middlewares'); -//const hooks = require('./hooks'); -//const plugins = require('./plugins'); -// const admin = require('./admin'); -//const store = require('./store'); - module.exports = { - //nestedConfigurations: nested, express, logger, - //appConfigurations: app, - //modules, models, - //middlewares, - //hooks, - //plugins, -// admin, - //store }; \ No newline at end of file diff --git a/core/passport.js b/core/passport.js index 38496a9..a057267 100644 --- a/core/passport.js +++ b/core/passport.js @@ -3,7 +3,6 @@ const passport = require('passport'); const { Strategy: LocalStrategy } = require('passport-local'); const { Strategy: JWTStrategy} = require('passport-jwt'); -const config = require('../config'); const models = require('./models'); const securityHelper = require('../helpers/security.helper'); @@ -28,7 +27,9 @@ const localOptions = { passport.use('local', new LocalStrategy(localOptions, async (email, password, done) => { try { - const user = await models.User.findOne({ where: { email } }); + const user = await models.User.findOne({ + where: { email }, + }); if (_.isNull(user)) { return done(null, false, { message: 'User not found' }) @@ -38,6 +39,7 @@ passport.use('local', new LocalStrategy(localOptions, async (email, password, do if (!isPasswordValid) { return done(null, false, { message: 'Wrong Password' }) } else { + delete user.password; return done(null, user, { message: 'Logged in Successfully' }); } } @@ -49,7 +51,11 @@ passport.use('local', new LocalStrategy(localOptions, async (email, password, do // JWT passport.use('jwt', new JWTStrategy(securityHelper.jwtOptions, async (jwtPayload, done) => { try { - const user = await models.User.findOne({ where: { id: jwtPayload.id } }); + const user = await models.User.findOne({ + attributes: { exclude: [ 'password' ] }, + where: { id: jwtPayload.id }, + raw: true + }); if (_.isNull(user)) { return done(null, false, { message: 'User not found' }) diff --git a/helpers/controller.helper.js b/helpers/controller.helper.js index b345f45..6c65b69 100644 --- a/helpers/controller.helper.js +++ b/helpers/controller.helper.js @@ -33,22 +33,95 @@ function buildErrorResponse(nameController, nameMethod, error) { return jsonResultFailed; } +function getTotalCount(result) { + + const toType = function (obj) { + return ({}).toString.call(obj).match(/\s([a-zA-Z]+)/)[1].toLowerCase() + } + + switch (toType(result)) { + case 'boolean': + return 1; + case 'object': + return 1; + case 'array': + return result.length; + case 'null': + return 0; + default: + return 0; + } +} + +function setPaginationInfo(totalCount, res) { + res.set({ + 'X-Total-Count': totalCount, + }); + + const params = extractParamsFromRequest(null, res); + console.log(params); + if (params.paginate) { + const + page = (params.paginate && params.paginate.page) ? params.paginate.page : null, + limit = (params.paginate && params.paginate.limit) ? params.paginate.limit : null, + count = (limit) ? Math.ceil(totalCount / limit) : null; + + if (params.paginate.hasNextPages(count)) { + const nextPage = params.paginate.href(); + res.set('Link', nextPage + '; rel=next'); + res.set('Pagination-Next-Page', true); + } else { + res.set('Pagination-Next-Page', false); + } + + res.set({ + 'Pagination-Count': count, + 'Pagination-Page': page, + 'Pagination-Limit': limit, + }); + } +} + //////////////////////////////////////////////////////////////////////////////// // PUBLIC FUNCTIONS //////////////////////////////////////////////////////////////////////////////// +function extractParamsFromRequest(req, res) { + const result = {}; + + result.params = (req && req.params) ? req.params : null; + result.query = (req && req.query) ? req.params : null; + + if (res && res.locals) { + Object.keys(res.locals).forEach(key => { + result[key] = res.locals[key] + }) + } + + return result; +} + + function handleErrorResponse(controllerName, methodName, error, res) { const jsonResultFailed = buildErrorResponse(controllerName, methodName, error); res.status(httpStatus.INTERNAL_SERVER_ERROR).send(jsonResultFailed); } +function handleResultResponse(result, totalCount = null, params, res, statusCode = httpStatus.OK) { + setPaginationInfo((totalCount) ? totalCount : getTotalCount(result), res); + res.status(statusCode).send(result); +} + + //////////////////////////////////////////////////////////////////////////////// // MODULE EXPORTS //////////////////////////////////////////////////////////////////////////////// module.exports = { + extractParamsFromRequest, handleErrorResponse, + handleResultResponse, // for testing buildErrorLog, buildErrorResponse diff --git a/helpers/middleware.helper.js b/helpers/middleware.helper.js new file mode 100644 index 0000000..22cf456 --- /dev/null +++ b/helpers/middleware.helper.js @@ -0,0 +1,19 @@ +'use strict'; + +function compose(middlewareArray) { + if (!middlewareArray.length) { + return function (_req, _res, next) { next(); }; + } + + let head = middlewareArray[0]; + let tail = middlewareArray.slice(1); + + return function (req, res, next) { + head(req, res, function (err) { + if (err) return next(err); + compose(tail)(req, res, next); + }); + }; +} + +module.exports = compose; \ No newline at end of file diff --git a/helpers/service.helper.js b/helpers/service.helper.js new file mode 100644 index 0000000..8b94107 --- /dev/null +++ b/helpers/service.helper.js @@ -0,0 +1,206 @@ +const _ = require('lodash'); + +const parseParamsToFindOptions = (params) => { + const result = {}; + + // Query + if (params.query) { + result.query = params.query + } + + // Where + if (params.params) { + result.where = params.params + } + + // Paginate + if (params.paginate) { + result.offset = params.paginate.limit * (params.paginate.page - 1); + result.limit = result.offset + params.paginate.limit; + } + + // Order + if (params.sort) { + result.order = []; + Object.keys(params.sort).forEach(key => { + let dir = params.sort[key] ? 'ASC' : 'DESC'; + result.order.push([key, dir]) + }); + } + + // Attributes + if (params.fields) { + if (params.fields.validFields) { + result.attributes = params.fields.validFields + } + if (params.fields.invalidFields && Array.isArray(params.fields.invalidFields)) { + result.attributes = { + ...result.attributes, + exclude: params.fields.invalidFields + } + } + } + + return result; +} + + +const defaultOptions = {}; + +const generateService = (model, extraMethods = {}, options = defaultOptions) => { + const defaultService = { + + fetchAll: async (params, context) => { + const findOptions = parseParamsToFindOptions(params); + return await model.findAndCountAll(findOptions); + }, + + fetchOne: async (params, context) => { + return await model.findOne({ where: { 'id': params.id } }); + }, + + count: async (params, context) => { + // Convert `params` object to filters compatible with Bookshelf. + //const filters = strapi.utils.models.convertParams('post', params); + + return await model.count(params); + }, + + create: async (values, context) => { + return await model.create(values); + }, + + update: async (params, values, context) => { + // Extract values related to relational data. + const relations = _.pick(values, Post.associations.map(ast => ast.alias)); + const data = _.omit(values, Post.associations.map(ast => ast.alias)); + + // Create entry with no-relational data. + const entry = await Post.forge(params).save(data); + + // Create relational data and return the entry. + return Post.updateRelations(Object.assign(params, { + values: relations + })); + }, + + delete: async (params, context) => { + const numAffectedRows = await model.destroy({ where: { 'id': params.id } }); + return (numAffectedRows > 0); + + /*params.values = {}; + Post.associations.map(association => { + switch (association.nature) { + case 'oneWay': + case 'oneToOne': + case 'manyToOne': + case 'oneToManyMorph': + params.values[association.alias] = null; + break; + case 'oneToMany': + case 'manyToMany': + case 'manyToManyMorph': + params.values[association.alias] = []; + break; + default: + } + }); + + await Post.updateRelations(params); + + return Post.forge(params).destroy();*/ + }, + + search: async (params, context) => { + // Convert `params` object to filters compatible with Bookshelf. + const filters = strapi.utils.models.convertParams('post', params); + // Select field to populate. + const populate = Post.associations + .filter(ast => ast.autoPopulate !== false) + .map(ast => ast.alias); + + const associations = Post.associations.map(x => x.alias); + const searchText = Object.keys(Post._attributes) + .filter(attribute => attribute !== Post.primaryKey && !associations.includes(attribute)) + .filter(attribute => ['string', 'text'].includes(Post._attributes[attribute].type)); + + const searchNoText = Object.keys(Post._attributes) + .filter(attribute => attribute !== Post.primaryKey && !associations.includes(attribute)) + .filter(attribute => !['string', 'text', 'boolean', 'integer', 'decimal', 'float'].includes(Post._attributes[attribute].type)); + + const searchInt = Object.keys(Post._attributes) + .filter(attribute => attribute !== Post.primaryKey && !associations.includes(attribute)) + .filter(attribute => ['integer', 'decimal', 'float'].includes(Post._attributes[attribute].type)); + + const searchBool = Object.keys(Post._attributes) + .filter(attribute => attribute !== Post.primaryKey && !associations.includes(attribute)) + .filter(attribute => ['boolean'].includes(Post._attributes[attribute].type)); + + const query = (params._q || '').replace(/[^a-zA-Z0-9.-\s]+/g, ''); + + return Post.query(qb => { + // Search in columns which are not text value. + searchNoText.forEach(attribute => { + qb.orWhereRaw(`LOWER(${attribute}) LIKE '%${_.toLower(query)}%'`); + }); + + if (!_.isNaN(_.toNumber(query))) { + searchInt.forEach(attribute => { + qb.orWhereRaw(`${attribute} = ${_.toNumber(query)}`); + }); + } + + if (query === 'true' || query === 'false') { + searchBool.forEach(attribute => { + qb.orWhereRaw(`${attribute} = ${_.toNumber(query === 'true')}`); + }); + } + + // Search in columns with text using index. + switch (Post.client) { + case 'mysql': + qb.orWhereRaw(`MATCH(${searchText.join(',')}) AGAINST(? IN BOOLEAN MODE)`, `*${query}*`); + break; + case 'pg': + { + const searchQuery = searchText.map(attribute => + _.toLower(attribute) === attribute ? + `to_tsvector(${attribute})` : + `to_tsvector('${attribute}')` + ); + + qb.orWhereRaw(`${searchQuery.join(' || ')} @@ to_tsquery(?)`, query); + break; + } + } + + if (filters.sort) { + qb.orderBy(filters.sort.key, filters.sort.order); + } + + if (filters.skip) { + qb.offset(_.toNumber(filters.skip)); + } + + if (filters.limit) { + qb.limit(_.toNumber(filters.limit)); + } + }).fetchAll({ + withRelated: populate + }); + } + } + + return { + ...defaultService, + //...associationControllers + ...extraMethods + } +} + + +module.exports = { + generateService, + parseParamsToFindOptions, + defaultOptions +} \ No newline at end of file diff --git a/middlewares/accessValidator.js b/middlewares/accessValidator.js index 209907c..e1ba954 100644 --- a/middlewares/accessValidator.js +++ b/middlewares/accessValidator.js @@ -1,19 +1,24 @@ 'use strict'; const passport = require('passport'); +const httpStatus = require('http-status'); +const compose = require('../helpers/middleware.helper'); -exports.isRegisteresUser = passport.authenticate('local', { session: false }); -exports.isLoggedUser = passport.authenticate('jwt', { session: false }); - -/** - * Authorization Required middleware. - */ -exports.isAuthorized = (req, res, next) => { - const provider = req.path.split('/').slice(-1)[0]; - const token = req.user.tokens.find(token => token.kind === provider); - if (token) { - next(); - } else { - res.redirect(`/auth/${provider}`); +const isRegisteredUser = passport.authenticate('local', { session: false }); +const isLoggedUser = passport.authenticate('jwt', { session: false }); +const isAdministratorUser = compose([isLoggedUser, + (req, res, next) => { + const user = req.user; + if (user.role >= 8) { + next(); + } else { + return res.status(httpStatus.UNAUTHORIZED).send('UNAUTHORIZED'); + } } +]); + +module.exports = { + isRegisteredUser, + isLoggedUser, + isAdministratorUser }; \ No newline at end of file diff --git a/middlewares/fields.js b/middlewares/fields.js new file mode 100644 index 0000000..d7f3296 --- /dev/null +++ b/middlewares/fields.js @@ -0,0 +1,66 @@ +"use strict"; + +/* + Fields middleware + This module exports a function that takes an optional configuration object parameter with the following + properties: + { + validFields: Array of strings (case sensitive), + invalidFields: Array of strings (case sensitive), + } + and returns an express compatible middleware function (req, res, next) that parses the following + parameters from the req.query object and sets an object on the res.locals.fields having the following + properties: + + Object - with fields for the attributes value fields + +*/ + +const middleware = (config) => { + + config = config || { + validFields: null, + invalidFields: null, + }; + + if (config.validFields && (!Array.isArray(config.validFields))) { + throw new Error("config.validFields should be a non empty array of strings"); + } + + if (config.invalidFields && (!Array.isArray(config.invalidFields))) { + throw new Error("config.invalidFields should be a non empty array of strings"); + } + + return function (req, res, next) { + const fieldNames = Array.isArray(req.query.fields) + ? req.query.fields + : req.query.fields + ? [req.query.fields] + : []; + + const fieldObject = fieldNames.filter(field => { + let isOk = true; + if (config.validFields) { + isOk = config.validFields.includes(field) + } + + if (config.invalidFields) { + isOk = !config.invalidFields.includes(field) + } + + return isOk; + }); + + res.locals.fields = {}; + + (Object.keys(fieldObject).length > 0) ? res.locals.fields['fields'] = fieldObject : null; + config.invalidFields ? res.locals.fields['invalidFields'] = config.invalidFields : null; + config.validFields ? res.locals.fields['validFields'] = config.validFields : null, + + next(); + }; +} + +module.exports = { + middleware +} \ No newline at end of file diff --git a/middlewares/paginate.js b/middlewares/paginate.js new file mode 100644 index 0000000..b849909 --- /dev/null +++ b/middlewares/paginate.js @@ -0,0 +1,122 @@ +'use strict'; + +// Node.js pagination middleware and view helpers. + +// * Author: [@niftylettuce](https://twitter.com/#!/niftylettuce) +// * Source: + +const qs = require('qs'); +const url = require('url'); +const _ = require('lodash'); +//const util = require('util'); + +const href = (req) => { + + return function (prev, params) { + const query = _.clone(req.query); + + if (typeof prev === 'object') { + params = prev; + prev = false; + } else { + prev = (typeof prev === 'boolean') ? prev : false; + query.page = parseInt(query.page, 10); + query.page = prev ? query.page -= 1 : query.page += 1; + query.page = (query.page < 1) ? 1 : query.page; + } + + // allow overriding querystring params + // (useful for sorting and filtering) + // another alias for `_.assign` is `_.extend` + if (_.isObject(params)) + query = _.assign(query, params); + + return url.parse(req.originalUrl).pathname + '?' + qs.stringify(query); + }; +}; + + +const hasNextPages = (req) => { + return function (pageCount) { + if (typeof pageCount !== 'number' || pageCount < 0) + throw new Error('middleware paginate: `pageCount` is not a number >= 0'); + return req.query.page < pageCount; + }; +}; + + +const getArrayPages = (req) => { + return function (limit, pageCount, currentPage) { + const maxPage = pageCount; + + // limit default is 3 + limit = limit || 3; + + if (typeof limit !== 'number' || limit < 0) + throw new Error('middleware paginate: `limit` is not a number >= 0'); + + if (typeof pageCount !== 'number' || pageCount < 0) + throw new Error('middleware paginate: `pageCount` is not a number >= 0'); + + currentPage = parseInt(currentPage, 10); + if (Number.isNaN(currentPage) || currentPage < 0) + throw new Error('middleware paginate: `currentPage` is not a number >= 0'); + + if (limit > 0) { + let end = Math.min(Math.max(currentPage + Math.floor(limit / 2), limit), pageCount); + let start = Math.max(1, (currentPage < (limit - 1)) ? 1 : (end - limit) + 1); + + let pages = []; + for (let i = start; i <= end; i++) { + pages.push({ + number: i, + url: href(req)() + .replace('page=' + (currentPage + 1), 'page=' + i) + }); + } + + return pages; + } + } +} + + +const middleware = (limit = 10, maxLimit = 50) => { + + const _limit = (typeof limit === 'number') ? parseInt(limit, 10) : 10; + const _maxLimit = (typeof maxLimit === 'number') ? parseInt(maxLimit, 10) : 50; + + return function _middleware(req, res, next) { + req.query.page = (typeof req.query.page === 'string') ? parseInt(req.query.page, 10) || 1 : 1; + req.query.limit = (typeof req.query.limit === 'string') ? parseInt(req.query.limit, 10) || 0 : _limit; + + if (req.query.limit > _maxLimit) + req.query.limit = _maxLimit; + + if (req.query.page < 1) + req.query.page = 1; + + if (req.query.limit < 0) + req.query.limit = 0; + + //req.skip = req.offset = (req.query.page * req.query.limit) - req.query.limit; + + res.locals.paginate = res.locals.paginate ? res.locals.paginate : {}; + res.locals.paginate.page = req.query.page; + res.locals.paginate.limit = req.query.limit; + res.locals.paginate.href = href(req); + res.locals.paginate.hasPreviousPages = req.query.page > 1; + res.locals.paginate.hasNextPages = hasNextPages(req); + res.locals.paginate.getArrayPages = getArrayPages(req); + + next(); + }; + +}; + +module.exports = { + href, + hasNextPages, + getArrayPages, + middleware +} \ No newline at end of file diff --git a/middlewares/schemaValidator.js b/middlewares/schemaValidator.js index d5d2753..1d58c07 100644 --- a/middlewares/schemaValidator.js +++ b/middlewares/schemaValidator.js @@ -28,7 +28,7 @@ module.exports = (schema, useJoiError = false) => { const _schema = schema if (_schema) { - console.log(req.body); + // Validate req.body using the schema and validation options return Joi.validate(req.body, _schema, _validationOptions, (err, data) => { diff --git a/middlewares/sort.js b/middlewares/sort.js new file mode 100644 index 0000000..7396e61 --- /dev/null +++ b/middlewares/sort.js @@ -0,0 +1,67 @@ +"use strict"; + +/* + Sort middleware + This module exports a function that takes a configuration object parameter with the following + properties: + { + validKeys: Array of strings (case sensitive), + default: String (optional, by default assigned to the first element of the + validKeys array with ascending sorting order unless prefixed by a "-") + } + and returns an express compatible middleware function (req, res, next) that parses the following + parameters from the req.query object and sets an object on the res.locals.sort having the following + properties: + + Object - with keys for the sort value fields and value a boolean denoting ascending order + or not + +*/ + +const middleware = (config) => { + if (typeof config !== "object" || config === null) { + throw new Error("The config parameter is mandatory and should be an object!"); + } + + config = config || {}; + if (!config.default && (!Array.isArray(config.validKeys) || config.validKeys.length === 0)) { + throw new Error("config.validKeys should be a non empty array of strings or a config.default key should be defined!"); + } + + config.default = config.default || config.validKeys[0]; + + const defaultSortIsAscending = config.default.substring(0, 1) !== "-"; + + return function (req, res, next) { + const sortKeys = Array.isArray(req.query.sort) + ? req.query.sort + : req.query.sort + ? [req.query.sort] + : []; + + const sortObject = sortKeys.reduce(function (c, key) { + const ascending = key.substring(0, 1) !== "-"; + if (!ascending) { + key = key.substr(1); + } + if (key && config.validKeys && config.validKeys.indexOf(key) !== -1 ) { + c[key] = ascending; + } + return c; + }, {}); + + if (Object.keys(sortObject).length === 0) { + sortObject[ + defaultSortIsAscending + ? config.default + : config.default.substr(1) + ] = defaultSortIsAscending; + } + res.locals.sort = sortObject; + next(); + }; +} + +module.exports = { + middleware +} \ No newline at end of file diff --git a/modules/auth/auth.controller.js b/modules/auth/auth.controller.js index ac9da5a..67e778c 100644 --- a/modules/auth/auth.controller.js +++ b/modules/auth/auth.controller.js @@ -2,11 +2,9 @@ const _ = require('lodash'); const httpStatus = require('http-status'); -const passport = require('passport'); const controllerHelper = require('../../helpers/controller.helper'); const messageHelper = require('../../helpers/message.helper'); const securityHelper = require('../../helpers/security.helper'); -const authService = require('./auth.service'); //////////////////////////////////////////////////////////////////////////////// diff --git a/modules/auth/auth.routes.js b/modules/auth/auth.routes.js index 57f7a3d..ea49620 100644 --- a/modules/auth/auth.routes.js +++ b/modules/auth/auth.routes.js @@ -13,7 +13,7 @@ const AccessValidator = require('../../middlewares/accessValidator'); routes.post('/auth', SchemaValidator(authValidation.LoginInputType, true), - AccessValidator.isRegisteresUser, + AccessValidator.isRegisteredUser, authController.login, ); diff --git a/modules/auth/user.model.js b/modules/auth/user.model.js index 0cf3fc5..04bbee7 100644 --- a/modules/auth/user.model.js +++ b/modules/auth/user.model.js @@ -19,6 +19,11 @@ module.exports = function (sequelize, DataTypes) { type: DataTypes.STRING, allowNull: false, }, + role: { + type: DataTypes.TINYINT, + allowNull: false, + defaultValue: 0, + }, }, { tableName: 'user', freezeTableName: true, diff --git a/modules/blog/blog.controller.js b/modules/blog/blog.controller.js new file mode 100644 index 0000000..65c1fc1 --- /dev/null +++ b/modules/blog/blog.controller.js @@ -0,0 +1,21 @@ +'use strict'; + +const generateControllers = require('../../core/controllers'); +const blogService = require('./blog.service'); + + +// Module Name +const MODULE_NAME = '[blog.controller]'; + +// Error Messages +//const NOT_FOUND = 'Videogame not found'; + +// Success Messages +//const VG_CT_VIDEOGAME_DELETED_SUCCESSFULLY = 'Videogame deleted successfully'; + + +const controllerOptions = { MODULE_NAME }; +const extraControllers = {}; + +module.exports = generateControllers(blogService, extraControllers, controllerOptions); + diff --git a/modules/blog/blog.routes.js b/modules/blog/blog.routes.js index e8be15e..7b0cac1 100644 --- a/modules/blog/blog.routes.js +++ b/modules/blog/blog.routes.js @@ -1,35 +1,37 @@ const routes = require('express').Router(); -const models = require('../../core/models'); -//const postService = require('./post.service')(models.Post); -//const postController = require('./post.controller')(postService); -const { ModelHandler } = require('sequelize-handlers'); -const postHandler = new ModelHandler(models.Post); +/*const postService = require('./post.service')(models.Post); +const postController = require('./post.controller')(postService);*/ -routes.use((req, res, next) => { - // here we can access the req.params object and make auth checks - next(); -}); +const { isAdministratorUser, isLoggedUser } = require('../../middlewares/accessValidator'); +const PaginateMiddleware = require('../../middlewares/paginate'); +const SortMiddleware = require('../../middlewares/sort'); +const FieldMiddleware = require('../../middlewares/fields'); -routes.get('/posts', postHandler.query()); +const blogController = require('./blog.controller') -/*routes.get('/posts', function (req, res) { - postController.find(req).then(data => { - console.log(data); - res.status(200).json(data); - }) -});*/ - -routes.get('/posts/count', function (req, res) { - //res.status(200).json(postController.count(req)); -}); - -routes.get('/posts/:id', function (req, res) { - //res.status(200).json(postController.findOne(req)); -}); +routes.get('/posts', + isLoggedUser, + FieldMiddleware.middleware({ + invalidFields: ['user', 'created'] + }), + PaginateMiddleware.middleware(), + SortMiddleware.middleware({ default: "date" }), + blogController.find); +routes.post('/posts', isAdministratorUser, blogController.create); +routes.get('/posts/:id', + isLoggedUser, + FieldMiddleware.middleware({ + invalidFields: ['updatedAt', 'createdAt'] + }), + //PaginateMiddleware.middleware(), + //SortMiddleware.middleware({ default: "date" }), + blogController.findOne); +routes.put('/posts/:id', isAdministratorUser, blogController.update); +routes.delete('/posts/:id', isAdministratorUser, blogController.delete); module.exports = routes; \ No newline at end of file diff --git a/modules/blog/blog.service.js b/modules/blog/blog.service.js new file mode 100644 index 0000000..d753810 --- /dev/null +++ b/modules/blog/blog.service.js @@ -0,0 +1,23 @@ +/* global Post */ +'use strict'; + +const _ = require('lodash'); +const { generateService, parseParamsToFindOptions } = require('../../helpers/service.helper'); +const models = require('../../core/models'); + +const extraMethods = { + fetchOne: async (params, context) => { + const findOptions = parseParamsToFindOptions(params); + console.log(findOptions); + const result = await models.Post.findByPk(findOptions.query.id, { + include: [{ all: true }] + }); + + console.log(result); + return result; + }, + + +} + +module.exports = generateService(models.Post, extraMethods); \ No newline at end of file diff --git a/modules/blog/category.controller.js b/modules/blog/category.controller.js new file mode 100644 index 0000000..214a0d4 --- /dev/null +++ b/modules/blog/category.controller.js @@ -0,0 +1,14 @@ +'use strict'; + +const generateControllers = require('../../core/controllers'); +const categoryService = require('./category.service'); + + +// Module Name +const MODULE_NAME = '[category.controller]'; + +const controllerOptions = { MODULE_NAME }; +const extraControllers = {}; + +module.exports = generateControllers(categoryService, extraControllers, controllerOptions); + diff --git a/modules/blog/category.model.js b/modules/blog/category.model.js index 4d05880..9a3ddc8 100755 --- a/modules/blog/category.model.js +++ b/modules/blog/category.model.js @@ -2,6 +2,7 @@ module.exports = function (sequelize, DataTypes) { const Category = sequelize.define('Category', { id: { type: DataTypes.INTEGER, + autoIncrement: true, primaryKey: true }, name: { @@ -11,7 +12,7 @@ module.exports = function (sequelize, DataTypes) { sort: { type: DataTypes.INTEGER, defaultValue: 0, - allowNull: false, + allowNull: false, } }, { tableName: 'category', @@ -20,7 +21,7 @@ module.exports = function (sequelize, DataTypes) { }); Category.beforeCreate((category) => { - category.dataValues.id = Math.floor(Math.random() * (999 - 8)) + 8; + //category.dataValues.id = Math.floor(Math.random() * (999 - 8)) + 8; }) Category.associate = function (models) { diff --git a/modules/blog/category.routes.js b/modules/blog/category.routes.js new file mode 100644 index 0000000..97c8d20 --- /dev/null +++ b/modules/blog/category.routes.js @@ -0,0 +1,17 @@ +const routes = require('express').Router(); + +const { isAdministratorUser, isLoggedUser } = require('../../middlewares/accessValidator'); +/*const PaginateMiddleware = require('../../middlewares/paginate'); +const SortMiddleware = require('../../middlewares/sort'); +const FieldMiddleware = require('../../middlewares/fields');*/ + +const categoryController = require('./category.controller'); + +routes.get('/categories', isLoggedUser, categoryController.find); +routes.get('/categories/:id', isLoggedUser, categoryController.findOne); + +routes.post('/categories/', isAdministratorUser, categoryController.create); +routes.put('/categories/:id', isAdministratorUser, categoryController.update); +routes.delete('/categories/:id', isAdministratorUser, categoryController.delete); + +module.exports = routes; \ No newline at end of file diff --git a/modules/blog/category.service.js b/modules/blog/category.service.js new file mode 100644 index 0000000..cc45bd4 --- /dev/null +++ b/modules/blog/category.service.js @@ -0,0 +1,10 @@ +/* global Post */ +'use strict'; + +const _ = require('lodash'); +const { generateService, parseParamsToFindOptions } = require('../../helpers/service.helper'); +const models = require('../../core/models'); + +const extraMethods = {}; + +module.exports = generateService(models.Category, extraMethods); \ No newline at end of file diff --git a/package.json b/package.json index 7449b67..3cbb950 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,6 @@ "pino": "^4.7.1", "response-time": "^2.3.2", "sequelize": "^5.3.5", - "sequelize-handlers": "^1.0.7", "vm": "^0.1.0", "winston": "^3.2.1" } diff --git a/yarn.lock b/yarn.lock index 62ea133..522b7c1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2828,13 +2828,6 @@ send@0.16.2: range-parser "~1.2.0" statuses "~1.4.0" -sequelize-handlers@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/sequelize-handlers/-/sequelize-handlers-1.0.7.tgz#1ef1d81447bdcd608ae42f5855d7fc2d978bf57a" - integrity sha512-fyNPYLSF03mRwy/gyJJOxH7fN3R6os5Ptsj18Gezfpt/T6mLFwOvxHp+dWd6O1KDNBr0JZi4kIj8FuDq55upbw== - dependencies: - lodash "^4.17.11" - sequelize-pool@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/sequelize-pool/-/sequelize-pool-1.0.2.tgz#89c767882bbdb8a41dac66922ed9820939a5401e"