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



## :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](http://qiita.com/Takumi_Kasuga/items/4b49fce119597005c655). 

: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回】マニュアル](http://qiita.com/k_shimoji/items/ae5a02bd0c7e03feae52) 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](http://gigazine.net/news/20140430-origin-of-log-in/) 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](https://qiita-image-store.s3.amazonaws.com/0/94375/b407e74b-ac1d-e213-c6fe-0dbe5a6d0002.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](https://qiita-image-store.s3.amazonaws.com/0/94375/c2ca85e5-7584-1777-591d-403a726effc9.png)


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

![detail.PNG](https://qiita-image-store.s3.amazonaws.com/0/94375/bf1e751e-1865-836d-7c64-381445adb74c.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](http://qiita.com/k_shimoji/items/6a66dbb93b0553fa7c7f) 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](https://qiita-image-store.s3.amazonaws.com/2006/65988/91df8b24-25b7-e0bd-6f87-6657a1d949f6.png "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](https://qiita-image-store.s3.amazonaws.com/0/94375/116dce1d-1ea9-0724-16ac-c4cac52208ae.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](https://qiita-image-store.s3.amazonaws.com/0/94375/119265e7-7f7d-4990-05fc-e969912ad3c2.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 

```diff: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

```diff: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ý.

```php: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ý

```json: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 `user` và `message`.

#### 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.

```php: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](https://qiita-image-store.s3.amazonaws.com/0/94375/0f1a1459-4539-37d3-bb08-e567bd93f5a4.png)


|URL|Request JSON | response code | response JSON |
|---|:----------------------------------------|--------|:----------------------|
|/rest-study/users/signup.json|{<br>　"username": "test2",<br>　"password": "test2",<br>　"name": "test2"<br>}|200|{<br>　"User": {<br>　　"username": "test2",<br>　　"name": "test２",<br>　　"id": "65"<br>　},<br>　"message": "Registered. You can log in!"<br>}
|/rest-study/users/login.json|{<br>　"username":"test",<br>　"password":"test"<br>}|200|{<br>　"User": {<br>　　"id": "1",<br>　　"username": "test",<br>　　"name": "test"<br>　},<br>　"message": "Login successfully"<br>}                    |
|/rest-study/users/login.json|{<br>　"username":"test",<br>　"password":"hoge"<br>}|200|{<br>"message": "Wrong user name or password"<br>}                    |
|/rest-study/users/logout.json| - |200|{<br>　"message": "Log out"<br>}|
|/rest-study/users/loggedin.json| - |200|{<br>　"User": {<br>　　"id": "1",<br>　　"username": "test",<br>　　"name": "Test"<br>　},<br>　"message": "You are already logged in"<br>}                    

- 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|{<br>　"name": "Unauthorized",<br>　"message": "Unauthorized",<br>　"url": "/rest-study/users/loggedIn.json"<br>}|

- 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|{<br>　"name": "Unauthorized",<br>　"message": "Unauthorized",<br>　"url": "/rest-study/users/todo_lists.json"<br>}|

### 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](https://github.com/suzukishouten-study/rest-study/commit/59c29b052dc9599002680a891987f914b83bfa38?diff=split)

Đế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](https://qiita-image-store.s3.amazonaws.com/0/94375/82a9d366-411b-e177-c711-f345fc1eb530.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](https://qiita-image-store.s3.amazonaws.com/0/94375/05274efc-ab12-e246-e51f-4a2cbcd5d65c.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.
- 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.
6. Sau khi nhận phản hồi từ server, XHR chạy hàm callback như bước 2.
7. 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

```diff: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

```diff: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óa `application.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

```diff: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 (`loggedin`API) 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

```diff: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

```diff: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àm`login`

- 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

```js: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

```js: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

```js: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àm`event` 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.js`tạo mới như hướng dẫn trên.
- :white_check_mark: `app/webroot/js/views/header-view.js`tạ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](https://github.com/suzukishouten-study/rest-study/commit/d4ae8a42ca598eb37fe84e4af34f17017da9be30)

Đó 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](https://qiita-image-store.s3.amazonaws.com/0/94375/e3d994e5-168e-b09b-9a72-207b203302bc.png)