Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ebf2d18969 |
@@ -1,93 +0,0 @@
|
|||||||
'use server';
|
|
||||||
|
|
||||||
import { validateCsrfToken } from '@/lib/csrf/server';
|
|
||||||
|
|
||||||
interface ContactFormData {
|
|
||||||
name: string;
|
|
||||||
email: string;
|
|
||||||
message: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ContactFormResult {
|
|
||||||
success: boolean;
|
|
||||||
error?: string;
|
|
||||||
errors?: Partial<Record<keyof ContactFormData, string>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Server action to handle contact form submissions
|
|
||||||
* Validates CSRF token and form data before processing
|
|
||||||
*/
|
|
||||||
export async function submitContactForm(
|
|
||||||
formData: FormData
|
|
||||||
): Promise<ContactFormResult> {
|
|
||||||
// Extract CSRF token from form data
|
|
||||||
const csrfToken = formData.get('csrfToken');
|
|
||||||
|
|
||||||
// Validate CSRF token
|
|
||||||
if (!csrfToken || typeof csrfToken !== 'string') {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: 'CSRF token is missing',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const isValidToken = await validateCsrfToken(csrfToken);
|
|
||||||
if (!isValidToken) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: 'Invalid CSRF token',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract and validate form fields
|
|
||||||
const name = formData.get('name')?.toString() || '';
|
|
||||||
const email = formData.get('email')?.toString() || '';
|
|
||||||
const message = formData.get('message')?.toString() || '';
|
|
||||||
|
|
||||||
// Validation
|
|
||||||
const errors: Partial<Record<keyof ContactFormData, string>> = {};
|
|
||||||
|
|
||||||
if (!name.trim()) {
|
|
||||||
errors.name = 'Name is required';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!email.trim()) {
|
|
||||||
errors.email = 'Email is required';
|
|
||||||
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
|
||||||
errors.email = 'Invalid email format';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!message.trim()) {
|
|
||||||
errors.message = 'Message is required';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Object.keys(errors).length > 0) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
errors,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// TODO: Implement actual email sending logic here
|
|
||||||
// For now, we'll simulate successful submission
|
|
||||||
// In production, this would integrate with an email service like:
|
|
||||||
// - Supabase Edge Functions
|
|
||||||
// - SendGrid
|
|
||||||
// - AWS SES
|
|
||||||
// - Resend
|
|
||||||
|
|
||||||
// Simulate processing delay
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: error instanceof Error ? error.message : 'Failed to send message',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+12
-10
@@ -1,23 +1,25 @@
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import { getTranslations } from 'next-intl/server';
|
||||||
|
import { defaultLocale } from '@/i18n/config';
|
||||||
|
|
||||||
|
export default async function NotFound() {
|
||||||
|
const t = await getTranslations('pages.notfound');
|
||||||
|
|
||||||
export default function NotFound() {
|
|
||||||
return (
|
return (
|
||||||
<html lang="de">
|
<div className="bg-zinc-900 text-zinc-50 min-h-screen flex items-center justify-center">
|
||||||
<body className="bg-zinc-900 text-zinc-50 min-h-screen flex items-center justify-center">
|
|
||||||
<div className="text-center px-4">
|
<div className="text-center px-4">
|
||||||
<h1 className="text-6xl font-bold mb-4">404</h1>
|
<h1 className="text-6xl font-bold mb-4">404</h1>
|
||||||
<h2 className="text-2xl font-semibold mb-4">Page Not Found</h2>
|
<h2 className="text-2xl font-semibold mb-4">{t('title')}</h2>
|
||||||
<p className="text-zinc-400 mb-8">
|
<p className="text-zinc-400 mb-8">
|
||||||
The page you are looking for does not exist or has been moved.
|
{t('description')}
|
||||||
</p>
|
</p>
|
||||||
<Link
|
<Link
|
||||||
href="/de"
|
href={`/${defaultLocale}`}
|
||||||
className="px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors inline-block"
|
className="px-6 py-3 bg-white text-zinc-900 hover:bg-zinc-200 rounded-lg transition-colors inline-block font-medium"
|
||||||
>
|
>
|
||||||
Go to Homepage
|
{t('backHome')}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</div>
|
||||||
</html>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
const CSRF_META_TAG_NAME = 'csrf-token';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the CSRF token from the meta tag in the document head
|
|
||||||
* The server should render: <meta name="csrf-token" content="..." />
|
|
||||||
*/
|
|
||||||
export function getCsrfToken(): string | null {
|
|
||||||
if (typeof document === 'undefined') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const metaTag = document.querySelector<HTMLMetaElement>(
|
|
||||||
`meta[name="${CSRF_META_TAG_NAME}"]`
|
|
||||||
);
|
|
||||||
|
|
||||||
return metaTag?.content || null;
|
|
||||||
}
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
import { cookies } from 'next/headers';
|
|
||||||
import { randomBytes } from 'crypto';
|
|
||||||
|
|
||||||
const CSRF_TOKEN_COOKIE_NAME = 'csrf_token';
|
|
||||||
const CSRF_TOKEN_LENGTH = 32;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate a cryptographically secure CSRF token
|
|
||||||
*/
|
|
||||||
export function generateCsrfToken(): string {
|
|
||||||
return randomBytes(CSRF_TOKEN_LENGTH).toString('base64url');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the current CSRF token from cookies or generate a new one
|
|
||||||
*/
|
|
||||||
export async function getCsrfToken(): Promise<string> {
|
|
||||||
const cookieStore = await cookies();
|
|
||||||
const existingToken = cookieStore.get(CSRF_TOKEN_COOKIE_NAME);
|
|
||||||
|
|
||||||
if (existingToken?.value) {
|
|
||||||
return existingToken.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newToken = generateCsrfToken();
|
|
||||||
await setCsrfToken(newToken);
|
|
||||||
return newToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the CSRF token in an httpOnly cookie
|
|
||||||
*/
|
|
||||||
export async function setCsrfToken(token: string): Promise<void> {
|
|
||||||
const cookieStore = await cookies();
|
|
||||||
|
|
||||||
try {
|
|
||||||
cookieStore.set(CSRF_TOKEN_COOKIE_NAME, token, {
|
|
||||||
httpOnly: true,
|
|
||||||
secure: process.env.NODE_ENV === 'production',
|
|
||||||
sameSite: 'strict',
|
|
||||||
maxAge: 60 * 60 * 24, // 24 hours
|
|
||||||
path: '/',
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
// Handle server component context where cookies can't be set
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate a CSRF token against the stored token in cookies
|
|
||||||
*/
|
|
||||||
export async function validateCsrfToken(token: string): Promise<boolean> {
|
|
||||||
const cookieStore = await cookies();
|
|
||||||
const storedToken = cookieStore.get(CSRF_TOKEN_COOKIE_NAME);
|
|
||||||
|
|
||||||
if (!storedToken?.value || !token) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Constant-time comparison to prevent timing attacks
|
|
||||||
return timingSafeEqual(
|
|
||||||
Buffer.from(storedToken.value),
|
|
||||||
Buffer.from(token)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Timing-safe string comparison to prevent timing attacks
|
|
||||||
*/
|
|
||||||
function timingSafeEqual(a: Buffer, b: Buffer): boolean {
|
|
||||||
if (a.length !== b.length) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let result = 0;
|
|
||||||
for (let i = 0; i < a.length; i++) {
|
|
||||||
result |= a[i] ^ b[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
return result === 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete the CSRF token cookie
|
|
||||||
*/
|
|
||||||
export async function deleteCsrfToken(): Promise<void> {
|
|
||||||
const cookieStore = await cookies();
|
|
||||||
|
|
||||||
try {
|
|
||||||
cookieStore.delete(CSRF_TOKEN_COOKIE_NAME);
|
|
||||||
} catch {
|
|
||||||
// Handle server component context
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+1
-34
@@ -1,45 +1,12 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
|
||||||
import createMiddleware from 'next-intl/middleware';
|
import createMiddleware from 'next-intl/middleware';
|
||||||
import { locales, defaultLocale } from './i18n/config';
|
import { locales, defaultLocale } from './i18n/config';
|
||||||
import { randomBytes } from 'crypto';
|
|
||||||
|
|
||||||
const CSRF_TOKEN_COOKIE_NAME = 'csrf_token';
|
export default createMiddleware({
|
||||||
const CSRF_TOKEN_LENGTH = 32;
|
|
||||||
|
|
||||||
const intlMiddleware = createMiddleware({
|
|
||||||
locales,
|
locales,
|
||||||
defaultLocale,
|
defaultLocale,
|
||||||
localePrefix: 'always',
|
localePrefix: 'always',
|
||||||
});
|
});
|
||||||
|
|
||||||
export default function middleware(request: NextRequest) {
|
|
||||||
// Run the i18n middleware first
|
|
||||||
const response = intlMiddleware(request);
|
|
||||||
|
|
||||||
// Check if CSRF token exists in cookies
|
|
||||||
const existingToken = request.cookies.get(CSRF_TOKEN_COOKIE_NAME);
|
|
||||||
|
|
||||||
// Generate and set CSRF token if it doesn't exist
|
|
||||||
if (!existingToken) {
|
|
||||||
const newToken = randomBytes(CSRF_TOKEN_LENGTH).toString('base64url');
|
|
||||||
|
|
||||||
// Create a new response or clone the existing one
|
|
||||||
const finalResponse = response || NextResponse.next();
|
|
||||||
|
|
||||||
finalResponse.cookies.set(CSRF_TOKEN_COOKIE_NAME, newToken, {
|
|
||||||
httpOnly: true,
|
|
||||||
secure: process.env.NODE_ENV === 'production',
|
|
||||||
sameSite: 'strict',
|
|
||||||
maxAge: 60 * 60 * 24, // 24 hours
|
|
||||||
path: '/',
|
|
||||||
});
|
|
||||||
|
|
||||||
return finalResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
matcher: [
|
matcher: [
|
||||||
'/',
|
'/',
|
||||||
|
|||||||
Reference in New Issue
Block a user