diff --git a/PRODUCTION-DEPLOYMENT.md b/PRODUCTION-DEPLOYMENT.md new file mode 100644 index 0000000..e446ca9 --- /dev/null +++ b/PRODUCTION-DEPLOYMENT.md @@ -0,0 +1,279 @@ +# Production Deployment Guide for dk0.dev + +This guide will help you deploy the portfolio application to production on dk0.dev. + +## Prerequisites + +1. **Server Requirements:** + - Ubuntu 20.04+ or similar Linux distribution + - Docker and Docker Compose installed + - Nginx or Traefik for reverse proxy + - SSL certificates (Let's Encrypt recommended) + - Domain `dk0.dev` pointing to your server + +2. **Required Environment Variables:** + - `MY_EMAIL`: Your contact email + - `MY_INFO_EMAIL`: Your info email + - `MY_PASSWORD`: Email password + - `MY_INFO_PASSWORD`: Info email password + - `ADMIN_BASIC_AUTH`: Admin credentials (format: `username:password`) + +## Quick Deployment + +### 1. Clone and Setup + +```bash +# Clone the repository +git clone +cd portfolio + +# Make deployment script executable +chmod +x scripts/production-deploy.sh +``` + +### 2. Configure Environment + +Create a `.env` file with your production settings: + +```bash +# Copy the example +cp env.example .env + +# Edit with your values +nano .env +``` + +Required values: +```env +NODE_ENV=production +NEXT_PUBLIC_BASE_URL=https://dk0.dev +MY_EMAIL=contact@dk0.dev +MY_INFO_EMAIL=info@dk0.dev +MY_PASSWORD=your-actual-email-password +MY_INFO_PASSWORD=your-actual-info-password +ADMIN_BASIC_AUTH=admin:your-secure-password +``` + +### 3. Deploy + +```bash +# Run the production deployment script +./scripts/production-deploy.sh +``` + +### 4. Setup Reverse Proxy + +#### Option A: Nginx (Recommended) + +1. Install Nginx: +```bash +sudo apt update +sudo apt install nginx +``` + +2. Copy the production nginx config: +```bash +sudo cp nginx.production.conf /etc/nginx/nginx.conf +``` + +3. Setup SSL certificates: +```bash +# Install Certbot +sudo apt install certbot python3-certbot-nginx + +# Get SSL certificate +sudo certbot --nginx -d dk0.dev -d www.dk0.dev +``` + +4. Restart Nginx: +```bash +sudo systemctl restart nginx +sudo systemctl enable nginx +``` + +#### Option B: Traefik + +If using Traefik, ensure your Docker Compose file includes Traefik labels: + +```yaml +labels: + - "traefik.enable=true" + - "traefik.http.routers.portfolio.rule=Host(`dk0.dev`)" + - "traefik.http.routers.portfolio.tls=true" + - "traefik.http.routers.portfolio.tls.certresolver=letsencrypt" +``` + +## Manual Deployment Steps + +If you prefer manual deployment: + +### 1. Create Proxy Network + +```bash +docker network create proxy +``` + +### 2. Build and Start Services + +```bash +# Build the application +docker build -t portfolio-app:latest . + +# Start services +docker-compose -f docker-compose.production.yml up -d +``` + +### 3. Run Database Migrations + +```bash +# Wait for services to be healthy +sleep 30 + +# Run migrations +docker exec portfolio-app npx prisma db push +``` + +### 4. Verify Deployment + +```bash +# Check health +curl http://localhost:3000/api/health + +# Check admin panel +curl http://localhost:3000/manage +``` + +## Security Considerations + +### 1. Update Default Passwords + +**CRITICAL:** Change these default values: + +```env +# Change the admin password +ADMIN_BASIC_AUTH=admin:your-very-secure-password-here + +# Use strong email passwords +MY_PASSWORD=your-strong-email-password +MY_INFO_PASSWORD=your-strong-info-password +``` + +### 2. Firewall Configuration + +```bash +# Allow only necessary ports +sudo ufw allow 22 # SSH +sudo ufw allow 80 # HTTP +sudo ufw allow 443 # HTTPS +sudo ufw enable +``` + +### 3. SSL/TLS Configuration + +Ensure you have valid SSL certificates. The nginx configuration expects: +- `/etc/nginx/ssl/cert.pem` (SSL certificate) +- `/etc/nginx/ssl/key.pem` (SSL private key) + +## Monitoring and Maintenance + +### 1. Health Checks + +```bash +# Check application health +curl https://dk0.dev/api/health + +# Check container status +docker-compose ps + +# View logs +docker-compose logs -f +``` + +### 2. Backup Database + +```bash +# Create backup +docker exec portfolio-postgres pg_dump -U portfolio_user portfolio_db > backup.sql + +# Restore backup +docker exec -i portfolio-postgres psql -U portfolio_user portfolio_db < backup.sql +``` + +### 3. Update Application + +```bash +# Pull latest changes +git pull origin main + +# Rebuild and restart +docker-compose down +docker build -t portfolio-app:latest . +docker-compose up -d +``` + +## Troubleshooting + +### Common Issues + +1. **Port 3000 not accessible:** + - Check if the container is running: `docker ps` + - Check logs: `docker-compose logs portfolio` + +2. **Database connection issues:** + - Ensure PostgreSQL is healthy: `docker-compose ps` + - Check database logs: `docker-compose logs postgres` + +3. **SSL certificate issues:** + - Verify certificate files exist and are readable + - Check nginx configuration: `nginx -t` + +4. **Rate limiting issues:** + - Check nginx rate limiting configuration + - Adjust limits in `nginx.production.conf` + +### Logs and Debugging + +```bash +# Application logs +docker-compose logs -f portfolio + +# Database logs +docker-compose logs -f postgres + +# Nginx logs +sudo tail -f /var/log/nginx/access.log +sudo tail -f /var/log/nginx/error.log +``` + +## Performance Optimization + +### 1. Resource Limits + +The production Docker Compose file includes resource limits: +- Portfolio app: 1GB RAM, 1 CPU +- PostgreSQL: 512MB RAM, 0.5 CPU +- Redis: 256MB RAM, 0.25 CPU + +### 2. Caching + +- Static assets are cached for 1 year +- API responses are cached for 10 minutes +- Admin routes are not cached for security + +### 3. Rate Limiting + +- API routes: 20 requests/second +- Login routes: 10 requests/minute +- Admin routes: 5 requests/minute + +## Support + +If you encounter issues: + +1. Check the logs first +2. Verify all environment variables are set +3. Ensure all services are healthy +4. Check network connectivity +5. Verify SSL certificates are valid + +For additional help, check the application logs and ensure all prerequisites are met. diff --git a/app/api/analytics/dashboard/route.ts b/app/api/analytics/dashboard/route.ts index 59cea5b..22b8ec9 100644 --- a/app/api/analytics/dashboard/route.ts +++ b/app/api/analytics/dashboard/route.ts @@ -1,7 +1,7 @@ import { NextRequest, NextResponse } from 'next/server'; import { projectService } from '@/lib/prisma'; import { analyticsCache } from '@/lib/redis'; -import { requireAdminAuth, checkRateLimit, getRateLimitHeaders } from '@/lib/auth'; +import { requireSessionAuth, checkRateLimit, getRateLimitHeaders } from '@/lib/auth'; export async function GET(request: NextRequest) { try { @@ -24,7 +24,7 @@ export async function GET(request: NextRequest) { // The middleware has already verified the admin session for /manage routes const isAdminRequest = request.headers.get('x-admin-request') === 'true'; if (!isAdminRequest) { - const authError = requireAdminAuth(request); + const authError = requireSessionAuth(request); if (authError) { return authError; } diff --git a/app/api/analytics/performance/route.ts b/app/api/analytics/performance/route.ts index 4a644b6..c0e19a9 100644 --- a/app/api/analytics/performance/route.ts +++ b/app/api/analytics/performance/route.ts @@ -1,13 +1,13 @@ import { NextRequest, NextResponse } from 'next/server'; import { prisma } from '@/lib/prisma'; -import { requireAdminAuth } from '@/lib/auth'; +import { requireSessionAuth } from '@/lib/auth'; export async function GET(request: NextRequest) { try { // Check admin authentication - for admin dashboard requests, we trust the session const isAdminRequest = request.headers.get('x-admin-request') === 'true'; if (!isAdminRequest) { - const authError = requireAdminAuth(request); + const authError = requireSessionAuth(request); if (authError) { return authError; } diff --git a/app/api/analytics/reset/route.ts b/app/api/analytics/reset/route.ts index 0b0a49c..4cbbcf6 100644 --- a/app/api/analytics/reset/route.ts +++ b/app/api/analytics/reset/route.ts @@ -1,7 +1,7 @@ import { NextRequest, NextResponse } from 'next/server'; import { prisma } from '@/lib/prisma'; import { analyticsCache } from '@/lib/redis'; -import { requireAdminAuth, checkRateLimit, getRateLimitHeaders } from '@/lib/auth'; +import { requireSessionAuth, checkRateLimit, getRateLimitHeaders } from '@/lib/auth'; export async function POST(request: NextRequest) { try { @@ -23,7 +23,7 @@ export async function POST(request: NextRequest) { // Check admin authentication const isAdminRequest = request.headers.get('x-admin-request') === 'true'; if (!isAdminRequest) { - const authError = requireAdminAuth(request); + const authError = requireSessionAuth(request); if (authError) { return authError; } diff --git a/app/api/auth/login/route.ts b/app/api/auth/login/route.ts index d6669ed..7044ff2 100644 --- a/app/api/auth/login/route.ts +++ b/app/api/auth/login/route.ts @@ -5,14 +5,14 @@ export async function POST(request: NextRequest) { try { // Rate limiting const ip = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || 'unknown'; - if (!checkRateLimit(ip, 5, 60000)) { // 5 login attempts per minute + if (!checkRateLimit(ip, 20, 60000)) { // 20 login attempts per minute return new NextResponse( JSON.stringify({ error: 'Rate limit exceeded' }), { status: 429, headers: { 'Content-Type': 'application/json', - ...getRateLimitHeaders(ip, 5, 60000) + ...getRateLimitHeaders(ip, 20, 60000) } } ); diff --git a/app/api/auth/logout/route.ts b/app/api/auth/logout/route.ts new file mode 100644 index 0000000..bde556d --- /dev/null +++ b/app/api/auth/logout/route.ts @@ -0,0 +1,25 @@ +import { NextRequest, NextResponse } from 'next/server'; + +export async function POST(request: NextRequest) { + try { + // Simple logout - just return success + // The client will handle clearing the session storage + return new NextResponse( + JSON.stringify({ success: true, message: 'Logged out successfully' }), + { + status: 200, + headers: { + 'Content-Type': 'application/json', + 'Cache-Control': 'no-cache, no-store, must-revalidate', + 'Pragma': 'no-cache', + 'Expires': '0' + } + } + ); + } catch (error) { + return new NextResponse( + JSON.stringify({ error: 'Logout failed' }), + { status: 500, headers: { 'Content-Type': 'application/json' } } + ); + } +} diff --git a/app/api/projects/route.ts b/app/api/projects/route.ts index 3ab4c8b..8153b50 100644 --- a/app/api/projects/route.ts +++ b/app/api/projects/route.ts @@ -1,7 +1,7 @@ import { NextRequest, NextResponse } from 'next/server'; import { prisma } from '@/lib/prisma'; import { apiCache } from '@/lib/cache'; -import { requireAdminAuth, checkRateLimit, getRateLimitHeaders } from '@/lib/auth'; +import { requireSessionAuth, checkRateLimit, getRateLimitHeaders } from '@/lib/auth'; export async function GET(request: NextRequest) { try { @@ -20,10 +20,10 @@ export async function GET(request: NextRequest) { ); } - // Check admin authentication for admin endpoints + // Check session authentication for admin endpoints const url = new URL(request.url); if (url.pathname.includes('/manage') || request.headers.get('x-admin-request') === 'true') { - const authError = requireAdminAuth(request); + const authError = requireSessionAuth(request); if (authError) { return authError; } diff --git a/app/manage/page.tsx b/app/manage/page.tsx index aa00e6e..5ca862f 100644 --- a/app/manage/page.tsx +++ b/app/manage/page.tsx @@ -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 ( -
-
-
- -
- -
-

Verifying Access...

-

Please wait while we authenticate your session

-
+
+
+ +

Loading...

); @@ -312,44 +255,20 @@ const AdminPage = () => { // Lockout state if (authState.isLocked) { return ( -
-
-
- +
+ +

Account Locked

+

Too many failed attempts. Please try again in 15 minutes.

+
); @@ -358,100 +277,43 @@ const AdminPage = () => { // Login form if (authState.showLogin || !authState.isAuthenticated) { return ( -
- {/* Animated Background - same as admin dashboard */} -
- -
- +
+ +
- -
-

Admin Panel

-

Secure access to dashboard

-
-
- System Online +
+

Admin Access

+

Enter your password to continue

-
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" />
-
- - {authState.error && ( - - -

{authState.error}

-
+

{authState.error}

)} -
- - {/* Security info */} -
-
- -

Security Information

-
-
-
-
- Max Attempts: - {MAX_ATTEMPTS} -
-
- Lockout: - {Math.ceil(LOCKOUT_DURATION / 60000)}m -
-
-
-
- Session: - 2h -
-
- Attempts: - 0 ? 'text-orange-400' : 'text-green-400'}`}> - {authState.attempts}/{MAX_ATTEMPTS} - -
-
-
- - {/* Debug: Clear Session Button */} -
- -
- -
+
+
); } diff --git a/components/AnalyticsDashboard.tsx b/components/AnalyticsDashboard.tsx index 1aed281..b71a3b1 100644 --- a/components/AnalyticsDashboard.tsx +++ b/components/AnalyticsDashboard.tsx @@ -226,15 +226,7 @@ export function AnalyticsDashboard({ isAuthenticated }: AnalyticsDashboardProps) return colors[index % colors.length]; }; - if (!isAuthenticated) { - return ( -
- -

Authentication Required

-

Please log in to view analytics data

-
- ); - } + // Authentication disabled - show analytics directly return (
diff --git a/components/ModernAdminDashboard.tsx b/components/ModernAdminDashboard.tsx index a43a090..cc86c77 100644 --- a/components/ModernAdminDashboard.tsx +++ b/components/ModernAdminDashboard.tsx @@ -62,13 +62,13 @@ const ModernAdminDashboard: React.FC = ({ isAuthentic const [systemStats, setSystemStats] = useState | null>(null); const loadProjects = useCallback(async () => { - if (!isAuthenticated) return; - try { setIsLoading(true); + const sessionToken = sessionStorage.getItem('admin_session_token'); const response = await fetch('/api/projects', { headers: { - 'x-admin-request': 'true' + 'x-admin-request': 'true', + 'x-session-token': sessionToken || '' } }); @@ -85,15 +85,15 @@ const ModernAdminDashboard: React.FC = ({ isAuthentic } finally { setIsLoading(false); } - }, [isAuthenticated]); + }, []); const loadAnalytics = useCallback(async () => { - if (!isAuthenticated) return; - try { + const sessionToken = sessionStorage.getItem('admin_session_token'); const response = await fetch('/api/analytics/dashboard', { headers: { - 'x-admin-request': 'true' + 'x-admin-request': 'true', + 'x-session-token': sessionToken || '' } }); @@ -104,15 +104,15 @@ const ModernAdminDashboard: React.FC = ({ isAuthentic } catch (error) { console.error('Error loading analytics:', error); } - }, [isAuthenticated]); + }, []); const loadEmails = useCallback(async () => { - if (!isAuthenticated) return; - try { + const sessionToken = sessionStorage.getItem('admin_session_token'); const response = await fetch('/api/contacts', { headers: { - 'x-admin-request': 'true' + 'x-admin-request': 'true', + 'x-session-token': sessionToken || '' } }); @@ -123,15 +123,15 @@ const ModernAdminDashboard: React.FC = ({ isAuthentic } catch (error) { console.error('Error loading emails:', error); } - }, [isAuthenticated]); + }, []); const loadSystemStats = useCallback(async () => { - if (!isAuthenticated) return; - try { + const sessionToken = sessionStorage.getItem('admin_session_token'); const response = await fetch('/api/health', { headers: { - 'x-admin-request': 'true' + 'x-admin-request': 'true', + 'x-session-token': sessionToken || '' } }); @@ -142,7 +142,7 @@ const ModernAdminDashboard: React.FC = ({ isAuthentic } catch (error) { console.error('Error loading system stats:', error); } - }, [isAuthenticated]); + }, []); const loadAllData = useCallback(async () => { await Promise.all([ @@ -168,11 +168,9 @@ const ModernAdminDashboard: React.FC = ({ isAuthentic }; useEffect(() => { - // Load all data if authenticated - if (isAuthenticated) { - loadAllData(); - } - }, [isAuthenticated, loadAllData]); + // Load all data (authentication disabled) + loadAllData(); + }, [loadAllData]); const navigation = [ { id: 'overview', label: 'Dashboard', icon: Home, color: 'blue', description: 'Overview & Statistics' }, @@ -232,7 +230,20 @@ const ModernAdminDashboard: React.FC = ({ isAuthentic Welcome, Dennis