LoginSignup
2
2

More than 5 years have passed since last update.

Các bài học lập trình ứng dụng RESTful ~ Chuyên đề phát triển Web - [Chương 5] Tạo chức năng đăng nhập với CakePHP

Last updated at Posted at 2015-10-21

:large_blue_circle: Chào mừng các bạn đã trở lại với series

Ở bài viết trước chúng ta đã tìm hiểu cách quản lý sự phụ thuộc của các với nhau bằng require.js, các bạn có thể xem lại ở [Bài 4] Sử dụng RequireJS để viết module cho JavaScript.

:warning: Bây giờ chúng ta bắt đầu viết chức năng đăng nhập cho chương trình, bài viết này được dịch từ CakePHPで実装するログイン機能 - AWS上で構築するRESTfulアプリ勉強会~Web開発ワークショップ~【第5回】マニュアル của tác giả @k_shimoji, mình đã làm theo đến hết và cố gắng Việt Hóa hoặc chuyển sang tiếng Anh cơ bản nhiều nhất có thể để các bạn dễ hình dung.
Ở chương 2 chúng ta đã tạo ra giao diện đăng nhập với Backbone.js ở bài 2, bây giờ mình sẽ bổ sung các chức năng.

:warning: Thông tin của user mình lưu trong database có một thành phần đặc biệt là mật khẩu, mình sẽ mã hóa nó trong quá trình đăng ký, vì vậy các bạn sẽ gặp lỗi nếu thêm dữ liệu trực tiếp bằng phpmyadmin.

:large_blue_circle: Đăng nhập là gì

Chắc nhiều bạn đã biết định nghĩa của login rồi nên không cần nhắc lại nhiều nữa.
Việc chúng ta cần làm là tạo một chức năng đăng nhập với 2 chức năng sau:

  • Xác thực - Authentication
  • Kiểm duyệt - Authorization

Xác thực - Authentication là gì ?

Authentication hay còn gọi là xác thực "identification".
Mình sắp tạo chức năng login, và mình sẽ sử dụng các form authentication truyền thống, cụ thể hơn là sử dụng "Password" và "User ID" để nhận dạng một người, một tài khoản....
Thường nói đến login là nói đến "Authentication", Chúng ta sẽ thực hiện các bước "approval" và phần set.

Kiểm duyệt - Authorization là gì ?

Một user sau khi đã được "authenticated" sẽ được cho phép truy cập vào các tài nguyên hay chức năng cụ thể. Bước kiểm duyệt ( Authorization) sẽ giúp cho phép họ vào đúng các chức năng mà họ được phép truy cập.

:large_blue_circle: Vị trí đặc biệt của việc thực hiện quá trình đăng nhập hiện tại

Tiêu đề bài học của chúng ta là "REST", "SinglePageApplication (SPA)".
Tuy nhiên không phải REST hay là SPA, đây là một app cơ bản bằng CakePHP ( Một Web application thông dụng để sinh ra các trang ở server side), nếu có truy cập vào trang cần xác thực thì bạn cần một page ở khu vực dành cho những người chưa được kiểm duyệt (unauthorized state) hay chưa đăng nhập, bạn sẽ được chuyển ( redirect ) về trang đăng nhập.
Tuy nhiên, vì SPA được cấu hình để tất cả các truy cập đều thông qua RestAPI, vì vậy việc redirect sẽ không được sử dụng, chúng ta đơn giản là return HTTP status code 401 (Unauthorized) tới client, chương trình ở client-side sẽ tự hiển thị màn hình đăng nhập.
Tất cả các API requests ở khu vực dành cho non-authorized đều sẽ được return HTTP status code 401, client side sẽ phải phản hồi nó.

:large_blue_circle: Nội dung bài học

Ở bài học này các bạn sẽ làm được những hàm dưới đây, hãy cố gắng nhé vì đây là những hàm được sử dụng rất thường xuyên !

Danh sách đặc điểm

  • Đăng ký User
    • Username và tên (name), mật khẩu (password) và sau khi Enter, mình sẽ tạo được user mới
    • Nếu thành công sẽ hiển thị thông báo thành công
  • Đăng nhập
    • Nhập username, password và Click button Login.
    • Nếu các thông tin đăng nhập là đúng, mình sẽ chuyển đến màn hình danh sách TODO list.
    • Nếu sai thông tin sẽ xuất ra lỗi.
    • Hiển thị thông báo đã đăng nhập nếu người dùng đã đăng nhập trước đó rồi.
    • Nếu user đã đăng nhập từ trước sẽ chuyển đến màn hình danh sách TODO list.
  • Đăng xuất
    • Click vào logout button để quay lại phần login.
    • Chuyển qua trang login.
  • Hiển thị Header
    • Sau khi đăng nhập thì mình sẽ luôn hiển thị username và nút logout ở phần trên cùng của màn hình (header).
  • Kiểm tra xác thực

    • Với những người chưa được xác thực đăng nhập (unauthorized users), tất cả các truy cập sẽ đều được chuyển trở lại màn hình login. ### Màn hình hiển thị sẽ trông như thế này
  • Đăng ký user

    • Ở một nửa màn hình trang login, phần dưới sẽ là màn hình đăng ký user.
      • Ở màn hình đăng ký, bạn cần đăng ký username, tên, và password.
    • :warning: Một quy trình đăng ký tốt cần phải thêm một phần xác nhận "re-enter the password" để đề phòng người dùng gõ sai chính tả, tuy nhiên chúng ta chỉ làm demo ví dụ nên sẽ tạm bỏ qua phần này. - OK bắt đầu làm thôi! Mục tiêu sau khi xong bài 5 này chúng ta sẽ được kết quả như sau

login.PNG

  • màn hình TODO list
    • Header sẽ xuất hiện ở trên cùng, hiển thị username, tên, và nút logout để đăng xuất.

list.PNG

  • Trang detail của mỗi TODO
    • Nó cũng hiển thị header ở trang này

detail.PNG

:large_blue_circle: Chuẩn bị trước khi tiến hành

Những sự chuẩn bị sẽ giống nhau ở mỗi bài học, tôi đã tổng hợp ở một bài viết riêng biệt
All 12 times of study sessions in do I have use of Git - RESTful application workshops will be built on AWS ~ Web development workshop ~ - Qiita các bạn có thể tham khảo .
- Tạo vol/05 trên git.

Ngoài ra các bạn hãy làm việc rất quan trọng sau nữa nhé
- Tạo table cho việc đăng ký user

Tạo table cho việc đăng ký user

Để tạo, chúng ta sẽ thêm table tên là users.

Đầu tiên hãy đăng nhập vào phpMyAdmin.

URL vẫn là http://(PublicIP)/phpMyAdmin/.
Nếu các bạn chưa thay đổi gì thì tên và pass đăng nhập cùng là study.

Như hình ảnh dưới đây, nhìn vào database [study] ở phần bên phải - Click vào [New] để tạo table mới.

create_table.png

Hãy thiết lập như sau

Tên table

users

Thiết lập cho mỗi column
Name Data type length / value default value collation attribute NULL Index A_I (AutoIncrement) Comments
id INT 10 None   UNSIGNED   PRIMARY v id
username VARCHAR 20 None utf8_general_ci     UNIQUE   tên user
password VARCHAR 100 None utf8_general_ci         password
name VARCHAR 50 None utf8_general_ci         tên
Các thiết lập
  • Table's comment:user
  • Storage Engine:InnoDB
  • Bảng mã:utf8_general_ci
  • Định nghĩa phân vùng: bỏ trống

Click save để tạo table

Chuẩn bị

Tóm tắt quá trình chuẩn bị

  • :white_check_mark: Sao lưu các branch cũ và tạo branch vol/05 mới.
  • :white_check_mark: Tạo table cho việc đăng ký user

Khi đã sẵn sàng hết, chúng ta bắt đầu bài 1

:large_blue_circle: Bài 1: Server side

Ở bài 1, mình sẽ implement phần server side của chương trình.
Để kiểm tra kết quả chạy, dử dụng "POSTMAN" mà mình đã cài đặt ở bài viết đầu tiên để kiểm tra kết quả trả về từ API.

Chú ý

Ứng dụng này không phải SPA.

Nếu bạn thử xem TODO List ở khu vực chưa login

Trường hợp web app không phải SPA ( Phần lớn )

Trình tự thao tác

minhhoa1.png

  1. Yêu cầu được vào màn hình TODO list gửi đến server
  2. Về phần server, vì đây là request từ một user chưa được xác thực, nó sẽ trả về 302, và sau đó redirected về màn hình đăng nhập.
  3. Sau khi trình duyệt nhận được kết quả trả về 302, request cụ thể bằng URL trong Location header để chuyển trang
  4. server trả về HTML của trang login
Trong trường hợp SPA

Nó sẽ như sau

minhoa2.png

  1. request tới server để vào màn hình TODO list.
  2. Về phần server server, vì đây là requst từ user chưa login, trả về status code 401, nó sẽ phản hồi rằng request này không được cho phép.
  3. Khi nhận về kết quả 401 thì Javascript sẽ hiểu và tự hiển thị màn hình login.

Hãy hiểu rằng 2 trường hợp trên khác nhau.

Tạo API

URL Http Method Mô tả Controller Action
/rest-study/users/login.json POST Đăng nhập UsersController login
/rest-study/users/logout.json POST Đăng xuất UsersController logout
/rest-study/users/loggedin.json GET Lấy thông tin đăng nhập của user ( kiểm tra đã login chưa) UsersController loggedIn
/rest-study/users/signup.json POST Đăng ký user UsersController signUp

Các file cần chỉnh sửa

Thao tác file Mô tả
Chỉnh sửa app/Config/routes.php Thêm các đường dẫn root của API
Chỉnh sửa app/Controller/AppController.php Các thiết lập của Auth component
Thêm mới app/Controller/UsersController.php Sử dụng
Thêm mới app/Model/User.php Truy cập vào table user.

routes.php

Thêm route cho API vừa thêm vào

routes.php

 /*
  * API
  */
+
+// Đăng nhập
+Router::connect('/users/login', array (
+   'controller' => 'users',
+   'action' => 'login',
+   'method' => array (
+       'POST'
+   )
+));
+
+// Đăng xuất
+Router::connect('/users/logout', array (
+   'controller' => 'users',
+   'action' => 'logout',
+   'method' => 'POST'
+));
+
+// Login check (kiểm tra thông tin đăng nhập)
+Router::connect('/users/loggedin', array (
+   'controller' => 'users',
+   'action' => 'loggedIn',
+   'method' => 'GET'
+));
+
+// Đăng ký
+Router::connect('/users/signup', array (
+   'controller' => 'users',
+   'action' => 'signUp',
+   'method' => array (
+       'POST'
+   )
+));
+
+
 Router::mapResources(array (
    'todo_lists',
 ));
 Router::parseExtensions('json');

Nếu một API để thực hiện một CRUD đơn giản,
Bằng hàm Router::mapResources(), Controller luôn được thêm .
route cho các hành động của index, view, add, edit, delete sẽ được set, nhưng vì URL đăng nhập không cùng loại nên nó sẽ được thêm riêng
Sử dụng hàm Router::connect()đã được set với các thành phần theo thứ tự như dưới đây.

  • URL
  • Controller để chạy
  • Hành động để thực thi
  • Http method mà bạn thích

VD ở hàm login
- URL -> /users/login
- Run tới controller -> users
- Thực thi hành động-> login
- Phương thức HTTP dể trỏ đến. -> POST

AppController.php

Component, thêm các bước xác thực

routes.php
 class AppController extends Controller {
-    public $components = array(
-        'RequestHandler'
-    );
+   public $components = array (
+       'RequestHandler',
+       'Auth' => array (
+           'authenticate' => array (
+               'Form' => array (
+                   'passwordHasher' => 'Blowfish'
+               )
+           )
+       )
+   );
+
+   public function beforeFilter() {
+       $user = $this->Auth->user();
+       if ($user === null
+               && $this->request->params['controller'] !== 'users'
+               && $this->request->params['action'] !== 'login'
+               && $this->request->params['action'] !== 'logout'
+               && $this->request->params['action'] !== 'loggedin'
+               && $this->request->params['action'] !== 'signup') {
+                   throw new UnauthorizedException();
+       }
+       //Cho phép tất cả các hành động
+       $this->Auth->allow();
+   }
+
 }
Biến $Components

Tiến trình authentication sử dụng Auth components mà CakePHP đã có sẵn.
Mình sử dụng Blowfish để xử lý thuật toán hash mã hóa password.

Hàm BeforeFilter

Một hàm để được thực thi trước tất cả các hành động gọi là hàm beforeFilter.
Tại đây sẽ kiểm tra user đang truy cập đã được authenticated hay chưa.
Nếu user đó là unauthenticated thì việc đăng nhập (login), đăng ký user (signup), khác với check trạng thái đăng nhập (loggedin) nên mình không chạy nó được.

  • $This->Auth->user() để lấy thông tin đăng nhập của user
  • Nếu thông tin đăng nhập của user không thể lấy được, với exception của users controller, sẽ trả về lỗi 401 error bằng cách throw UnauthorizedException.
    • login
    • loggedin
    • signup

UsersController.php

Đăng nhập, xác thực đăng nhập thành công, đăng xuất, thực hiện đăng ký.

UsersController.php
<?php

App::uses('AppController', 'Controller');
App::uses('BlowfishPasswordHasher', 'Controller/Component/Auth');

class UsersController extends AppController {

    public function login() {
        $user = $this->Auth->user();
        $res = array();
        if ($user) {
            if ($user['username'] === $this->request->data['username']) {
                $res['User'] = $user;
                $res['message'] = "You are already logged in";
            } else {
                $res['message'] = "another is loged in this account";
            }
        } else {
            $this->request->data['User']['username'] = $this->request->data['username'];
            $this->request->data['User']['password'] = $this->request->data['password'];
            if ($this->Auth->login()) {
                $user = $this->Auth->user();
                $res['User'] = $user;
                $res['message'] = "Login successfully";
            } else {
                $res['message'] = "Username or password are incorrect";
            }
        }
        $this->set(compact('res'));
        $this->set('_serialize', 'res');
    }

    public function loggedIn(){
        $user = $this->Auth->user();
        if ($user) {
            $res['User'] = $user;
            $res['message'] = "You are already logged in";
            $this->set(compact('res'));
            $this->set('_serialize', 'res');
        }else{
            throw new UnauthorizedException();
        }
    }

    public function logout() {
        $user = $this->Auth->user();
        $res = array();
        if ($user !== null) {
            $this->Auth->logout();
            $res['message'] = "Logout successfully";
        } else {
            $res['message'] = "can not logout";
        }
        $this->set(compact('res'));
        $this->set('_serialize', 'res');
    }

    public function signUp() {
        $data = $this->request->data;
        if (isset($data['password'])) {
            $passwordHasher = new BlowfishPasswordHasher();
            $data['password'] = $passwordHasher->hash($data['password']);
        }
        $res = $this->User->save($data);
        if($res){
            unset($res['User']['password']);
            $res['message'] = "Register successfully, you can Login!";
        }else{
            $res['message'] = "Fail to register";
        }
        $this->set(compact('res'));
        $this->set('_serialize', 'res');
    }

}

Hàm login

Nó sẽ chạy tiến trình login. Kết quả sẽ là một trong những cái sau:

  • Đăng nhập thành công
  • Đăng nhập thất bại (sai username hoặc password)
  • Đã đăng nhập
  • Người khác đã đăng nhập rồi

Tiến trình đăng nhập,
- Gửi username qua $this->request->data['User']['username']
- Gửi password qua$this->request->data['User']['password']
- Chạy $this->Auth->login()

As JSON item to return to the client, you have to set the following.

  • User -> Thông tin user (id, username, name)
  • message -> Tin nhắn thông báo kết quả xử lý
VD(KhiLoginThànhCông)
{
    "User": {
        "id": "1",
        "username": "test",
        "name": "test"
    },
    "message": "Login successfully"
}

Hàm loggedIn

Check xem user đã đăng nhập hay chưa.

  • Lây thông tin đăng nhập của user ở $this->Auth->user(), nó sẽ trả về kết quả null nếu chưa đăng nhập.
  • Nếu chưa đăng nhập, sinh ra lỗi 401 error bằng cách throw UnauthorizedException.
  • JSON trả về client giống như hàm login, xuất usermessage.

Hàm logout

Nó sẽ chạy tiến trình đăng xuẩt.

  • $This->Auth->user() để kiểm tra user đã đăng nhập hay chưa, nếu đã đăng nhập thì chạy tiến trình log out bằng $this->Auth->logout().
  • Nếu chưa login thì sẽ không chạy được tiến trình log out, thêm một message ở JSON.

Hàm register

Sử dụng User model để đăng ký thông tin user vào database.

  • Password được lưu bằng một chuỗi (string) được mã hóa bởi class BlowfishPasswordHasher.
  • Nếu đã lưu thành công mình sẽ set thông tin user ở JSON, riêng password thì là một ngoại lệ (unset($res['User']['password']);).

User.php

Table users là một model cho việc thêm data.

User.php
<?php

App::uses('AppModel', 'Model');

class User extends AppModel {
}

Nó sẽ không implement những hàm không đúng tiêu chuẩn của CakePHP model.
:warning: Bạn có thể dễ dàng tìm thấy các app phù hợp, nhưng phải implement của việc validation giá trị input tại đây, nó sẽ quyết định không implement chúng vì không phải ở giao diện này.

 Kiểm tra kết quả

Chúng ta sẽ kiểm tra từng API bằng POSTMAN.
Hãy để các ví dụ vào mỗi API, dữ liệu dưới chỉ để tham khảo.
:warning: user và password sẽ được tạo sau khi đăng ký ( nó sẽ tự sinh mã bằng đoạn hash cho password và lưu vào database chứ không phải định dạng như thường ), khi bạn nhập pass vào siginin thì nó lại sinh mã 1 lần nữa rồi so sánh với mã trong database .
Nếu bạn cố gắng thêm user và pass phpMyAdmin thì rất có thể bạn sẽ đăng nhập thất bại.
Sẽ tốt nhất nếu bạn bắt đầu test từ siginup API, xách POSTMAN lên và test thôi nào.

  • Ví Dụ 1.PNG
URL Request JSON response code response JSON
/rest-study/users/signup.json {
 "username": "test2",
 "password": "test2",
 "name": "test2"
}
200 {
 "User": {
  "username": "test2",
  "name": "test2",
  "id": "65"
 },
 "message": "Registered. You can log in!"
}
/rest-study/users/login.json {
 "username":"test",
 "password":"test"
}
200 {
 "User": {
  "id": "1",
  "username": "test",
  "name": "test"
 },
 "message": "Login successfully"
}
/rest-study/users/login.json {
 "username":"test",
 "password":"hoge"
}
200 {
"message": "Wrong user name or password"
}
/rest-study/users/logout.json - 200 {
 "message": "Log out"
}
/rest-study/users/loggedin.json - 200 {
 "User": {
  "id": "1",
  "username": "test",
  "name": "Test"
 },
 "message": "You are already logged in"
}
  • Ví dụ của lỗi 1 ( Kiểm tra login ở khu vực chưa login - trả về 401)
URL Request JSON response code response JSON
/rest-study/users/loggedin.json - 401 {
 "name": "Unauthorized",
 "message": "Unauthorized",
 "url": "/rest-study/users/loggedIn.json"
}
  • Ví dụ của lỗi 2 ( Truy cập TODO list khi chưa login - trả về 401)
URL Request JSON response code response JSON
/rest-study/users/todo_lists.json - 401 {
 "name": "Unauthorized",
 "message": "Unauthorized",
 "url": "/rest-study/users/todo_lists.json"
}

Tóm tắt bài học

Hãy kiểm tra sự hoạt động của API xem bạn đã thực sự làm đúng chưa

  • :white_check_mark: app/Config/routes.php sửa như hướng dẫn trên.
  • :white_check_mark: app/Controller/AppController.php sửa như hướng dẫn trên.
  • :white_check_mark: app/Controller/UsersController.php thêm mới như hướng dẫn trên.
  • :white_check_mark: app/Model/User.php thêm mới như hướng dẫn trên.
  • :white_check_mark: Kiểm tra kết quả.
  • :white_check_mark: Commit lên Git

Tham khảo bản diff hoàn chỉnh trên GitHub

Bài 5: serverside· suzukishouten-study/rest-study@59c29b0

Đến với bài 2 nào:

:large_blue_circle: Bài 2 : Client Side

Ở bài 2 này chúng ta sẽ bắt đầu implement client side của chương trình

Lưu ý

Hãy check vì có một vài điểm hấp dẫn bạn đây.
Mình sẽ giao tiếp với server bằng Ajax, nó sẽ không đồng bộ nên có thể sẽ gây ra một số sự khó chịu.

Sau đây mình sẽ diễn tả lại những tiến trình sẽ xảy ra sau khi bạn điền URL của màn hình TODO list vào trình duyệt.
Đầu tiên bạn cần gửi giữ liệu của user lên server để xác định đã đăng nhập hay chưa.
( nó cũng sẽ phục vụ việc lấy thông tin của user để hiển thị trên header.)

Biểu đồ hoạt động

Tiến trình các hoạt động được miêu tả bằng hình vẽ dưới đây

bieudo1.png

  1. Bắt đầu việc xác nhận người dùng đăng nhập hay chưa.
  2. Sử dụng XHR (XmlHttpRequest) để giao tiếp với server. Trong trường hợp này bạn sẽ gọi một hàm callback để thực hiện tại thời điểm nhận được phản hồi từ server đến XHR.
  3. Sau khi nhận được phản hồi từ server, XHR chạy hàm callback như đã nói ở bước 2.
  4. Tại hàm callback, xem phản hồi từ server và lưu trữ thông tin user (thông báo trạng thái là đã đăng nhập hay chưa đăng nhập).
  5. Chạy routing ( Hàm Backbone.history.start()).
  6. Controller được chạy, hàm hiển thị màn hình TODO list bắt đầu chạy.
  7. Kiểm tra thông tin user lưu tại bước 4
    • Nếu thông tin user được xác nhận là loggedIn mình sẽ cho họ xem màn hình TODO list.
      • Bạn có thể dễ dàng tìm thông tin user (chưa đăng nhập) và hiển thị màn hình đăng nhập.

Pattern tĩnh

Mở Chương tình của chúng ta ở thời điểm của bài trước, chạy routing như bước 5 trên kia, chúng ta sẽ chuẩn bị chạy hàm Backbone.history.start())
Tại phần này nó sẽ theo trình tự như sau.
bieudo2.png

  1. Bật routing (Hàm Backbone.history.start ()).
  2. Bắt đầu tiến trình xác nhận user đã đăng nhập chưa.
  3. Sử dụng XHR (XmlHttpRequest) để giao tiếp với server. Bạn bắt đầu gọi một hàm callback để chạy vào thời ddioeemr nhận phản hồi từ server đến XHR.
  4. Routing đã được bật ở phần 1 1, controller bắt đầu được thực thi sau khi nhận phản hồi từ server, Hàm hiển thị danh sách TODO list bắt đầu được thực thi
  5. Kiểm tra thông tin user để lưu lại sau khi nhận phản hồi từ server.
  6. Tuy nhiên trường hợp không có phản hồi do chưa đăng nhập thành công, mình sẽ hiển thị màn hình login.
  7. Sau khi nhận phản hồi từ server, XHR chạy hàm callback như bước 2.
  8. Bằng hàm callback, mình sẽ nhìn vào phản hồi từ server và lưu trữ thông tin user. Tuy nhiên chỉ sau khi bạn đã hiển thị xong màn hình login.

Dưới đây sẽ là trường hợp TODO list không xuất hiện vì chưa đăng nhập.

Những file cần thay đổi

Thao tác file Ý nghĩa
Sửa app/View/Layouts/default.ctp Sửa để thêm template mới
Sửa app/webroot/js/main.js thay đổi sời gian chạy application.start()
Sửa app/webroot/js/app.js Kiểm tra đăng nhập
Sửa app/webroot/js/routers/router.js Thực hiện routing
Sửa app/webroot/js/routers/controller.js Quản lý những thứ liên quan đến login
Thêm mới app/webroot/js/models/user-model.js Trỏ đến API
Thêm mới app/webroot/js/views/login-layout-view.js View cho màn hình login
Thêm mới app/webroot/js/views/header-view.js view cho header

default.ctp

Thêm như sau

default.ctp
 <title>TODO List</title>
 </head>
 <body>
+   <!-- header -->
+   <div id="header"></div>
    <!-- Content -->
    <div id="main"></div>
+   
+   <!-- Thêm header lên trên cùng, hiển thị tên, username và nút log out-->
+   <script type="text/template" id="header-template">
+   <p>user:<%- username %>(<%- name %>) <input type="button" id="logout" value="Log Out"></input></p>
+   <hr>
+   </script>

    <!-- TODO一覧表示のレイアウトテンプレート -->
  〜nội dung dài〜
    </script>

    <!-- 詳細画面のレイアウトテンプレート -->
    <script type="text/template" id="todo-detail-layout-template">
  〜nội dung dài〜
    </script>

    <!-- 詳細画面の表示内容テンプレート -->
    <script type="text/template" id="todo-detail-item-template">
 〜nội dung dài〜
    </script>

+   <!-- ログイン画面テンプレート -->
+   <script type="text/template" id="login-layout-template">
+   <h2>Login</h2>
+   <div>
+   <p>User name :<input type="text" id="username" placeholder="username" autofocus></input></p>
+   <p>Password :<input type="password" id="password" placeholder="password"></input></p>
+   <input type="button" id="login" value="Login"></input>
+   </div>
+   <hr>
+   <h2>User Registration</h2>
+   <div>
+   <p>Username <input type="text" id="signup-username" placeholder="username"></input></p>
+   <p>name :<input type="text" id="signup-name" placeholder="name"></input></p>
+   <p>Password :<input type="password" id="signup-password" placeholder="password"></input></p>
+   <input type="button" id="signup" value="Sign Up"></input>
+   </div>
+   </script>
+
    <!-- require -->
    <script type="text/javascript" src="js/require-config.js"></script>
    <script type="text/javascript" src="js/lib/require.js" data-main="main.js"></script>

 </body>
 </html>

Mình đã thêm vào những thành phần:

  • Thẻ Div cho region hiển thị thông tin trên header
  • Template cho phần view của việc hiển thị thông tin trên header.
    • Username, full name, một nút logout ở đây
  • Template cho màn hình login
    • Thêm vào các thành phần đăng nhập
      • Tên user
      • Password
      • Nút login
    • Các thành phần của việc đăng ký thành viên
      • Tên user
      • Tên đầy đủ
      • mật khẩu
      • Nút đăng ký

main.js

main.js
    require(['app'], function(Application){
        console.log('run main2');
        window.application = new Application();
-       window.application.start();
-       console.log('app start');
    });
 });

Xóa application.start().
Đây là điểm thú vị mà tôi đã nói trước đó.
Khi tiến trình kiểm tra login ở (app.js) (Kiểm tra login hay chưa) được hoàn tất, mình sẽ chạy application.start (). Sự khác nhau giữa việc xóaapplication.star()`, và không xóa sẽ được mô tả dựa trên code dưới đây.

Trường hợp bạn xóa application.start()( Nên làm)

  • Truy cập vào /rest-study/
  • Chạy main.js. applicaion object sẽ được tạo
  • Chạy login check tại hàm initialize của app.js ( Chạy loggedin API của giao tiếp Ajax )
  • Nhận phản hồi từ loggedin API,
    • Lấy thông tin User nếu đã login
      • Bỏ qua thông tin user nếu chưa login
    • Chạy application.start()
      • onStart() được thực thi, chạy Backbone.history.start(), bắt đầu routing.
      • controller bắt đầu chạy
        • Nếu giữ thông tin user, chuyển nó ra màn hình TODO list
        • Nếu bỏ qua thông tin user, Chạy hàm Backbone.history.navigate() để chuyển trở lại màn hình login (/#login)

Nếu không xóa application.start()

  • Truy cập vào /rest-study/
  • Chay main.js. applicaion.start() bắt đầu chạy
  • Chạy login check tại hàm initialize của app.js ( Chạy loggedin API của giao tiếp Ajax )
  • Trước khi nhận phản hồi từ loggedin API, onStart() của app.js sẽ được chạy, Backbone.history.start() chạy luôn, bắt đầu routing. Chạy hàm kiểm tra login ( Chạy loggedin API của giao tiếp Ajax )
    • controlle được thực thi.
      • Bởi vì loggedin vẫn chưa nhận được phản hồi bằng giao tiếp AJAX từ API, việc khởi tạo giá trị cho thông tin user đã bị bỏ qua, Chạy hàm Backbone.history.navigate() để chuyển trở lại màn hình login (/#login) ( Bất kể đã đăng nhập hay chưa ).
  • Nhận phản hồi từ loggedin API, set giá trị như dưới đây
    • Lấy thông tin User nếu đã login
      • Bỏ qua thông tin user nếu chưa login
    • Tuy nhiên, vì mình đã hoàn tất tiến trình của controller, màn hình login sẽ hiển thị ...

app.js

Thêm mỗi tiến trình của những thứ liên quan đến login

app.js
 define(function(require){
    console.log('run app');
    var Router = require('routers/router');
+   var UserModel = require('models/user-model');
    var Application = Marionette.Application.extend({
        initialize : function(){
            console.log('app.initialize');
+           // Set a common handler of error of Ajax
+           $(document).ajaxError(function ( e, xhr, options , message ) {
+               window.application.ajaxErrorHandler( e, xhr, options , message );
+           });
            new Router();
+           //Lấy thông tin login từ server
+           this.loginUser = new UserModel();
+           this.getLoginUser();
        },

        onStart : function(){
            Backbone.history.start();
        },

        regions : {
+           headerRegion : '#header',
            mainRegion : '#main'
-       }
+       },
+       
+       //thông tin user cần lưu trữ
+       loginUser : null,
+
+       //bắt đầu lấy thông tin đăng nhập
+       getLoginUser : function(){
+           this.loginUser.getLoginUser(
+               this.onLoggedIn,
+               this.onNotLoggedIn
+           );
+       },
+       
+       //Lấy thông tin user: trường hợp đã login
+       onLoggedIn : function(){
+           window.application.start(); // applicaiton.start() sau khi check login
+       },
+       
+       //Lấy thông tin user: trường hợp chưa login
+       onNotLoggedIn : function(){
+           window.application.clearLoginUser();    // mình sẽ bỏ qua thông tin login của user
+           window.application.start(); // applicaiton.start() sau khi check login
+       },
+       
+       //Các phần xử lý
+       isLoggedIn : function(){
+           return this.loginUser.isLoggedIn();
+       },
+       
+       //Xóa thông tin login của user khi logout
+       clearLoginUser : function(){
+           this.loginUser.clear();
+       },
+
+       // Các hàm bắt lỗi của ajax
+       ajaxErrorHandler : function(e, xhr, options , message){
+           if( xhr.status === 401 ){
+               this.clearLoginUser();
+               // Nếu unauthenticated thì chuyển về màn hình login
+               Backbone.history.navigate('#login', {trigger : true, replace : true});
+           }else if(xhr.status >= 400 && xhr.status < 500){
+               //Hiển thị các lỗi ClientError
+               alert(message);
+           }else if(xhr.status >= 500 && xhr.status < 600){
+               //Hiển thị các lỗi ServerError
+               alert(message);
+           }
+       },

    });
    return Application;

Những Chỉnh sửa chính.

Thêm require

  • Để thực hiện các API liên quan đến đăng nhập, models/user-model.js được require.

Hàm initialize

  • Thiết đặt các hàm bắt lỗi phổ bến bằng ajax( Hàm ajaxErrorHandler)
  • Để tạo và require user-model, login check (loggedinAPI) the execution. - API được chạy qua hàm getLoginUser - Nếu đã đăng nhập, Hàm onLoggedIn sẽ được thực thi. - Nếu chưa đăng nhập, hàm onNotLoggedIn được thực thi. #### ajaxErrorHandler

Những hàm bắt lỗi sẽ được dùng để Handler các lỗi của tất cả các giao tiếp Ajax.

  • Routing tới http status 401 (Unauthorized) nghĩa là (not login), Bằng cách chạy hàm Backbone.history.navigate(), bạn sẽ chuyển tới màn hình login (/#login)
  • Hiển thị một alert thay vì http status.

router.js

router.js
 〜nội dung dài〜
        //Cấu hình routing
        appRoutes : {
+           'login'             : 'login',
            ''                  : 'todoLists',
            'todo-lists'        : 'todoLists',
            'todo-lists/:id'    : 'todoDetail'
        },
 〜nội dung dài〜

Thêm router cho màn hình login

controller.js

controller.js
 //controller
 console.log('load controller');
 define(function() {
    console.log('run controller');
    var TodoController = Marionette.Controller.extend({
+       login : function(){
+           //màn hình login
+           this.nextView('views/login-layout-view', null, true);
+       },

        todoLists : function() {
            //Routing tới view cho Todo layout
            this.nextView('views/todo-layout-view');
        },

        todoDetail : function(id) {
            this.nextView('views/todo-detail-layout-view', {modelId : id});
        },

-       nextView : function(viewPath, option) {
+       nextView : function(viewPath, option, tryShowLoginScreen) {
+           if(window.application.isLoggedIn()){
+               //Đã đăng nhập
+               if(tryShowLoginScreen){
+                   //Nếu bạn đã đăng nhập nhưng vẫn cố chuyển đến trang login
+                   //routed nó đến màn hình TODO list
+                   Backbone.history.navigate('#todo-lists', {trigger: true, replace: true});
+                   return;
+               }
+           }else{
+               //Chưa đăng nhập
+               if(!tryShowLoginScreen){
+                   //Nếu bạn vẫn cố vào khu vực cần phải login 
+                   //route nó đến trang login
+                   Backbone.history.navigate('#login', {trigger: true, replace: true});
+                   return;
+               }
+           }
+           //Hiển thị header
+           this.showHeaderRegion(tryShowLoginScreen);
+           //Hiển thị nội dung
            require([viewPath], function(View){
                window.application.mainRegion.show(new View(option));
            });
        },
-
+       
+       showHeaderRegion : function(tryShowLoginScreen){
+           if(tryShowLoginScreen){
+               //Ẩn header khi chuyển sang trang login
+               window.application.headerRegion.empty();
+           }else if(!window.application.headerRegion.hasView()){
+               //Nếu không chuyển thì hiện header
+               require(['views/header-view'], function(View){
+                   window.application.headerRegion.show(new View({
+                       model : window.application.loginUser
+                   }));
+               });
+           }
+       }
+       
    });
    return TodoController;
 });

Thêm hàmlogin

  • Chạy ở giá trị true sẽ thêm vào hàm nextView tham số tryShowLoginScreen (được mô tả dưới đây)

Hàm nextView

  • Tại hàm nextView sẽ quyết định thêm những logic tại thời điểm login và login thực tế
    • Nó sẽ điều khiển sự hoạt động của 2 patterns sau ( Trạng thái cố chuyển sang trang login thì được định nghĩa ở tham số tryShowLoginScreen).
      • Route tới màn hình TODO list khi đã có sự cố gắng chuyển sang trang đăng nhập của user đã đăng nhập. - Route tới màn hình login hi có sự cố gắng chuyển sang trang TODO list và TODO detail (mà không pahri trang login) của user chưa đăng nhập
  • Trước khi hiển thị view của content (TODO list hoặc trang detail), mình sẽ hiển thị header.

Hàm showHeaderRegion

  • Tại màn hình login mình sẽ ẩn view của header.
  • Khi content hiển thị thì mình sẽ hiển thị view của header.

user-model.js

Cọi thêm các API

user-model.js
//login model
define(function() {
    var LoginModel = Backbone.Model.extend({

        parse : function(response) {
            if(response.message){
                this.loginMessage = response.message;
            }
            return response.User;
        },

        loginMessage : null,

        //Xác định đã đăng nhập chưa
        isLoggedIn : function(){
            return this.get('id') ? true : false;
        },

        //Lấy thông tin user khi đã login
        getLoginUser : function(onLoggedIn, onNotLoggedIn){
            this.urlRoot = '/rest-study/users/loggedin';
            this.fetch(
                {
                    wait : true,
                    success : function(){
                        onLoggedIn();
                    },
                    error : function(){
                        onNotLoggedIn();
                    },
                }
            );
        },

        //Login
        login : function(username, password, onLoginSuccess, onLoginError){
            this.urlRoot = '/rest-study/users/login';
            this.save(
                {
                    username : username, 
                    password : password
                }, {
                    success : function(model){
                        if(model.get('id')){
                            onLoginSuccess(model.loginMessage);
                        }else{
                            onLoginError(model.loginMessage);
                        }
                    },
                }
            );
        },

        //Đăng xuất
        logout : function(onLogoutSuccess){
            this.urlRoot = '/rest-study/users/logout';
            this.save(
                {}, 
                {
                    success : function(model){
                        onLogoutSuccess(model.loginMessage);
                    },
                }
            );
        },

        //Đăng ký ( sign up )
        signup : function(username, password, name, onSignUpSuccess, onSignUpError){
            this.urlRoot = '/rest-study/users/signup';
            this.save(
                {
                    username : username, 
                    password : password,
                    name     : name
                }, {
                    success : function(model){
                        if(model.get('id')){
                            onSignUpSuccess(model.loginMessage);
                        }else{
                            onSignUpError(model.loginMessage);
                        }
                    },
                }
            );
        },
    });
    return LoginModel;
});

hàm parse

Những phản hồi từ server mình cần xử lý như sau

  • Lưu message vào biến loginMessage.
  • User return để lấy thông tin (user information).

Cách thực hiện của mỗi API

  • Chạy một phương thức GET (loggedin API) thực hiện hàm fetch.
  • Chạy một phương thức POST (login,logout, mỗi API của signup chạy hàm save) .
  • URL của API được xác định bằng cách thay đổi giá trị biến urlRoot tại runtime của mỗi hàm.
  • Sau khi nhận phản hồi từ API, hoàn tất các xử lý của view mà có call back những hàm của view.
  • Sẽ có thể có lỗi xảy ra, vì vậy hàm ajaxErrorHandler được set ở app.js sẽ được thực thi, hiện tại nó không làm gì cả.

login-layout-view.js

Thừa kế Marionette.LayoutView để thực hiện

login-layout-view.js
//layout cho view của trang login
define(function(require) {
    var LoginModel = require('models/user-model');

    var LoginLayoutView = Marionette.LayoutView.extend({
        //template
        template : '#login-layout-template',

        //UI page
        ui : {
            username       : '#username',           
            password       : '#password',           
            loginButton    : '#login',              
            signupUsername : '#signup-username',    
            signupPassword : '#signup-password',    
            name           : '#signup-name',        
            signupButton   : '#signup'              
        },

        //event
        events : {
            //Khi click button login
            'click @ui.loginButton' : 'onLoginClick',
            //Khi click button Sign up
            'click @ui.signupButton' : 'onsignupClick',
        },

        //xử lý sự kiện click button login
        onLoginClick : function(){
            //Lấy giá trị trong textbox
            var username = this.ui.username.val();    //username
            var password = this.ui.password.val();    //password
            window.application.loginUser.login(
                username,
                password,
                this.onLoginSuccess,
                this.onLoginError);
        },

        //Callback khi đăng nhập thành công
        onLoginSuccess : function(message){
            Backbone.history.navigate('todo-lists', {trigger: true, replace: true});
            console.log(message);
        },

        //callback khi đăng nhập thất bại
        onLoginError : function(message){
            alert(message);
        },

        //Xử lý event click button sign up
        onsignupClick : function(){
            //Lấy giá trị từ checkbox
            var username = this.ui.signupUsername.val();    //username
            var password = this.ui.signupPassword.val();    //password
            var name = this.ui.name.val();                  //tên
            var userModel = new LoginModel();
            userModel.signup(
                    username,
                    password,
                    name,
                    this.onsignupSuccess,
                    this.onsignupError);
        },

        //Callback khi đăng ký thành công
        onsignupSuccess : function(message){
            alert(message);
        },

        //callback khi đăng ký thất bại
        onsignupError : function(message){
            alert(message);
        },

    });
    return LoginLayoutView;
});

Đây là các cách thực hiện truyền thống

  • Set các phần tử html của mỗi input và button trong ui
  • Tạo các hàm xử lý sự kiện click button ở event
  • Chạy API của mỗi hàm bắt sự kiện Click sử dụng user-model
    • Tại thời điểm login, ( để giữ thông tin user được return trong app.js) và sử dụng user-model còn lại trong app.js.
    • (Kể từ khi thông tin user được return ) trong quá trình đăng ký user, user-model mới được tạo để sử dụng
  • Chạy Callback tại thời điểm nhận được phản hồi từ API, chuyển màn hình và hiển thị các message
    • Tại hàm callback khi đăng nhập thành công chúng ta sẽ chuyển vào màn hình danh sách TODO list.

header-view.js

thừa kế từ Marionette.ItemView để implement

header-view.js
//Header view
define(function(require){
    var UserModel = require('models/user-model');

    var HeaderView = Marionette.ItemView.extend({

        //Tamplate
        template : '#header-template',

        ui : {
            logoutButton : '#logout',
        },

        //set các event hander cho DOM
        events : {
            //khi click log out button
            'click @ui.logoutButton' : 'onLogoutClick',
        },

        onLogoutClick : function(){
            var userModel = new UserModel();
            userModel.logout(this.onLogoutSuccess);
        },

        onLogoutSuccess : function(message){
            window.application.clearLoginUser();
            Backbone.history.navigate('#login', {trigger : true, replace : true});
            console.log(message);
        },

    });
    return HeaderView;
});

Nó cũng giống như việc implementation view truyền thống khác.

  • Thêm button Log out vào biến ui
  • Tại hàmevent mình thêm sự kiện onLogoutClick.
  • Tại hàm onLogoutClick mình chạy API sử dụng user-model
  • Khi phản hồi từ API cho hàm onLogoutSuccess được thực thi thì mình chuyển qua màn hình đăng nhập.

Tổng kết

hãy kiểm tra lại kết quả và fix các lỗi nếu có nhé các bạn.

  • :white_check_mark: app/View/Layouts/default.ctp sửa như hướng dẫn trên.
  • :white_check_mark: app/webroot/js/main.js sửa như hướng dẫn trên.
  • :white_check_mark: app/webroot/js/app.js sửa như hướng dẫn trên.
  • :white_check_mark: app/webroot/js/routers/router.js sửa như hướng dẫn trên.
  • :white_check_mark: app/webroot/js/routers/controller.js sửa như hướng dẫn trên.
  • :white_check_mark: app/webroot/js/models/user-model.js tạo mới như hướng dẫn trên.
  • :white_check_mark: app/webroot/js/views/login-layout-view.jstạo mới như hướng dẫn trên.
  • :white_check_mark: app/webroot/js/views/header-view.jstạo mới như hướng dẫn trên.
  • :white_check_mark: Kiểm tra kết quả.
  • :white_check_mark: Commit lên Git

Link tham khảo với hiển thị Diff trên Github

第5回 Lesson2 クライアントサイド · suzukishouten-study/rest-study@d4ae8a4

Đó là tất cả bài học này.

Đóng góp

Rất mong nhận được những phản hồi/góp ý của các bạn để bài viết tốt hơn, một số phần giải thích hơi lủng củng vì tiếng Nhật của mình không được tốt, rất hi vọng các bạn cùng giúp đỡ để có một series hoàn chỉnh nhất cho những người Việt muốn học backbone.

Cảm ơn các bạn rất nhiều vì đã theo dõi bài viết này.

Ngoài ra các bạn có thể thêm vài chức năng mới như kiểm tra mật khẩu xác nhận của đăng ký, thay đổi 1 tí giao diện, sử dụng thêm các event như keypress để thay vì click mình nhấn enter là chạy , mình đã làm tạm thời nó ra thế này và đang modify thêm vài ý tưởng dị dị nữa, hẹn gặp lại các bạn ở bài 6 :D
ec.PNG

2
2
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2