This commit is contained in:
parent
57fb8d8bb1
commit
e98eebbb47
5 changed files with 191 additions and 0 deletions
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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 };
|
|
@ -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 };
|
|
@ -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
1
types/bun.d.ts
vendored
|
@ -10,5 +10,6 @@ declare global {
|
|||
startPerf: number;
|
||||
query: Query;
|
||||
params: Params;
|
||||
session: UserSession | null;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue