arcjet-js icon indicating copy to clipboard operation
arcjet-js copied to clipboard

Support for Next.js server actions

Open davidmytton opened this issue 1 year ago • 6 comments

Server actions are the recommended way to do things like handling forms in Next.js. They don't act like route handlers and there is no request object to pass to the Arcjet protect() function.

We can get the headers using the headers function, but the main request details currently needs to be constructed manually. I hacked together a workaround for a form handler, but we should support this in a nicer way

import arcjet, { validateEmail } from "@arcjet/next";
import { headers } from "next/headers";
//import { ipAddress } from '@vercel/functions';

// Utility function to get the IP address of the client
// From https://nextjs.org/docs/app/api-reference/functions/headers#ip-address
function IP() {
    const FALLBACK_IP_ADDRESS = "127.0.0.1";

    // Uncomment if running on Vercel
    //const ip = ipAddress(request)
    //return ip ?? FALLBACK_IP_ADDRESS;

    const forwardedFor = headers().get("x-forwarded-for");

    if (forwardedFor) {
        return forwardedFor.split(",")[0] ?? FALLBACK_IP_ADDRESS;
    }

    return headers().get("x-real-ip") ?? FALLBACK_IP_ADDRESS;
}

// Arcjet client defined outside of the component for example purposes. You
// probably want to put this somewhere else in your app.
const aj = arcjet({
    key: process.env.ARCJET_KEY!, // Get your site key from https://app.arcjet.com
    rules: [
        validateEmail({
            mode: "LIVE", // will block requests. Use "DRY_RUN" to log only
            block: ["DISPOSABLE", "NO_MX_RECORDS"], // block disposable email addresses
        }),
    ],
});

// Test form component
export default function Form() {
    async function testForm(formData: FormData) {
        "use server";

        const headersList = headers();
        const host = headersList.get("host");

        // Construct the request needed by Arcjet because we're not in a request
        // handler here.
        const path = new URL(`http://${host}`);
        const req = {
            ip: IP(),
            method: "POST",
            host: headersList.get("host"),
            url: path.href,
            headers: headersList,
        };

        const email = formData.get("email") as string;

        const decision = await aj.protect(req, {
            email,
        });

        console.log("Decision: ", decision);
    }

    return (
        <form action={testForm}>
            <input type="email" name="email" />
            <button type="submit">Submit</button>
        </form>
    );
}

davidmytton avatar Jul 26 '24 17:07 davidmytton

zsa provides a nice typed wrapper for creating server actions which would be good to provide an example for.

davidmytton avatar Aug 29 '24 14:08 davidmytton

It looks like Vercel sets the x-real-ip header so we can add an "official check" in our library. We should find the documentation from Vercel that states they always override it.

blaine-arcjet avatar Sep 25 '24 18:09 blaine-arcjet

Looking at this again, I don't think this is going to work. new URL(`http://${host}`); doesn't give us the correct path since the URL is only constructed with a host.

Besides headers and IP via headers, I don't see any way to get the stuff we'd need for an "Arcjet Request". That being said, most of values are only required when you configure in a certain way. We could reduce those even further (for example, remove the match option on rate limit rules so we don't need a path to match).

If we want to do the above, we'd need to change the service to better handle lack of data since it currently fails immediately if things don't validate.

blaine-arcjet avatar Sep 30 '24 23:09 blaine-arcjet

We need to support server actions because it's the recommended way for handling things like forms, so we'll need to work on these changes.

davidmytton avatar Oct 01 '24 08:10 davidmytton

You guys have an official integration in Vercel. Maybe you could propose to them someway of accessing the request object within a Next.js Server Action.

empz avatar Oct 01 '24 12:10 empz

Thanks @empz, we'll ping our contact there. We also need a generic solution though because we support Next.js wherever it's hosted.

davidmytton avatar Oct 02 '24 09:10 davidmytton