Email SMTP Fix
This commit is contained in:
19
src/App.js
19
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() {
|
||||
<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={
|
||||
|
||||
150
src/components/ChangePasswordDialog.js
Normal file
150
src/components/ChangePasswordDialog.js
Normal 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;
|
||||
45
src/components/MemberRoute.js
Normal file
45
src/components/MemberRoute.js
Normal 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;
|
||||
@@ -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}
|
||||
|
||||
36
src/components/ui/password-input.jsx
Normal file
36
src/components/ui/password-input.jsx
Normal 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 }
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
184
src/pages/ChangePasswordRequired.js
Normal file
184
src/pages/ChangePasswordRequired.js
Normal 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;
|
||||
@@ -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
121
src/pages/ForgotPassword.js
Normal 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;
|
||||
@@ -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}
|
||||
|
||||
@@ -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
147
src/pages/ResetPassword.js
Normal 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;
|
||||
@@ -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]">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user