From 46625dd6cdf5acf6187b26284ed72db2df06eeca Mon Sep 17 00:00:00 2001 From: ryanwong Date: Sun, 6 Feb 2022 21:47:53 -0500 Subject: [PATCH] day 10 --- day10/README.md | 12 ++++ day10/app.js | 44 ++++++++++++ day10/bin/www | 90 ++++++++++++++++++++++++ day10/models/index.js | 68 +++++++++++++++++++ day10/models/location.js | 26 +++++++ day10/package.json | 20 ++++++ day10/public/stylesheets/style.css | 8 +++ day10/routes/index.js | 9 +++ day10/routes/users.js | 9 +++ day10/services/JwtService.js | 102 ++++++++++++++++++++++++++++ day10/services/ValidationService.js | 85 +++++++++++++++++++++++ day10/views/error.jade | 6 ++ day10/views/index.jade | 5 ++ day10/views/layout.jade | 7 ++ 14 files changed, 491 insertions(+) create mode 100644 day10/app.js create mode 100755 day10/bin/www create mode 100644 day10/models/index.js create mode 100644 day10/models/location.js create mode 100644 day10/package.json create mode 100644 day10/public/stylesheets/style.css create mode 100644 day10/routes/index.js create mode 100644 day10/routes/users.js create mode 100644 day10/services/JwtService.js create mode 100644 day10/services/ValidationService.js create mode 100644 day10/views/error.jade create mode 100644 day10/views/index.jade create mode 100644 day10/views/layout.jade diff --git a/day10/README.md b/day10/README.md index e69de29..ef0b244 100644 --- a/day10/README.md +++ b/day10/README.md @@ -0,0 +1,12 @@ +# day 10 + +## Instructions + +- setup project +- clone to your github +- Create a page /code where we show a QR Code. The QR code goes to the route /api/v1/code/?amount=1&service=software service. The code can be random +- When user scans the code, it will download pdf invoice with the amount specified in url. The service will also specified +- use this invoice template https://github.com/sparksuite/simple-html-invoice-template +- Use this library html-pdf-node + +- Everything must be done by end of date diff --git a/day10/app.js b/day10/app.js new file mode 100644 index 0000000..97a1728 --- /dev/null +++ b/day10/app.js @@ -0,0 +1,44 @@ +var createError = require('http-errors'); +var express = require('express'); +var path = require('path'); +var cookieParser = require('cookie-parser'); +var logger = require('morgan'); + +var indexRouter = require('./routes/index'); +var usersRouter = require('./routes/users'); + +const db = require("./models"); +var cors = require("cors"); + +var app = express(); +app.set("db", db); +// view engine setup +app.set('views', path.join(__dirname, 'views')); +app.set('view engine', 'jade'); +app.use(cors()); +app.use(logger('dev')); +app.use(express.json()); +app.use(express.urlencoded({ extended: false })); +app.use(cookieParser()); +app.use(express.static(path.join(__dirname, 'public'))); + +app.use('/', indexRouter); +app.use('/users', usersRouter); + +// catch 404 and forward to error handler +app.use(function (req, res, next) { + next(createError(404)); +}); + +// error handler +app.use(function (err, req, res, next) { + // set locals, only providing error in development + res.locals.message = err.message; + res.locals.error = req.app.get('env') === 'development' ? err : {}; + + // render the error page + res.status(err.status || 500); + res.render('error'); +}); + +module.exports = app; diff --git a/day10/bin/www b/day10/bin/www new file mode 100755 index 0000000..cd1a62f --- /dev/null +++ b/day10/bin/www @@ -0,0 +1,90 @@ +#!/usr/bin/env node + +/** + * Module dependencies. + */ + +var app = require('../app'); +var debug = require('debug')('day-1:server'); +var http = require('http'); + +/** + * Get port from environment and store in Express. + */ + +var port = normalizePort(process.env.PORT || '3000'); +app.set('port', port); + +/** + * Create HTTP server. + */ + +var server = http.createServer(app); + +/** + * Listen on provided port, on all network interfaces. + */ + +server.listen(port); +server.on('error', onError); +server.on('listening', onListening); + +/** + * Normalize a port into a number, string, or false. + */ + +function normalizePort(val) { + var port = parseInt(val, 10); + + if (isNaN(port)) { + // named pipe + return val; + } + + if (port >= 0) { + // port number + return port; + } + + return false; +} + +/** + * Event listener for HTTP server "error" event. + */ + +function onError(error) { + if (error.syscall !== 'listen') { + throw error; + } + + var bind = typeof port === 'string' + ? 'Pipe ' + port + : 'Port ' + port; + + // handle specific listen errors with friendly messages + switch (error.code) { + case 'EACCES': + console.error(bind + ' requires elevated privileges'); + process.exit(1); + break; + case 'EADDRINUSE': + console.error(bind + ' is already in use'); + process.exit(1); + break; + default: + throw error; + } +} + +/** + * Event listener for HTTP server "listening" event. + */ + +function onListening() { + var addr = server.address(); + var bind = typeof addr === 'string' + ? 'pipe ' + addr + : 'port ' + addr.port; + debug('Listening on ' + bind); +} diff --git a/day10/models/index.js b/day10/models/index.js new file mode 100644 index 0000000..39f447f --- /dev/null +++ b/day10/models/index.js @@ -0,0 +1,68 @@ +'use strict'; +/*Powered By: Manaknightdigital Inc. https://manaknightdigital.com/ Year: 2020*/ +/** + * Sequelize File + * @copyright 2020 Manaknightdigital Inc. + * @link https://manaknightdigital.com + * @license Proprietary Software licensing + * @author Ryan Wong + * + */ +const fs = require('fs'); +const path = require('path'); +let Sequelize = require('sequelize'); +const basename = path.basename(__filename); +const config = { + DB_DATABASE: 'mysql', + DB_USERNAME: 'root', + DB_PASSWORD: 'root', + DB_ADAPTER: 'mysql', + DB_NAME: 'day_1', + DB_HOSTNAME: 'localhost', + DB_PORT: 3306, +}; + +let db = {}; + +let sequelize = new Sequelize(config.DB_DATABASE, config.DB_USERNAME, config.DB_PASSWORD, { + dialect: config.DB_ADAPTER, + username: config.DB_USERNAME, + password: config.DB_PASSWORD, + database: config.DB_NAME, + host: config.DB_HOSTNAME, + port: config.DB_PORT, + logging: console.log, + timezone: '-04:00', + pool: { + maxConnections: 1, + minConnections: 0, + maxIdleTime: 100, + }, + define: { + timestamps: false, + underscoredAll: true, + underscored: true, + }, +}); + +// sequelize.sync({ force: true }); + +fs.readdirSync(__dirname) + .filter((file) => { + return file.indexOf('.') !== 0 && file !== basename && file.slice(-3) === '.js'; + }) + .forEach((file) => { + var model = sequelize['import'](path.join(__dirname, file)); + db[model.name] = model; + }); + +Object.keys(db).forEach((modelName) => { + if (db[modelName].associate) { + db[modelName].associate(db); + } +}); + +db.sequelize = sequelize; +db.Sequelize = Sequelize; + +module.exports = db; \ No newline at end of file diff --git a/day10/models/location.js b/day10/models/location.js new file mode 100644 index 0000000..7f430bb --- /dev/null +++ b/day10/models/location.js @@ -0,0 +1,26 @@ +module.exports = (sequelize, DataTypes) => { + const location = sequelize.define( + "location", + { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true, + }, + name: DataTypes.STRING, + created_at: DataTypes.DATEONLY, + updated_at: DataTypes.DATE, + }, + { + timestamps: true, + freezeTableName: true, + tableName: "location", + }, + { + underscoredAll: false, + underscored: false, + } + ); + + return location; +}; \ No newline at end of file diff --git a/day10/package.json b/day10/package.json new file mode 100644 index 0000000..b96eef4 --- /dev/null +++ b/day10/package.json @@ -0,0 +1,20 @@ +{ + "name": "day-1", + "version": "0.0.0", + "private": true, + "scripts": { + "start": "node ./bin/www" + }, + "dependencies": { + "cookie-parser": "~1.4.4", + "cors": "^2.8.5", + "debug": "~2.6.9", + "express": "~4.16.1", + "http-errors": "~1.6.3", + "jade": "~1.11.0", + "jsonwebtoken": "^8.5.1", + "morgan": "~1.9.1", + "mysql2": "^2.3.3", + "sequelize": "^6.15.1" + } +} diff --git a/day10/public/stylesheets/style.css b/day10/public/stylesheets/style.css new file mode 100644 index 0000000..9453385 --- /dev/null +++ b/day10/public/stylesheets/style.css @@ -0,0 +1,8 @@ +body { + padding: 50px; + font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; +} + +a { + color: #00B7FF; +} diff --git a/day10/routes/index.js b/day10/routes/index.js new file mode 100644 index 0000000..ecca96a --- /dev/null +++ b/day10/routes/index.js @@ -0,0 +1,9 @@ +var express = require('express'); +var router = express.Router(); + +/* GET home page. */ +router.get('/', function(req, res, next) { + res.render('index', { title: 'Express' }); +}); + +module.exports = router; diff --git a/day10/routes/users.js b/day10/routes/users.js new file mode 100644 index 0000000..623e430 --- /dev/null +++ b/day10/routes/users.js @@ -0,0 +1,9 @@ +var express = require('express'); +var router = express.Router(); + +/* GET users listing. */ +router.get('/', function(req, res, next) { + res.send('respond with a resource'); +}); + +module.exports = router; diff --git a/day10/services/JwtService.js b/day10/services/JwtService.js new file mode 100644 index 0000000..a3a712a --- /dev/null +++ b/day10/services/JwtService.js @@ -0,0 +1,102 @@ +/*Powered By: Manaknightdigital Inc. https://manaknightdigital.com/ Year: 2020*/ +/** + * JWT Service + * @copyright 2020 Manaknightdigital Inc. + * @link https://manaknightdigital.com + * @license Proprietary Software licensing + * @author Ryan Wong + * + */ +var jwt = require('jsonwebtoken'); +const config = { + DYNAMIC_CONFIG_JWT_EXPIRE_AT: 3600, + DYNAMIC_CONFIG_JWT_KEY: 'sdfsfdsfdsfsdfdsfds', + DYNAMIC_CONFIG_JWT_REFRESH_EXPIRE_AT: 7200, +}; +module.exports = { + createAccessToken: function (payload, expireIn = config.DYNAMIC_CONFIG_JWT_EXPIRE_AT) { + const secret = config.DYNAMIC_CONFIG_JWT_KEY; + + return jwt.sign(payload, secret, { + expiresIn: Number(expireIn), + algorithm: 'HS256', + }); + }, + createRefreshToken: function (payload) { + return jwt.sign(payload, config.DYNAMIC_CONFIG_JWT_KEY, { + expiresIn: Number(config.DYNAMIC_CONFIG_JWT_REFRESH_EXPIRE_AT), + algorithm: 'HS256', + }); + }, + + verifyAccessToken: function (token) { + try { + return jwt.verify(token, config.DYNAMIC_CONFIG_JWT_KEY); + } catch (err) { + return false; + } + }, + verifyRefreshToken: function (token) { + try { + return jwt.verify(token, config.DYNAMIC_CONFIG_JWT_KEY); + } catch (err) { + return false; + } + }, + generateString: function (length) { + let d = new Date().getTime(); + const time = new Date().getTime(); + const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx-xxxx'.replace(/[xy]/g, function (c) { + let r = (d + Math.random() * 16) % 16 | 0; + d = Math.floor(d / 16); + return (c == 'x' ? r : (r & 0x7) | 0x8).toString(16); + }); + + return (uuid.toUpperCase() + '-' + time.toString()).substring(0, length); + }, + generateUUID: function () { + let d = new Date().getTime(); + const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { + let r = (d + Math.random() * 16) % 16 | 0; + d = Math.floor(d / 16); + return (c == 'x' ? r : (r & 0x7) | 0x8).toString(16); + }); + return uuid.toUpperCase(); + }, + getToken: function (req) { + if (req.headers.authorization && req.headers.authorization.split(' ')[0] === 'Bearer') { + return req.headers.authorization.split(' ')[1]; + } else if (req.query && req.query.token) { + return req.query.token; + } + + return null; + }, + verifyTokenMiddleware: function (role) { + const self = this; + return function (req, res, next) { + const token = self.getToken(req); + if (!token) { + return res.status(401).json({ + success: false, + code: 'UNAUTHORIZED', + message: 'Access denied', + }); + } + const result = self.verifyAccessToken(token); + const roleId = result?.role_id; + const user = result?.user; + const credentialId = result?.credential_id; + + if (!result || !roleId || !user || !credentialId) { + return res.status(401).json({ + success: false, + code: 'TOKEN_INVALID', + message: 'Invalid token', + }); + } + req.tokenPayload = result; + next(); + }; + }, +}; diff --git a/day10/services/ValidationService.js b/day10/services/ValidationService.js new file mode 100644 index 0000000..bf60bc8 --- /dev/null +++ b/day10/services/ValidationService.js @@ -0,0 +1,85 @@ +const { Validator, addCustomMessages } = require('node-input-validator'); + +// {fieldName:message} eg:{email:"Invalid Email", password:"Password too short"} +const formatValidationError = (error) => { + const formatted = Object.entries(error) + .map(([key, value]) => ({ + field: key, + message: value.message, + })) + .reduce((accumulator, currentValue) => { + if (!accumulator[currentValue]) { + accumulator[currentValue.field] = currentValue.message; + } + return accumulator; + }, {}); + return formatted; +}; + +module.exports = { + /** + * Input Validator middleware for controller + * @param {object} validationObject object defining body fields and its validation types eg:{email:required|email} + * @param {object} _extendMessages object defining message to throw on validation error eg: {"email.required":"Email is required","email.email":"Invalid email"} + * + */ + validateInput: (validationObject = {}, _extendMessages = {}) => async ( + req, + res, + next, + ) => { + const validation = new Validator(req.body, validationObject); + addCustomMessages(_extendMessages); + + try { + const isValid = await validation.check(); + if (!isValid) { + req.validationError = formatValidationError(validation.errors); + } + return next(); + } catch (error) { + req.validationError = error.message; + return next(); + } + }, + + handleValidationErrorForViews: ( + req, + res, + viewModel, + viewPath = '/', + fieldsStoreKey, + defaultValue = {}, + ) => { + const validationError = req.validationError; + + if (validationError) { + // Remembers fields if validation error occurs + Object.entries(defaultValue).forEach(([key, value]) => { + viewModel[fieldsStoreKey][key] = value; + }); + + if (typeof validationError === 'string') { + viewModel.error = validationError; + } else { + viewModel.validationError = req.validationError; + } + return res.render(viewPath, viewModel); + } + }, + + handleValidationErrorForAPI: (req, res, next) => { + const validationError = req.validationError; + + if (validationError) { + let error; + if (typeof validationError === 'string') { + error = validationError; + } else { + error = req.validationError; + } + return res.json({ success: false, error }); + } + next(); + }, +}; diff --git a/day10/views/error.jade b/day10/views/error.jade new file mode 100644 index 0000000..51ec12c --- /dev/null +++ b/day10/views/error.jade @@ -0,0 +1,6 @@ +extends layout + +block content + h1= message + h2= error.status + pre #{error.stack} diff --git a/day10/views/index.jade b/day10/views/index.jade new file mode 100644 index 0000000..3d63b9a --- /dev/null +++ b/day10/views/index.jade @@ -0,0 +1,5 @@ +extends layout + +block content + h1= title + p Welcome to #{title} diff --git a/day10/views/layout.jade b/day10/views/layout.jade new file mode 100644 index 0000000..15af079 --- /dev/null +++ b/day10/views/layout.jade @@ -0,0 +1,7 @@ +doctype html +html + head + title= title + link(rel='stylesheet', href='/stylesheets/style.css') + body + block content