add logout, login
All checks were successful
Code quality checks / biome (push) Successful in 9s

This commit is contained in:
creations 2025-05-02 20:28:23 -04:00
parent 57fb8d8bb1
commit e98eebbb47
Signed by: creations
GPG key ID: 8F553AA4320FC711
5 changed files with 191 additions and 0 deletions

View file

@ -60,6 +60,28 @@ export async function invalidateSession(request: Request): Promise<void> {
await redis.del(keys[0]);
}
export async function invalidateSessionById(
sessionId: string,
): Promise<boolean> {
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<number> {
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,
};

View file

@ -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<Response> {
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 };

View file

@ -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<Response> {
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 };

View file

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

1
types/bun.d.ts vendored
View file

@ -10,5 +10,6 @@ declare global {
startPerf: number;
query: Query;
params: Params;
session: UserSession | null;
}
}