diff --git a/web/src/components/PasswordReset/ResetInfoCard.jsx b/web/src/components/PasswordReset/ResetInfoCard.jsx new file mode 100644 index 0000000..02d5654 --- /dev/null +++ b/web/src/components/PasswordReset/ResetInfoCard.jsx @@ -0,0 +1,47 @@ +import React from 'react'; +import {FaEnvelope, FaQuestionCircle, FaClock, FaExclamationTriangle, FaPhoneAlt} from 'react-icons/fa'; + +export default function ResetInfoCard() { + return ( + // ใช้ w-full h-full เพื่อให้ Component ขยายเต็มพื้นที่ Container +
+ +
+
+ + คำแนะนำในการรีเซ็ตรหัสผ่าน +
+ + {/* 1. ขั้นตอนการรีเซ็ต */} +

+ + กระบวนการ +

+
    +
  1. กรุณากรอกอีเมลที่ใช้ลงทะเบียน
  2. +
  3. ระบบจะส่งลิงก์รีเซ็ตไปให้คุณ
  4. +
  5. ลิงก์จะหมดอายุภายใน 24 ชั่วโมง
  6. +
  7. หากไม่พบอีเมล ให้ตรวจสอบใน /Spam Folder
  8. +
+ +
+ + {/* 2. ช่องทางติดต่อช่วยเหลือ */} +

+ + หากไม่ได้รับอีเมล +

+ +
+
+ ); +} \ No newline at end of file diff --git a/web/src/components/PasswordReset/ResetPasswordForm.jsx b/web/src/components/PasswordReset/ResetPasswordForm.jsx new file mode 100644 index 0000000..2638c2c --- /dev/null +++ b/web/src/components/PasswordReset/ResetPasswordForm.jsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { useForm } from 'react-hook-form'; +import { yupResolver } from '@hookform/resolvers/yup'; + +import InputText from '../InputText'; +import ErrorText from '../ErrorText'; +import { resetConfirmSchema } from '../../schemas/authSchema'; + +export default function ResetPasswordForm({ uid, token, confirmMutation }) { + + // 1. Hook Form Setup + const { register, handleSubmit, formState: { errors } } = useForm({ + resolver: yupResolver(resetConfirmSchema), + }); + + // 2. Submission Handler + const onSubmit = (data) => { + // ส่ง uid, token, new_password, และ re_new_password ไปให้ API + confirmMutation.mutate({ + uid: uid, + token: token, + new_password: data.new_password, + re_new_password: data.re_new_password + }); + }; + + const loading = confirmMutation.isPending; + + return ( +
+

ตั้งรหัสผ่านใหม่

+

กรุณากรอกรหัสผ่านใหม่เพื่อเข้าสู่ระบบ

+ + + + + {confirmMutation.isError && ( + {confirmMutation.error.message} + )} + + + + ); +} \ No newline at end of file diff --git a/web/src/pages/auth/ForgotPasswordPage.jsx b/web/src/pages/auth/ForgotPasswordPage.jsx new file mode 100644 index 0000000..f3940a1 --- /dev/null +++ b/web/src/pages/auth/ForgotPasswordPage.jsx @@ -0,0 +1,93 @@ +import React from 'react'; +import { useForm } from 'react-hook-form'; +import { yupResolver } from '@hookform/resolvers/yup'; +import { Link } from 'react-router-dom'; +import * as yup from 'yup'; + +// Imports Components และ Hooks +import InputText from '../../components/InputText'; +import ErrorText from '../../components/ErrorText'; +import { useRequestPasswordReset } from '../../services/authApi'; +import ResetInfoCard from '../../components/PasswordReset/ResetInfoCard'; + +// ---------------------------------------------------- +// Schema สำหรับตรวจสอบข้อมูล (รับแค่อีเมล) +// ---------------------------------------------------- +const requestSchema = yup.object().shape({ + email: yup.string().email('รูปแบบอีเมลไม่ถูกต้อง').required('กรุณากรอกอีเมลที่ใช้ลงทะเบียน'), +}); + +export default function ForgotPasswordPage() { + const requestMutation = useRequestPasswordReset(); + + // 1. Hook Form Setup + const { + register, + handleSubmit, + formState: { errors } + } = useForm({ + resolver: yupResolver(requestSchema), + }); + + const onSubmit = (data) => { + // ส่งคำขอรีเซ็ตผ่าน Mutation + requestMutation.mutate(data); + }; + + const loading = requestMutation.isPending; + + return ( + // Layout หลัก: ใช้ h-full เพื่อให้เต็มหน้าจอ +
+ + {/* Grid Layout: แบ่งเป็น 12 ส่วน, 6 ส่วนสำหรับ Info (ซ้าย), 6 ส่วนสำหรับ Form (ขวา) */} +
+ + {/* 1. คอลัมน์ซ้าย (คำแนะนำและช่วยเหลือ) */} +
+ +
+ + {/* 2. คอลัมน์ขวา (Form หลัก) */} +
+
+ +

ลืมรหัสผ่าน?

+

+ กรุณากรอกอีเมลของคุณเพื่อรับลิงก์สำหรับรีเซ็ตรหัสผ่าน +

+ +
+ + + {requestMutation.isError && ( + + {requestMutation.error.message || "การส่งคำขอล้มเหลว"} + + )} + + + + +
+ ย้อนกลับไปหน้าล็อกอิน +
+
+
+ +
+
+ ); +} \ No newline at end of file diff --git a/web/src/pages/auth/ResetConfirmPage.jsx b/web/src/pages/auth/ResetConfirmPage.jsx new file mode 100644 index 0000000..de1ddf9 --- /dev/null +++ b/web/src/pages/auth/ResetConfirmPage.jsx @@ -0,0 +1,49 @@ +import React from 'react'; +import { useParams, Link } from 'react-router-dom'; +import { useConfirmPasswordReset } from '../../services/authApi'; +import ResetPasswordForm from '../../components/PasswordReset/ResetPasswordForm'; + +import { FaLock } from 'react-icons/fa'; // Icon Lock + +export default function ResetConfirmPage() { + // 1. Logic Management (รับพารามิเตอร์และ Hook) + const { uid, token } = useParams(); + const confirmMutation = useConfirmPasswordReset(); + + // 2. Pre-check: ตรวจสอบ URL Validity + if (!uid || !token) { + return ( +
+
+ + ลิงก์รีเซ็ตรหัสผ่านไม่สมบูรณ์ หรือหมดอายุแล้ว +
กลับสู่หน้าล็อกอิน
+
+
+ ); + } + + // 3. Presentation (A-List UI) + return ( +
+
+ +
+
+ {/* ไอคอน Lock ขนาดใหญ่ พร้อมพื้นหลังสีอ่อน */} + +

ยืนยันการตั้งรหัสผ่าน

+
+ + {/* Rendering the separated Form Component */} + + +
+
+
+ ); +} \ No newline at end of file diff --git a/web/src/routes/AuthRoutes.jsx b/web/src/routes/AuthRoutes.jsx index 2855b93..109dc27 100644 --- a/web/src/routes/AuthRoutes.jsx +++ b/web/src/routes/AuthRoutes.jsx @@ -2,7 +2,8 @@ import { Route, Routes, Navigate } from "react-router-dom"; import AuthLayout from "../layouts/AuthLayout.jsx"; import LoginForm from "../components/LoginForm.jsx"; import RegisterPage from "../pages/auth/RegisterPage.jsx"; - +import ForgotPasswordPage from "../pages/auth/ForgotPasswordPage.jsx"; +import ResetConfirmPage from "../pages/auth/ResetConfirmPage.jsx"; export default function AuthRoutes() { return( @@ -14,7 +15,9 @@ export default function AuthRoutes() { }> }/> }/> - หน้าลืมรหัสผ่าน}/> + }/> + } + /> {/* 3. Fallback สำหรับเส้นทางที่ไม่รู้จักในส่วน Public */} diff --git a/web/src/schemas/authSchema.js b/web/src/schemas/authSchema.js index 28f8394..cfa9d96 100644 --- a/web/src/schemas/authSchema.js +++ b/web/src/schemas/authSchema.js @@ -17,4 +17,11 @@ export const registrationSchema = yup.object().shape({ confirm_password: yup.string() .oneOf([yup.ref('password'), null], 'รหัสผ่านไม่ตรงกัน') .required('กรุณายืนยันรหัสผ่าน'), +}); + +export const resetConfirmSchema = yup.object().shape({ + new_password: yup.string().required('กรุณากรอกรหัสผ่านใหม่').min(8, 'รหัสผ่านต้องมีอย่างน้อย 8 ตัวอักษร'), + re_new_password: yup.string() + .oneOf([yup.ref('new_password'), null], 'รหัสผ่านไม่ตรงกัน') + .required('กรุณายืนยันรหัสผ่านใหม่'), }); \ No newline at end of file diff --git a/web/src/services/authApi.js b/web/src/services/authApi.js index 7a2fd74..7699627 100644 --- a/web/src/services/authApi.js +++ b/web/src/services/authApi.js @@ -189,4 +189,65 @@ export const useRegisterMutation = () => { 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); + }, + }); }; \ No newline at end of file