feat: production deployment configuration for dk0.dev
- Fixed authentication system (removed HTTP Basic Auth popup) - Added session-based authentication with proper logout - Updated rate limiting (20 req/s for login, 5 req/m for admin) - Created production deployment scripts and configs - Updated nginx configuration for dk0.dev domain - Added comprehensive production deployment guide - Fixed logout button functionality - Optimized for production with proper resource limits
This commit is contained in:
@@ -2,23 +2,13 @@
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import {
|
||||
Lock,
|
||||
Eye,
|
||||
EyeOff,
|
||||
Shield,
|
||||
AlertTriangle,
|
||||
XCircle,
|
||||
Loader2
|
||||
} from 'lucide-react';
|
||||
import { Lock, Loader2 } from 'lucide-react';
|
||||
import ModernAdminDashboard from '@/components/ModernAdminDashboard';
|
||||
|
||||
// Security constants
|
||||
const MAX_ATTEMPTS = 3;
|
||||
// Constants
|
||||
const LOCKOUT_DURATION = 15 * 60 * 1000; // 15 minutes
|
||||
const RATE_LIMIT_DELAY = 1000; // 1 second base delay
|
||||
|
||||
// Rate limiting with exponential backoff
|
||||
const getRateLimitDelay = (attempts: number): number => {
|
||||
return RATE_LIMIT_DELAY * Math.pow(2, attempts);
|
||||
};
|
||||
@@ -93,63 +83,56 @@ const AdminPage = () => {
|
||||
|
||||
// Check session validity via API
|
||||
const checkSession = useCallback(async () => {
|
||||
const authStatus = sessionStorage.getItem('admin_authenticated');
|
||||
const sessionToken = sessionStorage.getItem('admin_session_token');
|
||||
const csrfToken = authState.csrfToken;
|
||||
|
||||
// If no session data, show login immediately
|
||||
if (!authStatus || !sessionToken || !csrfToken) {
|
||||
setAuthState(prev => ({
|
||||
...prev,
|
||||
isAuthenticated: false,
|
||||
isLoading: false,
|
||||
showLogin: true
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const sessionToken = sessionStorage.getItem('admin_session_token');
|
||||
if (!sessionToken) {
|
||||
setAuthState(prev => ({
|
||||
...prev,
|
||||
isAuthenticated: false,
|
||||
showLogin: true,
|
||||
isLoading: false
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch('/api/auth/validate', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': csrfToken
|
||||
'X-CSRF-Token': authState.csrfToken
|
||||
},
|
||||
body: JSON.stringify({
|
||||
sessionToken,
|
||||
csrfToken
|
||||
body: JSON.stringify({
|
||||
sessionToken,
|
||||
csrfToken: authState.csrfToken
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
setAuthState(prev => ({
|
||||
...prev,
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
showLogin: false
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok && data.valid) {
|
||||
setAuthState(prev => ({
|
||||
...prev,
|
||||
isAuthenticated: true,
|
||||
showLogin: false,
|
||||
isLoading: false
|
||||
}));
|
||||
return;
|
||||
sessionStorage.setItem('admin_authenticated', 'true');
|
||||
} else {
|
||||
// Clear invalid session
|
||||
sessionStorage.removeItem('admin_authenticated');
|
||||
sessionStorage.removeItem('admin_session_token');
|
||||
setAuthState(prev => ({
|
||||
...prev,
|
||||
isAuthenticated: false,
|
||||
isLoading: false,
|
||||
showLogin: true
|
||||
setAuthState(prev => ({
|
||||
...prev,
|
||||
isAuthenticated: false,
|
||||
showLogin: true,
|
||||
isLoading: false
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Session validation error:', error);
|
||||
// Clear session on error
|
||||
sessionStorage.removeItem('admin_authenticated');
|
||||
sessionStorage.removeItem('admin_session_token');
|
||||
setAuthState(prev => ({
|
||||
...prev,
|
||||
isAuthenticated: false,
|
||||
isLoading: false,
|
||||
showLogin: true
|
||||
} catch {
|
||||
setAuthState(prev => ({
|
||||
...prev,
|
||||
isAuthenticated: false,
|
||||
showLogin: true,
|
||||
isLoading: false
|
||||
}));
|
||||
}
|
||||
}, [authState.csrfToken]);
|
||||
@@ -172,7 +155,7 @@ const AdminPage = () => {
|
||||
if (authState.csrfToken && !authState.isLocked) {
|
||||
checkSession();
|
||||
}
|
||||
}, [authState.csrfToken, authState.isLocked]);
|
||||
}, [authState.csrfToken, authState.isLocked, checkSession]);
|
||||
|
||||
// Handle logout
|
||||
const handleLogout = useCallback(() => {
|
||||
@@ -215,95 +198,55 @@ const AdminPage = () => {
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok && data.success) {
|
||||
// Store session
|
||||
sessionStorage.setItem('admin_authenticated', 'true');
|
||||
sessionStorage.setItem('admin_session_token', data.sessionToken);
|
||||
|
||||
// Clear lockout data
|
||||
localStorage.removeItem('admin_lockout');
|
||||
|
||||
// Update state
|
||||
setAuthState(prev => ({
|
||||
...prev,
|
||||
isAuthenticated: true,
|
||||
showLogin: false,
|
||||
isLoading: false,
|
||||
password: '',
|
||||
error: '',
|
||||
attempts: 0,
|
||||
error: ''
|
||||
isLoading: false
|
||||
}));
|
||||
localStorage.removeItem('admin_lockout');
|
||||
} else {
|
||||
// Failed login
|
||||
const newAttempts = authState.attempts + 1;
|
||||
const newLastAttempt = Date.now();
|
||||
|
||||
if (newAttempts >= MAX_ATTEMPTS) {
|
||||
// Lock user out
|
||||
setAuthState(prev => ({
|
||||
...prev,
|
||||
error: data.error || 'Login failed',
|
||||
attempts: newAttempts,
|
||||
isLoading: false
|
||||
}));
|
||||
|
||||
if (newAttempts >= 5) {
|
||||
localStorage.setItem('admin_lockout', JSON.stringify({
|
||||
timestamp: newLastAttempt,
|
||||
timestamp: Date.now(),
|
||||
attempts: newAttempts
|
||||
}));
|
||||
|
||||
setAuthState(prev => ({
|
||||
...prev,
|
||||
isLocked: true,
|
||||
attempts: newAttempts,
|
||||
lastAttempt: newLastAttempt,
|
||||
isLoading: false,
|
||||
error: `Too many failed attempts. Access locked for ${Math.ceil(LOCKOUT_DURATION / 60000)} minutes.`
|
||||
}));
|
||||
} else {
|
||||
setAuthState(prev => ({
|
||||
...prev,
|
||||
attempts: newAttempts,
|
||||
lastAttempt: newLastAttempt,
|
||||
isLoading: false,
|
||||
error: data.error || `Wrong password. ${MAX_ATTEMPTS - newAttempts} attempts remaining.`,
|
||||
password: ''
|
||||
error: 'Too many failed attempts. Please try again in 15 minutes.'
|
||||
}));
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
setAuthState(prev => ({
|
||||
...prev,
|
||||
isLoading: false,
|
||||
error: 'An error occurred. Please try again.'
|
||||
error: 'Network error. Please try again.',
|
||||
isLoading: false
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
// Get remaining lockout time
|
||||
const getRemainingTime = () => {
|
||||
const lockoutData = localStorage.getItem('admin_lockout');
|
||||
if (lockoutData) {
|
||||
try {
|
||||
const { timestamp } = JSON.parse(lockoutData);
|
||||
const remaining = Math.ceil((LOCKOUT_DURATION - (Date.now() - timestamp)) / 1000 / 60);
|
||||
return Math.max(0, remaining);
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
// Loading state
|
||||
if (authState.isLoading && !authState.showLogin) {
|
||||
if (authState.isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<div className="fixed inset-0 animated-bg"></div>
|
||||
<div className="relative z-10 min-h-screen flex items-center justify-center">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
className="text-center admin-glass-card p-8 rounded-2xl"
|
||||
>
|
||||
<div className="w-16 h-16 bg-gradient-to-r from-blue-500 to-purple-500 rounded-2xl flex items-center justify-center mx-auto mb-6 shadow-lg">
|
||||
<Loader2 className="w-8 h-8 text-white animate-spin" />
|
||||
</div>
|
||||
<p className="text-white text-xl font-semibold">Verifying Access...</p>
|
||||
<p className="text-white/60 text-sm mt-2">Please wait while we authenticate your session</p>
|
||||
</motion.div>
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<Loader2 className="w-8 h-8 animate-spin mx-auto mb-4 text-blue-500" />
|
||||
<p className="text-white">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -312,44 +255,20 @@ const AdminPage = () => {
|
||||
// Lockout state
|
||||
if (authState.isLocked) {
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<div className="fixed inset-0 animated-bg"></div>
|
||||
<div className="relative z-10 min-h-screen flex items-center justify-center p-4">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
className="admin-glass-card border-red-500/40 p-8 lg:p-12 rounded-2xl max-w-md w-full text-center shadow-2xl"
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<Lock className="w-16 h-16 mx-auto mb-4 text-red-500" />
|
||||
<h2 className="text-2xl font-bold text-white mb-2">Account Locked</h2>
|
||||
<p className="text-white/60">Too many failed attempts. Please try again in 15 minutes.</p>
|
||||
<button
|
||||
onClick={() => {
|
||||
localStorage.removeItem('admin_lockout');
|
||||
window.location.reload();
|
||||
}}
|
||||
className="mt-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
|
||||
>
|
||||
<div className="mb-8">
|
||||
<div className="w-16 h-16 bg-gradient-to-r from-red-500 to-orange-500 rounded-2xl flex items-center justify-center mx-auto mb-6 shadow-lg">
|
||||
<Shield className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold text-white mb-3">Access Locked</h1>
|
||||
<p className="text-white/80 text-lg">
|
||||
Too many failed authentication attempts
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="admin-glass-light border border-red-500/40 rounded-xl p-6 mb-8">
|
||||
<AlertTriangle className="w-8 h-8 text-red-400 mx-auto mb-4" />
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<p className="text-white/60 mb-1">Attempts</p>
|
||||
<p className="text-red-300 font-bold text-lg">{authState.attempts}/{MAX_ATTEMPTS}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white/60 mb-1">Time Left</p>
|
||||
<p className="text-orange-300 font-bold text-lg">{getRemainingTime()}m</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="admin-glass-light border border-blue-500/30 rounded-xl p-4">
|
||||
<p className="text-white/70 text-sm">
|
||||
Access will be automatically restored in {Math.ceil(LOCKOUT_DURATION / 60000)} minutes
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
Try Again
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -358,100 +277,43 @@ const AdminPage = () => {
|
||||
// Login form
|
||||
if (authState.showLogin || !authState.isAuthenticated) {
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
{/* Animated Background - same as admin dashboard */}
|
||||
<div className="fixed inset-0 animated-bg"></div>
|
||||
|
||||
<div className="relative z-10 min-h-screen flex items-center justify-center p-4">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="admin-glass-card p-8 lg:p-12 rounded-2xl max-w-md w-full shadow-2xl"
|
||||
>
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
className="w-full max-w-md p-8"
|
||||
>
|
||||
<div className="bg-white/10 backdrop-blur-lg rounded-2xl p-8 border border-white/20">
|
||||
<div className="text-center mb-8">
|
||||
<div className="w-16 h-16 bg-gradient-to-r from-blue-500 to-purple-500 rounded-2xl flex items-center justify-center mx-auto mb-6 shadow-lg">
|
||||
<Shield className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold text-white mb-3">Admin Panel</h1>
|
||||
<p className="text-white/80 text-lg">Secure access to dashboard</p>
|
||||
<div className="flex items-center justify-center space-x-2 mt-4">
|
||||
<div className="w-2 h-2 bg-green-400 rounded-full animate-pulse"></div>
|
||||
<span className="text-white/60 text-sm font-medium">System Online</span>
|
||||
<Lock className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-white mb-2">Admin Access</h1>
|
||||
<p className="text-white/60">Enter your password to continue</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleLogin} className="space-y-6">
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-medium text-white/80 mb-3">
|
||||
Admin Password
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type={authState.showPassword ? 'text' : 'password'}
|
||||
id="password"
|
||||
value={authState.password}
|
||||
onChange={(e) => setAuthState(prev => ({ ...prev, password: e.target.value }))}
|
||||
className="w-full px-4 py-4 admin-glass-light border border-white/30 rounded-xl text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500/50 transition-all text-lg pr-12"
|
||||
placeholder="Enter admin password"
|
||||
required
|
||||
placeholder="Enter password"
|
||||
className="w-full px-4 py-3 bg-white/10 border border-white/20 rounded-xl text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
disabled={authState.isLoading}
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setAuthState(prev => ({ ...prev, showPassword: !prev.showPassword }))}
|
||||
className="absolute right-4 top-1/2 transform -translate-y-1/2 text-white/60 hover:text-white transition-colors p-1"
|
||||
disabled={authState.isLoading}
|
||||
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-white/50 hover:text-white"
|
||||
>
|
||||
{authState.showPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
|
||||
{authState.showPassword ? '👁️' : '👁️🗨️'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AnimatePresence>
|
||||
{authState.error && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: 'auto' }}
|
||||
exit={{ opacity: 0, height: 0 }}
|
||||
className="admin-glass-light border border-red-500/40 rounded-xl p-4 flex items-center space-x-3"
|
||||
>
|
||||
<XCircle className="w-5 h-5 text-red-400 flex-shrink-0" />
|
||||
<p className="text-red-300 text-sm font-medium">{authState.error}</p>
|
||||
</motion.div>
|
||||
<p className="mt-2 text-red-400 text-sm">{authState.error}</p>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Security info */}
|
||||
<div className="admin-glass-light border border-blue-500/30 rounded-xl p-6">
|
||||
<div className="flex items-center space-x-3 mb-4">
|
||||
<Shield className="w-5 h-5 text-blue-400" />
|
||||
<h3 className="text-blue-300 font-semibold">Security Information</h3>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4 text-xs">
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-white/60">Max Attempts:</span>
|
||||
<span className="text-white font-medium">{MAX_ATTEMPTS}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-white/60">Lockout:</span>
|
||||
<span className="text-white font-medium">{Math.ceil(LOCKOUT_DURATION / 60000)}m</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-white/60">Session:</span>
|
||||
<span className="text-white font-medium">2h</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-white/60">Attempts:</span>
|
||||
<span className={`font-medium ${authState.attempts > 0 ? 'text-orange-400' : 'text-green-400'}`}>
|
||||
{authState.attempts}/{MAX_ATTEMPTS}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
@@ -472,19 +334,8 @@ const AdminPage = () => {
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{/* Debug: Clear Session Button */}
|
||||
<div className="mt-6 pt-6 border-t border-white/20">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleLogout}
|
||||
className="w-full text-white/60 hover:text-white/80 text-sm py-2 px-4 rounded-lg border border-white/20 hover:border-white/40 transition-all"
|
||||
>
|
||||
Clear Session & Reload
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user