ปรับ 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 ขึ้นมา
เสร็จแล้วลองทดสอบโดยการกด login ผ่านเว็บดูว่าเป็น login google จริงเรียบร้อยแล้ว
** ถ้าขึ้นเป็น error ว่า เว็บยังไม่ได้ authorized ไม่ต้องตกใจ เราต้องเพิ่ม whitelist ตอนขึ้น domain จริงอยู่ดี
Cloud storage
สิ่งที่ต้องทำ
- จริงๆแล้ว Cloud storage เราไม่จำเป็นต้อง config อะไรเพิ่มแล้วก็สามารถ upload ขึ้นได้เลย
- แต่เพื่อความปลอดภัย เราจะนำ security rule ของ Cloud storage (จาก
storage.rules
ที่เราทำกันบน local) ไปวางไว้ใน Cloud storage
เมื่อใส่เรียบร้อย ให้ลอง Cloud storage (upload profile) ดูว่าสามารถขึ้นไปยั ง Cloud storage ถูกต้องแล้วหรือไม่
ถ้าสามารถ upload ขึ้น Cloud storage ได้ (เหมือนตัวอย่างนี้) = Cloud storage ติดตั้งแล้วเรียบร้อย ไ
Cloud Firestore
สิ่งที่ต้องทำ
- เช่นเดียวกันกับ Cloud storage เราไม่จำเป็นต้อง config อะไรเพิ่มก็สามารถต่อเข้ากับ Firestore ได้
- แต่เพื่อความปลอดภัย เราจะนำ security rule ของ Firestore (ใน
firestore.rules
) นำไปใส่ Rules ของ Firestore ใน Firebase
- ลอง Firestore (เปิดหน้าเว็บ) ว่าขึ้นจาก Firestore จริงแล้วหรือยัง (ใส่ข้อมูลทดสอบสักตัวใน Firestore) แล้วเปิดขึ้นมาดู
Realtime Database
สิ่งที่ต้องทำ
- เช่นเดียวกันกับ Cloud Firestore เราไม่จำเป็นต้อง config อะไรเพิ่มก็สามารถต่อเข้ากับ Realtime database ได้
- แต่เพื่อความปลอดภัย เราจะนำ security rule ของ Realtime database (ใน
database.rules.json
) นำไปใส่ Rules ของ Realtime database ใน Firebase
จากนั้นลองเพิ่มข้อมูล stats (ที่ใช้เฉพาะฝั่ง admin เข้าไปดู)
จากนั้นให้ลองเพิ่ม admin เข้าไปสักคนดู เช่น เคสนี้เพิ่ม [email protected] เข้าไปใน method email ผ่าน Firebase Authentication
เมื่อ login เข้ามาที่ฝั่ง admin ก็จะเจอ Error 2 อย่างขึ้นมา
- Menu ฝั่งซ้ายไม่ขึ้น (หรือข้อมูลไม่สามารถดึงมาได้) เนื่องจากยังไม่ใช่ admin
- Permission ไม่พอที่จะดู stats ขอ ง Realtime DB ได้
สำหรับเคสที่ 1 เราสามารถไปแก้ได้ผ่าน Firestore ได้เลย ด้วยการเข้าไปแก้ role ที่ข้อมูลของคนนั้น (เหมือนกับตอน Firestore Emulator เลย)
ทีนี้ก็จะเจอว่า เมนูขึ้นมาและข้อมูลอย่าง user, products ก็จะสามารถขึ้นมาได้
ทีนี้ ปัญหาอย่างหนึ่งของการ 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
จากนั้นนำ service-account.json มาวางไว้สักตำแหน่งในเครื่อง (แนะนำให้วางไว้ในตำแหน่งเดียวกับ functions
)
** file นี้สำคัญมาก หากไฟล์นี้หลุดไป = สามารถ control Firebase ทั้งหมดของเราได้เลย ดังนั้น เพื่อความแน่ใจเพิ่ม .gitignore
เข้าไปด้วยเพื่อป้องกันการ push ขึ้น git ไป
จากนั้นทำการเพิ่ม 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)
เมื่อแสดงค่าออกมาแบบนี้ได้ถือว่าถูกต้องแล้ว จากนั้นที่ 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
แทน
จากนั้น ทำการทดสอบกับ 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) = ต่อไปถูกต้องแล้ว
- ทำการเพิ่ม 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)
}
})
})
},
}
})
จากนั้นเมื่อเปิดหน้าเว็บมาอีกรอบ
เอา idToken นี้ไปยิงใส่ API /set-admin
ถ้ามี message ยืนยัน admin = เป็น admin ได้เรียบร้อย
เมื่อกลับมาเปิดหน้าเว็บอีกที จะสามารถดึงข้อมูลจาก Realtime Database ออกมาได้และไม่เกิด Error ออกมาแล้ว
เท่านี้ก็จะพบว่าสามารถใช้งานได้ครบทุกหน้าแล้ว (สามารถ test webhook เพิ่มเติมก่อนได้ว่ายิง order เข้าแล้วถูกต้องหรือไม่เพิ่มเติมได้)