-
Notifications
You must be signed in to change notification settings - Fork 38
fix(app): call contact form logic directly via server action instead … #1360
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| 'use server'; | ||
|
|
||
| import { | ||
| ContactFormData, | ||
| ContactResult, | ||
| submitContact, | ||
| } from '@/utils/app/contact'; | ||
|
|
||
| export async function getContactDetails( | ||
| contactDetails: ContactFormData, | ||
| ): Promise<ContactResult> { | ||
| try { | ||
| return await submitContact(contactDetails); | ||
| } catch (error) { | ||
| console.error('Contact form submission error:', error); | ||
| return { | ||
| error: 'Failed to submit contact form', | ||
| statusCode: 500, | ||
| success: false, | ||
| }; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,119 +1,22 @@ | ||
| import { SendEmailCommand, SESClient } from '@aws-sdk/client-ses'; | ||
| import type { TurnstileServerValidationResponse } from '@marsidev/react-turnstile'; | ||
| import { NextRequest, NextResponse } from 'next/server'; | ||
|
|
||
| const sesClient = new SESClient({ | ||
| region: process.env.AWS_REGION, | ||
| }); | ||
|
|
||
| const TO_EMAIL = process.env.AWS_SES_TO_EMAIL; | ||
| const FROM_EMAIL = process.env.AWS_SES_FROM_EMAIL; | ||
| const TURNSTILE_SECRET_KEY = process.env.TURNSTILE_SECRET_KEY; | ||
| const TURNSTILE_VERIFY_URL = | ||
| 'https://challenges.cloudflare.com/turnstile/v0/siteverify'; | ||
|
|
||
| interface ContactFormData { | ||
| description: string; | ||
| email: string; | ||
| name: string; | ||
| subject: string; | ||
| token: string; | ||
| } | ||
| import { ContactFormData, submitContact } from '@/utils/app/contact'; | ||
|
|
||
| export async function POST(request: NextRequest) { | ||
| try { | ||
| const body = (await request.json()) as ContactFormData; | ||
| const result = await submitContact(body); | ||
|
|
||
| const { description, email, name, subject, token } = body; | ||
|
|
||
| if (!name || !email || !subject || !description) { | ||
| return NextResponse.json( | ||
| { err: 'Missing required fields', status: 0 }, | ||
| { status: 400 }, | ||
| ); | ||
| } | ||
|
|
||
| if (!TO_EMAIL || !FROM_EMAIL || !TURNSTILE_SECRET_KEY) { | ||
| if (!result.success) { | ||
|
Comment on lines
6
to
+10
|
||
| return NextResponse.json( | ||
| { err: 'Server configuration error', status: 0 }, | ||
| { status: 500 }, | ||
| { err: result.error, status: 0 }, | ||
| { status: result.statusCode }, | ||
| ); | ||
| } | ||
|
|
||
| if (!token) { | ||
| return NextResponse.json( | ||
| { err: 'Captcha token is missing', status: 0 }, | ||
| { status: 400 }, | ||
| ); | ||
| } | ||
|
|
||
| const formData = new URLSearchParams(); | ||
| formData.append('secret', TURNSTILE_SECRET_KEY); | ||
| formData.append('response', token); | ||
|
|
||
| const verificationResponse = await fetch(TURNSTILE_VERIFY_URL, { | ||
| body: formData.toString(), | ||
| headers: { | ||
| 'Content-Type': 'application/x-www-form-urlencoded', | ||
| }, | ||
| method: 'POST', | ||
| }); | ||
|
|
||
| if (!verificationResponse.ok) { | ||
| console.error( | ||
| 'Turnstile verification request failed:', | ||
| verificationResponse.status, | ||
| ); | ||
| return NextResponse.json( | ||
| { err: 'Captcha verification service unavailable', status: 0 }, | ||
| { status: 502 }, | ||
| ); | ||
| } | ||
|
|
||
| const data = | ||
| (await verificationResponse.json()) as TurnstileServerValidationResponse; | ||
|
|
||
| if (!data.success) { | ||
| console.log('Captcha verification failed:', data); | ||
| return NextResponse.json( | ||
| { details: data, err: 'Captcha verification failed', status: 0 }, | ||
| { status: 400 }, | ||
| ); | ||
| } | ||
|
|
||
| const params = { | ||
| Destination: { | ||
| ToAddresses: [TO_EMAIL], | ||
| }, | ||
| Message: { | ||
| Body: { | ||
| Html: { | ||
| Data: ` | ||
| <h2>New Contact Form Submission</h2> | ||
| <p><strong>From:</strong> ${name} (${email})</p> | ||
| <p><strong>Subject:</strong> ${subject}</p> | ||
| <p><strong>Message:</strong></p> | ||
| <p>${description}</p> | ||
| `, | ||
| }, | ||
| Text: { | ||
| Data: `From: ${name} (${email})\nSubject: ${subject}\n\n${description}`, | ||
| }, | ||
| }, | ||
| Subject: { | ||
| Data: subject, | ||
| }, | ||
| }, | ||
| Source: FROM_EMAIL, | ||
| }; | ||
|
|
||
| const command = new SendEmailCommand(params); | ||
| const emailResponse = await sesClient.send(command); | ||
| console.log('Email sent successfully:', emailResponse); | ||
|
|
||
| return NextResponse.json( | ||
| { message: 'Message sent successfully', status: 1 }, | ||
| { status: 200 }, | ||
| { message: result.data?.message, status: 1 }, | ||
| { status: result.statusCode }, | ||
| ); | ||
|
Comment on lines
+10
to
20
|
||
| } catch (err) { | ||
| const error = err as Error; | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,8 +1,10 @@ | ||||||
| 'use client'; | ||||||
| import { Turnstile } from '@marsidev/react-turnstile'; | ||||||
| import type { TurnstileInstance } from '@marsidev/react-turnstile'; | ||||||
| import { useTheme } from 'next-themes'; | ||||||
| import { Link, useIntlRouter } from '@/i18n/routing'; | ||||||
|
|
||||||
| import { useState } from 'react'; | ||||||
| import { useRef, useState } from 'react'; | ||||||
| import { toast } from 'react-toastify'; | ||||||
| import { useConfig } from 'app/src/hooks/app/useConfig'; | ||||||
| import { localFormat } from '@/utils/app/libs'; | ||||||
|
|
@@ -33,12 +35,26 @@ const ApiActions = ({ | |||||
| const [name, setName] = useState(''); | ||||||
| const [email, setEmail] = useState(''); | ||||||
| const [description, setDescription] = useState(''); | ||||||
| const [captchaStatus, setCaptchaStatus] = useState<string | null>(null); | ||||||
| const [token, setToken] = useState<string>(); | ||||||
| const turnstileRef = useRef<TurnstileInstance>(null); | ||||||
| const router = useIntlRouter(); | ||||||
|
|
||||||
| const { docsUrl } = useConfig(); | ||||||
| const { docsUrl, siteKey } = useConfig(); | ||||||
|
|
||||||
| const submitForm = async (event: any) => { | ||||||
| event.preventDefault(); | ||||||
|
|
||||||
| if (!siteKey) { | ||||||
| toast.error('Captcha is currently unavailable.'); | ||||||
| return; | ||||||
| } | ||||||
|
|
||||||
| if (captchaStatus !== 'solved' || !token) { | ||||||
| setCaptchaStatus('error'); | ||||||
| return; | ||||||
| } | ||||||
|
|
||||||
| try { | ||||||
| setLoading(true); | ||||||
|
|
||||||
|
|
@@ -47,13 +63,16 @@ const ApiActions = ({ | |||||
| email: email, | ||||||
| name: name, | ||||||
| subject, | ||||||
| token: token, | ||||||
| }; | ||||||
| const response = await getContactDetails(contactDetails); | ||||||
| if (!response) { | ||||||
| throw new Error('Network response was not ok'); | ||||||
| if (!response?.success) { | ||||||
| toast.error(response?.error || 'Something went wrong!'); | ||||||
| return; | ||||||
| } | ||||||
| toast.success('Thank you!'); | ||||||
| } catch (err) { | ||||||
| console.error(err); | ||||||
| toast.error('Something went wrong!'); | ||||||
| } finally { | ||||||
| setLoading(false); | ||||||
|
|
@@ -491,10 +510,42 @@ const ApiActions = ({ | |||||
| value={description} | ||||||
| /> | ||||||
| </div> | ||||||
| <div className="flex my-4"> | ||||||
| {siteKey ? ( | ||||||
| <Turnstile | ||||||
| onError={() => setCaptchaStatus('error')} | ||||||
| onExpire={() => { | ||||||
| setCaptchaStatus('expired'); | ||||||
| setToken(''); | ||||||
| }} | ||||||
| onSuccess={(token) => { | ||||||
| setToken(token); | ||||||
| setCaptchaStatus('solved'); | ||||||
| }} | ||||||
| options={{ | ||||||
| appearance: 'always', | ||||||
| refreshExpired: 'auto', | ||||||
| size: 'normal', | ||||||
| theme: theme as any, | ||||||
|
||||||
| theme: theme as any, | |
| theme: theme === 'dark' ? 'dark' : 'light', |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,15 +1,8 @@ | ||
| import { postRequest } from '@/utils/app/api'; | ||
| import { getContactDetails } from '@/actions/contact'; | ||
|
|
||
| import ContactActions from '@/components/app/Contact/ContactActions'; | ||
|
|
||
| const ContactOptions = async () => { | ||
| const getContactDetails = async (contactDeatils: any) => { | ||
| 'use server'; | ||
|
|
||
| const contactRes = await postRequest('/api/contact', contactDeatils); | ||
| return contactRes; | ||
| }; | ||
|
|
||
| return <ContactActions getContactDetails={getContactDetails} />; | ||
| }; | ||
| export default ContactOptions; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The server action is named
getContactDetails, but it actually submits the contact form. Renaming it to something likesubmitContact/submitContactFormwould make call sites clearer and avoid confusing this with a read operation.