From b1e89b64c595cedc8b991ca90e19e28c492d9b87 Mon Sep 17 00:00:00 2001 From: Damjan Savic Date: Sun, 25 Jan 2026 06:31:45 +0100 Subject: [PATCH 1/4] auto-claude: subtask-1-1 - Create server-side CSRF token generation and valid --- src/lib/csrf/server.ts | 94 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 src/lib/csrf/server.ts diff --git a/src/lib/csrf/server.ts b/src/lib/csrf/server.ts new file mode 100644 index 0000000..6abb546 --- /dev/null +++ b/src/lib/csrf/server.ts @@ -0,0 +1,94 @@ +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 { + 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 { + 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 { + 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 { + const cookieStore = await cookies(); + + try { + cookieStore.delete(CSRF_TOKEN_COOKIE_NAME); + } catch { + // Handle server component context + } +} From 87261c6b92250562cf36290b43cbd0d284c9e3c3 Mon Sep 17 00:00:00 2001 From: Damjan Savic Date: Sun, 25 Jan 2026 06:33:36 +0100 Subject: [PATCH 2/4] auto-claude: subtask-1-2 - Create client-side CSRF token retrieval utility --- src/lib/csrf/client.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 src/lib/csrf/client.ts diff --git a/src/lib/csrf/client.ts b/src/lib/csrf/client.ts new file mode 100644 index 0000000..49c0c03 --- /dev/null +++ b/src/lib/csrf/client.ts @@ -0,0 +1,17 @@ +const CSRF_META_TAG_NAME = 'csrf-token'; + +/** + * Get the CSRF token from the meta tag in the document head + * The server should render: + */ +export function getCsrfToken(): string | null { + if (typeof document === 'undefined') { + return null; + } + + const metaTag = document.querySelector( + `meta[name="${CSRF_META_TAG_NAME}"]` + ); + + return metaTag?.content || null; +} From 8bd15d9d0337a24dc89f038db1d630ed0b328828 Mon Sep 17 00:00:00 2001 From: Damjan Savic Date: Sun, 25 Jan 2026 06:36:42 +0100 Subject: [PATCH 3/4] auto-claude: subtask-1-3 - Add CSRF token generation to middleware Co-Authored-By: Claude Sonnet 4.5 --- src/middleware.ts | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/src/middleware.ts b/src/middleware.ts index d815ec3..61cceea 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -1,12 +1,45 @@ +import { NextRequest, NextResponse } from 'next/server'; import createMiddleware from 'next-intl/middleware'; import { locales, defaultLocale } from './i18n/config'; +import { randomBytes } from 'crypto'; -export default createMiddleware({ +const CSRF_TOKEN_COOKIE_NAME = 'csrf_token'; +const CSRF_TOKEN_LENGTH = 32; + +const intlMiddleware = createMiddleware({ locales, defaultLocale, 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 = { matcher: [ '/', From b75fcc9bea6b16cc71769c88e90962a1357dd1e3 Mon Sep 17 00:00:00 2001 From: Damjan Savic Date: Sun, 25 Jan 2026 06:39:02 +0100 Subject: [PATCH 4/4] auto-claude: subtask-1-4 - Create contact form server action with CSRF valida --- src/actions/contact.ts | 93 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 src/actions/contact.ts diff --git a/src/actions/contact.ts b/src/actions/contact.ts new file mode 100644 index 0000000..d846011 --- /dev/null +++ b/src/actions/contact.ts @@ -0,0 +1,93 @@ +'use server'; + +import { validateCsrfToken } from '@/lib/csrf/server'; + +interface ContactFormData { + name: string; + email: string; + message: string; +} + +interface ContactFormResult { + success: boolean; + error?: string; + errors?: Partial>; +} + +/** + * Server action to handle contact form submissions + * Validates CSRF token and form data before processing + */ +export async function submitContactForm( + formData: FormData +): Promise { + // 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> = {}; + + 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', + }; + } +}