N

Nokfa Docs

ไม่มีชื่อบทความ

📑 โครงสร้าง 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,
};

🔑 สิ่งที่เกิดขึ้น

  1. ครั้งแรกที่ LINE Login → ไม่มี gohig/app/users/{uid} → สร้างพร้อม defaultWorkspace:null
  2. ถ้ามี membership อยู่แล้วจะดึง workspace แรกมาเป็น defaultWorkspace
  3. ทุกครั้งที่ออก JWT จะเติม permissions + workspaceScope
  4. ฝั่ง 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


📌 สรุปสั้น ๆ

  1. Auth collections (gohig/auth/*) ไม่ต้องแก้อะไร – NextAuth จัดการให้

  2. App collections (gohig/app/*) ใช้เก็บ business-data, role, workspace

  3. ใน jwt callback

    • โหลด/สร้าง gohig/app/users/{uid}
    • กำหนด defaultWorkspace ถ้ายังไม่มี
    • ดึง role.permissions + workspace.permissionScope
    • ใส่ทั้งหมดลง token
  4. ใน session callback ส่งชุด permission กลับไปหน้าเว็บ

  5. เขียน helper can() เพื่อเช็กสิทธิได้ทุกที่

  6. เสริม Security Rules ให้แน่นหนา

นำแนวทางนี้ไปต่อยอดได้เลย ถ้าอยากได้ตัวอย่างฟังก์ชันสร้าง workspace อัตโนมัติ, UI จัดการ Role, หรือ Firestore Rules แบบเต็ม ๆ ก็บอกมาได้เลยจ้า 🚀