wip: sessions

This commit is contained in:
Derock 2023-11-03 08:54:06 -04:00
parent 5e63ffb18c
commit 8a892c1e17
No known key found for this signature in database
7 changed files with 115 additions and 28 deletions

View file

@ -2,7 +2,6 @@ import "~/styles/globals.css";
import { Inter } from "next/font/google"; import { Inter } from "next/font/google";
import { headers } from "next/headers"; import { headers } from "next/headers";
import { TRPCReactProvider } from "~/trpc/react"; import { TRPCReactProvider } from "~/trpc/react";
const inter = Inter({ const inter = Inter({

View file

@ -1,11 +1,6 @@
import Link from "next/link"; import Link from "next/link";
import { CreatePost } from "~/app/_components/create-post"; export default function Home() {
import { api } from "~/trpc/server";
export default async function Home() {
const hello = await api.post.hello.query({ text: "from tRPC" });
return ( return (
<main className="flex min-h-screen flex-col items-center justify-center bg-gradient-to-b from-[#2e026d] to-[#15162c] text-white"> <main className="flex min-h-screen flex-col items-center justify-center bg-gradient-to-b from-[#2e026d] to-[#15162c] text-white">
<div className="container flex flex-col items-center justify-center gap-12 px-4 py-16 "> <div className="container flex flex-col items-center justify-center gap-12 px-4 py-16 ">
@ -47,19 +42,3 @@ export default async function Home() {
</main> </main>
); );
} }
async function CrudShowcase() {
const latestPost = await api.post.getLatest.query();
return (
<div className="w-full max-w-xs">
{latestPost ? (
<p className="truncate">Your most recent post: {latestPost.name}</p>
) : (
<p>You have no posts yet.</p>
)}
<CreatePost />
</div>
);
}

0
src/app/setup/layout.tsx Normal file
View file

1
src/app/setup/page.tsx Normal file
View file

@ -0,0 +1 @@
export default function SetupInstance() {}

View file

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

View file

@ -23,6 +23,7 @@ import { Session } from "../auth/Session";
*/ */
interface CreateContextOptions { interface CreateContextOptions {
headers: Headers; headers: Headers;
request: NextRequest;
session: Session | null; session: Session | null;
} }
@ -41,6 +42,7 @@ export const createInnerTRPCContext = (opts: CreateContextOptions) => {
headers: opts.headers, headers: opts.headers,
session: opts.session, session: opts.session,
db, db,
request: opts.request,
}; };
}; };
@ -64,6 +66,7 @@ export const createTRPCContext = async (opts: { req: NextRequest }) => {
return createInnerTRPCContext({ return createInnerTRPCContext({
headers: opts.req.headers, headers: opts.req.headers,
session, session,
request: opts.req,
}); });
}; };

View file

@ -1,6 +1,14 @@
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { db } from "../db"; import { db } from "../db";
import { users, sessions } from "../db/schema"; 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 { export class Session {
/** /**
@ -29,14 +37,21 @@ export class Session {
*/ */
static async fetchFromTokenAndUpdate( static async fetchFromTokenAndUpdate(
token: string, token: string,
context: { context: SessionUpdateData | NextRequest,
ip?: string;
ua?: string;
},
) { ) {
// parse context
const parsedContext =
context instanceof NextRequest
? Session.getContextFromRequest(context)
: context;
const [sessionData] = await db const [sessionData] = await db
.update(sessions) .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)) .where(eq(sessions.token, token))
.returning(); .returning();
@ -47,6 +62,47 @@ export class Session {
return new Session(sessionData); 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. * Create a new session instance from a user's session data.
* @param sessionData The user's session data. * @param sessionData The user's session data.