Email SMTP Fix

This commit is contained in:
Koncept Kit
2025-12-07 16:59:21 +07:00
parent 7b8ee6442a
commit 79cebd205c
15 changed files with 978 additions and 78 deletions

View File

@@ -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() {
<Route path="/register" element={<Register />} />
<Route path="/login" element={<Login />} />
<Route path="/verify-email" element={<VerifyEmail />} />
<Route path="/forgot-password" element={<ForgotPassword />} />
<Route path="/reset-password" element={<ResetPassword />} />
<Route path="/change-password-required" element={
<PrivateRoute>
<ChangePasswordRequired />
</PrivateRoute>
} />
<Route path="/plans" element={<Plans />} />
<Route path="/payment-success" element={<PaymentSuccess />} />
<Route path="/payment-cancel" element={<PaymentCancel />} />
@@ -65,14 +76,14 @@ function App() {
</PrivateRoute>
} />
<Route path="/events" element={
<PrivateRoute>
<MemberRoute>
<Events />
</PrivateRoute>
</MemberRoute>
} />
<Route path="/events/:id" element={
<PrivateRoute>
<MemberRoute>
<EventDetails />
</PrivateRoute>
</MemberRoute>
} />
<Route path="/admin" element={

View File

@@ -0,0 +1,150 @@
import React, { useState } from 'react';
import { useAuth } from '../context/AuthContext';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter
} from './ui/dialog';
import { Button } from './ui/button';
import { Input } from './ui/input';
import { Label } from './ui/label';
import { toast } from 'sonner';
import { Lock } from 'lucide-react';
const ChangePasswordDialog = ({ open, onOpenChange }) => {
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 (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md bg-white">
<DialogHeader>
<div className="flex items-center gap-2 mb-2">
<div className="inline-flex items-center justify-center w-10 h-10 rounded-full bg-[#FFF3E0]">
<Lock className="h-5 w-5 text-[#E07A5F]" />
</div>
<DialogTitle className="text-2xl font-semibold text-[#3D405B]">
Change Password
</DialogTitle>
</div>
<DialogDescription className="text-[#6B708D]">
Update your password to keep your account secure.
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4 mt-4">
<div>
<Label htmlFor="currentPassword">Current Password</Label>
<Input
id="currentPassword"
name="currentPassword"
type="password"
required
value={formData.currentPassword}
onChange={handleInputChange}
placeholder="Enter current password"
className="h-12 rounded-xl border-2 border-[#EAE0D5] focus:border-[#E07A5F]"
/>
</div>
<div>
<Label htmlFor="newPassword">New Password</Label>
<Input
id="newPassword"
name="newPassword"
type="password"
required
value={formData.newPassword}
onChange={handleInputChange}
placeholder="Enter new password (min. 6 characters)"
className="h-12 rounded-xl border-2 border-[#EAE0D5] focus:border-[#E07A5F]"
/>
</div>
<div>
<Label htmlFor="confirmPassword">Confirm New Password</Label>
<Input
id="confirmPassword"
name="confirmPassword"
type="password"
required
value={formData.confirmPassword}
onChange={handleInputChange}
placeholder="Re-enter new password"
className="h-12 rounded-xl border-2 border-[#EAE0D5] focus:border-[#E07A5F]"
/>
</div>
<DialogFooter className="mt-6">
<Button
type="button"
variant="outline"
onClick={() => onOpenChange(false)}
className="rounded-full px-6"
>
Cancel
</Button>
<Button
type="submit"
disabled={loading}
className="bg-[#E07A5F] text-white hover:bg-[#D0694E] rounded-full px-6 disabled:opacity-50"
>
{loading ? 'Changing...' : 'Change Password'}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
};
export default ChangePasswordDialog;

View File

@@ -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 (
<div className="min-h-screen flex items-center justify-center bg-[#FDFCF8]">
<p className="text-[#6B708D]">Loading...</p>
</div>
);
}
if (!user) {
return <Navigate to="/login" />;
}
// Allow admins to bypass payment requirement
if (user.role === 'admin') {
return children;
}
// Check if user is an active member
if (user.status !== 'active') {
return <Navigate to="/plans" />;
}
return children;
};
export default MemberRoute;

View File

@@ -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 }) => {
<div>
<Label htmlFor="password">Password *</Label>
<Input
<PasswordInput
id="password"
name="password"
type="password"
required
minLength={6}
value={formData.password}
@@ -53,10 +53,9 @@ const RegistrationStep4 = ({ formData, handleInputChange }) => {
<div>
<Label htmlFor="confirmPassword">Repeat Password *</Label>
<Input
<PasswordInput
id="confirmPassword"
name="confirmPassword"
type="password"
required
value={formData.confirmPassword}
onChange={handleInputChange}

View File

@@ -0,0 +1,36 @@
import * as React from "react"
import { Eye, EyeOff } from "lucide-react"
import { cn } from "@/lib/utils"
const PasswordInput = React.forwardRef(({ className, ...props }, ref) => {
const [showPassword, setShowPassword] = React.useState(false)
return (
<div className="relative">
<input
type={showPassword ? "text" : "password"}
className={cn(
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 pr-10 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
ref={ref}
{...props}
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-[#6B708D] hover:text-[#3D405B] transition-colors focus:outline-none"
tabIndex={-1}
>
{showPassword ? (
<EyeOff className="h-5 w-5" />
) : (
<Eye className="h-5 w-5" />
)}
</button>
</div>
)
})
PasswordInput.displayName = "PasswordInput"
export { PasswordInput }

View File

@@ -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 (
<AuthContext.Provider value={{ user, token, login, logout, register, refreshUser, loading }}>
<AuthContext.Provider value={{
user,
token,
login,
logout,
register,
refreshUser,
forgotPassword,
resetPassword,
changePassword,
resendVerificationEmail,
loading
}}>
{children}
</AuthContext.Provider>
);

View File

@@ -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 (
<div className="min-h-screen bg-[#FDFCF8]">
<Navbar />
<div className="max-w-md mx-auto px-6 py-12">
<Card className="p-8 md:p-12 bg-white rounded-2xl border border-[#EAE0D5] shadow-lg">
<div className="mb-8 text-center">
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-[#FFEBEE] mb-4">
<AlertTriangle className="h-8 w-8 text-[#E07A5F]" />
</div>
<h1 className="text-4xl md:text-5xl font-semibold fraunces text-[#3D405B] mb-4">
Password Change Required
</h1>
<p className="text-lg text-[#6B708D]">
Your password was reset by an administrator. Please create a new password to continue.
</p>
</div>
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<Label htmlFor="currentPassword">Current/Temporary Password</Label>
<Input
id="currentPassword"
name="currentPassword"
type="password"
required
value={formData.currentPassword}
onChange={handleInputChange}
placeholder="Enter temporary password"
className="h-14 rounded-xl border-2 border-[#EAE0D5] focus:border-[#E07A5F]"
/>
</div>
<div>
<Label htmlFor="newPassword">New Password</Label>
<Input
id="newPassword"
name="newPassword"
type="password"
required
value={formData.newPassword}
onChange={handleInputChange}
placeholder="Enter new password (min. 6 characters)"
className="h-14 rounded-xl border-2 border-[#EAE0D5] focus:border-[#E07A5F]"
/>
</div>
<div>
<Label htmlFor="confirmPassword">Confirm New Password</Label>
<Input
id="confirmPassword"
name="confirmPassword"
type="password"
required
value={formData.confirmPassword}
onChange={handleInputChange}
placeholder="Re-enter new password"
className="h-14 rounded-xl border-2 border-[#EAE0D5] focus:border-[#E07A5F]"
/>
</div>
<div className="bg-[#FFF3E0] border-l-4 border-[#E07A5F] p-4 rounded-lg">
<div className="flex items-start">
<Lock className="h-5 w-5 text-[#E07A5F] mr-2 mt-0.5 flex-shrink-0" />
<div className="text-sm text-[#6B708D]">
<p className="font-medium text-[#3D405B] mb-1">Password Requirements:</p>
<ul className="list-disc list-inside space-y-1">
<li>At least 6 characters long</li>
<li>Both passwords must match</li>
</ul>
</div>
</div>
</div>
<Button
type="submit"
disabled={loading}
className="w-full bg-[#E07A5F] text-white hover:bg-[#D0694E] rounded-full py-6 text-lg font-medium shadow-lg hover:scale-105 transition-transform disabled:opacity-50"
>
{loading ? 'Changing Password...' : 'Change Password'}
<ArrowRight className="ml-2 h-5 w-5" />
</Button>
<div className="text-center pt-4 border-t border-[#EAE0D5]">
<button
type="button"
onClick={handleLogout}
className="text-[#6B708D] hover:text-[#E07A5F] text-sm underline"
>
Logout instead
</button>
</div>
</form>
</Card>
</div>
</div>
);
};
export default ChangePasswordRequired;

View File

@@ -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 = () => {
</p>
</div>
{/* Email Verification Alert */}
{user && !user.email_verified && (
<Card className="p-6 bg-[#FFF3E0] border-2 border-[#E07A5F] mb-8">
<div className="flex items-start gap-4">
<AlertCircle className="h-6 w-6 text-[#E07A5F] flex-shrink-0 mt-1" />
<div className="flex-1">
<h3 className="text-lg font-semibold text-[#3D405B] mb-2">
Verify Your Email Address
</h3>
<p className="text-[#6B708D] mb-4">
Please verify your email address to complete your registration.
Check your inbox for the verification link.
</p>
<Button
onClick={handleResendVerification}
disabled={resendLoading}
variant="outline"
className="border-2 border-[#E07A5F] text-[#E07A5F] hover:bg-[#E07A5F] hover:text-white"
>
<Mail className="h-4 w-4 mr-2" />
{resendLoading ? 'Sending...' : 'Resend Verification Email'}
</Button>
</div>
</div>
</Card>
)}
{/* Status Card */}
<Card className="p-8 bg-white rounded-2xl border border-[#EAE0D5] shadow-lg mb-8" data-testid="status-card">
<div className="flex items-start justify-between flex-wrap gap-4">

121
src/pages/ForgotPassword.js Normal file
View File

@@ -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 (
<div className="min-h-screen bg-[#FDFCF8]">
<Navbar />
<div className="max-w-md mx-auto px-6 py-12">
<div className="mb-8">
<Link to="/login" className="inline-flex items-center text-[#6B708D] hover:text-[#E07A5F] transition-colors">
<ArrowLeft className="h-4 w-4 mr-2" />
Back to Login
</Link>
</div>
<Card className="p-8 md:p-12 bg-white rounded-2xl border border-[#EAE0D5] shadow-lg">
{!submitted ? (
<>
<div className="mb-8 text-center">
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-[#FFF3E0] mb-4">
<Mail className="h-8 w-8 text-[#E07A5F]" />
</div>
<h1 className="text-4xl md:text-5xl font-semibold fraunces text-[#3D405B] mb-4">
Forgot Password?
</h1>
<p className="text-lg text-[#6B708D]">
No worries! Enter your email and we'll send you reset instructions.
</p>
</div>
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<Label htmlFor="email">Email Address</Label>
<Input
id="email"
name="email"
type="email"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="your.email@example.com"
className="h-14 rounded-xl border-2 border-[#EAE0D5] focus:border-[#E07A5F]"
/>
</div>
<Button
type="submit"
disabled={loading}
className="w-full bg-[#E07A5F] text-white hover:bg-[#D0694E] rounded-full py-6 text-lg font-medium shadow-lg hover:scale-105 transition-transform disabled:opacity-50"
>
{loading ? 'Sending...' : 'Send Reset Link'}
<ArrowRight className="ml-2 h-5 w-5" />
</Button>
<p className="text-center text-[#6B708D]">
Remember your password?{' '}
<Link to="/login" className="text-[#E07A5F] hover:underline font-medium">
Login here
</Link>
</p>
</form>
</>
) : (
<div className="text-center">
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-[#E8F5E9] mb-6">
<CheckCircle className="h-8 w-8 text-[#4CAF50]" />
</div>
<h1 className="text-4xl md:text-5xl font-semibold fraunces text-[#3D405B] mb-4">
Check Your Email
</h1>
<p className="text-lg text-[#6B708D] mb-8">
If an account exists for <span className="font-medium text-[#3D405B]">{email}</span>,
you will receive a password reset link shortly.
</p>
<p className="text-sm text-[#6B708D] mb-8">
The link will expire in 1 hour. If you don't see the email, check your spam folder.
</p>
<Link to="/login">
<Button className="bg-[#E07A5F] text-white hover:bg-[#D0694E] rounded-full px-8 py-6 text-lg font-medium shadow-lg hover:scale-105 transition-transform">
Return to Login
<ArrowRight className="ml-2 h-5 w-5" />
</Button>
</Link>
</div>
)}
</Card>
</div>
</div>
);
};
export default ForgotPassword;

View File

@@ -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 = () => {
</div>
<div>
<Label htmlFor="password">Password</Label>
<Input
<div className="flex items-center justify-between mb-2">
<Label htmlFor="password">Password</Label>
<Link to="/forgot-password" className="text-sm text-[#E07A5F] hover:underline">
Forgot password?
</Link>
</div>
<PasswordInput
id="password"
name="password"
type="password"
required
value={formData.password}
onChange={handleInputChange}

View File

@@ -7,12 +7,14 @@ import { Input } from '../components/ui/input';
import { Label } from '../components/ui/label';
import { toast } from 'sonner';
import Navbar from '../components/Navbar';
import { User, Save } from 'lucide-react';
import { User, Save, Lock } from 'lucide-react';
import ChangePasswordDialog from '../components/ChangePasswordDialog';
const Profile = () => {
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 = () => {
</p>
</div>
</div>
<div className="mt-6">
<Button
type="button"
onClick={() => setPasswordDialogOpen(true)}
variant="outline"
className="border-2 border-[#E07A5F] text-[#E07A5F] hover:bg-[#FFF3E0] rounded-full px-6 py-3"
>
<Lock className="h-4 w-4 mr-2" />
Change Password
</Button>
</div>
</div>
{/* Editable Form */}
@@ -222,6 +236,11 @@ const Profile = () => {
</Button>
</form>
</Card>
<ChangePasswordDialog
open={passwordDialogOpen}
onOpenChange={setPasswordDialogOpen}
/>
</div>
</div>
);

147
src/pages/ResetPassword.js Normal file
View File

@@ -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 (
<div className="min-h-screen bg-[#FDFCF8]">
<Navbar />
<div className="max-w-md mx-auto px-6 py-12">
<Card className="p-8 md:p-12 bg-white rounded-2xl border border-[#EAE0D5] shadow-lg">
<div className="mb-8 text-center">
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-[#FFF3E0] mb-4">
<Lock className="h-8 w-8 text-[#E07A5F]" />
</div>
<h1 className="text-4xl md:text-5xl font-semibold fraunces text-[#3D405B] mb-4">
Reset Password
</h1>
<p className="text-lg text-[#6B708D]">
Enter your new password below.
</p>
</div>
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<Label htmlFor="newPassword">New Password</Label>
<Input
id="newPassword"
name="newPassword"
type="password"
required
value={formData.newPassword}
onChange={handleInputChange}
placeholder="Enter new password (min. 6 characters)"
className="h-14 rounded-xl border-2 border-[#EAE0D5] focus:border-[#E07A5F]"
/>
</div>
<div>
<Label htmlFor="confirmPassword">Confirm New Password</Label>
<Input
id="confirmPassword"
name="confirmPassword"
type="password"
required
value={formData.confirmPassword}
onChange={handleInputChange}
placeholder="Re-enter new password"
className="h-14 rounded-xl border-2 border-[#EAE0D5] focus:border-[#E07A5F]"
/>
</div>
<div className="bg-[#FFF3E0] border-l-4 border-[#E07A5F] p-4 rounded-lg">
<div className="flex items-start">
<AlertCircle className="h-5 w-5 text-[#E07A5F] mr-2 mt-0.5 flex-shrink-0" />
<div className="text-sm text-[#6B708D]">
<p className="font-medium text-[#3D405B] mb-1">Password Requirements:</p>
<ul className="list-disc list-inside space-y-1">
<li>At least 6 characters long</li>
<li>Both passwords must match</li>
</ul>
</div>
</div>
</div>
<Button
type="submit"
disabled={loading}
className="w-full bg-[#E07A5F] text-white hover:bg-[#D0694E] rounded-full py-6 text-lg font-medium shadow-lg hover:scale-105 transition-transform disabled:opacity-50"
>
{loading ? 'Resetting Password...' : 'Reset Password'}
<ArrowRight className="ml-2 h-5 w-5" />
</Button>
<p className="text-center text-[#6B708D]">
Remember your password?{' '}
<Link to="/login" className="text-[#E07A5F] hover:underline font-medium">
Login here
</Link>
</p>
</form>
</Card>
</div>
</div>
);
};
export default ResetPassword;

View File

@@ -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 = () => {
</p>
</div>
{/* Tab Navigation */}
<div className="border-b border-[#EAE0D5] mb-8">
<nav className="flex gap-8">
{tabs.map((tab) => (
<button
key={tab.path}
onClick={() => navigate(tab.path)}
className={`
pb-4 px-2 font-medium transition-colors relative
${location.pathname === tab.path
? 'text-[#E07A5F]'
: 'text-[#6B708D] hover:text-[#3D405B]'
}
`}
>
{tab.name}
{location.pathname === tab.path && (
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-[#E07A5F]" />
)}
</button>
))}
</nav>
</div>
{/* Stats */}
<div className="grid md:grid-cols-4 gap-4 mb-8">
<Card className="p-6 bg-white rounded-2xl border border-[#EAE0D5]">

View File

@@ -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 <div>Loading...</div>;
if (!user) return null;
@@ -86,6 +134,41 @@ const AdminUserView = () => {
</div>
</Card>
{/* Admin Actions */}
<Card className="p-6 bg-white rounded-2xl border border-[#EAE0D5] mb-8">
<h2 className="text-lg font-semibold fraunces text-[#3D405B] mb-4">
Admin Actions
</h2>
<div className="flex flex-wrap gap-3">
<Button
onClick={handleResetPassword}
disabled={resetPasswordLoading}
variant="outline"
className="border-2 border-[#E07A5F] text-[#E07A5F] hover:bg-[#FFF3E0] rounded-full px-4 py-2 disabled:opacity-50"
>
<Lock className="h-4 w-4 mr-2" />
{resetPasswordLoading ? 'Resetting...' : 'Reset Password'}
</Button>
{!user.email_verified && (
<Button
onClick={handleResendVerification}
disabled={resendVerificationLoading}
variant="outline"
className="border-2 border-[#E07A5F] text-[#E07A5F] hover:bg-[#FFF3E0] rounded-full px-4 py-2 disabled:opacity-50"
>
<Mail className="h-4 w-4 mr-2" />
{resendVerificationLoading ? 'Sending...' : 'Resend Verification Email'}
</Button>
)}
<div className="flex items-center gap-2 text-sm text-[#6B708D] ml-2">
<AlertTriangle className="h-4 w-4" />
<span>User will receive a temporary password via email</span>
</div>
</div>
</Card>
{/* Additional Details */}
<Card className="p-8 bg-white rounded-2xl border border-[#EAE0D5]">
<h2 className="text-2xl font-semibold fraunces text-[#3D405B] mb-6">

View File

@@ -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 (
<>
<div className="mb-8">
@@ -91,30 +105,6 @@ const AdminUsers = () => {
</p>
</div>
{/* Tab Navigation */}
<div className="border-b border-[#EAE0D5] mb-8">
<nav className="flex gap-8">
{tabs.map((tab) => (
<button
key={tab.path}
onClick={() => navigate(tab.path)}
className={`
pb-4 px-2 font-medium transition-colors relative
${location.pathname === tab.path
? 'text-[#E07A5F]'
: 'text-[#6B708D] hover:text-[#3D405B]'
}
`}
>
{tab.name}
{location.pathname === tab.path && (
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-[#E07A5F]" />
)}
</button>
))}
</nav>
</div>
{/* Filters */}
<Card className="p-6 bg-white rounded-2xl border border-[#EAE0D5] mb-8">
<div className="grid md:grid-cols-2 gap-4">
@@ -176,6 +166,30 @@ const AdminUsers = () => {
)}
</div>
</div>
<div className="flex items-center gap-2">
<Button
onClick={() => navigate(`/admin/users/${user.id}`)}
variant="ghost"
size="sm"
className="text-[#6B708D] hover:text-[#3D405B]"
>
<Eye className="h-4 w-4 mr-1" />
View
</Button>
{!user.email_verified && (
<Button
onClick={() => handleAdminResendVerification(user.id, user.email)}
disabled={resendingUserId === user.id}
variant="ghost"
size="sm"
className="text-[#E07A5F] hover:text-[#D0694E]"
>
<Mail className="h-4 w-4 mr-1" />
{resendingUserId === user.id ? 'Sending...' : 'Resend Verification'}
</Button>
)}
</div>
</div>
</Card>
))}