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 }; |
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
@michaelschufi
When using this approach, I just handle errors globally. For Next:
You can also do something similar with the other adapters I believe.