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 Register from './pages/Register';
|
||||||
import Login from './pages/Login';
|
import Login from './pages/Login';
|
||||||
import VerifyEmail from './pages/VerifyEmail';
|
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 Dashboard from './pages/Dashboard';
|
||||||
import Profile from './pages/Profile';
|
import Profile from './pages/Profile';
|
||||||
import Events from './pages/Events';
|
import Events from './pages/Events';
|
||||||
@@ -22,6 +25,7 @@ import AdminApprovals from './pages/admin/AdminApprovals';
|
|||||||
import AdminPlans from './pages/admin/AdminPlans';
|
import AdminPlans from './pages/admin/AdminPlans';
|
||||||
import AdminLayout from './layouts/AdminLayout';
|
import AdminLayout from './layouts/AdminLayout';
|
||||||
import { AuthProvider, useAuth } from './context/AuthContext';
|
import { AuthProvider, useAuth } from './context/AuthContext';
|
||||||
|
import MemberRoute from './components/MemberRoute';
|
||||||
|
|
||||||
const PrivateRoute = ({ children, adminOnly = false }) => {
|
const PrivateRoute = ({ children, adminOnly = false }) => {
|
||||||
const { user, loading } = useAuth();
|
const { user, loading } = useAuth();
|
||||||
@@ -50,6 +54,13 @@ function App() {
|
|||||||
<Route path="/register" element={<Register />} />
|
<Route path="/register" element={<Register />} />
|
||||||
<Route path="/login" element={<Login />} />
|
<Route path="/login" element={<Login />} />
|
||||||
<Route path="/verify-email" element={<VerifyEmail />} />
|
<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="/plans" element={<Plans />} />
|
||||||
<Route path="/payment-success" element={<PaymentSuccess />} />
|
<Route path="/payment-success" element={<PaymentSuccess />} />
|
||||||
<Route path="/payment-cancel" element={<PaymentCancel />} />
|
<Route path="/payment-cancel" element={<PaymentCancel />} />
|
||||||
@@ -65,14 +76,14 @@ function App() {
|
|||||||
</PrivateRoute>
|
</PrivateRoute>
|
||||||
} />
|
} />
|
||||||
<Route path="/events" element={
|
<Route path="/events" element={
|
||||||
<PrivateRoute>
|
<MemberRoute>
|
||||||
<Events />
|
<Events />
|
||||||
</PrivateRoute>
|
</MemberRoute>
|
||||||
} />
|
} />
|
||||||
<Route path="/events/:id" element={
|
<Route path="/events/:id" element={
|
||||||
<PrivateRoute>
|
<MemberRoute>
|
||||||
<EventDetails />
|
<EventDetails />
|
||||||
</PrivateRoute>
|
</MemberRoute>
|
||||||
} />
|
} />
|
||||||
|
|
||||||
<Route path="/admin" element={
|
<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 React from 'react';
|
||||||
import { Label } from '../ui/label';
|
import { Label } from '../ui/label';
|
||||||
import { Input } from '../ui/input';
|
import { Input } from '../ui/input';
|
||||||
|
import { PasswordInput } from '../ui/password-input';
|
||||||
|
|
||||||
const RegistrationStep4 = ({ formData, handleInputChange }) => {
|
const RegistrationStep4 = ({ formData, handleInputChange }) => {
|
||||||
return (
|
return (
|
||||||
@@ -34,10 +35,9 @@ const RegistrationStep4 = ({ formData, handleInputChange }) => {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="password">Password *</Label>
|
<Label htmlFor="password">Password *</Label>
|
||||||
<Input
|
<PasswordInput
|
||||||
id="password"
|
id="password"
|
||||||
name="password"
|
name="password"
|
||||||
type="password"
|
|
||||||
required
|
required
|
||||||
minLength={6}
|
minLength={6}
|
||||||
value={formData.password}
|
value={formData.password}
|
||||||
@@ -53,10 +53,9 @@ const RegistrationStep4 = ({ formData, handleInputChange }) => {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="confirmPassword">Repeat Password *</Label>
|
<Label htmlFor="confirmPassword">Repeat Password *</Label>
|
||||||
<Input
|
<PasswordInput
|
||||||
id="confirmPassword"
|
id="confirmPassword"
|
||||||
name="confirmPassword"
|
name="confirmPassword"
|
||||||
type="password"
|
|
||||||
required
|
required
|
||||||
value={formData.confirmPassword}
|
value={formData.confirmPassword}
|
||||||
onChange={handleInputChange}
|
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 (
|
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}
|
{children}
|
||||||
</AuthContext.Provider>
|
</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 { Button } from '../components/ui/button';
|
||||||
import { Badge } from '../components/ui/badge';
|
import { Badge } from '../components/ui/badge';
|
||||||
import Navbar from '../components/Navbar';
|
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 Dashboard = () => {
|
||||||
const { user } = useAuth();
|
const { user, resendVerificationEmail } = useAuth();
|
||||||
const [events, setEvents] = useState([]);
|
const [events, setEvents] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [resendLoading, setResendLoading] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchUpcomingEvents();
|
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 getStatusBadge = (status) => {
|
||||||
const statusConfig = {
|
const statusConfig = {
|
||||||
pending_email: { icon: Clock, label: 'Pending Email', className: 'bg-[#F2CC8F] text-[#3D405B]' },
|
pending_email: { icon: Clock, label: 'Pending Email', className: 'bg-[#F2CC8F] text-[#3D405B]' },
|
||||||
@@ -78,6 +93,33 @@ const Dashboard = () => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</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 */}
|
{/* Status Card */}
|
||||||
<Card className="p-8 bg-white rounded-2xl border border-[#EAE0D5] shadow-lg mb-8" data-testid="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">
|
<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 { useAuth } from '../context/AuthContext';
|
||||||
import { Button } from '../components/ui/button';
|
import { Button } from '../components/ui/button';
|
||||||
import { Input } from '../components/ui/input';
|
import { Input } from '../components/ui/input';
|
||||||
|
import { PasswordInput } from '../components/ui/password-input';
|
||||||
import { Label } from '../components/ui/label';
|
import { Label } from '../components/ui/label';
|
||||||
import { Card } from '../components/ui/card';
|
import { Card } from '../components/ui/card';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
@@ -31,6 +32,15 @@ const Login = () => {
|
|||||||
const user = await login(formData.email, formData.password);
|
const user = await login(formData.email, formData.password);
|
||||||
toast.success('Login successful!');
|
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') {
|
if (user.role === 'admin') {
|
||||||
navigate('/admin');
|
navigate('/admin');
|
||||||
} else {
|
} else {
|
||||||
@@ -82,11 +92,15 @@ const Login = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
<Label htmlFor="password">Password</Label>
|
<Label htmlFor="password">Password</Label>
|
||||||
<Input
|
<Link to="/forgot-password" className="text-sm text-[#E07A5F] hover:underline">
|
||||||
|
Forgot password?
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<PasswordInput
|
||||||
id="password"
|
id="password"
|
||||||
name="password"
|
name="password"
|
||||||
type="password"
|
|
||||||
required
|
required
|
||||||
value={formData.password}
|
value={formData.password}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
|
|||||||
@@ -7,12 +7,14 @@ import { Input } from '../components/ui/input';
|
|||||||
import { Label } from '../components/ui/label';
|
import { Label } from '../components/ui/label';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import Navbar from '../components/Navbar';
|
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 Profile = () => {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [profileData, setProfileData] = useState(null);
|
const [profileData, setProfileData] = useState(null);
|
||||||
|
const [passwordDialogOpen, setPasswordDialogOpen] = useState(false);
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
first_name: '',
|
first_name: '',
|
||||||
last_name: '',
|
last_name: '',
|
||||||
@@ -117,6 +119,18 @@ const Profile = () => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
{/* Editable Form */}
|
{/* Editable Form */}
|
||||||
@@ -222,6 +236,11 @@ const Profile = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<ChangePasswordDialog
|
||||||
|
open={passwordDialogOpen}
|
||||||
|
onOpenChange={setPasswordDialogOpen}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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 [paymentDialogOpen, setPaymentDialogOpen] = useState(false);
|
||||||
const [selectedUserForPayment, setSelectedUserForPayment] = useState(null);
|
const [selectedUserForPayment, setSelectedUserForPayment] = useState(null);
|
||||||
|
|
||||||
const tabs = [
|
|
||||||
{ name: 'All Users', path: '/admin/users' },
|
|
||||||
{ name: 'Staff', path: '/admin/staff' },
|
|
||||||
{ name: 'Members', path: '/admin/members' }
|
|
||||||
];
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchMembers();
|
fetchMembers();
|
||||||
}, []);
|
}, []);
|
||||||
@@ -105,30 +99,6 @@ const AdminMembers = () => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</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 */}
|
{/* Stats */}
|
||||||
<div className="grid md:grid-cols-4 gap-4 mb-8">
|
<div className="grid md:grid-cols-4 gap-4 mb-8">
|
||||||
<Card className="p-6 bg-white rounded-2xl border border-[#EAE0D5]">
|
<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 { Card } from '../../components/ui/card';
|
||||||
import { Button } from '../../components/ui/button';
|
import { Button } from '../../components/ui/button';
|
||||||
import { Badge } from '../../components/ui/badge';
|
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';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
const AdminUserView = () => {
|
const AdminUserView = () => {
|
||||||
@@ -12,6 +12,8 @@ const AdminUserView = () => {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [user, setUser] = useState(null);
|
const [user, setUser] = useState(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [resetPasswordLoading, setResetPasswordLoading] = useState(false);
|
||||||
|
const [resendVerificationLoading, setResendVerificationLoading] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchUserProfile();
|
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 (loading) return <div>Loading...</div>;
|
||||||
if (!user) return null;
|
if (!user) return null;
|
||||||
|
|
||||||
@@ -86,6 +134,41 @@ const AdminUserView = () => {
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</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 */}
|
{/* Additional Details */}
|
||||||
<Card className="p-8 bg-white rounded-2xl border border-[#EAE0D5]">
|
<Card className="p-8 bg-white rounded-2xl border border-[#EAE0D5]">
|
||||||
<h2 className="text-2xl font-semibold fraunces text-[#3D405B] mb-6">
|
<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 { Input } from '../../components/ui/input';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../components/ui/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../components/ui/select';
|
||||||
import { toast } from 'sonner';
|
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 AdminUsers = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -17,12 +17,7 @@ const AdminUsers = () => {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [statusFilter, setStatusFilter] = useState('all');
|
const [statusFilter, setStatusFilter] = useState('all');
|
||||||
|
const [resendingUserId, setResendingUserId] = useState(null);
|
||||||
const tabs = [
|
|
||||||
{ name: 'All Users', path: '/admin/users' },
|
|
||||||
{ name: 'Staff', path: '/admin/staff' },
|
|
||||||
{ name: 'Members', path: '/admin/members' }
|
|
||||||
];
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchUsers();
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
@@ -91,30 +105,6 @@ const AdminUsers = () => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</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 */}
|
{/* Filters */}
|
||||||
<Card className="p-6 bg-white rounded-2xl border border-[#EAE0D5] mb-8">
|
<Card className="p-6 bg-white rounded-2xl border border-[#EAE0D5] mb-8">
|
||||||
<div className="grid md:grid-cols-2 gap-4">
|
<div className="grid md:grid-cols-2 gap-4">
|
||||||
@@ -176,6 +166,30 @@ const AdminUsers = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
|
|||||||
Reference in New Issue
Block a user