2026-02-24 11:12:26 +07:00
|
|
|
import type { User, LoginResponse, RegisterPayload } from '@/types/auth'
|
2026-01-13 10:46:40 +07:00
|
|
|
|
2026-01-23 09:54:35 +07:00
|
|
|
// ==========================================
|
|
|
|
|
// Composable: useAuth
|
|
|
|
|
// หน้าที่: จัดการระบบ Authentication และ Authorization
|
|
|
|
|
// - จัดการ Login/Logout/Register
|
|
|
|
|
// - จัดการ Token (Access Token & Refresh Token)
|
|
|
|
|
// - เก็บ State ของผู้ใช้ปัจจุบัน (User State)
|
|
|
|
|
// ==========================================
|
2026-01-13 10:46:40 +07:00
|
|
|
export const useAuth = () => {
|
2026-01-14 15:15:31 +07:00
|
|
|
const config = useRuntimeConfig()
|
|
|
|
|
const API_BASE_URL = config.public.apiBase as string
|
2026-01-23 09:54:35 +07:00
|
|
|
|
|
|
|
|
// Cookie สำหรับเก็บ Access Token (หมดอายุ 1 วัน)
|
2026-01-13 10:46:40 +07:00
|
|
|
const token = useCookie('auth_token', {
|
2026-01-14 15:15:31 +07:00
|
|
|
maxAge: 60 * 60 * 24, // 1 day
|
|
|
|
|
sameSite: 'lax',
|
2026-01-23 09:54:35 +07:00
|
|
|
secure: false // ควรเป็น true ใน production (HTTPS)
|
2026-01-14 15:15:31 +07:00
|
|
|
})
|
|
|
|
|
|
2026-01-23 09:54:35 +07:00
|
|
|
// Cookie สำหรับเก็บข้อมูล User (หมดอายุ 7 วัน)
|
2026-01-14 15:15:31 +07:00
|
|
|
const user = useCookie<User | null>('auth_user_data', {
|
2026-01-13 10:46:40 +07:00
|
|
|
maxAge: 60 * 60 * 24 * 7, // 1 week
|
|
|
|
|
sameSite: 'lax',
|
2026-01-14 15:15:31 +07:00
|
|
|
secure: false
|
2026-01-13 10:46:40 +07:00
|
|
|
})
|
|
|
|
|
|
2026-01-23 09:54:35 +07:00
|
|
|
// Computed property เช็คว่า Login อยู่หรือไม่
|
2026-01-13 10:46:40 +07:00
|
|
|
const isAuthenticated = computed(() => !!token.value)
|
|
|
|
|
|
2026-01-23 09:54:35 +07:00
|
|
|
// Cookie สำหรับเก็บ Refresh Token (หมดอายุ 7 วัน)
|
2026-01-14 15:15:31 +07:00
|
|
|
const refreshToken = useCookie('auth_refresh_token', {
|
2026-01-23 09:54:35 +07:00
|
|
|
maxAge: 60 * 60 * 24 * 7, // 7 days (ตรงกับ API)
|
2026-01-14 15:15:31 +07:00
|
|
|
sameSite: 'lax',
|
|
|
|
|
secure: false
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
2026-01-22 11:04:57 +07:00
|
|
|
|
2026-01-23 09:54:35 +07:00
|
|
|
// ฟังก์ชันเข้าสู่ระบบ (Login)
|
2026-01-14 15:15:31 +07:00
|
|
|
const login = async (credentials: { email: string; password: string }) => {
|
|
|
|
|
try {
|
2026-02-02 11:11:25 +07:00
|
|
|
// API returns { code: 200, message: "...", data: { token, user, ... } }
|
|
|
|
|
const response = await $fetch<any>(`${API_BASE_URL}/auth/login`, {
|
2026-01-14 15:15:31 +07:00
|
|
|
method: 'POST',
|
|
|
|
|
body: credentials
|
|
|
|
|
})
|
|
|
|
|
|
2026-02-02 11:11:25 +07:00
|
|
|
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') {
|
2026-01-14 16:47:29 +07:00
|
|
|
return { success: false, error: 'Email ไม่ถูกต้อง' }
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-21 10:02:37 +07:00
|
|
|
token.value = data.token
|
2026-01-23 09:54:35 +07:00
|
|
|
refreshToken.value = data.refreshToken // บันทึก Refresh Token
|
2026-01-14 15:15:31 +07:00
|
|
|
|
2026-01-23 09:54:35 +07:00
|
|
|
// API ส่งข้อมูล profile มาใน user object
|
2026-01-21 10:02:37 +07:00
|
|
|
user.value = data.user
|
2026-01-14 15:15:31 +07:00
|
|
|
|
|
|
|
|
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 || 'เข้าสู่ระบบไม่สำเร็จ'
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-23 09:54:35 +07:00
|
|
|
// ฟังก์ชันลงทะเบียน (Register)
|
2026-01-14 15:15:31 +07:00
|
|
|
const register = async (payload: RegisterPayload) => {
|
|
|
|
|
try {
|
2026-01-22 10:33:05 +07:00
|
|
|
const data = await $fetch(`${API_BASE_URL}/auth/register-learner`, {
|
2026-01-14 15:15:31 +07:00
|
|
|
method: 'POST',
|
|
|
|
|
body: payload
|
|
|
|
|
})
|
|
|
|
|
|
2026-01-21 10:02:37 +07:00
|
|
|
return { success: true, data }
|
2026-01-14 15:15:31 +07:00
|
|
|
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
console.error('Register failed:', err)
|
|
|
|
|
return {
|
|
|
|
|
success: false,
|
2026-01-15 09:45:51 +07:00
|
|
|
error: err.data?.error?.message || err.data?.message || err.message || 'ลงทะเบียนไม่สำเร็จ'
|
2026-01-14 15:15:31 +07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-27 10:42:13 +07:00
|
|
|
// Shared state สำหรับเช็คว่ากำลังโหลดโปรไฟล์อยู่หรือไม่ เพื่อป้องกันการยิงซ้อน
|
|
|
|
|
const isProfileLoading = useState<boolean>('auth_profile_loading', () => false)
|
2026-01-29 11:09:29 +07:00
|
|
|
// Init to true if we already have user data in cookie to avoid fetch on every hard refresh
|
|
|
|
|
const isProfileLoaded = useState<boolean>('auth_profile_loaded', () => !!user.value)
|
2026-01-27 10:42:13 +07:00
|
|
|
|
2026-01-23 09:54:35 +07:00
|
|
|
// ฟังก์ชันดึงข้อมูลโปรไฟล์ผู้ใช้ล่าสุด
|
2026-01-27 10:42:13 +07:00
|
|
|
const fetchUserProfile = async (forceRefresh = false) => {
|
|
|
|
|
// ถ้าไม่มี Token หรือกำลังโหลดอยู่ หรือโหลดข้อมูลมาแล้ว (และไม่ได้สั่ง Refresh) ให้ข้าม
|
|
|
|
|
if (!token.value || isProfileLoading.value) return
|
|
|
|
|
if (isProfileLoaded.value && !forceRefresh) return
|
2026-01-14 15:15:31 +07:00
|
|
|
|
2026-01-27 10:42:13 +07:00
|
|
|
isProfileLoading.value = true
|
2026-01-14 15:15:31 +07:00
|
|
|
try {
|
2026-01-21 10:02:37 +07:00
|
|
|
const data = await $fetch<User>(`${API_BASE_URL}/user/me`, {
|
2026-01-14 15:15:31 +07:00
|
|
|
headers: {
|
|
|
|
|
Authorization: `Bearer ${token.value}`
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
2026-01-21 10:02:37 +07:00
|
|
|
if (data) {
|
|
|
|
|
user.value = data
|
2026-01-27 10:42:13 +07:00
|
|
|
isProfileLoaded.value = true
|
2026-01-21 10:02:37 +07:00
|
|
|
}
|
|
|
|
|
} catch (error: any) {
|
2026-01-23 09:54:35 +07:00
|
|
|
// กรณี Token หมดอายุ (401)
|
2026-01-21 10:02:37 +07:00
|
|
|
if (error.statusCode === 401) {
|
|
|
|
|
const refreshed = await refreshAccessToken()
|
|
|
|
|
if (refreshed) {
|
|
|
|
|
try {
|
|
|
|
|
const retryData = await $fetch<User>(`${API_BASE_URL}/user/me`, {
|
2026-01-15 10:30:40 +07:00
|
|
|
headers: {
|
|
|
|
|
Authorization: `Bearer ${token.value}`
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
2026-01-21 10:02:37 +07:00
|
|
|
if (retryData) {
|
|
|
|
|
user.value = retryData
|
2026-01-27 10:42:13 +07:00
|
|
|
isProfileLoaded.value = true
|
2026-01-15 10:30:40 +07:00
|
|
|
return
|
|
|
|
|
}
|
2026-01-21 10:02:37 +07:00
|
|
|
} catch (retryErr) {
|
|
|
|
|
console.error('Failed to fetch user profile after refresh:', retryErr)
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
logout()
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
console.error('Failed to fetch user profile:', error)
|
2026-01-14 15:15:31 +07:00
|
|
|
}
|
2026-01-27 10:42:13 +07:00
|
|
|
} finally {
|
|
|
|
|
isProfileLoading.value = false
|
2026-01-14 15:15:31 +07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-23 09:54:35 +07:00
|
|
|
// ฟังก์ชันอัปเดตข้อมูลโปรไฟล์
|
2026-01-16 10:03:04 +07:00
|
|
|
const updateUserProfile = async (payload: {
|
|
|
|
|
first_name: string
|
|
|
|
|
last_name: string
|
|
|
|
|
phone: string
|
|
|
|
|
prefix: { th: string; en: string }
|
|
|
|
|
}) => {
|
|
|
|
|
if (!token.value) return
|
|
|
|
|
|
|
|
|
|
try {
|
2026-01-21 10:02:37 +07:00
|
|
|
await $fetch(`${API_BASE_URL}/user/me`, {
|
2026-01-16 10:03:04 +07:00
|
|
|
method: 'PUT',
|
|
|
|
|
headers: {
|
|
|
|
|
Authorization: `Bearer ${token.value}`
|
|
|
|
|
},
|
|
|
|
|
body: payload
|
|
|
|
|
})
|
|
|
|
|
|
2026-01-23 09:54:35 +07:00
|
|
|
// หากสำเร็จ ให้ดึงข้อมูลโปรไฟล์ล่าสุดมาอัปเดตใน State
|
2026-01-16 10:03:04 +07:00
|
|
|
await fetchUserProfile()
|
|
|
|
|
|
|
|
|
|
return { success: true }
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
console.error('Failed to update profile:', err)
|
|
|
|
|
return { success: false, error: err.data?.message || err.message || 'บันทึกข้อมูลไม่สำเร็จ' }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-14 15:15:31 +07:00
|
|
|
const requestPasswordReset = async (email: string) => {
|
|
|
|
|
try {
|
2026-01-21 10:02:37 +07:00
|
|
|
await $fetch(`${API_BASE_URL}/auth/reset-request`, {
|
2026-01-14 15:15:31 +07:00
|
|
|
method: 'POST',
|
|
|
|
|
body: { email }
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return { success: true }
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
return { success: false, error: err.data?.message || 'ส่งคำขอไม่สำเร็จ' }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-15 11:06:44 +07:00
|
|
|
const confirmResetPassword = async (payload: { token: string; password: string }) => {
|
2026-01-14 15:15:31 +07:00
|
|
|
try {
|
2026-01-21 10:02:37 +07:00
|
|
|
await $fetch(`${API_BASE_URL}/auth/reset-password`, {
|
2026-01-14 15:15:31 +07:00
|
|
|
method: 'POST',
|
|
|
|
|
body: payload
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return { success: true }
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
return { success: false, error: err.data?.message || 'รีเซ็ตรหัสผ่านไม่สำเร็จ' }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-16 10:03:04 +07:00
|
|
|
const changePassword = async (payload: { oldPassword: string, newPassword: string }) => {
|
|
|
|
|
if (!token.value) return { success: false, error: 'ไม่พบ Token การใช้งาน' }
|
|
|
|
|
|
|
|
|
|
try {
|
2026-01-21 10:02:37 +07:00
|
|
|
await $fetch(`${API_BASE_URL}/user/change-password`, {
|
2026-01-16 10:03:04 +07:00
|
|
|
method: 'POST',
|
|
|
|
|
headers: {
|
|
|
|
|
Authorization: `Bearer ${token.value}`
|
|
|
|
|
},
|
|
|
|
|
body: payload
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return { success: true }
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
return { success: false, error: err.data?.message || 'เปลี่ยนรหัสผ่านไม่สำเร็จ' }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-28 11:54:20 +07:00
|
|
|
// ฟังก์ชันอัปโหลดรูปโปรไฟล์ (Upload Avatar)
|
|
|
|
|
const uploadAvatar = async (file: File) => {
|
|
|
|
|
if (!token.value) return { success: false, error: 'ไม่พบ Token การใช้งาน' }
|
|
|
|
|
|
2026-01-28 14:44:41 +07:00
|
|
|
// 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` }
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-28 11:54:20 +07:00
|
|
|
const formData = new FormData()
|
2026-01-28 14:44:41 +07:00
|
|
|
formData.append('file', file) // ✅ field = file
|
2026-01-28 11:54:20 +07:00
|
|
|
|
2026-01-28 14:44:41 +07:00
|
|
|
interface AvatarResponse {
|
|
|
|
|
code: number
|
|
|
|
|
message: string
|
|
|
|
|
data: {
|
|
|
|
|
id: number
|
|
|
|
|
avatar_url: string
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const doUpload = async () => {
|
|
|
|
|
return await $fetch<AvatarResponse>(`${API_BASE_URL}/user/upload-avatar`, {
|
2026-01-28 11:54:20 +07:00
|
|
|
method: 'POST',
|
|
|
|
|
headers: {
|
2026-01-28 14:44:41 +07:00
|
|
|
accept: 'application/json',
|
|
|
|
|
Authorization: `Bearer ${token.value}`
|
2026-01-28 11:54:20 +07:00
|
|
|
},
|
|
|
|
|
body: formData
|
|
|
|
|
})
|
2026-01-28 14:44:41 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|
2026-01-28 11:54:20 +07:00
|
|
|
}
|
|
|
|
|
|
2026-01-28 14:44:41 +07:00
|
|
|
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 }
|
2026-01-28 11:54:20 +07:00
|
|
|
} catch (err: any) {
|
|
|
|
|
console.error('Upload avatar failed:', err)
|
2026-01-28 14:44:41 +07:00
|
|
|
return { success: false, error: err.data?.message || err.message || 'อัปโหลดรูปภาพไม่สำเร็จ' }
|
2026-01-28 11:54:20 +07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-23 09:54:35 +07:00
|
|
|
// ฟังก์ชันขอ Access Token ใหม่ด้วย Refresh Token
|
2026-01-14 15:15:31 +07:00
|
|
|
const refreshAccessToken = async () => {
|
|
|
|
|
if (!refreshToken.value) return false
|
|
|
|
|
|
|
|
|
|
try {
|
2026-01-21 10:02:37 +07:00
|
|
|
const data = await $fetch<{ token: string; refreshToken: string }>(`${API_BASE_URL}/auth/refresh`, {
|
2026-01-14 15:15:31 +07:00
|
|
|
method: 'POST',
|
|
|
|
|
body: { refreshToken: refreshToken.value }
|
|
|
|
|
})
|
|
|
|
|
|
2026-01-21 10:02:37 +07:00
|
|
|
if (data) {
|
|
|
|
|
token.value = data.token
|
|
|
|
|
refreshToken.value = data.refreshToken
|
2026-01-14 15:15:31 +07:00
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
2026-01-23 09:54:35 +07:00
|
|
|
// Refresh failed (เช่น Refresh Token หมดอายุ) ให้ Force Logout
|
2026-01-14 15:15:31 +07:00
|
|
|
logout()
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
return false
|
2026-01-13 10:46:40 +07:00
|
|
|
}
|
|
|
|
|
|
2026-02-02 11:11:25 +07:00
|
|
|
// ฟังก์ชันส่งอีเมลยืนยันตัวตน
|
|
|
|
|
const sendVerifyEmail = async () => {
|
|
|
|
|
if (!token.value) return { success: false, error: 'Token missing' }
|
|
|
|
|
|
2026-02-03 11:01:33 +07:00
|
|
|
interface VerifyEmailResponse {
|
|
|
|
|
code: number
|
|
|
|
|
message: string
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 11:11:25 +07:00
|
|
|
const doSend = async () => {
|
2026-02-03 11:01:33 +07:00
|
|
|
return await $fetch<VerifyEmailResponse>(`${API_BASE_URL}/user/send-verify-email`, {
|
2026-02-02 11:11:25 +07:00
|
|
|
method: 'POST',
|
|
|
|
|
headers: {
|
|
|
|
|
Authorization: `Bearer ${token.value}`
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const res = await doSend()
|
2026-02-03 11:01:33 +07:00
|
|
|
return { success: true, message: res.message, code: res.code }
|
2026-02-02 11:11:25 +07:00
|
|
|
} 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()
|
2026-02-03 11:01:33 +07:00
|
|
|
return { success: true, message: res.message, code: res.code }
|
2026-02-02 11:11:25 +07:00
|
|
|
} 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' }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-03 11:01:33 +07:00
|
|
|
// ฟังก์ชันยืนยันอีเมลด้วย Token
|
|
|
|
|
const verifyEmail = async (token: string) => {
|
|
|
|
|
try {
|
|
|
|
|
interface VerifyResponse {
|
|
|
|
|
code: number
|
|
|
|
|
message: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const res = await $fetch<VerifyResponse>(`${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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-01-23 09:54:35 +07:00
|
|
|
// ฟังก์ชันออกจากระบบ (Logout)
|
2026-01-13 10:46:40 +07:00
|
|
|
const logout = () => {
|
|
|
|
|
token.value = null
|
2026-01-23 09:54:35 +07:00
|
|
|
refreshToken.value = null // ลบ Refresh Token
|
2026-01-14 15:15:31 +07:00
|
|
|
user.value = null
|
2026-02-04 16:22:42 +07:00
|
|
|
|
2026-02-05 09:56:10 +07:00
|
|
|
// Reset client-side storage (Keep remembered_email and theme)
|
2026-02-04 16:22:42 +07:00
|
|
|
if (import.meta.client) {
|
2026-02-04 16:57:25 +07:00
|
|
|
const rememberedEmail = localStorage.getItem('remembered_email')
|
2026-02-05 09:56:10 +07:00
|
|
|
const theme = localStorage.getItem('theme')
|
|
|
|
|
|
2026-02-04 16:22:42 +07:00
|
|
|
localStorage.clear()
|
2026-02-05 09:56:10 +07:00
|
|
|
|
2026-02-04 16:57:25 +07:00
|
|
|
if (rememberedEmail) {
|
|
|
|
|
localStorage.setItem('remembered_email', rememberedEmail)
|
|
|
|
|
}
|
2026-02-05 09:56:10 +07:00
|
|
|
if (theme) {
|
|
|
|
|
localStorage.setItem('theme', theme)
|
|
|
|
|
}
|
2026-02-04 16:22:42 +07:00
|
|
|
}
|
|
|
|
|
|
2026-02-05 09:56:10 +07:00
|
|
|
// 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')
|
|
|
|
|
}
|
2026-01-13 10:46:40 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
isAuthenticated,
|
|
|
|
|
token,
|
2026-01-14 15:15:31 +07:00
|
|
|
user,
|
|
|
|
|
currentUser: computed(() => {
|
|
|
|
|
if (!user.value) return null
|
|
|
|
|
|
2026-01-23 09:54:35 +07:00
|
|
|
// Helper ในการดึงข้อมูล user ที่อาจซ้อนกันอยู่หลายชั้น
|
2026-01-14 15:15:31 +07:00
|
|
|
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 {
|
2026-01-16 10:03:04 +07:00
|
|
|
prefix: user.value.profile?.prefix || { th: '', en: '' },
|
2026-01-14 15:15:31 +07:00
|
|
|
firstName,
|
|
|
|
|
lastName,
|
|
|
|
|
email: user.value.email,
|
2026-02-03 11:01:33 +07:00
|
|
|
emailVerifiedAt: user.value.email_verified_at,
|
2026-01-14 15:15:31 +07:00
|
|
|
phone: user.value.profile?.phone || '',
|
|
|
|
|
photoURL: user.value.profile?.avatar_url || '',
|
2026-01-16 10:03:04 +07:00
|
|
|
role: user.value.role,
|
|
|
|
|
createdAt: user.value.created_at || new Date().toISOString()
|
2026-01-14 15:15:31 +07:00
|
|
|
}
|
|
|
|
|
}),
|
2026-01-13 10:46:40 +07:00
|
|
|
login,
|
2026-01-14 15:15:31 +07:00
|
|
|
register,
|
|
|
|
|
fetchUserProfile,
|
2026-01-16 10:03:04 +07:00
|
|
|
updateUserProfile,
|
2026-01-14 15:15:31 +07:00
|
|
|
requestPasswordReset,
|
|
|
|
|
confirmResetPassword,
|
2026-01-16 10:03:04 +07:00
|
|
|
changePassword,
|
2026-01-28 11:54:20 +07:00
|
|
|
uploadAvatar,
|
2026-01-15 10:30:40 +07:00
|
|
|
refreshAccessToken,
|
2026-02-02 11:11:25 +07:00
|
|
|
logout,
|
2026-02-03 11:01:33 +07:00
|
|
|
sendVerifyEmail,
|
|
|
|
|
verifyEmail
|
2026-01-13 10:46:40 +07:00
|
|
|
}
|
|
|
|
|
}
|