พัฒนา Frontend ฟังก์ชัน ลืมรหัสผ่าน? (Forgot Password)
This commit is contained in:
parent
606008db88
commit
e67c3a75a1
47
web/src/components/PasswordReset/ResetInfoCard.jsx
Normal file
47
web/src/components/PasswordReset/ResetInfoCard.jsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
63
web/src/components/PasswordReset/ResetPasswordForm.jsx
Normal file
63
web/src/components/PasswordReset/ResetPasswordForm.jsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
93
web/src/pages/auth/ForgotPasswordPage.jsx
Normal file
93
web/src/pages/auth/ForgotPasswordPage.jsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
49
web/src/pages/auth/ResetConfirmPage.jsx
Normal file
49
web/src/pages/auth/ResetConfirmPage.jsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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 */}
|
||||||
|
|||||||
@ -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('กรุณายืนยันรหัสผ่านใหม่'),
|
||||||
});
|
});
|
||||||
@ -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);
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
Loading…
x
Reference in New Issue
Block a user