Skip to main content

ปรับ 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

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 เข้าไปสักคนดู เช่น เคสนี้เพิ่ม admin2@test.com เข้าไปใน 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)

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

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

$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
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 ทั้งหมดออกมา

// เพิ่ม 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 เข้ามา
// 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

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 เข้าแล้วถูกต้องหรือไม่เพิ่มเติมได้)