keyboard_tabSkip to content
listVue Firebase Masterclass

ปรับ Environment

สารบัญ

Step แรกเราจะเริ่มย้าย Zone จาก Emulator ไปที่ตัวจริงก่อนที่เราจะ deploy จริงกัน โดยเราจะค่อยๆย้ายไปทีละ service กัน

ปิดการใช้ emulator ผ่าน env ของ Vite

Ref: https://vitejs.dev/guide/env-and-mode.html

ในตัวแปร env ที่ Vite เตรียมมาให้นั้น

  • จะมีตัวแปรที่สามารถแยก DEV และ PROD ได้นั่นคือ import.meta.env.DEV
  • เราจะทำการเพิ่มว่า เราจะใช้ emulator ก็ต่อเมื่ออยู่ใน dev environment เท่านั้น

ที่ src/firebase.js

javascript
import { initializeApp } from 'firebase/app'
import { getFirestore, connectFirestoreEmulator } from 'firebase/firestore'
import { getAuth, connectAuthEmulator } from 'firebase/auth'
import { getStorage, connectStorageEmulator } from 'firebase/storage'
import { getDatabase, connectDatabaseEmulator } from 'firebase/database'

import firebaseConfig from './firebase.json'

const app = initializeApp(firebaseConfig)

const db = getFirestore(app)
const auth = getAuth()
const realtimeDB = getDatabase()
const storage = getStorage()

if (import.meta.env.DEV) {
  // ย้าย emulator มาข้างล่างทั้งหมดแทน
  connectFirestoreEmulator(db, '127.0.0.1', 8080)
  connectAuthEmulator(auth, 'http://127.0.0.1:9099')
  connectStorageEmulator(storage, '127.0.0.1', 9199)
  connectDatabaseEmulator(realtimeDB, '127.0.0.1', 9000)
}

export {
  db,
  auth,
  storage,
  realtimeDB
}

แต่ทั้งนี้เราจะ comment บรรทัด if (import.meta.env.DEV) { ออกไปก่อนเพื่อทดสอบการเรียก service จริงที่ Firebase ก่อน

ซึ่งเมื่อเราเอาออก = จะเจอว่า ทุก service จะเรียกไปยัง Firebase แทน Emulator แทน เราจะมาไล่ทำให้ทุก service ใช้งานได้อย่างถูกต้องกัน

Firebase Authentication

สิ่งที่ต้องทำ

  • ไปเปิด provider google และ email ใน Firebase Authentication ใน Firebase ขึ้นมา

environment-01

environment-02

เสร็จแล้วลองทดสอบโดยการกด login ผ่านเว็บดูว่าเป็น login google จริงเรียบร้อยแล้ว

environment-03

** ถ้าขึ้นเป็น error ว่า เว็บยังไม่ได้ authorized ไม่ต้องตกใจ เราต้องเพิ่ม whitelist ตอนขึ้น domain จริงอยู่ดี

Cloud storage

สิ่งที่ต้องทำ

  • จริงๆแล้ว Cloud storage เราไม่จำเป็นต้อง config อะไรเพิ่มแล้วก็สามารถ upload ขึ้นได้เลย
  • แต่เพื่อความปลอดภัย เราจะนำ security rule ของ Cloud storage (จาก storage.rules ที่เราทำกันบน local) ไปวางไว้ใน Cloud storage

environment-04

เมื่อใส่เรียบร้อย ให้ลอง Cloud storage (upload profile) ดูว่าสามารถขึ้นไปยัง Cloud storage ถูกต้องแล้วหรือไม่

environment-05

ถ้าสามารถ upload ขึ้น Cloud storage ได้ (เหมือนตัวอย่างนี้) = Cloud storage ติดตั้งแล้วเรียบร้อย ไ

Cloud Firestore

สิ่งที่ต้องทำ

  • เช่นเดียวกันกับ Cloud storage เราไม่จำเป็นต้อง config อะไรเพิ่มก็สามารถต่อเข้ากับ Firestore ได้
  • แต่เพื่อความปลอดภัย เราจะนำ security rule ของ Firestore (ใน firestore.rules) นำไปใส่ Rules ของ Firestore ใน Firebase

environment-06

  • ลอง Firestore (เปิดหน้าเว็บ) ว่าขึ้นจาก Firestore จริงแล้วหรือยัง (ใส่ข้อมูลทดสอบสักตัวใน Firestore) แล้วเปิดขึ้นมาดู

environment-07

Realtime Database

สิ่งที่ต้องทำ

  • เช่นเดียวกันกับ Cloud Firestore เราไม่จำเป็นต้อง config อะไรเพิ่มก็สามารถต่อเข้ากับ Realtime database ได้
  • แต่เพื่อความปลอดภัย เราจะนำ security rule ของ Realtime database (ใน database.rules.json) นำไปใส่ Rules ของ Realtime database ใน Firebase

environment-08

จากนั้นลองเพิ่มข้อมูล stats (ที่ใช้เฉพาะฝั่ง admin เข้าไปดู)

environment-09

จากนั้นให้ลองเพิ่ม admin เข้าไปสักคนดู เช่น เคสนี้เพิ่ม [email protected] เข้าไปใน method email ผ่าน Firebase Authentication

environment-10

เมื่อ login เข้ามาที่ฝั่ง admin ก็จะเจอ Error 2 อย่างขึ้นมา

  1. Menu ฝั่งซ้ายไม่ขึ้น (หรือข้อมูลไม่สามารถดึงมาได้) เนื่องจากยังไม่ใช่ admin
  2. Permission ไม่พอที่จะดู stats ของ Realtime DB ได้

environment-11

สำหรับเคสที่ 1 เราสามารถไปแก้ได้ผ่าน Firestore ได้เลย ด้วยการเข้าไปแก้ role ที่ข้อมูลของคนนั้น (เหมือนกับตอน Firestore Emulator เลย)

environment-12

ทีนี้ก็จะเจอว่า เมนูขึ้นมาและข้อมูลอย่าง user, products ก็จะสามารถขึ้นมาได้

environment-18

ทีนี้ ปัญหาอย่างหนึ่งของการ set ตัว Custom Claims ใน Firebase จริง (ตัว isAdmin ที่เราดักไว้ใน Realtime Database) มีอยู่อย่างหนึ่งคือ "ไม่สามารถทำผ่าน UI ของ Firebase ได้"

  • ดังนั้นเราจำเป็นต้องทำผ่านคำสั่งของ Firebase ผ่านตัว Cloud functions แทน = เราต้องให้ Cloud functions ต่อเข้าไปใน Firebase จริงเพื่อเพิ่มเรื่องนี้

Cloud function

Ref: https://firebase.google.com/docs/admin/setup

สิ่งที่ต้องทำ

  • เราจะทำการ load service-account.json มาใส่ cloud function - เปลี่ยนมา run cloud functions เฉพาะ service cloud functions แทน
  • ทำการเพิ่ม isAdmin custom ด้วย API /set-admin ที่สร้างมาเพิ่มใหม่โดยการแกะผ่าน Token ของ Firebase Authentication

เริ่มต้นมา load service-account.json จาก Firebase

environment-13

จากนั้นนำ service-account.json มาวางไว้สักตำแหน่งในเครื่อง (แนะนำให้วางไว้ในตำแหน่งเดียวกับ functions)

** file นี้สำคัญมาก หากไฟล์นี้หลุดไป = สามารถ control Firebase ทั้งหมดของเราได้เลย ดังนั้น เพื่อความแน่ใจเพิ่ม .gitignore เข้าไปด้วยเพื่อป้องกันการ push ขึ้น git ไป

environment-19

จากนั้นทำการเพิ่ม path service-account.json เข้าไปผ่าน environment ในเครื่อง โดยต้องระบุเป็น path เต็ม (สามารถดูตรงหัวข้อ "To set the environment variable" ใน Ref เพิ่มเติมได้)

(สำหรับ MacOS / Linux)

bash
export GOOGLE_APPLICATION_CREDENTIALS="/home/user/Downloads/functions/service-account-file.json"

(สำหรับ Windows - เปิด Powershell)

bash
$env:GOOGLE_APPLICATION_CREDENTIALS="C:\Users\username\Downloads\functions\service-account-file.json"

ให้พิมพ์ลง command ไปได้เลย เช่นแบบนี้ (อันนี้เป็นตัวอย่างที่ใช้ได้สำหรับ MacOS และ Linux)

environment-20

เมื่อแสดงค่าออกมาแบบนี้ได้ถือว่าถูกต้องแล้ว จากนั้นที่ functions/firebaseConfig.js ทำการเพิ่มการเรียก applicationDefault() ออกมา คำสั่งนี้ไอเดียของมันคือ

  • ถ้า run emulator ครบชุด = จะใช้ config emulator
  • ถ้า run เฉพาะ cloud function = จะใช้ credential จาก GOOGLE_APPLICATION_CREDENTIALS เพื่อเรียกใช้งาน Firebase
javascript
const { initializeApp, applicationDefault } = require('firebase-admin/app') // เพิ่ม applicationDefault เข้ามา
const { getFirestore } = require('firebase-admin/firestore')
const { getDatabase } = require('firebase-admin/database')
const { getAuth } = require('firebase-admin/auth')

// เปลี่ยน path realtime database ให้ไปใช้ path จริงแทน
let databaseURL  ='https://<project-name>-default-rtdb.asia-southeast1.firebasedatabase.app'

initializeApp({
  projectId: 'easy-commerce-workshop',
  credential: applicationDefault(),  // เพิ่ม applicationDefault เข้ามา
  databaseURL
})

const db = getFirestore()
const auth = getAuth() // เพิ่ม auth เข้ามา
const realtimeDB = getDatabase()

module.exports = {
  db,
  auth, // ส่ง auth ออกไป
  realtimeDB
}

จากนั้นให้เปลี่ยนมา run ด้วย firebase emulators:start --only functions แทน

environment-15

จากนั้น ทำการทดสอบกับ Firestore ก่อนว่าสามารถต่อเข้าไปได้หรือไม่ โดยการเพิ่ม api 1 ตัวที่ functions/index.js ชื่อ /api/test โดยทำการ list user ทั้งหมดออกมา

javascript
// เพิ่ม api นี้เข้ามา
app.get('/test', async (req, res) => {
  const userRef = db.collection('users')
  const userSnapshot = await userRef.get()

  const statRef = realtimeDB.ref('stats')
  const statSnapshot = await statRef.get()
  res.json({
    users: userSnapshot.docs.map(user => user.data()),
    stats: statSnapshot.val()
  })
})

แล้วลองยิงดู ถ้าเทียบกับ Firestore แล้วข้อมูลออกมาถูกต้อง (ไม่ได้มาจาก Emulator) = ต่อไปถูกต้องแล้ว

environment-21

  • ทำการเพิ่ม path /set-admin โดยการรับ token ผ่าน header เข้ามา
javascript
// import auth เพิ่มเข้ามา
const { db, auth, realtimeDB } = require('./firebaseConfig.js')

app.get('/set-admin', async (req, res) => {
  try {
    const idToken = req.headers.authorization
    let userUid = ''

    console.log('idToken', idToken)
    if (idToken) {
      // ทำการ decodedToken ออกมา
      const decodedToken = await auth.verifyIdToken(idToken)
      userUid = decodedToken.uid
      console.log('userUid', userUid)
      // add for set customClaim in Firebase Authentication
      await auth.setCustomUserClaims(userUid, { isAdmin: true })
    }

    res.json({
      message: `${userUid} is admin right now!`
    })
  } catch (error) {
    res.status(404).json({
      message: error.message
    })
  }
})

จากนั้นไปที่หน้าเว็บ ทำการ log idToken เพิ่มจาก src/stores/account.js

javascript
export const useAccountStore = defineStore('user-account', {
  state: () => ({
    /* ... */
  }),
  actions: {
    async checkAuthState () {
      return new Promise((resolve) => {
        onAuthStateChanged(auth, async (user) => {
          try {
            if (user) {
              this.user = user
              this.isLoggedIn = true
              const docRef = doc(db, 'users', user.uid)
              const docSnap = await getDoc(docRef)

              if (!docSnap.exists()) {
                const userData = {
                  name: user.displayName,
                  role: 'member',
                  status: 'active',
                  updatedAt: new Date()
                }
                await setDoc(docRef, userData)
                this.profile = userData
              } else {
                this.profile = docSnap.data()
              }

              this.profile.email = user.email

              if (this.profile.role !== 'member') {
                this.isAdmin = true
                // เพิ่มจุด log idToken เข้ามา
                const idToken = await auth.currentUser.getIdToken(true)
                console.log('idToken', idToken)
              }
              resolve(true)
            } else {
              resolve(false)
            }
          } catch (error) {
            console.log('error', error)
            resolve(false)
          }
        })
      })
    },
  }
})

จากนั้นเมื่อเปิดหน้าเว็บมาอีกรอบ

environment-14

เอา idToken นี้ไปยิงใส่ API /set-admin ถ้ามี message ยืนยัน admin = เป็น admin ได้เรียบร้อย

environment-16

เมื่อกลับมาเปิดหน้าเว็บอีกที จะสามารถดึงข้อมูลจาก Realtime Database ออกมาได้และไม่เกิด Error ออกมาแล้ว

environment-17

เท่านี้ก็จะพบว่าสามารถใช้งานได้ครบทุกหน้าแล้ว (สามารถ test webhook เพิ่มเติมก่อนได้ว่ายิง order เข้าแล้วถูกต้องหรือไม่เพิ่มเติมได้)