Sebelum lanjut, pastikan Anda sudah menginstall :

Langkah 1 : Membuat Express Project
Cara cepat untuk menginstall express-generator adalah melalui terminal dengan mengetikkan :

npm install -g express-generator

Untuk memeriksa apakah express-generator sudah terinstall, jalankan

express -V

Untuk membuat project, pada terminal ketikkan :

$ express tvshow-tracker

dimana tvshow-tracker adalah nama aplikasi yang akan kita bangun.

express-tvshowtracker

Arahkan ke direktori tvshow-tracker dengan mengetikkan $ cd tvshow-tracker.
Hapus folder views, routes dan bin karena kita tidak membutuhkannya dan rename app.js menjadi server.js karena kita nantinya punya app.js yang lain untuk bootstraping pada Angular.
deletefolder
Replace isi server.js dengan code berikut :


var express = require('express');
var path = require('path');
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');

var app = express();

app.set('port', process.env.PORT || 3000);
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded());
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.listen(app.get('port'), function() {
    console.log('Express server listening on port ' + app.get('port'));
});

Langkah 2 : Bootstraping pada Angular
Download dan ekstrak Bootstap Sass.
bootstrapSass
Copy semua glyphicons dari assets/fonts/bootstrap ke direktori public/fonts dan semua yang ada pada direktori assets/stylesheets, pidahkan ke direktori public/stylesheets/bootstrap.
listBoostrap
Download pavicon dan simpan pada direktori public walaupun Anda tidak terlalu membutuhkannya tapi ini cukup mempercantik tampilan web nya.
Anda juga harus men-download beberapa scripts dependency dan simpan pada direktori /public/vendor seperti :

donwload-angular
Buat file index.html dan simpan pada direktori public dengan code sebagai berikut :
indexhtml
Pada line 2, ng-app merupakan root element aplikasi Angular. Pada line 4, tag untuk mengaktifkan HTML5 History API pada AngularJS. Anda bisa menghapus URL tanpa simbol #. Pada line 14, ng-view merupakan directive untuk melakukan render template.
Buat file app.js dan panggil index.html setelah scipts vendor.
appjs
Masukkan code berikut pada file app.js

angular.module('MyApp', ['ngCookies', 'ngResource', 'ngMessages', 'ngRoute', 'mgcrea.ngStrap'])
    .config(function() {

    });

Tambahkan AngualrStrap Navbar setelah tag <body>:
navbar
Hanya ada satu alasan kenapa menggunakan AngularStrap Navbar, misalnya ketika kita mengubah tag <li> secara otomatis penutupnya </li> juga akan berubah dan lebih menghemat waktu. Beberapa keuntungan lainnya adalah directives yang terintegrasi dengan AngualrJS seperti Alert, Typeahead, Tooltip, Tab dan lain-lain.
Anda bisa mencoba menjalankan app untuk memastikan bahwa tidak ada error tetapi Anda tidak akan melihat tampilan Navbar karena belum menambahkan Bootstrap stylesheets. Kita akan menggunakan gulp untuk meng-compile Sass stylesheets.
Lanjutkan untuk meng-install gulp and gulp plugins:

//Langkah 1, install gulp secara global
sudo npm install -g gulp

//Langkah 2, install gulp dalam project Anda
npm install --save-dev gulp gulp-sass gulp-plumber

Anda bisa menginstall modul-modul NPM (dengan -g) agari bisa diakses secara global dari command line manapun dan --save-dev untuk ditambahkan ke dalam packages sebagai dependency pada package.json.
bowerjson
Buat gulpfile.js dan simpan pada folder Project :

var gulp = require('gulp');
var sass = require('gulp-sass');
var plumber = require('gulp-plumber');

gulp.task('sass', function() {
  gulp.src('public/stylesheets/style.scss')
    .pipe(plumber())
    .pipe(sass())
    .pipe(gulp.dest('public/stylesheets'));
});

gulp.task('watch', function() {
  gulp.watch('public/stylesheets/*.scss', ['sass']);
});
gulp.task('default', ['sass', 'watch']);

compilegulp
Ketika Anda menjalankan perintah gulp pada terminal, maka akan dilakukan compiling atau re-compiling secara otomatis terhadap Sass stylesheets dan bisa dilihat langsung perubahannya. Apa itu gulp-plumber? Gulp-plumber akan menghindari dari pipe breaking yang disebabkan error gulp plugins. Dengan kata lain ketika Anda menuliskan code yang salah dalam Sass stylesheet, makan gulp watcher tidak akan crash dan Anda bisa melihat error apa yang terjadi saat compiling:

tvshow-tracker-9
Buat file style.scss dan simpan pada direktori public/stylesheets :

@import url(http://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,600italic,700italic,400,300,600,700);

$icon-font-path: '../fonts/';
$body-bg: #e4e7ec;

$font-family-base: 'Open Sans', sans-serif;
$headings-color: #111;
$headings-font-family: Avenir, sans-serif;
$headings-font-weight: bold;

$brand-success: #22ae5f;
$brand-primary: #1d7cf4;
$brand-danger: #b30015;
$brand-warning: #ffd66a;

$text-muted: #90939a;
$link-color: #000;

$navbar-default-link-active-bg: #f7f7f7;
$navbar-default-link-color: #848484;
$navbar-default-bg: #fff;
$navbar-default-border: #e3e9ec;

$navbar-default-brand-color: #333;
$navbar-default-brand-hover-color: #ffe939;
$navbar-default-brand-hover-bg: #333;

$btn-success-bg: $brand-success;
$btn-success-border: darken($btn-success-bg, 3%);
$btn-primary-bg: $brand-primary;
$btn-primary-border: darken($btn-primary-bg, 3%);

$jumbotron-padding: 16px;
$jumbotron-bg: #f4f6f8;

$alert-border-radius: 0;
$input-border-radius: 0;

$alert-success-text: #fff;
$alert-success-bg: #60c060;
$alert-success-border: darken($alert-success-bg, 3%);

$alert-danger-text: #fff;
$alert-danger-bg: $brand-danger;
$alert-danger-border: darken($alert-danger-bg, 3%);

$alert-info-bg: #e5f7fd;
$alert-info-border: #bcf8f3;
$alert-info-text: #25484e;

@import 'bootstrap/../bootstrap';

body {
  padding-bottom: 20px;
}

em {
  font-style: normal;
  text-decoration: underline;
}

.alphabet {
  cursor: pointer;
  font-size: 22px;
  text-align: center;

  li {
    display: inline-block;
    padding-left: 5px;
    padding-right: 5px;

    &:hover {
      color: $brand-primary;
    }
  }
}

.genres {
  cursor: pointer;

  li {
    margin-right: 5px;
    @extend .label;
    @extend .label-default;

    &:active {
      box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.250);
    }
  }
}

.jumbotron {
  margin-top: -20px;
  border-bottom: 1px solid #dae2e4;
}

.media-object {
  max-width: 200px;
  margin-bottom: 10px;
}

.episode {
  border-left: 5px solid #111;
  padding-left: 10px;
}

.alert {
  box-shadow: 0 0px 5px rgba(0, 0, 0, 0.3);
}

.alert.top-right {
  position: fixed;
  top: 50px;
  right: 0;
  margin: 20px;
  z-index: 1050;
  outline: none;

  .close {
    padding-left: 10px
  }
}

.btn {
  border-radius: 2px;
}

.center-form {
  width: 330px;
  margin: 10% auto;

  input {
    border-radius: 0;
  }
}

.search {
  color: #4f4f4f;
  font-weight: 300;
  font-size: 1.5em;
  padding: 7px;
  margin-top: -10px;
  border: 0;
  background-color: transparent;
  outline: none;
  -webkit-appearance: none;

  &:focus {
    -webkit-transition: all .4s ease;
    transition: all .4s ease;
  }
}

.panel {
  border-color: #cfd9D7;
  border-radius: 2px;
  box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.1);
  -webkit-box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.1);
}

.panel-default > .panel-heading {
  color: #444;
  border-color: #cfd9db;
  font-weight: bold;
  font-size: 85%;
  text-transform: uppercase;
  background-color: #f6f6f6;
}

.label {
  display: inline-block;
  margin-bottom: 5px;
  padding: 4px 8px;
  border: 0;
  border-radius: 3px;
  font-size: 12px;
  transition: 0.1s all;
  -webkit-font-smoothing: antialiased;
}

.label-default {
  background-color: #e4e7ec;
  text-shadow: 0 1px 0 rgba(255, 255, 255, 0.7);
  color: #90939a;

  &:hover {
    background-color: #90939a;
    color: #f4f6f8;
    text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.2);
  }
}

.navbar {
  box-shadow: 0 3px 2px -3px rgba(0, 0, 0, 0.1);
}

.navbar-header {
  float: left;
  padding-left: 15px;

}

.navbar-brand {
  background-color: #ffe939;
  transition: 0.25s all;
  margin-left: -15px;
}

.navbar-nav {
  float: left;
  margin: 0;

  > li {
    float: left;

    > a {
      padding: 15px;
    }
  }
}

Jalan perintah gulp pada directori project dan refresh browser.
Catatan: Saya menjalankan node server.js pada tab lain (Ctrl + Shift + T), mongo di tab lain, gulp pada tab ketiga tab terakhir digunakan untuk git seperti git add atau git commit.
blankView

Langkah 3: AngularJS Routes dan Templates
Kembali ke app.js dan tambahkan code berikut untuk mengaktifkan HTML5 pushState:
$locationProvider.html5Mode(true);
Apa itu $locationProvider dan datang darimana? Sebuah built-in AngularJS service untuk konfigurasi linking path aplikasi. Dengan service ini, Anda bisa mengaktifkan HTML5 pushState atau mengubah prefix URL dari # seperti ini #!, sesuai kebutuhan dalam aplikasi AngularJS.

angular.module(['$locationProvide'], 'MyApp', ['ngCookies', 'ngResource', 'ngMessages', 'ngRoute', 'mgcrea.ngStrap'])
  .config(function($locationProvider) {
    $locationProvider.html5Mode(true);
  });

Selanjutnya, kita butuh routes untuk setiap pages/halaman:

  • Home – Menampilkan semua list film.
  • Detail – Informasi tentang satu film yang dipilih.
  • Login – Form login dari user.
  • Signup – Form signup dari user.
  • Add – Menambahkan film baru.
$routeProvider
            .when('/', {
                templateUrl: 'views/home.html',
                controller: 'MainCtrl'
            })
            .when('/shows/:id', {
                templateUrl: 'views/detail.html',
                controller: 'DetailCtrl'
            })
            .when('/login', {
                templateUrl: 'views/login.html',
                controller: 'LoginCtrl'
            })
            .when('/signup', {
                templateUrl: 'views/signup.html',
                controller: 'SignupCtrl'
            })
            .when('/add', {
                templateUrl: 'views/add.html',
                controller: 'AddCtrl'
            })
            .otherwise({
                redirectTo: '/'
            });

Untuk setiap route memiliki satu template dan satu controller. Jika Anda reload page sekarang dan buka console Browser’s Developer Tools, Anda akan melihat 404 (Not Found) karena kita belum membuat template nya.
Buat sebuah file home.html pada direktori public/views.
Berikut file home.html dari line 1-15:

index-line1-32

Line 16-38 :
line-33-38
ng-repeat  digunakan untuk pengulangan dari suatu array dari items yang terletak pada controlller  untuk halaman ini.
Selanjutnya membuat file main.js dalam direktori public/controllers dan kemudian panggil dari file index.html :
controlleradd

angular.module('MyApp')
    .controller('MainCtrl', ['$scope', 'Show', function($scope, Show) {

        $scope.alphabet = ['0-9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
            'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
            'Y', 'Z'];

        $scope.genres = ['Action', 'Adventure', 'Animation', 'Children', 'Comedy',
            'Crime', 'Documentary', 'Drama', 'Family', 'Fantasy', 'Food',
            'Home and Garden', 'Horror', 'Mini-Series', 'Mystery', 'News', 'Reality',
            'Romance', 'Sci-Fi', 'Sport', 'Suspense', 'Talk Show', 'Thriller',
            'Travel'];

        $scope.headingTitle = 'Top 12 Shows';

        $scope.shows = Show.query();

        $scope.filterByGenre = function(genre) {
            $scope.shows = Show.query({ genre: genre });
            $scope.headingTitle = genre;
        };

        $scope.filterByAlphabet = function(char) {
            $scope.shows = Show.query({ alphabet: char });
            $scope.headingTitle = char;
        };
    }]);

Arrays alphabet dan genre dipanggil menggunakan ng-repeat directive. Show service di inject secara otomatis oleh AngularJS. Ketika kita reload page maka muncul error: Unknown provider: ShowProvider Lanjutkan dengan membuat file show.js pada direktori public/services kemudian panggil melalui index.html :
showjs

angular.module('MyApp')
    .factory('Show', ['$resource', function($resource) {
        return $resource('/api/shows/:_id');
    }]);

Refresh page, Anda akan melihat api/shows 404 (Not Found) error.
arrayapp
Kembali ke Express app untuk implementasi skema dan API routes.
Langkah 4 : Skema Database
Untuk menginstall mongoose dan bcrypjs, jalankan perintah berikut pada direktri project :

npm install mongoose bcryptjs --save

Kemudian tambahkan pada dua baris pertama dari server.js:

var mongoose = require('mongoose');
var bcrypt = require('bcryptjs');

Pada baris dibawahnya tambahkan skema Show :

var showSchema = new mongoose.Schema({
    _id: Number,
    name: String,
    airsDayOfWeek: String,
    airsTime: String,
    firstAired: Date,
    genre: [String],
    network: String,
    overview: String,
    rating: Number,
    ratingCount: Number,
    status: String,
    poster: String,
    subscribers: [{
        type: mongoose.Schema.Types.ObjectId, ref: 'User'
    }],
    episodes: [{
        season: Number,
        episodeNumber: Number,
        episodeName: String,
        firstAired: Date,
        overview: String
    }]
});

Schema merepresentasikan data Anda di dalam MongoDB. Semua field diatas mempunyai relasi 1-to-1 dengan data response dari TheTVDB.com API. Dua hal yang harus dicatat:

  • Field _id merupakan overwrite dari ID The TVDB.
  • Field subscribers merupakan array dari Object User.

Selanjutnya buat skema User :

var userSchema = new mongoose.Schema({
  email: { type: String, unique: true },
  password: String
});

userSchema.pre('save', function(next) {
  var user = this;
  if (!user.isModified('password')) return next();
  bcrypt.genSalt(10, function(err, salt) {
    if (err) return next(err);
    bcrypt.hash(user.password, salt, function(err, hash) {
      if (err) return next(err);
      user.password = hash;
      next();
    });
  });
});

userSchema.methods.comparePassword = function(candidatePassword, cb) {
  bcrypt.compare(candidatePassword, this.password, function(err, isMatch) {
    if (err) return cb(err);
    cb(null, isMatch);
  });
};

Sekarang kita sudah mempunyai skema, kita harus membuat mongoose models yang digunakan untuk query ke MongoDB. Dimana, schema hanya sebuah representasi abstract dari suatu data dan model adalah object yang konkrit yang memiliki method untuk query, remove, update dan save data dari/ke MongoDB.

var User = mongoose.model('User', userSchema);
var Show = mongoose.model('Show', showSchema);

Dan terakhir, connect ke database :

mongoose.connect('localhost');

Aktifkan mongo – MongoDB server, kemudian restart server.js untuk memastikan bahwa aplikasi masih bekerja.

Langkah 5 : Express API Routes
Kita akan membuat dua buah route. Pertama untuk query all shows (semua data) dan kedua query untuk single show (satu data) yang diwakili oleh ID.
Jika kita ingin implementasi semua REST routes untuk /api/shows berikut table routingnya :

Route POST GET PUT DELETE
/api/shows Add a new show Get all shows Update all shows Remove all shows
/api/shows/:id N/A Get a show Update a show Delete a show

Tambahkan route berikut setelah Express middleware :

app.get('/api/shows', function(req, res, next) {
  var query = Show.find();
  if (req.query.genre) {
    query.where({ genre: req.query.genre });
  } else if (req.query.alphabet) {
    query.where({ name: new RegExp('^' + '[' + req.query.alphabet + ']', 'i') });
  } else {
    query.limit(12);
  }
  query.exec(function(err, shows) {
    if (err) return next(err);
    res.send(shows);
  });
});

Saya mempunyai 3 buah route berbeda untuk mencari popular shows pada homepage, pencarian berdasarkan genre dan pencarian berdasarkan huruf. Tetapi keduanya memiliki behaviour yang sama, sehingga saya gabungkan ke dalam single route menggunakan Mongoose query builder.

app.get('/api/shows/:id', function(req, res, next) {
  Show.findById(req.params.id, function(err, show) {
    if (err) return next(err);
    res.send(show);
  });
});

Jika ada error maka kita bisa menggunakan print stack trace pada console dengan return nya dengan format JSON.

app.use(function(err, req, res, next) {
  console.error(err.stack);
  res.send(500, { message: err.message });
});

parsing

500

Jika Anda ingin halaman Add, Login atau Signup, coba Refresh browser Anda dan melihat 404 error:
Cannot GET /add
Tambahkan route berikut untuk mengangani error:

app.get('*', function(req, res) {
  res.redirect('/#' + req.originalUrl);
});

Ketika kita tidak menemukan route yang cocok seperti /asdf, makan akan redirect/kembali ke page /:

.otherwise({
 redirectTo: '/'
});

Step 6: Query and Parse The TVDB API
Untuk menambahkan TV show baru ke database, kita butuh membuat route terpisah :

app.post('/api/shows', function(req, res, next) {
  var apiKey = '9EF1D1E7D28FDA0B';
  var parser = xml2js.Parser({
    explicitArray: false,
    normalizeTags: true
  });
  var seriesName = req.body.showName
    .toLowerCase()
    .replace(/ /g, '_')
    .replace(/[^\w-]+/g, '');
  
  async.waterfall([
    function(callback) {
      request.get('http://thetvdb.com/api/GetSeries.php?seriesname=' + seriesName, function(error, response, body) {
        if (error) return next(error);
        parser.parseString(body, function(err, result) {
          if (!result.data.series) {
            return res.send(404, { message: req.body.showName + ' was not found.' });
          }
          var seriesId = result.data.series.seriesid || result.data.series[0].seriesid;
          callback(err, seriesId);
        });
      });
    },
    function(seriesId, callback) {
      request.get('http://thetvdb.com/api/' + apiKey + '/series/' + seriesId + '/all/en.xml', function(error, response, body) {
        if (error) return next(error);
        parser.parseString(body, function(err, result) {
          var series = result.data.series;
          var episodes = result.data.episode;
          var show = new Show({
            _id: series.id,
            name: series.seriesname,
            airsDayOfWeek: series.airs_dayofweek,
            airsTime: series.airs_time,
            firstAired: series.firstaired,
            genre: series.genre.split('|').filter(Boolean),
            network: series.network,
            overview: series.overview,
            rating: series.rating,
            ratingCount: series.ratingcount,
            runtime: series.runtime,
            status: series.status,
            poster: series.poster,
            episodes: []
          });
          _.each(episodes, function(episode) {
            show.episodes.push({
              season: episode.seasonnumber,
              episodeNumber: episode.episodenumber,
              episodeName: episode.episodename,
              firstAired: episode.firstaired,
              overview: episode.overview
            });
          });
          callback(err, show);
        });
      });
    },
    function(show, callback) {
      var url = 'http://thetvdb.com/banners/' + show.poster;
      request({ url: url, encoding: null }, function(error, response, body) {
        show.poster = 'data:' + response.headers['content-type'] + ';base64,' + body.toString('base64');
        callback(error, show);
      });
    }
  ], function(err, show) {
    if (err) return next(err);
    show.save(function(err) {
      if (err) {
        if (err.code == 11000) {
          return res.send(409, { message: show.name + ' already exists.' });
        }
        return next(err);
      }
      res.send(200);
    });
  });
});

Saya sudah menambahkan kode error handling untuk duplikat Shows saat method show.save() dijalankan. Saya juga sudah menambahkan validation check jika ada seriesid yang sama. Jika tidak ditemukan, berarti TVDB tidak punya informasi tentang show/film tersebut dan muncul pesan “a show was not found”.
notfound
Sebelum lanjut, jangan lupa install dan tambahkan dependency yang digunakan pada route :

npm install --save async request xml2js lodash
var async = require('async');
var request = require('request');
var xml2js = require('xml2js');
var _ = require('lodash');

Langkah 7: Kembali ke AngularJS
Buat template baru add.html pada direktori views:
addhtml
Saya sudah menambahkan form validasi dan error messages saat melakukan Signup pada Langkah 8.
Ketika Anda menekan Add button, AngularJS akan menjalankan method addShow() yang didefenisikan pada AddCtrl controller karena pada line ini:
<form method="post" ng-submit="addShow()" name="addForm" class="form-inline">
tampilanAdd

Kita juga butuh sebuah controller pada page ini:
addjs

angular.module('MyApp')
  .controller('AddCtrl', function($scope, $alert, Show) {
    $scope.addShow = function() {
      Show.save({ showName: $scope.showName }).$promise
        .then(function() {
          $scope.showName = '';
          $scope.addForm.$setPristine();
          $alert({
            content: 'TV show has been added.',
            animation: 'fadeZoomFadeDown',
            type: 'material',
            duration: 3
          });
        })
        .catch(function(response) {
          $scope.showName = '';
          $scope.addForm.$setPristine();
          $alert({
            content: response.data.message,
            animation: 'fadeZoomFadeDown',
            type: 'material',
            duration: 3
          });
        });
    };
  });

Saya juga sudah menambahkan succes message ketika show/film berhasil ditambahkan, contohnya saya menambahkan film Sherlock.
berhasiTambah

Sekarang buat file detail.html pada public/views :
Line 1-32 :
detail1-32

Line 33-63 :
line33-63

Buat file fromNow.js pada direktori public/filters :

angular.module('MyApp').
  filter('fromNow', function() {
    return function(date) {
      return moment(date).fromNow();
    }
});

Seperti biasa, jangan lupa panggil lewat index.html:
formNow
Selanjutnya kita buat controller DetailCtrl :
detailctrl

angular.module('MyApp')
  .controller('DetailCtrl', ['$scope', '$rootScope', '$routeParams', 'Show', 'Subscription',
    function($scope, $rootScope, $routeParams, Show, Subscription) {
      Show.get({ _id: $routeParams.id }, function(show) {
        $scope.show = show;

        $scope.isSubscribed = function() {
          return $scope.show.subscribers.indexOf($rootScope.currentUser._id) !== -1;
        };

        $scope.subscribe = function() {
          Subscription.subscribe(show).success(function() {
            $scope.show.subscribers.push($rootScope.currentUser._id);
          });
        };

        $scope.unsubscribe = function() {
          Subscription.unsubscribe(show).success(function() {
            var index = $scope.show.subscribers.indexOf($rootScope.currentUser._id);
            $scope.show.subscribers.splice(index, 1);
          });
        };

        $scope.nextEpisode = show.episodes.filter(function(episode) {
          return new Date(episode.firstAired) > new Date();
        })[0];
      });
    }]);

Berikut Show service mempunyai beberapa method:

{ 'get': {method:'GET'},
'save': {method:'POST'},
'query': {method:'GET', isArray:true},
'remove': {method:'DELETE'},
'delete': {method:'DELETE'} };

Dengan kata lain, kita gunakan Show.get() untuk mendapatkan single show dan Show.query() untuk mendapatkan array dari list show.
Untuk membuat aksi subcribe/unscubscribe, buat file subscription.js di dalam direktori services:
subcription

angular.module('MyApp')
  .factory('Subscription', ['$http', function($http) {
    return {
      subscribe: function(show, user) {
        return $http.post('/api/subscribe', { showId: show._id });
      },
      unsubscribe: function(show, user) {
        return $http.post('/api/unsubscribe', { showId: show._id });
      }
    };
  }]);

ipman

Kita akan membuat Express routes /api/subscribe dan /api/unsubscribe pada Langkah 10, setelah kita mengimplementasikan otentikasi dari client-side dan server-side.

Langkah 8: Otentikasi Client-side
Buat template baru login.html :
Line 1-19 :
1-19login
Line 20-40 :
20-40Login
Tampilan login :
login
Buat template Signup.html :
Line 1-15 :
singup1-15
Line 16-44 :
singup16-44
Buat file repeatPassword.js dalam direktori public/directives dan panggil dari file index.html
passwordStreng

angular.module('MyApp')
  .directive('passwordStrength', function() {
    return {
      restrict: 'A',
      require: 'ngModel',
      link: function(scope, element, attrs, ngModel) {
        var indicator = element.children();
        var dots = Array.prototype.slice.call(indicator.children());
        var weakest = dots.slice(-1)[0];
        var weak = dots.slice(-2);
        var strong = dots.slice(-3);
        var strongest = dots.slice(-4);

        element.after(indicator);

        element.bind('keyup', function() {
          angular.forEach(dots, function(el) { el.style.backgroundColor = '#ebeef1'; });
          if (ngModel.$modelValue) {
            if (ngModel.$modelValue.length > 8) {
              angular.forEach(strongest, function(el) { el.style.backgroundColor = '#008cdd'; });
            } else if (ngModel.$modelValue.length > 5) {
              angular.forEach(strong, function(el) { el.style.backgroundColor = '#6ead09'; });
            } else if (ngModel.$modelValue.length > 3) {
              angular.forEach(weak, function(el) { el.style.backgroundColor = '#e09115'; });
            } else {
              weakest.style.backgroundColor = '#e01414';
            }
          }
        });
      },
      template: ''
    };
  });

signup

Buat controller untuk login.html and signup.html:
Berikut Signup controller:
signupctrll

angular.module('MyApp')
  .controller('SignupCtrl', ['$scope', 'Auth', function($scope, Auth) {
    $scope.signup = function() {
      Auth.signup({
        email: $scope.email,
        password: $scope.password
      });
    };
  }]);

Berikut Login controller:
loginsignupctrl

angular.module('MyApp')
  .controller('LoginCtrl', ['$scope', 'Auth', function($scope, Auth) {
    $scope.login = function() {
      Auth.login({
        email: $scope.email,
        password: $scope.password
      });
    };
  }]);

Baik Login maupun Signup controllers menggunakan Auth service.
Buat file service auth.js pada direktori services:
authjs

angular.module('MyApp')
  .factory('Auth', function($http, $location, $rootScope, $alert, $window) {
    var token = $window.localStorage.token;
    if (token) {
      var payload = JSON.parse($window.atob(token.split('.')[1]));
      $rootScope.currentUser = payload.user;
    }

    // Asynchronously initialize Facebook SDK
    $window.fbAsyncInit = function() {
      FB.init({
        appId: '624059410963642',
        responseType: 'token',
        version: 'v2.0'
      });
    };

    // Asynchronously load Facebook SDK
    (function(d, s, id) {
      var js, fjs = d.getElementsByTagName(s)[0];
      if (d.getElementById(id)) {
        return;
      }
      js = d.createElement(s);
      js.id = id;
      js.src = "//connect.facebook.net/en_US/sdk.js";
      fjs.parentNode.insertBefore(js, fjs);
    }(document, 'script', 'facebook-jssdk'));

    // Asynchronously load Google+ SDK
    (function() {
      var po = document.createElement('script');
      po.type = 'text/javascript';
      po.async = true;
      po.src = 'https://apis.google.com/js/client:plusone.js';
      var s = document.getElementsByTagName('script')[0];
      s.parentNode.insertBefore(po, s);
    })();

    return {
      facebookLogin: function() {
        FB.login(function(response) {
          FB.api('/me', function(profile) {
            var data = {
              signedRequest: response.authResponse.signedRequest,
              profile: profile
            };
            $http.post('/auth/facebook', data).success(function(token) {
              var payload = JSON.parse($window.atob(token.split('.')[1]));
              $window.localStorage.token = token;
              $rootScope.currentUser = payload.user;
              $location.path('/');
              $alert({
                title: 'Cheers!',
                content: 'You have successfully signed-in with Facebook.',
                animation: 'fadeZoomFadeDown',
                type: 'material',
                duration: 3
              });
            });
          });
        }, { scope: 'email, public_profile' });
      },
      googleLogin: function() {
        gapi.auth.authorize({
          client_id: '55262601920-5jhf3qth89okujq6a7lh8bqc9epr8475.apps.googleusercontent.com',
          scope: 'https://www.googleapis.com/auth/plus.login https://www.googleapis.com/auth/plus.profile.emails.read',
          immediate: false
        }, function(token) {
          gapi.client.load('plus', 'v1', function() {
            var request = gapi.client.plus.people.get({
              userId: 'me'
            });
            request.execute(function(authData) {
              $http.post('/auth/google', { profile: authData }).success(function(token) {
                var payload = JSON.parse($window.atob(token.split('.')[1]));
                $window.localStorage.token = token;
                $rootScope.currentUser = payload.user;
                $location.path('/');
                $alert({
                  title: 'Cheers!',
                  content: 'You have successfully signed-in with Google.',
                  animation: 'fadeZoomFadeDown',
                  type: 'material',
                  duration: 3
                });
              });
            });
          });
        });
      },
      login: function(user) {
        return $http.post('/auth/login', user)
          .success(function(data) {
            $window.localStorage.token = data.token;
            var payload = JSON.parse($window.atob(data.token.split('.')[1]));
            $rootScope.currentUser = payload.user;
            $location.path('/');
            $alert({
              title: 'Cheers!',
              content: 'You have successfully logged in.',
              animation: 'fadeZoomFadeDown',
              type: 'material',
              duration: 3
            });
          })
          .error(function() {
            delete $window.localStorage.token;
            $alert({
              title: 'Error!',
              content: 'Invalid username or password.',
              animation: 'fadeZoomFadeDown',
              type: 'material',
              duration: 3
            });
          });
      },
      signup: function(user) {
        return $http.post('/auth/signup', user)
          .success(function() {
            $location.path('/login');
            $alert({
              title: 'Congratulations!',
              content: 'Your account has been created.',
              animation: 'fadeZoomFadeDown',
              type: 'material',
              duration: 3
            });
          })
          .error(function(response) {
            $alert({
              title: 'Error!',
              content: response.data,
              animation: 'fadeZoomFadeDown',
              type: 'material',
              duration: 3
            });
          });
      },
      logout: function() {
        delete $window.localStorage.token;
        $rootScope.currentUser = null;
        $alert({
          content: 'You have been logged out.',
          animation: 'fadeZoomFadeDown',
          type: 'material',
          duration: 3
        });
      }
    };
  });

Pada sesi selanjutnya kita membuat sebuah Express middleware yang membuat User cookie setiap kali request.
authentication

Kemudian buat navbar.js:
src="controllers/navbar.js">

angular.module('MyApp')
  .controller('NavbarCtrl', ['$scope', 'Auth', function($scope, Auth) {
    $scope.logout = function() {
      Auth.logout();
    };
  }]);

Tentu saja kita belum bisa login atau membuat account baru karen kita belum melakukan implementasi pada server.

Langkah 9: Otentikasi Server-side

Install dependency :

npm install --save express-session passport passport-local

Kemudian tambahkan ke dalam modul dependency Anda:

var session = require('express-session');
var passport = require('passport');
var LocalStrategy = require('passport-local').Strategy;

Untuk melakukan pengaturan Passport.js kita butuh mengkonfigurasi empat hal:

  • Method serialize dan deserialize
  • Passport strategy
  • Express session middleware
  • Passport middleware

Method Serialize dan deserialize digunakan untuk menyimpan signed-in. Lebih lajut :

passport.serializeUser(function(user, done) {
  done(null, user.id);
});
passport.deserializeUser(function(id, done) {
  User.findById(id, function(err, user) {
    done(err, user);
  });
});

Kita tidak signin dengan Facebook, Google or Twitter. Kita akan menggunakan Passport’ LocalStrategy untuk signin menggunakan username dan password.

passport.use(new LocalStrategy({ usernameField: 'email' }, function(email, password, done) {
  User.findOne({ email: email }, function(err, user) {
    if (err) return done(err);
    if (!user) return done(null, false);
    user.comparePassword(password, function(err, isMatch) {
      if (err) return done(err);
      if (isMatch) return done(null, user);
      return done(null, false);
    });
  });
}));

Tambahkan Express Session dan Passport middleware setelah cookieParser() middleware:

app.use(session({ secret: 'keyboard cat' }));
app.use(passport.initialize());
app.use(passport.session());

Terakhir, jika user diotentikasi maka akan membuat cookie baru yang di handle oleh AngularJS service untuk membaca informasi user.

app.use(function(req, res, next) {
  if (req.user) {
    res.cookie('user', JSON.stringify(req.user));
  }
  next();
});

Lanjutkan untuk membuat akun baru dan coba untuk log in.
login berhasil

Langkah 10: Subscription

Pada bagian ini kita akan membuat dua buah route untuk subscribing dan unsubscribing ke/dari form show.

app.post('/api/subscribe', ensureAuthenticated, function(req, res, next) {
  Show.findById(req.body.showId, function(err, show) {
    if (err) return next(err);
    show.subscribers.push(req.user.id);
    show.save(function(err) {
      if (err) return next(err);
      res.send(200);
    });
  });
});
app.post('/api/unsubscribe', ensureAuthenticated, function(req, res, next) {
  Show.findById(req.body.showId, function(err, show) {
    if (err) return next(err);
    var index = show.subscribers.indexOf(req.user.id);
    show.subscribers.splice(index, 1);
    show.save(function(err) {
      if (err) return next(err);
      res.send(200);
    });
  });
});

Selesai..

Find Source on Github

Keep Learn, Have fun Coding !!!

Advertisements