Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
"react-syntax-highlighter": "^15.5.0",
"react-tabs": "^6.0.2",
"react-toastify": "10.0.5",
"server-only": "^0.0.1",
"swr": "^2.2.5",
"viem": "^2.21.53",
"yup": "^1.4.0",
Expand Down
22 changes: 22 additions & 0 deletions apps/app/src/actions/contact.ts
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(
Copy link

Copilot AI Feb 28, 2026

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 like submitContact/submitContactForm would make call sites clearer and avoid confusing this with a read operation.

Suggested change
export async function getContactDetails(
export async function submitContactForm(

Copilot uses AI. Check for mistakes.
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,
};
}
}
9 changes: 2 additions & 7 deletions apps/app/src/app/[locale]/apis/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { getContactDetails } from '@/actions/contact';
import ApiActions from '@/components/app/Apis/ApiActions';
import { getRequest, postRequest } from '@/utils/app/api';
import { getRequest } from '@/utils/app/api';
import { userApiURL } from '@/utils/app/config';

export default async function ApisPage(props: {
Expand All @@ -10,12 +11,6 @@ export default async function ApisPage(props: {
const { status } = searchParams;

const plans = await getRequest(`${userApiURL}plans`, {}, {}, false);
const getContactDetails = async (contactDeatils: any) => {
'use server';

const contactRes = await postRequest('/api/contact', contactDeatils);
return contactRes;
};
return (
<section>
<ApiActions
Expand Down
111 changes: 7 additions & 104 deletions apps/app/src/app/api/contact/route.ts
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
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If request.json() throws (invalid/malformed JSON), this handler currently returns a 500. Since that’s a client error, consider catching JSON parse errors separately and returning a 400 with an "Invalid JSON"-style message so consumers can distinguish bad input from server failures.

Copilot uses AI. Check for mistakes.
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
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The API response now sets message from result.data?.message, which can be undefined if submitContact ever returns success: true without data. For external consumers, it’s safer to always return a string message (e.g., fallback to a default) and ensure err is always a string in error cases to keep the response contract stable.

Copilot uses AI. Check for mistakes.
} catch (err) {
const error = err as Error;
Expand Down
63 changes: 57 additions & 6 deletions apps/app/src/components/app/Apis/ApiActions.tsx
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';
Expand Down Expand Up @@ -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);

Expand All @@ -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);
Expand Down Expand Up @@ -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,
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

theme from next-themes can be 'system', and casting to any may pass an unsupported value to Turnstile, causing incorrect rendering or runtime issues. Map to an explicit 'light' | 'dark' value (e.g., via resolvedTheme) instead of theme as any.

Suggested change
theme: theme as any,
theme: theme === 'dark' ? 'dark' : 'light',

Copilot uses AI. Check for mistakes.
}}
ref={turnstileRef}
siteKey={siteKey}
/>
) : (
<span className="text-red-500 text-sm">
Captcha is currently unavailable.
</span>
)}
{siteKey && captchaStatus === 'error' && (
<span className="text-red-500 text-sm p-6">
* Please verify the captcha
</span>
)}
</div>
<div className="w-full text-center my-2">
<button
className="text-sm text-white my-2 text-center font-thin px-7 py-3 dark:bg-green-250 bg-green-500 rounded"
disabled={loading}
className="text-sm text-white my-2 text-center font-thin px-7 py-3 dark:bg-green-250 bg-green-500 rounded disabled:opacity-50"
disabled={loading || !siteKey}
>
{loading ? <LoadingCircular /> : 'Send message'}
</button>
Expand Down
9 changes: 1 addition & 8 deletions apps/app/src/components/app/Contact/ContactOptions.tsx
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;
7 changes: 4 additions & 3 deletions apps/app/src/components/app/Contact/FormContact.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,9 @@ const FormContact = ({ getContactDetails, selectValue }: Props) => {
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;
} else {
setName('');
setEmail('');
Expand All @@ -66,7 +67,7 @@ const FormContact = ({ getContactDetails, selectValue }: Props) => {
toast.success('Thank you!');
}
} catch (err) {
console.log(err);
console.error(err);
toast.error('Something went wrong!');
} finally {
setLoading(false);
Expand Down
Loading