-
-
Save bnsngltn/0ac0236e38acadcb22fbe5ed071f3964 to your computer and use it in GitHub Desktop.
import type { NextApiRequest, NextApiResponse } from "next"; | |
import type { | |
AppRoute, | |
ServerInferRequest, | |
ServerInferResponses, | |
} from "@ts-rest/core"; | |
import { getServerSession } from "next-auth"; | |
// This is your | |
import { authOptions} from './authOptions' | |
import { UnauthenticatedError } from "./errors"; | |
type NextArgs = { | |
req: NextApiRequest; | |
res: NextApiResponse; | |
}; | |
// Copy pasted from the `@ts-rest/core` codebase since it was not exported | |
type TSRestArgs<TRoute extends AppRoute> = ServerInferRequest< | |
TRoute, | |
NextApiRequest["headers"] | |
> & | |
NextArgs; | |
// An almost copy paste from the source code as well | |
// So that it can easily be extended | |
// There might be better ways to do this, but maybe my TS chops are lacking | |
type Handler<TRoute extends AppRoute, TContext = unknown> = ( | |
args: TSRestArgs<TRoute> & { ctx: TContext }, | |
) => Promise<ServerInferResponses<TRoute>>; | |
// Inspired from the context creation in tRPC | |
async function createBaseCtx<TArgs extends NextArgs>(args: TArgs) { | |
const session = await getServerSession(args.req, args.res, authOptions); | |
return { session }; | |
} | |
type BaseCtx = Awaited<ReturnType<typeof createBaseCtx>>; | |
// Builds over the base context, basically still inspired from tRPC | |
// The API for this function can be changed though so that they can be chained together | |
async function createProtectedCtx<TArgs extends NextArgs>(args: TArgs) { | |
const ctx = await createBaseCtx(args); | |
// Make sure to handle this error on the file in which you called `createNextRouter` | |
// This might be an anti pattern for now since this is not directly reflected on the contract | |
if (!ctx.session?.user) { | |
throw new UnauthenticatedError(); | |
} | |
return { | |
session: ctx.session, | |
}; | |
} | |
type ProtectedCtx = Awaited<ReturnType<typeof createProtectedCtx>>; | |
// Everyone can access | |
function publicHandler<TRoute extends AppRoute>( | |
handler: Handler<TRoute, BaseCtx>, | |
) { | |
return async (args: TSRestArgs<TRoute>) => { | |
const ctx = await createBaseCtx(args); | |
return handler({ | |
...args, | |
...{ ctx: ctx }, | |
}); | |
}; | |
} | |
// Only authenticated users can access | |
function protectedHandler<TRoute extends AppRoute>( | |
handler: Handler<TRoute, ProtectedCtx>, | |
) { | |
return async (args: TSRestArgs<TRoute>) => { | |
const ctx = await createProtectedCtx(args); | |
return handler({ ...args, ...{ ctx: ctx } }); | |
}; | |
} | |
export { publicHandler, protectedHandler }; |
Thank you so much for that! This really helped and is working perfectly :D
In case of an unauthenticated request and the error is thrown, it results in an internal server error. What I did is omit the throw
but add the following in the protectedHandler
(line 77) in case the user isn't authenticated:
return {
status: 401,
body: {
message: 'Unauthorized',
},
};
Edit: Sorry. This seems to break the type of the handler. I need to check why... :/
Seems like a workaround to the type issue would be
return {
status: 401,
body: {
message: 'Unauthorized',
},
} as never;
In case of an unauthenticated request and the error is thrown, it results in an internal server error. What I did is omit the throw but add the following in the protectedHandler (line 77) in case the user isn't authenticated:
When using this approach, I just handle errors globally. For Next:
// pages/api/[...ts-rest].tsx
export default createNextRouter(api, router, {
responseValidation: true,
errorHandler: (error: unknown, req: NextApiRequest, res: NextApiResponse) => {
// You can also handle the base error here if all of your custom errors extends a base error class
if (error instanceof UnauthenticatedError) {
// your business logic
}
},
});
You can also do something similar with the other adapters I believe.
for anyone else having issues with types, try explicitly specifying the type of the route:
protectedHandler<typeof v1Contract.tasks.list>(async (args) => {
...
})
Sample usage:
The
args.body
and the return type for each handler is still fully inferred from thecontract
!