diff --git a/schema.prisma b/schema.prisma index a507bf7..52e3f86 100644 --- a/schema.prisma +++ b/schema.prisma @@ -20,12 +20,14 @@ model User { } model Session { - id BigInt @id @default(dbgenerated("(((unix_timestamp() * 1000) * pow(2,22)) + floor((rand() * pow(2,12))))")) @db.UnsignedBigInt - userId BigInt @map("user") @db.UnsignedBigInt - expiry_date DateTime? @default(dbgenerated("(now() + interval 30 day)")) @db.Timestamp(0) - user User @relation(fields: [userId], references: [id]) + id BigInt @id @default(dbgenerated("(((unix_timestamp() * 1000) * pow(2,22)) + floor((rand() * pow(2,12))))")) @db.UnsignedBigInt + userId BigInt @map("user") @db.UnsignedBigInt + sessionToken Bytes @db.Binary(64) + expiry_date DateTime? @default(dbgenerated("(now() + interval 30 day)")) @db.Timestamp(0) + user User @relation(fields: [userId], references: [id]) @@index([userId], map: "user_idx") + @@index([sessionToken]) @@map("sessions") } diff --git a/server/api/login.post.ts b/server/api/login.post.ts index 1b7f00d..bb7e022 100644 --- a/server/api/login.post.ts +++ b/server/api/login.post.ts @@ -4,7 +4,7 @@ import { defineEventHandler, getCookie, setCookie, readBody } from "h3"; import { database } from "../utils/database"; import { isString } from "../utils/isString"; import { cookieSettings } from "../utils/rootUtils"; -import Snowflake from "~/utils/snowflake"; +import SessionToken from "../utils/SessionToken"; import { createError } from "#imports"; @@ -40,14 +40,11 @@ export default defineEventHandler(async (e) => { if (account === null) throw createError({ statusCode: 400, message: "Invalid username or password." }); - const sessionId = new Snowflake(); + const session = new SessionToken(account.id); await database.session.create({ - data: { - id: sessionId.state, - userId: account.id, - }, + data: session.toPrisma(), }); - setCookie(e, "token", sessionId.toString(), cookieSettings); - return { message: "Login successful", token: sessionId.toString() }; + setCookie(e, "token", session.toString(), cookieSettings); + return { message: "Login successful", token: session.toString() }; }); diff --git a/server/middleware/auth.ts b/server/middleware/auth.ts index fb91a7d..e07e947 100644 --- a/server/middleware/auth.ts +++ b/server/middleware/auth.ts @@ -1,9 +1,11 @@ import { defineEventHandler, getCookie } from "h3"; -import { createError } from "#imports"; +import SessionToken from "../utils/SessionToken"; import { database } from "~/server/utils/database"; import getRequestingUser from "~/server/utils/getRequestingUser"; +import { createError } from "#imports"; + const endpointsWithoutAuth: string[] = [ "/dbtest", "/echo", @@ -37,7 +39,10 @@ export async function isAuthorised(token: string | undefined): Promise try { await database.session.findUniqueOrThrow({ where: { - id: BigInt(token), + ...SessionToken.fromString(token).toPrisma(), + expiry_date: { + gte: new Date(), + }, }, }); diff --git a/server/utils/SessionToken.ts b/server/utils/SessionToken.ts new file mode 100644 index 0000000..6a3129c --- /dev/null +++ b/server/utils/SessionToken.ts @@ -0,0 +1,49 @@ +import crypto from "node:crypto"; +import { type Session } from "@prisma/client"; + +import Snowflake from "~/utils/snowflake"; + +/** Represents a Session token, without expiry data. */ +export default class SessionToken { + userId: bigint; + sessionId: bigint; + sessionToken: Buffer; + + constructor(userId: bigint, sessionId?: bigint, sessionToken?: Buffer) { + this.userId = userId; + this.sessionId = sessionId ?? new Snowflake().state; + this.sessionToken = sessionToken ?? crypto.randomBytes(64); + } + + /** Creates SessionToken from a string. + * @param string The strinct to create from. + * @returns The SessionToken object. + */ + static fromString(string: string): SessionToken { + const parameters = string.split("."); + return new SessionToken( + Buffer.from(parameters[0], "base64").readBigUInt64LE(), + Buffer.from(parameters[1], "base64").readBigUInt64LE(), + Buffer.from(parameters[2], "base64"), + ); + } + + toString(): string { + const stringUserId = Buffer.copyBytesFrom(new BigUint64Array([this.userId])).toString("base64"); + const stringSessionId = Buffer.copyBytesFrom(new BigUint64Array([this.sessionId])).toString("base64"); + const stringSessionToken = this.sessionToken.toString("base64"); + return `${stringUserId}.${stringSessionId}.${stringSessionToken}`; + } + + /** Returns this SessionToken as Prisma object. + * For use in where parameter. + * @returns this as prisma object. + */ + toPrisma(): Omit { + return { + id: this.sessionId, + userId: this.userId, + sessionToken: this.sessionToken, + }; + } +} diff --git a/server/utils/getRequestingUser.ts b/server/utils/getRequestingUser.ts index 13ed3f9..60c4173 100644 --- a/server/utils/getRequestingUser.ts +++ b/server/utils/getRequestingUser.ts @@ -1,6 +1,7 @@ import { getCookie, H3Event } from "h3"; import { database } from "./database"; +import SessionToken from "./SessionToken"; import { createError } from "#imports"; @@ -9,7 +10,10 @@ export default async function getRequestingUser(e: H3Event) { if (!cookie) throw createError("User not found"); const { user } = await database.session.findUnique({ where: { - id: BigInt(cookie), + ...SessionToken.fromString(cookie).toPrisma(), + expiry_date: { + gte: new Date(), + }, }, select: { user: {