diff --git a/chapter04/package.json b/chapter04/package.json index fc6e929..f144c6b 100644 --- a/chapter04/package.json +++ b/chapter04/package.json @@ -5,7 +5,8 @@ "main": "index.js", "scripts": { "build": "flow-remove-types src/ -d out/", - "buildWithMaps": "flow-remove-types src/ -d out/ --pretty --sourcemaps", + "buildWithMaps": + "flow-remove-types src/ -d out/ --pretty --sourcemaps", "start": "npm run build && node out/doroundmath.js", "start-db": "npm run build && node out/dbaccess.js", "nodemon": "nodemon --watch src --delay 1 --exec npm start", @@ -19,11 +20,7 @@ "author": "Federico Kereki", "license": "ISC", "babel": { - "presets": [ - "env", - "node", - "flow" - ] + "presets": ["env", "node", "flow"] }, "eslintConfig": { "parserOptions": { @@ -35,37 +32,22 @@ "node": true, "es6": true }, - "extends": [ - "eslint:recommended", - "plugin:flowtype/recommended" - ], - "plugins": [ - "babel", - "flowtype" - ], + "extends": ["eslint:recommended", "plugin:flowtype/recommended"], + "plugins": ["babel", "flowtype"], "rules": { "no-console": "off", "no-var": "error", - "prefer-const": "error" + "prefer-const": "error", + "flowtype/no-types-missing-file-annotation": 0 } }, - "eslintIgnore": [ - "**/out/*.js" - ], + "eslintIgnore": ["**/out/*.js"], "flow-coverage-report": { "concurrentFiles": 1, - "excludeGlob": [ - "node_modules/**" - ], - "includeGlob": [ - "src/**/*.js" - ], + "excludeGlob": ["node_modules/**"], + "includeGlob": ["src/**/*.js"], "threshold": 90, - "type": [ - "text", - "html", - "json" - ] + "type": ["text", "html", "json"] }, "prettier": { "tabWidth": 4, diff --git a/chapter04/src/jwt_server.js b/chapter04/src/jwt_server.js index beea246..aa3eaac 100644 --- a/chapter04/src/jwt_server.js +++ b/chapter04/src/jwt_server.js @@ -4,33 +4,51 @@ const express = require("express"); const app = express(); const jwt = require("jsonwebtoken"); +const bodyParser = require("body-parser"); + +const validateUser = require("./validate_user.js"); const SECRET_JWT_KEY = "modernJSbook"; -const bodyParser = require("body-parser"); app.use(bodyParser.urlencoded({ extended: false })); app.get("/public", (req, res) => { res.send("the /public endpoint needs no token!"); }); +app.post("/gettoken", (req, res) => { + validateUser(req.body.user, req.body.password, (idErr, userid) => { + if (idErr !== null) { + res.status(401).send(idErr); + } else { + jwt.sign( + { userid }, + SECRET_JWT_KEY, + { algorithm: "HS256", expiresIn: "1h" }, + (err, token) => res.status(200).send(token) + ); + } + }); +}); + app.use((req, res, next) => { // First check for the Authorization header const authHeader = req.headers.authorization; - if (!authHeader.startsWith("Bearer ")) { + if (!authHeader || !authHeader.startsWith("Bearer ")) { return res.status(401).send("No token specified"); } // Now validate the token itself const token = authHeader.split(" ")[1]; - jwt.verify(token, SECRET_JWT_KEY, function(err, decoded) { + jwt.verify(token, SECRET_JWT_KEY, (err, decoded) => { if (err) { // Token bad formed, or expired, or other problem return res.status(403).send("Token expired or not valid"); } else { - req.decoded = decoded; - // Everything OK, keep processing the request + // Token OK; get the user id from it + req.userid = decoded.userid; + // Keep processing the request next(); } }); diff --git a/chapter04/src/restful_db.js b/chapter04/src/restful_db.js new file mode 100644 index 0000000..ccb5456 --- /dev/null +++ b/chapter04/src/restful_db.js @@ -0,0 +1,20 @@ +/* @flow */ +"use strict"; + +const mariaSQL = require("mariasql"); +const { promisify } = require("util"); + +const DB_HOST = "127.0.0.1"; +const DB_USER = "fkereki"; +const DB_PASS = "modernJS!!"; +const DB_SCHEMA = "world"; + +const getDbConnection = (host, user, password, db) => { + const dbConn = new mariaSQL({ host, user, password, db }); + dbConn.query = promisify(dbConn.query); + return dbConn; +}; + +const dbConn = getDbConnection(DB_HOST, DB_USER, DB_PASS, DB_SCHEMA); + +module.exports = dbConn; diff --git a/chapter04/src/restful_regions.js b/chapter04/src/restful_regions.js new file mode 100644 index 0000000..d1d8da7 --- /dev/null +++ b/chapter04/src/restful_regions.js @@ -0,0 +1,105 @@ +/* @flow */ +"use strict"; + +/* eslint-disable */ + +const getRegion = async ( + res: any, + dbConn: any, + country: ?string, + id: ?string +) => { + console.log("COUNTRY", country, "ID", id, typeof id); + + let sqlQuery = ""; + if (country == null) { + sqlQuery = ` + SELECT rr.*, cc.countryName + FROM regions rr + JOIN countries cc + ON cc.countryCode=rr.countryCode + ORDER BY cc.countryCode, rr.regionCode + `; + } else if (id == null) { + sqlQuery = ` + SELECT rr.*, cc.countryName + FROM regions rr + JOIN countries cc + ON cc.countryCode=rr.countryCode + WHERE rr.countryCode="${country}" + ORDER BY rr.regionCode + `; + } else { + sqlQuery = ` + SELECT rr.*, cc.countryName + FROM regions rr + JOIN countries cc + ON cc.countryCode=rr.countryCode + WHERE rr.countryCode="${country}" + AND rr.regionCode="${id}" + `; + } + + try { + const regions = await dbConn.query(sqlQuery); + res + .status(200) + .set("Content-Type", "application/json") + .send(JSON.stringify(regions)); + } catch (e) { + res.status(500).send("Server error"); + } +}; + +const deleteRegion = async ( + res: any, + dbConn: any, + country: string, + region: string +) => { + const sqlCities = ` + SELECT 1 FROM cities + WHERE countryCode="${country} + AND regionCode="${region} + LIMIT 1" + `; + + try { + const cities = await dbConn.query(sqlCities); + if (cities.length > 0) { + res.status(403).send("Cannot delete a region with cities"); + } else { + const deleteRegion = ` + DELETE FROM regions + WHERE countryCode="${country} + AND regionCode="${region} + `; + + const result = await dbConn.query(deleteRegion); + + if (result.affectedRows > 0) { + res.status(204).send("Region deleted"); + } else { + res.status(404).send("Region not found"); + } + } + } catch (e) { + res.status(500).send("Server error"); + } +}; + +const putRegion = async ( + res: any, + dbConn: any, + country: string, + id: string, + region: any +) => { + res.status(200).send("NOTHING DOING NOW..."); +}; + +const postRegion = async (res: any, dbConn: any, region: any) => { + res.status(200).send("NOTHING DOING NOW..."); +}; + +module.exports = { getRegion, putRegion, deleteRegion, postRegion }; diff --git a/chapter04/src/restful_server.js b/chapter04/src/restful_server.js new file mode 100644 index 0000000..30e137b --- /dev/null +++ b/chapter04/src/restful_server.js @@ -0,0 +1,95 @@ +/* @flow */ +"use strict"; + +const express = require("express"); +const app = express(); +const jwt = require("jsonwebtoken"); +const bodyParser = require("body-parser"); + +const validateUser = require("./validate_user.js"); + +const dbConn = require("./restful_db.js"); + +const { + getRegion, + deleteRegion, + putRegion, + postRegion +} = require("./restful_regions.js"); + +const SECRET_JWT_KEY = "modernJSbook"; + +app.use(bodyParser.urlencoded({ extended: false })); + +app.post("/gettoken", (req, res) => { + validateUser(req.body.user, req.body.password, (idErr, userid) => { + if (idErr !== null) { + res.status(401).send(idErr); + } else { + jwt.sign( + { userid }, + SECRET_JWT_KEY, + { algorithm: "HS256", expiresIn: "1h" }, + (err, token) => res.status(200).send(token) + ); + } + }); +}); +/* +app.use((req, res, next) => { + // First check for the Authorization header + const authHeader = req.headers.authorization; + if (!authHeader || !authHeader.startsWith("Bearer ")) { + return res.status(401).send("No token specified"); + } + + // Now validate the token itself + const token = authHeader.split(" ")[1]; + + jwt.verify(token, SECRET_JWT_KEY, (err, decoded) => { + if (err) { + // Token bad formed, or expired, or other problem + return res.status(403).send("Token expired or not valid"); + } else { + // Token OK; get the user id from it + req.userid = decoded.userid; + // Keep processing the request + next(); + } + }); +}); +*/ + +// START ROUTING FOR REGIONS + +app.get("/regions/", (req, res) => getRegion(res, dbConn)); + +app.get("/regions/:country/", (req, res) => + getRegion(res, dbConn, req.params.country) +); + +app.get("/regions/:country/:region/", (req, res) => + getRegion(res, dbConn, req.params.country, req.params.region) +); + +app.delete("/regions/:country/:region", (req, res) => + deleteRegion(res, dbConn, req.params.country, req.params.region) +); + +app.put("/regions/:country/:region", (req, res) => + putRegion(res, dbConn, req.params.country, req.params.region) +); + +app.post("/regions", (req, res) => postRegion(res, dbConn)); + +// END OF ROUTING FOR REGIONS + +// eslint-disable-next-line no-unused-vars +app.use((err, req, res, next) => { + console.error("Error....", err.message); + res.status(500).send("INTERNAL SERVER ERROR"); +}); + +app.listen(8080, () => + console.log("Mini JWT server ready, at http://localhost:8080/!") +); diff --git a/chapter04/src/validate_user.js b/chapter04/src/validate_user.js new file mode 100644 index 0000000..d3d7537 --- /dev/null +++ b/chapter04/src/validate_user.js @@ -0,0 +1,23 @@ +/* @flow */ +"use strict"; + +/* + In real life, validateUser could check a database, + look into an Active Directory, call another service, + etc. -- but for this demo, let's keep it quite + simple and only accept a single hardcoded user. +*/ + +const validateUser = ( + userName: string, + password: string, + callback: (?string, ?string) => void +) => { + if (userName === "fkereki" && password === "modernjsbook") { + callback(null, "fkereki"); // OK, send userName back + } else { + callback("Not valid user", null); + } +}; + +module.exports = validateUser;