diff --git a/docker-compose.yml b/docker-compose.yml index da08c71..efa8928 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,4 +15,5 @@ services: retries: 3 start_period: 40s environment: - - NODE_ENV=development + - NODE_ENV=production + - DISABLE_DUPLICATE_CHECK=true diff --git a/src/client/components/booking-form.tsx b/src/client/components/booking-form.tsx index 7567835..0d9eb78 100644 --- a/src/client/components/booking-form.tsx +++ b/src/client/components/booking-form.tsx @@ -138,7 +138,8 @@ export function BookingForm() { if (fileInput) fileInput.value = ''; }; - const handleSubmit = (e: React.FormEvent) => { + + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setErrorMessage(""); // Clear any previous error messages @@ -160,6 +161,8 @@ export function BookingForm() { setErrorMessage("Bitte bestätige die Kenntnisnahme der Allgemeinen Geschäftsbedingungen."); return; } + + // Email validation now handled in backend before slot reservation const slot = availableSlots.find((s) => s.id === selectedSlotId); const appointmentTime = slot?.time || ""; // console.log("Creating booking with data:", { @@ -206,19 +209,13 @@ export function BookingForm() { onError: (error: any) => { console.error("Booking error:", error); - // Extract error message from oRPC error structure + // Simple error handling for oRPC errors let errorText = "Ein unbekannter Fehler ist aufgetreten."; - if (error?.message) { + if (error?.cause?.message) { + errorText = error.cause.message; + } else if (error?.message && error.message !== "Internal server error") { errorText = error.message; - } else if (error?.data?.message) { - errorText = error.data.message; - } else if (error?.data?.error) { - errorText = error.data.error; - } else if (typeof error === 'string') { - errorText = error; - } else if (error?.toString) { - errorText = error.toString(); } setErrorMessage(errorText); diff --git a/src/server/lib/email-validator.ts b/src/server/lib/email-validator.ts index 30196cb..b7c599d 100644 --- a/src/server/lib/email-validator.ts +++ b/src/server/lib/email-validator.ts @@ -3,15 +3,17 @@ // Privacy-focused, no data storage, completely free type EmailValidationResult = { - valid: boolean; email: string; - domain?: string; - disposable?: boolean; - role?: boolean; - typo?: boolean; - suggestion?: string; - mx?: boolean; - error?: string; + validations: { + syntax: boolean; + domain_exists: boolean; + mx_records: boolean; + mailbox_exists: boolean; + is_disposable: boolean; + is_role_based: boolean; + }; + score: number; + status: string; }; /** @@ -25,7 +27,7 @@ export async function validateEmail(email: string): Promise<{ }> { try { // Call Rapid Email Validator API - const response = await fetch(`https://rapid-email-verifier.fly.dev/verify/${encodeURIComponent(email)}`, { + const response = await fetch(`https://rapid-email-verifier.fly.dev/api/validate?email=${encodeURIComponent(email)}`, { method: 'GET', headers: { 'Accept': 'application/json', @@ -33,15 +35,18 @@ export async function validateEmail(email: string): Promise<{ }); if (!response.ok) { - console.warn(`Email validation API error: ${response.status}`); - // If API is down, allow the email (fallback to Zod validation only) - return { valid: true }; + console.error(`Email validation API error: ${response.status}`); + // If API is down, reject the email with error message + return { + valid: false, + reason: 'E-Mail-Validierung ist derzeit nicht verfügbar. Bitte überprüfe deine E-Mail-Adresse und versuche es erneut.' + }; } const data: EmailValidationResult = await response.json(); // Check if email is disposable/temporary - if (data.disposable) { + if (data.validations.is_disposable) { return { valid: false, reason: 'Temporäre oder Wegwerf-E-Mail-Adressen sind nicht erlaubt. Bitte verwende eine echte E-Mail-Adresse.', @@ -49,20 +54,26 @@ export async function validateEmail(email: string): Promise<{ } // Check if MX records exist (deliverable) - if (data.mx === false) { + if (!data.validations.mx_records) { return { valid: false, reason: 'Diese E-Mail-Adresse kann keine E-Mails empfangen. Bitte überprüfe deine E-Mail-Adresse.', }; } - // Check if email is generally valid - if (!data.valid) { - const suggestion = data.suggestion ? ` Meintest du vielleicht: ${data.suggestion}?` : ''; + // Check if domain exists + if (!data.validations.domain_exists) { return { valid: false, - reason: `Ungültige E-Mail-Adresse.${suggestion}`, - suggestion: data.suggestion, + reason: 'Die E-Mail-Domain existiert nicht. Bitte überprüfe deine E-Mail-Adresse.', + }; + } + + // Check if email syntax is valid + if (!data.validations.syntax) { + return { + valid: false, + reason: 'Ungültige E-Mail-Adresse. Bitte überprüfe die Schreibweise.', }; } @@ -71,9 +82,11 @@ export async function validateEmail(email: string): Promise<{ } catch (error) { console.error('Email validation error:', error); - // If validation fails, allow the email (fallback to Zod validation only) - // This ensures the booking system continues to work even if the API is down - return { valid: true }; + // If validation fails, reject the email with error message + return { + valid: false, + reason: 'E-Mail-Validierung ist derzeit nicht verfügbar. Bitte überprüfe deine E-Mail-Adresse und versuche es erneut.' + }; } } diff --git a/src/server/routes/rpc.ts b/src/server/routes/rpc.ts index fc7495c..043bdc5 100644 --- a/src/server/routes/rpc.ts +++ b/src/server/routes/rpc.ts @@ -21,10 +21,7 @@ rpcApp.all("/*", async (c) => { } catch (error) { console.error("RPC Handler error:", error); - // Preserve the original error message if it's a known error - const errorMessage = error instanceof Error ? error.message : "Internal server error"; - const statusCode = error instanceof Error && error.message.includes("bereits eine Buchung") ? 400 : 500; - - return c.json({ error: errorMessage }, statusCode); + // Let oRPC handle errors properly + throw error; } }); diff --git a/src/server/rpc/bookings.ts b/src/server/rpc/bookings.ts index 10b6de2..e68ef3e 100644 --- a/src/server/rpc/bookings.ts +++ b/src/server/rpc/bookings.ts @@ -96,12 +96,15 @@ const create = os ); } - // Deep email validation using Rapid Email Validator API + // Email validation before slot reservation + console.log(`Validating email: ${input.customerEmail}`); const emailValidation = await validateEmail(input.customerEmail); + console.log(`Email validation result:`, emailValidation); if (!emailValidation.valid) { + console.log(`Email validation failed: ${emailValidation.reason}`); throw new Error(emailValidation.reason || "Ungültige E-Mail-Adresse"); } - + // Validate that the booking is not in the past const today = new Date().toISOString().split("T")[0]; // YYYY-MM-DD if (input.appointmentDate < today) { @@ -118,8 +121,8 @@ const create = os } // Prevent double booking: same customer email with pending/confirmed on same date - // Skip duplicate check in development mode - if (process.env.NODE_ENV !== 'development') { + // Skip duplicate check when DISABLE_DUPLICATE_CHECK is set + if (!process.env.DISABLE_DUPLICATE_CHECK) { const existing = await kv.getAllItems(); const hasConflict = existing.some(b => b.customerEmail.toLowerCase() === input.customerEmail.toLowerCase() && @@ -137,7 +140,11 @@ const create = os status: "pending" as const, createdAt: new Date().toISOString() }; - // If a slotId is provided, tentatively reserve the slot (mark reserved but pending) + + // First save the booking + await kv.setItem(id, booking); + + // Then reserve the slot only after successful booking creation if (booking.slotId) { const slot = await availabilityKV.getItem(booking.slotId); if (!slot) throw new Error("Availability slot not found"); @@ -149,12 +156,11 @@ const create = os }; await availabilityKV.setItem(slot.id, updatedSlot); } - await kv.setItem(id, booking); // Notify customer: request received (pending) void (async () => { // Create booking access token for status viewing - const bookingAccessToken = await queryClient.cancellation.createToken({ input: { bookingId: id } }); + const bookingAccessToken = await queryClient.cancellation.createToken({ bookingId: id }); const bookingUrl = generateUrl(`/booking/${bookingAccessToken.token}`); const formattedDate = formatDateGerman(input.appointmentDate); @@ -222,8 +228,10 @@ const create = os } })(); return booking; - } catch (error) { + } catch (error) { console.error("Booking creation error:", error); + + // Re-throw the error for oRPC to handle throw error; } }); @@ -297,7 +305,7 @@ const updateStatus = os try { if (input.status === "confirmed") { // Create booking access token for this booking (status + cancellation) - const bookingAccessToken = await queryClient.cancellation.createToken({ input: { bookingId: booking.id } }); + const bookingAccessToken = await queryClient.cancellation.createToken({ bookingId: booking.id }); const formattedDate = formatDateGerman(booking.appointmentDate); const bookingUrl = generateUrl(`/booking/${bookingAccessToken.token}`);