119 lines
5.5 KiB
JavaScript
119 lines
5.5 KiB
JavaScript
/**
|
|
* @file Verarbeitet Benutzer- und Kontoeinstellungen sowie Export und Import des Arbeitsbereichs.
|
|
*/
|
|
import express from "express";
|
|
import {asyncHandler} from "../middleware/asyncHandler.js";
|
|
import bcrypt from "bcrypt";
|
|
import {requireLogin} from "../middleware/auth.js";
|
|
import {validateBody} from "../middleware/validate.js";
|
|
import {newPasswordError} from "../services/passwordPolicy.js";
|
|
|
|
const usernameValidator = (body) => {
|
|
const username = String(body?.username || "").trim();
|
|
const password = String(body?.password || "");
|
|
if (!username) return {error: "New username is required"};
|
|
if (username.length < 3) return {error: "Username must be at least 3 characters"};
|
|
if (!password) return {error: "Current password is required"};
|
|
return {value: {username, password}};
|
|
};
|
|
|
|
const passwordValidator = (body) => {
|
|
const currentPassword = String(body?.currentPassword || "");
|
|
const newPassword = String(body?.newPassword || "");
|
|
if (!currentPassword || !newPassword) return {error: "Current and new password are required"};
|
|
if (newPassword.length < 6) return {error: "New password must be at least 6 characters"};
|
|
return {value: {currentPassword, newPassword}};
|
|
};
|
|
|
|
const currentPasswordValidator = (body) => {
|
|
const currentPassword = String(body?.currentPassword || "");
|
|
if (!currentPassword) return {error: "Current password is required"};
|
|
return {value: {currentPassword}};
|
|
};
|
|
|
|
const destroySession = (req) => new Promise((resolve, reject) => req.session.destroy(error => error ? reject(error) : resolve()));
|
|
|
|
export function createSettingsRouter({repositories}) {
|
|
const router = express.Router();
|
|
router.use(requireLogin);
|
|
|
|
router.get("/settings/profile", (req, res) => {
|
|
const user = repositories.getUser(req.session.user);
|
|
if (!user) return res.status(404).json({error: "User not found"});
|
|
return res.json({username: user.username, role: user.role});
|
|
});
|
|
|
|
router.put("/settings/username", validateBody(usernameValidator), asyncHandler(async (req, res) => {
|
|
const currentUsername = req.session.user;
|
|
const {username: nextUsername, password} = req.validatedBody;
|
|
if (nextUsername === currentUsername) {
|
|
return res.status(400).json({error: "That is already your username"});
|
|
}
|
|
|
|
const currentUser = repositories.getUser(currentUsername);
|
|
if (!currentUser) return res.status(404).json({error: "User not found"});
|
|
const valid = await bcrypt.compare(password, currentUser.password);
|
|
if (!valid) return res.status(403).json({error: "Current password is incorrect"});
|
|
if (repositories.getUser(nextUsername)) {
|
|
return res.status(409).json({error: "Username already exists", code: "USERNAME_TAKEN"});
|
|
}
|
|
|
|
try {
|
|
repositories.renameUser(currentUsername, nextUsername);
|
|
} catch (error) {
|
|
if (["SQLITE_CONSTRAINT_PRIMARYKEY", "SQLITE_CONSTRAINT_UNIQUE"].includes(error.code)) {
|
|
return res.status(409).json({error: "Username already exists", code: "USERNAME_TAKEN"});
|
|
}
|
|
throw error;
|
|
}
|
|
req.session.user = nextUsername;
|
|
return res.json({username: nextUsername});
|
|
}));
|
|
|
|
router.put("/settings/password", validateBody(passwordValidator), asyncHandler(async (req, res) => {
|
|
const username = req.session.user;
|
|
const {currentPassword, newPassword} = req.validatedBody;
|
|
const passwordError = newPasswordError(newPassword);
|
|
if (passwordError) return res.status(400).json({error: passwordError, code: "PASSWORD_TOO_LONG"});
|
|
const user = repositories.getUser(username);
|
|
if (!user) return res.status(404).json({error: "User not found"});
|
|
const valid = await bcrypt.compare(currentPassword, user.password);
|
|
if (!valid) return res.status(403).json({error: "Current password is incorrect"});
|
|
const hashed = await bcrypt.hash(newPassword, 10);
|
|
repositories.updatePassword(hashed, username);
|
|
const updatedUser = repositories.incrementSessionVersion(username);
|
|
req.session.sessionVersion = Number(updatedUser.session_version || 0);
|
|
return res.json({message: "Password updated"});
|
|
}));
|
|
|
|
router.post("/settings/logout-all-sessions", (req, res, next) => {
|
|
repositories.incrementSessionVersion(req.session.user);
|
|
req.session.destroy(error => error ? next(error) : res.json({message: "All sessions logged out"}));
|
|
});
|
|
|
|
router.get("/settings/export", (req, res) => {
|
|
const user = repositories.getUser(req.session.user);
|
|
if (!user) return res.status(404).json({error: "User not found"});
|
|
return res.json({
|
|
account: {
|
|
username: user.username,
|
|
exportedAt: new Date().toISOString(),
|
|
workspace: repositories.exportWorkspace(req.session.user)
|
|
}
|
|
});
|
|
});
|
|
|
|
router.delete("/settings/account", validateBody(currentPasswordValidator), asyncHandler(async (req, res) => {
|
|
const username = req.session.user;
|
|
const user = repositories.getUser(username);
|
|
if (!user) return res.status(404).json({error: "User not found"});
|
|
const valid = await bcrypt.compare(req.validatedBody.currentPassword, user.password);
|
|
if (!valid) return res.status(403).json({error: "Current password is incorrect"});
|
|
repositories.deleteUser(username);
|
|
await destroySession(req);
|
|
return res.json({message: "Account deleted"});
|
|
}));
|
|
|
|
return router;
|
|
}
|