full upgrade

This commit is contained in:
2026-01-07 23:13:25 +01:00
parent 4cd3f60c98
commit c5efd28383
23 changed files with 693 additions and 226 deletions

View File

@@ -1,21 +1,41 @@
import { NextRequest, NextResponse } from 'next/server';
import { checkRateLimit, getRateLimitHeaders } from '@/lib/auth';
export async function POST(request: NextRequest) {
try {
// Rate limiting for POST requests
const ip = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || 'unknown';
if (!checkRateLimit(ip, 30, 60000)) { // 30 requests per minute for analytics
return new NextResponse(
JSON.stringify({ error: 'Rate limit exceeded' }),
{
status: 429,
headers: {
'Content-Type': 'application/json',
...getRateLimitHeaders(ip, 30, 60000)
}
}
);
}
const body = await request.json();
// Log performance metrics (you can extend this to store in database)
console.log('Performance Metric:', {
timestamp: new Date().toISOString(),
...body,
});
if (process.env.NODE_ENV === 'development') {
console.log('Performance Metric:', {
timestamp: new Date().toISOString(),
...body,
});
}
// You could store this in a database or send to external service
// For now, we'll just log it since Umami handles the main analytics
return NextResponse.json({ success: true });
} catch (error) {
console.error('Analytics API Error:', error);
if (process.env.NODE_ENV === 'development') {
console.error('Analytics API Error:', error);
}
return NextResponse.json(
{ error: 'Failed to process analytics data' },
{ status: 500 }

View File

@@ -1,5 +1,7 @@
import { type NextRequest, NextResponse } from "next/server";
import { PrismaClient } from '@prisma/client';
import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library';
import { checkRateLimit, getRateLimitHeaders } from '@/lib/auth';
const prisma = new PrismaClient();
@@ -8,6 +10,21 @@ export async function PUT(
{ params }: { params: Promise<{ id: string }> }
) {
try {
// Rate limiting for PUT requests
const ip = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || 'unknown';
if (!checkRateLimit(ip, 5, 60000)) { // 5 requests per minute
return new NextResponse(
JSON.stringify({ error: 'Rate limit exceeded' }),
{
status: 429,
headers: {
'Content-Type': 'application/json',
...getRateLimitHeaders(ip, 5, 60000)
}
}
);
}
const resolvedParams = await params;
const id = parseInt(resolvedParams.id);
const body = await request.json();
@@ -35,7 +52,20 @@ export async function PUT(
});
} catch (error) {
console.error('Error updating contact:', error);
// Handle missing database table gracefully
if (error instanceof PrismaClientKnownRequestError && error.code === 'P2021') {
if (process.env.NODE_ENV === 'development') {
console.warn('Contact table does not exist.');
}
return NextResponse.json(
{ error: 'Database table not found. Please run migrations.' },
{ status: 503 }
);
}
if (process.env.NODE_ENV === 'development') {
console.error('Error updating contact:', error);
}
return NextResponse.json(
{ error: 'Failed to update contact' },
{ status: 500 }
@@ -48,6 +78,21 @@ export async function DELETE(
{ params }: { params: Promise<{ id: string }> }
) {
try {
// Rate limiting for DELETE requests
const ip = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || 'unknown';
if (!checkRateLimit(ip, 3, 60000)) { // 3 requests per minute for DELETE (more restrictive)
return new NextResponse(
JSON.stringify({ error: 'Rate limit exceeded' }),
{
status: 429,
headers: {
'Content-Type': 'application/json',
...getRateLimitHeaders(ip, 3, 60000)
}
}
);
}
const resolvedParams = await params;
const id = parseInt(resolvedParams.id);
@@ -67,7 +112,20 @@ export async function DELETE(
});
} catch (error) {
console.error('Error deleting contact:', error);
// Handle missing database table gracefully
if (error instanceof PrismaClientKnownRequestError && error.code === 'P2021') {
if (process.env.NODE_ENV === 'development') {
console.warn('Contact table does not exist.');
}
return NextResponse.json(
{ error: 'Database table not found. Please run migrations.' },
{ status: 503 }
);
}
if (process.env.NODE_ENV === 'development') {
console.error('Error deleting contact:', error);
}
return NextResponse.json(
{ error: 'Failed to delete contact' },
{ status: 500 }

View File

@@ -1,5 +1,7 @@
import { type NextRequest, NextResponse } from "next/server";
import { PrismaClient } from '@prisma/client';
import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library';
import { checkRateLimit, getRateLimitHeaders } from '@/lib/auth';
const prisma = new PrismaClient();
@@ -40,7 +42,21 @@ export async function GET(request: NextRequest) {
});
} catch (error) {
console.error('Error fetching contacts:', error);
// Handle missing database table gracefully
if (error instanceof PrismaClientKnownRequestError && error.code === 'P2021') {
if (process.env.NODE_ENV === 'development') {
console.warn('Contact table does not exist. Returning empty result.');
}
return NextResponse.json({
contacts: [],
total: 0,
hasMore: false
});
}
if (process.env.NODE_ENV === 'development') {
console.error('Error fetching contacts:', error);
}
return NextResponse.json(
{ error: 'Failed to fetch contacts' },
{ status: 500 }
@@ -50,6 +66,21 @@ export async function GET(request: NextRequest) {
export async function POST(request: NextRequest) {
try {
// Rate limiting for POST requests
const ip = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || 'unknown';
if (!checkRateLimit(ip, 5, 60000)) { // 5 requests per minute
return new NextResponse(
JSON.stringify({ error: 'Rate limit exceeded' }),
{
status: 429,
headers: {
'Content-Type': 'application/json',
...getRateLimitHeaders(ip, 5, 60000)
}
}
);
}
const body = await request.json();
const { name, email, subject, message } = body;
@@ -86,7 +117,20 @@ export async function POST(request: NextRequest) {
}, { status: 201 });
} catch (error) {
console.error('Error creating contact:', error);
// Handle missing database table gracefully
if (error instanceof PrismaClientKnownRequestError && error.code === 'P2021') {
if (process.env.NODE_ENV === 'development') {
console.warn('Contact table does not exist.');
}
return NextResponse.json(
{ error: 'Database table not found. Please run migrations.' },
{ status: 503 }
);
}
if (process.env.NODE_ENV === 'development') {
console.error('Error creating contact:', error);
}
return NextResponse.json(
{ error: 'Failed to create contact' },
{ status: 500 }

View File

@@ -45,7 +45,7 @@ export async function POST(request: NextRequest) {
const subject = sanitizeInput(body.subject || '', 200);
const message = sanitizeInput(body.message || '', 5000);
console.log('📧 Email request received:', { email, name, subject, messageLength: message.length });
// Email request received
// Validate input
if (!email || !name || !subject || !message) {
@@ -121,12 +121,7 @@ export async function POST(request: NextRequest) {
}
};
console.log('🚀 Creating transport with options:', {
host: transportOptions.host,
port: transportOptions.port,
secure: transportOptions.secure,
user: user.split('@')[0] + '@***' // Hide full email in logs
});
// Creating transport with configured options
const transport = nodemailer.createTransport(transportOptions);
@@ -138,15 +133,17 @@ export async function POST(request: NextRequest) {
while (verificationAttempts < maxVerificationAttempts && !verificationSuccess) {
try {
verificationAttempts++;
console.log(`🔍 SMTP verification attempt ${verificationAttempts}/${maxVerificationAttempts}`);
await transport.verify();
console.log('✅ SMTP connection verified successfully');
verificationSuccess = true;
} catch (verifyError) {
console.error(`❌ SMTP verification attempt ${verificationAttempts} failed:`, verifyError);
if (process.env.NODE_ENV === 'development') {
console.error(`SMTP verification attempt ${verificationAttempts} failed:`, verifyError);
}
if (verificationAttempts >= maxVerificationAttempts) {
console.error('❌ All SMTP verification attempts failed');
if (process.env.NODE_ENV === 'development') {
console.error('All SMTP verification attempts failed');
}
return NextResponse.json(
{ error: "E-Mail-Server-Verbindung fehlgeschlagen" },
{ status: 500 },
@@ -268,7 +265,7 @@ Diese E-Mail wurde automatisch von deinem Portfolio generiert.
`,
};
console.log('📤 Sending email...');
// Sending email
// Email sending with retry logic
let sendAttempts = 0;
@@ -279,16 +276,18 @@ Diese E-Mail wurde automatisch von deinem Portfolio generiert.
while (sendAttempts < maxSendAttempts && !sendSuccess) {
try {
sendAttempts++;
console.log(`📤 Email send attempt ${sendAttempts}/${maxSendAttempts}`);
// Email send attempt
const sendMailPromise = () =>
new Promise<string>((resolve, reject) => {
transport.sendMail(mailOptions, function (err, info) {
if (!err) {
console.log('✅ Email sent successfully:', info.response);
// Email sent successfully
resolve(info.response);
} else {
console.error("❌ Error sending email:", err);
if (process.env.NODE_ENV === 'development') {
console.error("Error sending email:", err);
}
reject(err.message);
}
});
@@ -296,12 +295,16 @@ Diese E-Mail wurde automatisch von deinem Portfolio generiert.
result = await sendMailPromise();
sendSuccess = true;
console.log('🎉 Email process completed successfully');
// Email process completed successfully
} catch (sendError) {
console.error(`❌ Email send attempt ${sendAttempts} failed:`, sendError);
if (process.env.NODE_ENV === 'development') {
console.error(`Email send attempt ${sendAttempts} failed:`, sendError);
}
if (sendAttempts >= maxSendAttempts) {
console.error('❌ All email send attempts failed');
if (process.env.NODE_ENV === 'development') {
console.error('All email send attempts failed');
}
throw new Error(`Failed to send email after ${maxSendAttempts} attempts: ${sendError}`);
}
@@ -321,9 +324,11 @@ Diese E-Mail wurde automatisch von deinem Portfolio generiert.
responded: false
}
});
console.log('✅ Contact saved to database');
// Contact saved to database
} catch (dbError) {
console.error('❌ Error saving contact to database:', dbError);
if (process.env.NODE_ENV === 'development') {
console.error('Error saving contact to database:', dbError);
}
// Don't fail the email send if DB save fails
}

View File

@@ -33,6 +33,26 @@ interface ActivityStatusRow {
export async function GET() {
try {
// Check if table exists first
const tableCheck = await prisma.$queryRawUnsafe<Array<{ exists: boolean }>>(
`SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name = 'activity_status'
) as exists`
);
if (!tableCheck || !tableCheck[0]?.exists) {
// Table doesn't exist, return empty state
return NextResponse.json({
activity: null,
music: null,
watching: null,
gaming: null,
status: null,
});
}
// Fetch from activity_status table
const result = await prisma.$queryRawUnsafe<ActivityStatusRow[]>(
`SELECT * FROM activity_status WHERE id = 1 LIMIT 1`,
@@ -118,7 +138,10 @@ export async function GET() {
},
);
} catch (error) {
console.error("Error fetching activity status:", error);
// Only log non-table-missing errors
if (error instanceof Error && !error.message.includes('does not exist')) {
console.error("Error fetching activity status:", error);
}
// Return empty state on error (graceful degradation)
return NextResponse.json(

View File

@@ -1,6 +1,8 @@
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
import { apiCache } from '@/lib/cache';
import { checkRateLimit, getRateLimitHeaders } from '@/lib/auth';
import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library';
export async function GET(
request: NextRequest,
@@ -23,7 +25,20 @@ export async function GET(
return NextResponse.json(project);
} catch (error) {
console.error('Error fetching project:', error);
// Handle missing database table gracefully
if (error instanceof PrismaClientKnownRequestError && error.code === 'P2021') {
if (process.env.NODE_ENV === 'development') {
console.warn('Project table does not exist. Returning 404.');
}
return NextResponse.json(
{ error: 'Project not found' },
{ status: 404 }
);
}
if (process.env.NODE_ENV === 'development') {
console.error('Error fetching project:', error);
}
return NextResponse.json(
{ error: 'Failed to fetch project' },
{ status: 500 }
@@ -36,6 +51,21 @@ export async function PUT(
{ params }: { params: Promise<{ id: string }> }
) {
try {
// Rate limiting for PUT requests
const ip = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || 'unknown';
if (!checkRateLimit(ip, 5, 60000)) { // 5 requests per minute for PUT
return new NextResponse(
JSON.stringify({ error: 'Rate limit exceeded' }),
{
status: 429,
headers: {
'Content-Type': 'application/json',
...getRateLimitHeaders(ip, 5, 60000)
}
}
);
}
// Check if this is an admin request
const isAdminRequest = request.headers.get('x-admin-request') === 'true';
if (!isAdminRequest) {
@@ -68,7 +98,20 @@ export async function PUT(
return NextResponse.json(project);
} catch (error) {
console.error('Error updating project:', error);
// Handle missing database table gracefully
if (error instanceof PrismaClientKnownRequestError && error.code === 'P2021') {
if (process.env.NODE_ENV === 'development') {
console.warn('Project table does not exist.');
}
return NextResponse.json(
{ error: 'Database table not found. Please run migrations.' },
{ status: 503 }
);
}
if (process.env.NODE_ENV === 'development') {
console.error('Error updating project:', error);
}
return NextResponse.json(
{ error: 'Failed to update project', details: error instanceof Error ? error.message : 'Unknown error' },
{ status: 500 }
@@ -81,6 +124,30 @@ export async function DELETE(
{ params }: { params: Promise<{ id: string }> }
) {
try {
// Rate limiting for DELETE requests
const ip = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || 'unknown';
if (!checkRateLimit(ip, 3, 60000)) { // 3 requests per minute for DELETE (more restrictive)
return new NextResponse(
JSON.stringify({ error: 'Rate limit exceeded' }),
{
status: 429,
headers: {
'Content-Type': 'application/json',
...getRateLimitHeaders(ip, 3, 60000)
}
}
);
}
// Check if this is an admin request
const isAdminRequest = request.headers.get('x-admin-request') === 'true';
if (!isAdminRequest) {
return NextResponse.json(
{ error: 'Admin access required' },
{ status: 403 }
);
}
const { id: idParam } = await params;
const id = parseInt(idParam);
@@ -94,7 +161,20 @@ export async function DELETE(
return NextResponse.json({ success: true });
} catch (error) {
console.error('Error deleting project:', error);
// Handle missing database table gracefully
if (error instanceof PrismaClientKnownRequestError && error.code === 'P2021') {
if (process.env.NODE_ENV === 'development') {
console.warn('Project table does not exist.');
}
return NextResponse.json(
{ error: 'Database table not found. Please run migrations.' },
{ status: 503 }
);
}
if (process.env.NODE_ENV === 'development') {
console.error('Error deleting project:', error);
}
return NextResponse.json(
{ error: 'Failed to delete project' },
{ status: 500 }

View File

@@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
import { apiCache } from '@/lib/cache';
import { requireSessionAuth, checkRateLimit, getRateLimitHeaders } from '@/lib/auth';
import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library';
export async function GET(request: NextRequest) {
try {
@@ -96,7 +97,22 @@ export async function GET(request: NextRequest) {
return NextResponse.json(result);
} catch (error) {
console.error('Error fetching projects:', error);
// Handle missing database table gracefully
if (error instanceof PrismaClientKnownRequestError && error.code === 'P2021') {
if (process.env.NODE_ENV === 'development') {
console.warn('Project table does not exist. Returning empty result.');
}
return NextResponse.json({
projects: [],
total: 0,
pages: 0,
currentPage: 1
});
}
if (process.env.NODE_ENV === 'development') {
console.error('Error fetching projects:', error);
}
return NextResponse.json(
{ error: 'Failed to fetch projects' },
{ status: 500 }
@@ -106,6 +122,21 @@ export async function GET(request: NextRequest) {
export async function POST(request: NextRequest) {
try {
// Rate limiting for POST requests
const ip = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || 'unknown';
if (!checkRateLimit(ip, 5, 60000)) { // 5 requests per minute for POST
return new NextResponse(
JSON.stringify({ error: 'Rate limit exceeded' }),
{
status: 429,
headers: {
'Content-Type': 'application/json',
...getRateLimitHeaders(ip, 5, 60000)
}
}
);
}
// Check if this is an admin request
const isAdminRequest = request.headers.get('x-admin-request') === 'true';
if (!isAdminRequest) {
@@ -136,7 +167,20 @@ export async function POST(request: NextRequest) {
return NextResponse.json(project);
} catch (error) {
console.error('Error creating project:', error);
// Handle missing database table gracefully
if (error instanceof PrismaClientKnownRequestError && error.code === 'P2021') {
if (process.env.NODE_ENV === 'development') {
console.warn('Project table does not exist.');
}
return NextResponse.json(
{ error: 'Database table not found. Please run migrations.' },
{ status: 503 }
);
}
if (process.env.NODE_ENV === 'development') {
console.error('Error creating project:', error);
}
return NextResponse.json(
{ error: 'Failed to create project', details: error instanceof Error ? error.message : 'Unknown error' },
{ status: 500 }