diff --git a/src/App.js b/src/App.js index 98278c8..3b51ce0 100644 --- a/src/App.js +++ b/src/App.js @@ -48,6 +48,7 @@ import AdminNewsletters from './pages/admin/AdminNewsletters'; import AdminFinancials from './pages/admin/AdminFinancials'; import AdminBylaws from './pages/admin/AdminBylaws'; import AdminRegistrationBuilder from './pages/admin/AdminRegistrationBuilder'; +import AdminDirectorySettings from './pages/admin/AdminDirectorySettings'; import History from './pages/History'; import MissionValues from './pages/MissionValues'; import BoardOfDirectors from './pages/BoardOfDirectors'; @@ -319,6 +320,8 @@ function App() { } /> } /> } /> + } /> + } /> {/* 404 - Catch all undefined routes */} diff --git a/src/components/IdleSessionWarning.js b/src/components/IdleSessionWarning.js index adf450c..138594e 100644 --- a/src/components/IdleSessionWarning.js +++ b/src/components/IdleSessionWarning.js @@ -1,6 +1,7 @@ import React, { useState, useEffect, useCallback, useRef } from 'react'; import { useNavigate } from 'react-router-dom'; import { useAuth } from '../context/AuthContext'; +import api from '../utils/api'; import logger from '../utils/logger'; import { Dialog, @@ -20,6 +21,7 @@ import { AlertTriangle, RefreshCw } from 'lucide-react'; * - Warns 1 minute before JWT expiry (at 29 minutes if JWT is 30 min) * - Auto-logout on expiration * - "Stay Logged In" extends session + * - Checks session validity when tab becomes visible after being hidden */ const IdleSessionWarning = () => { const { user, logout, refreshUser } = useAuth(); @@ -33,11 +35,13 @@ const IdleSessionWarning = () => { const [showWarning, setShowWarning] = useState(false); const [timeRemaining, setTimeRemaining] = useState(60); // seconds const [isExtending, setIsExtending] = useState(false); + const [isCheckingSession, setIsCheckingSession] = useState(false); const activityTimeoutRef = useRef(null); const warningTimeoutRef = useRef(null); const countdownIntervalRef = useRef(null); const lastActivityRef = useRef(Date.now()); + const lastVisibilityCheckRef = useRef(Date.now()); // Reset activity timer const resetActivityTimer = useCallback(() => { @@ -120,6 +124,83 @@ const IdleSessionWarning = () => { } }; + // Check if session is still valid (called when tab becomes visible) + const checkSessionValidity = useCallback(async () => { + if (!user || isCheckingSession) return; + + const token = localStorage.getItem('token'); + if (!token) { + logger.log('[IdleSessionWarning] No token found on visibility change'); + handleSessionExpired(); + return; + } + + setIsCheckingSession(true); + try { + // Make a lightweight API call to verify token is still valid + await api.get('/auth/me'); + logger.log('[IdleSessionWarning] Session still valid after visibility change'); + + // Reset the activity timer since user is back + resetActivityTimer(); + } catch (error) { + logger.error('[IdleSessionWarning] Session invalid on visibility change:', error); + + // If 401, the interceptor will handle redirect + // For other errors, still redirect to be safe + if (error.response?.status !== 401) { + handleSessionExpired(); + } + } finally { + setIsCheckingSession(false); + } + }, [user, isCheckingSession, handleSessionExpired, resetActivityTimer]); + + // Handle visibility change (when user returns to tab after being away) + useEffect(() => { + if (!user) return; + + const handleVisibilityChange = () => { + if (document.visibilityState === 'visible') { + const now = Date.now(); + const timeSinceLastCheck = now - lastVisibilityCheckRef.current; + + // Only check if tab was hidden for more than 1 minute + // This prevents unnecessary API calls for quick tab switches + if (timeSinceLastCheck > 60 * 1000) { + logger.log('[IdleSessionWarning] Tab became visible after', Math.round(timeSinceLastCheck / 1000), 'seconds'); + checkSessionValidity(); + } + + lastVisibilityCheckRef.current = now; + } + }; + + document.addEventListener('visibilitychange', handleVisibilityChange); + + return () => { + document.removeEventListener('visibilitychange', handleVisibilityChange); + }; + }, [user, checkSessionValidity]); + + // Listen for session expired event from API interceptor + useEffect(() => { + const handleAuthSessionExpired = () => { + logger.log('[IdleSessionWarning] Received auth:session-expired event'); + // Clear all timers + if (activityTimeoutRef.current) clearTimeout(activityTimeoutRef.current); + if (warningTimeoutRef.current) clearTimeout(warningTimeoutRef.current); + if (countdownIntervalRef.current) clearInterval(countdownIntervalRef.current); + setShowWarning(false); + }; + + window.addEventListener('auth:session-expired', handleAuthSessionExpired); + + return () => { + window.removeEventListener('auth:session-expired', handleAuthSessionExpired); + }; + }, []); + // Track user activity useEffect(() => { if (!user) return; diff --git a/src/components/MemberCard.js b/src/components/MemberCard.js index c61dcce..71feb7b 100644 --- a/src/components/MemberCard.js +++ b/src/components/MemberCard.js @@ -3,6 +3,7 @@ import { Card } from './ui/card'; import { Button } from './ui/button'; import { Heart, Calendar, Mail, Phone, MapPin, Facebook, Instagram, Twitter, Linkedin, UserCircle } from 'lucide-react'; import MemberBadge from './MemberBadge'; +import useDirectoryConfig from '../hooks/use-directory-config'; // Helper function to get initials const getInitials = (firstName, lastName) => { @@ -20,6 +21,7 @@ const getSocialMediaLink = (url) => { const MemberCard = ({ member, onViewProfile, tiers }) => { const memberSince = member.member_since || member.created_at; + const { isFieldEnabled } = useDirectoryConfig(); return ( {/* Member Tier Badge */} @@ -48,7 +50,7 @@ const MemberCard = ({ member, onViewProfile, tiers }) => { {/* Partner Name */} - {member.directory_partner_name && ( + {isFieldEnabled('directory_partner_name') && member.directory_partner_name && (
@@ -58,7 +60,7 @@ const MemberCard = ({ member, onViewProfile, tiers }) => { )} {/* Bio */} - {member.directory_bio && ( + {isFieldEnabled('directory_bio') && member.directory_bio && (

{member.directory_bio}

@@ -79,7 +81,7 @@ const MemberCard = ({ member, onViewProfile, tiers }) => { {/* Contact Information */}
- {member.directory_email && ( + {isFieldEnabled('directory_email') && member.directory_email && ( )} - {member.directory_phone && ( + {isFieldEnabled('directory_phone') && member.directory_phone && ( )} - {member.directory_address && ( + {isFieldEnabled('directory_address') && member.directory_address && (
@@ -116,7 +118,7 @@ const MemberCard = ({ member, onViewProfile, tiers }) => {
{/* Social Media Links */} - {(member.social_media_facebook || member.social_media_instagram || member.social_media_twitter || member.social_media_linkedin) && ( + {isFieldEnabled('social_media') && (member.social_media_facebook || member.social_media_instagram || member.social_media_twitter || member.social_media_linkedin) && (
{member.social_media_facebook && ( diff --git a/src/components/SettingsSidebar.js b/src/components/SettingsSidebar.js index 2c25883..551c827 100644 --- a/src/components/SettingsSidebar.js +++ b/src/components/SettingsSidebar.js @@ -1,12 +1,13 @@ import React from 'react'; import { NavLink, useLocation } from 'react-router-dom'; -import { CreditCard, Shield, Star, Palette, FileEdit } from 'lucide-react'; +import { CreditCard, Shield, Star, Palette, FileEdit, BookUser } from 'lucide-react'; const settingsItems = [ { label: 'Stripe', path: '/admin/settings/stripe', icon: CreditCard }, { label: 'Permissions', path: '/admin/settings/permissions', icon: Shield }, { label: 'Theme', path: '/admin/settings/theme', icon: Palette }, - + { label: 'Directory', path: '/admin/settings/directory', icon: BookUser }, + // { label: 'Registration', path: '/admin/settings/registration', icon: FileEdit }, // Commented out for future fallback ]; const SettingsTabs = () => { diff --git a/src/pages/Login.js b/src/pages/Login.js index 5bab4b7..efd100b 100644 --- a/src/pages/Login.js +++ b/src/pages/Login.js @@ -1,5 +1,5 @@ -import React, { useState } from 'react'; -import { useNavigate, Link } from 'react-router-dom'; +import React, { useState, useEffect } from 'react'; +import { useNavigate, Link, useLocation, useSearchParams } from 'react-router-dom'; import { useAuth } from '../context/AuthContext'; import { Button } from '../components/ui/button'; import { Input } from '../components/ui/input'; @@ -13,6 +13,8 @@ import { ArrowRight, ArrowLeft } from 'lucide-react'; const Login = () => { const navigate = useNavigate(); + const location = useLocation(); + const [searchParams] = useSearchParams(); const { login } = useAuth(); const [loading, setLoading] = useState(false); const [formData, setFormData] = useState({ @@ -20,6 +22,30 @@ const Login = () => { password: '' }); + // Show session expiry message on mount + useEffect(() => { + const sessionParam = searchParams.get('session'); + const stateMessage = location.state?.message; + + if (sessionParam === 'expired') { + toast.info('Your session has expired. Please log in again.', { + duration: 5000, + }); + // Clean up URL + window.history.replaceState({}, '', '/login'); + } else if (sessionParam === 'idle') { + toast.info('You were logged out due to inactivity. Please log in again.', { + duration: 5000, + }); + // Clean up URL + window.history.replaceState({}, '', '/login'); + } else if (stateMessage) { + toast.info(stateMessage, { + duration: 5000, + }); + } + }, [searchParams, location.state]); + const handleInputChange = (e) => { const { name, value } = e.target; setFormData(prev => ({ ...prev, [name]: value })); diff --git a/src/pages/Profile.js b/src/pages/Profile.js index b60790a..bea4e3d 100644 --- a/src/pages/Profile.js +++ b/src/pages/Profile.js @@ -13,6 +13,7 @@ import { Avatar, AvatarImage, AvatarFallback } from '../components/ui/avatar'; import ChangePasswordDialog from '../components/ChangePasswordDialog'; import PaymentMethodsSection from '../components/PaymentMethodsSection'; import { useNavigate } from 'react-router-dom'; +import useDirectoryConfig from '../hooks/use-directory-config'; const Profile = () => { const { user } = useAuth(); @@ -29,6 +30,7 @@ const Profile = () => { const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); const [initialFormData, setInitialFormData] = useState(null); const navigate = useNavigate(); + const { isFieldEnabled, loading: directoryConfigLoading } = useDirectoryConfig(); const [formData, setFormData] = useState({ first_name: '', last_name: '', @@ -427,107 +429,121 @@ const Profile = () => {
{/* Member Directory Settings */} -
-

- - Member Directory Settings -

-

- Control your visibility and information in the member directory. -

+ {isFieldEnabled('show_in_directory') && ( +
+

+ + Member Directory Settings +

+

+ Control your visibility and information in the member directory. +

-
- - -
- - {formData.show_in_directory && ( -
-
- - -
- -
- -