elearning/Frontend-Learner/composables/useAuth.ts

319 lines
10 KiB
TypeScript

// Interface สำหรับข้อมูลผู้ใช้งาน (User)
interface User {
id: number
username: string
email: string
created_at?: string
updated_at?: string
role: {
code: string // เช่น 'STUDENT', 'INSTRUCTOR', 'ADMIN'
name: { th: string; en: string }
}
profile?: {
prefix: { th: string; en: string }
first_name: string
last_name: string
phone: string | null
avatar_url: string | null
}
}
// Interface สำหรับข้อมูลตอบกลับตอน Login
interface loginResponse {
token: string
refreshToken: string
user: User
profile: User['profile']
}
// Interface สำหรับข้อมูลที่ใช้ลงทะเบียน
interface RegisterPayload {
username: string
email: string
password: string
first_name: string
last_name: string
prefix: { th: string; en: string }
phone: string
}
// ==========================================
// Composable: useAuth
// หน้าที่: จัดการระบบ Authentication และ Authorization
// - จัดการ Login/Logout/Register
// - จัดการ Token (Access Token & Refresh Token)
// - เก็บ State ของผู้ใช้ปัจจุบัน (User State)
// ==========================================
export const useAuth = () => {
const config = useRuntimeConfig()
const API_BASE_URL = config.public.apiBase as string
// Cookie สำหรับเก็บ Access Token (หมดอายุ 1 วัน)
const token = useCookie('auth_token', {
maxAge: 60 * 60 * 24, // 1 day
sameSite: 'lax',
secure: false // ควรเป็น true ใน production (HTTPS)
})
// Cookie สำหรับเก็บข้อมูล User (หมดอายุ 7 วัน)
const user = useCookie<User | null>('auth_user_data', {
maxAge: 60 * 60 * 24 * 7, // 1 week
sameSite: 'lax',
secure: false
})
// Computed property เช็คว่า Login อยู่หรือไม่
const isAuthenticated = computed(() => !!token.value)
// Cookie สำหรับเก็บ Refresh Token (หมดอายุ 7 วัน)
const refreshToken = useCookie('auth_refresh_token', {
maxAge: 60 * 60 * 24 * 7, // 7 days (ตรงกับ API)
sameSite: 'lax',
secure: false
})
// ฟังก์ชันเข้าสู่ระบบ (Login)
const login = async (credentials: { email: string; password: string }) => {
try {
const data = await $fetch<loginResponse>(`${API_BASE_URL}/auth/login`, {
method: 'POST',
body: credentials
})
if (data) {
// Validation: อนุญาตเฉพาะ Role 'STUDENT' เท่านั้น
if (data.user.role.code !== 'STUDENT') {
return { success: false, error: 'Email ไม่ถูกต้อง' }
}
token.value = data.token
refreshToken.value = data.refreshToken // บันทึก Refresh Token
// API ส่งข้อมูล profile มาใน user object
user.value = data.user
return { success: true }
}
return { success: false, error: 'No data returned' }
} catch (err: any) {
console.error('Login failed:', err)
return {
success: false,
error: err.data?.message || err.message || 'เข้าสู่ระบบไม่สำเร็จ'
}
}
}
// ฟังก์ชันลงทะเบียน (Register)
const register = async (payload: RegisterPayload) => {
try {
const data = await $fetch(`${API_BASE_URL}/auth/register-learner`, {
method: 'POST',
body: payload
})
return { success: true, data }
} catch (err: any) {
console.error('Register failed:', err)
return {
success: false,
error: err.data?.error?.message || err.data?.message || err.message || 'ลงทะเบียนไม่สำเร็จ'
}
}
}
// Shared state สำหรับเช็คว่ากำลังโหลดโปรไฟล์อยู่หรือไม่ เพื่อป้องกันการยิงซ้อน
const isProfileLoading = useState<boolean>('auth_profile_loading', () => false)
const isProfileLoaded = useState<boolean>('auth_profile_loaded', () => false)
// ฟังก์ชันดึงข้อมูลโปรไฟล์ผู้ใช้ล่าสุด
const fetchUserProfile = async (forceRefresh = false) => {
// ถ้าไม่มี Token หรือกำลังโหลดอยู่ หรือโหลดข้อมูลมาแล้ว (และไม่ได้สั่ง Refresh) ให้ข้าม
if (!token.value || isProfileLoading.value) return
if (isProfileLoaded.value && !forceRefresh) return
isProfileLoading.value = true
try {
const data = await $fetch<User>(`${API_BASE_URL}/user/me`, {
headers: {
Authorization: `Bearer ${token.value}`
}
})
if (data) {
user.value = data
isProfileLoaded.value = true
}
} catch (error: any) {
// กรณี Token หมดอายุ (401)
if (error.statusCode === 401) {
const refreshed = await refreshAccessToken()
if (refreshed) {
try {
const retryData = await $fetch<User>(`${API_BASE_URL}/user/me`, {
headers: {
Authorization: `Bearer ${token.value}`
}
})
if (retryData) {
user.value = retryData
isProfileLoaded.value = true
return
}
} catch (retryErr) {
console.error('Failed to fetch user profile after refresh:', retryErr)
}
} else {
logout()
}
} else {
console.error('Failed to fetch user profile:', error)
}
} finally {
isProfileLoading.value = false
}
}
// ฟังก์ชันอัปเดตข้อมูลโปรไฟล์
const updateUserProfile = async (payload: {
first_name: string
last_name: string
phone: string
prefix: { th: string; en: string }
}) => {
if (!token.value) return
try {
await $fetch(`${API_BASE_URL}/user/me`, {
method: 'PUT',
headers: {
Authorization: `Bearer ${token.value}`
},
body: payload
})
// หากสำเร็จ ให้ดึงข้อมูลโปรไฟล์ล่าสุดมาอัปเดตใน State
await fetchUserProfile()
return { success: true }
} catch (err: any) {
console.error('Failed to update profile:', err)
return { success: false, error: err.data?.message || err.message || 'บันทึกข้อมูลไม่สำเร็จ' }
}
}
const requestPasswordReset = async (email: string) => {
try {
await $fetch(`${API_BASE_URL}/auth/reset-request`, {
method: 'POST',
body: { email }
})
return { success: true }
} catch (err: any) {
return { success: false, error: err.data?.message || 'ส่งคำขอไม่สำเร็จ' }
}
}
const confirmResetPassword = async (payload: { token: string; password: string }) => {
try {
await $fetch(`${API_BASE_URL}/auth/reset-password`, {
method: 'POST',
body: payload
})
return { success: true }
} catch (err: any) {
return { success: false, error: err.data?.message || 'รีเซ็ตรหัสผ่านไม่สำเร็จ' }
}
}
const changePassword = async (payload: { oldPassword: string, newPassword: string }) => {
if (!token.value) return { success: false, error: 'ไม่พบ Token การใช้งาน' }
try {
await $fetch(`${API_BASE_URL}/user/change-password`, {
method: 'POST',
headers: {
Authorization: `Bearer ${token.value}`
},
body: payload
})
return { success: true }
} catch (err: any) {
return { success: false, error: err.data?.message || 'เปลี่ยนรหัสผ่านไม่สำเร็จ' }
}
}
// ฟังก์ชันขอ Access Token ใหม่ด้วย Refresh Token
const refreshAccessToken = async () => {
if (!refreshToken.value) return false
try {
const data = await $fetch<{ token: string; refreshToken: string }>(`${API_BASE_URL}/auth/refresh`, {
method: 'POST',
body: { refreshToken: refreshToken.value }
})
if (data) {
token.value = data.token
refreshToken.value = data.refreshToken
return true
}
} catch (err) {
// Refresh failed (เช่น Refresh Token หมดอายุ) ให้ Force Logout
logout()
return false
}
return false
}
// ฟังก์ชันออกจากระบบ (Logout)
const logout = () => {
token.value = null
refreshToken.value = null // ลบ Refresh Token
user.value = null
const router = useRouter()
router.push('/auth/login')
}
return {
isAuthenticated,
token,
user,
currentUser: computed(() => {
if (!user.value) return null
// Helper ในการดึงข้อมูล user ที่อาจซ้อนกันอยู่หลายชั้น
const prefix = user.value.profile?.prefix?.th || ''
const firstName = user.value.profile?.first_name || user.value.username
const lastName = user.value.profile?.last_name || ''
return {
prefix: user.value.profile?.prefix || { th: '', en: '' },
firstName,
lastName,
email: user.value.email,
phone: user.value.profile?.phone || '',
photoURL: user.value.profile?.avatar_url || '',
role: user.value.role,
createdAt: user.value.created_at || new Date().toISOString()
}
}),
login,
register,
fetchUserProfile,
updateUserProfile,
requestPasswordReset,
confirmResetPassword,
changePassword,
refreshAccessToken,
logout
}
}