diff --git a/README.md b/README.md
index ceca8dbe..899b96f3 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,24 @@
-# 🤖 AnythingLLM: A full-stack personalized AI assistant
+
{!isMobile &&
}
diff --git a/frontend/src/pages/WorkspaceChat/index.jsx b/frontend/src/pages/WorkspaceChat/index.jsx
index 1eec9041..612b51a0 100644
--- a/frontend/src/pages/WorkspaceChat/index.jsx
+++ b/frontend/src/pages/WorkspaceChat/index.jsx
@@ -11,11 +11,11 @@ import PasswordModal, {
import { isMobile } from "react-device-detect";
export default function WorkspaceChat() {
- const { requiresAuth } = usePasswordModal();
+ const { requiresAuth, mode } = usePasswordModal();
if (requiresAuth === null || requiresAuth) {
return (
<>
- {requiresAuth &&
}
+ {requiresAuth &&
}
{!isMobile && }
diff --git a/frontend/src/utils/constants.js b/frontend/src/utils/constants.js
index 53995e6a..e96e6836 100644
--- a/frontend/src/utils/constants.js
+++ b/frontend/src/utils/constants.js
@@ -1,2 +1,5 @@
export const API_BASE =
import.meta.env.VITE_API_BASE || "http://localhost:3001/api";
+
+export const AUTH_USER = "anythingllm_user";
+export const AUTH_TOKEN = "anythingllm_authToken";
diff --git a/frontend/src/utils/paths.js b/frontend/src/utils/paths.js
index 03ff7b54..57eee6e1 100644
--- a/frontend/src/utils/paths.js
+++ b/frontend/src/utils/paths.js
@@ -27,4 +27,21 @@ export default {
exports: () => {
return `${API_BASE.replace("/api", "")}/system/data-exports`;
},
+ admin: {
+ system: () => {
+ return `/admin/system-preferences`;
+ },
+ users: () => {
+ return `/admin/users`;
+ },
+ invites: () => {
+ return `/admin/invites`;
+ },
+ workspaces: () => {
+ return `/admin/workspaces`;
+ },
+ chats: () => {
+ return "/admin/workspace-chats";
+ },
+ },
};
diff --git a/frontend/src/utils/request.js b/frontend/src/utils/request.js
index 4de7681e..271c97b2 100644
--- a/frontend/src/utils/request.js
+++ b/frontend/src/utils/request.js
@@ -1,8 +1,18 @@
+import { AUTH_TOKEN, AUTH_USER } from "./constants";
+
// Sets up the base headers for all authenticated requests so that we are able to prevent
// basic spoofing since a valid token is required and that cannot be spoofed
+export function userFromStorage() {
+ try {
+ const userString = window.localStorage.getItem(AUTH_USER);
+ if (!userString) return null;
+ return JSON.parse(userString);
+ } catch {}
+ return {};
+}
+
export function baseHeaders(providedToken = null) {
- const token =
- providedToken || window.localStorage.getItem("anythingllm_authtoken");
+ const token = providedToken || window.localStorage.getItem(AUTH_TOKEN);
return {
Authorization: token ? `Bearer ${token}` : null,
};
diff --git a/frontend/src/utils/session.js b/frontend/src/utils/session.js
new file mode 100644
index 00000000..27228e48
--- /dev/null
+++ b/frontend/src/utils/session.js
@@ -0,0 +1,15 @@
+import { API_BASE } from "./constants";
+import { baseHeaders } from "./request";
+
+// Checks current localstorage and validates the session based on that.
+export default async function validateSessionTokenForUser() {
+ const isValidSession = await fetch(`${API_BASE}/system/check-token`, {
+ method: "GET",
+ cache: "default",
+ headers: baseHeaders(),
+ })
+ .then((res) => res.status === 200)
+ .catch(() => false);
+
+ return isValidSession;
+}
diff --git a/package.json b/package.json
index 7df9953f..12d46fe3 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "anything-llm",
- "version": "0.0.1-beta",
- "description": "Turn anything into a chattable document through a simple UI",
+ "version": "0.1.0",
+ "description": "The best solution for turning private documents into a chat bot using off-the-shelf tools and commercially viable AI technologies.",
"main": "index.js",
"author": "Timothy Carambat (Mintplex Labs)",
"license": "MIT",
diff --git a/server/.env.example b/server/.env.example
index a74cec57..f671f78d 100644
--- a/server/.env.example
+++ b/server/.env.example
@@ -16,8 +16,9 @@ PINECONE_INDEX=
# Enable all below if you are using vector database: LanceDB.
# VECTOR_DB="lancedb"
+JWT_SECRET="my-random-string-for-seeding" # Please generate random string at least 12 chars long.
+
# CLOUD DEPLOYMENT VARIRABLES ONLY
# AUTH_TOKEN="hunter2" # This is the password to your application if remote hosting.
-# JWT_SECRET="my-random-string-for-seeding" # Only needed if AUTH_TOKEN is set. Please generate random string at least 12 chars long.
# STORAGE_DIR= # absolute filesystem path with no trailing slash
# NO_DEBUG="true"
\ No newline at end of file
diff --git a/server/endpoints/admin.js b/server/endpoints/admin.js
new file mode 100644
index 00000000..b58dc0c1
--- /dev/null
+++ b/server/endpoints/admin.js
@@ -0,0 +1,348 @@
+const { Document } = require("../models/documents");
+const { Invite } = require("../models/invite");
+const { SystemSettings } = require("../models/systemSettings");
+const { User } = require("../models/user");
+const { DocumentVectors } = require("../models/vectors");
+const { Workspace } = require("../models/workspace");
+const { WorkspaceChats } = require("../models/workspaceChats");
+const { getVectorDbClass } = require("../utils/helpers");
+const { userFromSession, reqBody } = require("../utils/http");
+const { validatedRequest } = require("../utils/middleware/validatedRequest");
+
+function adminEndpoints(app) {
+ if (!app) return;
+
+ app.get("/admin/users", [validatedRequest], async (request, response) => {
+ try {
+ const user = await userFromSession(request, response);
+ if (!user || user?.role !== "admin") {
+ response.sendStatus(401).end();
+ return;
+ }
+ const users = (await User.where()).map((user) => {
+ const { password, ...rest } = user;
+ return rest;
+ });
+ response.status(200).json({ users });
+ } catch (e) {
+ console.error(e);
+ response.sendStatus(500).end();
+ }
+ });
+
+ app.post(
+ "/admin/users/new",
+ [validatedRequest],
+ async (request, response) => {
+ try {
+ const user = await userFromSession(request, response);
+ if (!user || user?.role !== "admin") {
+ response.sendStatus(401).end();
+ return;
+ }
+
+ const newUserParams = reqBody(request);
+ const { user: newUser, error } = await User.create(newUserParams);
+ response.status(200).json({ user: newUser, error });
+ } catch (e) {
+ console.error(e);
+ response.sendStatus(500).end();
+ }
+ }
+ );
+
+ app.post("/admin/user/:id", [validatedRequest], async (request, response) => {
+ try {
+ const user = await userFromSession(request, response);
+ if (!user || user?.role !== "admin") {
+ response.sendStatus(401).end();
+ return;
+ }
+
+ const { id } = request.params;
+ const updates = reqBody(request);
+ const { success, error } = await User.update(id, updates);
+ response.status(200).json({ success, error });
+ } catch (e) {
+ console.error(e);
+ response.sendStatus(500).end();
+ }
+ });
+
+ app.delete(
+ "/admin/user/:id",
+ [validatedRequest],
+ async (request, response) => {
+ try {
+ const user = await userFromSession(request, response);
+ if (!user || user?.role !== "admin") {
+ response.sendStatus(401).end();
+ return;
+ }
+ const { id } = request.params;
+ await User.delete(`id = ${id}`);
+ response.status(200).json({ success: true, error: null });
+ } catch (e) {
+ console.error(e);
+ response.sendStatus(500).end();
+ }
+ }
+ );
+
+ app.get("/admin/invites", [validatedRequest], async (request, response) => {
+ try {
+ const user = await userFromSession(request, response);
+ if (!user || user?.role !== "admin") {
+ response.sendStatus(401).end();
+ return;
+ }
+
+ const invites = await Invite.whereWithUsers();
+ response.status(200).json({ invites });
+ } catch (e) {
+ console.error(e);
+ response.sendStatus(500).end();
+ }
+ });
+
+ app.get(
+ "/admin/invite/new",
+ [validatedRequest],
+ async (request, response) => {
+ try {
+ const user = await userFromSession(request, response);
+ if (!user || user?.role !== "admin") {
+ response.sendStatus(401).end();
+ return;
+ }
+
+ const { invite, error } = await Invite.create(user.id);
+ response.status(200).json({ invite, error });
+ } catch (e) {
+ console.error(e);
+ response.sendStatus(500).end();
+ }
+ }
+ );
+
+ app.delete(
+ "/admin/invite/:id",
+ [validatedRequest],
+ async (request, response) => {
+ try {
+ const user = await userFromSession(request, response);
+ if (!user || user?.role !== "admin") {
+ response.sendStatus(401).end();
+ return;
+ }
+
+ const { id } = request.params;
+ const { success, error } = await Invite.deactivate(id);
+ response.status(200).json({ success, error });
+ } catch (e) {
+ console.error(e);
+ response.sendStatus(500).end();
+ }
+ }
+ );
+
+ app.get(
+ "/admin/workspaces",
+ [validatedRequest],
+ async (request, response) => {
+ try {
+ const user = await userFromSession(request, response);
+ if (!user || user?.role !== "admin") {
+ response.sendStatus(401).end();
+ return;
+ }
+ const workspaces = await Workspace.whereWithUsers();
+ response.status(200).json({ workspaces });
+ } catch (e) {
+ console.error(e);
+ response.sendStatus(500).end();
+ }
+ }
+ );
+
+ app.post(
+ "/admin/workspaces/new",
+ [validatedRequest],
+ async (request, response) => {
+ try {
+ const user = await userFromSession(request, response);
+ if (!user || user?.role !== "admin") {
+ response.sendStatus(401).end();
+ return;
+ }
+ const { name } = reqBody(request);
+ const { workspace, message: error } = await Workspace.new(
+ name,
+ user.id
+ );
+ response.status(200).json({ workspace, error });
+ } catch (e) {
+ console.error(e);
+ response.sendStatus(500).end();
+ }
+ }
+ );
+
+ app.post(
+ "/admin/workspaces/:workspaceId/update-users",
+ [validatedRequest],
+ async (request, response) => {
+ try {
+ const user = await userFromSession(request, response);
+ if (!user || user?.role !== "admin") {
+ response.sendStatus(401).end();
+ return;
+ }
+
+ const { workspaceId } = request.params;
+ const { userIds } = reqBody(request);
+ const { success, error } = await Workspace.updateUsers(
+ workspaceId,
+ userIds
+ );
+ response.status(200).json({ success, error });
+ } catch (e) {
+ console.error(e);
+ response.sendStatus(500).end();
+ }
+ }
+ );
+
+ app.delete(
+ "/admin/workspaces/:id",
+ [validatedRequest],
+ async (request, response) => {
+ try {
+ const user = await userFromSession(request, response);
+ if (!user || user?.role !== "admin") {
+ response.sendStatus(401).end();
+ return;
+ }
+
+ const { id } = request.params;
+ const VectorDb = getVectorDbClass();
+ const workspace = Workspace.get(`id = ${id}`);
+ if (!workspace) {
+ response.sendStatus(404).end();
+ return;
+ }
+
+ await Workspace.delete(`id = ${id}`);
+ await DocumentVectors.deleteForWorkspace(id);
+ await Document.delete(`workspaceId = ${Number(id)}`);
+ await WorkspaceChats.delete(`workspaceId = ${Number(id)}`);
+ try {
+ await VectorDb["delete-namespace"]({ namespace: workspace.slug });
+ } catch (e) {
+ console.error(e.message);
+ }
+
+ response.status(200).json({ success, error });
+ } catch (e) {
+ console.error(e);
+ response.sendStatus(500).end();
+ }
+ }
+ );
+
+ app.post(
+ "/admin/workspace-chats",
+ [validatedRequest],
+ async (request, response) => {
+ try {
+ const user = await userFromSession(request, response);
+ if (!user || user?.role !== "admin") {
+ response.sendStatus(401).end();
+ return;
+ }
+ const { offset = 0 } = reqBody(request);
+ const chats = await WorkspaceChats.whereWithData(`id >= ${offset}`, 20);
+ const hasPages = (await WorkspaceChats.count()) > 20;
+ response.status(200).json({ chats: chats.reverse(), hasPages });
+ } catch (e) {
+ console.error(e);
+ response.sendStatus(500).end();
+ }
+ }
+ );
+
+ app.delete(
+ "/admin/workspace-chats/:id",
+ [validatedRequest],
+ async (request, response) => {
+ try {
+ const user = await userFromSession(request, response);
+ if (!user || user?.role !== "admin") {
+ response.sendStatus(401).end();
+ return;
+ }
+
+ const { id } = request.params;
+ await WorkspaceChats.delete(`id = ${id}`);
+ response.status(200).json({ success, error });
+ } catch (e) {
+ console.error(e);
+ response.sendStatus(500).end();
+ }
+ }
+ );
+
+ app.get(
+ "/admin/system-preferences",
+ [validatedRequest],
+ async (request, response) => {
+ try {
+ const user = await userFromSession(request, response);
+ if (!user || user?.role !== "admin") {
+ response.sendStatus(401).end();
+ return;
+ }
+
+ const settings = {
+ users_can_delete_workspaces:
+ (await SystemSettings.get(`label = 'users_can_delete_workspaces'`))
+ ?.value === "true",
+ limit_user_messages:
+ (await SystemSettings.get(`label = 'limit_user_messages'`))
+ ?.value === "true",
+ message_limit:
+ Number(
+ (await SystemSettings.get(`label = 'message_limit'`))?.value
+ ) || 10,
+ };
+ response.status(200).json({ settings });
+ } catch (e) {
+ console.error(e);
+ response.sendStatus(500).end();
+ }
+ }
+ );
+
+ app.post(
+ "/admin/system-preferences",
+ [validatedRequest],
+ async (request, response) => {
+ try {
+ const user = await userFromSession(request, response);
+ if (!user || user?.role !== "admin") {
+ response.sendStatus(401).end();
+ return;
+ }
+
+ const updates = reqBody(request);
+ await SystemSettings.updateSettings(updates);
+ response.status(200).json({ success: true, error: null });
+ } catch (e) {
+ console.error(e);
+ response.sendStatus(500).end();
+ }
+ }
+ );
+}
+
+module.exports = { adminEndpoints };
diff --git a/server/endpoints/chat.js b/server/endpoints/chat.js
index 7c303643..0a9544d5 100644
--- a/server/endpoints/chat.js
+++ b/server/endpoints/chat.js
@@ -1,34 +1,73 @@
const { v4: uuidv4 } = require("uuid");
-const { reqBody } = require("../utils/http");
+const { reqBody, userFromSession, multiUserMode } = require("../utils/http");
const { Workspace } = require("../models/workspace");
const { chatWithWorkspace } = require("../utils/chats");
+const { validatedRequest } = require("../utils/middleware/validatedRequest");
+const { WorkspaceChats } = require("../models/workspaceChats");
+const { SystemSettings } = require("../models/systemSettings");
function chatEndpoints(app) {
if (!app) return;
- app.post("/workspace/:slug/chat", async (request, response) => {
- try {
- const { slug } = request.params;
- const { message, mode = "query" } = reqBody(request);
- const workspace = await Workspace.get(`slug = '${slug}'`);
- if (!workspace) {
- response.sendStatus(400).end();
- return;
- }
+ app.post(
+ "/workspace/:slug/chat",
+ [validatedRequest],
+ async (request, response) => {
+ try {
+ const user = await userFromSession(request, response);
+ const { slug } = request.params;
+ const { message, mode = "query" } = reqBody(request);
+ const workspace = multiUserMode(response)
+ ? await Workspace.getWithUser(user, `slug = '${slug}'`)
+ : await Workspace.get(`slug = '${slug}'`);
- const result = await chatWithWorkspace(workspace, message, mode);
- response.status(200).json({ ...result });
- } catch (e) {
- response.status(500).json({
- id: uuidv4(),
- type: "abort",
- textResponse: null,
- sources: [],
- close: true,
- error: e.message,
- });
+ if (!workspace) {
+ response.sendStatus(400).end();
+ return;
+ }
+
+ if (multiUserMode(response) && user.role !== "admin") {
+ const limitMessages =
+ (await SystemSettings.get(`label = 'limit_user_messages'`))
+ ?.value === "true";
+
+ if (limitMessages) {
+ const systemLimit = Number(
+ (await SystemSettings.get(`label = 'message_limit'`))?.value
+ );
+ if (!!systemLimit) {
+ const currentChatCount = await WorkspaceChats.count(
+ `user_id = ${user.id} AND createdAt > datetime(CURRENT_TIMESTAMP, '-1 days')`
+ );
+ if (currentChatCount >= systemLimit) {
+ response.status(500).json({
+ id: uuidv4(),
+ type: "abort",
+ textResponse: null,
+ sources: [],
+ close: true,
+ error: `You have met your maximum 24 hour chat quota of ${systemLimit} chats set by the instance administrators. Try again later.`,
+ });
+ return;
+ }
+ }
+ }
+ }
+
+ const result = await chatWithWorkspace(workspace, message, mode, user);
+ response.status(200).json({ ...result });
+ } catch (e) {
+ response.status(500).json({
+ id: uuidv4(),
+ type: "abort",
+ textResponse: null,
+ sources: [],
+ close: true,
+ error: e.message,
+ });
+ }
}
- });
+ );
}
module.exports = { chatEndpoints };
diff --git a/server/endpoints/invite.js b/server/endpoints/invite.js
new file mode 100644
index 00000000..3f3b1452
--- /dev/null
+++ b/server/endpoints/invite.js
@@ -0,0 +1,63 @@
+const { Invite } = require("../models/invite");
+const { User } = require("../models/user");
+const { reqBody } = require("../utils/http");
+
+function inviteEndpoints(app) {
+ if (!app) return;
+
+ app.get("/invite/:code", async (request, response) => {
+ try {
+ const { code } = request.params;
+ const invite = await Invite.get(`code = '${code}'`);
+ if (!invite) {
+ response.status(200).json({ invite: null, error: "Invite not found." });
+ return;
+ }
+
+ if (invite.status !== "pending") {
+ response
+ .status(200)
+ .json({ invite: null, error: "Invite is no longer valid." });
+ return;
+ }
+
+ response
+ .status(200)
+ .json({ invite: { code, status: invite.status }, error: null });
+ } catch (e) {
+ console.error(e);
+ response.sendStatus(500).end();
+ }
+ });
+
+ app.post("/invite/:code", async (request, response) => {
+ try {
+ const { code } = request.params;
+ const userParams = reqBody(request);
+ const invite = await Invite.get(`code = '${code}'`);
+ if (!invite || invite.status !== "pending") {
+ response
+ .status(200)
+ .json({ success: false, error: "Invite not found or is invalid." });
+ return;
+ }
+
+ const { user, error } = await User.create(userParams);
+ if (!user) {
+ console.error("Accepting invite:", error);
+ response
+ .status(200)
+ .json({ success: false, error: "Could not create user." });
+ return;
+ }
+
+ await Invite.markClaimed(invite.id, user);
+ response.status(200).json({ success: true, error: null });
+ } catch (e) {
+ console.error(e);
+ response.sendStatus(500).end();
+ }
+ });
+}
+
+module.exports = { inviteEndpoints };
diff --git a/server/endpoints/system.js b/server/endpoints/system.js
index a39ef3a3..e78af6ad 100644
--- a/server/endpoints/system.js
+++ b/server/endpoints/system.js
@@ -11,9 +11,17 @@ const {
const { purgeDocument } = require("../utils/files/purgeDocument");
const { getVectorDbClass } = require("../utils/helpers");
const { updateENV } = require("../utils/helpers/updateENV");
-const { reqBody, makeJWT } = require("../utils/http");
+const {
+ reqBody,
+ makeJWT,
+ userFromSession,
+ multiUserMode,
+} = require("../utils/http");
const { setupDataImports } = require("../utils/files/multer");
const { v4 } = require("uuid");
+const { SystemSettings } = require("../models/systemSettings");
+const { User } = require("../models/user");
+const { validatedRequest } = require("../utils/middleware/validatedRequest");
const { handleImports } = setupDataImports();
function systemEndpoints(app) {
@@ -28,7 +36,7 @@ function systemEndpoints(app) {
response.sendStatus(200);
});
- app.get("/setup-complete", (_, response) => {
+ app.get("/setup-complete", async (_, response) => {
try {
const vectorDB = process.env.VECTOR_DB || "pinecone";
const results = {
@@ -40,6 +48,7 @@ function systemEndpoints(app) {
AuthToken: !!process.env.AUTH_TOKEN,
JWTSecret: !!process.env.JWT_SECRET,
StorageDir: process.env.STORAGE_DIR,
+ MultiUserMode: await SystemSettings.isMultiUserMode(),
...(vectorDB === "pinecone"
? {
PineConeEnvironment: process.env.PINECONE_ENVIRONMENT,
@@ -60,35 +69,101 @@ function systemEndpoints(app) {
}
});
- app.get("/system/check-token", (_, response) => {
- response.sendStatus(200).end();
- });
+ 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;
+ }
- app.post("/request-token", (request, response) => {
+ response.sendStatus(200).end();
+ return;
+ }
+
+ response.sendStatus(200).end();
+ } catch (e) {
+ console.log(e.message, e);
+ response.sendStatus(500).end();
+ }
+ }
+ );
+
+ app.post("/request-token", async (request, response) => {
try {
- const { password } = reqBody(request);
- if (password !== process.env.AUTH_TOKEN) {
- response.status(402).json({
- valid: false,
- token: null,
- message: "Invalid password provided",
+ if (await SystemSettings.isMultiUserMode()) {
+ const { username, password } = reqBody(request);
+ const existingUser = await User.get(`username = '${username}'`);
+
+ if (!existingUser) {
+ response.status(200).json({
+ user: null,
+ valid: false,
+ token: null,
+ message: "[001] Invalid login credentials.",
+ });
+ return;
+ }
+
+ const bcrypt = require("bcrypt");
+ if (!bcrypt.compareSync(password, existingUser.password)) {
+ response.status(200).json({
+ user: null,
+ valid: false,
+ token: null,
+ message: "[002] Invalid login credentials.",
+ });
+ return;
+ }
+
+ if (existingUser.suspended) {
+ response.status(200).json({
+ user: null,
+ valid: false,
+ token: null,
+ message: "[004] Account suspended by admin.",
+ });
+ return;
+ }
+
+ response.status(200).json({
+ valid: true,
+ user: existingUser,
+ token: makeJWT(
+ { id: existingUser.id, username: existingUser.username },
+ "30d"
+ ),
+ message: null,
});
return;
- }
+ } else {
+ const { password } = reqBody(request);
+ if (password !== process.env.AUTH_TOKEN) {
+ response.status(401).json({
+ valid: false,
+ token: null,
+ message: "[003] Invalid password provided",
+ });
+ return;
+ }
- response.status(200).json({
- valid: true,
- token: makeJWT({ p: password }, "30d"),
- message: null,
- });
- return;
+ response.status(200).json({
+ valid: true,
+ token: makeJWT({ p: password }, "30d"),
+ message: null,
+ });
+ }
} catch (e) {
console.log(e.message, e);
response.sendStatus(500).end();
}
});
- app.get("/system/system-vectors", async (_, response) => {
+ app.get("/system/system-vectors", [validatedRequest], async (_, response) => {
try {
const VectorDb = getVectorDbClass();
const vectorCount = await VectorDb.totalIndicies();
@@ -99,18 +174,22 @@ function systemEndpoints(app) {
}
});
- app.delete("/system/remove-document", async (request, response) => {
- try {
- const { name, meta } = reqBody(request);
- await purgeDocument(name, meta);
- response.sendStatus(200).end();
- } catch (e) {
- console.log(e.message, e);
- response.sendStatus(500).end();
+ app.delete(
+ "/system/remove-document",
+ [validatedRequest],
+ async (request, response) => {
+ try {
+ const { name, meta } = reqBody(request);
+ await purgeDocument(name, meta);
+ response.sendStatus(200).end();
+ } catch (e) {
+ console.log(e.message, e);
+ response.sendStatus(500).end();
+ }
}
- });
+ );
- app.get("/system/local-files", async (_, response) => {
+ app.get("/system/local-files", [validatedRequest], async (_, response) => {
try {
const localFiles = await viewLocalFiles();
response.status(200).json({ localFiles });
@@ -120,57 +199,109 @@ function systemEndpoints(app) {
}
});
- app.get("/system/document-processing-status", async (_, response) => {
- try {
- const online = await checkPythonAppAlive();
- response.sendStatus(online ? 200 : 503);
- } catch (e) {
- console.log(e.message, e);
- response.sendStatus(500).end();
- }
- });
-
- app.get("/system/accepted-document-types", async (_, response) => {
- try {
- const types = await acceptedFileTypes();
- if (!types) {
- response.sendStatus(404).end();
- return;
+ app.get(
+ "/system/document-processing-status",
+ [validatedRequest],
+ async (_, response) => {
+ try {
+ const online = await checkPythonAppAlive();
+ response.sendStatus(online ? 200 : 503);
+ } catch (e) {
+ console.log(e.message, e);
+ response.sendStatus(500).end();
}
-
- response.status(200).json({ types });
- } catch (e) {
- console.log(e.message, e);
- response.sendStatus(500).end();
}
- });
+ );
- app.post("/system/update-env", async (request, response) => {
- try {
- const body = reqBody(request);
- const { newValues, error } = updateENV(body);
- response.status(200).json({ newValues, error });
- } catch (e) {
- console.log(e.message, e);
- response.sendStatus(500).end();
+ app.get(
+ "/system/accepted-document-types",
+ [validatedRequest],
+ async (_, response) => {
+ try {
+ const types = await acceptedFileTypes();
+ if (!types) {
+ response.sendStatus(404).end();
+ return;
+ }
+
+ response.status(200).json({ types });
+ } catch (e) {
+ console.log(e.message, e);
+ response.sendStatus(500).end();
+ }
}
- });
+ );
- app.post("/system/update-password", async (request, response) => {
- try {
- const { usePassword, newPassword } = reqBody(request);
- const { error } = updateENV({
- AuthToken: usePassword ? newPassword : "",
- JWTSecret: usePassword ? v4() : "",
- });
- response.status(200).json({ success: !error, error });
- } catch (e) {
- console.log(e.message, e);
- response.sendStatus(500).end();
+ app.post(
+ "/system/update-env",
+ [validatedRequest],
+ async (request, response) => {
+ try {
+ const body = reqBody(request);
+ const { newValues, error } = updateENV(body);
+ response.status(200).json({ newValues, error });
+ } catch (e) {
+ console.log(e.message, e);
+ response.sendStatus(500).end();
+ }
}
- });
+ );
- app.get("/system/data-export", async (_, response) => {
+ app.post(
+ "/system/update-password",
+ [validatedRequest],
+ async (request, response) => {
+ try {
+ const { usePassword, newPassword } = reqBody(request);
+ const { error } = updateENV({
+ AuthToken: usePassword ? newPassword : "",
+ JWTSecret: usePassword ? v4() : "",
+ });
+ response.status(200).json({ success: !error, error });
+ } catch (e) {
+ console.log(e.message, e);
+ response.sendStatus(500).end();
+ }
+ }
+ );
+
+ app.post(
+ "/system/enable-multi-user",
+ [validatedRequest],
+ async (request, response) => {
+ try {
+ const { username, password } = reqBody(request);
+ const multiUserModeEnabled = await SystemSettings.isMultiUserMode();
+ if (multiUserModeEnabled) {
+ response.status(200).json({
+ success: false,
+ error: "Multi-user mode is already enabled.",
+ });
+ return;
+ }
+
+ const { user, error } = await User.create({
+ username,
+ password,
+ role: "admin",
+ });
+ await SystemSettings.updateSettings({
+ multi_user_mode: true,
+ users_can_delete_workspaces: false,
+ limit_user_messages: false,
+ message_limit: 25,
+ });
+ process.env.AUTH_TOKEN = null;
+ process.env.JWT_SECRET = process.env.JWT_SECRET ?? v4(); // Make sure JWT_SECRET is set for JWT issuance.
+ response.status(200).json({ success: !!user, error });
+ } catch (e) {
+ console.log(e.message, e);
+ response.sendStatus(500).end();
+ }
+ }
+ );
+
+ app.get("/system/data-export", [validatedRequest], async (_, response) => {
try {
const { filename, error } = await exportData();
response.status(200).json({ filename, error });
@@ -180,18 +311,22 @@ function systemEndpoints(app) {
}
});
- app.get("/system/data-exports/:filename", (request, response) => {
- const filePath =
- __dirname + "/../storage/exports/" + request.params.filename;
- response.download(filePath, request.params.filename, (err) => {
- if (err) {
- response.send({
- error: err,
- msg: "Problem downloading the file",
- });
- }
- });
- });
+ app.get(
+ "/system/data-exports/:filename",
+ [validatedRequest],
+ (request, response) => {
+ const filePath =
+ __dirname + "/../storage/exports/" + request.params.filename;
+ response.download(filePath, request.params.filename, (err) => {
+ if (err) {
+ response.send({
+ error: err,
+ msg: "Problem downloading the file",
+ });
+ }
+ });
+ }
+ );
app.post(
"/system/data-import",
diff --git a/server/endpoints/workspaces.js b/server/endpoints/workspaces.js
index 3473c6d3..f103a1c5 100644
--- a/server/endpoints/workspaces.js
+++ b/server/endpoints/workspaces.js
@@ -1,4 +1,4 @@
-const { reqBody } = require("../utils/http");
+const { reqBody, multiUserMode, userFromSession } = require("../utils/http");
const { Workspace } = require("../models/workspace");
const { Document } = require("../models/documents");
const { DocumentVectors } = require("../models/vectors");
@@ -13,15 +13,18 @@ const {
checkPythonAppAlive,
processDocument,
} = require("../utils/files/documentProcessor");
+const { validatedRequest } = require("../utils/middleware/validatedRequest");
+const { SystemSettings } = require("../models/systemSettings");
const { handleUploads } = setupMulter();
function workspaceEndpoints(app) {
if (!app) return;
- app.post("/workspace/new", async (request, response) => {
+ app.post("/workspace/new", [validatedRequest], async (request, response) => {
try {
+ const user = await userFromSession(request, response);
const { name = null } = reqBody(request);
- const { workspace, message } = await Workspace.new(name);
+ const { workspace, message } = await Workspace.new(name, user?.id);
response.status(200).json({ workspace, message });
} catch (e) {
console.log(e.message, e);
@@ -29,27 +32,34 @@ function workspaceEndpoints(app) {
}
});
- app.post("/workspace/:slug/update", async (request, response) => {
- try {
- const { slug = null } = request.params;
- const data = reqBody(request);
- const currWorkspace = await Workspace.get(`slug = '${slug}'`);
+ app.post(
+ "/workspace/:slug/update",
+ [validatedRequest],
+ async (request, response) => {
+ try {
+ const user = await userFromSession(request, response);
+ const { slug = null } = request.params;
+ const data = reqBody(request);
+ const currWorkspace = multiUserMode(response)
+ ? await Workspace.getWithUser(user, `slug = '${slug}'`)
+ : await Workspace.get(`slug = '${slug}'`);
- if (!currWorkspace) {
- response.sendStatus(400).end();
- return;
+ if (!currWorkspace) {
+ response.sendStatus(400).end();
+ return;
+ }
+
+ const { workspace, message } = await Workspace.update(
+ currWorkspace.id,
+ data
+ );
+ response.status(200).json({ workspace, message });
+ } catch (e) {
+ console.log(e.message, e);
+ response.sendStatus(500).end();
}
-
- const { workspace, message } = await Workspace.update(
- currWorkspace.id,
- data
- );
- response.status(200).json({ workspace, message });
- } catch (e) {
- console.log(e.message, e);
- response.sendStatus(500).end();
}
- });
+ );
app.post(
"/workspace/:slug/upload",
@@ -81,57 +91,85 @@ function workspaceEndpoints(app) {
}
);
- app.post("/workspace/:slug/update-embeddings", async (request, response) => {
- try {
- const { slug = null } = request.params;
- const { adds = [], deletes = [] } = reqBody(request);
- const currWorkspace = await Workspace.get(`slug = '${slug}'`);
-
- if (!currWorkspace) {
- response.sendStatus(400).end();
- return;
- }
-
- await Document.removeDocuments(currWorkspace, deletes);
- await Document.addDocuments(currWorkspace, adds);
- const updatedWorkspace = await Workspace.get(`slug = '${slug}'`);
- response.status(200).json({ workspace: updatedWorkspace });
- } catch (e) {
- console.log(e.message, e);
- response.sendStatus(500).end();
- }
- });
-
- app.delete("/workspace/:slug", async (request, response) => {
- try {
- const VectorDb = getVectorDbClass();
- const { slug = "" } = request.params;
- const workspace = await Workspace.get(`slug = '${slug}'`);
-
- if (!workspace) {
- response.sendStatus(400).end();
- return;
- }
-
- await Workspace.delete(`slug = '${slug.toLowerCase()}'`);
- await DocumentVectors.deleteForWorkspace(workspace.id);
- await Document.delete(`workspaceId = ${Number(workspace.id)}`);
- await WorkspaceChats.delete(`workspaceId = ${Number(workspace.id)}`);
+ app.post(
+ "/workspace/:slug/update-embeddings",
+ [validatedRequest],
+ async (request, response) => {
try {
- await VectorDb["delete-namespace"]({ namespace: slug });
- } catch (e) {
- console.error(e.message);
- }
- response.sendStatus(200).end();
- } catch (e) {
- console.log(e.message, e);
- response.sendStatus(500).end();
- }
- });
+ const user = await userFromSession(request, response);
+ const { slug = null } = request.params;
+ const { adds = [], deletes = [] } = reqBody(request);
+ const currWorkspace = multiUserMode(response)
+ ? await Workspace.getWithUser(user, `slug = '${slug}'`)
+ : await Workspace.get(`slug = '${slug}'`);
- app.get("/workspaces", async (_, response) => {
+ if (!currWorkspace) {
+ response.sendStatus(400).end();
+ return;
+ }
+
+ await Document.removeDocuments(currWorkspace, deletes);
+ await Document.addDocuments(currWorkspace, adds);
+ const updatedWorkspace = await Workspace.get(`slug = '${slug}'`);
+ response.status(200).json({ workspace: updatedWorkspace });
+ } catch (e) {
+ console.log(e.message, e);
+ response.sendStatus(500).end();
+ }
+ }
+ );
+
+ app.delete(
+ "/workspace/:slug",
+ [validatedRequest],
+ async (request, response) => {
+ try {
+ const { slug = "" } = request.params;
+ const user = await userFromSession(request, response);
+ const VectorDb = getVectorDbClass();
+ const workspace = multiUserMode(response)
+ ? await Workspace.getWithUser(user, `slug = '${slug}'`)
+ : await Workspace.get(`slug = '${slug}'`);
+
+ if (!workspace) {
+ response.sendStatus(400).end();
+ return;
+ }
+
+ if (multiUserMode(response) && user.role !== "admin") {
+ const canDelete =
+ (await SystemSettings.get(`label = 'users_can_delete_workspaces'`))
+ ?.value === "true";
+ if (!canDelete) {
+ response.sendStatus(500).end();
+ return;
+ }
+ }
+
+ await Workspace.delete(`slug = '${slug.toLowerCase()}'`);
+ await DocumentVectors.deleteForWorkspace(workspace.id);
+ await Document.delete(`workspaceId = ${Number(workspace.id)}`);
+ await WorkspaceChats.delete(`workspaceId = ${Number(workspace.id)}`);
+ try {
+ await VectorDb["delete-namespace"]({ namespace: slug });
+ } catch (e) {
+ console.error(e.message);
+ }
+ response.sendStatus(200).end();
+ } catch (e) {
+ console.log(e.message, e);
+ response.sendStatus(500).end();
+ }
+ }
+ );
+
+ app.get("/workspaces", [validatedRequest], async (request, response) => {
try {
- const workspaces = await Workspace.where();
+ const user = await userFromSession(request, response);
+ const workspaces = multiUserMode(response)
+ ? await Workspace.whereWithUser(user)
+ : await Workspace.where();
+
response.status(200).json({ workspaces });
} catch (e) {
console.log(e.message, e);
@@ -139,10 +177,14 @@ function workspaceEndpoints(app) {
}
});
- app.get("/workspace/:slug", async (request, response) => {
+ app.get("/workspace/:slug", [validatedRequest], async (request, response) => {
try {
const { slug } = request.params;
- const workspace = await Workspace.get(`slug = '${slug}'`);
+ const user = await userFromSession(request, response);
+ const workspace = multiUserMode(response)
+ ? await Workspace.getWithUser(user, `slug = '${slug}'`)
+ : await Workspace.get(`slug = '${slug}'`);
+
response.status(200).json({ workspace });
} catch (e) {
console.log(e.message, e);
@@ -150,22 +192,33 @@ function workspaceEndpoints(app) {
}
});
- app.get("/workspace/:slug/chats", async (request, response) => {
- try {
- const { slug } = request.params;
- const workspace = await Workspace.get(`slug = '${slug}'`);
- if (!workspace) {
- response.sendStatus(400).end();
- return;
- }
+ app.get(
+ "/workspace/:slug/chats",
+ [validatedRequest],
+ async (request, response) => {
+ try {
+ const { slug } = request.params;
+ const user = await userFromSession(request, response);
+ const workspace = multiUserMode(response)
+ ? await Workspace.getWithUser(user, `slug = '${slug}'`)
+ : await Workspace.get(`slug = '${slug}'`);
- const history = await WorkspaceChats.forWorkspace(workspace.id);
- response.status(200).json({ history: convertToChatHistory(history) });
- } catch (e) {
- console.log(e.message, e);
- response.sendStatus(500).end();
+ if (!workspace) {
+ response.sendStatus(400).end();
+ return;
+ }
+
+ const history = multiUserMode(response)
+ ? await WorkspaceChats.forWorkspaceByUser(workspace.id, user.id)
+ : await WorkspaceChats.forWorkspace(workspace.id);
+
+ response.status(200).json({ history: convertToChatHistory(history) });
+ } catch (e) {
+ console.log(e.message, e);
+ response.sendStatus(500).end();
+ }
}
- });
+ );
}
module.exports = { workspaceEndpoints };
diff --git a/server/index.js b/server/index.js
index 83ff994d..0164a03e 100644
--- a/server/index.js
+++ b/server/index.js
@@ -7,13 +7,14 @@ const bodyParser = require("body-parser");
const serveIndex = require("serve-index");
const cors = require("cors");
const path = require("path");
-const { validatedRequest } = require("./utils/middleware/validatedRequest");
const { reqBody } = require("./utils/http");
const { systemEndpoints } = require("./endpoints/system");
const { workspaceEndpoints } = require("./endpoints/workspaces");
const { chatEndpoints } = require("./endpoints/chat");
const { getVectorDbClass } = require("./utils/helpers");
const { validateTablePragmas } = require("./utils/database");
+const { adminEndpoints } = require("./endpoints/admin");
+const { inviteEndpoints } = require("./endpoints/invite");
const app = express();
const apiRouter = express.Router();
@@ -26,12 +27,12 @@ app.use(
})
);
-apiRouter.use("/system/*", validatedRequest);
+app.use("/api", apiRouter);
systemEndpoints(apiRouter);
-
-apiRouter.use("/workspace/*", validatedRequest);
workspaceEndpoints(apiRouter);
chatEndpoints(apiRouter);
+adminEndpoints(apiRouter);
+inviteEndpoints(apiRouter);
apiRouter.post("/v/:command", async (request, response) => {
try {
@@ -61,8 +62,6 @@ apiRouter.post("/v/:command", async (request, response) => {
}
});
-app.use("/api", apiRouter);
-
if (process.env.NODE_ENV !== "development") {
app.use(
express.static(path.resolve(__dirname, "public"), { extensions: ["js"] })
diff --git a/server/models/documents.js b/server/models/documents.js
index 777bd717..0de83bcd 100644
--- a/server/models/documents.js
+++ b/server/models/documents.js
@@ -35,7 +35,7 @@ const Document = {
});
await db.exec(
- `CREATE TABLE IF NOT EXISTS ${this.tablename} (${this.colsInit})`
+ `PRAGMA foreign_keys = ON;CREATE TABLE IF NOT EXISTS ${this.tablename} (${this.colsInit})`
);
if (tracing) db.on("trace", (sql) => console.log(sql));
diff --git a/server/models/invite.js b/server/models/invite.js
new file mode 100644
index 00000000..0a945c4f
--- /dev/null
+++ b/server/models/invite.js
@@ -0,0 +1,191 @@
+const Invite = {
+ tablename: "invites",
+ writable: [],
+ colsInit: `
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ code TEXT UNIQUE NOT NULL,
+ status TEXT NOT NULL DEFAULT "pending",
+ claimedBy INTEGER DEFAULT NULL,
+ createdAt TEXT DEFAULT CURRENT_TIMESTAMP,
+ createdBy INTEGER NOT NULL,
+ lastUpdatedAt TEXT DEFAULT CURRENT_TIMESTAMP
+ `,
+ migrateTable: async function () {
+ const { checkForMigrations } = require("../utils/database");
+ console.log(`\x1b[34m[MIGRATING]\x1b[0m Checking for Invites migrations`);
+ const db = await this.db(false);
+ await checkForMigrations(this, db);
+ },
+ migrations: function () {
+ return [];
+ },
+ makeCode: () => {
+ const uuidAPIKey = require("uuid-apikey");
+ return uuidAPIKey.create().apiKey;
+ },
+ db: async function (tracing = true) {
+ const sqlite3 = require("sqlite3").verbose();
+ const { open } = require("sqlite");
+
+ const db = await open({
+ filename: `${
+ !!process.env.STORAGE_DIR ? `${process.env.STORAGE_DIR}/` : "storage/"
+ }anythingllm.db`,
+ driver: sqlite3.Database,
+ });
+
+ await db.exec(
+ `PRAGMA foreign_keys = ON;CREATE TABLE IF NOT EXISTS ${this.tablename} (${this.colsInit})`
+ );
+
+ if (tracing) db.on("trace", (sql) => console.log(sql));
+ return db;
+ },
+ create: async function (createdByUserId = 0) {
+ const db = await this.db();
+ const { id, success, message } = await db
+ .run(`INSERT INTO ${this.tablename} (code, createdBy) VALUES(?, ?)`, [
+ this.makeCode(),
+ createdByUserId,
+ ])
+ .then((res) => {
+ return { id: res.lastID, success: true, message: null };
+ })
+ .catch((error) => {
+ return { id: null, success: false, message: error.message };
+ });
+
+ if (!success) {
+ db.close();
+ console.error("FAILED TO CREATE USER.", message);
+ return { invite: null, error: message };
+ }
+
+ const invite = await db.get(
+ `SELECT * FROM ${this.tablename} WHERE id = ${id} `
+ );
+ db.close();
+
+ return { invite, error: null };
+ },
+ deactivate: async function (inviteId = null) {
+ const invite = await this.get(`id = ${inviteId}`);
+ if (!invite) return { success: false, error: "Invite does not exist." };
+ if (invite.status !== "pending")
+ return { success: false, error: "Invite is not in pending status." };
+
+ const db = await this.db();
+ const { success, message } = await db
+ .run(`UPDATE ${this.tablename} SET status=? WHERE id=?`, [
+ "disabled",
+ inviteId,
+ ])
+ .then(() => {
+ return { success: true, message: null };
+ })
+ .catch((error) => {
+ return { success: false, message: error.message };
+ });
+
+ db.close();
+ if (!success) {
+ console.error(message);
+ return { success: false, error: message };
+ }
+
+ return { success: true, error: null };
+ },
+ markClaimed: async function (inviteId = null, user) {
+ const invite = await this.get(`id = ${inviteId}`);
+ if (!invite) return { success: false, error: "Invite does not exist." };
+ if (invite.status !== "pending")
+ return { success: false, error: "Invite is not in pending status." };
+
+ const db = await this.db();
+ const { success, message } = await db
+ .run(`UPDATE ${this.tablename} SET status=?,claimedBy=? WHERE id=?`, [
+ "claimed",
+ user.id,
+ inviteId,
+ ])
+ .then(() => {
+ return { success: true, message: null };
+ })
+ .catch((error) => {
+ return { success: false, message: error.message };
+ });
+
+ db.close();
+ if (!success) {
+ console.error(message);
+ return { success: false, error: message };
+ }
+
+ return { success: true, error: null };
+ },
+ get: async function (clause = "") {
+ const db = await this.db();
+ const result = await db
+ .get(
+ `SELECT * FROM ${this.tablename} ${clause ? `WHERE ${clause}` : clause}`
+ )
+ .then((res) => res || null);
+ if (!result) return null;
+ db.close();
+ return { ...result };
+ },
+ count: async function (clause = null) {
+ const db = await this.db();
+ const { count } = await db.get(
+ `SELECT COUNT(*) as count FROM ${this.tablename} ${
+ clause ? `WHERE ${clause}` : ""
+ } `
+ );
+ db.close();
+
+ return count;
+ },
+ delete: async function (clause = "") {
+ const db = await this.db();
+ await db.get(`DELETE FROM ${this.tablename} WHERE ${clause}`);
+ db.close();
+
+ return true;
+ },
+ where: async function (clause = "", limit = null) {
+ const db = await this.db();
+ const results = await db.all(
+ `SELECT * FROM ${this.tablename} ${clause ? `WHERE ${clause}` : ""} ${
+ !!limit ? `LIMIT ${limit}` : ""
+ }`
+ );
+ db.close();
+
+ return results;
+ },
+ whereWithUsers: async function (clause = "", limit = null) {
+ const { User } = require("./user");
+ const results = await this.where(clause, limit);
+ for (const invite of results) {
+ console.log(invite);
+ if (!!invite.claimedBy) {
+ const acceptedUser = await User.get(`id = ${invite.claimedBy}`);
+ invite.claimedBy = {
+ id: acceptedUser?.id,
+ username: acceptedUser?.username,
+ };
+ }
+
+ if (!!invite.createdBy) {
+ const createdUser = await User.get(`id = ${invite.createdBy}`);
+ invite.createdBy = {
+ id: createdUser?.id,
+ username: createdUser?.username,
+ };
+ }
+ }
+ return results;
+ },
+};
+
+module.exports.Invite = Invite;
diff --git a/server/models/systemSettings.js b/server/models/systemSettings.js
new file mode 100644
index 00000000..1df81204
--- /dev/null
+++ b/server/models/systemSettings.js
@@ -0,0 +1,122 @@
+const SystemSettings = {
+ supportedFields: [
+ "multi_user_mode",
+ "users_can_delete_workspaces",
+ "limit_user_messages",
+ "message_limit",
+ ],
+ privateField: [],
+ tablename: "system_settings",
+ colsInit: `
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ label TEXT UNIQUE NOT NULL,
+ value TEXT,
+ createdAt TEXT DEFAULT CURRENT_TIMESTAMP,
+ lastUpdatedAt TEXT DEFAULT CURRENT_TIMESTAMP
+ `,
+ migrateTable: async function () {
+ const { checkForMigrations } = require("../utils/database");
+ console.log(
+ `\x1b[34m[MIGRATING]\x1b[0m Checking for System Setting migrations`
+ );
+ const db = await this.db(false);
+ await checkForMigrations(this, db);
+ },
+ migrations: function () {
+ return [];
+ },
+ db: async function (tracing = true) {
+ const sqlite3 = require("sqlite3").verbose();
+ const { open } = require("sqlite");
+
+ const db = await open({
+ filename: `${
+ !!process.env.STORAGE_DIR ? `${process.env.STORAGE_DIR}/` : "storage/"
+ }anythingllm.db`,
+ driver: sqlite3.Database,
+ });
+
+ await db.exec(
+ `PRAGMA foreign_keys = ON;CREATE TABLE IF NOT EXISTS ${this.tablename} (${this.colsInit})`
+ );
+
+ if (tracing) db.on("trace", (sql) => console.log(sql));
+ return db;
+ },
+ get: async function (clause = "") {
+ const db = await this.db();
+ const result = await db
+ .get(`SELECT * FROM ${this.tablename} WHERE ${clause}`)
+ .then((res) => res || null);
+ if (!result) return null;
+ db.close();
+
+ return result;
+ },
+ where: async function (clause = null, limit = null) {
+ const db = await this.db();
+ const results = await db.all(
+ `SELECT * FROM ${this.tablename} ${clause ? `WHERE ${clause}` : ""} ${
+ !!limit ? `LIMIT ${limit}` : ""
+ }`
+ );
+ db.close();
+
+ return results;
+ },
+ updateSettings: async function (updates = {}) {
+ const validConfigKeys = Object.keys(updates).filter((key) =>
+ this.supportedFields.includes(key)
+ );
+ for (const key of validConfigKeys) {
+ const existingRecord = await this.get(`label = '${key}'`);
+ if (!existingRecord) {
+ const db = await this.db();
+ const value = updates[key] === null ? null : String(updates[key]);
+ const { success, message } = await db
+ .run(`INSERT INTO ${this.tablename} (label, value) VALUES (?, ?)`, [
+ key,
+ value,
+ ])
+ .then((res) => {
+ return { id: res.lastID, success: true, message: null };
+ })
+ .catch((error) => {
+ return { id: null, success: false, message: error.message };
+ });
+ db.close();
+ if (!success) {
+ console.error("FAILED TO ADD SYSTEM CONFIG OPTION", message);
+ return { success: false, error: message };
+ }
+ } else {
+ const db = await this.db();
+ const value = updates[key] === null ? null : String(updates[key]);
+ const { success, message } = await db
+ .run(`UPDATE ${this.tablename} SET label=?,value=? WHERE id = ?`, [
+ key,
+ value,
+ existingRecord.id,
+ ])
+ .then(() => {
+ return { success: true, message: null };
+ })
+ .catch((error) => {
+ return { success: false, message: error.message };
+ });
+
+ db.close();
+ if (!success) {
+ console.error("FAILED TO UPDATE SYSTEM CONFIG OPTION", message);
+ return { success: false, error: message };
+ }
+ }
+ }
+ return { success: true, error: null };
+ },
+ isMultiUserMode: async function () {
+ return (await this.get(`label = 'multi_user_mode'`))?.value === "true";
+ },
+};
+
+module.exports.SystemSettings = SystemSettings;
diff --git a/server/models/user.js b/server/models/user.js
new file mode 100644
index 00000000..9bfee350
--- /dev/null
+++ b/server/models/user.js
@@ -0,0 +1,170 @@
+const User = {
+ tablename: "users",
+ writable: [],
+ colsInit: `
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ username TEXT UNIQUE,
+ password TEXT NOT NULL,
+ role TEXT NOT NULL DEFAULT "default",
+ suspended INTEGER NOT NULL DEFAULT 0,
+ createdAt TEXT DEFAULT CURRENT_TIMESTAMP,
+ lastUpdatedAt TEXT DEFAULT CURRENT_TIMESTAMP
+ `,
+ migrateTable: async function () {
+ const { checkForMigrations } = require("../utils/database");
+ console.log(`\x1b[34m[MIGRATING]\x1b[0m Checking for User migrations`);
+ const db = await this.db(false);
+ await checkForMigrations(this, db);
+ },
+ migrations: function () {
+ return [];
+ },
+ db: async function (tracing = true) {
+ const sqlite3 = require("sqlite3").verbose();
+ const { open } = require("sqlite");
+
+ const db = await open({
+ filename: `${
+ !!process.env.STORAGE_DIR ? `${process.env.STORAGE_DIR}/` : "storage/"
+ }anythingllm.db`,
+ driver: sqlite3.Database,
+ });
+
+ await db.exec(
+ `PRAGMA foreign_keys = ON;CREATE TABLE IF NOT EXISTS ${this.tablename} (${this.colsInit})`
+ );
+
+ if (tracing) db.on("trace", (sql) => console.log(sql));
+ return db;
+ },
+ create: async function ({ username, password, role = null }) {
+ const bcrypt = require("bcrypt");
+ const db = await this.db();
+ const { id, success, message } = await db
+ .run(
+ `INSERT INTO ${this.tablename} (username, password, role) VALUES(?, ?, ?)`,
+ [username, bcrypt.hashSync(password, 10), role ?? "default"]
+ )
+ .then((res) => {
+ return { id: res.lastID, success: true, message: null };
+ })
+ .catch((error) => {
+ return { id: null, success: false, message: error.message };
+ });
+
+ if (!success) {
+ db.close();
+ console.error("FAILED TO CREATE USER.", message);
+ return { user: null, error: message };
+ }
+
+ const user = await db.get(
+ `SELECT * FROM ${this.tablename} WHERE id = ${id} `
+ );
+ db.close();
+
+ return { user, error: null };
+ },
+ update: async function (userId, updates = {}) {
+ const user = await this.get(`id = ${userId}`);
+ if (!user) return { success: false, error: "User does not exist." };
+ const { username, password, role, suspended = 0 } = updates;
+ const toUpdate = { suspended };
+
+ if (user.username !== username && username?.length > 0) {
+ const usedUsername = !!(await this.get(`username = '${username}'`));
+ if (usedUsername)
+ return { success: false, error: `${username} is already in use.` };
+ toUpdate.username = username;
+ }
+
+ if (!!password) {
+ const bcrypt = require("bcrypt");
+ toUpdate.password = bcrypt.hashSync(password, 10);
+ }
+
+ if (user.role !== role && ["admin", "default"].includes(role)) {
+ // If was existing admin and that has been changed
+ // make sure at least one admin exists
+ if (user.role === "admin") {
+ const validAdminCount = (await this.count(`role = 'admin'`)) > 1;
+ if (!validAdminCount)
+ return {
+ success: false,
+ error: `There would be no admins if this action was completed. There must be at least one admin.`,
+ };
+ }
+
+ toUpdate.role = role;
+ }
+
+ if (Object.keys(toUpdate).length !== 0) {
+ const values = Object.values(toUpdate);
+ const template = `UPDATE ${this.tablename} SET ${Object.keys(
+ toUpdate
+ ).map((key) => {
+ return `${key}=?`;
+ })} WHERE id = ?`;
+
+ const db = await this.db();
+ const { success, message } = await db
+ .run(template, [...values, userId])
+ .then(() => {
+ return { success: true, message: null };
+ })
+ .catch((error) => {
+ return { success: false, message: error.message };
+ });
+
+ db.close();
+ if (!success) {
+ console.error(message);
+ return { success: false, error: message };
+ }
+ }
+
+ return { success: true, error: null };
+ },
+ get: async function (clause = "") {
+ const db = await this.db();
+ const result = await db
+ .get(
+ `SELECT * FROM ${this.tablename} ${clause ? `WHERE ${clause}` : clause}`
+ )
+ .then((res) => res || null);
+ if (!result) return null;
+ db.close();
+ return { ...result };
+ },
+ count: async function (clause = null) {
+ const db = await this.db();
+ const { count } = await db.get(
+ `SELECT COUNT(*) as count FROM ${this.tablename} ${
+ clause ? `WHERE ${clause}` : ""
+ } `
+ );
+ db.close();
+
+ return count;
+ },
+ delete: async function (clause = "") {
+ const db = await this.db();
+ await db.get(`DELETE FROM ${this.tablename} WHERE ${clause}`);
+ db.close();
+
+ return true;
+ },
+ where: async function (clause = "", limit = null) {
+ const db = await this.db();
+ const results = await db.all(
+ `SELECT * FROM ${this.tablename} ${clause ? `WHERE ${clause}` : ""} ${
+ !!limit ? `LIMIT ${limit}` : ""
+ }`
+ );
+ db.close();
+
+ return results;
+ },
+};
+
+module.exports = { User };
diff --git a/server/models/vectors.js b/server/models/vectors.js
index 9e1a8dd4..e568097b 100644
--- a/server/models/vectors.js
+++ b/server/models/vectors.js
@@ -34,7 +34,7 @@ const DocumentVectors = {
});
await db.exec(
- `CREATE TABLE IF NOT EXISTS ${this.tablename} (${this.colsInit})`
+ `PRAGMA foreign_keys = ON;CREATE TABLE IF NOT EXISTS ${this.tablename} (${this.colsInit})`
);
if (tracing) db.on("trace", (sql) => console.log(sql));
diff --git a/server/models/workspace.js b/server/models/workspace.js
index fed92434..5049a2ba 100644
--- a/server/models/workspace.js
+++ b/server/models/workspace.js
@@ -1,6 +1,7 @@
const slugify = require("slugify");
const { Document } = require("./documents");
const { checkForMigrations } = require("../utils/database");
+const { WorkspaceUser } = require("./workspaceUsers");
const Workspace = {
tablename: "workspaces",
@@ -70,13 +71,13 @@ const Workspace = {
});
await db.exec(
- `CREATE TABLE IF NOT EXISTS ${this.tablename} (${this.colsInit})`
+ `PRAGMA foreign_keys = ON;CREATE TABLE IF NOT EXISTS ${this.tablename} (${this.colsInit})`
);
if (tracing) db.on("trace", (sql) => console.log(sql));
return db;
},
- new: async function (name = null) {
+ new: async function (name = null, creatorId = null) {
if (!name) return { result: null, message: "name cannot be null" };
var slug = slugify(name, { lower: true });
@@ -109,6 +110,10 @@ const Workspace = {
);
db.close();
+ // If created with a user then we need to create the relationship as well.
+ // If creating with an admin User it wont change anything because admins can
+ // view all workspaces anyway.
+ if (!!creatorId) await WorkspaceUser.create(creatorId, workspace.id);
return { workspace, message: null };
},
update: async function (id = null, data = {}) {
@@ -142,6 +147,25 @@ const Workspace = {
const updatedWorkspace = await this.get(`id = ${id}`);
return { workspace: updatedWorkspace, message: null };
},
+ getWithUser: async function (user = null, clause = "") {
+ if (user.role === "admin") return this.get(clause);
+
+ const db = await this.db();
+ const result = await db
+ .get(
+ `SELECT * FROM ${this.tablename} as workspace
+ LEFT JOIN workspace_users as ws_users
+ ON ws_users.workspace_id = workspace.id
+ WHERE ws_users.user_id = ${user?.id} AND ${clause}`
+ )
+ .then((res) => res || null);
+ if (!result) return null;
+ db.close();
+
+ const workspace = { ...result, id: result.workspace_id };
+ const documents = await Document.forWorkspace(workspace.id);
+ return { ...workspace, documents };
+ },
get: async function (clause = "") {
const db = await this.db();
const result = await db
@@ -160,17 +184,55 @@ const Workspace = {
return true;
},
- where: async function (clause = "", limit = null) {
+ where: async function (clause = "", limit = null, orderBy = null) {
const db = await this.db();
const results = await db.all(
`SELECT * FROM ${this.tablename} ${clause ? `WHERE ${clause}` : ""} ${
!!limit ? `LIMIT ${limit}` : ""
- }`
+ } ${!!orderBy ? orderBy : ""}`
);
db.close();
return results;
},
+ whereWithUser: async function (
+ user,
+ clause = null,
+ limit = null,
+ orderBy = null
+ ) {
+ if (user.role === "admin") return await this.where(clause, limit);
+ const db = await this.db();
+ const results = await db.all(
+ `SELECT * FROM ${this.tablename} as workspace
+ LEFT JOIN workspace_users as ws_users
+ ON ws_users.workspace_id = workspace.id
+ WHERE ws_users.user_id = ${user.id} ${clause ? `AND ${clause}` : ""} ${
+ !!limit ? `LIMIT ${limit}` : ""
+ } ${!!orderBy ? orderBy : ""}`
+ );
+ db.close();
+ const workspaces = results.map((ws) => {
+ return { ...ws, id: ws.workspace_id };
+ });
+
+ return workspaces;
+ },
+ whereWithUsers: async function (clause = "", limit = null, orderBy = null) {
+ const workspaces = await this.where(clause, limit, orderBy);
+ for (const workspace of workspaces) {
+ const userIds = (
+ await WorkspaceUser.where(`workspace_id = ${workspace.id}`)
+ ).map((rel) => rel.user_id);
+ workspace.userIds = userIds;
+ }
+ return workspaces;
+ },
+ updateUsers: async function (workspaceId, userIds = []) {
+ await WorkspaceUser.delete(`workspace_id = ${workspaceId}`);
+ await WorkspaceUser.createManyUsers(userIds, workspaceId);
+ return { success: true, error: null };
+ },
};
module.exports = { Workspace };
diff --git a/server/models/workspaceChats.js b/server/models/workspaceChats.js
index 7a2aafb8..027448bf 100644
--- a/server/models/workspaceChats.js
+++ b/server/models/workspaceChats.js
@@ -8,8 +8,11 @@ const WorkspaceChats = {
prompt TEXT NOT NULL,
response TEXT NOT NULL,
include BOOL DEFAULT true,
+ user_id INTEGER DEFAULT NULL,
createdAt TEXT DEFAULT CURRENT_TIMESTAMP,
- lastUpdatedAt TEXT DEFAULT CURRENT_TIMESTAMP
+ lastUpdatedAt TEXT DEFAULT CURRENT_TIMESTAMP,
+
+ FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
`,
migrateTable: async function () {
console.log(
@@ -19,7 +22,13 @@ const WorkspaceChats = {
await checkForMigrations(this, db);
},
migrations: function () {
- return [];
+ return [
+ {
+ colName: "user_id",
+ execCmd: `ALTER TABLE ${this.tablename} ADD COLUMN user_id INTEGER DEFAULT NULL`,
+ doif: false,
+ },
+ ];
},
db: async function (tracing = true) {
const sqlite3 = require("sqlite3").verbose();
@@ -33,18 +42,18 @@ const WorkspaceChats = {
});
await db.exec(
- `CREATE TABLE IF NOT EXISTS ${this.tablename} (${this.colsInit})`
+ `PRAGMA foreign_keys = ON;CREATE TABLE IF NOT EXISTS ${this.tablename} (${this.colsInit})`
);
if (tracing) db.on("trace", (sql) => console.log(sql));
return db;
},
- new: async function ({ workspaceId, prompt, response = {} }) {
+ new: async function ({ workspaceId, prompt, response = {}, user = null }) {
const db = await this.db();
const { id, success, message } = await db
.run(
- `INSERT INTO ${this.tablename} (workspaceId, prompt, response) VALUES (?, ?, ?)`,
- [workspaceId, prompt, JSON.stringify(response)]
+ `INSERT INTO ${this.tablename} (workspaceId, prompt, response, user_id) VALUES (?, ?, ?, ?)`,
+ [workspaceId, prompt, JSON.stringify(response), user?.id || null]
)
.then((res) => {
return { id: res.lastID, success: true, message: null };
@@ -64,6 +73,18 @@ const WorkspaceChats = {
return { chat, message: null };
},
+ forWorkspaceByUser: async function (
+ workspaceId = null,
+ userId = null,
+ limit = null
+ ) {
+ if (!workspaceId || !userId) return [];
+ return await this.where(
+ `workspaceId = ${workspaceId} AND include = true AND user_id = ${userId}`,
+ limit,
+ "ORDER BY id ASC"
+ );
+ },
forWorkspace: async function (workspaceId = null, limit = null) {
if (!workspaceId) return [];
return await this.where(
@@ -72,21 +93,27 @@ const WorkspaceChats = {
"ORDER BY id ASC"
);
},
- markHistoryInvalid: async function (workspaceId = null) {
+ markHistoryInvalid: async function (workspaceId = null, user = null) {
if (!workspaceId) return;
const db = await this.db();
await db.run(
- `UPDATE ${this.tablename} SET include = false WHERE workspaceId = ?`,
+ `UPDATE ${this.tablename} SET include = false WHERE workspaceId = ? ${
+ user ? `AND user_id = ${user.id}` : ""
+ }`,
[workspaceId]
);
db.close();
return;
},
- get: async function (clause = "") {
+ get: async function (clause = "", limit = null, order = null) {
const db = await this.db();
const result = await db
- .get(`SELECT * FROM ${this.tablename} WHERE ${clause}`)
+ .get(
+ `SELECT * FROM ${this.tablename} WHERE ${clause} ${
+ !!order ? order : ""
+ } ${!!limit ? `LIMIT ${limit}` : ""}`
+ )
.then((res) => res || null);
db.close();
@@ -105,12 +132,40 @@ const WorkspaceChats = {
const results = await db.all(
`SELECT * FROM ${this.tablename} ${clause ? `WHERE ${clause}` : ""} ${
!!order ? order : ""
- } ${!!limit ? `LIMIT ${limit}` : ""} `
+ } ${!!limit ? `LIMIT ${limit}` : ""}`
);
db.close();
return results;
},
+ count: async function (clause = null) {
+ const db = await this.db();
+ const { count } = await db.get(
+ `SELECT COUNT(*) as count FROM ${this.tablename} ${
+ clause ? `WHERE ${clause}` : ""
+ } `
+ );
+ db.close();
+
+ return count;
+ },
+ whereWithData: async function (clause = "", limit = null, order = null) {
+ const { Workspace } = require("./workspace");
+ const { User } = require("./user");
+ const results = await this.where(clause, limit, order);
+ for (const res of results) {
+ const workspace = await Workspace.get(`id = ${res.workspaceId}`);
+ res.workspace = workspace
+ ? { name: workspace.name, slug: workspace.slug }
+ : { name: "deleted workspace", slug: null };
+
+ const user = await User.get(`id = ${res.user_id}`);
+ res.user = user
+ ? { username: user.username }
+ : { username: "deleted user" };
+ }
+ return results;
+ },
};
module.exports = { WorkspaceChats };
diff --git a/server/models/workspaceUsers.js b/server/models/workspaceUsers.js
new file mode 100644
index 00000000..8dacbac1
--- /dev/null
+++ b/server/models/workspaceUsers.js
@@ -0,0 +1,132 @@
+const WorkspaceUser = {
+ tablename: "workspace_users",
+ colsInit: `
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ user_id INTEGER NOT NULL,
+ workspace_id INTEGER NOT NULL,
+ createdAt TEXT DEFAULT CURRENT_TIMESTAMP,
+ lastUpdatedAt TEXT DEFAULT CURRENT_TIMESTAMP,
+
+ FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
+ FOREIGN KEY (workspace_id) REFERENCES workspaces (id) ON DELETE CASCADE
+ `,
+ migrateTable: async function () {
+ const { checkForMigrations } = require("../utils/database");
+ console.log(
+ `\x1b[34m[MIGRATING]\x1b[0m Checking for Workspace User migrations`
+ );
+ const db = await this.db(false);
+ await checkForMigrations(this, db);
+ },
+ migrations: function () {
+ return [];
+ },
+ db: async function (tracing = true) {
+ const sqlite3 = require("sqlite3").verbose();
+ const { open } = require("sqlite");
+
+ const db = await open({
+ filename: `${
+ !!process.env.STORAGE_DIR ? `${process.env.STORAGE_DIR}/` : "storage/"
+ }anythingllm.db`,
+ driver: sqlite3.Database,
+ });
+
+ await db.exec(
+ `PRAGMA foreign_keys = ON;CREATE TABLE IF NOT EXISTS ${this.tablename} (${this.colsInit})`
+ );
+
+ if (tracing) db.on("trace", (sql) => console.log(sql));
+ return db;
+ },
+ createMany: async function (userId, workspaceIds = []) {
+ if (workspaceIds.length === 0) return;
+ const db = await this.db();
+ const stmt = await db.prepare(
+ `INSERT INTO ${this.tablename} (user_id, workspace_id) VALUES (?,?)`
+ );
+
+ for (const workspaceId of workspaceIds) {
+ stmt.run([userId, workspaceId]);
+ }
+
+ stmt.finalize();
+ db.close();
+ return;
+ },
+ createManyUsers: async function (userIds = [], workspaceId) {
+ if (userIds.length === 0) return;
+ const db = await this.db();
+ const stmt = await db.prepare(
+ `INSERT INTO ${this.tablename} (user_id, workspace_id) VALUES (?,?)`
+ );
+
+ for (const userId of userIds) {
+ stmt.run([userId, workspaceId]);
+ }
+
+ stmt.finalize();
+ db.close();
+ return;
+ },
+ create: async function (userId = 0, workspaceId = 0) {
+ const db = await this.db();
+ const { success, message } = await db
+ .run(
+ `INSERT INTO ${this.tablename} (user_id, workspace_id) VALUES (?, ?)`,
+ [userId, workspaceId]
+ )
+ .then((res) => {
+ return { id: res.lastID, success: true, message: null };
+ })
+ .catch((error) => {
+ return { id: null, success: false, message: error.message };
+ });
+
+ if (!success) {
+ db.close();
+ console.error("FAILED TO CREATE WORKSPACE_USER RELATIONSHIP.", message);
+ return false;
+ }
+ return true;
+ },
+ get: async function (clause = "") {
+ const db = await this.db();
+ const result = await db
+ .get(`SELECT * FROM ${this.tablename} WHERE ${clause}`)
+ .then((res) => res || null);
+ if (!result) return null;
+ db.close();
+
+ return result;
+ },
+ where: async function (clause = null, limit = null) {
+ const db = await this.db();
+ const results = await db.all(
+ `SELECT * FROM ${this.tablename} ${clause ? `WHERE ${clause}` : ""} ${
+ !!limit ? `LIMIT ${limit}` : ""
+ }`
+ );
+ db.close();
+
+ return results;
+ },
+ count: async function (clause = null) {
+ const db = await this.db();
+ const { count } = await db.get(
+ `SELECT COUNT(*) as count FROM ${this.tablename} ${
+ clause ? `WHERE ${clause}` : ""
+ }`
+ );
+ db.close();
+
+ return count;
+ },
+ delete: async function (clause = null) {
+ const db = await this.db();
+ await db.get(`DELETE FROM ${this.tablename} WHERE ${clause}`);
+ return;
+ },
+};
+
+module.exports.WorkspaceUser = WorkspaceUser;
diff --git a/server/package.json b/server/package.json
index c9cc4d20..24b84210 100644
--- a/server/package.json
+++ b/server/package.json
@@ -18,6 +18,7 @@
"@googleapis/youtube": "^9.0.0",
"@pinecone-database/pinecone": "^0.1.6",
"archiver": "^5.3.1",
+ "bcrypt": "^5.1.0",
"body-parser": "^1.20.2",
"chromadb": "^1.5.2",
"cors": "^2.8.5",
@@ -35,6 +36,7 @@
"sqlite": "^4.2.1",
"sqlite3": "^5.1.6",
"uuid": "^9.0.0",
+ "uuid-apikey": "^1.5.3",
"vectordb": "0.1.12"
},
"devDependencies": {
diff --git a/server/utils/chats/commands/reset.js b/server/utils/chats/commands/reset.js
index 59f9448e..8851efdf 100644
--- a/server/utils/chats/commands/reset.js
+++ b/server/utils/chats/commands/reset.js
@@ -1,7 +1,7 @@
const { WorkspaceChats } = require("../../../models/workspaceChats");
-async function resetMemory(workspace, _message, msgUUID) {
- await WorkspaceChats.markHistoryInvalid(workspace.id);
+async function resetMemory(workspace, _message, msgUUID, user = null) {
+ await WorkspaceChats.markHistoryInvalid(workspace.id, user);
return {
uuid: msgUUID,
type: "textResponse",
diff --git a/server/utils/chats/index.js b/server/utils/chats/index.js
index 800003e2..5e099dad 100644
--- a/server/utils/chats/index.js
+++ b/server/utils/chats/index.js
@@ -59,14 +59,19 @@ function grepCommand(message) {
return null;
}
-async function chatWithWorkspace(workspace, message, chatMode = "chat") {
+async function chatWithWorkspace(
+ workspace,
+ message,
+ chatMode = "chat",
+ user = null
+) {
const uuid = uuidv4();
const openai = new OpenAi();
const VectorDb = getVectorDbClass();
const command = grepCommand(message);
if (!!command && Object.keys(VALID_COMMANDS).includes(command)) {
- return await VALID_COMMANDS[command](workspace, message, uuid);
+ return await VALID_COMMANDS[command](workspace, message, uuid, user);
}
const { safe, reasons = [] } = await openai.isSafe(message);
@@ -84,7 +89,8 @@ async function chatWithWorkspace(workspace, message, chatMode = "chat") {
}
const hasVectorizedSpace = await VectorDb.hasNamespace(workspace.slug);
- if (!hasVectorizedSpace) {
+ const embeddingsCount = await VectorDb.namespaceCount(workspace.slug);
+ if (!hasVectorizedSpace || embeddingsCount === 0) {
const rawHistory = await WorkspaceChats.forWorkspace(workspace.id);
const chatHistory = convertToPromptHistory(rawHistory);
const response = await openai.sendChat(chatHistory, message, workspace);
@@ -94,6 +100,7 @@ async function chatWithWorkspace(workspace, message, chatMode = "chat") {
workspaceId: workspace.id,
prompt: message,
response: data,
+ user,
});
return {
id: uuid,
@@ -137,6 +144,7 @@ async function chatWithWorkspace(workspace, message, chatMode = "chat") {
workspaceId: workspace.id,
prompt: message,
response: data,
+ user,
});
return {
id: uuid,
diff --git a/server/utils/database/index.js b/server/utils/database/index.js
index 0b1f42bd..e3b658a5 100644
--- a/server/utils/database/index.js
+++ b/server/utils/database/index.js
@@ -50,15 +50,23 @@ async function validateTablePragmas(force = false) {
);
return;
}
-
+ const { SystemSettings } = require("../../models/systemSettings");
+ const { User } = require("../../models/user");
const { Workspace } = require("../../models/workspace");
+ const { WorkspaceUser } = require("../../models/workspaceUsers");
const { Document } = require("../../models/documents");
const { DocumentVectors } = require("../../models/vectors");
const { WorkspaceChats } = require("../../models/workspaceChats");
+ const { Invite } = require("../../models/invite");
+
+ await SystemSettings.migrateTable();
+ await User.migrateTable();
await Workspace.migrateTable();
+ await WorkspaceUser.migrateTable();
await Document.migrateTable();
await DocumentVectors.migrateTable();
await WorkspaceChats.migrateTable();
+ await Invite.migrateTable();
} catch (e) {
console.error(`validateTablePragmas: Migrations failed`, e);
}
diff --git a/server/utils/http/index.js b/server/utils/http/index.js
index 9fd643b7..3543a36b 100644
--- a/server/utils/http/index.js
+++ b/server/utils/http/index.js
@@ -2,6 +2,7 @@ process.env.NODE_ENV === "development"
? require("dotenv").config({ path: `.env.${process.env.NODE_ENV}` })
: require("dotenv").config();
const JWT = require("jsonwebtoken");
+const { User } = require("../../models/user");
function reqBody(request) {
return typeof request.body === "string"
@@ -19,16 +20,43 @@ function makeJWT(info = {}, expiry = "30d") {
return JWT.sign(info, process.env.JWT_SECRET, { expiresIn: expiry });
}
+async function userFromSession(request, response = null) {
+ if (!!response && !!response.locals?.user) {
+ return response.locals.user;
+ }
+
+ const auth = request.header("Authorization");
+ const token = auth ? auth.split(" ")[1] : null;
+
+ if (!token) {
+ return null;
+ }
+
+ const valid = decodeJWT(token);
+ if (!valid || !valid.id) {
+ return null;
+ }
+
+ const user = await User.get(`id = ${valid.id}`);
+ return user;
+}
+
function decodeJWT(jwtToken) {
try {
return JWT.verify(jwtToken, process.env.JWT_SECRET);
} catch {}
- return { p: null };
+ return { p: null, id: null, username: null };
+}
+
+function multiUserMode(response) {
+ return response?.locals?.multiUserMode;
}
module.exports = {
reqBody,
+ multiUserMode,
queryParams,
makeJWT,
decodeJWT,
+ userFromSession,
};
diff --git a/server/utils/middleware/validatedRequest.js b/server/utils/middleware/validatedRequest.js
index 4e7c519a..1ff13f3d 100644
--- a/server/utils/middleware/validatedRequest.js
+++ b/server/utils/middleware/validatedRequest.js
@@ -1,6 +1,13 @@
+const { SystemSettings } = require("../../models/systemSettings");
+const { User } = require("../../models/user");
const { decodeJWT } = require("../http");
-function validatedRequest(request, response, next) {
+async function validatedRequest(request, response, next) {
+ const multiUserMode = await SystemSettings.isMultiUserMode();
+ response.locals.multiUserMode = multiUserMode;
+ if (multiUserMode)
+ return await validateMultiUserRequest(request, response, next);
+
// When in development passthrough auth token for ease of development.
// Or if the user simply did not set an Auth token or JWT Secret
if (
@@ -40,6 +47,37 @@ function validatedRequest(request, response, next) {
next();
}
+async function validateMultiUserRequest(request, response, next) {
+ const auth = request.header("Authorization");
+ const token = auth ? auth.split(" ")[1] : null;
+
+ if (!token) {
+ response.status(403).json({
+ error: "No auth token found.",
+ });
+ return;
+ }
+
+ const valid = decodeJWT(token);
+ if (!valid || !valid.id) {
+ response.status(403).json({
+ error: "Invalid auth token.",
+ });
+ return;
+ }
+
+ const user = await User.get(`id = ${valid.id}`);
+ if (!user) {
+ response.status(403).json({
+ error: "Invalid auth for user.",
+ });
+ return;
+ }
+
+ response.locals.user = user;
+ next();
+}
+
module.exports = {
validatedRequest,
};
diff --git a/server/utils/vectorDbProviders/chroma/index.js b/server/utils/vectorDbProviders/chroma/index.js
index 863b6f13..801a41db 100644
--- a/server/utils/vectorDbProviders/chroma/index.js
+++ b/server/utils/vectorDbProviders/chroma/index.js
@@ -44,6 +44,11 @@ const Chroma = {
}
return totalVectors;
},
+ namespaceCount: async function (_namespace = null) {
+ const { client } = await this.connect();
+ const namespace = await this.namespace(client, _namespace);
+ return namespace?.vectorCount || 0;
+ },
embeddingFunc: function () {
return new OpenAIEmbeddingFunction({
openai_api_key: process.env.OPEN_AI_KEY,
diff --git a/server/utils/vectorDbProviders/lance/index.js b/server/utils/vectorDbProviders/lance/index.js
index 3315028a..ddc18469 100644
--- a/server/utils/vectorDbProviders/lance/index.js
+++ b/server/utils/vectorDbProviders/lance/index.js
@@ -58,6 +58,14 @@ const LanceDb = {
}
return count;
},
+ namespaceCount: async function (_namespace = null) {
+ const { client } = await this.connect();
+ const exists = await this.namespaceExists(client, _namespace);
+ if (!exists) return 0;
+
+ const table = await client.openTable(_namespace);
+ return (await table.countRows()) || 0;
+ },
embeddingFunc: function () {
return new lancedb.OpenAIEmbeddingFunction(
"context",
diff --git a/server/utils/vectorDbProviders/pinecone/index.js b/server/utils/vectorDbProviders/pinecone/index.js
index 0c03e75b..e34391b1 100644
--- a/server/utils/vectorDbProviders/pinecone/index.js
+++ b/server/utils/vectorDbProviders/pinecone/index.js
@@ -86,6 +86,11 @@ const Pinecone = {
0
);
},
+ namespaceCount: async function (_namespace = null) {
+ const { pineconeIndex } = await this.connect();
+ const namespace = await this.namespace(pineconeIndex, _namespace);
+ return namespace?.vectorCount || 0;
+ },
similarityResponse: async function (index, namespace, queryVector) {
const result = {
contextTexts: [],
diff --git a/server/yarn.lock b/server/yarn.lock
index d5cef639..1a82497e 100644
--- a/server/yarn.lock
+++ b/server/yarn.lock
@@ -43,7 +43,7 @@
dependencies:
googleapis-common "^6.0.3"
-"@mapbox/node-pre-gyp@^1.0.0":
+"@mapbox/node-pre-gyp@^1.0.0", "@mapbox/node-pre-gyp@^1.0.10":
version "1.0.11"
resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz#417db42b7f5323d79e93b34a6d7a2a12c0df43fa"
integrity sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==
@@ -308,6 +308,14 @@ batch@0.6.1:
resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16"
integrity sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==
+bcrypt@^5.1.0:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/bcrypt/-/bcrypt-5.1.0.tgz#bbb27665dbc400480a524d8991ac7434e8529e17"
+ integrity sha512-RHBS7HI5N5tEnGTmtR/pppX0mmDSBpQ4aCBsj7CEQfYXDcO74A8sIBYcJMuCsis2E81zDxeENYhv66oZwLiA+Q==
+ dependencies:
+ "@mapbox/node-pre-gyp" "^1.0.10"
+ node-addon-api "^5.0.0"
+
bignumber.js@^9.0.0:
version "9.1.1"
resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.1.1.tgz#c4df7dc496bd849d4c9464344c1aa74228b4dac6"
@@ -515,6 +523,11 @@ color-support@^1.1.2, color-support@^1.1.3:
resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2"
integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==
+colors@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78"
+ integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==
+
combined-stream@^1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
@@ -542,6 +555,11 @@ command-line-usage@6.1.3:
table-layout "^1.0.2"
typical "^5.2.0"
+commander@^8.0.0:
+ version "8.3.0"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66"
+ integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==
+
compress-commons@^4.1.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/compress-commons/-/compress-commons-4.1.1.tgz#df2a09a7ed17447642bad10a85cc9a19e5c42a7d"
@@ -705,6 +723,11 @@ emoji-regex@^8.0.0:
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
+encode32@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/encode32/-/encode32-1.1.0.tgz#0c54b45fb314ad5502e3c230cb95acdc5e5cd1dd"
+ integrity sha512-BCmijZ4lWec5+fuGHclf7HSZf+mos2ncQkBjtvomvRWVEGAMI/tw56fuN2x4e+FTgQuTPbZjODPwX80lFy958w==
+
encodeurl@~1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
@@ -1654,6 +1677,11 @@ node-addon-api@^4.2.0:
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-4.3.0.tgz#52a1a0b475193e0928e98e0426a0d1254782b77f"
integrity sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==
+node-addon-api@^5.0.0:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-5.1.0.tgz#49da1ca055e109a23d537e9de43c09cca21eb762"
+ integrity sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==
+
node-fetch@^2.6.1, node-fetch@^2.6.12, node-fetch@^2.6.7, node-fetch@^2.6.9:
version "2.6.12"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.12.tgz#02eb8e22074018e3d5a83016649d04df0e348fba"
@@ -2340,6 +2368,21 @@ utils-merge@1.0.1:
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==
+uuid-apikey@^1.5.3:
+ version "1.5.3"
+ resolved "https://registry.yarnpkg.com/uuid-apikey/-/uuid-apikey-1.5.3.tgz#2e5d648dce93d2909018d7b73ec26ecb9fd2cdbd"
+ integrity sha512-v28vGJ1hRDzqLm6ufZ7b098Kmk159PInIHYWXfB47r3xOACZ5nRIAWe9VxFjvSW0MwckQYAnS1ucWUAXGKo95w==
+ dependencies:
+ colors "^1.4.0"
+ commander "^8.0.0"
+ encode32 "^1.1.0"
+ uuid "^8.3.1"
+
+uuid@^8.3.1:
+ version "8.3.2"
+ resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
+ integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
+
uuid@^9.0.0:
version "9.0.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5"