4. Add helper functions for sessions
To make it easy to access session information and protect our API routes we will create some helper functions:
app/sessionUtils.ts
import { serialize } from "cookie";
import { cookies, headers } from "next/headers";
import { NextRequest, NextResponse } from "next/server";
import Session, { SessionContainer, VerifySessionOptions } from "supertokens-node/recipe/session";
import { PreParsedRequest, CollectingResponse } from "supertokens-node/framework/custom";
import { HTTPMethod } from "supertokens-node/types";
import { ensureSuperTokensInit } from "./config/backend";
ensureSuperTokensInit();
export async function getSSRSession(
req?: NextRequest,
options?: VerifySessionOptions
): Promise<{
session: SessionContainer | undefined;
hasToken: boolean;
hasInvalidClaims: boolean;
baseResponse: CollectingResponse;
nextResponse?: NextResponse;
}> {
const query = req !== undefined ? Object.fromEntries(new URL(req.url).searchParams.entries()) : {};
const parsedCookies: Record<string, string> = Object.fromEntries(
(req !== undefined ? req.cookies : cookies()).getAll().map((cookie) => [cookie.name, cookie.value])
);
/**
* Pre parsed request is a wrapper exposed by SuperTokens. It is used as a helper to detect if the
* original request contains session tokens. We then use this pre parsed request to call `getSession`
* to check if there is a valid session.
*/
let baseRequest = new PreParsedRequest({
method: req !== undefined ? (req.method as HTTPMethod) : "get",
url: req !== undefined ? req.url : "",
query: query,
headers: req !== undefined ? req.headers : headers(),
cookies: parsedCookies,
getFormBody: () => req!.formData(),
getJSONBody: () => req!.json(),
});
/**
* Collecting response is a wrapper exposed by SuperTokens. In this case we are using an empty
* CollectingResponse when calling `getSession`. If the request contains valid session tokens
* the SuperTokens SDK will attach all the relevant tokens to the collecting response object which
* we can then use to return those session tokens in the final result (refer to `withSession` in this file)
*/
let baseResponse = new CollectingResponse();
try {
/**
* `getSession` will throw if session is required and there is no valid session. You can use
* `options` to configure whether or not you want to require sessions when calling `getSSRSession`
*/
let session = await Session.getSession(baseRequest, baseResponse, options);
return {
session,
hasInvalidClaims: false,
hasToken: session !== undefined,
baseResponse,
};
} catch (err) {
if (Session.Error.isErrorFromSuperTokens(err)) {
return {
hasToken: err.type !== Session.Error.UNAUTHORISED,
/**
* This allows us to protect our routes based on the current session claims. For example
* this will be true if email verification is required but the user has not verified their
* email.
*/
hasInvalidClaims: err.type === Session.Error.INVALID_CLAIMS,
session: undefined,
baseResponse,
nextResponse: new NextResponse("Authentication required", {
status: err.type === Session.Error.INVALID_CLAIMS ? 403 : 401,
}),
};
} else {
throw err;
}
}
}
export async function withSession(
request: NextRequest,
handler: (session: SessionContainer | undefined) => Promise<NextResponse>,
options?: VerifySessionOptions
) {
let { session, nextResponse, baseResponse } = await getSSRSession(request, options);
if (nextResponse) {
return nextResponse;
}
let userResponse = await handler(session);
let didAddCookies = false;
let didAddHeaders = false;
/**
* Base response is the response from SuperTokens that contains all the session tokens.
* We add all cookies and headers in the base response to the final response from the
* API to make sure sessions work correctly.
*/
for (const respCookie of baseResponse.cookies) {
didAddCookies = true;
userResponse.headers.append(
"Set-Cookie",
serialize(respCookie.key, respCookie.value, {
domain: respCookie.domain,
expires: new Date(respCookie.expires),
httpOnly: respCookie.httpOnly,
path: respCookie.path,
sameSite: respCookie.sameSite,
secure: respCookie.secure,
})
);
}
baseResponse.headers.forEach((value: string, key: string) => {
didAddHeaders = true;
userResponse.headers.set(key, value);
});
/**
* For some deployment services (Vercel for example) production builds can return cached results for
* APIs with older header values. In this case if the session tokens have changed (because of refreshing
* for example) the cached result would still contain the older tokens and sessions would stop working.
*
* As a result, if we add cookies or headers from base response we also set the Cache-Control header
* to make sure that the final result is not a cached version.
*/
if (didAddCookies || didAddHeaders) {
if (!userResponse.headers.has("Cache-Control")) {
// This is needed for production deployments with Vercel
userResponse.headers.set("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate");
}
}
return userResponse;
}
getSSRSessionwill be used in our frontend routes to get session information when rendering on the server side. This function will:{..., session: Object, hasToken: true}if a session exists{..., session: undefined, hasToken: false}if a session does not exist{..., session: undefined, hasToken: true, hasInvalidClaims: false}if the session is expired{..., session: undefined, hasToken: true, hasInvalidClaims: true}If the session claims fail their validation. For example if email verification if required but the user's email has not been verified.
withSessionwill be used as a session guard for each of our API routes. If a session exists it will be passed to the callback, which is where we will write our API logic. If no session exists it will passundefinedto the callback. This function will:- Return status
401if the session has expired - Return status
403if the session claims fail their validation. For example if email verification if required but the user's email has not been verified.
- Return status