You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
I was implementing the next-auth utility and was running into some challenges getting a standard api router which would interact with the next-auth framework natively and provide similar security to the next-auth embedded routes
I whipped up this utility which would perform those actions for me and wanted to share it here for anyone else wanting something similar or possibly for incorporating in the future
// utility code
import { NextApiRequest, NextApiResponse } from "next";
import { LoggerInstance } from "next-auth";
import { getToken, JWT } from "next-auth/jwt";
export type ApiMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
type ResolvedQuery = Record<string, string | string[] | number | number[]>;
export interface ApiRouteProps {
/**
* Original NextApiRequest
*/
req: NextApiRequest;
/**
* Original NextApiResponse
*/
res: NextApiResponse;
/**
* Decoded next-auth JWT token
*/
token: JWT;
/**
* Decoded query, merged with req.query
*/
query: ResolvedQuery;
}
export type ApiRouteHandler = (props?: ApiRouteProps) => Promise<any> | void;
export interface ApiRoute {
/**
* Is this route secure? This is superceeded by the ApiRouterProps.secure prop
*/
secure?: boolean;
/**
*
* @param token The decoded next-auth JWT token
* @param query The decoded query based on the route key
* @returns true if the user is authorized, or false if not
*/
isAuthorized?: (token: JWT, query: ResolvedQuery) => boolean | Promise<boolean>;
/**
* Primary handler for routes assuming all authorization passes
*/
handler: ApiRouteHandler;
}
type ApiRouteMap = Partial<Record<ApiMethod, ApiRoute | ApiRouteHandler>>;
export type ApiRoutes = Record<string, ApiRouteMap>;
export interface ApiRouterProps {
/**
* The name of the route (for logging)
*/
name?: string;
/**
* Should all routes under this path be secured?
*/
secure?: boolean;
/**
* A logger for the router - defaults to the `console` instance
*/
log?: LoggerInstance | Console;
/**
* where the key can be one of the following
* * /static/path - the parts are matched directly
* * /[id]/path - the [id] parameter is encoded as a string
* * /[+id]/path - the [+id] parameter is encoded as a number
*/
routes: ApiRoutes;
}
export function ApiRouter({ name = "ApiRouter", secure, routes, log = console }: ApiRouterProps) {
async function RouteHandler(req: NextApiRequest, res: NextApiResponse) {
const token = await getToken({ req });
if (token == null && secure) {
res.status(401).end();
return;
}
const { method, query: originalQuery, url } = req;
const { path: paths = [] } = originalQuery;
log?.info(`${name}: Recieved ${method} request ${url}`, paths);
if (!Array.isArray(paths)) {
res.status(500).json({
error: "Unable to find `path` param - check api route"
});
return;
}
const query: ResolvedQuery = {
...originalQuery,
};
// remove the internal path
delete query.path;
const routeKey = Object
.keys(routes)
.find(key => {
const parts = key
.split("/")
.filter(v => v.length > 0);
if (parts.length === paths.length) {
let match = true;
for (let i = 0; i < parts.length; i++) {
let path = paths[i];
let part = parts[i];
// decode the parameters from the initial query
if (part.startsWith("[") && part.endsWith("]")) {
// number parameter
if (part.startsWith("[+")) {
let num = +path;
// if the value is not a number, we do not allow the match
if (isNaN(num)) {
match = false;
break;
}
}
} else if (path !== part) {
// not a direct match - short circuit
match = false;
break;
}
}
return match;
}
return false;
});
if (routeKey == null) {
log?.error(`${name}: Route not found for ${paths?.join("/")}`, null);
res.status(404).end();
return;
}
// decode the embedded parameters
routeKey
.split("/")
.filter(v => v.length > 0)
.forEach((part, i) => {
if (part.startsWith("[") && part.endsWith("]")) {
let param = part.substring(1, part.length - 1);
let value: string | number = paths[i];
// number style parameter needs to encode the value as a number instead of a string
if (param.startsWith("+")) {
// trim the leading +
param = param.substring(1);
// convert the value to a number
value = +value;
}
let currentParam = query[param];
if (typeof currentParam === "string" || typeof currentParam === "number") {
// there are multiple values of the same type in the slug
// convert the value to an array
query[param] = [
currentParam as any,
value
];
} else {
query[param] = value;
}
}
});
const methods = routes[routeKey];
if (method in methods) {
try {
const route = methods[method as ApiMethod];
let handler: ApiRouteHandler;
if (typeof route !== "function") {
const { secure: securePath, isAuthorized, handler: routeHandler } = route;
if (securePath && token == null) {
res.status(401).end();
return;
}
if (isAuthorized) {
const authorized = await isAuthorized(token, query);
if (!authorized) {
res.status(401).end();
return;
}
}
handler = routeHandler;
} else {
handler = route;
}
const result = await handler({
req, res, token, query
});
if (!res.closed) {
if (result != null) {
if (result instanceof Date) {
res.status(200).send(result.toISOString());
}
else if (typeof result === "object") {
res.status(200).json(result);
}
else {
res.status(200).send(result);
}
} else {
res.status(200).end();
}
}
}
catch (e) {
log?.error(`${name}: An unhandled exception occurred when trying to execute the handler`, e as any);
res.status(500).end();
}
} else {
res.setHeader('Allow', Object.keys(methods));
res.status(405).end(`Method ${method} Not Allowed`);
}
return;
}
return RouteHandler;
}
Usage becomes as easy as incorporating the NextAuth method in the authentication route
// pages/api/organization/[...path].ts
import { ApiRouter } from "./utils/api.util";
export default ApiRouter({
name: "OrganizationApi", // a friendly name for logging purposes
secure: true, // indicates the routes this manages are secure
routes: {
"/": { // base path routing (routes to /api/organization)
"GET": (props) => {} // direct API method with no further help
},
"[+id]": { // routes to /api/organization/[+id], will decode the [+id] parameter as a number
"GET": {
isAuthorized: (token, query) => true, // this could return an async method as well to validate roles or something similar
handler: ({ req, res, token, query }) => {
// todo: my stuff
}
},
},
"[slug]": { // routes to /api/organization/[slug], will decode the [slug] parameter as a string
"GET": ({ req, res, token, query }) => {
// todo: my stuff
},
},
"[+id]/children": { // nested route
"GET": () => {}
}
}
});
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
I was implementing the next-auth utility and was running into some challenges getting a standard api router which would interact with the next-auth framework natively and provide similar security to the next-auth embedded routes
I whipped up this utility which would perform those actions for me and wanted to share it here for anyone else wanting something similar or possibly for incorporating in the future
Usage becomes as easy as incorporating the
NextAuth
method in the authentication routeBeta Was this translation helpful? Give feedback.
All reactions