Belajar Membuat Web Menggunakan Framework Express JS (Part 5)
Di part sebelumnya kita sudah bahas tentang membuat login di Framework Express JS, pada tutorial kali ini kita akan belajar untuk membuat halaman dashboard user menggunakan middleware, bagi yang belum mengikuti tutorial sebelumnya silakan ikuti terlebih dahulu di tutorial sebelumnya, bagi yang sudah silakan untuk melanjutkan di tutorial kali ini.
1. Edit halaman login
Buka file login.pug yang berada di dalam folder views/auth/login.pug, dibagian fungsi login() javascript terdapat response / callback yang belum bisa mengalihkan ke halaman dashboard, nah sekarang ubah source codenya menjadi seperti ini
extends layout
block stylesheets
//- Todo
block content
div(style="padding:50px;")
div.row
div.col-md-4.offset-md-4
form.login-form(action=`${process.env.NODE_APP_URL}/login`)
center
b
h4 Login User
hr
div.form-group
label(for="email") Email
input.form-control#email(type="email" placeholder="Email" name="email")
div.invalid-feedback#emailFeedback
div.form-group
label(for="password") Password
input.form-control#password(type="password" placeholder="Password" name="password")
div.invalid-feedback#passwordFeedback
div.form-group
button.btn.btn-primary.btn-block#login-button(type="button" onclick="login()") Login
br
center
a.text-muted(href=`${process.env.NODE_APP_URL}`) Back To Home
block scripts
script.
var _url = '#{process.env.NODE_APP_URL}';
function login() {
var loginButton = jQuery('#login-button'),
email = jQuery('#email'),
password = jQuery('#password'),
emailFeedback = jQuery('#emailFeedback'),
passwordFeedback = jQuery('#passwordFeedback');
jQuery.ajax({
url : _url + '/auth',
method : 'post',
dataType : 'json',
data : jQuery('.login-form').serialize(),
beforeSend : function() {
email.attr('class','form-control');
password.attr('class','form-control');
emailFeedback.html(``);
passwordFeedback.html(``);
loginButton.attr('disabled','disabled');
},
success : function(response) {
loginButton.removeAttr('disabled');
nativeToast({
message: response.message,
position: 'top',
});
if(!response.error) {
setTimeout(function() {
location.href = _url + '/user';
}, 1000);
return;
}
},
error : function(err) {
nativeToast({
message: 'Error the fields.',
position: 'top',
});
loginButton.removeAttr('disabled');
var jsonMessage = err.responseJSON.message;
if(jsonMessage.email != undefined) {
email.attr('class','form-control is-invalid');
emailFeedback.html(jsonMessage.email.msg);
}
if(jsonMessage.password != undefined) {
password.attr('class','form-control is-invalid');
passwordFeedback.html(jsonMessage.password.msg);
}
}
});
}
Dan sekarang kita sudah mempunyai fungsinya redirect nya, Jika berhasil login maka akan dialihkan ke dalam halaman dashboard jika user tidak ditemukan maka akan muncul pesan dengan popup, copy source code dibawah ini kemudian save file di dalam folder public/stylesheets/toastr.css dan public/javascripts/toastr.js.
toastr.css
.native-toast {
position: fixed;
background-color: rgba(50, 50, 50, .8);
border-radius: 33px;
color: white;
left: 50%;
text-align: center;
padding: 10px 20px;
opacity: 0;
z-index: 99999;
transition: transform .25s, opacity .25s, top .25s;
box-sizing: border-box;
}
.native-toast-bottom {
bottom: 50px;
-ms-transform: translateX(-50%) translateY(50px);
transform: translateX(-50%) translateY(50px)
}
.native-toast-bottom.native-toast-shown {
opacity: 1;
-ms-transform: translateX(-50%) translateY(0);
transform: translateX(-50%) translateY(0);
}
.native-toast-bottom.native-toast-edge {
bottom: 0;
}
.native-toast-top {
top: 50px;
-ms-transform: translateX(-50%) translateY(-50px);
transform: translateX(-50%) translateY(-50px)
}
.native-toast-top.native-toast-shown {
opacity: 1;
-ms-transform: translateX(-50%) translateY(0);
transform: translateX(-50%) translateY(0);
}
.native-toast-top.native-toast-edge {
top: 0;
}
.native-toast-center {
top: 0;
-ms-transform: translateX(-50%) translateY(-50px);
transform: translateX(-50%) translateY(-50px)
}
.native-toast-center.native-toast-shown {
opacity: 1;
top: 50%;
-ms-transform: translateX(-50%) translateY(-50%);
transform: translateX(-50%) translateY(-50%);
}
.native-toast-edge {
border-radius: 0;
width: 100%;
text-align: left;
}
@media screen and (min-width: 40rem) {
.native-toast:not(.native-toast-edge) {
max-width: 18rem;
}
}
/*
max-width does not seem to work in small screen?
*/
/*@media screen and (max-width: 768px) {
.native-toast:not(.native-toast-edge) {
max-width: 400px;
}
}
@media screen and (max-width: 468px) {
.native-toast:not(.native-toast-edge) {
max-width: 300px;
}
}*/
/* types */
.native-toast-error {
background-color: #d92727;
color: white;
}
.native-toast-success {
background-color: #62a465;
color: white;
}
.native-toast-warning {
background-color: #fdaf17;
color: white;
}
.native-toast-info {
background-color: #5060ba;
color: white;
}
[class^="native-toast-icon-"] {
vertical-align: middle;
margin-right: 8px
}
[class^="native-toast-icon-"] svg {
width: 16px;
height: 16px;
}
toastr.js
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global.nativeToast = factory());
}(this, (function () { 'use strict';
/*
object-assign
(c) Sindre Sorhus
@license MIT
*/
/* eslint-disable no-unused-vars */
var getOwnPropertySymbols = Object.getOwnPropertySymbols;
var hasOwnProperty = Object.prototype.hasOwnProperty;
var propIsEnumerable = Object.prototype.propertyIsEnumerable;
function toObject(val) {
if (val === null || val === undefined) {
throw new TypeError('Object.assign cannot be called with null or undefined');
}
return Object(val);
}
function shouldUseNative() {
try {
if (!Object.assign) {
return false;
}
// Detect buggy property enumeration order in older V8 versions.
// https://bugs.chromium.org/p/v8/issues/detail?id=4118
var test1 = new String('abc'); // eslint-disable-line no-new-wrappers
test1[5] = 'de';
if (Object.getOwnPropertyNames(test1)[0] === '5') {
return false;
}
// https://bugs.chromium.org/p/v8/issues/detail?id=3056
var test2 = {};
for (var i = 0; i < 10; i++) {
test2['_' + String.fromCharCode(i)] = i;
}
var order2 = Object.getOwnPropertyNames(test2).map(function (n) {
return test2[n];
});
if (order2.join('') !== '0123456789') {
return false;
}
// https://bugs.chromium.org/p/v8/issues/detail?id=3056
var test3 = {};
'abcdefghijklmnopqrst'.split('').forEach(function (letter) {
test3[letter] = letter;
});
if (Object.keys(Object.assign({}, test3)).join('') !==
'abcdefghijklmnopqrst') {
return false;
}
return true;
} catch (err) {
// We don't expect any of the above to throw, but better to be safe.
return false;
}
}
var index = shouldUseNative() ? Object.assign : function (target, source) {
var arguments$1 = arguments;
var from;
var to = toObject(target);
var symbols;
for (var s = 1; s < arguments.length; s++) {
from = Object(arguments$1[s]);
for (var key in from) {
if (hasOwnProperty.call(from, key)) {
to[key] = from[key];
}
}
if (getOwnPropertySymbols) {
symbols = getOwnPropertySymbols(from);
for (var i = 0; i < symbols.length; i++) {
if (propIsEnumerable.call(from, symbols[i])) {
to[symbols[i]] = from[symbols[i]];
}
}
}
}
return to;
};
var prevToast = null;
var icons = {
warning: "<svg viewBox=\"0 0 32 32\" width=\"32\" height=\"32\" fill=\"none\" stroke=\"currentcolor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"6.25%\"><path d=\"M8 17 C8 12 9 6 16 6 23 6 24 12 24 17 24 22 27 25 27 25 L5 25 C5 25 8 22 8 17 Z M20 25 C20 25 20 29 16 29 12 29 12 25 12 25 M16 3 L16 6\" /></svg>",
success: "<svg viewBox=\"0 0 32 32\" width=\"32\" height=\"32\" fill=\"none\" stroke=\"currentcolor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"6.25%\"><path d=\"M2 20 L12 28 30 4\" /></svg>",
info: "<svg viewBox=\"0 0 32 32\" width=\"32\" height=\"32\" fill=\"none\" stroke=\"currentcolor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"6.25%\"><path d=\"M16 14 L16 23 M16 8 L16 10\" /><circle cx=\"16\" cy=\"16\" r=\"14\" /></svg>",
error: "<svg viewBox=\"0 0 32 32\" width=\"32\" height=\"32\" fill=\"none\" stroke=\"currentcolor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"6.25%\"><path d=\"M16 3 L30 29 2 29 Z M16 11 L16 19 M16 23 L16 25\" /></svg>"
};
var Toast = function Toast(ref) {
if ( ref === void 0 ) ref = {};
var message = ref.message; if ( message === void 0 ) message = '';
var position = ref.position; if ( position === void 0 ) position = 'bottom';
var timeout = ref.timeout; if ( timeout === void 0 ) timeout = 3000;
var el = ref.el; if ( el === void 0 ) el = document.body;
var square = ref.square; if ( square === void 0 ) square = false;
var type = ref.type; if ( type === void 0 ) type = '';
var debug = ref.debug; if ( debug === void 0 ) debug = false;
var edge = ref.edge; if ( edge === void 0 ) edge = false;
if (prevToast) {
prevToast.destroy();
}
this.message = message;
this.position = position;
this.el = el;
this.timeout = timeout;
this.toast = document.createElement('div');
this.toast.className = "native-toast native-toast-" + (this.position);
if (type) {
this.toast.className += " native-toast-" + type;
this.message = "<span class=\"native-toast-icon-" + type + "\">" + (icons[type] || '') + "</span>" + (this.message);
}
this.toast.innerHTML = this.message;
if (edge) {
this.toast.className += ' native-toast-edge';
}
if (square) {
this.toast.style.borderRadius = '3px';
}
this.el.appendChild(this.toast);
prevToast = this;
this.show();
if (!debug) {
this.hide();
}
};
Toast.prototype.show = function show () {
var this$1 = this;
setTimeout(function () {
this$1.toast.classList.add('native-toast-shown');
}, 300);
};
Toast.prototype.hide = function hide () {
var this$1 = this;
setTimeout(function () {
this$1.destroy();
}, this.timeout);
};
Toast.prototype.destroy = function destroy () {
var this$1 = this;
if (!this.toast) { return }
this.toast.classList.remove('native-toast-shown');
setTimeout(function () {
if (this$1.toast) {
this$1.el.removeChild(this$1.toast);
this$1.toast = null;
}
}, 300);
};
function toast(options) {
return new Toast(options)
}
var loop = function () {
var type = list[i];
toast[type] = function (options) { return toast(index({}, {type: type}, options)); };
};
for (var i = 0, list = ['success', 'info', 'warning', 'error']; i < list.length; i += 1) loop();
return toast;
})));
Lalu tambahkan block css di dalam file layout login views/auth/layout.pug, dan panggil file kedua yang tadi kita buat, sehingga akan menjadi seperti ini
doctype html
html
head
title #{title}
meta(name='viewport', content='width=device-width, initial-scale=1, shrink-to-fit=no')
link(rel='stylesheet', href='/bootstrap/dist/css/bootstrap.min.css')
link(rel='stylesheet', href='/stylesheets/toastr.css')
style.
body {
background-color:#eee;
}
block stylesheets
body
div.container-fluid
block content
script(src="/jquery/dist/jquery.min.js", type='text/javascript')
script(src="/bootstrap/dist/js/bootstrap.min.js", type='text/javascript')
script(src="/javascripts/toastr.js", type='text/javascript')
block scripts
2. Membuat authenticated middleware
Apa itu middleware?
Middleware yaitu berfungsi sebagai penengah antara request dengan controller, maka ketika request diminta middleware otomatis akan mengecek request yang masuk terlebih dahulu kemudian jika valid maka akan dilanjutkan ke controller, jika request tidak valid maka request tidak akan dilanjutkan ke controller.
Buat folder dengan nama middleware, kemudian copy source code berikut lalu simpan dengan nama authenticatedMiddleware.js
const routes = require('../routes/routes'),
routePrefix = routes()
module.exports = function(authenticatedRoutes) {
return function (req, res, next) {
if(req.originalUrl == routePrefix.auth.login) {
if(req.session.userdata) {
return res.redirect(routePrefix.dashboards.users.index);
}
}
if(authenticatedRoutes.indexOf(req.originalUrl) >= 0) {
if(req.session.userdata == undefined || req.session.userdata == '') {
if(req.xhr == true) {
return res.status(403).json({
error : 1,
message : '403 Forbidden.'
});
}
return res.redirect(routePrefix.auth.login);
}
}
next();
}
}
Kemudian buat controller baru dengan nama userController.js seperti ini
exports.index = function(req, res, next) {
res.render('users/index', {
title : process.env.NODE_APP_NAME + ' Users',
sess : JSON.parse(req.session.userdata)
});
}
Lalu sekarang kita satukan semua route ke dalam 1 file route dan membuat list route nya di dalam folder routes, berikut source code nya
routes.js
module.exports = function() {
return {
index : '/',
auth : {
login : '/auth'
},
dashboards : {
users : {
index : '/user',
}
}
};
}
web.js
'use-strict'
const express = require('express'),
router = express.Router(),
authController = require('../controllers/authController'),
userController = require('../controllers/userController'),
{ check } = require('express-validator'),
routes = require('../routes/routes'),
routePrefix = routes()
router
/*
* Index Route
*/
.get(routePrefix.index, function(req, res, next) {
res.render('index', { title : process.env.NODE_APP_NAME })
})
/*
* Auth Routes
*/
.get(routePrefix.auth.login, authController.index)
.post(routePrefix.auth.login, [
check('email','Email is required.').not().isEmpty(),
check('email','Invalid email format.').isEmail(),
check('password', 'Password is required.').not().isEmpty(),
], authController.login)
/*
* User Dashboard Routes
*/
.get(routePrefix.dashboards.users.index, userController.index)
module.exports = router;
Kenapa kita satukan semua route di dalam file web.js karena jika ada beberapa route berbeda kita ribet juga harus membuat file route nya lagi satu persatu, maka untuk file route yang lama kita hapus saja dan sekarang di dalam folder routes hanya ada dua file yaitu web.js dan routes.js
Jika sudah, maka di dalam file app.js pun ikut berubah sehingga akan seperti ini
const createError = require('http-errors'),
express = require('express'),
path = require('path'),
cookieParser = require('cookie-parser'),
logger = require('morgan'),
expressSession = require('express-session'),
app = express(),
authenticatedMiddleware = require('./middleware/authenticatedMiddleware'),
routes = require('./routes/routes'),
routePrefix = routes(),
webRouter = require('./routes/web')
require('dotenv').config()
// view engine setup
app.set('views', path.join(__dirname, 'views'))
app.set('view engine', 'pug')
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(express.static(path.join(__dirname, 'bower_components')))
app.use(expressSession({
secret : process.env.NODE_APP_KEY,
saveUninitialized: false,
resave : false
}))
app.use(function (req, res, next) {
res.locals.route = routePrefix
next()
})
/*
* Authenticated Routes
*/
const authenticatedRoutes = [
routePrefix.dashboards.users.index
];
app.use(authenticatedMiddleware(authenticatedRoutes))
app.use('/', webRouter)
// 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;
Di dalam file app.js kita sudah memanggil authenticated middlewarenya, lihat source code berikut
/*
* Authenticated Routes
*/
const authenticatedRoutes = [
routePrefix.dashboards.users.index
];
app.use(authenticatedMiddleware(authenticatedRoutes))
3. Membuat halaman user dashboard
Pada tahap kedua kita sudah membuat middleware dan controllernya, di tahap ke tiga kita buat halaman dashboard user nya.
Buat folder baru bernama users di dalam folder views, kemudian buat file index.pug dan layout.pug simpan di dalam folder users tersebut.
Copy source code berikut
index.pug
extends layout
block stylesheets
//- Todo
block content
div.row
div.col-md-3
.card.x-pointer.menu(onclick="location.href = '/user'")
.card-body
center
i.fa.fa-user-circle-o.fa-5(style="font-size:5em;")
p
h3
b USERS
block scripts
script.
layout.pug
doctype html
html
head
title #{title}
meta(name='viewport', content='width=device-width, initial-scale=1, shrink-to-fit=no')
link(rel='stylesheet', href='/bootstrap/dist/css/bootstrap.min.css')
link(rel='stylesheet', href='/stylesheets/toastr.css')
link(href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css", rel="stylesheet", integrity="sha384-wvfXpqpZZVQGK6TAh5PVlGOfQNHSoD2xbE+QkPxCAFlNEevoEH3Sl0sibVcOQVnN", crossorigin="anonymous")
style.
body {
background-color:#eee;
}
.x-pointer {
cursor:pointer;
}
.menu {
background-color: #fff;
-webkit-transition: .2s all;
-webkit-transition-delay: .2s;
-moz-transition: .2s all;
-moz-transition-delay: .2s;
-ms-transition: .2s all;
-ms-transition-delay: .2s;
-o-transition: .2s all;
-o-transition-delay: .2s;
transition: .2s all;
transition-delay: .2s;
color : #2d3436;
}
.menu:hover {
border-bottom: 2px solid #0984e3;
color : #0984e3;
-webkit-transition-delay: 0s;
-moz-transition-delay: 0s;
-ms-transition-delay: 0s;
-o-transition-delay: 0s;
transition-delay: 0s;
}
block stylesheets
body
include includes/navbar
div(style="padding-bottom:50px;")
div.container-fluid
block content
script(src="/jquery/dist/jquery.min.js", type='text/javascript')
script(src="/bootstrap/dist/js/bootstrap.min.js", type='text/javascript')
script(src="/javascripts/toastr.js", type='text/javascript')
block scripts
Jika sudah kita kita buat folder baru dengan nama includes didalam folder views/users dan buat file baru dengan nama navbar.pug simpan di dalam folder tersebut, berikut source code nya
header.navbar.navbar-expand-lg.navbar-dark.bg-primary
a.navbar-brand(href=`${process.env.NODE_APP_URL}`) #{ process.env.NODE_APP_NAME }
button.navbar-toggler(type='button', data-toggle='collapse', data-target='#navbarSupportedContent', aria-controls='navbarSupportedContent', aria-expanded='false', aria-label='Toggle navigation')
span.navbar-toggler-icon
#navbarSupportedContent.collapse.navbar-collapse
ul.navbar-nav.mr-auto
li.nav-item.active
a.nav-link(href=`${ process.env.NODE_APP_URL + route.dashboards.users.index }`)
| Home
span.sr-only (current)
ul.nav.navbar-nav.navbar-right
li.nav-item.active
a.nav-link(href=`${ process.env.NODE_APP_URL + route.auth.logout }`) Logout
4. Membuat fungsi logout
Tambahkan list route logout di dalam object auth dengan nama objectnya logout, tepatnya ada di dalam file routes.js tadi, maka akan seperti ini jadinya.
module.exports = function() {
return {
index : '/',
auth : {
login : '/auth',
logout : '/auth/logout'
},
dashboards : {
users : {
index : '/user',
}
}
};
}
Kemudian kita tambahkan 1 method di dalam file authController.js untuk menambahkan fungsi logoutnya kurang lebih seperti ini
exports.logout = function(req, res, next) {
if(req.session.userdata) {
req.session.userdata = null
}
if(req.xhr == true) {
return res.json({
error : 0,
message : 'Logout successfull.'
});
}
return res.redirect(routePrefix.auth.login);
}
Jika sudah kita buat route logoutnya di dalam file web.js, tambahkan source code berikut
.get(routePrefix.auth.logout, authController.logout)
Maka akan seperti ini hasilnya
'use-strict'
const express = require('express'),
router = express.Router(),
authController = require('../controllers/authController'),
userController = require('../controllers/userController'),
{ check } = require('express-validator'),
routes = require('../routes/routes'),
routePrefix = routes()
router
/*
* Index Route
*/
.get(routePrefix.index, function(req, res, next) {
res.render('index', { title : process.env.NODE_APP_NAME })
})
/*
* Auth Routes
*/
.get(routePrefix.auth.login, authController.index)
.post(routePrefix.auth.login, [
check('email','Email is required.').not().isEmpty(),
check('email','Invalid email format.').isEmail(),
check('password', 'Password is required.').not().isEmpty(),
], authController.login)
.get(routePrefix.auth.logout, authController.logout)
/*
* User Dashboard Routes
*/
.get(routePrefix.dashboards.users.index, userController.index)
module.exports = router;
Finally, beres juga membuat halaman dashboard user dengan middleware menggunakan Framework Express JS
Dashboard User |