Back to skills
SKILL.md
Auth Pattern
ASecurityAuth reference สำหรับโปรเจกต์ KAM-WEB-SCHOOLJOB — Dual Auth (Supabase + Prisma), signup/signin flow, useAuthStore interface, role-based guard, redirect pattern ใช้เพื่อลด Token โดยไม่ต้องอ่านโค้ด auth ซ้ำทุก session และป้องกัน bug จาก dual-system mismatch
- 4 stars
- 0 votes
- 0 copies
- 3 views
- Added September 12, 2026
Works with
Security analysis
100/100Pro scans all 2 files and shows the line behind each finding
npx -y skills add aibot88/sec_skill_store --skill auth-pattern --agent claude-codeAre you the author of Auth Pattern?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/aibot88-auth-pattern)---
name: auth-pattern
description: Auth reference สำหรับโปรเจกต์ KAM-WEB-SCHOOLJOB — Dual Auth (Supabase + Prisma), signup/signin flow, useAuthStore interface, role-based guard, redirect pattern ใช้เพื่อลด Token โดยไม่ต้องอ่านโค้ด auth ซ้ำทุก session และป้องกัน bug จาก dual-system mismatch
---
# Auth Pattern — KAM-WEB-SCHOOLJOB
## Architecture: Dual Auth
```
[Client] → POST /api/v1/authenticate/signin
│
├─ 1. Supabase Auth → session token + user.id (uid)
│ ↳ supabase.auth.signInWithPassword({ email, password })
│
├─ 2. Prisma Profile → ดึง role + fullName จาก DB
│ ↳ prisma.profile.findUnique({ where: { userId: uid } })
│
└─ 3. Merge & Return → { user_id, email, role, full_name }
↳ role มาจาก user_metadata (Supabase) || profile.role (Prisma)
```
**หลักสำคัญ:** Supabase จัดการ credential/session, Prisma จัดการ profile data
Prisma failure ≠ auth failure (non-critical — ไม่ throw ถ้า Prisma ล้มเหลว)
---
## File Locations
| ไฟล์ | หน้าที่ |
|------|---------|
| `app/lib/supabase.ts` | Supabase client (ANON key) |
| `app/api/v1/authenticate/signup/route.ts` | POST /api/v1/authenticate/signup |
| `app/api/v1/authenticate/signin/route.ts` | POST /api/v1/authenticate/signin |
| `app/api/v1/authenticate/service/authenticate-service.ts` | AuthenticateService class |
| `app/api/v1/authenticate/validation/authenticate-schema.ts` | Zod schemas |
| `app/stores/auth-store.ts` | Zustand store (localStorage persist) |
| `app/components/layouts/layout-selector.tsx` | Route guard + layout selector |
| `app/pages/signin/_components/signin-form.tsx` | Signin UI + redirect logic |
---
## Supabase Client
```typescript
// app/lib/supabase.ts
import { createClient } from "@supabase/supabase-js";
export const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
);
```
> ใช้ ANON key เท่านั้น (client-side safe)
> SUPABASE_SERVICE_ROLE_KEY ไม่ถูกใช้งานใน codebase ปัจจุบัน
---
## Signup Flow
### Zod Schema
```typescript
// role alias: TEACHER → EMPLOYEE, SCHOOL → EMPLOYER (preprocess)
signupSchema: { email, password (min 6), full_name, role: "EMPLOYEE"|"EMPLOYER"|"ADMIN" }
```
### Service: `authService.signup(input)`
```typescript
// 1. supabase.auth.signUp({ email, password, options: { data: { full_name, role } } })
// → user.id = Supabase UID (uuid)
// → role + full_name บันทึกใน user_metadata ของ Supabase
// 2. prisma.profile.upsert({ where: { userId }, create/update: { email, firstName, role } })
// → Prisma failure ไม่ throw (warn only)
// → password field = "" (ไม่เก็บ password ใน Prisma)
// 3. Return: { id: supabaseUID, email }
```
### API Response (201)
```typescript
{
status_code: 201,
data: { user_id, email, full_name, role }
}
// Error → 400 พร้อม message_th ภาษาไทย
```
### Supabase Error Mapping
| Supabase message | Thai message |
|-----------------|--------------|
| "already registered" | "อีเมลนี้ถูกลงทะเบียนแล้ว" |
| "Password should be at least" | "รหัสผ่านต้องมีความยาวอย่างน้อย 6 ตัวอักษร" |
| "invalid email" | "อีเมลไม่ถูกต้อง" |
| "Invalid login credentials" | "อีเมลหรือรหัสผ่านไม่ถูกต้อง" |
| "Email not confirmed" | "โปรดยืนยันอีเมลของคุณก่อน" |
---
## Signin Flow
### Service: `authService.signin(input)`
```typescript
// 1. supabase.auth.signInWithPassword({ email, password })
// → data.user.id = Supabase UID
// → data.session = access/refresh token
// → data.user.user_metadata = { full_name, role } (บันทึกตอน signup)
// 2. prisma.profile.findUnique({ where: { userId: data.user.id } })
// → ดึง role, firstName เป็น fallback
// → Prisma failure ไม่ throw (warn only)
// 3. Merge priority:
// full_name = user_metadata.full_name || profile.firstName || ""
// role = user_metadata.role || profile.role || "EMPLOYEE"
```
### API Response (200)
```typescript
{
status_code: 200,
data: { user_id, email, role, full_name }
// ❌ ไม่คืน session token ไปฝั่ง client (Supabase จัดการ cookie เอง)
}
// Error → 401
```
---
## useAuthStore Interface
```typescript
// app/stores/auth-store.ts
// Persisted ใน localStorage key: "auth-store"
interface User {
user_id: string; // Supabase UID
email: string;
full_name: string;
role: string; // "EMPLOYEE" | "EMPLOYER" | "ADMIN"
is_first_login?: boolean;
}
interface AuthStore {
user: User | null;
isAuthenticated: boolean;
setUser: (user: User) => void;
setFirstLogin: (isFirst: boolean) => void;
logout: () => void;
}
export const useAuthStore = create<AuthStore>()(persist(..., { name: "auth-store" }));
```
### การใช้งาน
```typescript
// อ่าน user ใน component
const { user, isAuthenticated } = useAuthStore();
// อ่านใน callback / outside component
const user = useAuthStore.getState().user;
// ตั้งค่าหลัง signin สำเร็จ
setUser({ user_id, email, full_name, role, is_first_login: false });
// Logout
useAuthStore.getState().logout();
```
---
## Role-Based Guard Pattern
### Layout-level Guard (`layout-selector.tsx`)
```typescript
// ทำงานบน client-side เท่านั้น (รอ isMounted ก่อน)
// Admin route guard:
if (!user) → redirect "/pages/signin?redirect=<current_path>"
if (user.role !== "ADMIN") → redirect ROLE_HOME[user.role]
// Layouts:
// isAdminRoute || user.role === "ADMIN" → <AdminLayout>
// ทุกกรณีอื่น → <LandingLayout>
```
### Role Home Map
```typescript
const ROLE_HOME: Record<string, string> = {
EMPLOYEE: "/pages/employee/profile",
EMPLOYER: "/pages/employer/profile",
ADMIN: "/pages/admin",
};
```
### Route Guard ใน API (server-side)
```typescript
// ดึง userId จาก query/body → ตรวจ role จาก Prisma
const profile = await prisma.profile.findUnique({ where: { userId } });
if (!profile) return Response.json({ status_code: 401, ... }, { status: 401 });
if (profile.role !== UserRole.EMPLOYER)
return Response.json({ status_code: 403, ... }, { status: 403 });
```
### Role Check ใน Frontend Component
```typescript
const { user } = useAuthStore();
if (user?.role !== "EMPLOYER") return null; // หรือ redirect
// แสดง content เฉพาะ role
{user?.role === "EMPLOYEE" && <ApplyButton />}
```
---
## Signin Redirect Logic
```
1. มี ?redirect=<url> param → decode แล้ว push ไป
2. ไม่มี → ดู role จาก store:
EMPLOYEE → /pages/employee/profile
EMPLOYER → /pages/employer/profile (first_login check)
ADMIN → /pages/admin
```
```typescript
// ตัวอย่าง redirect เมื่อยังไม่ login (protected page)
router.push(`/pages/signin?redirect=${encodeURIComponent(pathname)}`);
```
---
## Common Bugs ที่ต้องระวัง
| สถานการณ์ | Bug | วิธีหลีกเลี่ยง |
|-----------|-----|----------------|
| อ่าน `user` ใน callback | `user` เป็น stale closure | ใช้ `useAuthStore.getState().user` |
| Guard ก่อน hydrate | redirect ผิดพลาด | ตรวจ `isMounted` ก่อนทุกครั้ง |
| Prisma fail ตอน signup | ไม่มี Profile ใน DB | signup ยังสำเร็จ แต่ต้อง handle ใน API ที่ต้องการ profile |
| role จาก Supabase metadata ≠ Prisma | ข้อมูลไม่ตรง | อัปเดตทั้งสองที่เมื่อเปลี่ยน role |
| `user_id` vs `id` | Profile.id ≠ Profile.userId | `userId` = Supabase UID, `id` = Prisma UUID |
---
## Environment Variables (Auth-related)
```env
NEXT_PUBLIC_SUPABASE_URL= # Supabase project URL
NEXT_PUBLIC_SUPABASE_ANON_KEY= # Public anon key (safe for client)
SUPABASE_SERVICE_ROLE_KEY= # Service role (ยังไม่ได้ใช้)
```
Files in this skill
- SKILL.md
- source.json
Attribution
Comments
Loading comments…
Newsletter
Get the weekly list of safe new skills, and the ones we pulled.