From 8a892c1e170fbad54bc1ff44eeaa7c2c0731e178 Mon Sep 17 00:00:00 2001 From: Derock Date: Fri, 3 Nov 2023 08:54:06 -0400 Subject: [PATCH] wip: sessions --- src/app/layout.tsx | 1 - src/app/page.tsx | 23 +----------- src/app/setup/layout.tsx | 0 src/app/setup/page.tsx | 1 + src/server/api/routers/setup.ts | 49 ++++++++++++++++++++++++ src/server/api/trpc.ts | 3 ++ src/server/auth/Session.ts | 66 ++++++++++++++++++++++++++++++--- 7 files changed, 115 insertions(+), 28 deletions(-) create mode 100644 src/app/setup/layout.tsx create mode 100644 src/app/setup/page.tsx create mode 100644 src/server/api/routers/setup.ts diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 9c1907b..a1b7dfa 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -2,7 +2,6 @@ import "~/styles/globals.css"; import { Inter } from "next/font/google"; import { headers } from "next/headers"; - import { TRPCReactProvider } from "~/trpc/react"; const inter = Inter({ diff --git a/src/app/page.tsx b/src/app/page.tsx index 4a06b52..aa85b61 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,11 +1,6 @@ import Link from "next/link"; -import { CreatePost } from "~/app/_components/create-post"; -import { api } from "~/trpc/server"; - -export default async function Home() { - const hello = await api.post.hello.query({ text: "from tRPC" }); - +export default function Home() { return (
@@ -47,19 +42,3 @@ export default async function Home() {
); } - -async function CrudShowcase() { - const latestPost = await api.post.getLatest.query(); - - return ( -
- {latestPost ? ( -

Your most recent post: {latestPost.name}

- ) : ( -

You have no posts yet.

- )} - - -
- ); -} diff --git a/src/app/setup/layout.tsx b/src/app/setup/layout.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/app/setup/page.tsx b/src/app/setup/page.tsx new file mode 100644 index 0000000..d245888 --- /dev/null +++ b/src/app/setup/page.tsx @@ -0,0 +1 @@ +export default function SetupInstance() {} diff --git a/src/server/api/routers/setup.ts b/src/server/api/routers/setup.ts new file mode 100644 index 0000000..7bcd3e6 --- /dev/null +++ b/src/server/api/routers/setup.ts @@ -0,0 +1,49 @@ +import { z } from "zod"; +import { publicProcedure } from "../trpc"; +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"; + +export const setupProcedure = publicProcedure + .input( + z.object({ + username: z.string(), + password: z.string(), + }), + ) + .mutation(async ({ ctx, input }) => { + // check if user already exists + const [userCount] = await ctx.db + .select({ + count: sql`count(*)`, + }) + .from(users) + .limit(1); + + // if user already exists, throw error + if (userCount && userCount.count > 0) + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Instance already set up", + }); + + // otherwise, create user + const [user] = await ctx.db + .insert(users) + .values({ + username: input.username, + password: input.password, + }) + .returning({ id: users.id }); + + // 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); + + return { + success: true, + }; + }); diff --git a/src/server/api/trpc.ts b/src/server/api/trpc.ts index bff6b66..8a7aa56 100644 --- a/src/server/api/trpc.ts +++ b/src/server/api/trpc.ts @@ -23,6 +23,7 @@ import { Session } from "../auth/Session"; */ interface CreateContextOptions { headers: Headers; + request: NextRequest; session: Session | null; } @@ -41,6 +42,7 @@ export const createInnerTRPCContext = (opts: CreateContextOptions) => { headers: opts.headers, session: opts.session, db, + request: opts.request, }; }; @@ -64,6 +66,7 @@ export const createTRPCContext = async (opts: { req: NextRequest }) => { return createInnerTRPCContext({ headers: opts.req.headers, session, + request: opts.req, }); }; diff --git a/src/server/auth/Session.ts b/src/server/auth/Session.ts index ff98db2..bf59f04 100644 --- a/src/server/auth/Session.ts +++ b/src/server/auth/Session.ts @@ -1,6 +1,14 @@ import { eq } from "drizzle-orm"; import { db } from "../db"; import { users, sessions } from "../db/schema"; +import { randomBytes } from "crypto"; +import assert from "assert"; +import { NextRequest } from "next/server"; + +export type SessionUpdateData = Partial<{ + ua: string; + ip: string; +}>; export class Session { /** @@ -29,14 +37,21 @@ export class Session { */ static async fetchFromTokenAndUpdate( token: string, - context: { - ip?: string; - ua?: string; - }, + context: SessionUpdateData | NextRequest, ) { + // parse context + const parsedContext = + context instanceof NextRequest + ? Session.getContextFromRequest(context) + : context; + const [sessionData] = await db .update(sessions) - .set({ lastAccessed: new Date(), lastIP: context.ip, lastUA: context.ua }) + .set({ + lastAccessed: new Date(), + lastIP: parsedContext.ip, + lastUA: parsedContext.ua, + }) .where(eq(sessions.token, token)) .returning(); @@ -47,6 +62,47 @@ export class Session { return new Session(sessionData); } + /** + * Create a new session for a user. + */ + static async createForUser( + userId: string, + context: SessionUpdateData | NextRequest, + ) { + // generate a session token + const token = randomBytes(64).toString("hex"); + + // parse context + const parsedContext = + context instanceof NextRequest + ? Session.getContextFromRequest(context) + : context; + + // insert the session into the database + const [sessionData] = await db + .insert(sessions) + .values({ + lastUA: parsedContext.ua, + lastIP: parsedContext.ip, + token, + userId, + }) + .returning(); + + assert(sessionData, "Session should be created"); + return new Session(sessionData); + } + + /** + * Utility function to extract context from a request. + */ + static getContextFromRequest(request: NextRequest) { + return { + ua: request.headers.get("user-agent") ?? undefined, + ip: request.ip, + } satisfies SessionUpdateData; + } + /** * Create a new session instance from a user's session data. * @param sessionData The user's session data.