ref: move trpc off next.js

This commit is contained in:
Derock 2023-11-14 22:14:29 -05:00
parent 8ab2e2f630
commit d812abfeb1
No known key found for this signature in database
10 changed files with 115 additions and 47 deletions

View file

@ -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",

View file

@ -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'}

View file

@ -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 };

9
src/app/test/page.tsx Normal file
View file

@ -0,0 +1,9 @@
"use client";
import { api } from "~/trpc/react";
export default function TestPage() {
const { data } = api.system.currentStats.useQuery();
return <div>CPU Usage: {data?.cpu.usage}</div>;
}

View file

@ -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,

View file

@ -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 }) => {

View file

@ -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,

View file

@ -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<string, string>;
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,
});
};

View file

@ -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;
}

View file

@ -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,
});
},
});