พัฒนา Frontend ฟังก์ชัน ลืมรหัสผ่าน? (Forgot Password)

This commit is contained in:
Flook 2025-11-15 23:20:18 +07:00
parent 606008db88
commit e67c3a75a1
7 changed files with 325 additions and 2 deletions

View File

@ -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
<div className="w-full h-full p-12 bg-base-200 flex flex-col justify-between">
<div className="flex flex-col">
<div className="flex items-center text-xl font-bold text-primary mb-4 border-b border-base-200 pb-3">
<FaQuestionCircle className="w-6 h-6 mr-3 text-warning" />
คำแนะนำในการรเซตรหสผาน
</div>
{/* 1. ขั้นตอนการรีเซ็ต */}
<h3 className="text-lg font-semibold text-base-content mb-2 flex items-center">
<FaClock className="w-4 h-4 mr-2 text-info" />
กระบวนการ
</h3>
<ol className="list-decimal list-inside space-y-2 text-sm text-base-content/70 ml-4">
<li>กรณากรอกอเมลทใชลงทะเบยน</li>
<li>ระบบจะสงลงกเซตไปให</li>
<li>งกจะหมดอายภายใน <b>24 วโมง</b></li>
<li>หากไมพบอเมล ใหตรวจสอบใน <b>/Spam Folder</b></li>
</ol>
<div className="divider my-4"></div>
{/* 2. ช่องทางติดต่อช่วยเหลือ */}
<h3 className="text-lg font-semibold text-base-content mb-2 flex items-center">
<FaExclamationTriangle className="w-4 h-4 mr-2 text-error" />
หากไมไดบอเมล
</h3>
<ul className="space-y-2 text-sm text-base-content/70 ml-2">
<li className='flex items-center'>
<FaEnvelope className="w-4 h-4 mr-2 text-info" />
<span className="font-semibold">ดต:</span> support@ddo.tech
</li>
<li className='flex items-center'>
<FaPhoneAlt className="w-4 h-4 mr-2 text-info" />
<span className="font-semibold">โทร:</span> 02-XXX-XXXX
</li>
</ul>
</div>
</div>
);
}

View File

@ -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 (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<h2 className="text-2xl font-bold mb-4 text-base-content text-center">งรหสผานใหม</h2>
<p className="text-sm text-gray-600 mb-6 text-center">กรณากรอกรหสผานใหมเพอเขาสระบบ</p>
<InputText
type="password"
labelTitle="รหัสผ่านใหม่"
placeholder="รหัสผ่านใหม่"
{...register('new_password')}
error={errors.new_password}
/>
<InputText
type="password"
labelTitle="ยืนยันรหัสผ่านใหม่"
placeholder="ยืนยันรหัสผ่าน"
{...register('re_new_password')}
error={errors.re_new_password}
containerStyle="mt-4"
/>
{confirmMutation.isError && (
<ErrorText styleClass="my-4">{confirmMutation.error.message}</ErrorText>
)}
<button
type="submit"
className={"btn btn-primary w-full mt-6" + (loading ? " loading" : "")}
disabled={loading}
>
{loading ? 'กำลังตั้งค่า...' : 'ตั้งค่ารหัสผ่านใหม่'}
</button>
</form>
);
}

View File

@ -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
<div className="card w-full h-full bg-base-100 rounded-none shadow-none">
{/* Grid Layout: แบ่งเป็น 12 ส่วน, 6 ส่วนสำหรับ Info (ซ้าย), 6 ส่วนสำหรับ Form (ขวา) */}
<div className="grid grid-cols-1 md:grid-cols-12 h-full">
{/* 1. คอลัมน์ซ้าย (คำแนะนำและช่วยเหลือ) */}
<div className="col-span-1 md:col-span-6 bg-base-200 h-full flex flex-col justify-center rounded-none">
<ResetInfoCard />
</div>
{/* 2. คอลัมน์ขวา (Form หลัก) */}
<div className="col-span-1 md:col-span-6 py-12 px-8 lg:px-16 flex flex-col justify-center items-center">
<div className="w-full max-w-lg">
<h2 className="text-3xl font-bold mb-4 text-base-content text-center">มรหสผาน?</h2>
<p className="text-sm text-gray-600 mb-6 text-center">
กรณากรอกอเมลของคณเพอรบลงกสำหรบรเซตรหสผาน
</p>
<form onSubmit={handleSubmit(onSubmit)}>
<InputText
type="email"
labelTitle="อีเมล"
placeholder="อีเมลที่ใช้ลงทะเบียน"
{...register('email')}
error={errors.email}
/>
{requestMutation.isError && (
<ErrorText styleClass="my-4">
{requestMutation.error.message || "การส่งคำขอล้มเหลว"}
</ErrorText>
)}
<button
type="submit"
className={"btn btn-primary w-full mt-6" + (loading ? " loading" : "")}
disabled={loading}
>
{loading ? 'กำลังส่งคำขอ...' : 'ส่งคำขอรีเซ็ต'}
</button>
</form>
<div className="text-center mt-4">
<Link to="/login" className="link link-hover text-sm">อนกลบไปหนาลอกอ</Link>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@ -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 (
<div className="flex items-center justify-center h-screen bg-base-200 p-4">
<div className="max-w-md alert alert-error text-center shadow-lg">
<FaLock className="w-6 h-6 mr-2" />
<span>งกเซตรหสผานไมสมบรณ หรอหมดอายแล</span>
<div className="mt-2 text-sm"><Link to="/login" className="link">กลบสหนาลอกอ</Link></div>
</div>
</div>
);
}
// 3. Presentation (A-List UI)
return (
<div className="flex items-center justify-center h-screen bg-base-200 p-4">
<div className="card w-full max-w-lg shadow-2xl bg-white animate__animated animate__fadeInDown">
<div className="card-body p-8">
<div className="text-center mb-6">
{/* ไอคอน Lock ขนาดใหญ่ พร้อมพื้นหลังสีอ่อน */}
<FaLock className="w-12 h-12 mx-auto text-error bg-error/10 p-3 rounded-full mb-3" />
<h1 className="text-3xl font-extrabold text-base-content">นยนการตงรหสผาน</h1>
</div>
{/* Rendering the separated Form Component */}
<ResetPasswordForm
uid={uid}
token={token}
confirmMutation={confirmMutation}
/>
</div>
</div>
</div>
);
}

View File

@ -2,7 +2,8 @@ import { Route, Routes, Navigate } from "react-router-dom";
import AuthLayout from "../layouts/AuthLayout.jsx"; import AuthLayout from "../layouts/AuthLayout.jsx";
import LoginForm from "../components/LoginForm.jsx"; import LoginForm from "../components/LoginForm.jsx";
import RegisterPage from "../pages/auth/RegisterPage.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() { export default function AuthRoutes() {
return( return(
@ -14,7 +15,9 @@ export default function AuthRoutes() {
<Route element={<AuthLayout/>}> <Route element={<AuthLayout/>}>
<Route path="/login" element={<LoginForm/>}/> <Route path="/login" element={<LoginForm/>}/>
<Route path="/register" element={<RegisterPage/>}/> <Route path="/register" element={<RegisterPage/>}/>
<Route path="/forgot-password" element={<div>หนาลมรหสผาน</div>}/> <Route path="/forgot-password" element={<ForgotPasswordPage/>}/>
<Route path="/password/reset/confirm/:uid/:token" element={<ResetConfirmPage/>}
/>
</Route> </Route>
{/* 3. Fallback สำหรับเส้นทางที่ไม่รู้จักในส่วน Public */} {/* 3. Fallback สำหรับเส้นทางที่ไม่รู้จักในส่วน Public */}

View File

@ -17,4 +17,11 @@ export const registrationSchema = yup.object().shape({
confirm_password: yup.string() confirm_password: yup.string()
.oneOf([yup.ref('password'), null], 'รหัสผ่านไม่ตรงกัน') .oneOf([yup.ref('password'), null], 'รหัสผ่านไม่ตรงกัน')
.required('กรุณายืนยันรหัสผ่าน'), .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('กรุณายืนยันรหัสผ่านใหม่'),
}); });

View File

@ -189,4 +189,65 @@ export const useRegisterMutation = () => {
throw new Error(error.message); 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);
},
});
}; };