Developing a custom functionality to retrieve a server cookie for authentication in NextJS version 14

I am in the process of incorporating an email address verification feature for users registering on my NextJS website with a WordPress installation as a headless CMS. Here's what I plan to do:

  1. Set a server token with the following value
{id: <user_id_in_WP_user_database_>, token: <token_fetched_from_REST_API_endpoint>}
  1. Send an email to the user using nodemailer and prompt them to redirect to my (/verified) route, where I can access the cookie I've set. This allows me to authenticate the user (using another WP REST API endpoint) and update their status to authorized in the WP user database.

The issue arises when I try to retrieve the cookie value upon user redirection from the email link (as the function returns undefined). While inspecting through Chrome developer tools, I can confirm that the cookie is present. Additionally, if I manually input my Route handler URL (/API/cookie), I receive the expected result (the cookie value).

Below, you will find my route handler api/cookie, which handles setting and retrieving cookies based on the HTTP method:

// API route that validates the user's token by storing it in a cookie
import { NextRequest, NextResponse } from 'next/server';
import { cookies } from "next/headers";

// Export function for GET method
export async function GET(req: NextRequest) {
  // GET request to fetch the verification cookie
  try {
    const cookie = cookies().get("verify")?.value;
    console.log('Getting cookie: ', cookies().has("verify"));
    if (cookie == undefined) {
      return new NextResponse(JSON.stringify({error: 'Missing verification cookie.'}), {status: 400});
    }
    else {
      return new NextResponse(cookie, {status: 200});
    }
  } 
  catch (error) {
    console.error('Error during verification:', error);
    return new NextResponse(JSON.stringify({error}), {status: 500});;
  }

}

export async function POST(req: NextRequest) {
  try {
    const body = await req.json();
    console.log('POST request body: ', body)
    const expires = new Date(Date.now() + 60 * 60 * 1000);
    cookies().set("verify", JSON.stringify(body), { expires, httpOnly: true });
    console.log('cookie created: ', cookies().has('verify'))
    return new NextResponse(body, {status: 200});;
  } catch (error) {
    console.error('Error:', error);
    return new NextResponse(JSON.stringify({error: 'Server error while creating cookie.'}), {status: 500});;
  }
}

Displayed below is the /verified server route, where users land after clicking the email link:

'use server'

import MaxWidthWrapper from "@/components/MaxWidthWrapper";
import { buttonVariants } from "@/components/ui/button";
import Link from "next/link";
import { wp_fetch } from "@/lib/wp-fetch";
import { Url } from "next/dist/shared/lib/router/router";
import { verify } from "@/lib/verify";

export default  async function Verified() {

  let verified: String;
  let buttonText: String;
  let buttonRedirect: Url;

  async function isVerified () {
    const wpVerified = await verify();
    if (typeof wpVerified == 'string'){
      verified = wpVerified;
      buttonText = 'Return to register form';
      buttonRedirect = '/register';
      return {verified, buttonText, buttonRedirect};
    }
    else {
      const authenticated = await wp_fetch('PUT', `users/${wpVerified.id}?roles=subscriber`, {roles: 'subscriber'});
      if(authenticated.id){
        verified = 'Your registration to <span className="text-rose-500">NextJStore</span> has been verified.';
        buttonText = 'Please return to homepage and login with your account.';
        buttonRedirect = '/';
        return {verified, buttonText, buttonRedirect};
      }
      else{
        verified = 'Internal server error. Please try again.';
        buttonText = 'Return to register form';
        buttonRedirect = '/register';
        return  {verified, buttonText, buttonRedirect};
      }
    }
  }

  return (
    <>
      <MaxWidthWrapper>
        <div className="py-20 mx-auto text-center flex flex-col items-center max-w-3xl">
          <h1 className="text-3xl font-bold tracking-tight text-gray-700 sm:text-5xl">
            {(await isVerified()).verified}
          </h1>
          <p className="mt-6 text-lg max-w-prose text-muted-foreground">
            {(await isVerified()).buttonText}
          </p>
          <div className="flex flex-col sm:flex-row gap-4 mt-6">
            {/* buttonVariants() applies default styles of the button component. With parameters, the styles change */}
            <Link href={(await isVerified()).buttonRedirect} className={buttonVariants()}>
              Return
            </Link>
          </div>
        </div>
      </MaxWidthWrapper>
    </>
  );
}

Finally, here is the verify.ts helper function that I invoke within the route without any parameters:

import { sendEmail } from "@/app/api/cookie/mailer";
import SMTPTransport from "nodemailer/lib/smtp-transport";

export async function verify(username?: string, email?: string, password?: string, id?: number) {
  if (username && email && password && id){
    const wpAppCredentials = {
      username: process.env.NEXT_PUBLIC_WP_ADMIN_USERNAME,
      password: process.env.NEXT_PUBLIC_WP_REGISTER_APP_PASSWORD,
    };
    const encryptedWpAppCredentials = btoa(`${wpAppCredentials.username}:${wpAppCredentials.password}`);
    const tokenFetch = await fetch(process.env.NEXT_PUBLIC_JWT_BASE + 'token', {
      method: 'POST',
      body: JSON.stringify({username, password}),
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Basic ${encryptedWpAppCredentials}`
      },
    });
    const json = await tokenFetch.json()
    console.log('Token:', json)
    if(tokenFetch.ok){
      const token = json.token
      const mailer: SMTPTransport.SentMessageInfo = await sendEmail(username, email, 'VERIFY');
      const res = await fetch(process.env.NEXT_PUBLIC_DEPLOY_URL + '/api/cookie', {
        method: 'POST',
        body: `{"token": "${token}", "id": "${id}"}`
      })
      const cookie = await res.json();
      console.log('Created cookie value: ', cookie)
      if (res.ok){
        return cookie;
        }
      else{
        console.error('Error while creating the cookie.')
        return ('Error while creating the cookie')
      }
    }
    else{
      console.error('Error while fetching the token from CMS.')
      return ('Error while fetching the token from CMS')
    }
  }
  else if (!username && !email && !password && !id){
    const res = await fetch(`${process.env.NEXT_PUBLIC_DEPLOY_URL}api/cookie`, {
      method: 'GET'
    });
    const cookie = await res.json();
    console.log('cookie: ', cookie);
    if (res.ok){
      const validated = await fetch(process.env.NEXT_PUBLIC_JWT_BASE + 'token/validate', {
        method: 'POST',
        headers: {
          'Authorization': `Basic ${cookie.token}`
        },
      });
      console.log('validated: ', validated)
      if(validated.status == 200){
        return cookie;
      }
      else{
        console.error('Error validating the token.')
        return ('Error validating the token');
      }
    }
    else{
      console.log('Cookie not found.')
      return('Please visit your email to validate the submitted address')
    }
  }
  else{
    console.error('Missing required parameters.')
    return ('Wrong parameters. Please try again.')
  }
}

Answer №1

In my opinion, you are making things more complex than they need to be. Feel free to utilize Lucia auth. This tool handles cookie management on the server side.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

"Exploring the dynamic duo of Angular2 and ng2Material

I am currently facing an issue with the styling in my code while using ng2Material with Angular2. First: A demonstration of Material style functioning properly can be seen in this plunker. When you click on the button, you will notice an animation effect. ...

What are the steps for creating a TypeScript version of a custom CKEditor5 build?

Currently, I am in the process of creating a personalized version of CKEditor5. By following the guidelines provided in the official documentation, I successfully obtained ckeditor.js. My goal now is to produce a typescript file (ckeditor.ts or ckeditor.d ...

Wrapping an anonymous function in a wrapper function in Typescript can prevent the inferred typing

I am encountering an issue with typing while coding: function identity<T>(v: T): T{ return v; } function execute(fn: {(n: number):string}) {} execute((n) => { // type of n is 'number' return n.toFixed(); }) execute(identity(( ...

Is there a tool in Node.js to set up a new project, similar to the scaffolding feature in Visual Studio for C# projects

Is there a way to efficiently create a node.js project with TypeScript and Express, and embed an SPA client using React and Redux templates written in TypeScript as well? Is there a scaffolding tool available to streamline this process, similar to the ea ...

Discovering subtype relationships in JSON with TypeScript

Consider the scenario where there are parent and child typescript objects: class Parent { private parentField: string; } class Child extends Parent { private childField: string; } Suppose you receive a list of JSON objects for both types via a R ...

What is the best way to send {...rest} properties to a text field in react material?

When using a material textfield inside a wrapper component and passing the remaining props as {...otherprops} in a JavaScript file, everything works fine. However, when attempting to do the same in TypeScript, an error occurs. const TextFieldWrapper = (pro ...

Configuring the correct loader in Webpack for Next JS is crucial for optimal performance

I'm encountering an issue while trying to load an HTML file with Next JS and html-loader. The error message I'm seeing is: Module parse failed: Unexpected token (2:2) You may need an appropriate loader to handle this file type, currently no loade ...

The type 'number[]' is lacking the properties 0, 1, 2, and 3 found in the type '[number, number, number, number]'

type spacing = [number, number, number, number] interface ISpacingProps { defaultValue?: spacing className?: string disabled?: boolean min?: number max?: number onChange?: (value: number | string) => void } interface IFieldState { value: ...

The transformation from className to class attribute does not occur for custom elements in JSX

I recently encountered an issue with one of my React components where the "className" attribute was being converted to "classname" in the resulting HTML, instead of the expected "class" attribute. The component had a custom element definition as follows: ...

Validation messages in an Angular application using Typescript are failing to display or disappear sporadically when applied to an HTML form that has

I am currently working on a simple app that retrieves website content from a CMS [Umbraco]. After making an Ajax call, the form comes back to me as plain HTML. I then append the form to the page and use the Angular $compile service to compile the result. T ...

Combine two arrays into one

When attempting to combine two arrays, the result looks like the image linked below: https://i.sstatic.net/3FWMZ.png I want the merged array to resemble the following example: {0: {…}, storedArr: Array(2)} 0: address: "ifanio de los Santos Ave, Ma ...

What is the best way to generate an object in TypeScript with a variety of fields as well as specific fields and methods?

In JavaScript, I can achieve this using the following code: var obj = { get(k) { return this[k] || ''; }, set(k, v) { this[k] = v; return this; } }; obj.set('a', 'A'); obj.get('a'); // returns &ap ...

Ensure that there is a predetermined search result displayed on the search page prior to the user initiating a search

After setting up a search page in nextjs and implementing the functionality to fetch data from a database (supabase), users now see a search bar upon landing on the page. However, as expected, no results are displayed until the user initiates a search. Th ...

registering a back button action in Ionic2 for multiple pages

Currently, I am in the process of developing my Ionic2 app and have encountered a dilemma regarding the functionality of registerBackButtonAction. On one page, let's call it pageA, I have implemented this function and everything is functioning as exp ...

shared interfaces in a complete javascript application

In the past, I have typically used different languages for front-end and back-end development. But now, I want to explore the benefits of using JavaScript/TypeScript on both sides so that I can have key data models defined in one central location for both ...

Is there a way to modify an object in Supabase without overwriting it?

I have a custom function called updateHistory: export const updateHistory = async (id, historyObj) => { const { data, error } = await supabase .from("property") .update({ history: historyObj }) .eq("id", id); return ...

Tips for incorporating a mesh into Forge Viewer v6 with Typescript

Is there a way to add meshes to Forge Viewer v6 using Type script? I've tried various methods that worked with v4, but I'm encountering issues now. private wallGeometry: THREE.BoxBufferGeometry; drawWalls() { ...

Determining interface value based on the presence of another optional interface value

I am working with an interface that looks like this: export interface IButton { label: string; withIcon?: boolean; underlined?: boolean; selected?: boolean; iconName?: string; isLink?: boolean; href?: string; onCLick?: () => void; } My question ...

The call in TypeScript React does not match any overload

Encountering an error with a React component that I wrote and seeking assistance. The primary component code snippet: export interface ICode { code: (code: string) => void; } export default class UserCode extends React.Component{ state = { formFil ...

What purpose does the pipe method serve in RxJS?

It seems like I understand the basic concept, but there are a few unclear aspects. Here is my typical usage pattern with an Observable: observable.subscribe(x => { }) If I need to filter data, I can achieve this by: import { first, last, map, reduce, ...