wip: sessions
This commit is contained in:
parent
5e63ffb18c
commit
8a892c1e17
|
@ -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({
|
||||||
|
|
|
@ -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
0
src/app/setup/layout.tsx
Normal file
1
src/app/setup/page.tsx
Normal file
1
src/app/setup/page.tsx
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export default function SetupInstance() {}
|
49
src/server/api/routers/setup.ts
Normal file
49
src/server/api/routers/setup.ts
Normal 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,
|
||||||
|
};
|
||||||
|
});
|
|
@ -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,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
Loading…
Reference in a new issue