next.jsroutesmiddlewareserver-action

middleware doesn't recognize session when using server actions


after some work i found out that when using server actions to hit some route handler the session will be undefined because server actions run on the server without a request context from the browser, and this means middleware-based session management (like using cookies or tokens) will not work with server actions, so is there a way to still use server actions to invoke route handlers and still protect them via middleware or should i completely get rid of the server actions and invoke route handlers directly ?

middleware.js

// middleware.ts
import { NextResponse } from "next/server";
import createMiddleware from "next-intl/middleware";
import { routing } from "./i18n/routing";
import { decrypt, encrypt } from "@/utils/session";

// Next-Intl Middleware
const intlMiddleware = createMiddleware(routing);

// Authentication Middleware
const authMiddleware = async (request) => {
  const path = request.nextUrl.pathname;

  // Skip auth check for non-protected routes
  if (!path.match(/\/(ar|en)\/(quizzes|admin)/)) {
    return NextResponse.next();
  }

  const sessionCookie = request.cookies.get("session")?.value;
  console.log(sessionCookie);
  // If there is no session, redirect to login
  if (!sessionCookie) {
    return NextResponse.redirect(new URL("/ar/login", request.nextUrl));
  }

  try {
    // Decrypt session to check validity
    const session = await decrypt(sessionCookie);
    if (!session?.userId) {
      const response = NextResponse.redirect(
        new URL("/ar/login", request.nextUrl)
      );
      response.cookies.delete("session");
      return response;
    }
    return NextResponse.next();
  } catch (err) {
    const response = NextResponse.redirect(
      new URL("/ar/login", request.nextUrl)
    );
    response.cookies.delete("session");
    return response;
  }
};
// Session Refresh Middleware
const updateSessionMiddleWare = async (request) => {
  const session = request.cookies.get("session")?.value;
  if (!session) return null;

  // Refresh the session expiration
  const parsed = await decrypt(session);

  const token = await encrypt({ userId: parsed.userId });
  const response = NextResponse.next();
  response.cookies.set({
    name: "session",
    value: token,
    httpOnly: true,
    secure: process.env.NODE_ENV === "production", // Ensure cookie is only sent over HTTPS in production
    sameSite: "Lax", // Allow cross-origin requests
    maxAge: 60 * 60, // 1 hour in seconds
    path: "/", // Ensure the cookie is available to all routes
  });
  return response;
};

// Combined Middleware
export default async function middleware(request) {
  const intlResponse = intlMiddleware(request);

  const sessionResponse = await updateSessionMiddleWare(request);

  const authResponse = await authMiddleware(request);
  if (authResponse.status === 307) {
    return authResponse;
  }

  if (sessionResponse) {
    const sessionCookie = sessionResponse.cookies.get("session");
    if (sessionCookie) {
      intlResponse.cookies.set(sessionCookie);
    }
  }

  // Return the final response
  return intlResponse;
}

// Matcher Configuration
export const config = {
  matcher: ["/", "/(ar|en)/:path*"],
};

the request from some client component

import { generateQuiz } from "./action";

const handleGenerate = ()=>{
const data = await generateQuiz(locale, quiz, requestBody)
}

generateQuiz server action

"use server";
import { fetchData } from "@/utils/fetchData";

// Mark this as a Server Action

export async function generateQuiz(locale, quiz, requestBody) {
  const response = await fetchData(
    `http://localhost:3000/${locale}/quizzes/initiate-quiz/${quiz}/api`,
    "POST",
    requestBody
  );
  return response;
}

fetchData function

export const fetchData = async (url, method, body = null) => {
  try {
    const options = {
      method: method,
      headers: {
        "Content-Type": "application/json",
      },
      credentials: "include", // Ensure the session cookie is sent along with the request
    };

    // Only include the body if the method is not GET or HEAD
    if (method !== "GET" && method !== "HEAD" && body !== null) {
      options.body = JSON.stringify(body);
    }

    const response = await fetch(url, options);

    if (!response.ok) {
      return false;
    }

    return await response.json();
  } catch (error) {
    return false;
  }
};

route handler to handle POST request

export async function POST(request, { params }) {
  const session = await verifySession(); 
  console.log(session); // undefined when invoked from server action, recognized otherwise
  const t = await getTranslations("HomePage");
}

Solution

  • since the cookie is not automatically passed, i had to pass it manually in the server action:

    import { cookies } from "next/headers";
    
    export async function generateQuiz(locale, quiz, requestBody) {
      const cookieStore = await cookies();
      const token = cookieStore.get("session")?.value;
      if (!token) return false;
      const response = await fetchData(
        `http://localhost:3000/${locale}/quizzes/initiate-quiz/${quiz}/api`,
        "POST",
        requestBody,
        {
          Cookie: `session=${token}`,
        }
      );
      return response;
    }
    
    

    and modified fetchData function to accept extra headers:

    export const fetchData = async (
      url,
      method,
      body = null,
      extraHeaders = {}
    ) => {
      try {
        const options = {
          method: method,
          headers: {
            "Content-Type": "application/json",
            ...extraHeaders,
          },
        };
    
        // Only include the body if the method is not GET or HEAD
        if (method !== "GET" && method !== "HEAD" && body !== null) {
          options.body = JSON.stringify(body);
        }
    
        const response = await fetch(url, options);
    
        if (!response.ok) {
          return false;
        }
    
        return await response.json();
      } catch (error) {
        console.log(error);
        return false;
      }
    };