-
-
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.bodyand the return type for each handler is still fully inferred from thecontract!