dev_zone_auth
v2.0.1
Published
Middleware for Dev zone page and Dev zone API
Downloads
267
Readme
dev_zone_auth
Package dùng chung cho các app Avada / Megaplaza để bảo vệ trang/route /dev_zone bằng Firebase Google SSO (chỉ cho phép email @avada / @megaplaza) và log mọi request đi qua API /dev_zone lên BigQuery.
Package gồm 2 phần độc lập:
| Phần | Chạy ở | Vai trò | |---|---|---| | Client | Browser (React) | Provider auth + guard route + UI login + UserMenu | | Server | Node / Firebase Functions (Koa) | Middleware verify Firebase ID token + tracking BigQuery |
Toàn bộ ví dụ dưới đây lấy trực tiếp từ app blogs (SeoOn Blog) — bạn có thể sao chép nguyên xi rồi đổi đường dẫn / tên cho phù hợp với app đích.
Mục lục
- Cài đặt
- Entry points & cách import
- Phần Client — React
- Phần Server — Koa middleware
- BigQuery tracking
- Restriction domain email
- Service account & Firebase config
- Checklist tích hợp app mới
- Dev / Build package
- Troubleshooting
1. Cài đặt
# Version mới nhất trên npm
cd packages/assets
yarn add dev_zone_auth
cd packages/functions
yarn add dev_zone_authPeer dependencies
Consumer app phải có sẵn:
{
"@shopify/polaris": ">=13.0.0",
"@shopify/polaris-icons": ">=6.0.0",
"firebase": ">=10.0.0",
"firebase-admin": ">=12.0.0",
"react": ">=17.0.0",
"react-dom": ">=17.0.0",
"react-router-dom": "^5.0.1"
}
firebase-adminlà optional — chỉ cần khi consumer dùng phầnserver.
2. Entry points & cách import
Package publish 3 entry qua field exports trong package.json:
| Import path | Chạy ở | Re-export |
|---|---|---|
| dev_zone_auth | — | toàn bộ client + server |
| dev_zone_auth/client | Browser | AuthPage, DevAuthGuard, DevZoneAuth, UserMenu, useAuth, useAuthToast, type AuthContextValue |
| dev_zone_auth/server | Node / Firebase Functions | authenticate, authenticateForMissingToken |
Quy tắc: Code chạy browser chỉ import từ
dev_zone_auth/client, code Node chỉ import từdev_zone_auth/server. Đừng import từ root (dev_zone_auth) trong code FE — sẽ kéo cảfirebase-adminvào bundle.
3. Phần Client — React
Quy trình tích hợp vào blogs/packages/assets gồm 4 bước: khai báo env → tạo file config → bọc AuthPage → dùng useAuth trong trang DevZone.
3.1. Khai báo firebaseConfig riêng cho DevZone
DevZone dùng Firebase project riêng (hiện tại là seoon-image-optimizer-staging), không dùng chung với Firebase của app (blogs dùng avada-blog-staging). Mỗi app cần đặt biến env riêng để không xung đột.
packages/assets/.env.development — thêm 7 biến:
# DevZone Firebase (shared across Avada/Megaplaza apps for /dev_zone auth)
VITE_DEVZONE_FIREBASE_API_KEY=AIzaSyDz-N1Wl9TbgQExtcHUd6l4iLXvhd3UrRg
VITE_DEVZONE_FIREBASE_AUTH_DOMAIN=seoon-image-optimizer-staging.firebaseapp.com
VITE_DEVZONE_FIREBASE_PROJECT_ID=seoon-image-optimizer-staging
VITE_DEVZONE_FIREBASE_STORAGE_BUCKET=seoon-image-optimizer-staging.appspot.com
VITE_DEVZONE_FIREBASE_MESSAGING_SENDER_ID=68856638145
VITE_DEVZONE_FIREBASE_APP_ID=1:68856638145:web:e1495b4fdb3b653de00240
VITE_DEVZONE_FIREBASE_MEASUREMENT_ID=G-D37BSMJN5Npackages/assets/src/config/devZoneAuth.js — đọc env ra object:
export const devZoneFirebaseConfig = {
apiKey: import.meta.env.VITE_DEVZONE_FIREBASE_API_KEY,
authDomain: import.meta.env.VITE_DEVZONE_FIREBASE_AUTH_DOMAIN,
projectId: import.meta.env.VITE_DEVZONE_FIREBASE_PROJECT_ID,
storageBucket: import.meta.env.VITE_DEVZONE_FIREBASE_STORAGE_BUCKET,
messagingSenderId: import.meta.env.VITE_DEVZONE_FIREBASE_MESSAGING_SENDER_ID,
appId: import.meta.env.VITE_DEVZONE_FIREBASE_APP_ID,
measurementId: import.meta.env.VITE_DEVZONE_FIREBASE_MEASUREMENT_ID
};3.2. Bọc <AuthPage> trong layout
packages/assets/src/layouts/AppTranslate.js:
import {AppProvider} from '@shopify/polaris';
import {Router} from 'react-router-dom';
import {AuthPage, UserMenu} from 'dev_zone_auth/client';
import {devZoneFirebaseConfig} from '@assets/config/devZoneAuth';
import {history} from '@assets/history';
import Routes from '@assets/routes/routes';
function AppTranslate({isEmbedApp = false, shop}) {
return (
<AppProvider i18n={...} linkComponent={ReactRouterLink}>
<Router history={history}>
{/* các provider khác: QueryClient, UpgradePlan, MainLayout, ... */}
<AuthPage isProduction={true} firebaseConfig={devZoneFirebaseConfig}>
<UserMenu />
<Routes />
</AuthPage>
</Router>
</AppProvider>
);
}Yêu cầu vị trí trong tree:
<AuthPage>phải nằm trong<Router>/<BrowserRouter>— bên trong dùnguseLocation()để check pathname.<AuthPage>phải nằm trong<AppProvider>của Polaris — login page vàUserMenudùng Polaris components.firebaseConfigphải stable — khai báo ngoài component hoặcuseMemo. Nếu reference thay đổi mỗi render thì auth instance sẽ re-create.
3.3. UserMenu — Avatar + Sign out
Render fixed ở top: 12, right: 12, zIndex: 999. Click avatar → popover hiện tên + email + nút Sign out.
Không nhận props. Tự ẩn nếu user === null (chưa login hoặc đang ở mode isProduction=false).
Đặt trực tiếp bên trong <AuthPage> trước <Routes /> như ví dụ mục 3.2.
3.4. Dùng useAuth trong trang DevZone
packages/assets/src/pages/DevZone/index.jsx:
import {useAuth} from 'dev_zone_auth/client';
function DevZone() {
const {
userState: {user: userFirebase}
} = useAuth();
// userFirebase: { uid, email, displayName, photoURL, ... }
return <div>Hello {userFirebase?.displayName}</div>;
}
export default DevZone;Gọi
useAuth()phải nằm trong cây<AuthPage>, ngoài sẽ throwuseAuth must be used within an AuthProvider.
Full return của useAuth:
interface AuthContextValue {
userState: { user: User | null; isAuthenticated: boolean };
isLoading: boolean;
isSigningIn: boolean;
signInWithGoogle: () => Promise<void>;
signOut: () => Promise<void>;
}3.5. Gọi API có auth từ FE
FE phải đính kèm Firebase ID token vào header x-auth-firebase-token cho mọi request đi qua route được middleware authenticate bảo vệ. Lấy token bằng userFirebase.getIdToken() — userFirebase là user trả về từ hook useAuth(), đã được sign bởi đúng project Firebase DevZone.
packages/assets/src/pages/DevZone/index.jsx:
import {useAuth} from 'dev_zone_auth/client';
import api from '@assets/helpers/api';
function DevZone() {
const {
userState: {user: userFirebase}
} = useAuth();
const handleDevZone = async (data = {}, field) => {
try {
const idToken = await userFirebase.getIdToken();
await api(`/dev_zone?type=${field}`, {
headers: {
'x-auth-firebase-token': idToken
},
body: {data},
method: 'PUT'
});
} catch (e) {
// ...
}
};
}Token Firebase ID hết hạn 1h — luôn gọi
getIdToken()ngay trước khi request, đừng cache.
⚠️ Phải dùng
userFirebasetừuseAuth(), không dùngauth.currentUsercủa app bình thường — hai Firebase project khác nhau, token sẽ fail khi server verify.
3.6. Tham khảo props
<AuthPage>
| Prop | Type | Bắt buộc | Default | Mô tả |
|---|---|---|---|---|
| children | React.ReactNode | ✅ | — | Cây component bên trong (thường là <Routes/>) |
| isProduction | boolean | ⚠️ Nên truyền | true | false → bypass hoàn toàn Firebase auth (local dev). true → chạy thật onAuthStateChanged + Google popup |
| firebaseConfig | FirebaseOptions | ✅ | — | Firebase web config của project DevZone. Bắt buộc có projectId; package cache auth instance theo projectId |
Các export khác từ dev_zone_auth/client
| Export | Loại | Mô tả |
|---|---|---|
| DevAuthGuard | Component | Guard route (đã lồng sẵn trong AuthPage, chỉ import khi tự build provider) |
| DevZoneAuth | Component | Trang login Google (auto render khi chưa authen) |
| UserMenu | Component | Avatar + popover Sign out |
| useAuth | Hook | State + action auth |
| useAuthToast | Hook | State toast (cho login page tự build) |
| AuthContextValue | Type | Type return của useAuth |
4. Phần Server — Koa middleware
4.1. Chuẩn bị service account DevZone
Service account phải là của project Firebase DevZone (không phải của app blogs). Lấy từ GCP Console của project DevZone → IAM → Service Accounts → tạo key JSON.
Quyền tối thiểu:
| Quyền | Dùng để |
|---|---|
| firebaseauth.admin (hoặc Firebase Authentication Admin) | Verify Firebase ID token (adminAuth.verifyIdToken) |
| bigquery.dataEditor | Insert row + tự tạo dataset dev_zone_logs.ui_events khi cần |
Đặt file trong blogs:
packages/functions/serviceAccount.devzone.jsonThêm vào .gitignore (của blogs, không commit):
serviceAccount.devzone.json
serviceAccount*.json4.2. Khai báo devZoneAuthDefaults
Vì authenticate / authenticateForMissingToken đều nhận cùng 1 bộ options trong cùng 1 app, gom vào 1 constant để dùng chung:
packages/functions/src/routes/api.js:
import Router from 'koa-router';
import {authenticate, authenticateForMissingToken} from 'dev_zone_auth/server';
import app from '@functions/config/app';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const devZoneServiceAccount = require('../../serviceAccount.devzone.json');
const devZoneAuthDefaults = {
appId: 'seoon-blog', // ghi vào cột app_id BigQuery
isProduction: true, // true = verify thật (xem lưu ý bên dưới)
serviceAccount: devZoneServiceAccount
};Lưu ý
isProduction: true: Trong blogs hiện set cứngtruecho cả dev lẫn prod → route/dev_zoneluôn bắt token kể cả local. Nếu muốn bypass khi dev, đổi thànhisProduction: app.isProduction.
4.3. authenticate(options) — bắt buộc token
Route chỉ cho phép user đã login DevZone vào.
Flow
isProduction === false→next()luôn (bypass).- Đọc header
x-auth-firebase-token.- Không có →
401{success: false, message: "Missing authentication token"}.
- Không có →
adminAuth.verifyIdToken(token)(cache Firebase Admin app theoproject_id).- Email không chứa
@avada/@megaplaza→403Access denied. - Pass → fire-and-forget
trackEvent(...)lên BigQuery →await next(). - Lỗi:
auth/id-token-expired/auth/id-token-revoked/auth/argument-error→401Invalid or expired token.- Khác →
500Internal server error.
Ví dụ (blogs)
router.put('/dev_zone', authenticate(devZoneAuthDefaults), devZoneController.update);4.4. authenticateForMissingToken(options) — opportunistic
Khác authenticate: không có token → vẫn next(). Chỉ verify + log khi client có gửi token.
Dùng cho route public muốn tracking nếu user đã login DevZone, không bắt buộc.
Flow
isProduction === false→next()luôn.- Không có header →
next()luôn (không log). - Có token → verify, log BigQuery (cột
action = ""), rồinext(). - Lỗi (kể cả token sai) → log error →
next(). Tuyệt đối không chặn request.
Ví dụ (blogs)
router.post(
'/article/create',
authenticateForMissingToken(devZoneAuthDefaults),
articleController.create
);
router.post(
'/shop',
jsonType,
authenticateForMissingToken(devZoneAuthDefaults),
shopController.updateShop
);5. BigQuery tracking
Mỗi request qua authenticate (và authenticateForMissingToken khi có token) sẽ insert 1 row vào bảng <project>.dev_zone_logs.ui_events trong project của service account.
Schema
| Column | Type | Nội dung |
|---|---|---|
| app_id | STRING | options.appId (vd "seoon-blog") |
| user_id | STRING | Firebase UID |
| email | STRING | Email user |
| action | STRING | JSON.stringify(ctx.query). Với authenticateForMissingToken luôn là "" |
| path | STRING | ctx.path |
| method | STRING | HTTP method |
| metadata | STRING | JSON.stringify(ctx.request.body) (hoặc null) |
| created_at | TIMESTAMP | ISO timestamp |
Field action
Middleware không fix cứng key nào — JSON.stringify toàn bộ ctx.query:
| Request | action được log |
|---|---|
| /api/dev_zone?x=click | {"x":"click"} |
| /api/dev_zone?action=view&page=home | {"action":"view","page":"home"} |
| /api/dev_zone (không query) | {} |
Auto-create dataset & table
Dataset dev_zone_logs + table ui_events tự tạo khi insert lần đầu nếu chưa tồn tại — xem createDatasetAndTable trong src/server/bigquery.ts. Service account phải có quyền bigquery.dataEditor.
6. Restriction domain email
Hard-code trong src/pages/AuthPage.tsx và src/server/middleware.ts:
const ALLOWED_DOMAINS = ["@avada", "@megaplaza"];Email không chứa một trong 2 chuỗi này sẽ bị reject:
- Client: hiện toast
Access denied. Only Avada email accounts are allowed. - Server: trả
403 Access denied.
Thêm domain mới → sửa cả 2 chỗ trong package rồi yarn build lại.
7. Service account & Firebase config
Package không bundle service account hay firebase config. Mỗi consumer phải tự cung cấp.
Client — firebaseConfig
| Nguồn | Khuyến nghị khi |
|---|---|
| Env vars (VITE_DEVZONE_FIREBASE_*) | Mặc định — mỗi env (dev/staging/prod) có thể dùng project khác nhau |
| Hardcode trong file config/devZoneAuth.js | Chỉ khi chắc chắn 1 project duy nhất cho mọi env |
Firebase web config (apiKey, appId…) không phải secret theo Firebase — có thể commit vào source, nhưng tốt nhất vẫn đi qua env để linh hoạt.
Server — serviceAccount
| Nguồn | Khuyến nghị khi |
|---|---|
| File JSON (require('../../serviceAccount.devzone.json')) | Deploy truyền thống, file sync qua secret store / manual copy. Phải thêm vào .gitignore |
| Env var chứa JSON | Firebase Functions / Cloud Run — JSON.parse(process.env.DEVZONE_SA_JSON) |
| Google Secret Manager | Có infra Secret Manager sẵn — load lúc boot |
⚠️ Không commit service account vào git.
Đa project trong cùng process
Package cache theo project_id:
- Firebase Admin app theo
serviceAccount.project_id - BigQuery client theo
serviceAccount.project_id - Firebase client (browser) theo
firebaseConfig.projectId
→ Nhiều app cùng chạy trong 1 Node process với SA khác nhau vẫn an toàn, không đè lên nhau.
8. Checklist tích hợp app mới
Client
- [ ]
yarn add dev_zone_auth@^2.0.0(hoặcfile:.../ npm version) - [ ] Thêm 7 biến
VITE_DEVZONE_FIREBASE_*vào.env.development(và.env.productionnếu có) - [ ] Tạo
src/config/devZoneAuth.jsexportdevZoneFirebaseConfig - [ ] Bọc
<AuthPage firebaseConfig={devZoneFirebaseConfig} isProduction={...}>trong<Router>+<AppProvider> - [ ] Đặt
<UserMenu />và<Routes />bên trong<AuthPage> - [ ] Có ít nhất 1 route có pathname chứa
/dev_zone - [ ] Dùng
useAuth()trong trang DevZone nếu cầnuser.email/user.uid - [ ] Mọi
fetchđi qua route được guard: thêm headerx-auth-firebase-token: <idToken>
Server
- [ ] Copy
serviceAccount.devzone.json(của project DevZone) vàopackages/functions/ - [ ] Thêm
serviceAccount*.jsonvào.gitignore - [ ] Service account có quyền
firebaseauth.admin+bigquery.dataEditor - [ ] Khai báo
devZoneAuthDefaultsđầu fileroutes/api.jsvớiappIdcủa app - [ ]
authenticate(devZoneAuthDefaults)cho route bắt buộc auth - [ ]
authenticateForMissingToken(devZoneAuthDefaults)cho route public có tracking - [ ] Cân nhắc
isProduction: app.isProductionđể bypass khi dev local
9. Dev / Build package
yarn dev # vite playground ở thư mục dev/
yarn build # build tsup → dist/
yarn watch # build watch
yarn lint # eslint --fix
yarn test # jestSau khi sửa source
avada-auth→yarn build→ restart consumer (Firebase Functions / Koa app / Vite dev server) để loaddistmới. Node module cache không tự refresh.
10. Troubleshooting
| Triệu chứng | Nguyên nhân hay gặp |
|---|---|
| Trang /dev_zone luôn hiện login dù đã login | AuthPage không nằm trong <Router> → useLocation lỗi, hoặc email không match @avada/@megaplaza |
| Dev local cũng bị bắt Google sign-in | isProduction không truyền / truyền true cho AuthPage |
| useAuth must be used within an AuthProvider | Component gọi useAuth() không nằm trong <AuthPage> |
| API trả 401 Missing authentication token | FE chưa gắn header x-auth-firebase-token, hoặc giá trị rỗng |
| API trả 401 Invalid or expired token | Token đã hết hạn (>1h) — FE phải gọi lại getIdToken() trước mỗi request |
| API trả 403 Access denied | Token hợp lệ nhưng email không thuộc domain whitelist |
| BigQuery không có row mới | 1) Consumer chưa restart sau khi rebuild dist; 2) request bị chặn 401/403 trước khi tới trackEvent; 3) SA thiếu quyền BigQuery; 4) void trackEvent bị cắt khi Cloud Function kết thúc quá nhanh — xem log [error bigquery.trackEvent] |
| dist/client / dist/server không tồn tại | Chưa chạy yarn build sau khi clone/install |
| Bundle FE bị kéo firebase-admin → fail build | Đang import từ root dev_zone_auth thay vì dev_zone_auth/client |
| [dev_zone_auth] serviceAccount is missing project_id/... | Object serviceAccount truyền vào không hợp lệ — check lại path require / nội dung file |
| [dev_zone_auth] firebaseConfig.projectId is required | Env VITE_DEVZONE_FIREBASE_PROJECT_ID rỗng — check .env và restart Vite |
| Token verify thất bại với auth/argument-error | FE đang dùng Firebase project khác với SA server → user login vào project A nhưng server verify bằng SA của project B. Đảm bảo firebaseConfig FE và serviceAccount BE cùng project DevZone |
