diff --git a/README.md b/README.md index ce32b1e..74a4e31 100644 --- a/README.md +++ b/README.md @@ -1 +1,14 @@ -commerce +# Template for REST API with Node, Babel and Mongo + +## Environment variables + +| Variable | Description | +| ------ | ------ | +| **NODE_ENV** | "prod" or other (dev is default), define in which environment the server will run | +| **APP_PORT** | define in which port the server will run | +| **DB_PROD** | define the Mongo production host | +| **DB_PROD** | define the Mongo production host | +| **ENCRYPT_TEXT** | define the string that will encrypt password when create a new user | +| **PRIVATE_KEY** | define the string that will check if token is valid | + +Made with ♥ by Victor Lucas \ No newline at end of file diff --git a/src/controller/ProfileController.js b/src/controller/ProfileController.js new file mode 100644 index 0000000..5f31db6 --- /dev/null +++ b/src/controller/ProfileController.js @@ -0,0 +1,67 @@ +import { ProfileRepository } from "../repository"; +import { GlobalHandler, ThrowableError, getMessage } from "../utils"; + +export default class ProfileController { + + constructor(){ + this.repository = new ProfileRepository(); + } + + async listProfiles(req, res) { + try { + const filters = req.filters; + const { includes } = req.query; + + let profiles = await this.repository.findAll(filters, includes) + res.send(profiles) + } catch(error){ + const sanitizedError = GlobalHandler.handle(error); + + res.status(sanitizedError.code).send(sanitizedError) + } + } + + async saveProfile(req, res) { + try { + const profile = req.body; + + let savedProfile = await this.repository.store(profile) + res.send(savedProfile) + } catch (error){ + const sanitizedError = GlobalHandler.handle(error); + + res.status(sanitizedError.code).send(sanitizedError) + } + } + + async updateProfileRoles(req, res) { + try { + const { id } = req.params; + const roles = req.body; + + if(!Array.isArray(roles)) throw new ThrowableError(getMessage('bodyMustBeArray')('roles'), 'ValidationError', 400); + + let updatedProfile = await this.repository.update(roles, id) + res.send(updatedProfile) + } catch (error){ + const sanitizedError = GlobalHandler.handle(error); + + res.status(sanitizedError.code).send(sanitizedError) + } + } + + async deleteProfile(req, res) { + try { + const { id } = req.params; + + await this.repository.delete(id) + res.json({ + message: `Profile with id ${id} successful deleted!` + }) + } catch (error){ + const sanitizedError = GlobalHandler.handle(error); + + res.status(sanitizedError.code).send(sanitizedError) + } + } +} \ No newline at end of file diff --git a/src/controller/RoleController.js b/src/controller/RoleController.js new file mode 100644 index 0000000..8341ff3 --- /dev/null +++ b/src/controller/RoleController.js @@ -0,0 +1,44 @@ +import { RoleRepository } from "../repository"; +import { GlobalHandler, ThrowableError, } from "../utils"; + +export default class ProfileController { + + constructor(){ + this.repository = new RoleRepository(); + } + + async listRoles(req, res) { + const filters = req.filters; + + let roles = await this.repository.findAll(filters) + res.send(roles) + } + + async saveRole(req, res) { + try { + const role = req.body; + + let savedRole = await this.repository.store(role) + res.send(savedRole) + } catch (error){ + const sanitizedError = GlobalHandler.handle(error); + + res.status(sanitizedError.code).send(sanitizedError) + } + } + + async deleteRole(req, res) { + try { + const { id } = req.params; + + await this.repository.delete(id) + res.json({ + message: `Role with id ${id} successful deleted!` + }) + } catch (error){ + const sanitizedError = GlobalHandler.handle(error); + + res.status(sanitizedError.code).send(sanitizedError) + } + } +} \ No newline at end of file diff --git a/src/controller/UserController.js b/src/controller/UserController.js index fd0dfe5..b69a112 100644 --- a/src/controller/UserController.js +++ b/src/controller/UserController.js @@ -15,6 +15,8 @@ export default class UserController { const user = req.body; const foundUser = await this.repository.findOneByEmail(user.email); + + foundUser._doc.profile.roles.map((v, index) => foundUser._doc.profile.roles[index] = v.name); const validPass = checkEncrypt(user.password, foundUser.password); @@ -25,6 +27,8 @@ export default class UserController { const token = jwt.sign({ id: foundUser._id, email: foundUser.email, + profile: foundUser.profile.name, + roles: foundUser.profile.roles }, environment.privateJWT, { expiresIn: "7d" } diff --git a/src/controller/index.js b/src/controller/index.js index 27aff16..77899f7 100644 --- a/src/controller/index.js +++ b/src/controller/index.js @@ -1,3 +1,7 @@ import user from "./UserController.js"; +import profile from "./ProfileController"; +import role from "./RoleController"; -export const UserController = user; \ No newline at end of file +export const UserController = user; +export const ProfileController = profile; +export const RoleController = role; \ No newline at end of file diff --git a/src/middleware/HasRoles.js b/src/middleware/HasRoles.js new file mode 100644 index 0000000..659003b --- /dev/null +++ b/src/middleware/HasRoles.js @@ -0,0 +1,25 @@ +import { GlobalHandler, ThrowableError, getMessage } from "../utils"; + +export const HasRoles = function(roles = []) { + return [ + (req,res,next) => { + try { + if(req.decoded && req.decoded.roles) { + if(req.decoded.roles.length == 0) throw new ThrowableError(getMessage('unauthorizedAccess'), 'AuthorizationError', 403); + req.decoded.roles.forEach(role => { + if(!roles.includes(role)) { + throw new ThrowableError(getMessage('roleNotPresent')(role), 'AuthorizationError', 403); + } + }) + + next(); + } else throw new ThrowableError(getMessage('unauthorizedAccess'), 'AuthorizationError', 403); + + } catch (error) { + const sanitizedError = GlobalHandler.handle(error); + + return res.status(sanitizedError.code).send(sanitizedError) + } + } + ]; +} \ No newline at end of file diff --git a/src/middleware/index.js b/src/middleware/index.js index 31b081a..5226c19 100644 --- a/src/middleware/index.js +++ b/src/middleware/index.js @@ -1,3 +1,4 @@ export * from "./Cors"; export * from "./RetrieveFilters"; export * from "./CheckToken"; +export * from "./HasRoles"; diff --git a/src/model/ProfileModel.js b/src/model/ProfileModel.js new file mode 100644 index 0000000..35da9f9 --- /dev/null +++ b/src/model/ProfileModel.js @@ -0,0 +1,30 @@ +import { Schema, model } from "mongoose"; + +const schema = new Schema({ + name: { + type: String, + required: true, + unique: true, + }, + + roles: [ + { + type: String, + ref: "Role" + } + ], + + lastUpdated: { + type: Date, + default: Date.now(), + }, + + createdAt: { + type: Date, + default: Date.now(), + }, +}, { + versionKey: false, +}); + +export default model("Profile", schema, "profiles"); \ No newline at end of file diff --git a/src/model/RoleModel.js b/src/model/RoleModel.js new file mode 100644 index 0000000..90ca8c3 --- /dev/null +++ b/src/model/RoleModel.js @@ -0,0 +1,23 @@ +import { Schema, model } from "mongoose"; + +const schema = new Schema({ + name: { + type: String, + required: true, + unique: true, + }, + + lastUpdated: { + type: Date, + default: Date.now(), + }, + + createdAt: { + type: Date, + default: Date.now(), + }, +}, { + versionKey: false, +}); + +export default model("Role", schema, "roles"); diff --git a/src/model/UserModel.js b/src/model/UserModel.js index 39998f8..0c5568e 100644 --- a/src/model/UserModel.js +++ b/src/model/UserModel.js @@ -12,6 +12,11 @@ const schema = new Schema({ default: "", }, + profile: { + type: String, + ref: "Profile" + }, + lastUpdated: { type: Date, default: Date.now(), diff --git a/src/model/index.js b/src/model/index.js index 852f087..165cd2e 100644 --- a/src/model/index.js +++ b/src/model/index.js @@ -1,3 +1,7 @@ import user from "./UserModel"; +import profile from "./ProfileModel"; +import role from "./RoleModel"; -export const UserModel = user; \ No newline at end of file +export const UserModel = user; +export const ProfileModel = profile; +export const RoleModel = role; \ No newline at end of file diff --git a/src/repository/ProfileRepository.js b/src/repository/ProfileRepository.js new file mode 100644 index 0000000..1abf35e --- /dev/null +++ b/src/repository/ProfileRepository.js @@ -0,0 +1,68 @@ +import { ProfileModel } from "../model"; +import { ThrowableError } from "../utils"; +import { getMessage } from "../utils"; + +export default class ProfileRepository { + /** + * List all profiles in DB + * @param {object} [filters={}] + * @param {string[]} [includes=[]] + * @memberof ProfileRepository + */ + async findAll(filters = {}, includes = null){ + let profiles = await ProfileModel + .find(filters) + .populate(includes) + + return profiles + } + + /** + * Store a Profile in DB + * @param {ProfileModel} profile + * @memberof ProfileRepository + */ + async store(profile){ + let storedProfile = await ProfileModel.create(profile); + return storedProfile; + + } + + /** + * Update roles in a Profile + * @param {number[]} roles + * @param {number} id + * @memberof ProfileRepository + */ + async update(roles, id){ + let foundProfile = await ProfileModel + .findById(id); + + if(!foundProfile) { + throw new ThrowableError(getMessage('profileNotFound')('id', id), 'MongoError', 404); + } + + foundProfile.roles = roles + + await foundProfile.save(); + + return foundProfile; + } + + /** + * Delete a Profile in DB + * @param {number} id + * @memberof ProfileRepository + */ + async delete(id){ + let foundProfile = await ProfileModel.findById(id); + + if(!foundProfile) { + throw new ThrowableError(getMessage('profileNotFound')('id', id), 'MongoError', 404); + } + + await ProfileModel.deleteOne({ _id: id }); + + return getMessage('profileDeleted')(id); + } +} \ No newline at end of file diff --git a/src/repository/RoleRepository.js b/src/repository/RoleRepository.js new file mode 100644 index 0000000..a4ad67f --- /dev/null +++ b/src/repository/RoleRepository.js @@ -0,0 +1,45 @@ +import { RoleModel } from "../model"; +import { ThrowableError } from "../utils"; +import { getMessage } from "../utils"; + +export default class RoleRepository { + /** + * List all roles in DB + * @param {object} [filters={}] + * @memberof RoleRepository + */ + async findAll(filters = {}){ + let roles = await RoleModel + .find(filters) + + return roles + } + + /** + * Store a Role in DB + * @param {RoleModel} role + * @memberof RoleRepository + */ + async store(role){ + let storedRole = await RoleModel.create(role); + return storedRole; + + } + + /** + * Delete a Role in DB + * @param {number} id + * @memberof RoleRepository + */ + async delete(id){ + let foundRole = await RoleModel.findById(id); + + if(!foundRole) { + throw new ThrowableError(getMessage('profileNotFound')('id', id), 'MongoError', 404); + } + + await RoleModel.deleteOne({ _id: id }); + + return getMessage('profileDeleted')(id); + } +} \ No newline at end of file diff --git a/src/repository/UserRepository.js b/src/repository/UserRepository.js index eff260f..3ff643d 100644 --- a/src/repository/UserRepository.js +++ b/src/repository/UserRepository.js @@ -17,7 +17,7 @@ export default class UserRepository { } /** - * List all users in DB filtering by e-mail and return the encoded password + * List all users in DB filtering by e-mail and return the encoded password and profiles * @param {string} email * @memberof UserRepository */ @@ -25,6 +25,14 @@ export default class UserRepository { let foundUser = await UserModel .findOne({ email: email + }) + .populate({ + path: 'profile', + select: 'roles name -_id', + populate: { + path: 'roles', + select: 'name -_id' + } }); if(!foundUser) { diff --git a/src/repository/index.js b/src/repository/index.js index f00abf7..c4a2799 100644 --- a/src/repository/index.js +++ b/src/repository/index.js @@ -1,3 +1,7 @@ import user from "./UserRepository"; +import profile from "./ProfileRepository"; +import role from "./RoleRepository"; -export const UserRepository = user; \ No newline at end of file +export const UserRepository = user; +export const ProfileRepository = profile; +export const RoleRepository = role; \ No newline at end of file diff --git a/src/routes/ProfileRoute.js b/src/routes/ProfileRoute.js new file mode 100644 index 0000000..3931700 --- /dev/null +++ b/src/routes/ProfileRoute.js @@ -0,0 +1,19 @@ +import { ProfileController } from "../controller"; +import { RetrieveFilters, CheckToken, getMessage } from "../middleware"; + +export default class ProfileRoute { + + constructor(){ + this.controller = new ProfileController() + } + + useRoute(app){ + app.get('/profile', [RetrieveFilters, CheckToken], (req,res) => this.controller.listProfiles(req,res)); + app.post('/profile', [CheckToken], (req,res) => this.controller.saveProfile(req,res)); + app.put('/profile/:id', [CheckToken], (req,res) => this.controller.updateProfileRoles(req,res)); + app.delete('/profile/:id', [CheckToken], (req,res) => this.controller.deleteProfile(req,res)); + return app + } + + +} \ No newline at end of file diff --git a/src/routes/RoleRoute.js b/src/routes/RoleRoute.js new file mode 100644 index 0000000..f55f5df --- /dev/null +++ b/src/routes/RoleRoute.js @@ -0,0 +1,16 @@ +import { RoleController } from "../controller"; +import { RetrieveFilters, CheckToken, getMessage } from "../middleware"; + +export default class RoleRoute { + + constructor(){ + this.controller = new RoleController() + } + + useRoute(app){ + app.get('/role', [RetrieveFilters, CheckToken], (req,res) => this.controller.listRoles(req,res)); + app.post('/role', [CheckToken], (req,res) => this.controller.saveRole(req,res)); + app.delete('/role/:id', [CheckToken], (req,res) => this.controller.deleteRole(req,res)); + return app + } +} \ No newline at end of file diff --git a/src/routes/UserRoute.js b/src/routes/UserRoute.js index 5c2d324..8363301 100644 --- a/src/routes/UserRoute.js +++ b/src/routes/UserRoute.js @@ -1,5 +1,5 @@ import { UserController } from "../controller"; -import { RetrieveFilters, CheckToken } from "../middleware"; +import { RetrieveFilters, CheckToken, HasRoles } from "../middleware"; export default class UserRoute { @@ -7,7 +7,7 @@ export default class UserRoute { this.controller = new UserController() } - #registerRoutes(app){ + useRoute(app){ /* * A ArrowFunction é necessária pra que os métodos não percam a instância do * Controller quando passarem os parâmetros. Caso não seja passado dessa forma @@ -15,17 +15,13 @@ export default class UserRoute { * estar disponíveis, gerando um erro de ponteiro vazio (this ficará = undefined). */ - app.post('/login', (req,res) => this.controller.login(req,res)); - app.get('/', [RetrieveFilters, CheckToken], (req,res) => this.controller.listUsers(req,res)); - app.post('/', [CheckToken], (req,res) => this.controller.saveUser(req,res)); - app.put('/:id', [CheckToken], (req,res) => this.controller.updateUser(req,res)); - app.delete('/:id', [CheckToken], (req,res) => this.controller.deleteUser(req,res)); - - return app; - } + app.post('/user/login', (req,res) => this.controller.login(req,res)); + app.get('/user/', [CheckToken, RetrieveFilters, HasRoles(["READ_USERS"])], (req,res) => this.controller.listUsers(req,res)); + app.post('/user/', [CheckToken], (req,res) => this.controller.saveUser(req,res)); + app.put('/user/:id', [CheckToken], (req,res) => this.controller.updateUser(req,res)); + app.delete('/user/:id', [CheckToken], (req,res) => this.controller.deleteUser(req,res)); - useRoute(app){ - app.use('/user', this.#registerRoutes(app)) + return app; } diff --git a/src/routes/index.js b/src/routes/index.js index 4957978..c755c3f 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -1,10 +1,14 @@ import UserRoute from "./UserRoute"; +import ProfileRoute from "./ProfileRoute"; +import RoleRoute from "./RoleRoute"; export default class Routes { constructor(app){ this.app = app; this.routes = [ UserRoute, + ProfileRoute, + RoleRoute ]; } diff --git a/src/utils/GlobalHandler.js b/src/utils/GlobalHandler.js index bed79ed..7094d3f 100644 --- a/src/utils/GlobalHandler.js +++ b/src/utils/GlobalHandler.js @@ -3,6 +3,7 @@ class GlobalHandler { this.errorTypes = { 'MNGE': 'MongoError', 'VDTE': 'ValidationError', + 'ATHE': 'AuthorizationError', 'ITRE': 'InternalError', } } @@ -43,12 +44,26 @@ class GlobalHandler { return this.makeError() } + #handleAuthError(error) { + if(error.message && error.code){ + return this.makeError(error.message, 403, 'ATHE') + }else if(error.message) { + let errorCode = (error.code) ? error.code : 403; + return this.makeError(error.message, errorCode, 'ATHE') + } + + console.error(error); + return this.makeError() + } + handle(error) { if(error){ if(error.name === 'MongoError'){ return this.#handleMongoError(error); }else if(error.name === 'ValidationError'){ // O Mongoose também poder jogar excessão ValidationError, validar isso dps return this.#handleYupError(error); + }else if(error.name === 'AuthorizationError'){ // O Mongoose também poder jogar excessão ValidationError, validar isso dps + return this.#handleAuthError(error); } } diff --git a/src/utils/IntlMessages.js b/src/utils/IntlMessages.js index ea1b29e..1ced346 100644 --- a/src/utils/IntlMessages.js +++ b/src/utils/IntlMessages.js @@ -1,6 +1,11 @@ const languages = { enUS: { - userNotFound: (id) => `User with id ${id} not found!` + userNotFound: (id) => `User with id ${id} not found!`, + profileNotFound: (field, val) => `Profile with ${field} ${val} not found!`, + profileDeleted: id => `Profile with id ${id} deleted!`, + bodyMustBeArray: arrayOf => `Body must be array ${arrayOf ? `of ${arrayOf}` : ''}`, + roleNotPresent: role => `Unauthorized! Role ${role} must be present!`, + unauthorizedAccess: `Unauthorized!`, }, }