Revamp workspace archive handling with async support, input validation improvements and better session management
CI / check (22) (push) Failing after 4m51s
CI / check (24) (push) Successful in 9m43s

This commit is contained in:
2026-10-13 18:10:56 +02:00
parent 5489e03138
commit 068843beda
16 changed files with 279 additions and 70 deletions
+16 -9
View File
@@ -6,13 +6,14 @@ 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 = String(body?.username || "").trim();
const username = normalizeUsername(body?.username);
const password = String(body?.password || "");
const inviteCode = String(body?.inviteCode || "").trim();
if (!username) return {error: "Username is required"};
if (username.length < 3) return {error: "Username must be at least 3 characters"};
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}};
@@ -20,6 +21,14 @@ const credentialsValidator = (body) => {
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();
@@ -30,7 +39,7 @@ export function createAuthRouter({repositories, authRateLimiters, config}) {
if (config.registrationInviteCode && inviteCode !== config.registrationInviteCode) {
return res.status(403).json({error: "Valid invite code is required"});
}
if (repositories.getUser(username)) {
if (repositories.getUserCaseInsensitive(username)) {
return res.status(409).json({error: "User already exists", code: "USERNAME_TAKEN"});
}
@@ -44,21 +53,19 @@ export function createAuthRouter({repositories, authRateLimiters, config}) {
}
throw error;
}
req.session.user = username;
req.session.sessionVersion = 0;
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.getUser(username);
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"});
}
req.session.user = username;
req.session.sessionVersion = Number(user.session_version || 0);
await regenerateSession(req, {user: user.username, sessionVersion: Number(user.session_version || 0)});
return res.json({message: "Login successful"});
}));