2023-12-19 20:35:20 +01:00
|
|
|
const path = require("path");
|
2024-01-18 03:12:25 +01:00
|
|
|
const fs = require("fs");
|
2023-06-08 22:13:48 +02:00
|
|
|
process.env.NODE_ENV === "development"
|
|
|
|
? require("dotenv").config({ path: `.env.${process.env.NODE_ENV}` })
|
2023-12-19 20:35:20 +01:00
|
|
|
: require("dotenv").config({
|
2024-07-20 00:25:09 +02:00
|
|
|
path: process.env.STORAGE_DIR
|
|
|
|
? path.resolve(process.env.STORAGE_DIR, ".env")
|
|
|
|
: path.resolve(__dirname, ".env"),
|
|
|
|
});
|
2023-12-19 20:35:20 +01:00
|
|
|
|
2024-05-23 21:52:04 +02:00
|
|
|
const { viewLocalFiles, normalizePath, isWithin } = require("../utils/files");
|
2023-12-19 00:48:02 +01:00
|
|
|
const { purgeDocument, purgeFolder } = require("../utils/files/purgeDocument");
|
2023-12-04 17:38:15 +01:00
|
|
|
const { getVectorDbClass } = require("../utils/helpers");
|
2023-08-16 01:00:27 +02:00
|
|
|
const { updateENV, dumpENV } = require("../utils/helpers/updateENV");
|
2023-07-25 19:37:04 +02:00
|
|
|
const {
|
|
|
|
reqBody,
|
|
|
|
makeJWT,
|
|
|
|
userFromSession,
|
|
|
|
multiUserMode,
|
2024-01-10 22:18:48 +01:00
|
|
|
queryParams,
|
2023-07-25 19:37:04 +02:00
|
|
|
} = require("../utils/http");
|
2024-04-01 21:06:47 +02:00
|
|
|
const { handleAssetUpload, handlePfpUpload } = require("../utils/files/multer");
|
2023-07-21 00:25:47 +02:00
|
|
|
const { v4 } = require("uuid");
|
2023-07-25 19:37:04 +02:00
|
|
|
const { SystemSettings } = require("../models/systemSettings");
|
|
|
|
const { User } = require("../models/user");
|
|
|
|
const { validatedRequest } = require("../utils/middleware/validatedRequest");
|
2023-08-15 00:22:55 +02:00
|
|
|
const {
|
|
|
|
getDefaultFilename,
|
|
|
|
determineLogoFilepath,
|
|
|
|
fetchLogo,
|
|
|
|
validFilename,
|
|
|
|
renameLogoFile,
|
|
|
|
removeCustomLogo,
|
2023-10-23 22:10:34 +02:00
|
|
|
LOGO_FILENAME,
|
2023-08-15 00:22:55 +02:00
|
|
|
} = require("../utils/files/logo");
|
2023-08-15 02:42:17 +02:00
|
|
|
const { Telemetry } = require("../models/telemetry");
|
2023-08-17 02:30:46 +02:00
|
|
|
const { WelcomeMessages } = require("../models/welcomeMessages");
|
2023-08-24 04:15:07 +02:00
|
|
|
const { ApiKey } = require("../models/apiKeys");
|
2023-10-31 19:38:28 +01:00
|
|
|
const { getCustomModels } = require("../utils/helpers/customModels");
|
2023-11-09 02:36:54 +01:00
|
|
|
const { WorkspaceChats } = require("../models/workspaceChats");
|
2024-01-22 23:14:01 +01:00
|
|
|
const {
|
|
|
|
flexUserRoleValid,
|
|
|
|
ROLES,
|
2024-04-26 01:52:30 +02:00
|
|
|
isMultiUserSetup,
|
2024-01-22 23:14:01 +01:00
|
|
|
} = require("../utils/middleware/multiUserProtected");
|
2023-12-07 23:11:51 +01:00
|
|
|
const { fetchPfp, determinePfpFilepath } = require("../utils/files/pfp");
|
2024-01-22 23:14:01 +01:00
|
|
|
const {
|
|
|
|
prepareWorkspaceChatsForExport,
|
|
|
|
exportChatsAsType,
|
|
|
|
} = require("../utils/helpers/chat/convertTo");
|
[FEAT] Automated audit logging (#667)
* WIP event logging - new table for events and new settings view for viewing
* WIP add logging
* UI for log rows
* rename files to Logging to prevent getting gitignore
* add metadata for all logging events and colored badges in logs page
* remove unneeded comment
* cleanup namespace for logging
* clean up backend calls
* update logging to show to => from settings changes
* add logging for invitations, created, deleted, and accepted
* add logging for user created, updated, suspended, or removed
* add logging for workspace deleted
* add logging for chat logs exported
* add logging for API keys, LLM, embedder, vector db, embed chat, and reset button
* modify event logs
* update to event log types
* simplify rendering of event badges
---------
Co-authored-by: timothycarambat <rambat1010@gmail.com>
2024-02-07 00:21:40 +01:00
|
|
|
const { EventLogs } = require("../models/eventLogs");
|
2024-02-17 01:32:25 +01:00
|
|
|
const { CollectorApi } = require("../utils/collectorApi");
|
2024-04-26 01:52:30 +02:00
|
|
|
const {
|
|
|
|
recoverAccount,
|
|
|
|
resetPassword,
|
|
|
|
generateRecoveryCodes,
|
|
|
|
} = require("../utils/PasswordRecovery");
|
2024-05-10 21:35:33 +02:00
|
|
|
const { SlashCommandPresets } = require("../models/slashCommandsPresets");
|
2024-08-14 02:54:12 +02:00
|
|
|
const { EncryptionManager } = require("../utils/EncryptionManager");
|
2023-06-04 04:28:07 +02:00
|
|
|
|
|
|
|
function systemEndpoints(app) {
|
|
|
|
if (!app) return;
|
|
|
|
|
2023-06-08 06:31:35 +02:00
|
|
|
app.get("/ping", (_, response) => {
|
2023-08-17 00:43:46 +02:00
|
|
|
response.status(200).json({ online: true });
|
2023-06-08 06:31:35 +02:00
|
|
|
});
|
2023-06-04 04:28:07 +02:00
|
|
|
|
2023-06-17 01:01:27 +02:00
|
|
|
app.get("/migrate", async (_, response) => {
|
|
|
|
response.sendStatus(200);
|
|
|
|
});
|
|
|
|
|
2023-08-16 01:00:27 +02:00
|
|
|
app.get("/env-dump", async (_, response) => {
|
|
|
|
if (process.env.NODE_ENV !== "production")
|
|
|
|
return response.sendStatus(200).end();
|
2024-06-21 05:50:14 +02:00
|
|
|
dumpENV();
|
2023-08-16 01:00:27 +02:00
|
|
|
response.sendStatus(200).end();
|
|
|
|
});
|
|
|
|
|
2023-07-25 19:37:04 +02:00
|
|
|
app.get("/setup-complete", async (_, response) => {
|
2023-06-08 22:13:48 +02:00
|
|
|
try {
|
2023-08-24 04:15:07 +02:00
|
|
|
const results = await SystemSettings.currentSettings();
|
2023-06-08 22:13:48 +02:00
|
|
|
response.status(200).json({ results });
|
|
|
|
} catch (e) {
|
2024-07-04 01:39:33 +02:00
|
|
|
console.error(e.message, e);
|
2023-06-08 22:13:48 +02:00
|
|
|
response.sendStatus(500).end();
|
|
|
|
}
|
2023-06-08 06:31:35 +02:00
|
|
|
});
|
2023-06-04 04:28:07 +02:00
|
|
|
|
2023-07-25 19:37:04 +02:00
|
|
|
app.get(
|
|
|
|
"/system/check-token",
|
|
|
|
[validatedRequest],
|
|
|
|
async (request, response) => {
|
|
|
|
try {
|
|
|
|
if (multiUserMode(response)) {
|
|
|
|
const user = await userFromSession(request, response);
|
|
|
|
if (!user || user.suspended) {
|
|
|
|
response.sendStatus(403).end();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
response.sendStatus(200).end();
|
|
|
|
return;
|
|
|
|
}
|
2023-06-09 20:27:27 +02:00
|
|
|
|
2023-07-25 19:37:04 +02:00
|
|
|
response.sendStatus(200).end();
|
|
|
|
} catch (e) {
|
2024-07-04 01:39:33 +02:00
|
|
|
console.error(e.message, e);
|
2023-07-25 19:37:04 +02:00
|
|
|
response.sendStatus(500).end();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
app.post("/request-token", async (request, response) => {
|
2023-06-09 20:27:27 +02:00
|
|
|
try {
|
2024-01-11 19:54:55 +01:00
|
|
|
const bcrypt = require("bcrypt");
|
|
|
|
|
2023-07-25 19:37:04 +02:00
|
|
|
if (await SystemSettings.isMultiUserMode()) {
|
|
|
|
const { username, password } = reqBody(request);
|
2024-05-22 19:32:39 +02:00
|
|
|
const existingUser = await User._get({ username: String(username) });
|
2023-07-25 19:37:04 +02:00
|
|
|
|
|
|
|
if (!existingUser) {
|
[FEAT] Automated audit logging (#667)
* WIP event logging - new table for events and new settings view for viewing
* WIP add logging
* UI for log rows
* rename files to Logging to prevent getting gitignore
* add metadata for all logging events and colored badges in logs page
* remove unneeded comment
* cleanup namespace for logging
* clean up backend calls
* update logging to show to => from settings changes
* add logging for invitations, created, deleted, and accepted
* add logging for user created, updated, suspended, or removed
* add logging for workspace deleted
* add logging for chat logs exported
* add logging for API keys, LLM, embedder, vector db, embed chat, and reset button
* modify event logs
* update to event log types
* simplify rendering of event badges
---------
Co-authored-by: timothycarambat <rambat1010@gmail.com>
2024-02-07 00:21:40 +01:00
|
|
|
await EventLogs.logEvent(
|
|
|
|
"failed_login_invalid_username",
|
|
|
|
{
|
|
|
|
ip: request.ip || "Unknown IP",
|
|
|
|
username: username || "Unknown user",
|
|
|
|
},
|
|
|
|
existingUser?.id
|
|
|
|
);
|
2023-07-25 19:37:04 +02:00
|
|
|
response.status(200).json({
|
|
|
|
user: null,
|
|
|
|
valid: false,
|
|
|
|
token: null,
|
|
|
|
message: "[001] Invalid login credentials.",
|
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2024-03-29 19:47:30 +01:00
|
|
|
if (!bcrypt.compareSync(String(password), existingUser.password)) {
|
[FEAT] Automated audit logging (#667)
* WIP event logging - new table for events and new settings view for viewing
* WIP add logging
* UI for log rows
* rename files to Logging to prevent getting gitignore
* add metadata for all logging events and colored badges in logs page
* remove unneeded comment
* cleanup namespace for logging
* clean up backend calls
* update logging to show to => from settings changes
* add logging for invitations, created, deleted, and accepted
* add logging for user created, updated, suspended, or removed
* add logging for workspace deleted
* add logging for chat logs exported
* add logging for API keys, LLM, embedder, vector db, embed chat, and reset button
* modify event logs
* update to event log types
* simplify rendering of event badges
---------
Co-authored-by: timothycarambat <rambat1010@gmail.com>
2024-02-07 00:21:40 +01:00
|
|
|
await EventLogs.logEvent(
|
|
|
|
"failed_login_invalid_password",
|
|
|
|
{
|
|
|
|
ip: request.ip || "Unknown IP",
|
|
|
|
username: username || "Unknown user",
|
|
|
|
},
|
|
|
|
existingUser?.id
|
|
|
|
);
|
2023-07-25 19:37:04 +02:00
|
|
|
response.status(200).json({
|
|
|
|
user: null,
|
|
|
|
valid: false,
|
|
|
|
token: null,
|
|
|
|
message: "[002] Invalid login credentials.",
|
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (existingUser.suspended) {
|
[FEAT] Automated audit logging (#667)
* WIP event logging - new table for events and new settings view for viewing
* WIP add logging
* UI for log rows
* rename files to Logging to prevent getting gitignore
* add metadata for all logging events and colored badges in logs page
* remove unneeded comment
* cleanup namespace for logging
* clean up backend calls
* update logging to show to => from settings changes
* add logging for invitations, created, deleted, and accepted
* add logging for user created, updated, suspended, or removed
* add logging for workspace deleted
* add logging for chat logs exported
* add logging for API keys, LLM, embedder, vector db, embed chat, and reset button
* modify event logs
* update to event log types
* simplify rendering of event badges
---------
Co-authored-by: timothycarambat <rambat1010@gmail.com>
2024-02-07 00:21:40 +01:00
|
|
|
await EventLogs.logEvent(
|
|
|
|
"failed_login_account_suspended",
|
|
|
|
{
|
|
|
|
ip: request.ip || "Unknown IP",
|
|
|
|
username: username || "Unknown user",
|
|
|
|
},
|
|
|
|
existingUser?.id
|
|
|
|
);
|
2023-07-25 19:37:04 +02:00
|
|
|
response.status(200).json({
|
|
|
|
user: null,
|
|
|
|
valid: false,
|
|
|
|
token: null,
|
|
|
|
message: "[004] Account suspended by admin.",
|
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2023-11-11 01:02:46 +01:00
|
|
|
await Telemetry.sendTelemetry(
|
|
|
|
"login_event",
|
|
|
|
{ multiUserMode: false },
|
|
|
|
existingUser?.id
|
|
|
|
);
|
[FEAT] Automated audit logging (#667)
* WIP event logging - new table for events and new settings view for viewing
* WIP add logging
* UI for log rows
* rename files to Logging to prevent getting gitignore
* add metadata for all logging events and colored badges in logs page
* remove unneeded comment
* cleanup namespace for logging
* clean up backend calls
* update logging to show to => from settings changes
* add logging for invitations, created, deleted, and accepted
* add logging for user created, updated, suspended, or removed
* add logging for workspace deleted
* add logging for chat logs exported
* add logging for API keys, LLM, embedder, vector db, embed chat, and reset button
* modify event logs
* update to event log types
* simplify rendering of event badges
---------
Co-authored-by: timothycarambat <rambat1010@gmail.com>
2024-02-07 00:21:40 +01:00
|
|
|
|
|
|
|
await EventLogs.logEvent(
|
|
|
|
"login_event",
|
|
|
|
{
|
|
|
|
ip: request.ip || "Unknown IP",
|
|
|
|
username: existingUser.username || "Unknown user",
|
|
|
|
},
|
|
|
|
existingUser?.id
|
|
|
|
);
|
|
|
|
|
2024-04-26 01:52:30 +02:00
|
|
|
// Check if the user has seen the recovery codes
|
|
|
|
if (!existingUser.seen_recovery_codes) {
|
|
|
|
const plainTextCodes = await generateRecoveryCodes(existingUser.id);
|
|
|
|
|
|
|
|
// Return recovery codes to frontend
|
|
|
|
response.status(200).json({
|
|
|
|
valid: true,
|
2024-05-22 19:32:39 +02:00
|
|
|
user: User.filterFields(existingUser),
|
2024-04-26 01:52:30 +02:00
|
|
|
token: makeJWT(
|
|
|
|
{ id: existingUser.id, username: existingUser.username },
|
|
|
|
"30d"
|
|
|
|
),
|
|
|
|
message: null,
|
|
|
|
recoveryCodes: plainTextCodes,
|
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2023-07-25 19:37:04 +02:00
|
|
|
response.status(200).json({
|
|
|
|
valid: true,
|
2024-05-22 19:32:39 +02:00
|
|
|
user: User.filterFields(existingUser),
|
2023-07-25 19:37:04 +02:00
|
|
|
token: makeJWT(
|
|
|
|
{ id: existingUser.id, username: existingUser.username },
|
|
|
|
"30d"
|
|
|
|
),
|
|
|
|
message: null,
|
2023-06-09 21:59:22 +02:00
|
|
|
});
|
2023-06-09 20:27:27 +02:00
|
|
|
return;
|
2023-07-25 19:37:04 +02:00
|
|
|
} else {
|
|
|
|
const { password } = reqBody(request);
|
2024-01-11 19:54:55 +01:00
|
|
|
if (
|
|
|
|
!bcrypt.compareSync(
|
|
|
|
password,
|
|
|
|
bcrypt.hashSync(process.env.AUTH_TOKEN, 10)
|
|
|
|
)
|
|
|
|
) {
|
[FEAT] Automated audit logging (#667)
* WIP event logging - new table for events and new settings view for viewing
* WIP add logging
* UI for log rows
* rename files to Logging to prevent getting gitignore
* add metadata for all logging events and colored badges in logs page
* remove unneeded comment
* cleanup namespace for logging
* clean up backend calls
* update logging to show to => from settings changes
* add logging for invitations, created, deleted, and accepted
* add logging for user created, updated, suspended, or removed
* add logging for workspace deleted
* add logging for chat logs exported
* add logging for API keys, LLM, embedder, vector db, embed chat, and reset button
* modify event logs
* update to event log types
* simplify rendering of event badges
---------
Co-authored-by: timothycarambat <rambat1010@gmail.com>
2024-02-07 00:21:40 +01:00
|
|
|
await EventLogs.logEvent("failed_login_invalid_password", {
|
|
|
|
ip: request.ip || "Unknown IP",
|
|
|
|
multiUserMode: false,
|
|
|
|
});
|
2023-07-25 19:37:04 +02:00
|
|
|
response.status(401).json({
|
|
|
|
valid: false,
|
|
|
|
token: null,
|
|
|
|
message: "[003] Invalid password provided",
|
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
2023-06-09 20:27:27 +02:00
|
|
|
|
2023-11-11 01:02:46 +01:00
|
|
|
await Telemetry.sendTelemetry("login_event", { multiUserMode: false });
|
[FEAT] Automated audit logging (#667)
* WIP event logging - new table for events and new settings view for viewing
* WIP add logging
* UI for log rows
* rename files to Logging to prevent getting gitignore
* add metadata for all logging events and colored badges in logs page
* remove unneeded comment
* cleanup namespace for logging
* clean up backend calls
* update logging to show to => from settings changes
* add logging for invitations, created, deleted, and accepted
* add logging for user created, updated, suspended, or removed
* add logging for workspace deleted
* add logging for chat logs exported
* add logging for API keys, LLM, embedder, vector db, embed chat, and reset button
* modify event logs
* update to event log types
* simplify rendering of event badges
---------
Co-authored-by: timothycarambat <rambat1010@gmail.com>
2024-02-07 00:21:40 +01:00
|
|
|
await EventLogs.logEvent("login_event", {
|
|
|
|
ip: request.ip || "Unknown IP",
|
|
|
|
multiUserMode: false,
|
|
|
|
});
|
2023-07-25 19:37:04 +02:00
|
|
|
response.status(200).json({
|
|
|
|
valid: true,
|
2024-08-14 02:54:12 +02:00
|
|
|
token: makeJWT(
|
|
|
|
{ p: new EncryptionManager().encrypt(password) },
|
|
|
|
"30d"
|
|
|
|
),
|
2023-07-25 19:37:04 +02:00
|
|
|
message: null,
|
|
|
|
});
|
|
|
|
}
|
2023-06-09 20:27:27 +02:00
|
|
|
} catch (e) {
|
2024-07-04 01:39:33 +02:00
|
|
|
console.error(e.message, e);
|
2023-06-09 20:27:27 +02:00
|
|
|
response.sendStatus(500).end();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2024-04-26 01:52:30 +02:00
|
|
|
app.post(
|
|
|
|
"/system/recover-account",
|
|
|
|
[isMultiUserSetup],
|
|
|
|
async (request, response) => {
|
|
|
|
try {
|
|
|
|
const { username, recoveryCodes } = reqBody(request);
|
|
|
|
const { success, resetToken, error } = await recoverAccount(
|
|
|
|
username,
|
|
|
|
recoveryCodes
|
|
|
|
);
|
|
|
|
|
|
|
|
if (success) {
|
|
|
|
response.status(200).json({ success, resetToken });
|
|
|
|
} else {
|
|
|
|
response.status(400).json({ success, message: error });
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
console.error("Error recovering account:", error);
|
|
|
|
response
|
|
|
|
.status(500)
|
|
|
|
.json({ success: false, message: "Internal server error" });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
app.post(
|
|
|
|
"/system/reset-password",
|
|
|
|
[isMultiUserSetup],
|
|
|
|
async (request, response) => {
|
|
|
|
try {
|
|
|
|
const { token, newPassword, confirmPassword } = reqBody(request);
|
|
|
|
const { success, message, error } = await resetPassword(
|
|
|
|
token,
|
|
|
|
newPassword,
|
|
|
|
confirmPassword
|
|
|
|
);
|
|
|
|
|
|
|
|
if (success) {
|
|
|
|
response.status(200).json({ success, message });
|
|
|
|
} else {
|
|
|
|
response.status(400).json({ success, error });
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
console.error("Error resetting password:", error);
|
|
|
|
response.status(500).json({ success: false, message: error.message });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
2024-01-10 22:18:48 +01:00
|
|
|
app.get(
|
|
|
|
"/system/system-vectors",
|
2024-01-22 23:31:19 +01:00
|
|
|
[validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
|
2024-01-10 22:18:48 +01:00
|
|
|
async (request, response) => {
|
|
|
|
try {
|
|
|
|
const query = queryParams(request);
|
|
|
|
const VectorDb = getVectorDbClass();
|
|
|
|
const vectorCount = !!query.slug
|
|
|
|
? await VectorDb.namespaceCount(query.slug)
|
|
|
|
: await VectorDb.totalVectors();
|
|
|
|
response.status(200).json({ vectorCount });
|
|
|
|
} catch (e) {
|
2024-07-04 01:39:33 +02:00
|
|
|
console.error(e.message, e);
|
2024-01-10 22:18:48 +01:00
|
|
|
response.sendStatus(500).end();
|
|
|
|
}
|
2023-06-08 22:13:48 +02:00
|
|
|
}
|
2024-01-10 22:18:48 +01:00
|
|
|
);
|
2023-06-04 04:28:07 +02:00
|
|
|
|
2023-07-25 19:37:04 +02:00
|
|
|
app.delete(
|
|
|
|
"/system/remove-document",
|
2024-01-22 23:31:19 +01:00
|
|
|
[validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
|
2023-07-25 19:37:04 +02:00
|
|
|
async (request, response) => {
|
|
|
|
try {
|
2023-12-19 00:48:02 +01:00
|
|
|
const { name } = reqBody(request);
|
|
|
|
await purgeDocument(name);
|
|
|
|
response.sendStatus(200).end();
|
|
|
|
} catch (e) {
|
2024-07-04 01:39:33 +02:00
|
|
|
console.error(e.message, e);
|
2023-12-19 00:48:02 +01:00
|
|
|
response.sendStatus(500).end();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
2024-02-27 20:53:42 +01:00
|
|
|
app.delete(
|
|
|
|
"/system/remove-documents",
|
|
|
|
[validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
|
|
|
|
async (request, response) => {
|
|
|
|
try {
|
|
|
|
const { names } = reqBody(request);
|
|
|
|
for await (const name of names) await purgeDocument(name);
|
|
|
|
response.sendStatus(200).end();
|
|
|
|
} catch (e) {
|
2024-07-04 01:39:33 +02:00
|
|
|
console.error(e.message, e);
|
2024-02-27 20:53:42 +01:00
|
|
|
response.sendStatus(500).end();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
2023-12-19 00:48:02 +01:00
|
|
|
app.delete(
|
|
|
|
"/system/remove-folder",
|
2024-01-22 23:31:19 +01:00
|
|
|
[validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
|
2023-12-19 00:48:02 +01:00
|
|
|
async (request, response) => {
|
|
|
|
try {
|
|
|
|
const { name } = reqBody(request);
|
|
|
|
await purgeFolder(name);
|
2023-07-25 19:37:04 +02:00
|
|
|
response.sendStatus(200).end();
|
|
|
|
} catch (e) {
|
2024-07-04 01:39:33 +02:00
|
|
|
console.error(e.message, e);
|
2023-07-25 19:37:04 +02:00
|
|
|
response.sendStatus(500).end();
|
|
|
|
}
|
2023-06-27 02:20:09 +02:00
|
|
|
}
|
2023-07-25 19:37:04 +02:00
|
|
|
);
|
2023-06-27 02:20:09 +02:00
|
|
|
|
2024-01-22 23:31:19 +01:00
|
|
|
app.get(
|
|
|
|
"/system/local-files",
|
|
|
|
[validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
|
|
|
|
async (_, response) => {
|
|
|
|
try {
|
|
|
|
const localFiles = await viewLocalFiles();
|
|
|
|
response.status(200).json({ localFiles });
|
|
|
|
} catch (e) {
|
2024-07-04 01:39:33 +02:00
|
|
|
console.error(e.message, e);
|
2024-01-22 23:31:19 +01:00
|
|
|
response.sendStatus(500).end();
|
|
|
|
}
|
2023-06-08 22:13:48 +02:00
|
|
|
}
|
2024-01-22 23:31:19 +01:00
|
|
|
);
|
2023-06-17 01:01:27 +02:00
|
|
|
|
2023-07-25 19:37:04 +02:00
|
|
|
app.get(
|
|
|
|
"/system/document-processing-status",
|
|
|
|
[validatedRequest],
|
|
|
|
async (_, response) => {
|
|
|
|
try {
|
2024-02-17 01:32:25 +01:00
|
|
|
const online = await new CollectorApi().online();
|
2023-07-25 19:37:04 +02:00
|
|
|
response.sendStatus(online ? 200 : 503);
|
|
|
|
} catch (e) {
|
2024-07-04 01:39:33 +02:00
|
|
|
console.error(e.message, e);
|
2023-07-25 19:37:04 +02:00
|
|
|
response.sendStatus(500).end();
|
|
|
|
}
|
2023-06-17 01:01:27 +02:00
|
|
|
}
|
2023-07-25 19:37:04 +02:00
|
|
|
);
|
2023-06-17 01:01:27 +02:00
|
|
|
|
2023-07-25 19:37:04 +02:00
|
|
|
app.get(
|
|
|
|
"/system/accepted-document-types",
|
|
|
|
[validatedRequest],
|
|
|
|
async (_, response) => {
|
|
|
|
try {
|
2024-02-17 01:32:25 +01:00
|
|
|
const types = await new CollectorApi().acceptedFileTypes();
|
2023-07-25 19:37:04 +02:00
|
|
|
if (!types) {
|
|
|
|
response.sendStatus(404).end();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
response.status(200).json({ types });
|
|
|
|
} catch (e) {
|
2024-07-04 01:39:33 +02:00
|
|
|
console.error(e.message, e);
|
2023-07-25 19:37:04 +02:00
|
|
|
response.sendStatus(500).end();
|
2023-06-17 01:01:27 +02:00
|
|
|
}
|
2023-07-25 19:37:04 +02:00
|
|
|
}
|
|
|
|
);
|
2023-06-17 01:01:27 +02:00
|
|
|
|
2023-07-25 19:37:04 +02:00
|
|
|
app.post(
|
|
|
|
"/system/update-env",
|
2024-01-22 23:14:01 +01:00
|
|
|
[validatedRequest, flexUserRoleValid([ROLES.admin])],
|
2023-07-25 19:37:04 +02:00
|
|
|
async (request, response) => {
|
|
|
|
try {
|
|
|
|
const body = reqBody(request);
|
[FEAT] Automated audit logging (#667)
* WIP event logging - new table for events and new settings view for viewing
* WIP add logging
* UI for log rows
* rename files to Logging to prevent getting gitignore
* add metadata for all logging events and colored badges in logs page
* remove unneeded comment
* cleanup namespace for logging
* clean up backend calls
* update logging to show to => from settings changes
* add logging for invitations, created, deleted, and accepted
* add logging for user created, updated, suspended, or removed
* add logging for workspace deleted
* add logging for chat logs exported
* add logging for API keys, LLM, embedder, vector db, embed chat, and reset button
* modify event logs
* update to event log types
* simplify rendering of event badges
---------
Co-authored-by: timothycarambat <rambat1010@gmail.com>
2024-02-07 00:21:40 +01:00
|
|
|
const { newValues, error } = await updateENV(
|
|
|
|
body,
|
|
|
|
false,
|
|
|
|
response?.locals?.user?.id
|
|
|
|
);
|
2023-07-25 19:37:04 +02:00
|
|
|
response.status(200).json({ newValues, error });
|
|
|
|
} catch (e) {
|
2024-07-04 01:39:33 +02:00
|
|
|
console.error(e.message, e);
|
2023-07-25 19:37:04 +02:00
|
|
|
response.sendStatus(500).end();
|
|
|
|
}
|
2023-06-17 01:01:27 +02:00
|
|
|
}
|
2023-07-25 19:37:04 +02:00
|
|
|
);
|
2023-06-26 20:38:38 +02:00
|
|
|
|
2023-07-25 19:37:04 +02:00
|
|
|
app.post(
|
|
|
|
"/system/update-password",
|
|
|
|
[validatedRequest],
|
|
|
|
async (request, response) => {
|
|
|
|
try {
|
2023-09-29 19:44:40 +02:00
|
|
|
// Cannot update password in multi - user mode.
|
|
|
|
if (multiUserMode(response)) {
|
|
|
|
response.sendStatus(401).end();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2024-05-23 19:46:12 +02:00
|
|
|
let error = null;
|
2023-07-25 19:37:04 +02:00
|
|
|
const { usePassword, newPassword } = reqBody(request);
|
2024-05-23 19:54:07 +02:00
|
|
|
if (!usePassword) {
|
|
|
|
// Password is being disabled so directly unset everything to bypass validation.
|
2024-05-23 19:46:12 +02:00
|
|
|
process.env.AUTH_TOKEN = "";
|
|
|
|
process.env.JWT_SECRET = "";
|
|
|
|
} else {
|
|
|
|
error = await updateENV(
|
|
|
|
{
|
|
|
|
AuthToken: newPassword,
|
|
|
|
JWTSecret: v4(),
|
|
|
|
},
|
|
|
|
true
|
|
|
|
)?.error;
|
|
|
|
}
|
2023-07-25 19:37:04 +02:00
|
|
|
response.status(200).json({ success: !error, error });
|
|
|
|
} catch (e) {
|
2024-07-04 01:39:33 +02:00
|
|
|
console.error(e.message, e);
|
2023-07-25 19:37:04 +02:00
|
|
|
response.sendStatus(500).end();
|
|
|
|
}
|
2023-07-21 00:25:47 +02:00
|
|
|
}
|
2023-07-25 19:37:04 +02:00
|
|
|
);
|
2023-07-21 00:25:47 +02:00
|
|
|
|
2023-07-25 19:37:04 +02:00
|
|
|
app.post(
|
|
|
|
"/system/enable-multi-user",
|
|
|
|
[validatedRequest],
|
|
|
|
async (request, response) => {
|
|
|
|
try {
|
2024-03-27 20:20:53 +01:00
|
|
|
if (response.locals.multiUserMode) {
|
2023-07-25 19:37:04 +02:00
|
|
|
response.status(200).json({
|
|
|
|
success: false,
|
|
|
|
error: "Multi-user mode is already enabled.",
|
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2024-03-27 20:20:53 +01:00
|
|
|
const { username, password } = reqBody(request);
|
2023-07-25 19:37:04 +02:00
|
|
|
const { user, error } = await User.create({
|
|
|
|
username,
|
|
|
|
password,
|
2024-01-22 23:14:01 +01:00
|
|
|
role: ROLES.admin,
|
2023-07-25 19:37:04 +02:00
|
|
|
});
|
2024-08-16 21:55:14 +02:00
|
|
|
|
|
|
|
if (error || !user) {
|
|
|
|
response.status(400).json({
|
|
|
|
success: false,
|
|
|
|
error: error || "Failed to enable multi-user mode.",
|
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2024-03-29 18:56:32 +01:00
|
|
|
await SystemSettings._updateSettings({
|
2023-07-25 19:37:04 +02:00
|
|
|
multi_user_mode: true,
|
|
|
|
limit_user_messages: false,
|
|
|
|
message_limit: 25,
|
|
|
|
});
|
2023-09-29 19:44:40 +02:00
|
|
|
|
2024-01-17 21:59:25 +01:00
|
|
|
await updateENV(
|
2023-09-29 19:44:40 +02:00
|
|
|
{
|
2023-10-23 22:10:34 +02:00
|
|
|
JWTSecret: process.env.JWT_SECRET || v4(),
|
2023-09-29 19:44:40 +02:00
|
|
|
},
|
|
|
|
true
|
|
|
|
);
|
2023-11-11 01:02:46 +01:00
|
|
|
await Telemetry.sendTelemetry("enabled_multi_user_mode", {
|
|
|
|
multiUserMode: true,
|
|
|
|
});
|
[FEAT] Automated audit logging (#667)
* WIP event logging - new table for events and new settings view for viewing
* WIP add logging
* UI for log rows
* rename files to Logging to prevent getting gitignore
* add metadata for all logging events and colored badges in logs page
* remove unneeded comment
* cleanup namespace for logging
* clean up backend calls
* update logging to show to => from settings changes
* add logging for invitations, created, deleted, and accepted
* add logging for user created, updated, suspended, or removed
* add logging for workspace deleted
* add logging for chat logs exported
* add logging for API keys, LLM, embedder, vector db, embed chat, and reset button
* modify event logs
* update to event log types
* simplify rendering of event badges
---------
Co-authored-by: timothycarambat <rambat1010@gmail.com>
2024-02-07 00:21:40 +01:00
|
|
|
await EventLogs.logEvent("multi_user_mode_enabled", {}, user?.id);
|
2023-07-25 19:37:04 +02:00
|
|
|
response.status(200).json({ success: !!user, error });
|
|
|
|
} catch (e) {
|
2023-10-23 22:10:34 +02:00
|
|
|
await User.delete({});
|
2024-03-29 18:56:32 +01:00
|
|
|
await SystemSettings._updateSettings({
|
2023-10-23 22:10:34 +02:00
|
|
|
multi_user_mode: false,
|
|
|
|
});
|
|
|
|
|
2024-07-04 01:39:33 +02:00
|
|
|
console.error(e.message, e);
|
2023-07-25 19:37:04 +02:00
|
|
|
response.sendStatus(500).end();
|
|
|
|
}
|
2023-06-26 20:38:38 +02:00
|
|
|
}
|
2023-07-25 19:37:04 +02:00
|
|
|
);
|
2023-07-15 02:32:30 +02:00
|
|
|
|
2024-01-22 23:14:01 +01:00
|
|
|
app.get("/system/multi-user-mode", async (_, response) => {
|
2023-10-23 22:10:34 +02:00
|
|
|
try {
|
|
|
|
const multiUserMode = await SystemSettings.isMultiUserMode();
|
|
|
|
response.status(200).json({ multiUserMode });
|
|
|
|
} catch (e) {
|
2024-07-04 01:39:33 +02:00
|
|
|
console.error(e.message, e);
|
2023-10-23 22:10:34 +02:00
|
|
|
response.sendStatus(500).end();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2024-01-22 23:14:01 +01:00
|
|
|
app.get("/system/logo", async function (_, response) {
|
2023-08-15 00:22:55 +02:00
|
|
|
try {
|
2023-10-23 22:10:34 +02:00
|
|
|
const defaultFilename = getDefaultFilename();
|
2023-08-15 00:22:55 +02:00
|
|
|
const logoPath = await determineLogoFilepath(defaultFilename);
|
2023-12-07 23:11:51 +01:00
|
|
|
const { found, buffer, size, mime } = fetchLogo(logoPath);
|
2024-05-23 23:14:53 +02:00
|
|
|
|
2023-12-07 23:11:51 +01:00
|
|
|
if (!found) {
|
|
|
|
response.sendStatus(204).end();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2024-05-23 23:14:53 +02:00
|
|
|
const currentLogoFilename = await SystemSettings.currentLogoFilename();
|
2023-08-15 00:22:55 +02:00
|
|
|
response.writeHead(200, {
|
2024-05-23 23:14:53 +02:00
|
|
|
"Access-Control-Expose-Headers":
|
|
|
|
"Content-Disposition,X-Is-Custom-Logo,Content-Type,Content-Length",
|
2023-08-15 00:22:55 +02:00
|
|
|
"Content-Type": mime || "image/png",
|
|
|
|
"Content-Disposition": `attachment; filename=${path.basename(
|
|
|
|
logoPath
|
|
|
|
)}`,
|
|
|
|
"Content-Length": size,
|
2024-05-23 23:14:53 +02:00
|
|
|
"X-Is-Custom-Logo":
|
|
|
|
currentLogoFilename !== null &&
|
|
|
|
currentLogoFilename !== defaultFilename,
|
2023-08-15 00:22:55 +02:00
|
|
|
});
|
|
|
|
response.end(Buffer.from(buffer, "base64"));
|
|
|
|
return;
|
|
|
|
} catch (error) {
|
|
|
|
console.error("Error processing the logo request:", error);
|
|
|
|
response.status(500).json({ message: "Internal server error" });
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2024-02-08 21:17:01 +01:00
|
|
|
app.get("/system/footer-data", [validatedRequest], async (_, response) => {
|
2023-12-07 23:11:51 +01:00
|
|
|
try {
|
2024-02-08 21:17:01 +01:00
|
|
|
const footerData =
|
|
|
|
(await SystemSettings.get({ label: "footer_data" }))?.value ??
|
|
|
|
JSON.stringify([]);
|
|
|
|
response.status(200).json({ footerData: footerData });
|
|
|
|
} catch (error) {
|
|
|
|
console.error("Error fetching footer data:", error);
|
|
|
|
response.status(500).json({ message: "Internal server error" });
|
|
|
|
}
|
|
|
|
});
|
2023-12-07 23:11:51 +01:00
|
|
|
|
2024-02-19 19:30:41 +01:00
|
|
|
app.get("/system/support-email", [validatedRequest], async (_, response) => {
|
|
|
|
try {
|
|
|
|
const supportEmail =
|
|
|
|
(
|
|
|
|
await SystemSettings.get({
|
|
|
|
label: "support_email",
|
|
|
|
})
|
|
|
|
)?.value ?? null;
|
|
|
|
response.status(200).json({ supportEmail: supportEmail });
|
2023-12-07 23:11:51 +01:00
|
|
|
} catch (error) {
|
2024-02-19 19:30:41 +01:00
|
|
|
console.error("Error fetching support email:", error);
|
2023-12-07 23:11:51 +01:00
|
|
|
response.status(500).json({ message: "Internal server error" });
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2024-05-23 23:14:53 +02:00
|
|
|
// No middleware protection in order to get this on the login page
|
|
|
|
app.get("/system/custom-app-name", async (_, response) => {
|
|
|
|
try {
|
|
|
|
const customAppName =
|
|
|
|
(
|
|
|
|
await SystemSettings.get({
|
|
|
|
label: "custom_app_name",
|
|
|
|
})
|
|
|
|
)?.value ?? null;
|
|
|
|
response.status(200).json({ customAppName: customAppName });
|
|
|
|
} catch (error) {
|
|
|
|
console.error("Error fetching custom app name:", error);
|
|
|
|
response.status(500).json({ message: "Internal server error" });
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2024-01-22 23:14:01 +01:00
|
|
|
app.get(
|
|
|
|
"/system/pfp/:id",
|
|
|
|
[validatedRequest, flexUserRoleValid([ROLES.all])],
|
|
|
|
async function (request, response) {
|
|
|
|
try {
|
|
|
|
const { id } = request.params;
|
|
|
|
const pfpPath = await determinePfpFilepath(id);
|
2023-12-07 23:11:51 +01:00
|
|
|
|
2024-01-22 23:14:01 +01:00
|
|
|
if (!pfpPath) {
|
|
|
|
response.sendStatus(204).end();
|
|
|
|
return;
|
|
|
|
}
|
2023-12-07 23:11:51 +01:00
|
|
|
|
2024-01-22 23:14:01 +01:00
|
|
|
const { found, buffer, size, mime } = fetchPfp(pfpPath);
|
|
|
|
if (!found) {
|
|
|
|
response.sendStatus(204).end();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
response.writeHead(200, {
|
|
|
|
"Content-Type": mime || "image/png",
|
|
|
|
"Content-Disposition": `attachment; filename=${path.basename(
|
|
|
|
pfpPath
|
|
|
|
)}`,
|
|
|
|
"Content-Length": size,
|
|
|
|
});
|
|
|
|
response.end(Buffer.from(buffer, "base64"));
|
2023-12-07 23:11:51 +01:00
|
|
|
return;
|
2024-01-22 23:14:01 +01:00
|
|
|
} catch (error) {
|
|
|
|
console.error("Error processing the logo request:", error);
|
|
|
|
response.status(500).json({ message: "Internal server error" });
|
2023-12-07 23:11:51 +01:00
|
|
|
}
|
|
|
|
}
|
2024-01-22 23:14:01 +01:00
|
|
|
);
|
2023-12-07 23:11:51 +01:00
|
|
|
|
|
|
|
app.post(
|
|
|
|
"/system/upload-pfp",
|
2024-04-01 21:06:47 +02:00
|
|
|
[validatedRequest, flexUserRoleValid([ROLES.all]), handlePfpUpload],
|
2023-12-07 23:11:51 +01:00
|
|
|
async function (request, response) {
|
|
|
|
try {
|
|
|
|
const user = await userFromSession(request, response);
|
|
|
|
const uploadedFileName = request.randomFileName;
|
|
|
|
if (!uploadedFileName) {
|
|
|
|
return response.status(400).json({ message: "File upload failed." });
|
|
|
|
}
|
|
|
|
|
|
|
|
const userRecord = await User.get({ id: user.id });
|
2024-01-22 23:14:01 +01:00
|
|
|
const oldPfpFilename = userRecord.pfpFilename;
|
2023-12-07 23:11:51 +01:00
|
|
|
if (oldPfpFilename) {
|
2024-05-23 21:52:04 +02:00
|
|
|
const storagePath = path.join(__dirname, "../storage/assets/pfp");
|
2023-12-07 23:11:51 +01:00
|
|
|
const oldPfpPath = path.join(
|
2024-05-23 21:52:04 +02:00
|
|
|
storagePath,
|
|
|
|
normalizePath(userRecord.pfpFilename)
|
2023-12-07 23:11:51 +01:00
|
|
|
);
|
2024-05-23 21:52:04 +02:00
|
|
|
if (!isWithin(path.resolve(storagePath), path.resolve(oldPfpPath)))
|
|
|
|
throw new Error("Invalid path name");
|
2023-12-07 23:11:51 +01:00
|
|
|
if (fs.existsSync(oldPfpPath)) fs.unlinkSync(oldPfpPath);
|
|
|
|
}
|
|
|
|
|
|
|
|
const { success, error } = await User.update(user.id, {
|
|
|
|
pfpFilename: uploadedFileName,
|
|
|
|
});
|
|
|
|
|
|
|
|
return response.status(success ? 200 : 500).json({
|
|
|
|
message: success
|
|
|
|
? "Profile picture uploaded successfully."
|
|
|
|
: error || "Failed to update with new profile picture.",
|
|
|
|
});
|
|
|
|
} catch (error) {
|
|
|
|
console.error("Error processing the profile picture upload:", error);
|
|
|
|
response.status(500).json({ message: "Internal server error" });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
app.delete(
|
|
|
|
"/system/remove-pfp",
|
2024-01-22 23:14:01 +01:00
|
|
|
[validatedRequest, flexUserRoleValid([ROLES.all])],
|
2023-12-07 23:11:51 +01:00
|
|
|
async function (request, response) {
|
|
|
|
try {
|
|
|
|
const user = await userFromSession(request, response);
|
|
|
|
const userRecord = await User.get({ id: user.id });
|
2024-01-22 23:14:01 +01:00
|
|
|
const oldPfpFilename = userRecord.pfpFilename;
|
|
|
|
|
2023-12-07 23:11:51 +01:00
|
|
|
if (oldPfpFilename) {
|
2024-05-23 21:52:04 +02:00
|
|
|
const storagePath = path.join(__dirname, "../storage/assets/pfp");
|
2023-12-07 23:11:51 +01:00
|
|
|
const oldPfpPath = path.join(
|
2024-05-23 21:52:04 +02:00
|
|
|
storagePath,
|
|
|
|
normalizePath(oldPfpFilename)
|
2023-12-07 23:11:51 +01:00
|
|
|
);
|
2024-05-23 21:52:04 +02:00
|
|
|
if (!isWithin(path.resolve(storagePath), path.resolve(oldPfpPath)))
|
|
|
|
throw new Error("Invalid path name");
|
2023-12-07 23:11:51 +01:00
|
|
|
if (fs.existsSync(oldPfpPath)) fs.unlinkSync(oldPfpPath);
|
|
|
|
}
|
|
|
|
|
|
|
|
const { success, error } = await User.update(user.id, {
|
|
|
|
pfpFilename: null,
|
|
|
|
});
|
|
|
|
|
|
|
|
return response.status(success ? 200 : 500).json({
|
|
|
|
message: success
|
|
|
|
? "Profile picture removed successfully."
|
|
|
|
: error || "Failed to remove profile picture.",
|
|
|
|
});
|
|
|
|
} catch (error) {
|
|
|
|
console.error("Error processing the profile picture removal:", error);
|
|
|
|
response.status(500).json({ message: "Internal server error" });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
2023-08-15 00:22:55 +02:00
|
|
|
app.post(
|
|
|
|
"/system/upload-logo",
|
2024-04-01 21:06:47 +02:00
|
|
|
[
|
|
|
|
validatedRequest,
|
|
|
|
flexUserRoleValid([ROLES.admin, ROLES.manager]),
|
|
|
|
handleAssetUpload,
|
|
|
|
],
|
2023-08-15 00:22:55 +02:00
|
|
|
async (request, response) => {
|
2024-04-01 21:06:47 +02:00
|
|
|
if (!request?.file || !request?.file.originalname) {
|
2023-08-15 00:22:55 +02:00
|
|
|
return response.status(400).json({ message: "No logo file provided." });
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!validFilename(request.file.originalname)) {
|
|
|
|
return response.status(400).json({
|
|
|
|
message: "Invalid file name. Please choose a different file.",
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
const newFilename = await renameLogoFile(request.file.originalname);
|
|
|
|
const existingLogoFilename = await SystemSettings.currentLogoFilename();
|
|
|
|
await removeCustomLogo(existingLogoFilename);
|
|
|
|
|
2024-03-29 18:56:32 +01:00
|
|
|
const { success, error } = await SystemSettings._updateSettings({
|
2023-08-15 00:22:55 +02:00
|
|
|
logo_filename: newFilename,
|
|
|
|
});
|
|
|
|
|
|
|
|
return response.status(success ? 200 : 500).json({
|
|
|
|
message: success
|
|
|
|
? "Logo uploaded successfully."
|
|
|
|
: error || "Failed to update with new logo.",
|
|
|
|
});
|
|
|
|
} catch (error) {
|
|
|
|
console.error("Error processing the logo upload:", error);
|
|
|
|
response.status(500).json({ message: "Error uploading the logo." });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
2024-01-22 23:14:01 +01:00
|
|
|
app.get("/system/is-default-logo", async (_, response) => {
|
2023-10-23 22:10:34 +02:00
|
|
|
try {
|
|
|
|
const currentLogoFilename = await SystemSettings.currentLogoFilename();
|
|
|
|
const isDefaultLogo = currentLogoFilename === LOGO_FILENAME;
|
|
|
|
response.status(200).json({ isDefaultLogo });
|
|
|
|
} catch (error) {
|
|
|
|
console.error("Error processing the logo request:", error);
|
|
|
|
response.status(500).json({ message: "Internal server error" });
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2023-08-15 00:22:55 +02:00
|
|
|
app.get(
|
|
|
|
"/system/remove-logo",
|
2024-01-22 23:14:01 +01:00
|
|
|
[validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
|
2023-11-13 23:51:16 +01:00
|
|
|
async (_request, response) => {
|
2023-08-15 00:22:55 +02:00
|
|
|
try {
|
|
|
|
const currentLogoFilename = await SystemSettings.currentLogoFilename();
|
|
|
|
await removeCustomLogo(currentLogoFilename);
|
2024-03-29 18:56:32 +01:00
|
|
|
const { success, error } = await SystemSettings._updateSettings({
|
2023-10-23 22:10:34 +02:00
|
|
|
logo_filename: LOGO_FILENAME,
|
2023-08-15 00:22:55 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
return response.status(success ? 200 : 500).json({
|
|
|
|
message: success
|
|
|
|
? "Logo removed successfully."
|
|
|
|
: error || "Failed to update with new logo.",
|
|
|
|
});
|
|
|
|
} catch (error) {
|
|
|
|
console.error("Error processing the logo removal:", error);
|
|
|
|
response.status(500).json({ message: "Error removing the logo." });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
2023-08-17 02:30:46 +02:00
|
|
|
|
2024-01-22 23:14:01 +01:00
|
|
|
app.get(
|
|
|
|
"/system/welcome-messages",
|
|
|
|
[validatedRequest, flexUserRoleValid([ROLES.all])],
|
|
|
|
async function (_, response) {
|
|
|
|
try {
|
|
|
|
const welcomeMessages = await WelcomeMessages.getMessages();
|
|
|
|
response.status(200).json({ success: true, welcomeMessages });
|
|
|
|
} catch (error) {
|
|
|
|
console.error("Error fetching welcome messages:", error);
|
|
|
|
response
|
|
|
|
.status(500)
|
|
|
|
.json({ success: false, message: "Internal server error" });
|
|
|
|
}
|
2023-08-17 02:30:46 +02:00
|
|
|
}
|
2024-01-22 23:14:01 +01:00
|
|
|
);
|
2023-08-17 02:30:46 +02:00
|
|
|
|
|
|
|
app.post(
|
|
|
|
"/system/set-welcome-messages",
|
2024-01-22 23:14:01 +01:00
|
|
|
[validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
|
2023-08-17 02:30:46 +02:00
|
|
|
async (request, response) => {
|
|
|
|
try {
|
|
|
|
const { messages = [] } = reqBody(request);
|
|
|
|
if (!Array.isArray(messages)) {
|
|
|
|
return response.status(400).json({
|
|
|
|
success: false,
|
|
|
|
message: "Invalid message format. Expected an array of messages.",
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
await WelcomeMessages.saveAll(messages);
|
|
|
|
return response.status(200).json({
|
|
|
|
success: true,
|
|
|
|
message: "Welcome messages saved successfully.",
|
|
|
|
});
|
|
|
|
} catch (error) {
|
|
|
|
console.error("Error processing the welcome messages:", error);
|
|
|
|
response.status(500).json({
|
|
|
|
success: true,
|
|
|
|
message: "Error saving the welcome messages.",
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
2023-08-24 04:15:07 +02:00
|
|
|
|
2023-10-23 22:10:34 +02:00
|
|
|
app.get("/system/api-keys", [validatedRequest], async (_, response) => {
|
2023-08-24 04:15:07 +02:00
|
|
|
try {
|
|
|
|
if (response.locals.multiUserMode) {
|
|
|
|
return response.sendStatus(401).end();
|
|
|
|
}
|
|
|
|
|
2023-10-23 22:10:34 +02:00
|
|
|
const apiKeys = await ApiKey.where({});
|
2023-08-24 04:15:07 +02:00
|
|
|
return response.status(200).json({
|
2023-10-23 22:10:34 +02:00
|
|
|
apiKeys,
|
2023-08-24 04:15:07 +02:00
|
|
|
error: null,
|
|
|
|
});
|
|
|
|
} catch (error) {
|
|
|
|
console.error(error);
|
|
|
|
response.status(500).json({
|
|
|
|
apiKey: null,
|
|
|
|
error: "Could not find an API Key.",
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
app.post(
|
|
|
|
"/system/generate-api-key",
|
|
|
|
[validatedRequest],
|
|
|
|
async (_, response) => {
|
|
|
|
try {
|
|
|
|
if (response.locals.multiUserMode) {
|
|
|
|
return response.sendStatus(401).end();
|
|
|
|
}
|
|
|
|
|
|
|
|
const { apiKey, error } = await ApiKey.create();
|
[FEAT] Automated audit logging (#667)
* WIP event logging - new table for events and new settings view for viewing
* WIP add logging
* UI for log rows
* rename files to Logging to prevent getting gitignore
* add metadata for all logging events and colored badges in logs page
* remove unneeded comment
* cleanup namespace for logging
* clean up backend calls
* update logging to show to => from settings changes
* add logging for invitations, created, deleted, and accepted
* add logging for user created, updated, suspended, or removed
* add logging for workspace deleted
* add logging for chat logs exported
* add logging for API keys, LLM, embedder, vector db, embed chat, and reset button
* modify event logs
* update to event log types
* simplify rendering of event badges
---------
Co-authored-by: timothycarambat <rambat1010@gmail.com>
2024-02-07 00:21:40 +01:00
|
|
|
await Telemetry.sendTelemetry("api_key_created");
|
|
|
|
await EventLogs.logEvent(
|
|
|
|
"api_key_created",
|
|
|
|
{},
|
|
|
|
response?.locals?.user?.id
|
|
|
|
);
|
2023-08-24 04:15:07 +02:00
|
|
|
return response.status(200).json({
|
|
|
|
apiKey,
|
|
|
|
error,
|
|
|
|
});
|
|
|
|
} catch (error) {
|
|
|
|
console.error(error);
|
|
|
|
response.status(500).json({
|
|
|
|
apiKey: null,
|
|
|
|
error: "Error generating api key.",
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
app.delete("/system/api-key", [validatedRequest], async (_, response) => {
|
|
|
|
try {
|
|
|
|
if (response.locals.multiUserMode) {
|
|
|
|
return response.sendStatus(401).end();
|
|
|
|
}
|
|
|
|
|
|
|
|
await ApiKey.delete();
|
[FEAT] Automated audit logging (#667)
* WIP event logging - new table for events and new settings view for viewing
* WIP add logging
* UI for log rows
* rename files to Logging to prevent getting gitignore
* add metadata for all logging events and colored badges in logs page
* remove unneeded comment
* cleanup namespace for logging
* clean up backend calls
* update logging to show to => from settings changes
* add logging for invitations, created, deleted, and accepted
* add logging for user created, updated, suspended, or removed
* add logging for workspace deleted
* add logging for chat logs exported
* add logging for API keys, LLM, embedder, vector db, embed chat, and reset button
* modify event logs
* update to event log types
* simplify rendering of event badges
---------
Co-authored-by: timothycarambat <rambat1010@gmail.com>
2024-02-07 00:21:40 +01:00
|
|
|
await EventLogs.logEvent(
|
|
|
|
"api_key_deleted",
|
|
|
|
{ deletedBy: response.locals?.user?.username },
|
|
|
|
response?.locals?.user?.id
|
|
|
|
);
|
2023-08-24 04:15:07 +02:00
|
|
|
return response.status(200).end();
|
|
|
|
} catch (error) {
|
|
|
|
console.error(error);
|
|
|
|
response.status(500).end();
|
|
|
|
}
|
|
|
|
});
|
2023-10-31 19:38:28 +01:00
|
|
|
|
|
|
|
app.post(
|
|
|
|
"/system/custom-models",
|
|
|
|
[validatedRequest],
|
|
|
|
async (request, response) => {
|
|
|
|
try {
|
2023-11-14 21:31:44 +01:00
|
|
|
const { provider, apiKey = null, basePath = null } = reqBody(request);
|
|
|
|
const { models, error } = await getCustomModels(
|
|
|
|
provider,
|
|
|
|
apiKey,
|
|
|
|
basePath
|
|
|
|
);
|
2023-10-31 19:38:28 +01:00
|
|
|
return response.status(200).json({
|
|
|
|
models,
|
|
|
|
error,
|
|
|
|
});
|
|
|
|
} catch (error) {
|
|
|
|
console.error(error);
|
|
|
|
response.status(500).end();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
2023-11-09 02:36:54 +01:00
|
|
|
|
[FEAT] Automated audit logging (#667)
* WIP event logging - new table for events and new settings view for viewing
* WIP add logging
* UI for log rows
* rename files to Logging to prevent getting gitignore
* add metadata for all logging events and colored badges in logs page
* remove unneeded comment
* cleanup namespace for logging
* clean up backend calls
* update logging to show to => from settings changes
* add logging for invitations, created, deleted, and accepted
* add logging for user created, updated, suspended, or removed
* add logging for workspace deleted
* add logging for chat logs exported
* add logging for API keys, LLM, embedder, vector db, embed chat, and reset button
* modify event logs
* update to event log types
* simplify rendering of event badges
---------
Co-authored-by: timothycarambat <rambat1010@gmail.com>
2024-02-07 00:21:40 +01:00
|
|
|
app.post(
|
|
|
|
"/system/event-logs",
|
|
|
|
[validatedRequest, flexUserRoleValid([ROLES.admin])],
|
|
|
|
async (request, response) => {
|
|
|
|
try {
|
2024-05-02 01:13:20 +02:00
|
|
|
const { offset = 0, limit = 10 } = reqBody(request);
|
[FEAT] Automated audit logging (#667)
* WIP event logging - new table for events and new settings view for viewing
* WIP add logging
* UI for log rows
* rename files to Logging to prevent getting gitignore
* add metadata for all logging events and colored badges in logs page
* remove unneeded comment
* cleanup namespace for logging
* clean up backend calls
* update logging to show to => from settings changes
* add logging for invitations, created, deleted, and accepted
* add logging for user created, updated, suspended, or removed
* add logging for workspace deleted
* add logging for chat logs exported
* add logging for API keys, LLM, embedder, vector db, embed chat, and reset button
* modify event logs
* update to event log types
* simplify rendering of event badges
---------
Co-authored-by: timothycarambat <rambat1010@gmail.com>
2024-02-07 00:21:40 +01:00
|
|
|
const logs = await EventLogs.whereWithData({}, limit, offset * limit, {
|
|
|
|
id: "desc",
|
|
|
|
});
|
|
|
|
const totalLogs = await EventLogs.count();
|
|
|
|
const hasPages = totalLogs > (offset + 1) * limit;
|
|
|
|
|
|
|
|
response.status(200).json({ logs: logs, hasPages, totalLogs });
|
|
|
|
} catch (e) {
|
|
|
|
console.error(e);
|
|
|
|
response.sendStatus(500).end();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
app.delete(
|
|
|
|
"/system/event-logs",
|
|
|
|
[validatedRequest, flexUserRoleValid([ROLES.admin])],
|
|
|
|
async (_, response) => {
|
|
|
|
try {
|
|
|
|
await EventLogs.delete();
|
|
|
|
await EventLogs.logEvent(
|
|
|
|
"event_logs_cleared",
|
|
|
|
{},
|
|
|
|
response?.locals?.user?.id
|
|
|
|
);
|
|
|
|
response.json({ success: true });
|
|
|
|
} catch (e) {
|
|
|
|
console.error(e);
|
|
|
|
response.sendStatus(500).end();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
2023-11-09 02:36:54 +01:00
|
|
|
app.post(
|
|
|
|
"/system/workspace-chats",
|
2024-01-22 23:14:01 +01:00
|
|
|
[validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
|
2023-11-09 02:36:54 +01:00
|
|
|
async (request, response) => {
|
|
|
|
try {
|
|
|
|
const { offset = 0, limit = 20 } = reqBody(request);
|
|
|
|
const chats = await WorkspaceChats.whereWithData(
|
|
|
|
{},
|
|
|
|
limit,
|
|
|
|
offset * limit,
|
|
|
|
{ id: "desc" }
|
|
|
|
);
|
|
|
|
const totalChats = await WorkspaceChats.count();
|
|
|
|
const hasPages = totalChats > (offset + 1) * limit;
|
|
|
|
|
|
|
|
response.status(200).json({ chats: chats, hasPages, totalChats });
|
|
|
|
} catch (e) {
|
|
|
|
console.error(e);
|
|
|
|
response.sendStatus(500).end();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
app.delete(
|
|
|
|
"/system/workspace-chats/:id",
|
2024-01-22 23:14:01 +01:00
|
|
|
[validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
|
2023-11-09 02:36:54 +01:00
|
|
|
async (request, response) => {
|
|
|
|
try {
|
|
|
|
const { id } = request.params;
|
2024-05-22 23:27:56 +02:00
|
|
|
Number(id) === -1
|
|
|
|
? await WorkspaceChats.delete({}, true)
|
|
|
|
: await WorkspaceChats.delete({ id: Number(id) });
|
2024-02-06 22:11:44 +01:00
|
|
|
response.json({ success: true, error: null });
|
2023-11-09 02:36:54 +01:00
|
|
|
} catch (e) {
|
|
|
|
console.error(e);
|
|
|
|
response.sendStatus(500).end();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
app.get(
|
|
|
|
"/system/export-chats",
|
2024-01-22 23:14:01 +01:00
|
|
|
[validatedRequest, flexUserRoleValid([ROLES.manager, ROLES.admin])],
|
2024-01-19 02:59:51 +01:00
|
|
|
async (request, response) => {
|
2023-11-09 02:36:54 +01:00
|
|
|
try {
|
2024-01-19 02:59:51 +01:00
|
|
|
const { type = "jsonl" } = request.query;
|
2024-02-13 19:12:59 +01:00
|
|
|
const chats = await prepareWorkspaceChatsForExport(type);
|
2024-01-22 23:14:01 +01:00
|
|
|
const { contentType, data } = await exportChatsAsType(chats, type);
|
[FEAT] Automated audit logging (#667)
* WIP event logging - new table for events and new settings view for viewing
* WIP add logging
* UI for log rows
* rename files to Logging to prevent getting gitignore
* add metadata for all logging events and colored badges in logs page
* remove unneeded comment
* cleanup namespace for logging
* clean up backend calls
* update logging to show to => from settings changes
* add logging for invitations, created, deleted, and accepted
* add logging for user created, updated, suspended, or removed
* add logging for workspace deleted
* add logging for chat logs exported
* add logging for API keys, LLM, embedder, vector db, embed chat, and reset button
* modify event logs
* update to event log types
* simplify rendering of event badges
---------
Co-authored-by: timothycarambat <rambat1010@gmail.com>
2024-02-07 00:21:40 +01:00
|
|
|
await EventLogs.logEvent(
|
|
|
|
"exported_chats",
|
|
|
|
{
|
|
|
|
type,
|
2023-11-09 02:36:54 +01:00
|
|
|
},
|
[FEAT] Automated audit logging (#667)
* WIP event logging - new table for events and new settings view for viewing
* WIP add logging
* UI for log rows
* rename files to Logging to prevent getting gitignore
* add metadata for all logging events and colored badges in logs page
* remove unneeded comment
* cleanup namespace for logging
* clean up backend calls
* update logging to show to => from settings changes
* add logging for invitations, created, deleted, and accepted
* add logging for user created, updated, suspended, or removed
* add logging for workspace deleted
* add logging for chat logs exported
* add logging for API keys, LLM, embedder, vector db, embed chat, and reset button
* modify event logs
* update to event log types
* simplify rendering of event badges
---------
Co-authored-by: timothycarambat <rambat1010@gmail.com>
2024-02-07 00:21:40 +01:00
|
|
|
response.locals.user?.id
|
2023-11-09 02:36:54 +01:00
|
|
|
);
|
2024-01-22 23:14:01 +01:00
|
|
|
response.setHeader("Content-Type", contentType);
|
|
|
|
response.status(200).send(data);
|
2023-11-09 02:36:54 +01:00
|
|
|
} catch (e) {
|
|
|
|
console.error(e);
|
|
|
|
response.sendStatus(500).end();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
2023-12-07 23:11:51 +01:00
|
|
|
|
2024-01-22 23:14:01 +01:00
|
|
|
// Used for when a user in multi-user updates their own profile
|
|
|
|
// from the UI.
|
2023-12-07 23:11:51 +01:00
|
|
|
app.post("/system/user", [validatedRequest], async (request, response) => {
|
|
|
|
try {
|
|
|
|
const sessionUser = await userFromSession(request, response);
|
|
|
|
const { username, password } = reqBody(request);
|
|
|
|
const id = Number(sessionUser.id);
|
|
|
|
|
|
|
|
if (!id) {
|
|
|
|
response.status(400).json({ success: false, error: "Invalid user ID" });
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const updates = {};
|
|
|
|
if (username) {
|
2024-05-22 20:21:26 +02:00
|
|
|
updates.username = User.validations.username(String(username));
|
2023-12-07 23:11:51 +01:00
|
|
|
}
|
|
|
|
if (password) {
|
2024-04-01 21:11:46 +02:00
|
|
|
updates.password = String(password);
|
2023-12-07 23:11:51 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if (Object.keys(updates).length === 0) {
|
|
|
|
response
|
|
|
|
.status(400)
|
|
|
|
.json({ success: false, error: "No updates provided" });
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const { success, error } = await User.update(id, updates);
|
|
|
|
response.status(200).json({ success, error });
|
|
|
|
} catch (e) {
|
|
|
|
console.error(e);
|
|
|
|
response.sendStatus(500).end();
|
|
|
|
}
|
|
|
|
});
|
2024-05-10 21:35:33 +02:00
|
|
|
|
|
|
|
app.get(
|
|
|
|
"/system/slash-command-presets",
|
|
|
|
[validatedRequest, flexUserRoleValid([ROLES.all])],
|
|
|
|
async (request, response) => {
|
|
|
|
try {
|
|
|
|
const user = await userFromSession(request, response);
|
|
|
|
const userPresets = await SlashCommandPresets.getUserPresets(user?.id);
|
|
|
|
response.status(200).json({ presets: userPresets });
|
|
|
|
} catch (error) {
|
|
|
|
console.error("Error fetching slash command presets:", error);
|
|
|
|
response.status(500).json({ message: "Internal server error" });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
app.post(
|
|
|
|
"/system/slash-command-presets",
|
|
|
|
[validatedRequest, flexUserRoleValid([ROLES.all])],
|
|
|
|
async (request, response) => {
|
|
|
|
try {
|
|
|
|
const user = await userFromSession(request, response);
|
|
|
|
const { command, prompt, description } = reqBody(request);
|
|
|
|
const presetData = {
|
|
|
|
command: SlashCommandPresets.formatCommand(String(command)),
|
|
|
|
prompt: String(prompt),
|
|
|
|
description: String(description),
|
|
|
|
};
|
|
|
|
|
|
|
|
const preset = await SlashCommandPresets.create(user?.id, presetData);
|
|
|
|
if (!preset) {
|
|
|
|
return response
|
|
|
|
.status(500)
|
|
|
|
.json({ message: "Failed to create preset" });
|
|
|
|
}
|
|
|
|
response.status(201).json({ preset });
|
|
|
|
} catch (error) {
|
|
|
|
console.error("Error creating slash command preset:", error);
|
|
|
|
response.status(500).json({ message: "Internal server error" });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
app.post(
|
|
|
|
"/system/slash-command-presets/:slashCommandId",
|
|
|
|
[validatedRequest, flexUserRoleValid([ROLES.all])],
|
|
|
|
async (request, response) => {
|
|
|
|
try {
|
|
|
|
const user = await userFromSession(request, response);
|
|
|
|
const { slashCommandId } = request.params;
|
|
|
|
const { command, prompt, description } = reqBody(request);
|
|
|
|
|
|
|
|
// Valid user running owns the preset if user session is valid.
|
|
|
|
const ownsPreset = await SlashCommandPresets.get({
|
|
|
|
userId: user?.id ?? null,
|
|
|
|
id: Number(slashCommandId),
|
|
|
|
});
|
|
|
|
if (!ownsPreset)
|
|
|
|
return response.status(404).json({ message: "Preset not found" });
|
|
|
|
|
|
|
|
const updates = {
|
|
|
|
command: SlashCommandPresets.formatCommand(String(command)),
|
|
|
|
prompt: String(prompt),
|
|
|
|
description: String(description),
|
|
|
|
};
|
|
|
|
|
|
|
|
const preset = await SlashCommandPresets.update(
|
|
|
|
Number(slashCommandId),
|
|
|
|
updates
|
|
|
|
);
|
|
|
|
if (!preset) return response.sendStatus(422);
|
|
|
|
response.status(200).json({ preset: { ...ownsPreset, ...updates } });
|
|
|
|
} catch (error) {
|
|
|
|
console.error("Error updating slash command preset:", error);
|
|
|
|
response.status(500).json({ message: "Internal server error" });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
app.delete(
|
|
|
|
"/system/slash-command-presets/:slashCommandId",
|
|
|
|
[validatedRequest, flexUserRoleValid([ROLES.all])],
|
|
|
|
async (request, response) => {
|
|
|
|
try {
|
|
|
|
const { slashCommandId } = request.params;
|
|
|
|
const user = await userFromSession(request, response);
|
|
|
|
|
|
|
|
// Valid user running owns the preset if user session is valid.
|
|
|
|
const ownsPreset = await SlashCommandPresets.get({
|
|
|
|
userId: user?.id ?? null,
|
|
|
|
id: Number(slashCommandId),
|
|
|
|
});
|
|
|
|
if (!ownsPreset)
|
|
|
|
return response
|
|
|
|
.status(403)
|
|
|
|
.json({ message: "Failed to delete preset" });
|
|
|
|
|
|
|
|
await SlashCommandPresets.delete(Number(slashCommandId));
|
|
|
|
response.sendStatus(204);
|
|
|
|
} catch (error) {
|
|
|
|
console.error("Error deleting slash command preset:", error);
|
|
|
|
response.status(500).json({ message: "Internal server error" });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
2023-06-04 04:28:07 +02:00
|
|
|
}
|
|
|
|
|
2023-06-08 22:13:48 +02:00
|
|
|
module.exports = { systemEndpoints };
|