Skip to content

Commit

Permalink
Propagate IP to trpc ctx
Browse files Browse the repository at this point in the history
  • Loading branch information
MohamedBassem committed Oct 19, 2024
1 parent 6fa199a commit af58635
Show file tree
Hide file tree
Showing 5 changed files with 40 additions and 20 deletions.
29 changes: 26 additions & 3 deletions apps/web/server/api/client.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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<Context> => {
export const createContext = async (
database?: typeof db,
ip?: string | null,
): Promise<Context> => {
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,
},
};
};

Expand Down
14 changes: 1 addition & 13 deletions apps/web/server/auth.ts
Original file line number Diff line number Diff line change
@@ -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, {
Expand Down Expand Up @@ -71,10 +70,6 @@ async function isAdmin(email: string): Promise<boolean> {
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...")
Expand All @@ -84,14 +79,7 @@ const providers: Provider[] = [
password: { label: "Password", type: "password" },
},
async authorize(credentials, req) {
const request = req as NextApiRequest;

if (!credentials) {
logAuthenticationError(
"<unknown>",
"Credentials missing",
getIp(request),
);
return null;
}

Expand All @@ -105,7 +93,7 @@ const providers: Provider[] = [
logAuthenticationError(
credentials?.email,
error.message,
getIp(request),
requestIp.getClientIp({ headers: req.headers }),
);
return null;
}
Expand Down
6 changes: 6 additions & 0 deletions packages/trpc/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions packages/trpc/routers/apiKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -91,23 +91,23 @@ export const apiKeysAppRouter = router({
user = await validatePassword(input.email, input.password);
} catch (e) {
const error = e as Error;
logAuthenticationError(input.email, error.message, "<unknown>");
logAuthenticationError(input.email, error.message, ctx.req.ip);
throw new TRPCError({ code: "UNAUTHORIZED" });
}
return await generateApiKey(input.keyName, user.id);
}),
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 {
success: true,
};
} catch (e) {
const error = e as Error;
logAuthenticationError("<unknown>", error.message, "<unknown>");
logAuthenticationError("<unknown>", error.message, ctx.req.ip);
throw e;
}
}),
Expand Down
3 changes: 3 additions & 0 deletions packages/trpc/testUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ export function getApiCaller(db: TestDB, userId?: string, email?: string) {
}
: null,
db,
req: {
ip: null,
},
});
}

Expand Down

0 comments on commit af58635

Please sign in to comment.