ref: move trpc off next.js
This commit is contained in:
parent
8ab2e2f630
commit
d812abfeb1
|
@ -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",
|
||||
|
|
|
@ -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'}
|
||||
|
|
|
@ -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
9
src/app/test/page.tsx
Normal 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>;
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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 }) => {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue