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. ขั้นตอนการรีเซ็ต */}
+
+
+ กระบวนการ
+
+
+ - กรุณากรอกอีเมลที่ใช้ลงทะเบียน
+ - ระบบจะส่งลิงก์รีเซ็ตไปให้คุณ
+ - ลิงก์จะหมดอายุภายใน 24 ชั่วโมง
+ - หากไม่พบอีเมล ให้ตรวจสอบใน /Spam Folder
+
+
+
+
+ {/* 2. ช่องทางติดต่อช่วยเหลือ */}
+
+
+ หากไม่ได้รับอีเมล
+
+
+ -
+
+ ติดต่อ: support@ddo.tech
+
+ -
+
+ โทร: 02-XXX-XXXX
+
+
+
+
+ );
+}
\ 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 (
+
+ );
+}
\ 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 หลัก) */}
+
+
+
+
ลืมรหัสผ่าน?
+
+ กรุณากรอกอีเมลของคุณเพื่อรับลิงก์สำหรับรีเซ็ตรหัสผ่าน
+
+
+
+
+
+ ย้อนกลับไปหน้าล็อกอิน
+
+
+
+
+
+
+ );
+}
\ 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