blastoise/routes/auth.ts

180 lines
6.4 KiB
TypeScript

import {
createUser,
findUserByUsername,
validatePassword,
createSession,
createGuestSession,
deleteSession,
getUserPermissions,
getAllUsers,
grantPermission,
revokePermission,
} from "../db";
import {
getUser,
requirePermission,
setSessionCookie,
clearSessionCookie,
getClientInfo,
} from "../auth";
import { config } from "../config";
import { state } from "../state";
import { getOrCreateUser, userHasPermission } from "./helpers";
// Auth: signup
export async function handleSignup(req: Request, server: any): Promise<Response> {
try {
const { username, password } = await req.json();
if (!username || !password) {
return Response.json({ error: "Username and password required" }, { status: 400 });
}
if (username.length < 3 || password.length < 6) {
return Response.json({ error: "Username min 3 chars, password min 6 chars" }, { status: 400 });
}
const existing = findUserByUsername(username);
if (existing) {
return Response.json({ error: "Username already taken" }, { status: 400 });
}
const user = await createUser(username, password);
const userAgent = req.headers.get("user-agent") ?? undefined;
const ipAddress = req.headers.get("x-forwarded-for")?.split(",")[0]?.trim()
?? req.headers.get("x-real-ip")
?? server.requestIP(req)?.address
?? undefined;
const token = createSession(user.id, userAgent, ipAddress);
console.log(`[AUTH] Signup: user="${username}" id=${user.id} admin=${user.is_admin} session=${token} ip=${ipAddress} ua="${userAgent?.slice(0, 50)}..."`);
state.library.logActivity("account_created", { title: user.is_admin ? "admin" : "user" }, { id: user.id, username: user.username });
return Response.json(
{ user: { id: user.id, username: user.username, isAdmin: user.is_admin } },
{ headers: { "Set-Cookie": setSessionCookie(token) } }
);
} catch (e) {
return Response.json({ error: "Signup failed" }, { status: 500 });
}
}
// Auth: login
export async function handleLogin(req: Request, server: any): Promise<Response> {
try {
const { username, password } = await req.json();
const user = findUserByUsername(username);
if (!user || !(await validatePassword(user, password))) {
console.log(`[AUTH] Login failed: user="${username}"`);
return Response.json({ error: "Invalid username or password" }, { status: 401 });
}
const userAgent = req.headers.get("user-agent") ?? undefined;
const ipAddress = req.headers.get("x-forwarded-for")?.split(",")[0]?.trim()
?? req.headers.get("x-real-ip")
?? server.requestIP(req)?.address
?? undefined;
const token = createSession(user.id, userAgent, ipAddress);
console.log(`[AUTH] Login: user="${username}" id=${user.id} session=${token} ip=${ipAddress} ua="${userAgent?.slice(0, 50)}..."`);
return Response.json(
{ user: { id: user.id, username: user.username, isAdmin: user.is_admin } },
{ headers: { "Set-Cookie": setSessionCookie(token) } }
);
} catch (e) {
return Response.json({ error: "Login failed" }, { status: 500 });
}
}
// Auth: logout
export function handleLogout(req: Request): Response {
const token = req.headers.get("cookie")?.match(/blastoise_session=([^;]+)/)?.[1];
if (token) {
const { validateSession } = require("../db");
const user = validateSession(token);
console.log(`[AUTH] Logout: user="${user?.username ?? "unknown"}" session=${token}`);
deleteSession(token);
}
return Response.json(
{ success: true },
{ headers: { "Set-Cookie": clearSessionCookie() } }
);
}
// Auth: get current user
export function handleGetMe(req: Request, server: any): Response {
const { user, headers } = getOrCreateUser(req, server);
if (!user) {
return Response.json({ user: null });
}
const permissions = getUserPermissions(user.id);
const effectivePermissions = [...permissions];
if (config.defaultPermissions) {
for (const perm of config.defaultPermissions) {
if (user.is_guest && perm === "control") continue;
effectivePermissions.push({
id: 0,
user_id: user.id,
resource_type: "channel",
resource_id: null,
permission: perm,
});
}
}
return Response.json({
user: { id: user.id, username: user.username, isAdmin: user.is_admin, isGuest: user.is_guest },
permissions: effectivePermissions,
}, { headers });
}
// Kick all other clients for current user
export function handleKickOthers(req: Request, server: any): Response {
const { user } = getOrCreateUser(req, server);
if (!user) {
return Response.json({ error: "Not authenticated" }, { status: 401 });
}
const connections = state.userConnections.get(user.id);
if (!connections || connections.size === 0) {
return Response.json({ kicked: 0 });
}
let kickedCount = 0;
for (const ws of connections) {
ws.send(JSON.stringify({ type: "kick", reason: "Kicked by another session" }));
kickedCount++;
}
console.log(`[Kick] User ${user.username} kicked ${kickedCount} other clients`);
return Response.json({ kicked: kickedCount });
}
// Admin: list users
export function handleListUsers(req: Request, server: any): Response {
try {
requirePermission(req, "global", null, "admin", server);
return Response.json(getAllUsers());
} catch (e) {
if (e instanceof Response) return e;
return Response.json({ error: "Failed" }, { status: 500 });
}
}
// Admin: grant permission
export async function handleGrantPermission(req: Request, server: any, userId: number): Promise<Response> {
try {
requirePermission(req, "global", null, "admin", server);
const { resourceType, resourceId, permission } = await req.json();
grantPermission(userId, resourceType, resourceId, permission);
return Response.json({ success: true });
} catch (e) {
if (e instanceof Response) return e;
return Response.json({ error: "Failed" }, { status: 500 });
}
}
// Admin: revoke permission
export async function handleRevokePermission(req: Request, server: any, userId: number): Promise<Response> {
try {
requirePermission(req, "global", null, "admin", server);
const { resourceType, resourceId, permission } = await req.json();
revokePermission(userId, resourceType, resourceId, permission);
return Response.json({ success: true });
} catch (e) {
if (e instanceof Response) return e;
return Response.json({ error: "Failed" }, { status: 500 });
}
}