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 [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
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user