fix: login redirect, admin auth guard, JWT_SECRET env
This commit is contained in:
+84
-39
@@ -1481,6 +1481,8 @@ export default function AdminPage() {
|
||||
const [activeSection, setActiveSection] = useState('dashboard');
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
const [toasts, setToasts] = useState<{ id: number; message: string; type: string }[]>([]);
|
||||
const [authChecked, setAuthChecked] = useState(false);
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||
|
||||
const [slides, setSlides] = useState<Slide[]>([]);
|
||||
const [rooms, setRooms] = useState<Room[]>([]);
|
||||
@@ -1504,8 +1506,20 @@ export default function AdminPage() {
|
||||
setToasts(prev => prev.filter(t => t.id !== id));
|
||||
}, []);
|
||||
|
||||
/* ── Auth check ── */
|
||||
useEffect(() => {
|
||||
fetch('/api/auth/me', { credentials: 'include' })
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.authenticated) setIsAuthenticated(true);
|
||||
setAuthChecked(true);
|
||||
})
|
||||
.catch(() => setAuthChecked(true));
|
||||
}, []);
|
||||
|
||||
/* ── Data fetching ── */
|
||||
useEffect(() => {
|
||||
if (!isAuthenticated) return;
|
||||
const fetchAll = async () => {
|
||||
try {
|
||||
const [slidesRes, roomsRes, calRes, socialRes, menuRes, placesRes, reviewsRes, imagesRes] = await Promise.all([
|
||||
@@ -1529,7 +1543,7 @@ export default function AdminPage() {
|
||||
} catch (err) { console.error('Failed to load admin data:', err); }
|
||||
};
|
||||
fetchAll();
|
||||
}, []);
|
||||
}, [isAuthenticated]);
|
||||
|
||||
const deleteImage = async (id: number) => {
|
||||
try {
|
||||
@@ -1567,47 +1581,78 @@ export default function AdminPage() {
|
||||
{/* Toasts */}
|
||||
{toasts.map(t => <Toast key={t.id} message={t.message} type={t.type} onClose={() => removeToast(t.id)} />)}
|
||||
|
||||
{/* Mobile sidebar toggle */}
|
||||
<div style={{ display: 'none' }} /> {/* placeholder */}
|
||||
{/* Auth loading */}
|
||||
{!authChecked && (
|
||||
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<div style={{ fontSize: '32px', marginBottom: '16px' }}>⏳</div>
|
||||
<div style={{ fontSize: '16px', color: C.text }}>Checking authentication...</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Sidebar */}
|
||||
<aside style={{
|
||||
width: 260, background: C.navy, height: '100vh', position: 'sticky', top: 0, overflowY: 'auto',
|
||||
display: 'flex', flexDirection: 'column', transition: 'transform 300ms',
|
||||
borderRight: '1px solid rgba(255,255,255,0.06)',
|
||||
}}>
|
||||
<div style={{ padding: '24px 20px', borderBottom: '1px solid rgba(255,255,255,0.06)' }}>
|
||||
<h1 style={{ margin: 0, fontSize: '18px', fontWeight: 700, color: C.gold }}>
|
||||
🏨 Hotel La Huasca
|
||||
</h1>
|
||||
<p style={{ margin: '4px 0 0', fontSize: '11px', color: 'rgba(255,255,255,0.4)' }}>Admin Portal</p>
|
||||
{/* Not authenticated - show login prompt */}
|
||||
{authChecked && !isAuthenticated && (
|
||||
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<div style={{ textAlign: 'center', maxWidth: 400, padding: '40px', background: '#fff', borderRadius: 16, boxShadow: C.shadow }}>
|
||||
<div style={{ fontSize: '48px', marginBottom: '16px' }}>🔒</div>
|
||||
<h2 style={{ margin: '0 0 12px', fontSize: '24px', color: C.navy }}>Admin Access Required</h2>
|
||||
<p style={{ margin: '0 0 24px', fontSize: '14px', color: C.textLight }}>
|
||||
Please log in to access the admin panel.
|
||||
</p>
|
||||
<a href="/login" style={{ display: 'inline-block', padding: '12px 32px', background: C.gold, color: '#fff', borderRadius: 8, textDecoration: 'none', fontWeight: 600, fontSize: '15px' }}>
|
||||
Go to Login
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<nav style={{ padding: '16px 12px', flex: 1 }}>
|
||||
{NAV.map(n => (
|
||||
<button key={n.id} onClick={() => { setActiveSection(n.id); setSidebarOpen(false); }}
|
||||
style={{ width: '100%', display: 'flex', alignItems: 'center', gap: '12px', padding: '10px 14px', marginBottom: '4px',
|
||||
borderRadius: 10, border: 'none', cursor: 'pointer', fontSize: '14px', fontWeight: 500,
|
||||
background: activeSection === n.id ? C.goldLight : 'transparent',
|
||||
color: activeSection === n.id ? C.gold : 'rgba(255,255,255,0.6)',
|
||||
transition: 'all 200ms', textAlign: 'left' }}
|
||||
onMouseEnter={e => { if (activeSection !== n.id) { e.currentTarget.style.background = 'rgba(255,255,255,0.06)'; e.currentTarget.style.color = '#fff'; } }}
|
||||
onMouseLeave={e => { if (activeSection !== n.id) { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.color = 'rgba(255,255,255,0.6)'; } }}>
|
||||
<span style={{ fontSize: '18px' }}>{n.icon}</span>
|
||||
{n.label}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
<div style={{ padding: '16px 20px', borderTop: '1px solid rgba(255,255,255,0.06)', fontSize: '11px', color: 'rgba(255,255,255,0.3)' }}>
|
||||
v1.0.0
|
||||
</div>
|
||||
</aside>
|
||||
)}
|
||||
|
||||
{/* Main content */}
|
||||
<main style={{ flex: 1, padding: '32px 40px', overflowY: 'auto', maxHeight: '100vh' }}>
|
||||
<div style={{ maxWidth: 1200, margin: '0 auto' }}>
|
||||
{renderSection()}
|
||||
</div>
|
||||
</main>
|
||||
{/* Authenticated - show admin panel */}
|
||||
{authChecked && isAuthenticated && (
|
||||
<>
|
||||
{/* Mobile sidebar toggle */}
|
||||
<div style={{ display: 'none' }} /> {/* placeholder */}
|
||||
|
||||
{/* Sidebar */}
|
||||
<aside style={{
|
||||
width: 260, background: C.navy, height: '100vh', position: 'sticky', top: 0, overflowY: 'auto',
|
||||
display: 'flex', flexDirection: 'column', transition: 'transform 300ms',
|
||||
borderRight: '1px solid rgba(255,255,255,0.06)',
|
||||
}}>
|
||||
<div style={{ padding: '24px 20px', borderBottom: '1px solid rgba(255,255,255,0.06)' }}>
|
||||
<h1 style={{ margin: 0, fontSize: '18px', fontWeight: 700, color: C.gold }}>
|
||||
🏨 Hotel La Huasca
|
||||
</h1>
|
||||
<p style={{ margin: '4px 0 0', fontSize: '11px', color: 'rgba(255,255,255,0.4)' }}>Admin Portal</p>
|
||||
</div>
|
||||
<nav style={{ padding: '16px 12px', flex: 1 }}>
|
||||
{NAV.map(n => (
|
||||
<button key={n.id} onClick={() => { setActiveSection(n.id); setSidebarOpen(false); }}
|
||||
style={{ width: '100%', display: 'flex', alignItems: 'center', gap: '12px', padding: '10px 14px', marginBottom: '4px',
|
||||
borderRadius: 10, border: 'none', cursor: 'pointer', fontSize: '14px', fontWeight: 500,
|
||||
background: activeSection === n.id ? C.goldLight : 'transparent',
|
||||
color: activeSection === n.id ? C.gold : 'rgba(255,255,255,0.6)',
|
||||
transition: 'all 200ms', textAlign: 'left' }}
|
||||
onMouseEnter={e => { if (activeSection !== n.id) { e.currentTarget.style.background = 'rgba(255,255,255,0.06)'; e.currentTarget.style.color = '#fff'; } }}
|
||||
onMouseLeave={e => { if (activeSection !== n.id) { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.color = 'rgba(255,255,255,0.6)'; } }}>
|
||||
<span style={{ fontSize: '18px' }}>{n.icon}</span>
|
||||
{n.label}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
<div style={{ padding: '16px 20px', borderTop: '1px solid rgba(255,255,255,0.06)', fontSize: '11px', color: 'rgba(255,255,255,0.3)' }}>
|
||||
v1.0.0
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Main content */}
|
||||
<main style={{ flex: 1, padding: '32px 40px', overflowY: 'auto', maxHeight: '100vh' }}>
|
||||
<div style={{ maxWidth: 1200, margin: '0 auto' }}>
|
||||
{renderSection()}
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user