From e98eebbb47bb87a4061406adfeb0239c852f1e1e Mon Sep 17 00:00:00 2001 From: creations Date: Fri, 2 May 2025 20:28:23 -0400 Subject: [PATCH] add logout, login --- src/lib/jwt.ts | 24 +++++++++ src/routes/user/login.ts | 108 ++++++++++++++++++++++++++++++++++++++ src/routes/user/logout.ts | 55 +++++++++++++++++++ src/server.ts | 3 ++ types/bun.d.ts | 1 + 5 files changed, 191 insertions(+) diff --git a/src/lib/jwt.ts b/src/lib/jwt.ts index 09e22dc..1ca24dd 100644 --- a/src/lib/jwt.ts +++ b/src/lib/jwt.ts @@ -60,6 +60,28 @@ export async function invalidateSession(request: Request): Promise { await redis.del(keys[0]); } +export async function invalidateSessionById( + sessionId: string, +): Promise { + const keys = await redis.keys(`session:*:${sessionId}`); + if (!keys.length) return false; + await redis.del(keys[0]); + return true; +} + +export async function invalidateAllSessionsForUser( + userId: string, +): Promise { + const keys = await redis.keys(`session:${userId}:*`); + if (keys.length === 0) return 0; + + for (const key of keys) { + await redis.del(key); + } + + return keys.length; +} + // helpers function extractToken(request: Request): string | null { return request.headers.get("Cookie")?.match(/session=([^;]+)/)?.[1] || null; @@ -118,4 +140,6 @@ export const sessionManager = { verifySession, decodeSession, invalidateSession, + invalidateSessionById, + invalidateAllSessionsForUser, }; diff --git a/src/routes/user/login.ts b/src/routes/user/login.ts index e69de29..c630ec3 100644 --- a/src/routes/user/login.ts +++ b/src/routes/user/login.ts @@ -0,0 +1,108 @@ +import { cassandra } from "@lib/cassandra"; +import { jsonResponse } from "@lib/http"; +import { sessionManager } from "@lib/jwt"; +import { isValidEmail, isValidPassword } from "@lib/validators"; +import type { Client } from "cassandra-driver"; + +const routeDef: RouteDef = { + method: "POST", + accepts: "application/json", + returns: "application/json;charset=utf-8", + needsBody: "json", +}; + +async function handler( + request: ExtendedRequest, + requestBody: unknown, +): Promise { + const { email, password } = requestBody as { + email: string; + password: string; + }; + const { force } = request.query; + + if (request.session && force !== "true" && force !== "1") { + return jsonResponse(400, { + message: "Already logged in", + error: "You are already logged in", + }); + } + + const errors: string[] = []; + + if (!email || typeof email !== "string") { + errors.push("email is required"); + } else { + const result = isValidEmail(email); + if (!result.valid) errors.push(`email: ${result.error}`); + } + + if (!password || typeof password !== "string") { + errors.push("password is required"); + } else { + const result = isValidPassword(password); + if (!result.valid) errors.push(`password: ${result.error}`); + } + + if (errors.length > 0) { + return jsonResponse(400, { + message: "Validation failed", + error: errors.join("; "), + }); + } + + const client: Client = cassandra.getClient(); + const result = await client.execute( + "SELECT id, username, email, password FROM users WHERE email = ?", + [email], + { prepare: true }, + ); + + if (result.rowLength === 0) { + return jsonResponse(401, { + message: "Invalid credentials", + error: "Invalid email or password", + }); + } + + const user = result.first(); + const isPasswordValid = await Bun.password.verify(password, user.password); + + if (!isPasswordValid) { + return jsonResponse(401, { + message: "Invalid credentials", + error: "Invalid email or password", + }); + } + + const cookie = await sessionManager.createSession( + { + id: user.id, + username: user.username, + email: user.email, + is_verified: false, + }, + request.headers.get("User-Agent") || "unknown", + ); + + return Response.json( + { + status: "OK", + message: "Login successful", + data: { + id: user.id, + username: user.username, + email: user.email, + }, + }, + { + status: 200, + headers: { + "Content-Type": "application/json;charset=utf-8", + "Set-Cookie": cookie, + }, + }, + ); +} + +export { handler, routeDef }; diff --git a/src/routes/user/logout.ts b/src/routes/user/logout.ts index e69de29..cf968b7 100644 --- a/src/routes/user/logout.ts +++ b/src/routes/user/logout.ts @@ -0,0 +1,55 @@ +import { jsonResponse } from "@lib/http"; +import { sessionManager } from "@lib/jwt"; +import { redis } from "bun"; + +const routeDef: RouteDef = { + method: "DELETE", + accepts: "*/*", + returns: "application/json;charset=utf-8", +}; + +async function handler(request: ExtendedRequest): Promise { + const { id: sessionId, all } = request.query; + + if ((all === "true" || all === "1") && request.session) { + const keys = await redis.keys(`session:${request.session.id}:*`); + for (const key of keys) { + await redis.del(key); + } + + return new Response(null, { + status: 204, + headers: { + "Set-Cookie": + "session=; Max-Age=0; Path=/; HttpOnly; Secure; SameSite=Strict", + }, + }); + } + + if (sessionId && typeof sessionId === "string") { + const keys = await redis.keys(`session:*:${sessionId}`); + if (!keys.length) { + return jsonResponse(404, { + message: "Session not found", + error: "No session exists with the given ID", + }); + } + await redis.del(keys[0]); + + return jsonResponse(200, { + message: "Session invalidated", + }); + } + + await sessionManager.invalidateSession(request); + + return new Response(null, { + status: 204, + headers: { + "Set-Cookie": + "session=; Max-Age=0; Path=/; HttpOnly; Secure; SameSite=Strict", + }, + }); +} + +export { handler, routeDef }; diff --git a/src/server.ts b/src/server.ts index d4b6ba1..3bb20c7 100644 --- a/src/server.ts +++ b/src/server.ts @@ -10,6 +10,7 @@ import { } from "bun"; import { webSocketHandler } from "@/websocket"; +import { getSession } from "@lib/jwt"; class ServerHandler { private router: FileSystemRouter; @@ -174,6 +175,8 @@ class ServerHandler { extendedRequest.params = params; extendedRequest.query = query; + extendedRequest.session = (await getSession(request)) || null; + response = await routeModule.handler( extendedRequest, requestBody, diff --git a/types/bun.d.ts b/types/bun.d.ts index 018bf35..9bcfb97 100644 --- a/types/bun.d.ts +++ b/types/bun.d.ts @@ -10,5 +10,6 @@ declare global { startPerf: number; query: Query; params: Params; + session: UserSession | null; } }