Files
KrisHD_AI/src/routes/auth.js
T
2026-10-13 18:10:56 +02:00

85 lines
3.8 KiB
JavaScript

/**
* @file Verarbeitet Registrierung, Anmeldung, Abmeldung und die Abfrage der aktuellen Sitzung.
*/
import express from "express";
import {asyncHandler} from "../middleware/asyncHandler.js";
import bcrypt from "bcrypt";
import {validateBody} from "../middleware/validate.js";
import {newPasswordError} from "../services/passwordPolicy.js";
import {normalizeUsername, usernameError} from "../services/usernamePolicy.js";
const credentialsValidator = (body) => {
const username = normalizeUsername(body?.username);
const password = String(body?.password || "");
const inviteCode = String(body?.inviteCode || "").trim();
const usernameValidationError = usernameError(username);
if (usernameValidationError) return {error: usernameValidationError};
if (!password) return {error: "Password is required"};
if (password.length < 6) return {error: "Password must be at least 6 characters"};
return {value: {username, password, inviteCode}};
};
const DUMMY_PASSWORD_HASH = "$2b$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy";
const regenerateSession = (req, fields) => new Promise((resolve, reject) => {
req.session.regenerate(error => {
if (error) return reject(error);
Object.assign(req.session, fields);
resolve();
});
});
export function createAuthRouter({repositories, authRateLimiters, config}) {
const router = express.Router();
router.post("/register", ...authRateLimiters, validateBody(credentialsValidator), asyncHandler(async (req, res) => {
const {username, password, inviteCode} = req.validatedBody;
const passwordError = newPasswordError(password);
if (passwordError) return res.status(400).json({error: passwordError, code: "PASSWORD_TOO_LONG"});
if (config.registrationInviteCode && inviteCode !== config.registrationInviteCode) {
return res.status(403).json({error: "Valid invite code is required"});
}
if (repositories.getUserCaseInsensitive(username)) {
return res.status(409).json({error: "User already exists", code: "USERNAME_TAKEN"});
}
const hashed = await bcrypt.hash(password, 10);
try {
repositories.insertUser(username, hashed);
} catch (error) {
// The precheck can race while bcrypt yields; the database is authoritative.
if (["SQLITE_CONSTRAINT_PRIMARYKEY", "SQLITE_CONSTRAINT_UNIQUE"].includes(error.code)) {
return res.status(409).json({error: "User already exists", code: "USERNAME_TAKEN"});
}
throw error;
}
await regenerateSession(req, {user: username, sessionVersion: 0});
return res.json({message: "Registration successful"});
}));
router.post("/login", ...authRateLimiters, validateBody(credentialsValidator), asyncHandler(async (req, res) => {
const {username, password} = req.validatedBody;
const user = repositories.getUserCaseInsensitive(username);
const valid = await bcrypt.compare(password, user?.password || DUMMY_PASSWORD_HASH);
if (!user || !valid) {
// Keep the message generic and still perform bcrypt work for unknown users.
return res.status(user ? 403 : 400).json({error: "Invalid username or password"});
}
await regenerateSession(req, {user: user.username, sessionVersion: Number(user.session_version || 0)});
return res.json({message: "Login successful"});
}));
router.post("/logout", (req, res, next) => {
req.session.destroy(error => error ? next(error) : res.json({message: "Logged out"}));
});
router.get("/session", (req, res) => {
return res.json({
user: req.session.user || null,
inviteOnlyRegistration: Boolean(config.registrationInviteCode)
});
});
return router;
}