diff --git a/.gitignore b/.gitignore index 25c8fdb..8f5e467 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules -package-lock.json \ No newline at end of file +package-lock.json +.env \ No newline at end of file diff --git a/FINAL REPORT README.md b/FINAL REPORT README.md new file mode 100644 index 0000000..79790b4 --- /dev/null +++ b/FINAL REPORT README.md @@ -0,0 +1,29 @@ +**FINAL REPORT** + +The Final Task was finished + +You can see the Postman Document for API access in +## **[API DOCUMENT](https://documenter.getpostman.com/view/10683014/TzecD5dB#91cd7156-7c27-5154-c562-ed94ff92ad2a)** + +I use the the next APIs: + +- [NASA API](https://api.nasa.gov/) +- [API del Servicio de Normalización de Datos Geográficos de Argentina](https://datosgobar.github.io/georef-ar-api/) + +Mi personal Github for this work is [here](https://github.com/ucusita/jwt-refresh-token-node-js-mongodb/). + +**I make seven (7) endpoints that must consume external service APIs .** + +**I respect the structure used for the endpoint.** I used: + +* Routes. +* Controllers. +* Services. +* Environment variables for sensitive data (like API keys or DB access). + +**In one of the three endpoints, i saved the response to a Mongodb collection called apinasas.** For this I used MongoDB Atlas. + +**The endpoints are protected with a JWT Token.** +Only the endpoints append to the initial code allow users with the role "user" to use the endpoint. +The rest of the endpoints allow the use. + diff --git a/README.md b/README.md index d00dbdc..d45c9cb 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,39 @@ +This is a fork of the original project. It's intended to set as a codebase for the final task for the students of the course 4 of Pilar Tecno. + +## **[You're now facing the Final Task](https://www.youtube.com/watch?v=Jxk9DqdYsJ4)** + + + +You have the freedom to use any free or open API for your project. You may use as many as you need/want. + +Some suggestions are: + + - [NASA API](https://api.nasa.gov/) + - [The Audio DB](https://www.theaudiodb.com/api_guide.php) +- [The cat facts](https://alexwohlbruck.github.io/cat-facts/docs/) +- [API del Servicio de Normalización de Datos Geográficos de + Argentina](https://datosgobar.github.io/georef-ar-api/) + +You can browse more API's [here](https://public-apis.io/). + +Keep in mind some API's will need some registration and/or are free for trial purposes only. + +**The final task consists in you making three or more endpoints that must consume one external service API (like the ones listed before).** + +**It is required that you pay special attention to the structure used for the endpoint.** Make sure to use: + +* Routes. + +* Controllers. + +* Services. + +* Environment variables for sensitive data (like API keys or DB access). + +**In one of the three endpoints you code, it is required that you save the response to a Mongodb collection.** You can use MongoDB Atlas for easy setup, or a local instance if you want. + +**It is required that the endpoints are protected with a JWT Token.** The endpoints should only allow users with the role "user" to use the endpoint. + # Node.js JWT Refresh Token with MongoDB example JWT Refresh Token Implementation with Node.js Express and MongoDB. You can know how to expire the JWT, then renew the Access Token with Refresh Token. diff --git a/app/config/Schema/apod.schema.js b/app/config/Schema/apod.schema.js new file mode 100644 index 0000000..5ad89ef --- /dev/null +++ b/app/config/Schema/apod.schema.js @@ -0,0 +1,15 @@ +const Schema = require('mongoose').Schema; +const mongoose = require('mongoose'); + +const apodSchema = new Schema({ + date: String, + explanation: String, + media_type: String, + service_version: String, + title: String, + url: String, + creation_date: { type: Date, default: Date.now }, + last_modified_date: { type: Date, default: Date.now } +}); + +module.exports = mongoose.model('apinasa', apodSchema); \ No newline at end of file diff --git a/app/config/auth.config.js b/app/config/auth.config.js index 14e0bce..1471b9e 100644 --- a/app/config/auth.config.js +++ b/app/config/auth.config.js @@ -1,9 +1,9 @@ module.exports = { secret: "bezkoder-secret-key", - // jwtExpiration: 3600, // 1 hour - // jwtRefreshExpiration: 86400, // 24 hours + jwtExpiration: 3600, // 1 hour + jwtRefreshExpiration: 86400, // 24 hours /* for test */ - jwtExpiration: 60, // 1 minute - jwtRefreshExpiration: 120, // 2 minutes + // jwtExpiration: 30, // 1 minute + // jwtRefreshExpiration: 120, // 2 minutes }; diff --git a/app/config/db.config.js b/app/config/db.config.js index ac4ae86..20a40ac 100644 --- a/app/config/db.config.js +++ b/app/config/db.config.js @@ -1,5 +1,16 @@ +require('dotenv').config(); + +const dbUser = process.env.DB_USER; +const dbPass = process.env.DB_PASSWORD; +const dbName = process.env.DB_NAME; +const dbUri = `mongodb+srv://${dbUser}:${dbPass}@cluster0.xj3as.mongodb.net/${dbName}?retryWrites=true&w=majority`; +const mongooseOptions = + { + useNewUrlParser: true, + useUnifiedTopology: true, + useCreateIndex: true + }; + module.exports = { - HOST: "localhost", - PORT: 27017, - DB: "bezkoder_db" -}; \ No newline at end of file + dbUri, mongooseOptions +} \ No newline at end of file diff --git a/app/controllers/auth.controller.js b/app/controllers/auth.controller.js index 2065bb3..6be7b11 100644 --- a/app/controllers/auth.controller.js +++ b/app/controllers/auth.controller.js @@ -9,7 +9,7 @@ exports.signup = (req, res) => { const user = new User({ username: req.body.username, email: req.body.email, - password: bcrypt.hashSync(req.body.password, 8), + password: bcrypt.hashSync(req.body.password, 8) }); user.save((err, user) => { @@ -63,7 +63,7 @@ exports.signup = (req, res) => { exports.signin = (req, res) => { User.findOne({ - username: req.body.username, + username: req.body.username }) .populate("roles", "-__v") .exec(async (err, user) => { diff --git a/app/controllers/gis.controller.js b/app/controllers/gis.controller.js new file mode 100644 index 0000000..dfb5884 --- /dev/null +++ b/app/controllers/gis.controller.js @@ -0,0 +1,61 @@ +const axios = require('axios').default; +const querystring = require('querystring'); +const apiKey = process.env.API_KEY; +const apodMongoService = require('../services/database/apod.mongo.service'); + +async function getCities(req, res){ + axios.get(`https://apis.datos.gob.ar/georef/api/provincias`) + .then((response) => { + res.json(response.data); + }) + .catch(err => { + res.status(500).json(err); + }); +} + +async function getCitiesFilter(req, res){ + const query = { + nombre: req.query.nombre + }; + const axiosParams = querystring.stringify({...query}); + axios.get(`https://apis.datos.gob.ar/georef/api/provincias?${axiosParams}`) + .then((response) => { + res.json(response.data); + }) + .catch(err => { + res.status(500).json(err); + }); +} + +async function getDepartmentFilter(req, res){ + const query = { + provincia: req.query.provincia, + max : 100, + campos: "completo" + }; + const axiosParams = querystring.stringify({...query}); + axios.get(`https://apis.datos.gob.ar/georef/api/departamentos?${axiosParams}`) + .then((response) => { + res.json(response.data); + }) + .catch(err => { + res.status(500).json(err); + }); +} + +async function getLocalitiesFilter(req, res){ + const query = { + provincia: req.query.provincia, + max : 100 + }; + const axiosParams = querystring.stringify({...query}); + axios.get(`https://apis.datos.gob.ar/georef/api/localidades?${axiosParams}`) + .then((response) => { + res.json(response.data); + }) + .catch(err => { + res.status(500).json(err); + }); +} + +module.exports = {getCities, getCitiesFilter, getDepartmentFilter, getLocalitiesFilter}; \ No newline at end of file diff --git a/app/controllers/nasa.controller.js b/app/controllers/nasa.controller.js new file mode 100644 index 0000000..c99d793 --- /dev/null +++ b/app/controllers/nasa.controller.js @@ -0,0 +1,60 @@ +const axios = require('axios').default; +const querystring = require('querystring'); +const apiKey = process.env.API_KEY; +const apodMongoService = require('../services/database/apod.mongo.service'); + +async function getIndex(req, res){ + res.json({message: 'This is the Nasa root route'}); +} + +async function getPictureOfTheDay(req, res){ + const query = { + start_date: req.query.start_date, + end_date: req.query.end_date + }; + const axiosParams = querystring.stringify({api_key: apiKey, ...query}); + console.log(axiosParams); + axios.get(`https://api.nasa.gov/planetary/apod?${axiosParams}`) + .then((response) => { + res.json(response.data); + }) + .catch(err => { + res.status(500).json(err); + }); +} + +async function getMarsPicture(req, res){ + const query = { + earth_date: req.query.earth_date + }; + const axiosParams = querystring.stringify({api_key: apiKey, ...query}); + axios.get(`https://api.nasa.gov/mars-photos/api/v1/rovers/curiosity/photos?${axiosParams}`) + .then((response) => { + res.json(response.data); + }) + .catch(err => { + res.status(500).json(err); + }); +} + +async function savePictureOfTheDate(req, res){ + const query = { + date: req.query.date + }; + const axiosParams = querystring.stringify({api_key: apiKey, ...query}); + console.log(axiosParams); + axios.get(`https://api.nasa.gov/planetary/apod?${axiosParams}`) + .then((response) => { + savePictureOfTheDay(response.data, res); + }) + .catch(err => { + res.status(500).json(err); + }); +} + +async function savePictureOfTheDay(req, res){ + const response = await apodMongoService.saveNasa(req); + res.json(response); +} + +module.exports = {getIndex, getPictureOfTheDay, getMarsPicture, savePictureOfTheDay, savePictureOfTheDate}; \ No newline at end of file diff --git a/app/controllers/user.controller.js b/app/controllers/user.controller.js index e2fa15b..99573a5 100644 --- a/app/controllers/user.controller.js +++ b/app/controllers/user.controller.js @@ -1,3 +1,6 @@ +const nasaController = require('../controllers/nasa.controller'); +const gisController = require('../controllers/gis.controller'); + exports.allAccess = (req, res) => { res.status(200).send("Public Content."); }; @@ -6,10 +9,40 @@ exports.userBoard = (req, res) => { res.status(200).send("User Content."); }; +/* Personal Develop Area */ +exports.apiaBoard = (req, res) => { + nasaController.getPictureOfTheDay(req, res); +}; + +exports.apibBoard = (req, res) => { + nasaController.getMarsPicture(req, res); +}; + +exports.apicBoard = (req, res) => { + nasaController.savePictureOfTheDate(req, res); +}; + +exports.apidBoard = (req, res) => { + gisController.getCities(req, res); +}; + +exports.apieBoard = (req, res) => { + gisController.getCitiesFilter(req, res); +}; + +exports.apifBoard = (req, res) => { + gisController.getDepartmentFilter(req, res); +}; + +exports.apigBoard = (req, res) => { + gisController.getLocalitiesFilter(req, res); +}; +/* End of Personal Develop Area */ + exports.adminBoard = (req, res) => { - res.status(200).send("Admin Content."); + res.status(200).send("Admin Content. You can´t access to another contents."); }; exports.moderatorBoard = (req, res) => { - res.status(200).send("Moderator Content."); + res.status(200).send("Moderator Content. You can´t access to another contents."); }; diff --git a/app/middlewares/authJwt.js b/app/middlewares/authJwt.js index 3a02e98..c518297 100644 --- a/app/middlewares/authJwt.js +++ b/app/middlewares/authJwt.js @@ -92,9 +92,41 @@ const isModerator = (req, res, next) => { }); }; +const isUser = (req, res, next) => { + User.findById(req.userId).exec((err, user) => { + if (err) { + res.status(500).send({ message: err }); + return; + } + + Role.find( + { + _id: { $in: user.roles } + }, + (err, roles) => { + if (err) { + res.status(500).send({ message: err }); + return; + } + + for (let i = 0; i < roles.length; i++) { + if (roles[i].name === "user") { + next(); + return; + } + } + + res.status(403).send({ message: "Require User Role!" }); + return; + } + ); + }); +}; + const authJwt = { verifyToken, isAdmin, - isModerator + isModerator, + isUser }; module.exports = authJwt; diff --git a/app/models/index.js b/app/models/index.js index ad610fe..607ccf0 100644 --- a/app/models/index.js +++ b/app/models/index.js @@ -7,6 +7,7 @@ db.mongoose = mongoose; db.user = require("./user.model"); db.role = require("./role.model"); + db.refreshToken = require("./refreshToken.model"); db.ROLES = ["user", "admin", "moderator"]; diff --git a/app/models/user.model.js b/app/models/user.model.js index 03af285..ed8c63d 100644 --- a/app/models/user.model.js +++ b/app/models/user.model.js @@ -8,7 +8,7 @@ const User = mongoose.model( password: String, roles: [ { - type: mongoose.Schema.Types.ObjectId, + type: mongoose.Schema.Types.ObjectId, ref: "Role" } ] diff --git a/app/routes/user.routes.js b/app/routes/user.routes.js index 0b2b2fa..7a2834d 100644 --- a/app/routes/user.routes.js +++ b/app/routes/user.routes.js @@ -10,6 +10,52 @@ module.exports = function(app) { next(); }); + /* Personal develop area for Final Work */ + app.get( + "/api/test/apia", + [authJwt.verifyToken, authJwt.isUser], + controller.apiaBoard + ); + + app.get( + "/api/test/apib", + [authJwt.verifyToken, authJwt.isUser], + controller.apibBoard + ); + + app.post( + "/api/test/apic", + [authJwt.verifyToken, authJwt.isUser], + controller.apicBoard + ); + + //------------------------------------- + + app.get( + "/api/test/apid", + [authJwt.verifyToken, authJwt.isUser], + controller.apidBoard + ); + + app.get( + "/api/test/apie", + [authJwt.verifyToken, authJwt.isUser], + controller.apieBoard + ); + + app.get( + "/api/test/apif", + [authJwt.verifyToken, authJwt.isUser], + controller.apifBoard + ); + + app.get( + "/api/test/apig", + [authJwt.verifyToken, authJwt.isUser], + controller.apigBoard + ); + /* End of Personal develop area for Final Work */ + app.get("/api/test/all", controller.allAccess); app.get("/api/test/user", [authJwt.verifyToken], controller.userBoard); diff --git a/app/services/database/apod.mongo.service.js b/app/services/database/apod.mongo.service.js new file mode 100644 index 0000000..784e6cb --- /dev/null +++ b/app/services/database/apod.mongo.service.js @@ -0,0 +1,23 @@ +const apod = require('../../config/Schema/apod.schema'); + +async function saveNasa(data){ + const apodToday = new apod({ + date: data.date, + explanation: data.explanation, + media_type: data.media_type, + service_version: data.service_version, + title: data.title, + url: data.url + }); + try{ + await apodToday.save((err, apodToday) => { + console.log('new element added to the DB', apodToday); + }); + } catch(err){ + throw err; + } + + return {status: 'Data saved correctly. ok'}; +}; + +module.exports = {saveNasa}; diff --git a/final-task.md b/final-task.md new file mode 100644 index 0000000..79bc112 --- /dev/null +++ b/final-task.md @@ -0,0 +1,33 @@ +## **[You're now facing the Final Task](https://www.youtube.com/watch?v=Jxk9DqdYsJ4)** + + + +You have the freedom to use any free or open API for your project. You may use as many as you need/want. + +Some suggestions are: + + - [NASA API](https://api.nasa.gov/) + - [The Audio DB](https://www.theaudiodb.com/api_guide.php) +- [The cat facts](https://alexwohlbruck.github.io/cat-facts/docs/) +- [API del Servicio de Normalización de Datos Geográficos de + Argentina](https://datosgobar.github.io/georef-ar-api/) + +You can browse more API's [here](https://public-apis.io/). + +Keep in mind some API's will need some registration and/or are free for trial purposes only. + +**The final task consists in you making three or more endpoints that must consume one external service API (like the ones listed before).** + +**It is required that you pay special attention to the structure used for the endpoint.** Make sure to use: + +* Routes. + +* Controllers. + +* Services. + +* Environment variables for sensitive data (like API keys or DB access). + +**In one of the three endpoints you code, it is required that you save the response to a Mongodb collection.** You can use MongoDB Atlas for easy setup, or a local instance if you want. + +**It is required that the endpoints are protected with a JWT Token.** The endpoints should only allow users with the role "user" to use the endpoint. \ No newline at end of file diff --git a/package.json b/package.json index c42b568..ffd297c 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "description": "Node.js JWT Refresh Token with MongoDB", "main": "server.js", "scripts": { + "start": "nodemon server.js", "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [ @@ -18,12 +19,19 @@ "author": "bezkoder", "license": "ISC", "dependencies": { + "axios": "^0.21.1", + "bcrypt": "^5.0.1", "bcryptjs": "^2.4.3", "body-parser": "^1.19.0", "cors": "^2.8.5", + "dotenv": "^10.0.0", "express": "^4.17.1", "jsonwebtoken": "^8.5.1", - "mongoose": "^5.12.10", + "mongoose": "^5.12.14", + "querystring": "^0.2.1", "uuid": "^8.3.2" + }, + "devDependencies": { + "nodemon": "^2.0.7" } } diff --git a/server.js b/server.js index cd3fd87..4c4bbb2 100644 --- a/server.js +++ b/server.js @@ -5,7 +5,8 @@ const dbConfig = require("./app/config/db.config"); const app = express(); let corsOptions = { - origin: "http://localhost:8081" + //origin: "http://localhost:8081" + origin: "*" }; app.use(cors(corsOptions)); @@ -18,15 +19,14 @@ app.use(express.urlencoded({ extended: true })); const db = require("./app/models"); const Role = db.role; +const User = db.user; db.mongoose - .connect(`mongodb://${dbConfig.HOST}:${dbConfig.PORT}/${dbConfig.DB}`, { - useNewUrlParser: true, - useUnifiedTopology: true - }) + .connect(dbConfig.dbUri, dbConfig.mongooseOptions) .then(() => { console.log("Successfully connect to MongoDB."); initial(); + initialUser(); }) .catch(err => { console.error("Connection error", err); @@ -35,7 +35,7 @@ db.mongoose // simple route app.get("/", (req, res) => { - res.json({ message: "Welcome to bezkoder application." }); + res.json({ message: "Welcome to pilarTecno application." }); }); // routes @@ -43,11 +43,31 @@ require("./app/routes/auth.routes")(app); require("./app/routes/user.routes")(app); // set port, listen for requests -const PORT = process.env.PORT || 8080; +const PORT = process.env.PORT || 3000; app.listen(PORT, () => { console.log(`Server is running on port ${PORT}.`); }); +function initialUser() { + User.estimatedDocumentCount((err, count) => { + if (!err && count === 0) { + new User({ + username: "Marcelo", + email: "conect2000@gmail.com", + password: "12345678", + roles: "60d2a75331deac23284d8b7a" + }).save(err => { + if (err) { + console.log("error", err); + } + + console.log("added the 'new user' to Users collection"); + }); + + } + }); +} + function initial() { Role.estimatedDocumentCount((err, count) => { if (!err && count === 0) { @@ -80,6 +100,18 @@ function initial() { console.log("added 'admin' to roles collection"); }); + + new Role({ + name: "operator" + }).save(err => { + if (err) { + console.log("error", err); + } + + console.log("added 'operator' to roles collection"); + }); } }); + + }