Compare commits

..
Author SHA1 Message Date
damjan_savicandClaude Sonnet 4.5 ebf2d18969 auto-claude: subtask-1-1 - Update not-found.tsx to use next-intl and match design system
- Add next-intl internationalization support
- Use translation keys from pages.notfound (title, description, backHome)
- Change button from blue to white/zinc design system colors
- Link to default locale homepage
- Convert component to async for getTranslations

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-25 06:33:39 +01:00
5 changed files with 21 additions and 256 deletions
-93
View File
@@ -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',
};
}
}
+20 -18
View File
@@ -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">{t('title')}</h2>
<h2 className="text-2xl font-semibold mb-4">Page Not Found</h2> <p className="text-zinc-400 mb-8">
<p className="text-zinc-400 mb-8"> {t('description')}
The page you are looking for does not exist or has been moved. </p>
</p> <Link
<Link href={`/${defaultLocale}`}
href="/de" className="px-6 py-3 bg-white text-zinc-900 hover:bg-zinc-200 rounded-lg transition-colors inline-block font-medium"
className="px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors inline-block" >
> {t('backHome')}
Go to Homepage </Link>
</Link> </div>
</div> </div>
</body>
</html>
); );
} }
-17
View File
@@ -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;
}
-94
View File
@@ -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
View File
@@ -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: [
'/', '/',