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 { headers } from "next/headers";
import { TRPCReactProvider } from "~/trpc/react";
const inter = Inter({

View file

@ -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 (
<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 ">
@ -47,19 +42,3 @@ export default async function Home() {
</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 {
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,
});
};

View file

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