diff --git a/app/api/analytics/dashboard/route.ts b/app/api/analytics/dashboard/route.ts index 1978a66..59cea5b 100644 --- a/app/api/analytics/dashboard/route.ts +++ b/app/api/analytics/dashboard/route.ts @@ -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 diff --git a/app/api/analytics/performance/route.ts b/app/api/analytics/performance/route.ts index 2f845c2..4a644b6 100644 --- a/app/api/analytics/performance/route.ts +++ b/app/api/analytics/performance/route.ts @@ -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 diff --git a/app/api/analytics/reset/route.ts b/app/api/analytics/reset/route.ts new file mode 100644 index 0000000..0b0a49c --- /dev/null +++ b/app/api/analytics/reset/route.ts @@ -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 } + ); + } +} diff --git a/app/api/auth/csrf/route.ts b/app/api/auth/csrf/route.ts index cb8120e..40dc59f 100644 --- a/app/api/auth/csrf/route.ts +++ b/app/api/auth/csrf/route.ts @@ -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>).csrfRateLimit || ((global as unknown as Record>).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' } } diff --git a/app/api/auth/login/route.ts b/app/api/auth/login/route.ts index 7d98aea..d6669ed 100644 --- a/app/api/auth/login/route.ts +++ b/app/api/auth/login/route.ts @@ -1,11 +1,5 @@ import { NextRequest, NextResponse } from 'next/server'; -import { requireAdminAuth, checkRateLimit, getRateLimitHeaders } from '@/lib/auth'; - -// Generate CSRF token -async function generateCSRFToken(): Promise { - 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' } } + ); + } } diff --git a/app/api/auth/validate/route.ts b/app/api/auth/validate/route.ts index 397e622..117649a 100644 --- a/app/api/auth/validate/route.ts +++ b/app/api/auth/validate/route.ts @@ -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' } } ); } diff --git a/app/api/email/respond/route.tsx b/app/api/email/respond/route.tsx index b488017..4fa1582 100644 --- a/app/api/email/respond/route.tsx +++ b/app/api/email/respond/route.tsx @@ -319,6 +319,95 @@ const emailTemplates = { ` + }, + reply: { + subject: "Antwort auf deine Nachricht 📧", + template: (name: string, originalMessage: string) => ` + + + + + + Antwort - Dennis Konkol + + +
+ + +
+

+ 📧 Hallo ${name}! +

+

+ Hier ist meine Antwort auf deine Nachricht +

+
+ + +
+ + +
+
+
+ 💬 +
+

Meine Antwort

+
+
+

${originalMessage}

+
+
+ + +
+

+ + Deine ursprüngliche Nachricht +

+
+

${originalMessage}

+
+
+ + +
+

Weitere Fragen?

+

+ Falls du weitere Fragen hast oder mehr über meine Projekte erfahren möchtest, zögere nicht, mir zu schreiben! +

+ +
+ +
+ + +
+

+ Dennis Konkoldki.one +

+

+ ${new Date().toLocaleString('de-DE', { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + })} +

+
+ +
+ + + ` } }; @@ -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; }; diff --git a/app/api/email/route.tsx b/app/api/email/route.tsx index cb04271..1535415 100644 --- a/app/api/email/route.tsx +++ b/app/api/email/route.tsx @@ -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 diff --git a/app/api/projects/import/route.ts b/app/api/projects/import/route.ts index 16b50f1..e38b953 100644 --- a/app/api/projects/import/route.ts +++ b/app/api/projects/import/route.ts @@ -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, diff --git a/app/api/projects/route.ts b/app/api/projects/route.ts index 5b59f57..11c70d8 100644 --- a/app/api/projects/route.ts +++ b/app/api/projects/route.ts @@ -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 } } }); diff --git a/app/editor/page.tsx b/app/editor/page.tsx new file mode 100644 index 0000000..9196c0c --- /dev/null +++ b/app/editor/page.tsx @@ -0,0 +1,666 @@ +'use client'; + +import React, { useState, useEffect, useRef } from 'react'; +import { useSearchParams } from 'next/navigation'; +import { motion, AnimatePresence } from 'framer-motion'; +import { + ArrowLeft, + Save, + Eye, + Plus, + X, + Bold, + Italic, + Code, + Image, + Link, + Type, + List, + ListOrdered, + Quote, + Hash, + Loader2, + Upload, + Check +} from 'lucide-react'; + +interface Project { + id: string; + title: string; + description: string; + content?: string; + category: string; + difficulty?: string; + tags?: string[]; + featured: boolean; + published: boolean; + github?: string; + live?: string; + image?: string; + createdAt: string; + updatedAt: string; +} + +export default function EditorPage() { + const searchParams = useSearchParams(); + const projectId = searchParams.get('id'); + const contentRef = useRef(null); + + const [project, setProject] = useState(null); + const [isAuthenticated, setIsAuthenticated] = useState(false); + const [isLoading, setIsLoading] = useState(true); + const [isSaving, setIsSaving] = useState(false); + const [isCreating, setIsCreating] = useState(!projectId); + const [showPreview, setShowPreview] = useState(false); + + // Form state + const [formData, setFormData] = useState({ + title: '', + description: '', + content: '', + category: 'web', + difficulty: 'beginner', + tags: [] as string[], + featured: false, + published: false, + github: '', + live: '', + image: '' + }); + + // Check authentication and load project + useEffect(() => { + const init = async () => { + try { + // Check auth + const authStatus = sessionStorage.getItem('admin_authenticated'); + const sessionToken = sessionStorage.getItem('admin_session_token'); + + console.log('Editor Auth check:', { authStatus, hasSessionToken: !!sessionToken, projectId }); + + if (authStatus === 'true' && sessionToken) { + console.log('User is authenticated'); + setIsAuthenticated(true); + + // Load project if editing + if (projectId) { + console.log('Loading project with ID:', projectId); + await loadProject(projectId); + } else { + console.log('Creating new project'); + setIsCreating(true); + } + } else { + console.log('User not authenticated'); + setIsAuthenticated(false); + } + } catch (error) { + console.error('Error in init:', error); + setIsAuthenticated(false); + } finally { + setIsLoading(false); + } + }; + + init(); + }, [projectId]); + + const loadProject = async (id: string) => { + try { + console.log('Fetching projects...'); + const response = await fetch('/api/projects'); + + if (response.ok) { + const data = await response.json(); + console.log('Projects loaded:', data); + + const foundProject = data.projects.find((p: Project) => p.id.toString() === id); + console.log('Found project:', foundProject); + + if (foundProject) { + setProject(foundProject); + setFormData({ + title: foundProject.title || '', + description: foundProject.description || '', + content: foundProject.content || '', + category: foundProject.category || 'web', + difficulty: foundProject.difficulty || 'beginner', + tags: foundProject.tags || [], + featured: foundProject.featured || false, + published: foundProject.published || false, + github: foundProject.github || '', + live: foundProject.live || '', + image: foundProject.image || '' + }); + console.log('Form data set:', formData); + } else { + console.log('Project not found with ID:', id); + } + } else { + console.error('Failed to fetch projects:', response.status); + } + } catch (error) { + console.error('Error loading project:', error); + } + }; + + const handleSave = async () => { + try { + setIsSaving(true); + + const url = projectId ? `/api/projects/${projectId}` : '/api/projects'; + const method = projectId ? 'PUT' : 'POST'; + + console.log('Saving project:', { url, method, formData }); + + const response = await fetch(url, { + method, + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(formData) + }); + + if (response.ok) { + const savedProject = await response.json(); + console.log('Project saved:', savedProject); + + // Show success and redirect + setTimeout(() => { + window.location.href = '/manage'; + }, 1000); + } else { + console.error('Error saving project:', response.status); + alert('Error saving project'); + } + } catch (error) { + console.error('Error saving project:', error); + alert('Error saving project'); + } finally { + setIsSaving(false); + } + }; + + const handleInputChange = (field: string, value: any) => { + setFormData(prev => ({ + ...prev, + [field]: value + })); + }; + + const handleTagsChange = (tagsString: string) => { + const tags = tagsString.split(',').map(tag => tag.trim()).filter(tag => tag); + setFormData(prev => ({ + ...prev, + tags + })); + }; + + // Rich text editor functions + const insertFormatting = (format: string) => { + const content = contentRef.current; + if (!content) return; + + const selection = window.getSelection(); + if (!selection || selection.rangeCount === 0) return; + + const range = selection.getRangeAt(0); + let newText = ''; + + switch (format) { + case 'bold': + newText = `**${selection.toString() || 'bold text'}**`; + break; + case 'italic': + newText = `*${selection.toString() || 'italic text'}*`; + break; + case 'code': + newText = `\`${selection.toString() || 'code'}\``; + break; + case 'h1': + newText = `# ${selection.toString() || 'Heading 1'}`; + break; + case 'h2': + newText = `## ${selection.toString() || 'Heading 2'}`; + break; + case 'h3': + newText = `### ${selection.toString() || 'Heading 3'}`; + break; + case 'list': + newText = `- ${selection.toString() || 'List item'}`; + break; + case 'orderedList': + newText = `1. ${selection.toString() || 'List item'}`; + break; + case 'quote': + newText = `> ${selection.toString() || 'Quote'}`; + break; + case 'link': + const url = prompt('Enter URL:'); + if (url) { + newText = `[${selection.toString() || 'link text'}](${url})`; + } + break; + case 'image': + const imageUrl = prompt('Enter image URL:'); + if (imageUrl) { + newText = `![${selection.toString() || 'alt text'}](${imageUrl})`; + } + break; + } + + if (newText) { + range.deleteContents(); + range.insertNode(document.createTextNode(newText)); + + // Update form data + setFormData(prev => ({ + ...prev, + content: content.textContent || '' + })); + } + }; + + if (isLoading) { + return ( +
+
+ +

Loading editor...

+
+
+ ); + } + + if (!isAuthenticated) { + return ( +
+ +
+
+ +
+

Access Denied

+

You need to be logged in to access the editor.

+
+ + +
+
+ ); + } + + return ( +
+ {/* Header */} +
+
+
+
+ +
+

+ {isCreating ? 'Create New Project' : `Edit: ${formData.title || 'Untitled'}`} +

+
+ +
+ + + +
+
+
+
+ + {/* Editor Content */} +
+
+ {/* Main Editor */} +
+ {/* Project Title */} + + handleInputChange('title', e.target.value)} + className="w-full text-3xl font-bold bg-white/10 text-white placeholder-white/50 focus:outline-none p-4 rounded-lg border border-white/20 focus:ring-2 focus:ring-blue-500" + placeholder="Enter project title..." + /> + + + {/* Rich Text Toolbar */} + +
+
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + +
+
+
+ + {/* Content Editor */} + +

Content

+
{ + const target = e.target as HTMLDivElement; + setFormData(prev => ({ + ...prev, + content: target.textContent || '' + })); + }} + suppressContentEditableWarning={true} + > + {formData.content || 'Start writing your project content...'} +
+

+ Supports Markdown formatting. Use the toolbar above or type directly. +

+
+ + {/* Description */} + +

Description

+