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 ( + + + +
+
+ +
+ + Change Password + +
+ + Update your password to keep your account secure. + +
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ + + + + +
+
+
+ ); +}; + +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 ( +
+

Loading...

+
+ ); + } + + 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. +

+
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+
+ +
+

Password Requirements:

+
    +
  • At least 6 characters long
  • +
  • Both passwords must match
  • +
+
+
+
+ + + +
+ +
+
+
+
+
+ ); +}; + +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 ( +
+ + +
+
+ + + Back to Login + +
+ + + {!submitted ? ( + <> +
+
+ +
+

+ Forgot Password? +

+

+ No worries! Enter your email and we'll send you reset instructions. +

+
+ +
+
+ + setEmail(e.target.value)} + placeholder="your.email@example.com" + className="h-14 rounded-xl border-2 border-[#EAE0D5] focus:border-[#E07A5F]" + /> +
+ + + +

+ Remember your password?{' '} + + Login here + +

+
+ + ) : ( +
+
+ +
+

+ 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. +

+
+ +
+
+ + +
+ +
+ + +
+ +
+
+ +
+

Password Requirements:

+
    +
  • At least 6 characters long
  • +
  • Both passwords must match
  • +
+
+
+
+ + + +

+ Remember your password?{' '} + + Login here + +

+
+
+
+
+ ); +}; + +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 && ( + + )} +
))}