ไม่มีชื่อบทความ
📑 โครงสร้าง Firestore 2 ก้อน ( Auth vs App )
| Root path | ใช้ทำอะไร | ตัวอย่างคอลเลกชันย่อย |
|---|---|---|
**gohig/auth/** |
เก็บข้อมูลที่ NextAuth-Adapter สร้างอัตโนมัติ | users, accounts, sessions, verificationTokens |
**gohig/app/** |
เก็บข้อมูลธุรกิจของแอป | users, workspaces, workspaces/{wsId}/roles, memberships |
การแยก root ทำให้ ข้อมูล auth กับ business ไม่ปนกัน และสามารถตั้ง Security Rules แยกชุดได้ง่าย
🗂 เลย์เอาต์คอลเลกชันฝั่ง App
gohig/app/
├─ users/{userId} ← โปรไฟล์ business + defaultWorkspace
├─ workspaces/{wsId}
│ ├─ name, permissionScope[]
│ └─ roles/{roleId} ← name, permissions[]
└─ memberships/{membershipId} ← userId, workspaceId, roleId
ข้อบังคับ:
role.permissionsต้องเป็น subset ของworkspace.permissionScope
🔄 งานที่ต้องทำใน NextAuth callbacks
1. jwt callback
เป้าหมาย – โหลด/สร้างเอกสาร
gohig/app/users/{userId}แล้วแนบ defaultWorkspace, ข้อมูล Role+Permission เข้าสู่ JWT
import { FieldValue, getFirestore, doc, getDoc, setDoc,
query, collection, where, getDocs } from 'firebase-admin/firestore';
const db = getFirestore();
const APP_ROOT = ['gohig', 'app']; // helper array
export const authConfig: NextAuthConfig = {
/* … providers / pages / adapter เหมือนเดิม … */
callbacks: {
async jwt({ token, user, account }) {
// ➊ ครั้งแรกที่ sign-in จะมี `user` ติดมา
if (user) {
const uid = user.id;
/* ---------- A) สร้าง/โหลด App-User ---------- */
const userRef = doc(db, ...APP_ROOT, 'users', uid);
let userSnap = await getDoc(userRef);
if (!userSnap.exists()) {
await setDoc(userRef, {
email: user.email ?? null,
createdAt: FieldValue.serverTimestamp(),
defaultWorkspace: null
});
userSnap = await getDoc(userRef);
}
const appUser = userSnap.data()!;
/* ---------- B) หา workspace ปริยาย ---------- */
// ถ้ายังไม่มี default ให้เอาตัวแรกที่พบจาก membership
let defaultWs = appUser.defaultWorkspace as string | null;
if (!defaultWs) {
const mSnap = await getDocs(
query(
collection(db, ...APP_ROOT, 'memberships'),
where('userId', '==', uid)
)
);
defaultWs = mSnap.docs[0]?.data().workspaceId ?? null;
if (defaultWs) {
await setDoc(userRef, { defaultWorkspace: defaultWs }, { merge: true });
}
}
/* ---------- C) โหลด Role / Permission ---------- */
let rolePermissions: string[] = [];
let workspaceScope: string[] = [];
if (defaultWs) {
// 1) membership → roleId
const msSnap = await getDocs(
query(
collection(db, ...APP_ROOT, 'memberships'),
where('userId', '==', uid),
where('workspaceId', '==', defaultWs)
)
);
const mem = msSnap.docs[0]?.data();
if (mem?.roleId) {
// 2) role → permissions[]
const roleRef = doc(
db, ...APP_ROOT, 'workspaces', defaultWs, 'roles', mem.roleId
);
const roleSnap = await getDoc(roleRef);
rolePermissions = roleSnap.exists() ? roleSnap.data()!.permissions : [];
}
// 3) workspace → permissionScope[]
const wsSnap = await getDoc(doc(db, ...APP_ROOT, 'workspaces', defaultWs));
workspaceScope = wsSnap.exists() ? wsSnap.data()!.permissionScope : [];
}
/* ---------- D) ยัดทั้งหมดใส่ token ---------- */
token.appUserId = uid;
token.defaultWs = defaultWs;
token.rolePermissions = rolePermissions;
token.wsScope = workspaceScope;
}
return token;
},
/* ---------- 2. session callback ---------- */
async session({ session, token }) {
session.appUserId = token.appUserId as string;
session.currentWs = token.defaultWs as string | null;
session.permissions = token.rolePermissions as string[];
session.workspaceScope = token.wsScope as string[];
return session;
}
},
session: { strategy: 'jwt' },
secret: process.env.AUTH_SECRET,
};
🔑 สิ่งที่เกิดขึ้น
- ครั้งแรกที่ LINE Login → ไม่มี
gohig/app/users/{uid}→ สร้างพร้อมdefaultWorkspace:null - ถ้ามี
membershipอยู่แล้วจะดึง workspace แรกมาเป็น defaultWorkspace - ทุกครั้งที่ออก JWT จะเติม
permissions+workspaceScope - ฝั่ง Client
useSession()จะได้session.permissionsทันที
🚀 ตัวช่วยเช็กสิทธิ (Server / Client)
export function can(session: any, perm: string) {
return session?.permissions?.includes(perm);
}
API / Server Component
if (!can(session, 'manage_users')) { return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); }UI (React)
{can(session, 'edit_content') && <Editor />}
🛠 สร้าง User/Workspace เองอัตโนมัติ (ทางเลือก)
ถ้าอยาก “สร้าง Workspace ส่วนตัว” ให้ผู้ใช้ตอนแรกเข้า:
- หลังสร้าง
users/{uid}ให้เพิ่มขั้นตอนสร้างworkspaces/{newId} - เพิ่ม
membershipsที่ role =owner - ตั้ง
defaultWorkspace = newId
🔒 แนวคิด Security Rules (Firebase)
match /gohig/app/workspaces/{wsId} {
allow read: if request.auth != null && isMember(wsId);
allow update: if hasPermission(wsId, "manage_workspace");
}
match /gohig/app/workspaces/{wsId}/roles/{roleId} {
allow read: if isMember(wsId);
allow write: if hasPermission(wsId, "manage_roles");
}
isMemberและhasPermissionเป็น custom functions ที่เช็กจาก memberships collection
📌 สรุปสั้น ๆ
Auth collections (
gohig/auth/*) ไม่ต้องแก้อะไร – NextAuth จัดการให้App collections (
gohig/app/*) ใช้เก็บ business-data, role, workspaceใน
jwtcallback- โหลด/สร้าง
gohig/app/users/{uid} - กำหนด
defaultWorkspaceถ้ายังไม่มี - ดึง
role.permissions+workspace.permissionScope - ใส่ทั้งหมดลง token
- โหลด/สร้าง
ใน
sessioncallback ส่งชุด permission กลับไปหน้าเว็บเขียน helper
can()เพื่อเช็กสิทธิได้ทุกที่เสริม Security Rules ให้แน่นหนา
นำแนวทางนี้ไปต่อยอดได้เลย ถ้าอยากได้ตัวอย่างฟังก์ชันสร้าง workspace อัตโนมัติ, UI จัดการ Role, หรือ Firestore Rules แบบเต็ม ๆ ก็บอกมาได้เลยจ้า 🚀