Login phần 2, API, Firebase Authentication

Ở bài trước chúng ta đã có thể login bằng cách cài thêm plugin cho Wordpress. Về bản chất, đó là việc dùng Wordpress làm server xử lý thông tin đăng nhập. Wordpress vừa hiển thị giao diện, vừa xử lý thông tin đằng sau. Ở bài này chúng ta sẽ học cách xử dụng Wordpress chỉ để hiển thị giao diện, còn việc xử lý thông tin đằng sau sẽ làm ở 1 server khác. Đây là cách làm khá phổ biến hiện nay. Để cho dễ hiểu, chúng ta có thể tưởng tượng về Iron Man và Iron Legion.

Iron Legion là các cỗ máy được điều khiển từ xa bởi Jarvis/Friday. Các cỗ máy Iron Legion chỉ là 1 cái vỏ bọc không có khả năng tự điều khiển, tuy nhiên lợi thế của nó là có thể nhân bản rất nhiều và chỉ cần 1 bộ điều khiển có thể điều khiển tất cả các cỗ máy.

Với lập trình web, khái niệm frontend và backend dùng để mô tả các concept tương tự. Frontend sẽ là vỏ bọc, chủ yếu hiển thị giao diện. Backend sẽ là bộ điều khiển đằng sau. Frontend có thể không giới hạn cho web, mà còn có thể nói về mobile app. Khi đó, 1 bộ điều khiển Backend có thể sử dụng cho cả web lẫn mobile app. và cách mà Backend và Frontend kết nối với nhau thông qua API.

API & HTTP Request

API (Application programming interface) có thể hiểu đơn giản là "phương thức điều khiển". API là các phương thức được tạo ra có chủ ý để kết nối với bên ngoài. API là khái niệm sử dụng rộng rãi trong lập trình nói chung, cái gì cũng có thể có API. Các phương thức này thường được hiểu là các phương thức kĩ thuật, không phải giao diện người dùng, dùng để kết nối với nhiều bên thứ 3.

Một số ví dụ:

  • Một số ngân hàng ngoài việc cung cấp app + website để chuyển khoản (giao diện người dùng), họ còn cung cấp API để các bên thứ 3 (VD Ví điện tử) kết nối vào và thực hiện việc nạp tiền/chuyển khoản ngay trên ứng dụng thứ 3 mà không cần sử dụng app của ngân hàng.

  • Facebook, Google và nhiều bên social khác, cung cấp API để tích hợp Login bằng Facebook, Google vào các trang web khác.

  • Youtube cung cấp API để nhúng video youtube vào các website khác

  • Telegram cung cấp API để lập trình viên bên thứ 3 có thể tự build 1 phần mềm chat khác bản gốc. VD như Telegram X, Nicegram,..

Như đã nói, API là 1 khái niệm rộng, sử dụng ở các cấp độ giữa nhiều phần tử to (Bank, Facebook, Google, ...), hoặc phần tử bé (backend, frontend, component,...). API thường không ám chỉ cụ thể cách thức, mà mỗi API có cách sử dụng riêng, thường sẽ có 1 văn bản hướng dẫn (gọi là API documentation).

Giờ chúng ta sẽ nói về các trường hợp phổ biến của Web API (tức là API để backend và frontend tương tác với nhau). Với Web API thì thường được thể hiện dưới dạng HTTP Request. Mình giới thiệu 1 số gạch đầu dòng về HTTP Request như sau:

  • Tất cả các website bạn truy cập, (abc.com,...) domain đó đứng sau sẽ là 1 địa chỉ IP address. 1 địa chỉ IP address là 1 địa chỉ của 1 server. 1 server là 1 máy tính được thiết lập chuyên biệt để làm webserver. ( => Máy tính cá nhân của bạn cũng có thể làm server)

  • Khi truy cập vào 1 website, tức là bạn đã gửi 1 request lên server, gọi là http request. Server nhận request và trả lại 1 response. Browser đọc response và hiển thị tương ứng. (Http request thường là cách gọi chung cho cả 1 request + 1 response)

  • Cấu trúc của HTTP Request: URL, Headers, Method, Body

  • Cấu trúc của HTTP Response: Status, Headers, Body

  • Chúng ta có thể dùng Chrome devtool tab Network để hiểu rõ hơn về cấu trúc của 1 HTTP Request

Điểm mấu chốt của cấu trúc HTTP request đó là Header: Content-Type. Cái này có thể có ở cả Request Header lẫn Response Header. Dùng để định dạng Request Body/Response Body, và từ đó xử lý sao cho hợp lý. Như hình ở trên thì Content-Type là text/html, Chrome hiểu được là nhận về HTML và sẽ vẽ giao diện từ mã HTML đó.

Một số content-type thông dụng:

text/html: là mã HTML, mặc định các trình duyệt sẽ đọc mã HTML và vẽ lên giao diện để người dùng sử dụng. Tất cả mọi website đều bắt đầu bằng 1 mã HTML. (Ở trong mã HTML này có thể có nhiều các request (JS, CSS,...) thêm để làm website thêm phong phú)

text/plain: là text thuần túy (loại text mà bạn hay thấy trong .txt)

image/png hoặc image/jpg: là file ảnh

application/octet-stream: là dạng file binary mà sẽ không đọc được bằng trình duyệt, gặp những file này thì trình duyệt có thể sẽ tự động tải về. Thường thấy khi bạn tải 1 file về

application/pdf: file pdf

application/x-www-form-urlencoded: đây là dạng type mặc định khi dùng với form của html. thông thường form khi submit sẽ gửi 1 request application/x-www-form-urlencoded và khiến cho web reload để hiển thị response của request. Các cách xử lý hiện đại sẽ là chặn reload bằng e.preventDefault(); và sau đó tự gọi API bằng JS code.

multipart/form-data: cũng là 1 trong các type của form của html, dùng để upload file.

application/json: là file json, các file định dạng json thường được dùng làm API. JSON thường thuần túy mang dữ liệu và được dùng làm cách thức liên lạc giữa backend và frontend. JSON có cấu trúc rất gần với JS Object, nên đây sẽ là định dạng chính mà bạn sử dụng để code.

Để gọi 1 API từ frontend code, bạn sẽ thực hiện 1 HTTP Request bằng cách sử dụng fetch. Code mẫu như sau:

fetch('https://DOMAIN/api/users/login', {
    method: 'POST', // POST, PUT, DELETE,...
    headers: {
       'Content-Type': 'application/json'
    },
    body: JSON.stringify({ email: '...', password: '...' })
})
.then(res => res.json())
.then(json => {
  console.log('Response body:', json);
})
.catch(err => {
  console.log('error', err);
})

Thông thường khi gọi 1 API thì sẽ nhận được dữ liệu trả về luôn, hoặc sẽ gặp lỗi. các API dạng GET dùng để lấy dữ liệu và thường không có Body. Các API dạng POST dùng để gửi dữ liệu lên cho backend xử lý (dữ liệu thường có dạng là JSON và gửi trong body).

Nội dung trên là lý thuyết và thường thì khi làm việc với 1 Backend developer xịn, bạn sẽ được cung cấp sẵn các đoạn code snippet để sử dụng API luôn mà không cần phải viết thủ công. Hoặc bạn có thể sử dụng các phần mềm như Postman để gen ra đoạn code ở nhiều ngôn ngữ khác nhau

Generate code for a REST API call using Postman in just a few clicks |  Devportal

Login bằng Firebase Authentication

Quay trở lại bài toán, bạn cần làm 1 cổng đăng nhập. Bài trước sử dụng plugin WP là Ultimate Member, bạn cần có WP để chạy được. Bài này sử dụng Firebase Authentication là 1 dịch vụ ngoài, cung cấp API để chúng ta có thể đăng nhập bằng Email hoặc bằng Google (và nhiều bên khác). Bạn không cần dùng WP để kết nối, mà có thể sử dụng HTML thuần túy. (Tức là nếu/khi dùng với WP, bạn tạo 1 block HTML và code như bình thường).

Bước 1: Vào console của firebase tại: https://console.firebase.google.com

Bạn cần 1 tài khoản Google, nếu lần đầu vào nó sẽ hỏi qua mấy thứ, khi hoàn thành hết thì sẽ có giao diện giống như này:

Bước 2: Tạo 1 dự án, sau khi tạo xong thì add 1 web vào

Các bước khá đơn giản và khi vào console của firebase bạn sẽ nhìn thấy luôn. Khi add 1 web vào thì nó sẽ có phần hướng dẫn kết nối như này:

Chọn Use a <script> tag để kết nối. và copy đoạn firebaseConfig

Bước 3: setup Authentication ở sidebar bên trái

Ở đây nó sẽ cho bạn sử dụng rất nhiều các lựa chọn đăng nhập. Phổ biến và dễ kết nối nhất là dùng Email & password và dùng Login Google

Chọn xong và bật nó lên. sau đó thì vô mục settings để thêm domain (chỉ hoạt động trên các domain đc khai báo).

Bước 4: Thêm code vào HTML

<script src="https://www.gstatic.com/firebasejs/8.6.1/firebase-app.js"></script>
<script src="https://www.gstatic.com/firebasejs/8.6.1/firebase-auth.js"></script>

<script>
const firebaseConfig = {
    apiKey: "AIzaSyAuSBjXfXeAKaHX2gfhrbGAHJJ0LKm7Ky4",
    authDomain: "fir-project-5efdc.firebaseapp.com",
    projectId: "fir-project-5efdc",
    storageBucket: "fir-project-5efdc.appspot.com",
    messagingSenderId: "730873134202",
    appId: "1:730873134202:web:67dea549eea8ee4ae38fc3",
    measurementId: "G-LC637F7CKS"
  };
firebase.initializeApp(firebaseConfig);
</script>

Như vậy là firebase đã được thêm vào website của bạn.

Bước 5: Thêm nút bấm vào để kích hoạt sử dụng firebase

Có 2 đoạn code quan trọng của firebase authentication

  • onAuthStateChanged : là listener để lắng nghe khi trạng thái thay đổi. Dùng cả cho các lần vào web tiếp theo khi firebase tự lưu thông tin đăng nhập và đăng nhập lần sau.

  • signInWithPopup(provider): để mở popup login

Demo code HTML:

<!DOCTYPE html>
<html>

<head>
    <title>Demo blog</title>
    <script src="https://www.gstatic.com/firebasejs/8.6.1/firebase-app.js"></script>
    <script src="https://www.gstatic.com/firebasejs/8.6.1/firebase-auth.js"></script>
    <style>
        body {
            font-family: Helvetica, Arial, sans-serif;
        }

        /* style button nicely, with hover change background  */
        button {
            padding: 5px;
            margin: 5px;
            border-radius: 5px;
            border: 1px solid black;
            background-color: white;
            cursor: pointer;
        }

        button:hover {
            background-color: #eee;
        }
    </style>
</head>

<body>
    <h1>Demo login with Firebase Authentication</h1>

    <!-- Firebase Auth -->
    <button id="sign-in-button">Sign in with Google</button>
    <div id="authen-data">
        <h1>Welcome</h1>
    </div>

    <script>
        const firebaseConfig = {
            apiKey: "AIzaSyAuSBjXfXeAKaHX2gfhrbGAHJJ0LKm7Ky4",
            authDomain: "fir-project-5efdc.firebaseapp.com",
            projectId: "fir-project-5efdc",
            storageBucket: "fir-project-5efdc.appspot.com",
            messagingSenderId: "730873134202",
            appId: "1:730873134202:web:67dea549eea8ee4ae38fc3",
            measurementId: "G-LC637F7CKS"
        };

        var state = {
            user: null,
            idToken: null,
        }
        const setState = (obj) => {
            if (obj === state) return;
            state = obj;
            render();
        }

        firebase.initializeApp(firebaseConfig);

        // detect logged in user
        firebase.auth().onAuthStateChanged((user) => {
            console.log('onAuthStateChanged', user);
            if (user) {
                user.getIdToken()
                    .then((idToken) => {
                        console.log('idToken', idToken);
                        setState({ user, idToken });
                    });
            } else {
                setState({ user: null });
            }
        });

        // click button will trigger the popup
        document.getElementById('sign-in-button').addEventListener('click', () => {
            var provider = new firebase.auth.GoogleAuthProvider();
            firebase.auth().signInWithPopup(provider).then((result) => {
                // This gives you a Google Access Token. You can use it to access the Google API.
                var token = result.credential.accessToken;
            }).catch((error) => {
                // Handle Errors here.
                var errorCode = error.code;
                var errorMessage = error.message;
                console.log(errorCode, errorMessage);
            });
        });

        const render = async () => {
            console.log('state', state);
            if (state.user && state.idToken) {
                document.getElementById('sign-in-button').style.display = 'none';
                document.getElementById('authen-data').style.display = 'block';

                // get user info
                const userName = state.user.displayName;
                const userEmail = state.user.email;
                // show in authen-data welcome message
                document.getElementById('authen-data').innerHTML = `
                    <h1>Welcome ${userName}</h1>
                    <p>Email: ${userEmail}</p>
                    <button id="sign-out-button">Sign out</button>
                `;
                // click sign out will sign out 
                document.getElementById('sign-out-button').addEventListener('click', () => {
                    firebase.auth().signOut().then(() => {
                        console.log('sign out success');
                    }).catch((error) => {
                        console.log('sign out error', error);
                    });
                });
            } else {
                document.getElementById('sign-in-button').style.display = 'block';
                document.getElementById('authen-data').style.display = 'none';
            }
        }

        render();
    </script>
</body>

</html>

Sau khi làm xong nó sẽ như thế này:

Khi login thành công dữ liệu sẽ xuất hiện trên Firebase:

Bài tập:

  • Làm lại luồng login này với việc dùng HTML thuần và dùng trên LocalWP

  • Làm thêm login bằng Email + Password