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");
}
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;
}
};