Skip to content

Instantly share code, notes, and snippets.

@bnsngltn
Last active July 14, 2024 06:16
Show Gist options
  • Save bnsngltn/0ac0236e38acadcb22fbe5ed071f3964 to your computer and use it in GitHub Desktop.
Save bnsngltn/0ac0236e38acadcb22fbe5ed071f3964 to your computer and use it in GitHub Desktop.
TS-Rest+Next Auth
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 };
@bnsngltn
Copy link
Author

Sample usage:

import { protectedHandler, publicHandler } from "@/server/ctx";
import { contract } from "./contract"

const postRouter = createNextRoute(contract, {
  public: publicHandler(async (args) => {
      // session is nullable
     args.ctx.session?.user
  }),
  protected: protectedHandler(async (args) => {
    // session and user is always defined 
    args.ctx.session.user
  }),
});

The args.body and the return type for each handler is still fully inferred from the contract!

@michaelschufi
Copy link

michaelschufi commented Oct 4, 2023

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;

@bnsngltn
Copy link
Author

bnsngltn commented Oct 4, 2023

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:

@michaelschufi

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.

@AndreyMay
Copy link

AndreyMay commented Jul 14, 2024

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