diff --git a/src/lib/errors.ts b/src/lib/errors.ts index a80b6e2..fe71e74 100644 --- a/src/lib/errors.ts +++ b/src/lib/errors.ts @@ -23,3 +23,12 @@ export const enum RegisterResponseCause { PasswordLength, DisplayNameLength } + +export const enum LoginResponseCause { + Server = 1, + MalformedRequest, + EmailLength, + PasswordLength, + NotFound, + Timeout +} diff --git a/src/lib/server/usermgmt.ts b/src/lib/server/usermgmt.ts index b885797..40eb37b 100644 --- a/src/lib/server/usermgmt.ts +++ b/src/lib/server/usermgmt.ts @@ -1,14 +1,80 @@ import Bun from "bun" +import Crypto from "node:crypto" +import { Prisma } from "@prisma/client" import { ArgumentError, DuplicateError } from "$lib/errors" import Log from "$lib/server/log" import db from "$lib/server/database" -import { Prisma } from "@prisma/client" + +const SESSION_LIFETIME = 15 * 60 * 1000 + +export type User = Prisma.UserGetPayload + +export interface SessionData { + user: User + token: string + issued: Date + expires: Date +} + + +interface CacheUserInfo { + user: User + last_update: Date +} + +interface CacheSessionInfo { + user_id: number + issued: Date + expires: Date +} + +class Cache { + private _id_map: Map = new Map() + private _session_map: Map = new Map() + + add(user: User, token: string, issued: Date, expires: Date): SessionData { + this._id_map.set(user.id, { + user: user, + last_update: new Date() + }) + this._session_map.set(token, { + user_id: user.id, + issued: issued, + expires: expires + }) + + return { + user: user, + token: token, + issued: issued, + expires: expires + } + } + + get_session(token: string): SessionData|null { + const session_info = this._session_map.get(token) + if (!session_info) return null + + const user_info = this._id_map.get(session_info.user_id) + if (!user_info) return null + + return { + user: user_info.user, + token: token, + issued: session_info.issued, + expires: session_info.expires + } + } + +} + class UserMgmt { + _cache: Cache = new Cache() - async register(email: string, password: string, display_name: string) { + async register(email: string, password: string, display_name: string): Promise { if (email.length == 0 || password.length == 0 || display_name.length == 0) { throw new ArgumentError("No field may be empty") } @@ -23,7 +89,6 @@ class UserMgmt { }) Log.info(Log.module.USER, `Created user with id ${user.id}`) - console.log(user) return user; } catch (e) { @@ -37,16 +102,32 @@ class UserMgmt { } } - async login(email: string, password: string) { + async login(email: string, password: string): Promise { const user = await db.user.findUnique({ where: { email: email } }) - - } + if (!user) { + // throw of timing attacks + await Bun.password.verify("a", "$argon2id$v=19$m=16,t=2,p=1$ZHB6Zjd4NXV6RXZBZk9wRg$QaYjeAGLon+x3c5I1KB7UQ") + return null + } + if (!await Bun.password.verify(password, user.password_hash)) { + return null + } + + const token = Crypto.randomBytes(32).toBase64() + const session_info = this._cache.add(user, token, new Date(), new Date(Date.now() + SESSION_LIFETIME)) + + return session_info + } + + async session_login(token: string): Promise { + return this._cache.get_session(token) + } } const _manager = new UserMgmt() diff --git a/src/routes/api/login/+server.ts b/src/routes/api/login/+server.ts new file mode 100644 index 0000000..f61adb3 --- /dev/null +++ b/src/routes/api/login/+server.ts @@ -0,0 +1,33 @@ +import type { RequestHandler } from "./$types" + +import { error, json, text } from "@sveltejs/kit" + +import UserMgmt from "$lib/server/usermgmt" + +import { LoginResponseCause } from "$lib/errors" + +export const POST: RequestHandler = async ({ request, cookies }) => { + + const data = await request.formData() + + const email = data.get("email") + const password = data.get("password") + + if (data.keys.length > 2 || !(typeof email === "string" && typeof password === "string")) { + return error(400, { cause: LoginResponseCause.MalformedRequest, message: "Invalid request" }) + } + + if (email.length == 0) { + return error(400, { cause: LoginResponseCause.EmailLength, message: "Email must be provided" }) + } + if (password.length == 0) { + return error(400, { cause: LoginResponseCause.PasswordLength, message: "Password must be provided" }) + } + + const session = await UserMgmt.login(email, password) + if (!session) { + return error(401, { message: "Invalid username or password" }) + } + + return json({ token: session.token, expires: session.expires }) +}