updates
This commit is contained in:
@@ -18,16 +18,21 @@ import {
|
||||
Eye,
|
||||
EyeOff,
|
||||
RefreshCw,
|
||||
KeyRound
|
||||
KeyRound,
|
||||
LogOut,
|
||||
Monitor
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'react-toastify';
|
||||
import authService from '../../features/auth/services/authService';
|
||||
import sessionService from '../../features/auth/services/sessionService';
|
||||
import useAuthStore from '../../store/useAuthStore';
|
||||
import Loading from '../../shared/components/Loading';
|
||||
import EmptyState from '../../shared/components/EmptyState';
|
||||
import { useAsync } from '../../shared/hooks/useAsync';
|
||||
import { useGlobalLoading } from '../../shared/contexts/LoadingContext';
|
||||
import { normalizeImageUrl } from '../../shared/utils/imageUtils';
|
||||
import { formatDate } from '../../shared/utils/format';
|
||||
import { UserSession } from '../../features/auth/services/sessionService';
|
||||
|
||||
const profileValidationSchema = yup.object().shape({
|
||||
name: yup
|
||||
@@ -55,7 +60,11 @@ const passwordValidationSchema = yup.object().shape({
|
||||
newPassword: yup
|
||||
.string()
|
||||
.required('New password is required')
|
||||
.min(6, 'Password must be at least 6 characters'),
|
||||
.min(8, 'Password must be at least 8 characters')
|
||||
.matches(/[A-Z]/, 'Password must contain at least one uppercase letter')
|
||||
.matches(/[a-z]/, 'Password must contain at least one lowercase letter')
|
||||
.matches(/[0-9]/, 'Password must contain at least one number')
|
||||
.matches(/[!@#$%^&*(),.?":{}|<>]/, 'Password must contain at least one special character (!@#$%^&*(),.?":{}|<>)'),
|
||||
confirmPassword: yup
|
||||
.string()
|
||||
.required('Please confirm your password')
|
||||
@@ -68,7 +77,7 @@ type PasswordFormData = yup.InferType<typeof passwordValidationSchema>;
|
||||
const ProfilePage: React.FC = () => {
|
||||
const { userInfo, setUser } = useAuthStore();
|
||||
const { setLoading } = useGlobalLoading();
|
||||
const [activeTab, setActiveTab] = useState<'profile' | 'password' | 'mfa'>('profile');
|
||||
const [activeTab, setActiveTab] = useState<'profile' | 'password' | 'mfa' | 'sessions'>('profile');
|
||||
const [avatarPreview, setAvatarPreview] = useState<string | null>(null);
|
||||
const [showPassword, setShowPassword] = useState<{
|
||||
current: boolean;
|
||||
@@ -402,6 +411,17 @@ const ProfilePage: React.FC = () => {
|
||||
<Shield className="w-4 h-4 sm:w-5 sm:h-5 inline mr-2" />
|
||||
Two-Factor Authentication
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('sessions')}
|
||||
className={`py-3 sm:py-4 px-2 sm:px-1 border-b-2 font-medium text-xs sm:text-sm transition-colors whitespace-nowrap ${
|
||||
activeTab === 'sessions'
|
||||
? 'border-[#d4af37] text-[#d4af37]'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
<Monitor className="w-4 h-4 sm:w-5 sm:h-5 inline mr-2" />
|
||||
Active Sessions
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -967,9 +987,133 @@ const ProfilePage: React.FC = () => {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Sessions Tab */}
|
||||
{activeTab === 'sessions' && (
|
||||
<SessionsTab />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const SessionsTab: React.FC = () => {
|
||||
const [sessions, setSessions] = useState<UserSession[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetchSessions();
|
||||
}, []);
|
||||
|
||||
const fetchSessions = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await sessionService.getMySessions();
|
||||
setSessions(response.data.sessions || []);
|
||||
} catch (error: any) {
|
||||
toast.error(error.response?.data?.message || 'Unable to load sessions');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRevoke = async (sessionId: number) => {
|
||||
if (!confirm('Are you sure you want to revoke this session?')) return;
|
||||
|
||||
try {
|
||||
await sessionService.revokeSession(sessionId);
|
||||
toast.success('Session revoked');
|
||||
fetchSessions();
|
||||
} catch (error: any) {
|
||||
toast.error(error.response?.data?.message || 'Unable to revoke session');
|
||||
}
|
||||
};
|
||||
|
||||
const handleRevokeAll = async () => {
|
||||
if (!confirm('Are you sure you want to revoke all other sessions?')) return;
|
||||
|
||||
try {
|
||||
await sessionService.revokeAllSessions();
|
||||
toast.success('All other sessions revoked');
|
||||
fetchSessions();
|
||||
} catch (error: any) {
|
||||
toast.error(error.response?.data?.message || 'Unable to revoke sessions');
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="luxury-glass rounded-sm p-4 sm:p-6 lg:p-8 border border-[#d4af37]/20 shadow-2xl">
|
||||
<Loading text="Loading sessions..." />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="luxury-glass rounded-sm p-4 sm:p-6 lg:p-8 border border-[#d4af37]/20 shadow-2xl animate-slide-up space-y-5 sm:space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h2 className="text-xl sm:text-2xl font-serif font-semibold text-gray-900 mb-2 flex items-center gap-2">
|
||||
<Monitor className="w-5 h-5 sm:w-6 sm:h-6 text-[#d4af37]" />
|
||||
Active Sessions
|
||||
</h2>
|
||||
<p className="text-xs sm:text-sm text-gray-600 font-light">
|
||||
Manage your active sessions across different devices
|
||||
</p>
|
||||
</div>
|
||||
{sessions.length > 1 && (
|
||||
<button
|
||||
onClick={handleRevokeAll}
|
||||
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 text-sm"
|
||||
>
|
||||
Revoke All Others
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{sessions.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-slate-500">No active sessions</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{sessions.map((session) => (
|
||||
<div
|
||||
key={session.id}
|
||||
className="border border-gray-200 rounded-lg p-4 hover:shadow-md transition-shadow"
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-start gap-4 flex-1">
|
||||
<Monitor className="w-5 h-5 text-gray-500 mt-1" />
|
||||
<div className="flex-1">
|
||||
<p className="font-semibold mb-1">
|
||||
{session.user_agent || 'Unknown Device'}
|
||||
</p>
|
||||
<p className="text-gray-600 text-sm mb-1">
|
||||
IP: {session.ip_address || 'Unknown'}
|
||||
</p>
|
||||
<p className="text-gray-500 text-xs">
|
||||
Last Activity: {formatDate(session.last_activity)}
|
||||
</p>
|
||||
<p className="text-gray-500 text-xs">
|
||||
Expires: {formatDate(session.expires_at)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleRevoke(session.id)}
|
||||
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 flex items-center gap-2 text-sm"
|
||||
>
|
||||
<LogOut className="w-4 h-4" />
|
||||
Revoke
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfilePage;
|
||||
|
||||
Reference in New Issue
Block a user