From d812abfeb171a309396669d209fcbb229166250b Mon Sep 17 00:00:00 2001 From: Derock Date: Tue, 14 Nov 2023 22:14:29 -0500 Subject: [PATCH] ref: move trpc off next.js --- package.json | 2 + pnpm-lock.yaml | 15 ++++++ src/app/api/trpc/[trpc]/route.ts | 15 ------ src/app/test/page.tsx | 9 ++++ src/server/api/routers/auth/index.ts | 11 ++++- src/server/api/routers/auth/sessions.ts | 1 - src/server/api/routers/setup.ts | 7 +-- src/server/api/trpc.ts | 61 ++++++++++++++++++------- src/server/auth/Session.ts | 15 +++--- src/server/server.ts | 26 +++++++++-- 10 files changed, 115 insertions(+), 47 deletions(-) delete mode 100644 src/app/api/trpc/[trpc]/route.ts create mode 100644 src/app/test/page.tsx diff --git a/package.json b/package.json index 97e4371..ec26a20 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "chalk": "^5.3.0", "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", + "cookie": "^0.6.0", "date-fns": "^2.30.0", "dotenv": "^16.3.1", "drizzle-orm": "^0.28.6", @@ -58,6 +59,7 @@ }, "devDependencies": { "@types/better-sqlite3": "^7.6.7", + "@types/cookie": "^0.5.4", "@types/eslint": "^8.44.7", "@types/node": "^20.9.0", "@types/node-os-utils": "^1.3.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9d1ded8..a106cc2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -59,6 +59,9 @@ dependencies: clsx: specifier: ^2.0.0 version: 2.0.0 + cookie: + specifier: ^0.6.0 + version: 0.6.0 date-fns: specifier: ^2.30.0 version: 2.30.0 @@ -130,6 +133,9 @@ devDependencies: '@types/better-sqlite3': specifier: ^7.6.7 version: 7.6.7 + '@types/cookie': + specifier: ^0.5.4 + version: 0.5.4 '@types/eslint': specifier: ^8.44.7 version: 8.44.7 @@ -1307,6 +1313,10 @@ packages: dependencies: '@types/node': 20.9.0 + /@types/cookie@0.5.4: + resolution: {integrity: sha512-7z/eR6O859gyWIAjuvBWFzNURmf2oPBmJlfVWkwehU5nzIyjwBsTh7WMmEEV4JFnHuQ3ex4oyTvfKzcyJVDBNA==} + dev: true + /@types/cross-spawn@6.0.3: resolution: {integrity: sha512-BDAkU7WHHRHnvBf5z89lcvACsvkz/n7Tv+HyD/uW76O29HoH1Tk/W6iQrepaZVbisvlEek4ygwT8IW7ow9XLAA==} dependencies: @@ -2373,6 +2383,11 @@ packages: resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} dev: false + /cookie@0.6.0: + resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} + engines: {node: '>= 0.6'} + dev: false + /copy-anything@3.0.5: resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==} engines: {node: '>=12.13'} diff --git a/src/app/api/trpc/[trpc]/route.ts b/src/app/api/trpc/[trpc]/route.ts deleted file mode 100644 index ff8fb29..0000000 --- a/src/app/api/trpc/[trpc]/route.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { fetchRequestHandler } from "@trpc/server/adapters/fetch"; -import { type NextRequest } from "next/server.js"; - -import { appRouter } from "~/server/api/root"; -import { createTRPCContext } from "~/server/api/trpc"; - -const handler = (req: NextRequest) => - fetchRequestHandler({ - endpoint: "/api/trpc", - req, - router: appRouter, - createContext: ({ resHeaders }) => createTRPCContext({ req, resHeaders }), - }); - -export { handler as GET, handler as POST }; diff --git a/src/app/test/page.tsx b/src/app/test/page.tsx new file mode 100644 index 0000000..08a2722 --- /dev/null +++ b/src/app/test/page.tsx @@ -0,0 +1,9 @@ +"use client"; + +import { api } from "~/trpc/react"; + +export default function TestPage() { + const { data } = api.system.currentStats.useQuery(); + + return
CPU Usage: {data?.cpu.usage}
; +} diff --git a/src/server/api/routers/auth/index.ts b/src/server/api/routers/auth/index.ts index fb7c497..9446a30 100644 --- a/src/server/api/routers/auth/index.ts +++ b/src/server/api/routers/auth/index.ts @@ -10,6 +10,7 @@ import { TRPCError } from "@trpc/server"; import argon2 from "argon2"; import { Session } from "~/server/auth/Session"; import { sessionsRouter } from "./sessions"; +import assert from "assert"; export const authRouter = createTRPCRouter({ sessions: sessionsRouter, @@ -31,6 +32,14 @@ export const authRouter = createTRPCRouter({ }), ) .mutation(async ({ ctx, input }) => { + assert( + ctx.response, + new TRPCError({ + code: "BAD_REQUEST", + message: "Cannot sign in over WebSocket.", + }), + ); + const [user] = await ctx.db .select({ password: users.password, id: users.id, mfa: users.mfaToken }) .from(users) @@ -67,7 +76,7 @@ export const authRouter = createTRPCRouter({ } const session = await Session.createForUser(user.id, ctx.request); - ctx.headers.set("Set-Cookie", session.getCookieString()); + ctx.response.setHeader("Set-Cookie", session.getCookieString()); return { success: true, diff --git a/src/server/api/routers/auth/sessions.ts b/src/server/api/routers/auth/sessions.ts index 59e7803..6effdb4 100644 --- a/src/server/api/routers/auth/sessions.ts +++ b/src/server/api/routers/auth/sessions.ts @@ -1,7 +1,6 @@ import { sessions } from "~/server/db/schema"; import { authenticatedProcedure, createTRPCRouter } from "../../trpc"; import { eq } from "drizzle-orm"; -import { Session } from "~/server/auth/Session"; export const sessionsRouter = createTRPCRouter({ list: authenticatedProcedure.query(async ({ ctx }) => { diff --git a/src/server/api/routers/setup.ts b/src/server/api/routers/setup.ts index 6e6dd0c..6b727a7 100644 --- a/src/server/api/routers/setup.ts +++ b/src/server/api/routers/setup.ts @@ -4,7 +4,6 @@ import { sql } from "drizzle-orm"; import { users } from "~/server/db/schema"; import { TRPCError } from "@trpc/server"; import assert from "assert"; -import { Session } from "~/server/auth/Session"; import { hash } from "argon2"; export const setupRouter = createTRPCRouter({ @@ -42,8 +41,10 @@ export const setupRouter = createTRPCRouter({ // log the user in assert(user, "User should be created"); - const session = await Session.createForUser(user.id, ctx.request); - ctx.request.cookies.set("session", session.data.token); + + // TODO: fix setup + // const session = await Session.createForUser(user.id, ctx.request); + // ctx.request.cookies.set("session", session.data.token); return { success: true, diff --git a/src/server/api/trpc.ts b/src/server/api/trpc.ts index 2c6c73a..b5aee44 100644 --- a/src/server/api/trpc.ts +++ b/src/server/api/trpc.ts @@ -13,6 +13,14 @@ import { ZodError } from "zod"; import { db } from "~/server/db"; import { Session } from "../auth/Session"; import ipaddr from "ipaddr.js"; +import { IncomingMessage, ServerResponse } from "http"; +import logger from "../utils/logger"; +import cookie from "cookie"; + +export type ExtendedRequest = IncomingMessage & { + cookies: Record; + ip: string; +}; /** * 1. CONTEXT @@ -22,8 +30,8 @@ import ipaddr from "ipaddr.js"; * These allow you to access things when processing a request, like the database, the session, etc. */ interface CreateContextOptions { - headers: Headers; - request: NextRequest; + request: ExtendedRequest; + response?: ServerResponse; session: Session | null; } @@ -39,10 +47,10 @@ interface CreateContextOptions { */ export const createInnerTRPCContext = (opts: CreateContextOptions) => { return { - headers: opts.headers, session: opts.session, - db, request: opts.request, + response: opts.response, + db, }; }; @@ -53,36 +61,55 @@ export const createInnerTRPCContext = (opts: CreateContextOptions) => { * @see https://trpc.io/docs/context */ export const createTRPCContext = async (opts: { - req: NextRequest; - resHeaders: Headers; + req: IncomingMessage; + res?: ServerResponse; }) => { // disable caching - opts.resHeaders.set("Cache-Control", "no-store"); + opts.res?.setHeader("Cache-Control", "no-store"); // resolve real IP - const forwardedFor = opts.req.headers.get("x-forwarded-for"); - let ip = forwardedFor?.split(",")[0]; + let forwardedFor = opts.req.headers["x-forwarded-for"]; + let ip = opts.req.socket.remoteAddress; - // validate IP - if (!ip || !ipaddr.isValid(ip)) { - ip = opts.req.ip; + if (forwardedFor) { + if (Array.isArray(forwardedFor)) { + logger.debug("Multiple forwarded-for headers found, using first"); + forwardedFor = forwardedFor[0]; + } + + ip = forwardedFor?.split(",")[0]; } + // now double check that the IP is valid + if (!ip || !ipaddr.isValid(ip)) { + logger.warn("Unable to resolve IP address from headers, using socket. ", { + forwardedFor, + ip, + }); + ip = opts.req.socket.remoteAddress; + } + + // set the IP + (opts.req as ExtendedRequest).ip = ip ?? ""; + // resolve session data - const sessionToken = opts.req.cookies.get("sessionToken")?.value; + const cookies = ((opts.req as ExtendedRequest).cookies = cookie.parse( + opts.req.headers.cookie ?? "", + )); + const sessionToken = cookies["sessionToken"]; // fetch session data from token const session: Session | null = sessionToken ? await Session.fetchFromTokenAndUpdate(sessionToken, { - ip: opts.req.ip, - ua: opts.req.headers.get("user-agent") ?? undefined, + ip, + ua: opts.req.headers["user-agent"] ?? undefined, }) : null; return createInnerTRPCContext({ - headers: opts.resHeaders, session, - request: opts.req, + request: opts.req as ExtendedRequest, + response: opts.res, }); }; diff --git a/src/server/auth/Session.ts b/src/server/auth/Session.ts index c2f51c3..e7a92f0 100644 --- a/src/server/auth/Session.ts +++ b/src/server/auth/Session.ts @@ -3,9 +3,10 @@ import { db } from "../db"; import { users, sessions } from "../db/schema"; import { randomBytes } from "crypto"; import assert from "assert"; -import { NextRequest, userAgent } from "next/server.js"; import { hash as argon2Hash } from "argon2"; import { env } from "~/env"; +import { IncomingMessage } from "http"; +import { ExtendedRequest } from "../api/trpc"; export type SessionUpdateData = Partial<{ ua: string; @@ -57,11 +58,11 @@ export class Session { */ static async fetchFromTokenAndUpdate( token: string, - context: SessionUpdateData | NextRequest, + context: SessionUpdateData | ExtendedRequest, ) { // parse context const parsedContext = - context instanceof NextRequest + context instanceof IncomingMessage ? Session.getContextFromRequest(context) : context; @@ -90,14 +91,14 @@ export class Session { */ static async createForUser( userId: string, - context: SessionUpdateData | NextRequest, + context: SessionUpdateData | ExtendedRequest, ) { // generate a session token const token = randomBytes(64).toString("hex"); // parse context const parsedContext = - context instanceof Request + context instanceof IncomingMessage ? Session.getContextFromRequest(context) : context; @@ -119,10 +120,10 @@ export class Session { /** * Utility function to extract context from a request. */ - static getContextFromRequest(request: NextRequest) { + static getContextFromRequest(request: ExtendedRequest) { return { - ua: userAgent(request).ua ?? undefined, ip: request.ip, + ua: request.headers["user-agent"], } satisfies SessionUpdateData; } diff --git a/src/server/server.ts b/src/server/server.ts index ce62eb8..ad98661 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -14,6 +14,7 @@ import { mkdir, stat } from "fs/promises"; import path from "path"; import { version } from "../../package.json"; import { stats } from "./modules/stats"; +import { nodeHTTPRequestHandler } from "@trpc/server/adapters/node-http"; // check if database folder exists try { @@ -54,6 +55,26 @@ const upgradeHandler = app.getUpgradeHandler(); // create the http server const server = createServer((req, res) => { + // routes starting with /api/trpc are handled by trpc + if (req.url?.startsWith("/api/trpc")) { + const path = new URL( + req.url.startsWith("/") ? `http://127.0.0.1${req.url}` : req.url, + ).pathname.replace("/api/trpc/", ""); + + return nodeHTTPRequestHandler({ + path, + req, + res, + router: appRouter, + createContext: ({ req }) => { + return createTRPCContext({ + req, + res, + }); + }, + }); + } + getHandler(req, res).catch((error) => { logger.error(error); res.statusCode = 500; @@ -66,10 +87,9 @@ const wss = new WebSocketServer({ noServer: true }); const trpcHandler = applyWSSHandler({ wss, router: appRouter, - createContext: ({ req }) => { + createContext: ({ req, res }) => { return createTRPCContext({ - req: incomingRequestToNextRequest(req), - resHeaders: new Headers(), + req, }); }, });