Fix admin authorization and harden chat requests
This commit is contained in:
+12
-1
@@ -2,6 +2,7 @@
|
||||
* @file Verbindet Express, Sitzungen, Datenbank und Routen und verwaltet den Server-Lebenszyklus.
|
||||
*/
|
||||
import fs from "fs";
|
||||
import {createRequire} from "node:module";
|
||||
import {randomUUID} from "crypto";
|
||||
import {performance} from "perf_hooks";
|
||||
import express from "express";
|
||||
@@ -225,7 +226,11 @@ function enforceSessionVersion(repositories) {
|
||||
const user = repositories.getUser(req.session.user);
|
||||
const currentVersion = Number(user?.session_version || 0);
|
||||
const sessionVersion = Number(req.session.sessionVersion || 0);
|
||||
if (user && currentVersion === sessionVersion) return next();
|
||||
if (user && currentVersion === sessionVersion) {
|
||||
// Read the role on every request so revocation also affects existing sessions.
|
||||
req.account = user;
|
||||
return next();
|
||||
}
|
||||
|
||||
req.session.destroy(() => {
|
||||
if (req.path === "/session") {
|
||||
@@ -296,6 +301,12 @@ export function createApp(options = {}) {
|
||||
app.locals.close = createCloseHandler(closeHandlers);
|
||||
|
||||
app.use(createPagesRouter(config));
|
||||
const require = createRequire(import.meta.url);
|
||||
const dompurifyPath = require.resolve("dompurify").replace(/purify\.cjs\.js$/, "purify.min.js");
|
||||
app.get("/vendor/dompurify.js", (_req, res) => {
|
||||
res.set("Cache-Control", "no-cache");
|
||||
res.sendFile(dompurifyPath);
|
||||
});
|
||||
app.use(express.static(config.publicDir, {
|
||||
maxAge: config.staticMaxAge,
|
||||
etag: config.staticEtag,
|
||||
|
||||
@@ -46,7 +46,7 @@ function normalizeMarketPersonaType(personaType) {
|
||||
export function createRepositories(db, config) {
|
||||
const statements = {
|
||||
insertUser: db.prepare("INSERT INTO users (username, password) VALUES (?, ?)"),
|
||||
getUser: db.prepare("SELECT username, password, session_version FROM users WHERE username = ?"),
|
||||
getUser: db.prepare("SELECT username, password, role, session_version FROM users WHERE username = ?"),
|
||||
updateUsername: db.prepare("UPDATE users SET username = ? WHERE username = ?"),
|
||||
updatePassword: db.prepare("UPDATE users SET password = ? WHERE username = ?"),
|
||||
incrementSessionVersion: db.prepare("UPDATE users SET session_version = COALESCE(session_version, 0) + 1 WHERE username = ?"),
|
||||
|
||||
@@ -10,6 +10,7 @@ export function initializeSchema(db) {
|
||||
(
|
||||
username TEXT PRIMARY KEY,
|
||||
password TEXT NOT NULL,
|
||||
role TEXT NOT NULL DEFAULT 'user' CHECK (role IN ('user', 'admin')),
|
||||
session_version INTEGER DEFAULT 0
|
||||
)
|
||||
`).run();
|
||||
@@ -256,6 +257,7 @@ export function runMigrations(db, {dropLegacyChats = true} = {}) {
|
||||
};
|
||||
|
||||
const userColumns = db.prepare("PRAGMA table_info(users)").all();
|
||||
ensureColumn("users", userColumns, "role", "ALTER TABLE users ADD COLUMN role TEXT NOT NULL DEFAULT 'user' CHECK (role IN ('user', 'admin'))");
|
||||
ensureColumn("users", userColumns, "session_version", "ALTER TABLE users ADD COLUMN session_version INTEGER DEFAULT 0");
|
||||
db.prepare("UPDATE users SET session_version = 0 WHERE session_version IS NULL").run();
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
/** Forward rejected route promises to Express 4's error middleware. */
|
||||
export const asyncHandler = (handler) => (req, res, next) =>
|
||||
Promise.resolve().then(() => handler(req, res, next)).catch(next);
|
||||
@@ -7,3 +7,10 @@ export function requireLogin(req, res, next) {
|
||||
}
|
||||
return next();
|
||||
}
|
||||
|
||||
export function requireAdmin(req, res, next) {
|
||||
if (req.account?.role !== "admin") {
|
||||
return res.status(403).json({error: "Admin only"});
|
||||
}
|
||||
return next();
|
||||
}
|
||||
|
||||
+17
-8
@@ -2,6 +2,7 @@
|
||||
* @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";
|
||||
|
||||
@@ -19,23 +20,31 @@ const credentialsValidator = (body) => {
|
||||
export function createAuthRouter({repositories, authRateLimiters, config}) {
|
||||
const router = express.Router();
|
||||
|
||||
router.post("/register", ...authRateLimiters, validateBody(credentialsValidator), async (req, res) => {
|
||||
router.post("/register", ...authRateLimiters, validateBody(credentialsValidator), asyncHandler(async (req, res) => {
|
||||
const {username, password, inviteCode} = req.validatedBody;
|
||||
if (config.registrationInviteCode && inviteCode !== config.registrationInviteCode) {
|
||||
return res.status(403).json({error: "Valid invite code is required"});
|
||||
}
|
||||
if (repositories.getUser(username)) {
|
||||
return res.status(400).json({error: "User already exists"});
|
||||
return res.status(409).json({error: "User already exists", code: "USERNAME_TAKEN"});
|
||||
}
|
||||
|
||||
const hashed = await bcrypt.hash(password, 10);
|
||||
repositories.insertUser(username, hashed);
|
||||
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;
|
||||
}
|
||||
req.session.user = username;
|
||||
req.session.sessionVersion = 0;
|
||||
return res.json({message: "Registration successful"});
|
||||
});
|
||||
}));
|
||||
|
||||
router.post("/login", ...authRateLimiters, validateBody(credentialsValidator), async (req, res) => {
|
||||
router.post("/login", ...authRateLimiters, validateBody(credentialsValidator), asyncHandler(async (req, res) => {
|
||||
const {username, password} = req.validatedBody;
|
||||
const user = repositories.getUser(username);
|
||||
if (!user) return res.status(400).json({error: "User not found"});
|
||||
@@ -44,10 +53,10 @@ export function createAuthRouter({repositories, authRateLimiters, config}) {
|
||||
req.session.user = username;
|
||||
req.session.sessionVersion = Number(user.session_version || 0);
|
||||
return res.json({message: "Login successful"});
|
||||
});
|
||||
}));
|
||||
|
||||
router.post("/logout", (req, res) => {
|
||||
req.session.destroy(() => res.json({message: "Logged out"}));
|
||||
router.post("/logout", (req, res, next) => {
|
||||
req.session.destroy(error => error ? next(error) : res.json({message: "Logged out"}));
|
||||
});
|
||||
|
||||
router.get("/session", (req, res) => {
|
||||
|
||||
+24
-24
@@ -2,6 +2,8 @@
|
||||
* @file Stellt Chat-Routen für Nachrichten, Streaming, Antwortvarianten, Verzweigungen und Einstellungen bereit.
|
||||
*/
|
||||
import express from "express";
|
||||
import {createTextStreamEvents} from "../services/streamEvents.js";
|
||||
import {asyncHandler} from "../middleware/asyncHandler.js";
|
||||
import multer from "multer";
|
||||
import {requireLogin} from "../middleware/auth.js";
|
||||
import {validateBody} from "../middleware/validate.js";
|
||||
@@ -139,14 +141,14 @@ export function createChatsRouter({repositories, chatService, modelService, conf
|
||||
next();
|
||||
});
|
||||
|
||||
router.get("/models", async (req, res) => {
|
||||
router.get("/models", asyncHandler(async (req, res) => {
|
||||
try {
|
||||
return res.json(await modelService.listModels());
|
||||
} catch (error) {
|
||||
const mapped = modelService.mapError(error);
|
||||
return res.status(mapped.status).json(mapped.body);
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
router.get("/chats", (req, res) => res.json({chats: repositories.listChats(req.session.user)}));
|
||||
router.get("/chats/search", (req, res) => res.json({chats: repositories.searchChats(req.session.user, req.query.q || "")}));
|
||||
@@ -262,7 +264,7 @@ export function createChatsRouter({repositories, chatService, modelService, conf
|
||||
return res.json(result);
|
||||
});
|
||||
|
||||
router.post("/chats/:id/messages/:messageId/edit-resend", validateBody(sendMessageValidator), async (req, res) => {
|
||||
router.post("/chats/:id/messages/:messageId/edit-resend", validateBody(sendMessageValidator), asyncHandler(async (req, res) => {
|
||||
if (req.body?.confirmed !== true) return res.status(400).json({error: "Confirmation is required before creating an edited branch"});
|
||||
const chatId = Number(req.params.id);
|
||||
const targetMessage = repositories.getChatMessage(chatId, Number(req.params.messageId));
|
||||
@@ -285,7 +287,7 @@ export function createChatsRouter({repositories, chatService, modelService, conf
|
||||
const mapped = modelService.mapError(error);
|
||||
return res.status(mapped.status).json(mapped.body);
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
router.put("/chats/:id/messages/:messageId", validateBody(messageValidator), (req, res) => {
|
||||
const chatId = Number(req.params.id);
|
||||
@@ -446,7 +448,7 @@ export function createChatsRouter({repositories, chatService, modelService, conf
|
||||
return res.json({state});
|
||||
});
|
||||
|
||||
router.post("/chat", ...chatRateLimiters, validateBody(sendMessageValidator), async (req, res) => {
|
||||
router.post("/chat", ...chatRateLimiters, validateBody(sendMessageValidator), asyncHandler(async (req, res) => {
|
||||
try {
|
||||
const result = await chatService.sendChatMessage({
|
||||
user: req.session.user,
|
||||
@@ -464,9 +466,9 @@ export function createChatsRouter({repositories, chatService, modelService, conf
|
||||
const mapped = modelService.mapError(error);
|
||||
return res.status(mapped.status).json(mapped.body);
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
router.post("/chat/stream", ...chatRateLimiters, validateBody(sendMessageValidator), async (req, res) => {
|
||||
router.post("/chat/stream", ...chatRateLimiters, validateBody(sendMessageValidator), asyncHandler(async (req, res) => {
|
||||
const generationController = new AbortController();
|
||||
let generationFinished = false;
|
||||
res.once("close", () => {
|
||||
@@ -493,26 +495,25 @@ export function createChatsRouter({repositories, chatService, modelService, conf
|
||||
message: req.validatedBody.message,
|
||||
attachments: req.validatedBody.attachments,
|
||||
model: req.validatedBody.model,
|
||||
onChunk: (chunk, fullReply) => sendEvent("chunk", {chunk, fullReply}),
|
||||
onThinking: (chunk, fullThinking) => sendEvent("thinking", {chunk, fullThinking}),
|
||||
...createTextStreamEvents(sendEvent),
|
||||
webSearch: req.validatedBody.webSearch,
|
||||
reasoning: req.validatedBody.reasoning,
|
||||
onStatus: (status) => sendEvent("status", status),
|
||||
signal: generationController.signal
|
||||
});
|
||||
if (result.error) {
|
||||
sendEvent("error", {error: result.error, ...(result.searchState ? {searchState: result.searchState} : {})});
|
||||
sendEvent("error", {error: result.error, status: result.status, requestId: req.id, ...(result.searchState ? {searchState: result.searchState} : {})});
|
||||
} else {
|
||||
sendEvent("done", {reply: result.reply, searchState: result.searchState, ...(result.sources ? {sources: result.sources} : {})});
|
||||
}
|
||||
} catch (error) {
|
||||
const mapped = modelService.mapError(error);
|
||||
sendEvent("error", mapped.body);
|
||||
sendEvent("error", {...mapped.body, status: mapped.status, requestId: req.id});
|
||||
} finally {
|
||||
generationFinished = true;
|
||||
res.end();
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
router.post("/chats/:id/messages/:messageId/versions/:index", (req, res) => {
|
||||
const chatId = Number(req.params.id), messageId = Number(req.params.messageId);
|
||||
@@ -528,7 +529,7 @@ export function createChatsRouter({repositories, chatService, modelService, conf
|
||||
res.json({message: chatService.formatChatMessage(repositories.getChatMessage(chatId, messageId))});
|
||||
});
|
||||
|
||||
router.post(["/chats/:id/messages/:messageId/retry", "/chats/:id/messages/:messageId/retry/stream"], ...chatRateLimiters, async (req, res) => {
|
||||
router.post(["/chats/:id/messages/:messageId/retry", "/chats/:id/messages/:messageId/retry/stream"], ...chatRateLimiters, asyncHandler(async (req, res) => {
|
||||
const chatId = Number(req.params.id);
|
||||
const messageId = Number(req.params.messageId);
|
||||
const chat = repositories.getChat(chatId, req.session.user);
|
||||
@@ -559,28 +560,27 @@ export function createChatsRouter({repositories, chatService, modelService, conf
|
||||
reasoning: req.body?.reasoning === true,
|
||||
signal: controller.signal,
|
||||
...(streaming ? {
|
||||
onChunk: (chunk, fullReply) => sendEvent("chunk", {chunk, fullReply}),
|
||||
onThinking: (chunk, fullThinking) => sendEvent("thinking", {chunk, fullThinking}),
|
||||
...createTextStreamEvents(sendEvent),
|
||||
onStatus: status => sendEvent("status", status)
|
||||
} : {})
|
||||
});
|
||||
if (streaming) {
|
||||
sendEvent(result.error ? "error" : "done", result.error ? {error: result.error, searchState: result.searchState} : {...result, reply: result.message.content});
|
||||
sendEvent(result.error ? "error" : "done", result.error ? {error: result.error, status: result.status, requestId: req.id, searchState: result.searchState} : {...result, reply: result.message.content});
|
||||
return;
|
||||
}
|
||||
if (result.error) return res.status(result.status).json({error: result.error});
|
||||
return res.json(result);
|
||||
} catch (error) {
|
||||
const mapped = modelService.mapError(error);
|
||||
if (streaming) { sendEvent("error", mapped.body); return; }
|
||||
if (streaming) { sendEvent("error", {...mapped.body, status: mapped.status, requestId: req.id}); return; }
|
||||
return res.status(mapped.status).json(mapped.body);
|
||||
} finally {
|
||||
finished = true;
|
||||
if (streaming) res.end();
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
router.post("/chats/:id/messages/by-index/:index/retry", ...chatRateLimiters, async (req, res) => {
|
||||
router.post("/chats/:id/messages/by-index/:index/retry", ...chatRateLimiters, asyncHandler(async (req, res) => {
|
||||
const chatId = Number(req.params.id);
|
||||
const chat = repositories.getChat(chatId, req.session.user);
|
||||
if (!chat) return res.status(404).json({error: "Chat not found"});
|
||||
@@ -605,7 +605,7 @@ export function createChatsRouter({repositories, chatService, modelService, conf
|
||||
const mapped = modelService.mapError(error);
|
||||
return res.status(mapped.status).json(mapped.body);
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
router.get("/exports/chats/:id", (req, res) => {
|
||||
const chatId = Number(req.params.id);
|
||||
@@ -627,7 +627,7 @@ export function createChatsRouter({repositories, chatService, modelService, conf
|
||||
});
|
||||
|
||||
router.get("/exports/workspace", (req, res) => res.json({workspace: repositories.exportWorkspace(req.session.user)}));
|
||||
router.get("/exports/workspace/archive", async (req, res) => {
|
||||
router.get("/exports/workspace/archive", asyncHandler(async (req, res) => {
|
||||
try {
|
||||
const workspace = repositories.exportWorkspace(req.session.user);
|
||||
const images = new Map();
|
||||
@@ -642,7 +642,7 @@ export function createChatsRouter({repositories, chatService, modelService, conf
|
||||
} catch (error) {
|
||||
return res.status(error.message.includes("250 MB") ? 413 : 500).json({error: "Workspace archive could not be created"});
|
||||
}
|
||||
});
|
||||
}));
|
||||
router.post("/imports/workspace/archive/preview", readArchive, (req, res) => {
|
||||
const validation = validateWorkspaceImport(req.archive.workspace, repositories.exportWorkspace(req.session.user));
|
||||
if (validation.ok) {
|
||||
@@ -651,7 +651,7 @@ export function createChatsRouter({repositories, chatService, modelService, conf
|
||||
}
|
||||
return res.status(validation.ok ? 200 : 400).json({ok: validation.ok, errors: validation.errors, warnings: validation.warnings, preview: validation.preview});
|
||||
});
|
||||
router.post("/imports/workspace/archive", readArchive, async (req, res) => {
|
||||
router.post("/imports/workspace/archive", readArchive, asyncHandler(async (req, res) => {
|
||||
const validation = validateWorkspaceImport(req.archive.workspace, repositories.exportWorkspace(req.session.user));
|
||||
if (!validation.ok) return res.status(400).json({error: "Workspace import validation failed", errors: validation.errors});
|
||||
if (validation.preview.duplicates.length && req.body?.allowDuplicates !== "true") {
|
||||
@@ -677,7 +677,7 @@ export function createChatsRouter({repositories, chatService, modelService, conf
|
||||
for (const id of restored) await attachmentService.remove(id, req.session.user).catch(() => {});
|
||||
return res.status(error.status || 400).json({error: error.message || "Workspace archive import failed"});
|
||||
}
|
||||
});
|
||||
}));
|
||||
router.post("/imports/workspace/preview", (req, res) => {
|
||||
const validation = validateWorkspaceImport(req.body?.workspace, repositories.exportWorkspace(req.session.user));
|
||||
return res.status(validation.ok ? 200 : 400).json({
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* @file Verwaltet eigene Personas sowie Veröffentlichung, Suche und Interaktionen auf dem Persona-Marktplatz.
|
||||
*/
|
||||
import express from "express";
|
||||
import {asyncHandler} from "../middleware/asyncHandler.js";
|
||||
import {requireLogin} from "../middleware/auth.js";
|
||||
import {validateBody} from "../middleware/validate.js";
|
||||
import {normalizeOptionalText, validatePersonaPayload} from "../services/personaService.js";
|
||||
@@ -49,7 +50,7 @@ const templateValidator = (body) => {
|
||||
export function createPersonasRouter({repositories, chatService, modelService, config}) {
|
||||
const router = express.Router();
|
||||
router.use(requireLogin);
|
||||
const isAdmin = (req) => req.session.user === "admin";
|
||||
const isAdmin = (req) => req.account?.role === "admin";
|
||||
const parseMarketPersonaType = (value) => ["assistant", "user"].includes(value) ? value : null;
|
||||
|
||||
router.get("/personas", (req, res) => {
|
||||
@@ -214,7 +215,7 @@ export function createPersonasRouter({repositories, chatService, modelService, c
|
||||
return res.json({message: "Report submitted"});
|
||||
});
|
||||
|
||||
router.post("/personas/market/:id/chat", async (req, res) => {
|
||||
router.post("/personas/market/:id/chat", asyncHandler(async (req, res) => {
|
||||
const user = req.session.user;
|
||||
const marketPersona = repositories.getMarketPersona(Number(req.params.id));
|
||||
const userPersonaId = req.body?.userPersonaId ? Number(req.body.userPersonaId) : null;
|
||||
@@ -281,9 +282,9 @@ export function createPersonasRouter({repositories, chatService, modelService, c
|
||||
} catch {
|
||||
return res.json({persona, chat, existing: false, generatedInitialMessage: false, degraded: true});
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
router.post("/roleplays/start", validateBody(roleplayStartValidator), async (req, res) => {
|
||||
router.post("/roleplays/start", validateBody(roleplayStartValidator), asyncHandler(async (req, res) => {
|
||||
const user = req.session.user;
|
||||
const {assistantPersonaId, userPersonaId, scenarioPrompt, model} = req.validatedBody;
|
||||
const assistantPersona = repositories.getPersona(assistantPersonaId, user);
|
||||
@@ -345,7 +346,7 @@ export function createPersonasRouter({repositories, chatService, modelService, c
|
||||
modelErrorCode: mapped.body.code
|
||||
});
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
router.get("/templates", (req, res) => {
|
||||
const personaId = req.query.personaId ? Number(req.query.personaId) : null;
|
||||
|
||||
+20
-12
@@ -2,6 +2,7 @@
|
||||
* @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";
|
||||
@@ -29,7 +30,7 @@ const currentPasswordValidator = (body) => {
|
||||
return {value: {currentPassword}};
|
||||
};
|
||||
|
||||
const destroySession = (req) => new Promise((resolve) => req.session.destroy(() => resolve()));
|
||||
const destroySession = (req) => new Promise((resolve, reject) => req.session.destroy(error => error ? reject(error) : resolve()));
|
||||
|
||||
export function createSettingsRouter({repositories}) {
|
||||
const router = express.Router();
|
||||
@@ -38,10 +39,10 @@ export function createSettingsRouter({repositories}) {
|
||||
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});
|
||||
return res.json({username: user.username, role: user.role});
|
||||
});
|
||||
|
||||
router.put("/settings/username", validateBody(usernameValidator), async (req, res) => {
|
||||
router.put("/settings/username", validateBody(usernameValidator), asyncHandler(async (req, res) => {
|
||||
const currentUsername = req.session.user;
|
||||
const {username: nextUsername, password} = req.validatedBody;
|
||||
if (nextUsername === currentUsername) {
|
||||
@@ -53,15 +54,22 @@ export function createSettingsRouter({repositories}) {
|
||||
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(400).json({error: "Username already exists"});
|
||||
return res.status(409).json({error: "Username already exists", code: "USERNAME_TAKEN"});
|
||||
}
|
||||
|
||||
repositories.renameUser(currentUsername, nextUsername);
|
||||
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), async (req, res) => {
|
||||
router.put("/settings/password", validateBody(passwordValidator), asyncHandler(async (req, res) => {
|
||||
const username = req.session.user;
|
||||
const {currentPassword, newPassword} = req.validatedBody;
|
||||
const user = repositories.getUser(username);
|
||||
@@ -73,11 +81,11 @@ export function createSettingsRouter({repositories}) {
|
||||
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) => {
|
||||
router.post("/settings/logout-all-sessions", (req, res, next) => {
|
||||
repositories.incrementSessionVersion(req.session.user);
|
||||
req.session.destroy(() => res.json({message: "All sessions logged out"}));
|
||||
req.session.destroy(error => error ? next(error) : res.json({message: "All sessions logged out"}));
|
||||
});
|
||||
|
||||
router.get("/settings/export", (req, res) => {
|
||||
@@ -92,7 +100,7 @@ export function createSettingsRouter({repositories}) {
|
||||
});
|
||||
});
|
||||
|
||||
router.delete("/settings/account", validateBody(currentPasswordValidator), async (req, res) => {
|
||||
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"});
|
||||
@@ -101,7 +109,7 @@ export function createSettingsRouter({repositories}) {
|
||||
repositories.deleteUser(username);
|
||||
await destroySession(req);
|
||||
return res.json({message: "Account deleted"});
|
||||
});
|
||||
}));
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
+6
-12
@@ -2,14 +2,8 @@
|
||||
* @file Stellt Modellinformationen und administrative Diagnosedaten bereit.
|
||||
*/
|
||||
import express from "express";
|
||||
import {requireLogin} from "../middleware/auth.js";
|
||||
|
||||
function requireAdmin(req, res, next) {
|
||||
if (req.session.user !== "admin") {
|
||||
return res.status(403).json({error: "Admin only"});
|
||||
}
|
||||
return next();
|
||||
}
|
||||
import {asyncHandler} from "../middleware/asyncHandler.js";
|
||||
import {requireLogin, requireAdmin} from "../middleware/auth.js";
|
||||
|
||||
function buildConfigWarnings(config) {
|
||||
const warnings = [];
|
||||
@@ -34,18 +28,18 @@ function buildConfigWarnings(config) {
|
||||
export function createSystemRouter({modelService, config}) {
|
||||
const router = express.Router();
|
||||
|
||||
router.get("/health", async (req, res) => {
|
||||
router.get("/health", asyncHandler(async (req, res) => {
|
||||
const model = await modelService.checkHealth();
|
||||
return res.status(model.ok ? 200 : 503).json({ok: model.ok, model});
|
||||
});
|
||||
}));
|
||||
|
||||
router.get("/admin/diagnostics", requireLogin, requireAdmin, async (req, res) => {
|
||||
router.get("/admin/diagnostics", requireLogin, requireAdmin, asyncHandler(async (req, res) => {
|
||||
const diagnostics = await modelService.getDiagnostics();
|
||||
return res.status(diagnostics.model.ok ? 200 : 503).json({
|
||||
model: diagnostics.model,
|
||||
configWarnings: buildConfigWarnings(config)
|
||||
});
|
||||
});
|
||||
}));
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* @file Verarbeitet das Hochladen, Abrufen und Entfernen von Bildanhängen mit Zugriffskontrolle.
|
||||
*/
|
||||
import express from "express";
|
||||
import {asyncHandler} from "../middleware/asyncHandler.js";
|
||||
import multer from "multer";
|
||||
import {requireLogin} from "../middleware/auth.js";
|
||||
import {AttachmentError} from "../services/attachmentService.js";
|
||||
@@ -31,17 +32,17 @@ export function createUploadsRouter(service, config) {
|
||||
catch (failure) { fail(res, failure); }
|
||||
});
|
||||
});
|
||||
router.get("/attachments/:id", requireLogin, async (req, res) => {
|
||||
router.get("/attachments/:id", requireLogin, asyncHandler(async (req, res) => {
|
||||
try {
|
||||
const row = service.owned(req.params.id, req.session.user);
|
||||
const buffer = await service.storage.read(row.storage_key);
|
||||
res.set({"Content-Type": row.mime_type, "Content-Length": buffer.length, "Cache-Control": "private, no-store",
|
||||
"X-Content-Type-Options": "nosniff", "Content-Disposition": "inline"}).send(buffer);
|
||||
} catch (error) { fail(res, error); }
|
||||
});
|
||||
router.delete("/attachments/:id", requireLogin, async (req, res) => {
|
||||
}));
|
||||
router.delete("/attachments/:id", requireLogin, asyncHandler(async (req, res) => {
|
||||
try { await service.remove(req.params.id, req.session.user); res.json({success: true}); }
|
||||
catch (error) { fail(res, error); }
|
||||
});
|
||||
}));
|
||||
return router;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
/** Translate model callbacks to small wire events, including corrective-pass resets. */
|
||||
export function createTextStreamEvents(sendEvent) {
|
||||
const emit = (event, target) => (chunk, fullText) => {
|
||||
if (chunk === "" && fullText === "") sendEvent("replace", {target, text: ""});
|
||||
else if (chunk) sendEvent(event, {chunk});
|
||||
};
|
||||
return {onChunk: emit("chunk", "reply"), onThinking: emit("thinking", "thinking")};
|
||||
}
|
||||
Reference in New Issue
Block a user