wpengine / faustjs

Faust.js™ - The Headless WordPress Framework
https://faustjs.org
Other
1.41k stars 126 forks source link

[BUG] onLogin function does not work properly on vercel once deployed, but works on local development #1919

Open CesarBenavides777 opened 2 months ago

CesarBenavides777 commented 2 months ago

Description

As a user I want to be able to use the onLogin function to properly log in and see the my-account page using the @faustwp/experimental-app-router package. The implementation shown in the example directory does in fact work with local development as intended. But when deployed to production on vercel you get a:

There was an error logging in the user

Steps to reproduce

  1. Clone https://github.com/CesarBenavides777/cesar-benavides
  2. Enter your own WP settings into the .env file
  3. Run bun dev
  4. Navigate to /login page and log in
  5. Works as expected
  6. Deploy to vercel
  7. Repeat step 4
  8. It does not work

Additional context

Related Discord discussion: https://discord.com/channels/836253505944813629/1233495010440122419

Vercel Error Logs:

[GET] /api/faust/token?code=BHyAt9ZOeAApN%2FQ4b40C11EeT0neTyjYgYaBZAiOgbMfQj%2BjAF9YHyUs6CHvp8KSYKrcEjFJ8KXP8e9YwDG4nw%3D%3D&nxtProute=token status=500

image

Invalid response for authorize handler: TypeError: t.NextResponse is not a constructor
    at nW (/var/task/.next/server/chunks/25.js:49:3499)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async /var/task/node_modules/next/dist/compiled/next-server/app-route.runtime.prod.js:6:36258
    at async eR.execute (/var/task/node_modules/next/dist/compiled/next-server/app-route.runtime.prod.js:6:26874)
    at async eR.handle (/var/task/node_modules/next/dist/compiled/next-server/app-route.runtime.prod.js:6:37512)
    at async es (/var/task/node_modules/next/dist/compiled/next-server/server.runtime.prod.js:16:25465)
    at async en.responseCache.get.routeKind (/var/task/node_modules/next/dist/compiled/next-server/server.runtime.prod.js:17:1026)
    at async r6.renderToResponseWithComponentsImpl (/var/task/node_modules/next/dist/compiled/next-server/server.runtime.prod.js:17:508)
    at async r6.renderPageComponent (/var/task/node_modules/next/dist/compiled/next-server/server.runtime.prod.js:17:5121)
    at async r6.renderToResponseImpl (/var/task/node_modules/next/dist/compiled/next-server/server.runtime.prod.js:17:5708)

image

@faustwp/core Version

^3.0.3

@faustwp/cli Version

^3.0.2

FaustWP Plugin Version

1.3.2

WordPress Version

6.5.5

Additional environment details

Deployed on Vercel WordPress is on instaWP

Cookies have been confirmed to work across domains.

WPGraphQL JWT Auth does in fact break this because of a wrong number of segments FYI

Links: CMS: https://cms.cesarbenavides.com Frontend: https://staging.cesarbenavides.com

Using the WPGraphQL CORS plugin for additional testing (doesn't work when installed or uninstalled)

Please confirm that you have searched existing issues in the repo.

CesarBenavides777 commented 2 months ago

I think it has something to do with how the Server object is passed into the tokenHandler when logged locally I see the full server object but when logged on production it doesn't seem to get the NextResponse object/constructor.

Production:

server Object [Module] {}

Locally:

server Object [Module] {
  ImageResponse: [Getter],
  NextRequest: [Getter],
  NextResponse: [Getter],
  URLPattern: [Getter],
  unstable_after: [Getter],
  userAgent: [Getter],
  userAgentFromString: [Getter]
}

For some reason production builds don't have access to this?

CesarBenavides777 commented 2 months ago

I think importing NextResponse directly inside the tokenHandler seems to fix it!!!!

Patched this file experimental-app-router/dist/server/routeHandler/tokenHandler.js

import { cookies } from 'next/headers.js';
import { getWpUrl, getWpSecret } from '../../faust-core-utils.js';
import { NextResponse } from 'next/server';

export async function tokenHandler(req, s) {
    var _a, _b;
    try {
        const secretKey = getWpSecret();
        if (!secretKey) {
            throw new Error('FAUST_SECRET_KEY must be set');
        }
        const { url } = req;
        const code = (_a = new URL(url).searchParams.get('code')) !== null && _a !== void 0 ? _a : undefined;
        const cookieStore = cookies();
        const cookieName = `${getWpUrl()}-rt`;
        const refreshToken = (_b = cookieStore.get(cookieName)) === null || _b === void 0 ? void 0 : _b.value;
        if (!refreshToken && !code) {
            return new Response(JSON.stringify({ error: 'Unauthorized' }), {
                status: 401,
                headers: {
                    'Content-Type': 'application/json',
                },
            });
        }
        const wpFaustAuthorizeEndpoint = `${getWpUrl()}/?rest_route=/faustwp/v1/authorize`;
        const response = await fetch(wpFaustAuthorizeEndpoint, {
            headers: {
                'Content-Type': 'application/json',
                'x-faustwp-secret': secretKey,
            },
            method: 'POST',
            body: JSON.stringify({
                code,
                refreshToken,
            }),
        });

        // Log response status and body
        console.log('Response status:', response.status);
        const responseBody = await response.text();
        console.log('Response body:', responseBody);

        if (!response.ok) {
            // @TODO Delete the cookie
            // cookieStore.delete(cookieName);
            // @TODO throw different errors based on response
            return new Response(JSON.stringify({ error: 'Unauthorized' }), {
                status: 401,
                headers: {
                    'Content-Type': 'application/json',
                },
            });
        }

        const data = JSON.parse(responseBody);

        const res = new NextResponse(JSON.stringify(data), { // Ensure correct usage
            status: 200,
        });

        console.log("data", data);
        console.log("res", res);

        res.cookies.set(cookieName, data.refreshToken, {
            secure: true,
            httpOnly: true,
            path: '/',
            expires: new Date(data.refreshTokenExpiration * 1000),
            sameSite: 'lax',
        });

        return res;
    } catch (err) {
        console.error('Invalid response for authorize handler:', err);
        return new Response(JSON.stringify({ error: 'Internal Server Error' }), {
            status: 500,
            headers: {
                'Content-Type': 'application/json',
            },
        });
    }
}
CesarBenavides777 commented 2 months ago

I also updated the useFormState hook to useActionState using React 19 and Next 15 RCs. On the login page.

"use client";

import { useFormStatus } from "react-dom";
import { useActionState } from "react";
import { loginAction } from "./action";

function SubmitButton() {
  const status = useFormStatus();

  return (
    <button disabled={status.pending}>
      {status.pending ? "Loading..." : "Login"}
    </button>
  );
}

export default function Page() {
  const [state, formAction] = useActionState(loginAction, {});

  return (
    <>
      <h2>Login</h2>

      <form action={formAction}>
        <fieldset>
          <label htmlFor="usernameEmail">Username or Email</label>
          <input type="name" name="usernameEmail" />
        </fieldset>

        <fieldset>
          <label htmlFor="password">Password</label>
          <input type="password" name="password" />
        </fieldset>

        <SubmitButton />

        {state.error && (
          <p dangerouslySetInnerHTML={{ __html: state.error }}></p>
        )}
      </form>
    </>
  );
}
theodesp commented 2 months ago

@CesarBenavides777 Thank you I will take a look. I remember we had to provide a workaround at one point https://github.com/wpengine/faustjs/commit/42ded8091e0025828638dbf149777c876a0559b2

in order to fix some build issues. I think Vercel is messing around with those runtimes. It does not make sense that this is happening. I will check your solution as well.

cwhatley commented 2 months ago

FYI, I'm using experimental app router with the same dependencies listed here and am not having any issues on a production instance on vercel. Is there perhaps a vercel configuration that you @CesarBenavides777 has set that might be interfering here? We're not using any of the premium vercel features at this point.

CesarBenavides777 commented 2 months ago

@cwhatley

Nothing outside the ordinary: image image

Maybe not using npm or yarn? Using bun currently

cwhatley commented 2 months ago

I'm using yarn + corepack, node 20.x and have the same env vars set.

I do have a monorepo, so my build + start look like:

corepack enable && yarn workspace XXX build and yarn workspace XXX start

akalex-x commented 2 weeks ago

I am experience the same issue when deploying to vercel, let me know if you find a fix!

cgar420 commented 2 weeks ago

I have the same problem on Vercel and Netlify