fix: login redirect, admin auth guard, JWT_SECRET env

This commit is contained in:
2026-06-28 22:41:08 -05:00
parent c2c2217028
commit f99da73118
2 changed files with 90 additions and 60 deletions
+84 -39
View File
@@ -1481,6 +1481,8 @@ export default function AdminPage() {
const [activeSection, setActiveSection] = useState('dashboard'); const [activeSection, setActiveSection] = useState('dashboard');
const [sidebarOpen, setSidebarOpen] = useState(false); const [sidebarOpen, setSidebarOpen] = useState(false);
const [toasts, setToasts] = useState<{ id: number; message: string; type: string }[]>([]); 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 [slides, setSlides] = useState<Slide[]>([]);
const [rooms, setRooms] = useState<Room[]>([]); const [rooms, setRooms] = useState<Room[]>([]);
@@ -1504,8 +1506,20 @@ export default function AdminPage() {
setToasts(prev => prev.filter(t => t.id !== id)); 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 ── */ /* ── Data fetching ── */
useEffect(() => { useEffect(() => {
if (!isAuthenticated) return;
const fetchAll = async () => { const fetchAll = async () => {
try { try {
const [slidesRes, roomsRes, calRes, socialRes, menuRes, placesRes, reviewsRes, imagesRes] = await Promise.all([ 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); } } catch (err) { console.error('Failed to load admin data:', err); }
}; };
fetchAll(); fetchAll();
}, []); }, [isAuthenticated]);
const deleteImage = async (id: number) => { const deleteImage = async (id: number) => {
try { try {
@@ -1567,47 +1581,78 @@ export default function AdminPage() {
{/* Toasts */} {/* Toasts */}
{toasts.map(t => <Toast key={t.id} message={t.message} type={t.type} onClose={() => removeToast(t.id)} />)} {toasts.map(t => <Toast key={t.id} message={t.message} type={t.type} onClose={() => removeToast(t.id)} />)}
{/* Mobile sidebar toggle */} {/* Auth loading */}
<div style={{ display: 'none' }} /> {/* placeholder */} {!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 */} {/* Not authenticated - show login prompt */}
<aside style={{ {authChecked && !isAuthenticated && (
width: 260, background: C.navy, height: '100vh', position: 'sticky', top: 0, overflowY: 'auto', <div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
display: 'flex', flexDirection: 'column', transition: 'transform 300ms', <div style={{ textAlign: 'center', maxWidth: 400, padding: '40px', background: '#fff', borderRadius: 16, boxShadow: C.shadow }}>
borderRight: '1px solid rgba(255,255,255,0.06)', <div style={{ fontSize: '48px', marginBottom: '16px' }}>🔒</div>
}}> <h2 style={{ margin: '0 0 12px', fontSize: '24px', color: C.navy }}>Admin Access Required</h2>
<div style={{ padding: '24px 20px', borderBottom: '1px solid rgba(255,255,255,0.06)' }}> <p style={{ margin: '0 0 24px', fontSize: '14px', color: C.textLight }}>
<h1 style={{ margin: 0, fontSize: '18px', fontWeight: 700, color: C.gold }}> Please log in to access the admin panel.
🏨 Hotel La Huasca </p>
</h1> <a href="/login" style={{ display: 'inline-block', padding: '12px 32px', background: C.gold, color: '#fff', borderRadius: 8, textDecoration: 'none', fontWeight: 600, fontSize: '15px' }}>
<p style={{ margin: '4px 0 0', fontSize: '11px', color: 'rgba(255,255,255,0.4)' }}>Admin Portal</p> Go to Login
</a>
</div>
</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 */} {/* Authenticated - show admin panel */}
<main style={{ flex: 1, padding: '32px 40px', overflowY: 'auto', maxHeight: '100vh' }}> {authChecked && isAuthenticated && (
<div style={{ maxWidth: 1200, margin: '0 auto' }}> <>
{renderSection()} {/* Mobile sidebar toggle */}
</div> <div style={{ display: 'none' }} /> {/* placeholder */}
</main>
{/* 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> </div>
); );
} }
+6 -21
View File
@@ -6,29 +6,14 @@ import type { NextRequest } from 'next/server';
// In a production environment, you would want to verify the token // In a production environment, you would want to verify the token
export function middleware(request: NextRequest) { export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl; const { pathname } = request.nextUrl;
const token = request.cookies.get('session')?.value;
// Protect admin routes // Protect admin routes - let page handle auth UI
if (pathname.startsWith('/admin')) { // No redirect here; the admin page shows login prompt if not authenticated
// Get session token from cookies
const token = request.cookies.get('session')?.value;
// If no token, redirect to login
if (!token) {
return NextResponse.redirect(new URL('/login', request.url));
}
// Note: In a real implementation, you would verify the token here
// For now, we're just checking that a token exists
}
// Redirect authenticated users from login page to home // If logged in and trying to access login, redirect to admin
if (pathname === '/login') { if (pathname === '/login' && token) {
const token = request.cookies.get('session')?.value; return NextResponse.redirect(new URL('/admin', request.url));
if (token) {
// In a real implementation, you would verify the token and check role
// For now, we'll just redirect to home
return NextResponse.redirect(new URL('/', request.url));
}
} }
return NextResponse.next(); return NextResponse.next();