Last active
July 14, 2024 06:16
-
-
Save bnsngltn/0ac0236e38acadcb22fbe5ed071f3964 to your computer and use it in GitHub Desktop.
TS-Rest+Next Auth
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }; |
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) => {
...
})
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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 theprotectedHandler
(line 77) in case the user isn't authenticated: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