🔧 Enhance Middleware and Admin Features
✅ Updated Middleware Logic: - Enhanced admin route protection with Basic Auth for legacy routes and session-based auth for `/manage` and `/editor`. ✅ Improved Admin Panel Styles: - Added glassmorphism styles for admin components to enhance UI aesthetics. ✅ Refined Rate Limiting: - Adjusted rate limits for admin dashboard requests to allow more generous access. ✅ Introduced Analytics Reset API: - Added a new endpoint for resetting analytics data with rate limiting and admin authentication. 🎯 Overall Improvements: - Strengthened security and user experience for admin functionalities. - Enhanced visual design for better usability. - Streamlined analytics management processes.
This commit is contained in:
@@ -5,9 +5,9 @@ import { requireAdminAuth, checkRateLimit, getRateLimitHeaders } from '@/lib/aut
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
// Rate limiting
|
||||
// Rate limiting - more generous for admin dashboard
|
||||
const ip = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || 'unknown';
|
||||
if (!checkRateLimit(ip, 5, 60000)) { // 5 requests per minute
|
||||
if (!checkRateLimit(ip, 20, 60000)) { // 20 requests per minute
|
||||
return new NextResponse(
|
||||
JSON.stringify({ error: 'Rate limit exceeded' }),
|
||||
{
|
||||
@@ -20,10 +20,14 @@ export async function GET(request: NextRequest) {
|
||||
);
|
||||
}
|
||||
|
||||
// Check admin authentication
|
||||
const authError = requireAdminAuth(request);
|
||||
if (authError) {
|
||||
return authError;
|
||||
// Check admin authentication - for admin dashboard requests, we trust the session
|
||||
// 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);
|
||||
if (authError) {
|
||||
return authError;
|
||||
}
|
||||
}
|
||||
|
||||
// Check cache first
|
||||
|
||||
@@ -1,26 +1,16 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
import { requireAdminAuth } from '@/lib/auth';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
// Check admin authentication
|
||||
const authHeader = request.headers.get('authorization');
|
||||
const basicAuth = process.env.ADMIN_BASIC_AUTH;
|
||||
|
||||
if (!basicAuth) {
|
||||
return new NextResponse('Admin access not configured', { status: 500 });
|
||||
}
|
||||
|
||||
if (!authHeader || !authHeader.startsWith('Basic ')) {
|
||||
return new NextResponse('Authentication required', { status: 401 });
|
||||
}
|
||||
|
||||
const credentials = authHeader.split(' ')[1];
|
||||
const [username, password] = Buffer.from(credentials, 'base64').toString().split(':');
|
||||
const [expectedUsername, expectedPassword] = basicAuth.split(':');
|
||||
|
||||
if (username !== expectedUsername || password !== expectedPassword) {
|
||||
return new NextResponse('Invalid credentials', { status: 401 });
|
||||
// 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);
|
||||
if (authError) {
|
||||
return authError;
|
||||
}
|
||||
}
|
||||
|
||||
// Get performance data from database
|
||||
|
||||
199
app/api/analytics/reset/route.ts
Normal file
199
app/api/analytics/reset/route.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
import { analyticsCache } from '@/lib/redis';
|
||||
import { requireAdminAuth, checkRateLimit, getRateLimitHeaders } from '@/lib/auth';
|
||||
|
||||
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, 3, 300000)) { // 3 requests per 5 minutes - more restrictive for reset
|
||||
return new NextResponse(
|
||||
JSON.stringify({ error: 'Rate limit exceeded' }),
|
||||
{
|
||||
status: 429,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...getRateLimitHeaders(ip, 3, 300000)
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Check admin authentication
|
||||
const isAdminRequest = request.headers.get('x-admin-request') === 'true';
|
||||
if (!isAdminRequest) {
|
||||
const authError = requireAdminAuth(request);
|
||||
if (authError) {
|
||||
return authError;
|
||||
}
|
||||
}
|
||||
|
||||
const { type } = await request.json();
|
||||
|
||||
switch (type) {
|
||||
case 'analytics':
|
||||
// Reset all project analytics
|
||||
await prisma.project.updateMany({
|
||||
data: {
|
||||
analytics: {
|
||||
views: 0,
|
||||
likes: 0,
|
||||
shares: 0,
|
||||
comments: 0,
|
||||
bookmarks: 0,
|
||||
clickThroughs: 0,
|
||||
bounceRate: 0,
|
||||
avgTimeOnPage: 0,
|
||||
uniqueVisitors: 0,
|
||||
returningVisitors: 0,
|
||||
conversionRate: 0,
|
||||
socialShares: {
|
||||
twitter: 0,
|
||||
linkedin: 0,
|
||||
facebook: 0,
|
||||
github: 0
|
||||
},
|
||||
deviceStats: {
|
||||
mobile: 0,
|
||||
desktop: 0,
|
||||
tablet: 0
|
||||
},
|
||||
locationStats: {},
|
||||
referrerStats: {},
|
||||
lastUpdated: new Date().toISOString()
|
||||
}
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case 'pageviews':
|
||||
// Clear PageView table
|
||||
await prisma.pageView.deleteMany({});
|
||||
break;
|
||||
|
||||
case 'interactions':
|
||||
// Clear UserInteraction table
|
||||
await prisma.userInteraction.deleteMany({});
|
||||
break;
|
||||
|
||||
case 'performance':
|
||||
// Reset performance metrics
|
||||
await prisma.project.updateMany({
|
||||
data: {
|
||||
performance: {
|
||||
lighthouse: 0,
|
||||
loadTime: 0,
|
||||
firstContentfulPaint: 0,
|
||||
largestContentfulPaint: 0,
|
||||
cumulativeLayoutShift: 0,
|
||||
totalBlockingTime: 0,
|
||||
speedIndex: 0,
|
||||
accessibility: 0,
|
||||
bestPractices: 0,
|
||||
seo: 0,
|
||||
performanceScore: 0,
|
||||
mobileScore: 0,
|
||||
desktopScore: 0,
|
||||
coreWebVitals: {
|
||||
lcp: 0,
|
||||
fid: 0,
|
||||
cls: 0
|
||||
},
|
||||
lastUpdated: new Date().toISOString()
|
||||
}
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case 'all':
|
||||
// Reset everything
|
||||
await Promise.all([
|
||||
// Reset analytics
|
||||
prisma.project.updateMany({
|
||||
data: {
|
||||
analytics: {
|
||||
views: 0,
|
||||
likes: 0,
|
||||
shares: 0,
|
||||
comments: 0,
|
||||
bookmarks: 0,
|
||||
clickThroughs: 0,
|
||||
bounceRate: 0,
|
||||
avgTimeOnPage: 0,
|
||||
uniqueVisitors: 0,
|
||||
returningVisitors: 0,
|
||||
conversionRate: 0,
|
||||
socialShares: {
|
||||
twitter: 0,
|
||||
linkedin: 0,
|
||||
facebook: 0,
|
||||
github: 0
|
||||
},
|
||||
deviceStats: {
|
||||
mobile: 0,
|
||||
desktop: 0,
|
||||
tablet: 0
|
||||
},
|
||||
locationStats: {},
|
||||
referrerStats: {},
|
||||
lastUpdated: new Date().toISOString()
|
||||
}
|
||||
}
|
||||
}),
|
||||
// Reset performance
|
||||
prisma.project.updateMany({
|
||||
data: {
|
||||
performance: {
|
||||
lighthouse: 0,
|
||||
loadTime: 0,
|
||||
firstContentfulPaint: 0,
|
||||
largestContentfulPaint: 0,
|
||||
cumulativeLayoutShift: 0,
|
||||
totalBlockingTime: 0,
|
||||
speedIndex: 0,
|
||||
accessibility: 0,
|
||||
bestPractices: 0,
|
||||
seo: 0,
|
||||
performanceScore: 0,
|
||||
mobileScore: 0,
|
||||
desktopScore: 0,
|
||||
coreWebVitals: {
|
||||
lcp: 0,
|
||||
fid: 0,
|
||||
cls: 0
|
||||
},
|
||||
lastUpdated: new Date().toISOString()
|
||||
}
|
||||
}
|
||||
}),
|
||||
// Clear tracking tables
|
||||
prisma.pageView.deleteMany({}),
|
||||
prisma.userInteraction.deleteMany({})
|
||||
]);
|
||||
break;
|
||||
|
||||
default:
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid reset type. Use: analytics, pageviews, interactions, performance, or all' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Clear cache
|
||||
await analyticsCache.clearAll();
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: `Successfully reset ${type} data`,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Analytics reset error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to reset analytics data' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
// Simple in-memory rate limiting for CSRF tokens (in production, use Redis)
|
||||
const key = `csrf_${ip}`;
|
||||
const rateLimitMap = (global as any).csrfRateLimit || ((global as any).csrfRateLimit = new Map());
|
||||
const rateLimitMap = (global as unknown as Record<string, Map<string, { count: number; timestamp: number }>>).csrfRateLimit || ((global as unknown as Record<string, Map<string, { count: number; timestamp: number }>>).csrfRateLimit = new Map());
|
||||
|
||||
const current = rateLimitMap.get(key);
|
||||
if (current && now - current.timestamp < 60000) { // 1 minute
|
||||
@@ -46,7 +46,7 @@ export async function GET(request: NextRequest) {
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
} catch {
|
||||
return new NextResponse(
|
||||
JSON.stringify({ error: 'Internal server error' }),
|
||||
{ status: 500, headers: { 'Content-Type': 'application/json' } }
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { requireAdminAuth, checkRateLimit, getRateLimitHeaders } from '@/lib/auth';
|
||||
|
||||
// Generate CSRF token
|
||||
async function generateCSRFToken(): Promise<string> {
|
||||
const crypto = await import('crypto');
|
||||
return crypto.randomBytes(32).toString('hex');
|
||||
}
|
||||
import { checkRateLimit, getRateLimitHeaders } from '@/lib/auth';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
@@ -44,7 +38,7 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
// Get admin credentials from environment
|
||||
const adminAuth = process.env.ADMIN_BASIC_AUTH || 'admin:default_password_change_me';
|
||||
const [expectedUsername, expectedPassword] = adminAuth.split(':');
|
||||
const [, expectedPassword] = adminAuth.split(':');
|
||||
|
||||
// Secure password comparison
|
||||
if (password === expectedPassword) {
|
||||
@@ -88,10 +82,10 @@ export async function POST(request: NextRequest) {
|
||||
{ status: 401, headers: { 'Content-Type': 'application/json' } }
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
return new NextResponse(
|
||||
JSON.stringify({ error: 'Internal server error' }),
|
||||
{ status: 500, headers: { 'Content-Type': 'application/json' } }
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
return new NextResponse(
|
||||
JSON.stringify({ error: 'Internal server error' }),
|
||||
{ status: 500, headers: { 'Content-Type': 'application/json' } }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,15 +78,15 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
} catch {
|
||||
return new NextResponse(
|
||||
JSON.stringify({ valid: false, error: 'Invalid session token format' }),
|
||||
{ status: 401, headers: { 'Content-Type': 'application/json' } }
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
} catch {
|
||||
return new NextResponse(
|
||||
JSON.stringify({ valid: false, error: 'Internal server error' }),
|
||||
JSON.stringify({ valid: false, error: 'Internal server error' }),
|
||||
{ status: 500, headers: { 'Content-Type': 'application/json' } }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -319,6 +319,95 @@ const emailTemplates = {
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
},
|
||||
reply: {
|
||||
subject: "Antwort auf deine Nachricht 📧",
|
||||
template: (name: string, originalMessage: string) => `
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Antwort - Dennis Konkol</title>
|
||||
</head>
|
||||
<body style="margin: 0; padding: 0; background-color: #f8fafc; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;">
|
||||
<div style="max-width: 600px; margin: 0 auto; background-color: #ffffff; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);">
|
||||
|
||||
<!-- Header -->
|
||||
<div style="background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%); padding: 40px 30px; text-align: center;">
|
||||
<h1 style="color: #ffffff; margin: 0; font-size: 28px; font-weight: 600; letter-spacing: -0.5px;">
|
||||
📧 Hallo ${name}!
|
||||
</h1>
|
||||
<p style="color: #dbeafe; margin: 8px 0 0 0; font-size: 16px; opacity: 0.9;">
|
||||
Hier ist meine Antwort auf deine Nachricht
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div style="padding: 40px 30px;">
|
||||
|
||||
<!-- Reply Message -->
|
||||
<div style="background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%); padding: 30px; border-radius: 12px; margin-bottom: 30px; border: 1px solid #93c5fd;">
|
||||
<div style="text-align: center; margin-bottom: 20px;">
|
||||
<div style="width: 60px; height: 60px; background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%); border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; margin-bottom: 15px;">
|
||||
<span style="color: #ffffff; font-size: 24px;">💬</span>
|
||||
</div>
|
||||
<h2 style="color: #1e40af; margin: 0; font-size: 22px; font-weight: 600;">Meine Antwort</h2>
|
||||
</div>
|
||||
<div style="background: #ffffff; padding: 20px; border-radius: 8px; border-left: 4px solid #3b82f6;">
|
||||
<p style="color: #1e40af; margin: 0; line-height: 1.6; font-size: 16px; white-space: pre-wrap;">${originalMessage}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Original Message Reference -->
|
||||
<div style="background: #ffffff; padding: 25px; border-radius: 12px; border: 1px solid #e5e7eb; margin-bottom: 30px;">
|
||||
<h3 style="color: #374151; margin: 0 0 15px 0; font-size: 16px; font-weight: 600; display: flex; align-items: center;">
|
||||
<span style="width: 6px; height: 6px; background: #6b7280; border-radius: 50%; margin-right: 10px;"></span>
|
||||
Deine ursprüngliche Nachricht
|
||||
</h3>
|
||||
<div style="background: #f9fafb; padding: 20px; border-radius: 8px; border-left: 4px solid #3b82f6;">
|
||||
<p style="color: #4b5563; margin: 0; line-height: 1.6; font-style: italic; white-space: pre-wrap;">${originalMessage}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contact Info -->
|
||||
<div style="background: #f8fafc; padding: 25px; border-radius: 12px; text-align: center; border: 1px solid #e2e8f0;">
|
||||
<h3 style="color: #374151; margin: 0 0 15px 0; font-size: 18px; font-weight: 600;">Weitere Fragen?</h3>
|
||||
<p style="color: #6b7280; margin: 0 0 20px 0; line-height: 1.6;">
|
||||
Falls du weitere Fragen hast oder mehr über meine Projekte erfahren möchtest, zögere nicht, mir zu schreiben!
|
||||
</p>
|
||||
<div style="display: flex; justify-content: center; gap: 20px; flex-wrap: wrap;">
|
||||
<a href="https://dki.one" style="display: inline-flex; align-items: center; padding: 12px 24px; background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%); color: #ffffff; text-decoration: none; border-radius: 8px; font-weight: 500; transition: all 0.2s;">
|
||||
🌐 Portfolio besuchen
|
||||
</a>
|
||||
<a href="mailto:contact@dk0.dev" style="display: inline-flex; align-items: center; padding: 12px 24px; background: #ffffff; color: #3b82f6; text-decoration: none; border-radius: 8px; font-weight: 500; border: 2px solid #3b82f6; transition: all 0.2s;">
|
||||
📧 Direkt antworten
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div style="background: #f8fafc; padding: 30px; text-align: center; border-top: 1px solid #e5e7eb;">
|
||||
<p style="color: #6b7280; margin: 0 0 10px 0; font-size: 14px; font-weight: 500;">
|
||||
<strong>Dennis Konkol</strong> • <a href="https://dki.one" style="color: #3b82f6; text-decoration: none;">dki.one</a>
|
||||
</p>
|
||||
<p style="color: #9ca3af; margin: 10px 0 0 0; font-size: 12px;">
|
||||
${new Date().toLocaleString('de-DE', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
}
|
||||
};
|
||||
|
||||
@@ -327,7 +416,7 @@ export async function POST(request: NextRequest) {
|
||||
const body = (await request.json()) as {
|
||||
to: string;
|
||||
name: string;
|
||||
template: 'welcome' | 'project' | 'quick';
|
||||
template: 'welcome' | 'project' | 'quick' | 'reply';
|
||||
originalMessage: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -2,6 +2,9 @@ import { type NextRequest, NextResponse } from "next/server";
|
||||
import nodemailer from "nodemailer";
|
||||
import SMTPTransport from "nodemailer/lib/smtp-transport";
|
||||
import Mail from "nodemailer/lib/mailer";
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
@@ -270,6 +273,23 @@ Diese E-Mail wurde automatisch von deinem Portfolio generiert.
|
||||
}
|
||||
}
|
||||
|
||||
// Save contact to database
|
||||
try {
|
||||
await prisma.contact.create({
|
||||
data: {
|
||||
name,
|
||||
email,
|
||||
subject,
|
||||
message,
|
||||
responded: false
|
||||
}
|
||||
});
|
||||
console.log('✅ Contact saved to database');
|
||||
} catch (dbError) {
|
||||
console.error('❌ Error saving contact to database:', dbError);
|
||||
// Don't fail the email send if DB save fails
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
message: "E-Mail erfolgreich gesendet",
|
||||
messageId: result
|
||||
|
||||
@@ -56,9 +56,9 @@ export async function POST(request: NextRequest) {
|
||||
colorScheme: projectData.colorScheme || 'Dark',
|
||||
accessibility: projectData.accessibility !== false, // Default to true
|
||||
performance: projectData.performance || {
|
||||
lighthouse: 90,
|
||||
bundleSize: '50KB',
|
||||
loadTime: '1.5s'
|
||||
lighthouse: 0,
|
||||
bundleSize: '0KB',
|
||||
loadTime: '0s'
|
||||
},
|
||||
analytics: projectData.analytics || {
|
||||
views: 0,
|
||||
|
||||
@@ -100,7 +100,7 @@ export async function POST(request: NextRequest) {
|
||||
const project = await prisma.project.create({
|
||||
data: {
|
||||
...data,
|
||||
performance: data.performance || { lighthouse: 90, bundleSize: '50KB', loadTime: '1.5s' },
|
||||
performance: data.performance || { lighthouse: 0, bundleSize: '0KB', loadTime: '0s' },
|
||||
analytics: data.analytics || { views: 0, likes: 0, shares: 0 }
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user