From 79cebd205ccbf8b35d2c90bf464a5e20dcd698bb Mon Sep 17 00:00:00 2001
From: Koncept Kit <63216427+konceptkit@users.noreply.github.com>
Date: Sun, 7 Dec 2025 16:59:21 +0700
Subject: [PATCH] Email SMTP Fix
---
src/App.js | 19 +-
src/components/ChangePasswordDialog.js | 150 ++++++++++++++
src/components/MemberRoute.js | 45 +++++
.../registration/RegistrationStep4.js | 7 +-
src/components/ui/password-input.jsx | 36 ++++
src/context/AuthContext.js | 67 ++++++-
src/pages/ChangePasswordRequired.js | 184 ++++++++++++++++++
src/pages/Dashboard.js | 46 ++++-
src/pages/ForgotPassword.js | 121 ++++++++++++
src/pages/Login.js | 22 ++-
src/pages/Profile.js | 21 +-
src/pages/ResetPassword.js | 147 ++++++++++++++
src/pages/admin/AdminMembers.js | 30 ---
src/pages/admin/AdminUserView.js | 85 +++++++-
src/pages/admin/AdminUsers.js | 76 +++++---
15 files changed, 978 insertions(+), 78 deletions(-)
create mode 100644 src/components/ChangePasswordDialog.js
create mode 100644 src/components/MemberRoute.js
create mode 100644 src/components/ui/password-input.jsx
create mode 100644 src/pages/ChangePasswordRequired.js
create mode 100644 src/pages/ForgotPassword.js
create mode 100644 src/pages/ResetPassword.js
diff --git a/src/App.js b/src/App.js
index a660e7f..074ba66 100644
--- a/src/App.js
+++ b/src/App.js
@@ -5,6 +5,9 @@ import Landing from './pages/Landing';
import Register from './pages/Register';
import Login from './pages/Login';
import VerifyEmail from './pages/VerifyEmail';
+import ForgotPassword from './pages/ForgotPassword';
+import ResetPassword from './pages/ResetPassword';
+import ChangePasswordRequired from './pages/ChangePasswordRequired';
import Dashboard from './pages/Dashboard';
import Profile from './pages/Profile';
import Events from './pages/Events';
@@ -22,6 +25,7 @@ import AdminApprovals from './pages/admin/AdminApprovals';
import AdminPlans from './pages/admin/AdminPlans';
import AdminLayout from './layouts/AdminLayout';
import { AuthProvider, useAuth } from './context/AuthContext';
+import MemberRoute from './components/MemberRoute';
const PrivateRoute = ({ children, adminOnly = false }) => {
const { user, loading } = useAuth();
@@ -50,6 +54,13 @@ function App() {
} />
} />
} />
+ } />
+ } />
+
+
+
+ } />
} />
} />
} />
@@ -65,14 +76,14 @@ function App() {
} />
+
-
+
} />
+
-
+
} />
{
+ const { changePassword } = useAuth();
+ const [loading, setLoading] = useState(false);
+ const [formData, setFormData] = useState({
+ currentPassword: '',
+ newPassword: '',
+ confirmPassword: ''
+ });
+
+ const handleInputChange = (e) => {
+ const { name, value } = e.target;
+ setFormData(prev => ({ ...prev, [name]: value }));
+ };
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+
+ if (formData.newPassword.length < 6) {
+ toast.error('New password must be at least 6 characters');
+ return;
+ }
+
+ if (formData.newPassword !== formData.confirmPassword) {
+ toast.error('New passwords do not match');
+ return;
+ }
+
+ setLoading(true);
+
+ try {
+ await changePassword(formData.currentPassword, formData.newPassword);
+ toast.success('Password changed successfully!');
+
+ // Reset form
+ setFormData({
+ currentPassword: '',
+ newPassword: '',
+ confirmPassword: ''
+ });
+
+ // Close dialog
+ onOpenChange(false);
+ } catch (error) {
+ const errorMessage = error.response?.data?.detail || 'Failed to change password';
+ toast.error(errorMessage);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+ );
+};
+
+export default ChangePasswordDialog;
diff --git a/src/components/MemberRoute.js b/src/components/MemberRoute.js
new file mode 100644
index 0000000..566209a
--- /dev/null
+++ b/src/components/MemberRoute.js
@@ -0,0 +1,45 @@
+import React, { useEffect } from 'react';
+import { Navigate } from 'react-router-dom';
+import { useAuth } from '../context/AuthContext';
+import { toast } from 'sonner';
+
+const MemberRoute = ({ children }) => {
+ const { user, loading } = useAuth();
+ const [hasShownToast, setHasShownToast] = React.useState(false);
+
+ useEffect(() => {
+ // Show toast only once when user is not active
+ if (!loading && user && user.status !== 'active' && !hasShownToast) {
+ toast.error('Active membership required. Please complete your payment to access this feature.', {
+ duration: 5000
+ });
+ setHasShownToast(true);
+ }
+ }, [user, loading, hasShownToast]);
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ if (!user) {
+ return ;
+ }
+
+ // Allow admins to bypass payment requirement
+ if (user.role === 'admin') {
+ return children;
+ }
+
+ // Check if user is an active member
+ if (user.status !== 'active') {
+ return ;
+ }
+
+ return children;
+};
+
+export default MemberRoute;
diff --git a/src/components/registration/RegistrationStep4.js b/src/components/registration/RegistrationStep4.js
index d0a5779..d9c8005 100644
--- a/src/components/registration/RegistrationStep4.js
+++ b/src/components/registration/RegistrationStep4.js
@@ -1,6 +1,7 @@
import React from 'react';
import { Label } from '../ui/label';
import { Input } from '../ui/input';
+import { PasswordInput } from '../ui/password-input';
const RegistrationStep4 = ({ formData, handleInputChange }) => {
return (
@@ -34,10 +35,9 @@ const RegistrationStep4 = ({ formData, handleInputChange }) => {
-
{
-
{
+ const [showPassword, setShowPassword] = React.useState(false)
+
+ return (
+
+
+
+
+ )
+})
+PasswordInput.displayName = "PasswordInput"
+
+export { PasswordInput }
diff --git a/src/context/AuthContext.js b/src/context/AuthContext.js
index 0e5d653..2a1da0c 100644
--- a/src/context/AuthContext.js
+++ b/src/context/AuthContext.js
@@ -71,8 +71,73 @@ export const AuthProvider = ({ children }) => {
}
};
+ const forgotPassword = async (email) => {
+ const response = await axios.post(`${API_URL}/api/auth/forgot-password`, { email });
+ return response.data;
+ };
+
+ const resetPassword = async (token, newPassword) => {
+ const response = await axios.post(`${API_URL}/api/auth/reset-password`, {
+ token,
+ new_password: newPassword
+ });
+ return response.data;
+ };
+
+ const changePassword = async (currentPassword, newPassword) => {
+ const currentToken = localStorage.getItem('token');
+ if (!currentToken) {
+ throw new Error('Not authenticated');
+ }
+
+ const response = await axios.put(
+ `${API_URL}/api/users/change-password`,
+ {
+ current_password: currentPassword,
+ new_password: newPassword
+ },
+ {
+ headers: { Authorization: `Bearer ${currentToken}` }
+ }
+ );
+
+ // Refresh user data to clear force_password_change flag if it was set
+ await refreshUser();
+
+ return response.data;
+ };
+
+ const resendVerificationEmail = async () => {
+ const currentToken = localStorage.getItem('token');
+ if (!currentToken) {
+ throw new Error('Not authenticated');
+ }
+
+ const response = await axios.post(
+ `${API_URL}/api/auth/resend-verification-email`,
+ {},
+ {
+ headers: { Authorization: `Bearer ${currentToken}` }
+ }
+ );
+
+ return response.data;
+ };
+
return (
-
+
{children}
);
diff --git a/src/pages/ChangePasswordRequired.js b/src/pages/ChangePasswordRequired.js
new file mode 100644
index 0000000..5b1863d
--- /dev/null
+++ b/src/pages/ChangePasswordRequired.js
@@ -0,0 +1,184 @@
+import React, { useState, useEffect } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { useAuth } from '../context/AuthContext';
+import { Button } from '../components/ui/button';
+import { Input } from '../components/ui/input';
+import { Label } from '../components/ui/label';
+import { Card } from '../components/ui/card';
+import { toast } from 'sonner';
+import Navbar from '../components/Navbar';
+import { ArrowRight, Lock, AlertTriangle } from 'lucide-react';
+
+const ChangePasswordRequired = () => {
+ const navigate = useNavigate();
+ const { user, changePassword, logout } = useAuth();
+ const [loading, setLoading] = useState(false);
+ const [formData, setFormData] = useState({
+ currentPassword: '',
+ newPassword: '',
+ confirmPassword: ''
+ });
+
+ useEffect(() => {
+ // If user is not logged in or doesn't have force_password_change, redirect
+ if (!user) {
+ navigate('/login');
+ } else if (!user.force_password_change) {
+ // User doesn't need to change password, redirect to appropriate page
+ if (user.role === 'admin') {
+ navigate('/admin');
+ } else {
+ navigate('/dashboard');
+ }
+ }
+ }, [user, navigate]);
+
+ const handleInputChange = (e) => {
+ const { name, value } = e.target;
+ setFormData(prev => ({ ...prev, [name]: value }));
+ };
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+
+ if (formData.newPassword.length < 6) {
+ toast.error('New password must be at least 6 characters');
+ return;
+ }
+
+ if (formData.newPassword !== formData.confirmPassword) {
+ toast.error('New passwords do not match');
+ return;
+ }
+
+ setLoading(true);
+
+ try {
+ await changePassword(formData.currentPassword, formData.newPassword);
+ toast.success('Password changed successfully! Redirecting...');
+
+ // Wait a moment then redirect to dashboard
+ setTimeout(() => {
+ if (user.role === 'admin') {
+ navigate('/admin');
+ } else {
+ navigate('/dashboard');
+ }
+ }, 1500);
+ } catch (error) {
+ const errorMessage = error.response?.data?.detail || 'Failed to change password';
+ toast.error(errorMessage);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleLogout = () => {
+ logout();
+ navigate('/login');
+ };
+
+ if (!user || !user.force_password_change) {
+ return null;
+ }
+
+ return (
+
+
+
+
+
+
+
+
+ Password Change Required
+
+
+ Your password was reset by an administrator. Please create a new password to continue.
+
+
+
+
+
+
+
+ );
+};
+
+export default ChangePasswordRequired;
diff --git a/src/pages/Dashboard.js b/src/pages/Dashboard.js
index e49065c..09c7582 100644
--- a/src/pages/Dashboard.js
+++ b/src/pages/Dashboard.js
@@ -6,12 +6,14 @@ import { Card } from '../components/ui/card';
import { Button } from '../components/ui/button';
import { Badge } from '../components/ui/badge';
import Navbar from '../components/Navbar';
-import { Calendar, User, CheckCircle, Clock, AlertCircle } from 'lucide-react';
+import { Calendar, User, CheckCircle, Clock, AlertCircle, Mail } from 'lucide-react';
+import { toast } from 'sonner';
const Dashboard = () => {
- const { user } = useAuth();
+ const { user, resendVerificationEmail } = useAuth();
const [events, setEvents] = useState([]);
const [loading, setLoading] = useState(true);
+ const [resendLoading, setResendLoading] = useState(false);
useEffect(() => {
fetchUpcomingEvents();
@@ -29,6 +31,19 @@ const Dashboard = () => {
}
};
+ const handleResendVerification = async () => {
+ setResendLoading(true);
+ try {
+ await resendVerificationEmail();
+ toast.success('Verification email sent! Please check your inbox.');
+ } catch (error) {
+ const errorMessage = error.response?.data?.detail || 'Failed to send verification email';
+ toast.error(errorMessage);
+ } finally {
+ setResendLoading(false);
+ }
+ };
+
const getStatusBadge = (status) => {
const statusConfig = {
pending_email: { icon: Clock, label: 'Pending Email', className: 'bg-[#F2CC8F] text-[#3D405B]' },
@@ -78,6 +93,33 @@ const Dashboard = () => {
+ {/* Email Verification Alert */}
+ {user && !user.email_verified && (
+
+
+
+
+
+ Verify Your Email Address
+
+
+ Please verify your email address to complete your registration.
+ Check your inbox for the verification link.
+
+
+
+
+
+ )}
+
{/* Status Card */}
diff --git a/src/pages/ForgotPassword.js b/src/pages/ForgotPassword.js
new file mode 100644
index 0000000..c46cebb
--- /dev/null
+++ b/src/pages/ForgotPassword.js
@@ -0,0 +1,121 @@
+import React, { useState } from 'react';
+import { Link } from 'react-router-dom';
+import { useAuth } from '../context/AuthContext';
+import { Button } from '../components/ui/button';
+import { Input } from '../components/ui/input';
+import { Label } from '../components/ui/label';
+import { Card } from '../components/ui/card';
+import { toast } from 'sonner';
+import Navbar from '../components/Navbar';
+import { ArrowRight, ArrowLeft, Mail, CheckCircle } from 'lucide-react';
+
+const ForgotPassword = () => {
+ const { forgotPassword } = useAuth();
+ const [loading, setLoading] = useState(false);
+ const [submitted, setSubmitted] = useState(false);
+ const [email, setEmail] = useState('');
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ setLoading(true);
+
+ try {
+ await forgotPassword(email);
+ setSubmitted(true);
+ toast.success('Password reset email sent!');
+ } catch (error) {
+ toast.error(error.response?.data?.detail || 'Failed to send reset email. Please try again.');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+ {!submitted ? (
+ <>
+
+
+
+
+
+ Forgot Password?
+
+
+ No worries! Enter your email and we'll send you reset instructions.
+
+
+
+
+ >
+ ) : (
+
+
+
+
+
+ Check Your Email
+
+
+ If an account exists for {email},
+ you will receive a password reset link shortly.
+
+
+ The link will expire in 1 hour. If you don't see the email, check your spam folder.
+
+
+
+
+
+ )}
+
+
+
+ );
+};
+
+export default ForgotPassword;
diff --git a/src/pages/Login.js b/src/pages/Login.js
index 28c5abc..f8e61a1 100644
--- a/src/pages/Login.js
+++ b/src/pages/Login.js
@@ -3,6 +3,7 @@ import { useNavigate, Link } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
import { Button } from '../components/ui/button';
import { Input } from '../components/ui/input';
+import { PasswordInput } from '../components/ui/password-input';
import { Label } from '../components/ui/label';
import { Card } from '../components/ui/card';
import { toast } from 'sonner';
@@ -30,7 +31,16 @@ const Login = () => {
try {
const user = await login(formData.email, formData.password);
toast.success('Login successful!');
-
+
+ // Check if password change is required
+ if (user.force_password_change) {
+ toast.warning('You must change your password before continuing', {
+ duration: 5000
+ });
+ navigate('/change-password-required');
+ return;
+ }
+
if (user.role === 'admin') {
navigate('/admin');
} else {
@@ -82,11 +92,15 @@ const Login = () => {
-
-
+
+
+ Forgot password?
+
+
+ {
const { user } = useAuth();
const [loading, setLoading] = useState(false);
const [profileData, setProfileData] = useState(null);
+ const [passwordDialogOpen, setPasswordDialogOpen] = useState(false);
const [formData, setFormData] = useState({
first_name: '',
last_name: '',
@@ -117,6 +119,18 @@ const Profile = () => {
+
+
+
+
{/* Editable Form */}
@@ -222,6 +236,11 @@ const Profile = () => {
+
+
);
diff --git a/src/pages/ResetPassword.js b/src/pages/ResetPassword.js
new file mode 100644
index 0000000..561b836
--- /dev/null
+++ b/src/pages/ResetPassword.js
@@ -0,0 +1,147 @@
+import React, { useState, useEffect } from 'react';
+import { useNavigate, useSearchParams, Link } from 'react-router-dom';
+import { useAuth } from '../context/AuthContext';
+import { Button } from '../components/ui/button';
+import { Input } from '../components/ui/input';
+import { Label } from '../components/ui/label';
+import { Card } from '../components/ui/card';
+import { toast } from 'sonner';
+import Navbar from '../components/Navbar';
+import { ArrowRight, Lock, AlertCircle } from 'lucide-react';
+
+const ResetPassword = () => {
+ const navigate = useNavigate();
+ const [searchParams] = useSearchParams();
+ const { resetPassword } = useAuth();
+ const [loading, setLoading] = useState(false);
+ const [token, setToken] = useState('');
+ const [formData, setFormData] = useState({
+ newPassword: '',
+ confirmPassword: ''
+ });
+
+ useEffect(() => {
+ const tokenParam = searchParams.get('token');
+ if (!tokenParam) {
+ toast.error('Invalid reset link');
+ navigate('/login');
+ } else {
+ setToken(tokenParam);
+ }
+ }, [searchParams, navigate]);
+
+ const handleInputChange = (e) => {
+ const { name, value } = e.target;
+ setFormData(prev => ({ ...prev, [name]: value }));
+ };
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+
+ if (formData.newPassword.length < 6) {
+ toast.error('Password must be at least 6 characters');
+ return;
+ }
+
+ if (formData.newPassword !== formData.confirmPassword) {
+ toast.error('Passwords do not match');
+ return;
+ }
+
+ setLoading(true);
+
+ try {
+ await resetPassword(token, formData.newPassword);
+ toast.success('Password reset successful! Please login with your new password.');
+ navigate('/login');
+ } catch (error) {
+ const errorMessage = error.response?.data?.detail || 'Failed to reset password. The link may have expired.';
+ toast.error(errorMessage);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ Reset Password
+
+
+ Enter your new password below.
+
+
+
+
+
+
+
+ );
+};
+
+export default ResetPassword;
diff --git a/src/pages/admin/AdminMembers.js b/src/pages/admin/AdminMembers.js
index 290eb1c..aeef5f5 100644
--- a/src/pages/admin/AdminMembers.js
+++ b/src/pages/admin/AdminMembers.js
@@ -21,12 +21,6 @@ const AdminMembers = () => {
const [paymentDialogOpen, setPaymentDialogOpen] = useState(false);
const [selectedUserForPayment, setSelectedUserForPayment] = useState(null);
- const tabs = [
- { name: 'All Users', path: '/admin/users' },
- { name: 'Staff', path: '/admin/staff' },
- { name: 'Members', path: '/admin/members' }
- ];
-
useEffect(() => {
fetchMembers();
}, []);
@@ -105,30 +99,6 @@ const AdminMembers = () => {
- {/* Tab Navigation */}
-
-
-
-
{/* Stats */}
diff --git a/src/pages/admin/AdminUserView.js b/src/pages/admin/AdminUserView.js
index 9d0843f..f3b015e 100644
--- a/src/pages/admin/AdminUserView.js
+++ b/src/pages/admin/AdminUserView.js
@@ -4,7 +4,7 @@ import api from '../../utils/api';
import { Card } from '../../components/ui/card';
import { Button } from '../../components/ui/button';
import { Badge } from '../../components/ui/badge';
-import { ArrowLeft, Mail, Phone, MapPin, Calendar } from 'lucide-react';
+import { ArrowLeft, Mail, Phone, MapPin, Calendar, Lock, AlertTriangle } from 'lucide-react';
import { toast } from 'sonner';
const AdminUserView = () => {
@@ -12,6 +12,8 @@ const AdminUserView = () => {
const navigate = useNavigate();
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
+ const [resetPasswordLoading, setResetPasswordLoading] = useState(false);
+ const [resendVerificationLoading, setResendVerificationLoading] = useState(false);
useEffect(() => {
fetchUserProfile();
@@ -29,6 +31,52 @@ const AdminUserView = () => {
}
};
+ const handleResetPassword = async () => {
+ const confirmed = window.confirm(
+ `Reset password for ${user.first_name} ${user.last_name}?\n\n` +
+ `A temporary password will be emailed to ${user.email}.\n` +
+ `They will be required to change it on next login.`
+ );
+
+ if (!confirmed) return;
+
+ setResetPasswordLoading(true);
+
+ try {
+ await api.put(`/admin/users/${userId}/reset-password`, {
+ force_change: true
+ });
+ toast.success(`Password reset email sent to ${user.email}`);
+ } catch (error) {
+ const errorMessage = error.response?.data?.detail || 'Failed to reset password';
+ toast.error(errorMessage);
+ } finally {
+ setResetPasswordLoading(false);
+ }
+ };
+
+ const handleResendVerification = async () => {
+ const confirmed = window.confirm(
+ `Resend verification email to ${user.email}?`
+ );
+
+ if (!confirmed) return;
+
+ setResendVerificationLoading(true);
+
+ try {
+ await api.post(`/admin/users/${userId}/resend-verification`);
+ toast.success(`Verification email sent to ${user.email}`);
+ // Refresh user data to get updated email_verified status if changed
+ await fetchUserProfile();
+ } catch (error) {
+ const errorMessage = error.response?.data?.detail || 'Failed to send verification email';
+ toast.error(errorMessage);
+ } finally {
+ setResendVerificationLoading(false);
+ }
+ };
+
if (loading) return Loading...
;
if (!user) return null;
@@ -86,6 +134,41 @@ const AdminUserView = () => {
+ {/* Admin Actions */}
+
+
+ Admin Actions
+
+
+
+
+ {!user.email_verified && (
+
+ )}
+
+
+
+
User will receive a temporary password via email
+
+
+
+
{/* Additional Details */}
diff --git a/src/pages/admin/AdminUsers.js b/src/pages/admin/AdminUsers.js
index 9987d50..b02ce9d 100644
--- a/src/pages/admin/AdminUsers.js
+++ b/src/pages/admin/AdminUsers.js
@@ -7,7 +7,7 @@ import { Badge } from '../../components/ui/badge';
import { Input } from '../../components/ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../components/ui/select';
import { toast } from 'sonner';
-import { Users, Search, CheckCircle, Clock } from 'lucide-react';
+import { Users, Search, CheckCircle, Clock, Mail, Eye } from 'lucide-react';
const AdminUsers = () => {
const navigate = useNavigate();
@@ -17,12 +17,7 @@ const AdminUsers = () => {
const [loading, setLoading] = useState(true);
const [searchQuery, setSearchQuery] = useState('');
const [statusFilter, setStatusFilter] = useState('all');
-
- const tabs = [
- { name: 'All Users', path: '/admin/users' },
- { name: 'Staff', path: '/admin/staff' },
- { name: 'Members', path: '/admin/members' }
- ];
+ const [resendingUserId, setResendingUserId] = useState(null);
useEffect(() => {
fetchUsers();
@@ -80,6 +75,25 @@ const AdminUsers = () => {
);
};
+ const handleAdminResendVerification = async (userId, userEmail) => {
+ const confirmed = window.confirm(
+ `Resend verification email to ${userEmail}?`
+ );
+
+ if (!confirmed) return;
+
+ setResendingUserId(userId);
+ try {
+ await api.post(`/admin/users/${userId}/resend-verification`);
+ toast.success(`Verification email sent to ${userEmail}`);
+ } catch (error) {
+ const errorMessage = error.response?.data?.detail || 'Failed to send verification email';
+ toast.error(errorMessage);
+ } finally {
+ setResendingUserId(null);
+ }
+ };
+
return (
<>
@@ -91,30 +105,6 @@ const AdminUsers = () => {
- {/* Tab Navigation */}
-
-
-
-
{/* Filters */}
@@ -176,6 +166,30 @@ const AdminUsers = () => {
)}
+
+
+
+ {!user.email_verified && (
+
+ )}
+
))}