import type { User, LoginResponse, RegisterPayload } from '@/types/auth' // ========================================== // 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('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 { // API returns { code: 200, message: "...", data: { token, user, ... } } const response = await $fetch(`${API_BASE_URL}/auth/login`, { method: 'POST', body: credentials }) if (response && response.data) { const data = response.data // Validation: Ensure user and role exist, then check for Role 'STUDENT' if (!data.user || !data.user.role || 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('auth_profile_loading', () => false) // Init to true if we already have user data in cookie to avoid fetch on every hard refresh const isProfileLoaded = useState('auth_profile_loaded', () => !!user.value) // ฟังก์ชันดึงข้อมูลโปรไฟล์ผู้ใช้ล่าสุด 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(`${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(`${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 || 'เปลี่ยนรหัสผ่านไม่สำเร็จ' } } } // ฟังก์ชันอัปโหลดรูปโปรไฟล์ (Upload Avatar) const uploadAvatar = async (file: File) => { if (!token.value) return { success: false, error: 'ไม่พบ Token การใช้งาน' } // optional: validate เบื้องต้น const allowed = ['image/jpeg', 'image/png', ] if (!allowed.includes(file.type)) { return { success: false, error: 'รองรับเฉพาะไฟล์ jpg/png/' } } const maxMB = 5 if (file.size > maxMB * 1024 * 1024) { return { success: false, error: `ไฟล์ต้องไม่เกิน ${maxMB}MB` } } const formData = new FormData() formData.append('file', file) // ✅ field = file interface AvatarResponse { code: number message: string data: { id: number avatar_url: string } } const doUpload = async () => { return await $fetch(`${API_BASE_URL}/user/upload-avatar`, { method: 'POST', headers: { accept: 'application/json', Authorization: `Bearer ${token.value}` }, body: formData }) } try { let response: AvatarResponse try { response = await doUpload() } catch (err: any) { // ✅ ถ้า 401 ให้ refresh แล้วลองใหม่ 1 ครั้ง if (err?.statusCode === 401) { const refreshed = await refreshAccessToken() if (!refreshed) { logout() return { success: false, error: 'Token หมดอายุ กรุณาเข้าสู่ระบบใหม่' } } response = await doUpload() } else { throw err } } const newUrl = response?.data?.avatar_url // ✅ สำคัญ: re-assign ทั้งก้อน เพื่อให้ cookie/reactivity อัปเดตชัวร์ if (newUrl && user.value) { user.value = { ...user.value, profile: { ...(user.value.profile ?? { prefix: { th: '', en: '' }, first_name: '', last_name: '', phone: null, avatar_url: null }), avatar_url: newUrl } } } // ✅ ถ้าต้องการ sync ให้ตรง server 100% ค่อย force refresh (optional) await fetchUserProfile(true) return { success: true, data: response.data } } catch (err: any) { console.error('Upload avatar failed:', err) return { success: false, error: err.data?.message || err.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 } // ฟังก์ชันส่งอีเมลยืนยันตัวตน const sendVerifyEmail = async () => { if (!token.value) return { success: false, error: 'Token missing' } interface VerifyEmailResponse { code: number message: string } const doSend = async () => { return await $fetch(`${API_BASE_URL}/user/send-verify-email`, { method: 'POST', headers: { Authorization: `Bearer ${token.value}` } }) } try { const res = await doSend() return { success: true, message: res.message, code: res.code } } catch (err: any) { if (err.statusCode === 400) { return { success: false, error: 'Email already verified', code: 400 } } if (err.statusCode === 401) { const refreshed = await refreshAccessToken() if (refreshed) { try { const res = await doSend() return { success: true, message: res.message, code: res.code } } catch (retryErr: any) { return { success: false, error: retryErr.data?.message || 'Failed to send verification email' } } } else { logout() return { success: false, error: 'Session expired' } } } return { success: false, error: err.data?.message || 'Failed to send verification email' } } } // ฟังก์ชันยืนยันอีเมลด้วย Token const verifyEmail = async (token: string) => { try { interface VerifyResponse { code: number message: string } const res = await $fetch(`${API_BASE_URL}/user/verify-email`, { method: 'POST', body: { token } }) // Handle HTTP 200 but logical error if API behaves that way (defensive) if (res && typeof res.code === 'number' && res.code !== 200) { return { success: false, message: res.message, code: res.code, error: res.message } } return { success: true, message: res.message, code: res.code } } catch (err: any) { // Prioritize backend 'code' field, then HTTP status const status = err.data?.code || err.statusCode || err.response?.status || err.status return { success: false, error: err.data?.message || err.message || 'Verification failed', code: status } } } // ฟังก์ชันออกจากระบบ (Logout) const logout = () => { token.value = null refreshToken.value = null // ลบ Refresh Token user.value = null // Reset client-side storage (Keep remembered_email and theme) if (import.meta.client) { const rememberedEmail = localStorage.getItem('remembered_email') const theme = localStorage.getItem('theme') localStorage.clear() if (rememberedEmail) { localStorage.setItem('remembered_email', rememberedEmail) } if (theme) { localStorage.setItem('theme', theme) } } // Clear and Redirect using hard reload for complete state reset if (import.meta.client) { window.location.href = '/auth/login' } else { 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, emailVerifiedAt: user.value.email_verified_at, 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, uploadAvatar, refreshAccessToken, logout, sendVerifyEmail, verifyEmail } }