add contact api
This commit is contained in:
54
src/app/api/contact/route.ts
Normal file
54
src/app/api/contact/route.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { rateLimited } from "@/lib/server-utils";
|
||||
|
||||
const validateInput = (data: any) => {
|
||||
return (
|
||||
typeof data !== "object" ||
|
||||
(
|
||||
!data.anon &&
|
||||
(
|
||||
typeof data.name !== "string" ||
|
||||
typeof data.email !== "string"
|
||||
) ||
|
||||
typeof data.message !== "string"
|
||||
) ||
|
||||
!data.anon &&
|
||||
(
|
||||
!data.name.trim() ||
|
||||
!data.email.trim()
|
||||
) ||
|
||||
!data.message.trim()
|
||||
)
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const agent = req.headers.get("x-forwarded-for") ?? "damn";
|
||||
|
||||
const isRateLimited = await rateLimited(agent);
|
||||
|
||||
if (isRateLimited) {
|
||||
return NextResponse.json(
|
||||
{ error: "Too many requests" },
|
||||
{ status: 429 }
|
||||
);
|
||||
}
|
||||
|
||||
let data;
|
||||
try {
|
||||
data = await req.json();
|
||||
} catch {
|
||||
return NextResponse.json(
|
||||
{ error: "Invalid input" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
if (validateInput(data)) {
|
||||
return NextResponse.json(
|
||||
{ error: "Invalid input" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({ status: "ok" });
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export function GET (req: Request) {
|
||||
console.log(req.headers.get("x-forwarded-for") ?? 'damn');
|
||||
|
||||
return NextResponse.json({ status: "ok" });
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Roboto_Mono } from 'next/font/google'
|
||||
import "./globals.css";
|
||||
import Script from "next/script";
|
||||
|
||||
const roboto_mono = Roboto_Mono({
|
||||
subsets: ['latin']
|
||||
@@ -22,6 +23,7 @@ export default function RootLayout({
|
||||
className={`bg-background ${roboto_mono.className} antialiased text-white`}
|
||||
>
|
||||
{children}
|
||||
<Script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
'use client'
|
||||
|
||||
import { ChangeEvent, useRef, useState } from "react";
|
||||
import { useRef, useState } from "react";
|
||||
import axios from "axios";
|
||||
|
||||
import { CheckboxInput, Input, Submit } from "./form";
|
||||
import { FloatingLabel } from "../floating-label";
|
||||
|
||||
@@ -33,8 +35,8 @@ export default function ContactForm() {
|
||||
message: formData.get('message')!.toString()
|
||||
}
|
||||
|
||||
if ((data.name && data.email) && (data.name.length < 1 || data.email.length < 1) && !data.anon) {
|
||||
setErrorMsg("Name and email are required. If you prefer not to provide them, check the box to send anonymously.");
|
||||
if ((!data.name || !data.email) && !data.anon) {
|
||||
setErrorMsg("Name and email are required. Unless you check the box to send anonymously");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -44,42 +46,48 @@ export default function ContactForm() {
|
||||
}
|
||||
|
||||
setStatus('loading');
|
||||
//process
|
||||
|
||||
await new Promise(res => setTimeout(res, 2000));
|
||||
setStatus('success');
|
||||
try {
|
||||
const req = await axios.post('/api/contact', data);
|
||||
const res = req.data;
|
||||
console.log(res);
|
||||
setStatus('success');
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
setStatus('failed');
|
||||
setErrorMsg("Something went wrong, Sorry...");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form action="POST" className="space-y-4" ref={formRef} onSubmit={send}>
|
||||
<FloatingLabel placeholder="Leave an anonymous message, but I won't answer it">
|
||||
<FloatingLabel placeholder="Leave an anonymous message, but I can't reply to you">
|
||||
<CheckboxInput
|
||||
name="anon"
|
||||
placeholder="Send anonymously🤫"
|
||||
onChange={setAnon}
|
||||
/>
|
||||
</FloatingLabel>
|
||||
{!anon && <>
|
||||
<label className="block" htmlFor="contact-name">
|
||||
<div className="mb-2">Name</div>
|
||||
<Input
|
||||
id="contact-name"
|
||||
className="w-full"
|
||||
name="name"
|
||||
type="text"
|
||||
placeholder="Any name you want me to know"
|
||||
/>
|
||||
</label>
|
||||
<label className="block" htmlFor="contact-email">
|
||||
<div className="mb-2">Email</div>
|
||||
<Input
|
||||
id="contact-email"
|
||||
className="w-full"
|
||||
name="email"
|
||||
type="email"
|
||||
/>
|
||||
</label>
|
||||
</>}
|
||||
<label className={anon ? "hidden" : "block"} htmlFor="contact-name">
|
||||
<div className="mb-2">Name</div>
|
||||
<Input
|
||||
id="contact-name"
|
||||
className="w-full"
|
||||
name="name"
|
||||
type="text"
|
||||
placeholder="Any name you want me to know"
|
||||
/>
|
||||
</label>
|
||||
<label className={anon ? "hidden" : "block"} htmlFor="contact-email">
|
||||
<div className="mb-2">Email</div>
|
||||
<Input
|
||||
id="contact-email"
|
||||
className="w-full"
|
||||
name="email"
|
||||
type="email"
|
||||
/>
|
||||
</label>
|
||||
<label className="block" htmlFor="contact-msg">
|
||||
<div className="mb-2">Message</div>
|
||||
<textarea
|
||||
|
||||
9
src/components/turnstile.tsx
Normal file
9
src/components/turnstile.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
'use client'
|
||||
|
||||
export const CloudflareTurnstile = () => (
|
||||
<div
|
||||
className="cf-turnstile [&_#content]:bg-background"
|
||||
data-sitekey={process.env.NEXT_PUBLIC_CF_SITE_KEY}
|
||||
data-theme="dark"
|
||||
/>
|
||||
)
|
||||
5
src/lib/redis.ts
Normal file
5
src/lib/redis.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import Redis from "ioredis";
|
||||
|
||||
export const redis = new Redis(process.env.REDIS_URL!, {
|
||||
maxRetriesPerRequest: 3,
|
||||
});
|
||||
37
src/lib/server-utils.ts
Normal file
37
src/lib/server-utils.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import axios from "axios";
|
||||
import { redis } from "./redis";
|
||||
|
||||
export async function rateLimited(clientId: string) {
|
||||
const key = `contact:${clientId}`;
|
||||
const count = await redis.incr(key);
|
||||
if (count === 1) {
|
||||
await redis.expire(key, 600);
|
||||
}
|
||||
return count > 5;
|
||||
}
|
||||
|
||||
export async function validateTurnstile(token: string, remoteip: string) {
|
||||
const body = new URLSearchParams({
|
||||
secret: process.env.CF_SECRET_KEY!,
|
||||
token,
|
||||
remoteip
|
||||
})
|
||||
|
||||
try {
|
||||
const res = await axios.post("https://challenges.cloudflare.com/turnstile/v0/siteverify", body, {
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded"
|
||||
}
|
||||
});
|
||||
|
||||
const result = await res.data;
|
||||
console.log(result);
|
||||
} catch (error) {
|
||||
console.error("Turnstile validation error:", error);
|
||||
return { success: false, "error-codes": ["internal-error"] };
|
||||
}
|
||||
}
|
||||
|
||||
export async function sendEmail(target: string) {
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user