Closed magnusrodseth closed 1 year ago
Can you send me the code you are attempting? Are you using the latest @clerk/nextjs package ?
I'm attaching some relevant snippets:
packages/api/src/context.ts
import { prisma } from "@acme/db";
import { type inferAsyncReturnType } from "@trpc/server";
import type { User } from "@clerk/nextjs/api";
import { influx } from "./lib/influx";
import { RequestLike } from "@clerk/nextjs/dist/server/types";
import { getUser } from "./lib/clerk";
/**
* This metadata comes from Clerk. We ensure that the role is typesafe if it exists.
*/
export type CustomClerkMetadata = Record<string, unknown> & {
role?: "user" | "admin" | undefined;
};
export type UserProps = {
user: (User & CustomClerkMetadata) | null;
};
/** Use this helper for:
* - testing, where we dont have to Mock Next.js' req/res
* - trpc's `createSSGHelpers` where we don't have req/res
* @see https://beta.create.t3.gg/en/usage/trpc#-servertrpccontextts
*/
export const createContextInner = async ({ user }: UserProps) => {
console.log(user);
return {
user,
prisma,
influx,
};
};
/**
* This is the actual context you'll use in your router
* @link https://trpc.io/docs/context
**/
export const createContext = async (req: RequestLike) => {
const user = await getUser(req);
return await createContextInner({ user });
};
export type Context = inferAsyncReturnType<typeof createContext>;
packages/api/src/lib/clerk.ts
import { RequestLike } from "@clerk/nextjs/dist/server/types";
import { getAuth, clerkClient } from "@clerk/nextjs/server";
import { inferAsyncReturnType } from "@trpc/server";
import { CustomClerkMetadata } from "../context";
export const getUser = async (req: RequestLike) => {
const { userId } = getAuth(req);
const user = userId ? await clerkClient.users.getUser(userId) : null;
return user
? {
...user,
// Clerk's privateMetadata is a `Record<string, unknown>`, so we need to parse it
privateMetadata: user?.privateMetadata as CustomClerkMetadata,
}
: null;
};
export const isAdmin = (user: inferAsyncReturnType<typeof getUser>) => {
return user?.privateMetadata?.role === "admin";
};
packages/api/src/router/auth.ts
import { isAdmin } from "../lib/clerk";
import { protectedProcedure, publicProcedure, router } from "../trpc";
export const authRouter = router({
isAdmin: protectedProcedure.query(({ ctx }) => {
return !!isAdmin(ctx.user);
}),
getSession: publicProcedure.query(({ ctx }) => {
return ctx.user;
}),
getSecretMessage: protectedProcedure.query(() => {
return "you can see this secret message!";
}),
});
apps/web/src/middleware.ts
import { withClerkMiddleware } from "@clerk/nextjs/server";
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { getUser, isAdmin } from "@acme/api/src/lib/clerk";
const publicPaths = ["/", "/sign-in*", "/sign-up*"];
const adminPaths = ["/admin*"];
const isPublicPath = (path: string) => {
return publicPaths.find((x) =>
path.match(new RegExp(`^${x}$`.replace("*$", "($|/)"))),
);
};
const isAdminPath = (path: string) => {
return adminPaths.find((x) =>
path.match(new RegExp(`^${x}$`.replace("*$", "($|/)"))),
);
};
export default withClerkMiddleware(async (req: NextRequest) => {
if (isPublicPath(req.nextUrl.pathname)) {
return NextResponse.next();
}
const user = await getUser(req);
if (!user) {
const signInUrl = new URL("/sign-in", req.url);
return NextResponse.redirect(signInUrl);
}
if (isAdminPath(req.nextUrl.pathname) && !isAdmin(user)) {
const signInUrl = new URL("/sign-in", req.url);
return NextResponse.redirect(signInUrl);
}
return NextResponse.next();
});
export const config = {
matcher: [
/**
* Match request paths except for the ones starting with:
* - _next
* - static (static files)
* - favicon.ico (favicon file)
*
* This includes images, and requests from TRPC.
*/
"/(.*?trpc.*?|(?!static|.*\\..*|_next|favicon.ico).*)",
],
};
I just updated @clerk/nextjs
, and I'm now using ^4.11.2
. Problem still occurs.
Sorry for the wall of text/code.
For some more context, take a look at what happens when creating the context.
packages/api/src/context.ts
/**
* This is the actual context you'll use in your router
* @link https://trpc.io/docs/context
**/
export const createContext = async (req: RequestLike) => {
console.log("checkpoint 1");
const user = await getUser(req);
console.log("checkpoint 2");
return await createContextInner({ user });
};
Only checkpoint 1
gets displayed in the terminal when I refresh the page:
This tells me that something fishy is happening in the getUser
function.
packages/api/src/lib/clerk.ts
export const getUser = async (req: RequestLike) => {
const { userId } = getAuth(req);
const user = userId ? await clerkClient.users.getUser(userId) : null;
console.log(user);
return user
? {
...user,
// Clerk's privateMetadata is a `Record<string, unknown>`, so we need to parse it
privateMetadata: user?.privateMetadata as CustomClerkMetadata,
}
: null;
};
When I console.log(user)
in the function, I get a sensible result with the currently logged in user. However, something clearly happens to the returned value, as checkpoint 2
is never reached.
Hey thanks for this info...
I will take a look and have an answer today or tomorrow.
I can confirm that the x-clerk-auth-status
is not included in the request being sent to your tRPC server. Am I supposed to set this myself, should it be set automatically, or is this a Clerk error?
I have narrowed the problem down to originating from packages/api/src/context.ts. This is how the context looked initially, setup by Clerk:
/**
* This is the actual context you'll use in your router
* @link https://trpc.io/docs/context
**/
export const createContext = async (options: CreateNextContextOptions) => {
async function getUser() {
const { userId } = getAuth(options.req);
const user = userId ? await clerkClient.users.getUser(userId) : null;
return user;
}
const user = await getUser();
return await createContextInner({ user });
};
Because I wanted to pass in some slightly different data, I changed the parameter in the following way:
// Pay attention to the line below
export const createContext = async (req: RequestLike) => {
async function getUser() {
// Pay attention to the line below
const { userId } = getAuth(req);
const user = userId ? await clerkClient.users.getUser(userId) : null;
return user;
}
const user = await getUser();
return await createContextInner({ user });
};
This caused the x-clerk-auth-status
to not be set in the request header.
@perkinsjr In summary, the issue was just me being stupid by tweaking the types in the tRPC context (from CreateNextContextOptions
to RequestLike
), thus causing the x-clerk-auth-status
. I think we can close this issue with that. Do you think it's necessary to add some docs or comments about this, or do we leave it with that? ☺️
Think I am going to close it in favor of not documenting this but will keep it in mind in case I need to document it in the future.
I have set up a pretty basic repository with Next.js, tRPC, and Clerk. Clerk generally works like a charm 🫶🏼
I have now set up some middleware to redirect the user if they're not an administrator of the service.
I now get an error that looks like the following:
Please see the screenshot for reference.
I am aware that I provide very little information here, and I apologize. I am hoping that we can have a discussion about the origin of this error could be and that this discussion could help me debug the issue further. 😁
EDIT: Note that this header error affects all procedures defined using tRPC.