import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useDispatch, useSelector } from 'react-redux'; import { loginSuccess, updateUser } from '../features/auth/authSlice'; import { useNavigate } from 'react-router-dom'; import axios from 'axios'; // ใช้ Axios ธรรมดาสำหรับการเรียกที่ยังไม่มี Token import { addToast } from '../features/toast/toastSlice'; import axiosClient from "./axiosClient.js"; const API_BASE_URL = import.meta.env.VITE_API_BASE_URL; // ---------------------------------------------------- // Helper Functions // ---------------------------------------------------- /** * แปลง Object ให้เป็นฟอร์มข้อมูล (x-www-form-urlencoded) */ const toFormUrlEncoded = (data) => { // วนซ้ำ key และ encode return Object.keys(data) .map(key => { let value = data[key]; // รับค่า remember_me (boolean) มาแปลงเป็น String 'true' if (value === true) { value = 'true'; } else if (value === false) { value = 'false'; // หรือจะส่งเฉพาะเมื่อเป็น true ก็ได้ } return encodeURIComponent(key) + '=' + encodeURIComponent(value); }) .join('&'); }; /** * กำหนดบทบาท (Role) จาก Django User Object ที่สมบูรณ์ * @param {object} user - User Object จาก /users/me/ */ const determineRole = (user) => { if (!user || !user.id) { return 'guest'; } // 1. ใช้ฟิลด์ 'role' ที่ส่งมาจาก Backend โดยตรง (ถ้ามี) if (user.role) { return user.role.toLowerCase(); // เช่น 'ADMIN' -> 'admin' } // 2. ใช้ฟิลด์ is_superuser/is_staff เป็น Fallback/เกณฑ์มาตรฐาน if (user.is_superuser) { return 'admin'; } if (user.is_staff) { // ถ้าเป็น is_staff แต่ไม่ใช่ superuser return 'manager'; } // ผู้ใช้ทั่วไป return 'viewer'; }; /** * ฟังก์ชันหลักในการล็อกอิน: ขั้นตอนที่ 1 (รับ Token) และ ขั้นตอนที่ 2 (รับ User Object) */ const loginUser = async (credentials) => { // credentials จะเป็น { username, password } const formData = toFormUrlEncoded(credentials); let access, refresh; // --------------------------------------------- // ขั้นตอนที่ 1: รับ Access/Refresh Token (POST /jwt/create/) // --------------------------------------------- try { const tokenResponse = await axios.post(`${API_BASE_URL}/api/v1/auth/jwt/create/`, formData, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }); access = tokenResponse.data.access; refresh = tokenResponse.data.refresh; } catch (error) { // จัดการ Error จากการล็อกอิน const errorMessage = error.response?.data?.detail || "ชื่อผู้ใช้หรือรหัสผ่านไม่ถูกต้อง"; throw new Error(errorMessage); } // --------------------------------------------- // ขั้นตอนที่ 2: ใช้ Access Token เพื่อดึง User Object (GET /users/me/) // --------------------------------------------- let user; try { const userResponse = await axios.get(`${API_BASE_URL}/api/v1/auth/users/me/`, { headers: { // ใช้ JWT เพื่อยืนยันตัวตน 'Authorization': `Bearer ${access}`, }, }); user = userResponse.data; } catch (error) { console.error("Failed to fetch user data after token creation:", error); throw new Error("ล็อกอินสำเร็จ แต่ไม่สามารถดึงข้อมูลผู้ใช้ได้ (กรุณาติดต่อผู้ดูแลระบบ)"); } // --------------------------------------------- // ขั้นตอนที่ 3: คำนวณ Role และส่งกลับข้อมูล // --------------------------------------------- const userRole = determineRole(user); return { access_token: access, refresh_token: refresh, user: user, role: userRole, }; }; // ---------------------------------------------------- // Custom Hook สำหรับจัดการการล็อกอิน // ---------------------------------------------------- export const useLoginMutation = () => { const dispatch = useDispatch(); const navigate = useNavigate(); const queryClient = useQueryClient(); return useMutation({ mutationFn: loginUser, onSuccess: (data) => { // 1. จัดการ Token และ User Data localStorage.setItem('token', data.access_token); // บันทึก Refresh Token แยกต่างหาก localStorage.setItem('refresh_token', data.refresh_token); localStorage.setItem('user', JSON.stringify(data.user)); localStorage.setItem('role', data.role); // บันทึก Role ที่ถูกต้อง // 2. อัปเดต Redux State dispatch(loginSuccess({ user: data.user, role: data.role })); // 3. นำทางผู้ใช้ navigate('/dashboard', { replace: true }); // Invalidating Query ที่เกี่ยวข้อง queryClient.invalidateQueries({ queryKey: ['userData'] }); queryClient.invalidateQueries({ queryKey: ['userProfile'] }); queryClient.invalidateQueries({ queryKey: ['modelList'] }); }, onError: (error) => { // error.message ถูกโยนมาจาก loginUser function console.error('Login API Error:', error.message); throw new Error(error.message); }, }); }; // ---------------------------------------------------- // Helper Function: Registration API Call // ---------------------------------------------------- const registerNewUser = async (userData) => { // Djoser Endpoint: POST /api/v1/auth/users/ (รับ username, email, password, phone_number) // Djoser /users/ Endpoint รับ JSON Body ได้โดยตรง (ไม่ใช้ x-www-form-urlencoded) // ลบ confirm_password ออกจาก payload ก่อนส่ง // ใช้ _ นำหน้าเพื่อบอก ESLint ว่าตัวแปรนี้จะไม่ถูกใช้ // ต้องการ confirm_password เพื่อให้ Yup (Validation) ทำงาน แต่เราไม่ต้องการส่งมันไปยัง API Backend (เพราะ Djoser API ไม่ได้รับ Field นี้) const { confirm_password: _, ...payload } = userData; try { const response = await axios.post(`${API_BASE_URL}/api/v1/auth/users/`, payload); return response.data; } catch (error) { // จัดการ Error เช่น Username/Email ซ้ำ const errorDetail = error.response?.data; let errorMessage = "การลงทะเบียนล้มเหลว โปรดตรวจสอบข้อมูลอีกครั้ง"; if (errorDetail) { if (errorDetail.username) errorMessage = `ชื่อผู้ใช้งาน: ${errorDetail.username[0]}`; else if (errorDetail.email) errorMessage = `อีเมล: ${errorDetail.email[0]}`; else if (errorDetail.phone_number) errorMessage = `เบอร์โทรศัพท์: ${errorDetail.phone_number[0]}`; } throw new Error(errorMessage); } }; // ---------------------------------------------------- // Custom Hook สำหรับจัดการการลงทะเบียน // ---------------------------------------------------- export const useRegisterMutation = () => { const dispatch = useDispatch(); const navigate = useNavigate(); return useMutation({ mutationFn: registerNewUser, onSuccess: () => { dispatch(addToast({ message: 'ลงทะเบียนสำเร็จ! คุณสามารถเข้าสู่ระบบได้ทันที', type: 'success' })); navigate('/login'); }, onError: (error) => { // ส่ง Toast แจ้งเตือนข้อผิดพลาดที่โยนมาจาก registerNewUser dispatch(addToast({ message: `ลงทะเบียนล้มเหลว: ${error.message}`, type: 'error' })); throw new Error(error.message); }, }); }; // ---------------------------------------------------- // Hook สำหรับส่งคำขอรีเซ็ตรหัสผ่าน (ขั้นตอน A) // ---------------------------------------------------- export const useRequestPasswordReset = () => { const dispatch = useDispatch(); return useMutation({ mutationFn: async ({ email }) => { // Djoser Endpoint: รับ email/username เพื่อส่งอีเมล const response = await axios.post( `${API_BASE_URL}/api/v1/auth/users/reset_password/`, { email: email } // Djoser รับ JSON Body ); return response.data; }, onSuccess: () => { dispatch(addToast({ message: 'คำขอรีเซ็ตถูกส่งไปยังอีเมลของคุณแล้ว', type: 'success' })); }, onError: (error) => { const msg = error.response?.data?.detail || 'ไม่พบผู้ใช้งานด้วยอีเมลนี้'; dispatch(addToast({ message: `ล้มเหลว: ${msg}`, type: 'error' })); throw new Error(msg); }, }); }; // ---------------------------------------------------- // Hook สำหรับยืนยันและตั้งรหัสผ่านใหม่ (ขั้นตอน B) // ---------------------------------------------------- export const useConfirmPasswordReset = () => { const dispatch = useDispatch(); const navigate = useNavigate(); return useMutation({ mutationFn: async (data) => { // Djoser Endpoint: รับ uid, token, new_password const response = await axios.post( `${API_BASE_URL}/api/v1/auth/users/reset_password_confirm/`, data ); return response.data; }, onSuccess: () => { dispatch(addToast({ message: 'รหัสผ่านถูกตั้งค่าใหม่เรียบร้อยแล้ว!', type: 'success' })); navigate('/login', { replace: true }); }, onError: (error) => { const msg = error.response?.data?.detail || 'ลิงก์ไม่ถูกต้องหรือหมดอายุ'; dispatch(addToast({ message: `ตั้งรหัสผ่านใหม่ล้มเหลว: ${msg}`, type: 'error' })); throw new Error(msg); }, }); }; /** * ฟังก์ชันสำหรับเรียก API เพื่อต่ออายุ Access Token ด้วย Refresh Token */ export const refreshAccessToken = async () => { const refresh = localStorage.getItem('refresh_token'); // เก็บ Refresh Token แยกต่างหาก if (!refresh) { throw new Error("Refresh token not found."); } const response = await axios.post(`${API_BASE_URL}/api/v1/auth/jwt/refresh/`, { refresh: refresh, }); // คืนค่า Access Token ใหม่ return response.data.access; }; // ---------------------------------------------------- // Hook ดึงข้อมูลผู้ใช้ปัจจุบัน (GET /users/me/) // ---------------------------------------------------- export const useUserQuery = () => { // ดึงสถานะปัจจุบันของ User จาก Redux Store const userId = useSelector(state => state.auth.user?.id); const isAuthenticated = useSelector(state => state.auth.isAuthenticated); return useQuery({ // ทำให้ Query Key ขึ้นอยู่กับ User ID queryKey: ['userProfile', userId], // ไม่รัน Query ถ้าผู้ใช้ไม่ได้ล็อกอิน enabled: isAuthenticated && !!userId, queryFn: async () => { const response = await axiosClient.get(`${API_BASE_URL}/api/v1/auth/users/me/`); return response.data; }, staleTime: 60000, }); }; // ---------------------------------------------------- // Hook อัปเดตข้อมูลผู้ใช้ (PATCH /users/me/) // ---------------------------------------------------- export const useUpdateProfileMutation = () => { const queryClient = useQueryClient(); const dispatch = useDispatch(); return useMutation({ mutationFn: async (profileData) => { // Djoser Endpoint: PATCH /users/me/ const response = await axiosClient.patch(`${API_BASE_URL}/api/v1/auth/users/me/`, profileData); return response.data; }, onSuccess: (updatedUser) => { dispatch(addToast({ message: 'แก้ไขข้อมูลส่วนตัวสำเร็จแล้ว!', type: 'success' })); // อัปเดต Redux Store และ Local Storage ทันที const newRole = determineRole(updatedUser); dispatch(updateUser({ user: updatedUser, role: newRole })); // Invalidate Query เพื่อดึงข้อมูล Profile ใหม่ queryClient.invalidateQueries({ queryKey: ['userProfile'] }); }, onError: (error) => { const msg = error.response?.data?.email || error.response?.data?.detail || 'การอัปเดตล้มเหลว'; dispatch(addToast({ message: `อัปเดต Profile ล้มเหลว: ${msg}`, type: 'error' })); }, }); };