From af58635a9d77898fd7838c964b8aa7bfe0645ada Mon Sep 17 00:00:00 2001 From: MohamedBassem Date: Sat, 19 Oct 2024 19:50:10 +0000 Subject: [PATCH] Propagate IP to trpc ctx --- apps/web/server/api/client.ts | 29 ++++++++++++++++++++++++++--- apps/web/server/auth.ts | 14 +------------- packages/trpc/index.ts | 6 ++++++ packages/trpc/routers/apiKeys.ts | 8 ++++---- packages/trpc/testUtils.ts | 3 +++ 5 files changed, 40 insertions(+), 20 deletions(-) diff --git a/apps/web/server/api/client.ts b/apps/web/server/api/client.ts index 6a0a8909..fb2d84bc 100644 --- a/apps/web/server/api/client.ts +++ b/apps/web/server/api/client.ts @@ -1,4 +1,6 @@ +import { headers } from "next/headers"; import { getServerAuthSession } from "@/server/auth"; +import requestIp from "request-ip"; import { db } from "@hoarder/db"; import { Context, createCallerFactory } from "@hoarder/trpc"; @@ -8,25 +10,46 @@ import { appRouter } from "@hoarder/trpc/routers/_app"; export async function createContextFromRequest(req: Request) { // TODO: This is a hack until we offer a proper REST API instead of the trpc based one. // Check if the request has an Authorization token, if it does, assume that API key authentication is requested. + const ip = requestIp.getClientIp({ + headers: Object.fromEntries(req.headers.entries()), + }); const authorizationHeader = req.headers.get("Authorization"); if (authorizationHeader && authorizationHeader.startsWith("Bearer ")) { const token = authorizationHeader.split(" ")[1]; try { const user = await authenticateApiKey(token); - return { user, db }; + return { + user, + db, + req: { + ip, + }, + }; } catch (e) { // Fallthrough to cookie-based auth } } - return createContext(); + return createContext(db, ip); } -export const createContext = async (database?: typeof db): Promise => { +export const createContext = async ( + database?: typeof db, + ip?: string | null, +): Promise => { const session = await getServerAuthSession(); + if (ip === undefined) { + const hdrs = headers(); + ip = requestIp.getClientIp({ + headers: Object.fromEntries(hdrs.entries()), + }); + } return { user: session?.user ?? null, db: database ?? db, + req: { + ip, + }, }; }; diff --git a/apps/web/server/auth.ts b/apps/web/server/auth.ts index 0144e8b2..ee226743 100644 --- a/apps/web/server/auth.ts +++ b/apps/web/server/auth.ts @@ -1,5 +1,4 @@ import type { Adapter } from "next-auth/adapters"; -import { NextApiRequest } from "next"; import { DrizzleAdapter } from "@auth/drizzle-adapter"; import { and, count, eq } from "drizzle-orm"; import NextAuth, { @@ -71,10 +70,6 @@ async function isAdmin(email: string): Promise { return res?.role == "admin"; } -function getIp(req: NextApiRequest): string | null { - return requestIp.getClientIp(req); -} - const providers: Provider[] = [ CredentialsProvider({ // The name to display on the sign in form (e.g. "Sign in with...") @@ -84,14 +79,7 @@ const providers: Provider[] = [ password: { label: "Password", type: "password" }, }, async authorize(credentials, req) { - const request = req as NextApiRequest; - if (!credentials) { - logAuthenticationError( - "", - "Credentials missing", - getIp(request), - ); return null; } @@ -105,7 +93,7 @@ const providers: Provider[] = [ logAuthenticationError( credentials?.email, error.message, - getIp(request), + requestIp.getClientIp({ headers: req.headers }), ); return null; } diff --git a/packages/trpc/index.ts b/packages/trpc/index.ts index 5f351a8e..26d8ea96 100644 --- a/packages/trpc/index.ts +++ b/packages/trpc/index.ts @@ -15,11 +15,17 @@ interface User { export interface Context { user: User | null; db: typeof db; + req: { + ip: string | null; + }; } export interface AuthedContext { user: User; db: typeof db; + req: { + ip: string | null; + }; } // Avoid exporting the entire t-object diff --git a/packages/trpc/routers/apiKeys.ts b/packages/trpc/routers/apiKeys.ts index 3f032eb4..c55dc095 100644 --- a/packages/trpc/routers/apiKeys.ts +++ b/packages/trpc/routers/apiKeys.ts @@ -78,7 +78,7 @@ export const apiKeysAppRouter = router({ }), ) .output(zApiKeySchema) - .mutation(async ({ input }) => { + .mutation(async ({ input, ctx }) => { let user; // Special handling as otherwise the extension would show "username or password is wrong" if (serverConfig.auth.disablePasswordAuth) { @@ -91,7 +91,7 @@ export const apiKeysAppRouter = router({ user = await validatePassword(input.email, input.password); } catch (e) { const error = e as Error; - logAuthenticationError(input.email, error.message, ""); + logAuthenticationError(input.email, error.message, ctx.req.ip); throw new TRPCError({ code: "UNAUTHORIZED" }); } return await generateApiKey(input.keyName, user.id); @@ -99,7 +99,7 @@ export const apiKeysAppRouter = router({ validate: publicProcedure .input(z.object({ apiKey: z.string() })) .output(z.object({ success: z.boolean() })) - .mutation(async ({ input }) => { + .mutation(async ({ input, ctx }) => { try { await authenticateApiKey(input.apiKey); // Throws if the key is invalid return { @@ -107,7 +107,7 @@ export const apiKeysAppRouter = router({ }; } catch (e) { const error = e as Error; - logAuthenticationError("", error.message, ""); + logAuthenticationError("", error.message, ctx.req.ip); throw e; } }), diff --git a/packages/trpc/testUtils.ts b/packages/trpc/testUtils.ts index 04e6b0a3..23dcdb33 100644 --- a/packages/trpc/testUtils.ts +++ b/packages/trpc/testUtils.ts @@ -37,6 +37,9 @@ export function getApiCaller(db: TestDB, userId?: string, email?: string) { } : null, db, + req: { + ip: null, + }, }); }