From 1722eaf41260305cc5e0900e7e7cc0d8aa731504 Mon Sep 17 00:00:00 2001 From: timothycarambat Date: Fri, 21 Jul 2023 16:19:05 -0700 Subject: [PATCH 01/15] multi user wip --- frontend/src/App.jsx | 6 +- frontend/src/AuthContext.jsx | 13 +- frontend/src/components/Modals/Password.jsx | 121 ------------ .../Modals/Password/MultiUserAuth.jsx | 92 ++++++++++ .../Modals/Password/SingleUserAuth.jsx | 76 ++++++++ .../src/components/Modals/Password/index.jsx | 97 ++++++++++ .../components/Modals/Settings/Keys/index.jsx | 172 ++++++++---------- .../Modals/Settings/MultiUserMode/index.jsx | 153 ++++++++++++++++ .../Settings/PasswordProtection/index.jsx | 121 ++++++------ .../src/components/Modals/Settings/index.jsx | 127 ++++++++++--- frontend/src/components/Preloader.jsx | 16 ++ .../src/components/PrivateRoute/index.jsx | 63 +++++++ frontend/src/components/Sidebar/index.jsx | 2 +- frontend/src/hooks/useQuery.js | 3 + frontend/src/hooks/useUser.js | 18 ++ frontend/src/models/system.js | 12 ++ frontend/src/pages/Main/index.jsx | 4 +- frontend/src/pages/WorkspaceChat/index.jsx | 4 +- frontend/src/utils/constants.js | 3 + frontend/src/utils/paths.js | 5 + frontend/src/utils/request.js | 14 +- frontend/src/utils/session.js | 15 ++ server/.env.example | 3 +- server/endpoints/system.js | 131 +++++++++++-- server/models/systemSettings.js | 114 ++++++++++++ server/models/user.js | 96 ++++++++++ server/package.json | 3 +- server/utils/database/index.js | 3 + server/utils/http/index.js | 21 ++- server/yarn.lock | 15 +- 30 files changed, 1175 insertions(+), 348 deletions(-) delete mode 100644 frontend/src/components/Modals/Password.jsx create mode 100644 frontend/src/components/Modals/Password/MultiUserAuth.jsx create mode 100644 frontend/src/components/Modals/Password/SingleUserAuth.jsx create mode 100644 frontend/src/components/Modals/Password/index.jsx create mode 100644 frontend/src/components/Modals/Settings/MultiUserMode/index.jsx create mode 100644 frontend/src/components/Preloader.jsx create mode 100644 frontend/src/components/PrivateRoute/index.jsx create mode 100644 frontend/src/hooks/useQuery.js create mode 100644 frontend/src/hooks/useUser.js create mode 100644 frontend/src/utils/session.js create mode 100644 server/models/systemSettings.js create mode 100644 server/models/user.js diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index c29c66936a3..024dae04b4f 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,6 +1,7 @@ import React, { lazy, Suspense } from "react"; import { Routes, Route } from "react-router-dom"; import { ContextWrapper } from "./AuthContext"; +import PrivateRoute from "./components/PrivateRoute"; const Main = lazy(() => import("./pages/Main")); const WorkspaceChat = lazy(() => import("./pages/WorkspaceChat")); @@ -11,7 +12,10 @@ export default function App() { } /> - } /> + } + /> diff --git a/frontend/src/AuthContext.jsx b/frontend/src/AuthContext.jsx index 4b7f820acf4..219fbea4035 100644 --- a/frontend/src/AuthContext.jsx +++ b/frontend/src/AuthContext.jsx @@ -1,9 +1,10 @@ import React, { useState, createContext } from "react"; +import { AUTH_TOKEN, AUTH_USER } from "./utils/constants"; export const AuthContext = createContext(null); export function ContextWrapper(props) { - const localUser = localStorage.getItem("anythingllm_user"); - const localAuthToken = localStorage.getItem("anythingllm_authToken"); + const localUser = localStorage.getItem(AUTH_USER); + const localAuthToken = localStorage.getItem(AUTH_TOKEN); const [store, setStore] = useState({ user: localUser ? JSON.parse(localUser) : null, authToken: localAuthToken ? localAuthToken : null, @@ -11,13 +12,13 @@ export function ContextWrapper(props) { const [actions] = useState({ updateUser: (user, authToken = "") => { - localStorage.setItem("anythingllm_user", JSON.stringify(user)); - localStorage.setItem("anythingllm_authToken", authToken); + localStorage.setItem(AUTH_USER, JSON.stringify(user)); + localStorage.setItem(AUTH_TOKEN, authToken); setStore({ user, authToken }); }, unsetUser: () => { - localStorage.removeItem("anythingllm_user"); - localStorage.removeItem("anythingllm_authToken"); + localStorage.removeItem(AUTH_USER); + localStorage.removeItem(AUTH_TOKEN); setStore({ user: null, authToken: null }); }, }); diff --git a/frontend/src/components/Modals/Password.jsx b/frontend/src/components/Modals/Password.jsx deleted file mode 100644 index 30c628fbb6b..00000000000 --- a/frontend/src/components/Modals/Password.jsx +++ /dev/null @@ -1,121 +0,0 @@ -import React, { useState, useEffect, useRef } from "react"; -import System from "../../models/system"; - -export default function PasswordModal() { - const [loading, setLoading] = useState(false); - const formEl = useRef(null); - const [error, setError] = useState(null); - const handleLogin = async (e) => { - setError(null); - setLoading(true); - e.preventDefault(); - const data = {}; - - const form = new FormData(formEl.current); - for (var [key, value] of form.entries()) data[key] = value; - const { valid, token, message } = await System.requestToken(data); - if (valid && !!token) { - window.localStorage.setItem("anythingllm_authtoken", token); - window.location.reload(); - } else { - setError(message); - setLoading(false); - } - setLoading(false); - }; - - return ( -
-
-
-
-
-
-

- This workspace is password protected. -

-
-
-
-
- - -
- {error && ( -

- Error: {error} -

- )} -

- You will only have to enter this password once. After - successful login it will be stored in your browser. -

-
-
-
- -
-
-
-
-
- ); -} - -export function usePasswordModal() { - const [requiresAuth, setRequiresAuth] = useState(null); - useEffect(() => { - async function checkAuthReq() { - if (!window) return; - if (import.meta.env.DEV) { - setRequiresAuth(false); - } else { - const currentToken = window.localStorage.getItem( - "anythingllm_authtoken" - ); - const settings = await System.keys(); - const requiresAuth = settings?.RequiresAuth || false; - - // If Auth is disabled - skip check - if (!requiresAuth) { - setRequiresAuth(requiresAuth); - return; - } - - if (!!currentToken) { - const valid = await System.checkAuth(currentToken); - if (!valid) { - setRequiresAuth(true); - window.localStorage.removeItem("anythingllm_authtoken"); - return; - } else { - setRequiresAuth(false); - return; - } - } - setRequiresAuth(true); - } - } - checkAuthReq(); - }, []); - - return { requiresAuth }; -} diff --git a/frontend/src/components/Modals/Password/MultiUserAuth.jsx b/frontend/src/components/Modals/Password/MultiUserAuth.jsx new file mode 100644 index 00000000000..630c01caa8b --- /dev/null +++ b/frontend/src/components/Modals/Password/MultiUserAuth.jsx @@ -0,0 +1,92 @@ +import React, { useState } from "react"; +import System from "../../../models/system"; +import { AUTH_TOKEN, AUTH_USER } from "../../../utils/constants"; + +export default function MultiUserAuth() { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const handleLogin = async (e) => { + setError(null); + setLoading(true); + e.preventDefault(); + const data = {}; + + const form = new FormData(e.target); + for (var [key, value] of form.entries()) data[key] = value; + const { valid, user, token, message } = await System.requestToken(data); + if (valid && !!token && !!user) { + window.localStorage.setItem(AUTH_USER, JSON.stringify(user)); + window.localStorage.setItem(AUTH_TOKEN, token); + window.location.reload(); + } else { + setError(message); + setLoading(false); + } + setLoading(false); + }; + + return ( +
+
+
+

+ This instance is password protected. +

+
+
+
+
+ + +
+ +
+ + +
+ {error && ( +

+ Error: {error} +

+ )} +

+ You will only have to enter this password once. After successful + login it will be stored in your browser. +

+
+
+
+ +
+
+
+ ); +} diff --git a/frontend/src/components/Modals/Password/SingleUserAuth.jsx b/frontend/src/components/Modals/Password/SingleUserAuth.jsx new file mode 100644 index 00000000000..c930289a655 --- /dev/null +++ b/frontend/src/components/Modals/Password/SingleUserAuth.jsx @@ -0,0 +1,76 @@ +import React, { useState } from "react"; +import System from "../../../models/system"; +import { AUTH_TOKEN } from "../../../utils/constants"; + +export default function SingleUserAuth() { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const handleLogin = async (e) => { + setError(null); + setLoading(true); + e.preventDefault(); + const data = {}; + + const form = new FormData(e.target); + for (var [key, value] of form.entries()) data[key] = value; + const { valid, token, message } = await System.requestToken(data); + if (valid && !!token) { + window.localStorage.setItem(AUTH_TOKEN, token); + window.location.reload(); + } else { + setError(message); + setLoading(false); + } + setLoading(false); + }; + + return ( +
+
+
+

+ This workspace is password protected. +

+
+
+
+
+ + +
+ {error && ( +

+ Error: {error} +

+ )} +

+ You will only have to enter this password once. After successful + login it will be stored in your browser. +

+
+
+
+ +
+
+
+ ); +} diff --git a/frontend/src/components/Modals/Password/index.jsx b/frontend/src/components/Modals/Password/index.jsx new file mode 100644 index 00000000000..ec5a0b44e54 --- /dev/null +++ b/frontend/src/components/Modals/Password/index.jsx @@ -0,0 +1,97 @@ +import React, { useState, useEffect } from "react"; +import System from "../../../models/system"; +import SingleUserAuth from "./SingleUserAuth"; +import MultiUserAuth from "./MultiUserAuth"; +import { AUTH_TOKEN, AUTH_USER } from "../../../utils/constants"; + +export default function PasswordModal({ mode = "single" }) { + return ( +
+
+
+ {mode === "single" ? : } +
+
+ ); +} + +export function usePasswordModal() { + const [auth, setAuth] = useState({ + required: false, + mode: "single", + }); + + useEffect(() => { + async function checkAuthReq() { + if (!window) return; + const settings = await System.keys(); + + if (settings?.MultiUserMode) { + const currentToken = window.localStorage.getItem(AUTH_TOKEN); + if (!!currentToken) { + const valid = await System.checkAuth(currentToken); + if (!valid) { + setAuth({ + requiresAuth: true, + mode: "multi", + }); + window.localStorage.removeItem(AUTH_USER); + window.localStorage.removeItem(AUTH_TOKEN); + return; + } else { + setAuth({ + requiresAuth: false, + mode: "multi", + }); + return; + } + } else { + setAuth({ + requiresAuth: true, + mode: "multi", + }); + return; + } + } else { + // Running token check in single user Auth mode. + // If Single user Auth is disabled - skip check + const requiresAuth = settings?.RequiresAuth || false; + if (!requiresAuth) { + setAuth({ + requiresAuth: false, + mode: "single", + }); + return; + } + + const currentToken = window.localStorage.getItem(AUTH_TOKEN); + if (!!currentToken) { + const valid = await System.checkAuth(currentToken); + if (!valid) { + setAuth({ + requiresAuth: true, + mode: "single", + }); + window.localStorage.removeItem(AUTH_TOKEN); + return; + } else { + setAuth({ + requiresAuth: false, + mode: "single", + }); + return; + } + } else { + setAuth({ + requiresAuth: true, + mode: "single", + }); + return; + } + } + } + checkAuthReq(); + }, []); + + return auth; +} diff --git a/frontend/src/components/Modals/Settings/Keys/index.jsx b/frontend/src/components/Modals/Settings/Keys/index.jsx index 77e1a2f4b6e..36709c39be4 100644 --- a/frontend/src/components/Modals/Settings/Keys/index.jsx +++ b/frontend/src/components/Modals/Settings/Keys/index.jsx @@ -1,12 +1,12 @@ -import React, { useState, useEffect } from "react"; -import { AlertCircle, Loader, X } from "react-feather"; +import React, { useState } from "react"; +import { AlertCircle, Loader } from "react-feather"; import System from "../../../../models/system"; const noop = () => false; -export default function SystemKeys({ hideModal = noop }) { - const [loading, setLoading] = useState(true); - const [settings, setSettings] = useState({}); - +export default function SystemKeys({ hideModal = noop, user, settings = {} }) { + const canDebug = settings.MultiUserMode + ? settings?.CanDebug && user?.role === "admin" + : settings?.CanDebug; function validSettings(settings) { return ( settings?.OpenAiKey && @@ -20,14 +20,6 @@ export default function SystemKeys({ hideModal = noop }) { : true) ); } - useEffect(() => { - async function fetchKeys() { - const settings = await System.keys(); - setSettings(settings); - setLoading(false); - } - fetchKeys(); - }, []); return (
@@ -40,83 +32,75 @@ export default function SystemKeys({ hideModal = noop }) {

- {loading ? ( -
-

- loading system settings -

-
- ) : ( -
- {!validSettings(settings) && ( -
- -

- Ensure all fields are green before attempting to use - AnythingLLM or it may not function as expected! -

-
- )} - - -
- - {settings?.VectorDB === "pinecone" && ( - <> - - - - - )} - {settings?.VectorDB === "chroma" && ( - <> - - - )} -
- )} +
+ {!validSettings(settings) && ( +
+ +

+ Ensure all fields are green before attempting to use + AnythingLLM or it may not function as expected! +

+
+ )} + + +
+ + {settings?.VectorDB === "pinecone" && ( + <> + + + + + )} + {settings?.VectorDB === "chroma" && ( + <> + + + )} +
)} @@ -269,7 +253,7 @@ function ShowKey({ name, env, value, valid, allowDebug = true }) { onClick={() => setDebug(true)} className="mt-2 text-xs text-slate-300 dark:text-slate-500" > - Debug + Change )}
diff --git a/frontend/src/components/Modals/Settings/MultiUserMode/index.jsx b/frontend/src/components/Modals/Settings/MultiUserMode/index.jsx new file mode 100644 index 00000000000..6a8b96e9edb --- /dev/null +++ b/frontend/src/components/Modals/Settings/MultiUserMode/index.jsx @@ -0,0 +1,153 @@ +import React, { useState } from "react"; +import System from "../../../../models/system"; +import { AUTH_TOKEN, AUTH_USER } from "../../../../utils/constants"; +import paths from "../../../../utils/paths"; + +const noop = () => false; +export default function MultiUserMode({ hideModal = noop }) { + const [saving, setSaving] = useState(false); + const [success, setSuccess] = useState(false); + const [error, setError] = useState(null); + const [useMultiUserMode, setUseMultiUserMode] = useState(false); + + const handleSubmit = async (e) => { + e.preventDefault(); + setSaving(true); + setSuccess(false); + setError(null); + + const form = new FormData(e.target); + const data = { + username: form.get("username"), + password: form.get("password"), + }; + + const { success, error } = await System.setupMultiUser(data); + if (success) { + setSuccess(true); + setSaving(false); + setTimeout(() => { + window.localStorage.removeItem(AUTH_USER); + window.localStorage.removeItem(AUTH_TOKEN); + window.location = paths.admin.users(); + }, 2_000); + return; + } + + setError(error); + setSaving(false); + }; + + return ( +
+
+
+

+ Update your AnythingLLM instance to support multiple concurrent + users with their own workspaces. As the admin you can view all + workspaces and add people into workspaces as well. This change is + not reversible and will permanently alter your AnythingLLM + installation. +

+
+ {(error || success) && ( +
+ {error && ( +
+ {error} +
+ )} + {success && ( +
+ Your page will refresh in a few seconds. +
+ )} +
+ )} +
+
+
+
+ + + +
+
+ {useMultiUserMode && ( + <> +

+ By default, you will be the only admin. As an admin you + will need to create accounts for all new users or admins. + Do not lose your password as only an Admin user can reset + passwords. +

+
+ + +
+
+ + +
+ + + )} +
+
+
+
+
+ +
+
+
+ ); +} diff --git a/frontend/src/components/Modals/Settings/PasswordProtection/index.jsx b/frontend/src/components/Modals/Settings/PasswordProtection/index.jsx index 2b6444edba7..387c44bc68a 100644 --- a/frontend/src/components/Modals/Settings/PasswordProtection/index.jsx +++ b/frontend/src/components/Modals/Settings/PasswordProtection/index.jsx @@ -1,13 +1,16 @@ -import React, { useState, useEffect } from "react"; +import React, { useState } from "react"; import System from "../../../../models/system"; +import { AUTH_TOKEN, AUTH_USER } from "../../../../utils/constants"; const noop = () => false; -export default function PasswordProtection({ hideModal = noop }) { - const [loading, setLoading] = useState(true); +export default function PasswordProtection({ + hideModal = noop, + settings = {}, +}) { const [saving, setSaving] = useState(false); const [success, setSuccess] = useState(false); const [error, setError] = useState(null); - const [usePassword, setUsePassword] = useState(false); + const [usePassword, setUsePassword] = useState(settings?.RequiresAuth); const handleSubmit = async (e) => { e.preventDefault(); @@ -26,7 +29,8 @@ export default function PasswordProtection({ hideModal = noop }) { setSuccess(true); setSaving(false); setTimeout(() => { - window.localStorage.removeItem("anythingllm_authToken"); + window.localStorage.removeItem(AUTH_USER); + window.localStorage.removeItem(AUTH_TOKEN); window.location.reload(); }, 2_000); return; @@ -36,15 +40,6 @@ export default function PasswordProtection({ hideModal = noop }) { setSaving(false); }; - useEffect(() => { - async function fetchKeys() { - const settings = await System.keys(); - setUsePassword(settings?.RequiresAuth); - setLoading(false); - } - fetchKeys(); - }, []); - return (
@@ -69,62 +64,54 @@ export default function PasswordProtection({ hideModal = noop }) {
)}
- {loading ? ( -
-

- loading system settings -

-
- ) : ( -
-
-
- +
+ +
+ -
+
+ {usePassword && ( +
+ setUsePassword(!usePassword)} - checked={usePassword} - className="peer sr-only pointer-events-none" + name="password" + type="text" + className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-stone-600 dark:border-stone-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" + placeholder="Your Instance Password" + minLength={8} + required={true} + autoComplete="off" /> -
- -
-
- {usePassword && ( -
- - -
- )} - -
- -
- )} +
+ )} + +
+ +
- +
- + {loading ? ( +
+
+
+ ) : ( + + )}
); } -function SettingTabs({ selectedTab, changeTab }) { +function SettingTabs({ selectedTab, changeTab, settings, user }) { + if (!settings) { + return ( +
+
+
+ ); + } + return ( -
-
    - } - onClick={changeTab} - /> - } - onClick={changeTab} - /> - } - onClick={changeTab} - /> -
-
+
    + } + onClick={changeTab} + /> + } + onClick={changeTab} + /> + {!settings?.MultiUserMode ? ( + <> + } + onClick={changeTab} + /> + } + onClick={changeTab} + /> + + ) : ( + + )} +
); } @@ -102,6 +150,25 @@ function SettingTab({ ); } +function LogoutTab({ user }) { + if (!user) return null; + + return ( +
  • + +
  • + ); +} + export function useSystemSettingsModal() { const [showing, setShowing] = useState(false); const showModal = () => { diff --git a/frontend/src/components/Preloader.jsx b/frontend/src/components/Preloader.jsx new file mode 100644 index 00000000000..728f41bfc4c --- /dev/null +++ b/frontend/src/components/Preloader.jsx @@ -0,0 +1,16 @@ +export default function PreLoader() { + return ( +
    + ); +} + +export function FullScreenLoader() { + return ( +
    +
    +
    + ); +} diff --git a/frontend/src/components/PrivateRoute/index.jsx b/frontend/src/components/PrivateRoute/index.jsx new file mode 100644 index 00000000000..33f5c63371b --- /dev/null +++ b/frontend/src/components/PrivateRoute/index.jsx @@ -0,0 +1,63 @@ +import { useEffect, useState } from "react"; +import { Navigate } from "react-router-dom"; +import { FullScreenLoader } from "../Preloader"; +import validateSessionTokenForUser from "../../utils/session"; +import paths from "../../utils/paths"; +import { AUTH_TOKEN, AUTH_USER } from "../../utils/constants"; +import { userFromStorage } from "../../utils/request"; +import System from "../../models/system"; + +// Used only for Multi-user mode only as we permission specific pages based on auth role. +// When in single user mode we just bypass any authchecks. +function useIsAuthenticated() { + const [isAuthd, setIsAuthed] = useState(null); + + useEffect(() => { + const validateSession = async () => { + const multiUserMode = (await System.keys()).MultiUserMode; + if (!multiUserMode) { + setIsAuthed(true); + return; + } + + const localUser = localStorage.getItem(AUTH_USER); + const localAuthToken = localStorage.getItem(AUTH_TOKEN); + if (!localUser || !localAuthToken) { + setIsAuthed(false); + return; + } + + const isValid = await validateSessionTokenForUser(); + if (!isValid) { + localStorage.removeItem(AUTH_USER); + localStorage.removeItem(AUTH_TOKEN); + setIsAuthed(false); + return; + } + + setIsAuthed(true); + }; + validateSession(); + }, []); + + return isAuthd; +} + +export function AdminRoute({ Component }) { + const authed = useIsAuthenticated(); + if (authed === null) return ; + + const user = userFromStorage(); + return authed && user?.role === "admin" ? ( + + ) : ( + + ); +} + +export default function PrivateRoute({ Component }) { + const authed = useIsAuthenticated(); + if (authed === null) return ; + + return authed ? : ; +} diff --git a/frontend/src/components/Sidebar/index.jsx b/frontend/src/components/Sidebar/index.jsx index 6abdd51d415..ad32f42cba4 100644 --- a/frontend/src/components/Sidebar/index.jsx +++ b/frontend/src/components/Sidebar/index.jsx @@ -144,9 +144,9 @@ export default function Sidebar() { } export function SidebarMobileHeader() { + const sidebarRef = useRef(null); const [showSidebar, setShowSidebar] = useState(false); const [showBgOverlay, setShowBgOverlay] = useState(false); - const sidebarRef = useRef(null); const { showing: showingSystemSettingsModal, showModal: showSystemSettingsModal, diff --git a/frontend/src/hooks/useQuery.js b/frontend/src/hooks/useQuery.js new file mode 100644 index 00000000000..2af24ed4816 --- /dev/null +++ b/frontend/src/hooks/useQuery.js @@ -0,0 +1,3 @@ +export default function useQuery() { + return new URLSearchParams(window.location.search); +} diff --git a/frontend/src/hooks/useUser.js b/frontend/src/hooks/useUser.js new file mode 100644 index 00000000000..c3feb04bb8f --- /dev/null +++ b/frontend/src/hooks/useUser.js @@ -0,0 +1,18 @@ +import { useContext } from "react"; +import { AuthContext } from "../AuthContext"; + +// interface IStore { +// store: { +// user: { +// id: string; +// username: string | null; +// role: string; +// }; +// }; +// } + +export default function useUser() { + const context = useContext(AuthContext); + + return { ...context.store }; +} diff --git a/frontend/src/models/system.js b/frontend/src/models/system.js index 7e5f61bfdd4..43c0013f2ae 100644 --- a/frontend/src/models/system.js +++ b/frontend/src/models/system.js @@ -98,6 +98,18 @@ const System = { return { success: false, error: e.message }; }); }, + setupMultiUser: async (data) => { + return await fetch(`${API_BASE}/system/enable-multi-user`, { + method: "POST", + headers: baseHeaders(), + body: JSON.stringify(data), + }) + .then((res) => res.json()) + .catch((e) => { + console.error(e); + return { success: false, error: e.message }; + }); + }, deleteDocument: async (name, meta) => { return await fetch(`${API_BASE}/system/remove-document`, { method: "DELETE", diff --git a/frontend/src/pages/Main/index.jsx b/frontend/src/pages/Main/index.jsx index 29b5ee3e722..d65881812a1 100644 --- a/frontend/src/pages/Main/index.jsx +++ b/frontend/src/pages/Main/index.jsx @@ -9,11 +9,11 @@ import PasswordModal, { import { isMobile } from "react-device-detect"; export default function Main() { - const { requiresAuth } = usePasswordModal(); + const { requiresAuth, mode } = usePasswordModal(); if (requiresAuth === null || requiresAuth) { return ( <> - {requiresAuth && } + {requiresAuth && }
    {!isMobile && } diff --git a/frontend/src/pages/WorkspaceChat/index.jsx b/frontend/src/pages/WorkspaceChat/index.jsx index 1eec9041243..612b51a0be6 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 53995e6ad14..e96e6836382 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 03ff7b54f55..05920c8fa25 100644 --- a/frontend/src/utils/paths.js +++ b/frontend/src/utils/paths.js @@ -27,4 +27,9 @@ export default { exports: () => { return `${API_BASE.replace("/api", "")}/system/data-exports`; }, + admin: { + users: () => { + return `/admin/users`; + }, + }, }; diff --git a/frontend/src/utils/request.js b/frontend/src/utils/request.js index 4de7681e988..271c97b2004 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 00000000000..27228e482b4 --- /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/server/.env.example b/server/.env.example index a74cec57059..f671f78dfce 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/system.js b/server/endpoints/system.js index a39ef3a3b14..b3a8ed4715f 100644 --- a/server/endpoints/system.js +++ b/server/endpoints/system.js @@ -11,9 +11,11 @@ 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 } = require("../utils/http"); const { setupDataImports } = require("../utils/files/multer"); const { v4 } = require("uuid"); +const { SystemSettings } = require("../models/systemSettings"); +const { User } = require("../models/user"); const { handleImports } = setupDataImports(); function systemEndpoints(app) { @@ -28,8 +30,11 @@ function systemEndpoints(app) { response.sendStatus(200); }); - app.get("/setup-complete", (_, response) => { + app.get("/setup-complete", async (_, response) => { try { + const multiUserMode = + (await SystemSettings.get(`label = 'multi_user_mode'`))?.value || + "false"; const vectorDB = process.env.VECTOR_DB || "pinecone"; const results = { CanDebug: !!!process.env.NO_DEBUG, @@ -40,6 +45,7 @@ function systemEndpoints(app) { AuthToken: !!process.env.AUTH_TOKEN, JWTSecret: !!process.env.JWT_SECRET, StorageDir: process.env.STORAGE_DIR, + MultiUserMode: multiUserMode === "true", ...(vectorDB === "pinecone" ? { PineConeEnvironment: process.env.PINECONE_ENVIRONMENT, @@ -60,28 +66,86 @@ function systemEndpoints(app) { } }); - app.get("/system/check-token", (_, response) => { - response.sendStatus(200).end(); + app.get("/system/check-token", async (request, response) => { + try { + const multiUserMode = + (await SystemSettings.get("label = 'multi_user_mode'"))?.value === + "true"; + if (multiUserMode) { + const user = await userFromSession(request); + if (!user) { + response.sendStatus(403).end(); + return; + } + + response.sendStatus(200).end(); + return; + } + + response.sendStatus(200).end(); + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } }); - app.post("/request-token", (request, response) => { + 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", + const multiUserMode = + (await SystemSettings.get("label = 'multi_user_mode'"))?.value === + "true"; + if (multiUserMode) { + 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; + } + + 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: "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(); @@ -170,6 +234,37 @@ function systemEndpoints(app) { } }); + app.post("/system/enable-multi-user", async (request, response) => { + try { + const { username, password } = reqBody(request); + const multiUserModeEnabled = + (await SystemSettings.get(`label = 'multi_user_mode'`))?.value === + "true"; + 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 }); + 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", async (_, response) => { try { const { filename, error } = await exportData(); diff --git a/server/models/systemSettings.js b/server/models/systemSettings.js new file mode 100644 index 00000000000..4425d938b41 --- /dev/null +++ b/server/models/systemSettings.js @@ -0,0 +1,114 @@ +const SystemSettings = { + supportedFields: ["multi_user_mode"], + 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( + `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 }; + }, +}; + +module.exports.SystemSettings = SystemSettings; diff --git a/server/models/user.js b/server/models/user.js new file mode 100644 index 00000000000..df72673cb4e --- /dev/null +++ b/server/models/user.js @@ -0,0 +1,96 @@ +const User = { + tablename: "users", + writable: [], + colsInit: ` + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE, + password TEXT NOT NULL, + role TEXT NOT NULL DEFAULT "default", + 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( + `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 }; + }, + 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 }; + }, + 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/package.json b/server/package.json index c9cc4d2033e..53fa3c58ccf 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", @@ -41,4 +42,4 @@ "nodemon": "^2.0.22", "prettier": "^2.4.1" } -} \ No newline at end of file +} diff --git a/server/utils/database/index.js b/server/utils/database/index.js index 0b1f42bdcc4..755c640e29d 100644 --- a/server/utils/database/index.js +++ b/server/utils/database/index.js @@ -1,3 +1,5 @@ +const { SystemSettings } = require("../../models/systemSettings"); + function checkColumnTemplate(tablename = null, column = null) { if (!tablename || !column) throw new Error(`Migration Error`, { tablename, column }); @@ -55,6 +57,7 @@ async function validateTablePragmas(force = false) { const { Document } = require("../../models/documents"); const { DocumentVectors } = require("../../models/vectors"); const { WorkspaceChats } = require("../../models/workspaceChats"); + await SystemSettings.migrateTable(); await Workspace.migrateTable(); await Document.migrateTable(); await DocumentVectors.migrateTable(); diff --git a/server/utils/http/index.js b/server/utils/http/index.js index 9fd643b7563..41a3fc92286 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,11 +20,28 @@ function makeJWT(info = {}, expiry = "30d") { return JWT.sign(info, process.env.JWT_SECRET, { expiresIn: expiry }); } +async function userFromSession(request) { + 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 { id: null, username: null }; } module.exports = { @@ -31,4 +49,5 @@ module.exports = { queryParams, makeJWT, decodeJWT, + userFromSession, }; diff --git a/server/yarn.lock b/server/yarn.lock index d5cef6394b4..6f8ccca3e4c 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" @@ -1654,6 +1662,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" From 57a563ff408aa87024d7ea955b062c59357ad0f7 Mon Sep 17 00:00:00 2001 From: timothycarambat Date: Sun, 23 Jul 2023 13:47:20 -0700 Subject: [PATCH 02/15] WIP MUM features --- frontend/src/App.jsx | 9 +- .../src/components/AdminSidebar/index.jsx | 233 ++++++++++++++++ .../src/components/Modals/Settings/index.jsx | 2 +- frontend/src/components/Sidebar/index.jsx | 17 ++ .../ChatHistory/HistoricalMessage/index.jsx | 3 +- frontend/src/hooks/usePrefersDarkMode.js | 9 + frontend/src/index.css | 20 ++ frontend/src/models/admin.js | 54 ++++ frontend/src/models/workspace.js | 6 +- .../pages/Admin/Users/NewUserModal/index.jsx | 126 +++++++++ .../Users/UserRow/EditUserModal/index.jsx | 123 +++++++++ .../src/pages/Admin/Users/UserRow/index.jsx | 52 ++++ frontend/src/pages/Admin/Users/index.jsx | 104 +++++++ server/endpoints/admin.js | 86 ++++++ server/endpoints/chat.js | 53 ++-- server/endpoints/system.js | 257 ++++++++++-------- server/endpoints/workspaces.js | 202 ++++++++------ server/index.js | 9 +- server/models/documents.js | 2 +- server/models/systemSettings.js | 5 +- server/models/user.js | 78 +++++- server/models/vectors.js | 2 +- server/models/workspace.js | 55 +++- server/models/workspaceChats.js | 39 ++- server/models/workspaceUsers.js | 117 ++++++++ server/package.json | 4 +- server/utils/chats/commands/reset.js | 4 +- server/utils/chats/index.js | 14 +- server/utils/database/index.js | 8 +- server/utils/http/index.js | 13 +- server/utils/middleware/validatedRequest.js | 40 ++- .../utils/vectorDbProviders/chroma/index.js | 5 + server/utils/vectorDbProviders/lance/index.js | 8 + .../utils/vectorDbProviders/pinecone/index.js | 5 + server/yarn.lock | 201 ++++++++------ 35 files changed, 1629 insertions(+), 336 deletions(-) create mode 100644 frontend/src/components/AdminSidebar/index.jsx create mode 100644 frontend/src/hooks/usePrefersDarkMode.js create mode 100644 frontend/src/models/admin.js create mode 100644 frontend/src/pages/Admin/Users/NewUserModal/index.jsx create mode 100644 frontend/src/pages/Admin/Users/UserRow/EditUserModal/index.jsx create mode 100644 frontend/src/pages/Admin/Users/UserRow/index.jsx create mode 100644 frontend/src/pages/Admin/Users/index.jsx create mode 100644 server/endpoints/admin.js create mode 100644 server/models/workspaceUsers.js diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 024dae04b4f..6a0204a1265 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,10 +1,11 @@ import React, { lazy, Suspense } from "react"; import { Routes, Route } from "react-router-dom"; import { ContextWrapper } from "./AuthContext"; -import PrivateRoute from "./components/PrivateRoute"; +import PrivateRoute, { AdminRoute } from "./components/PrivateRoute"; const Main = lazy(() => import("./pages/Main")); const WorkspaceChat = lazy(() => import("./pages/WorkspaceChat")); +const AdminUsers = lazy(() => import("./pages/Admin/Users")); export default function App() { return ( @@ -16,6 +17,12 @@ export default function App() { path="/workspace/:slug" element={} /> + + {/* Admin Routes */} + } + /> diff --git a/frontend/src/components/AdminSidebar/index.jsx b/frontend/src/components/AdminSidebar/index.jsx new file mode 100644 index 00000000000..3b736890f58 --- /dev/null +++ b/frontend/src/components/AdminSidebar/index.jsx @@ -0,0 +1,233 @@ +import React, { useEffect, useRef, useState } from "react"; +import { BookOpen, GitHub, Menu, Users, X } from "react-feather"; +import IndexCount from "../Sidebar/IndexCount"; +import LLMStatus from "../Sidebar/LLMStatus"; +import paths from "../../utils/paths"; +import Discord from "../Icons/Discord"; + +export default function AdminSidebar() { + const sidebarRef = useRef(null); + return ( + <> +
    +
    + {/* Header Information */} +
    +

    + AnythingLLM Admin +

    +
    + + + +
    +
    + + {/* Primary Body */} +
    +
    +
    +
    +
    +
    +
    +
    + + +
    +
    + + {/* Footer */} + +
    +
    +
    +
    + + ); +} + +export function SidebarMobileHeader() { + const sidebarRef = useRef(null); + const [showSidebar, setShowSidebar] = useState(false); + const [showBgOverlay, setShowBgOverlay] = useState(false); + + useEffect(() => { + function handleBg() { + if (showSidebar) { + setTimeout(() => { + setShowBgOverlay(true); + }, 300); + } else { + setShowBgOverlay(false); + } + } + handleBg(); + }, [showSidebar]); + + return ( + <> +
    + +

    + AnythingLLM +

    +
    +
    +
    setShowSidebar(false)} + /> +
    +
    + {/* Header Information */} +
    +

    + AnythingLLM Admin +

    +
    + + + +
    +
    + + {/* Primary Body */} +
    +
    +
    +
    +
    +
    +
    +
    + + +
    +
    + + {/* Footer */} + +
    +
    +
    +
    +
    + + ); +} + +const Option = ({ btnText, icon, href }) => { + const isActive = window.location.pathname === href; + return ( + + ); +}; diff --git a/frontend/src/components/Modals/Settings/index.jsx b/frontend/src/components/Modals/Settings/index.jsx index 7781f37e083..9d02d94e8b7 100644 --- a/frontend/src/components/Modals/Settings/index.jsx +++ b/frontend/src/components/Modals/Settings/index.jsx @@ -163,7 +163,7 @@ function LogoutTab({ user }) { }} className="flex items-center gap-x-1 p-4 border-b-2 rounded-t-lg group whitespace-nowrap border-transparent hover:text-gray-600 hover:border-gray-300 dark:hover:text-gray-300" > - Log out + Log out of {user.username} ); diff --git a/frontend/src/components/Sidebar/index.jsx b/frontend/src/components/Sidebar/index.jsx index ad32f42cba4..80f957401d0 100644 --- a/frontend/src/components/Sidebar/index.jsx +++ b/frontend/src/components/Sidebar/index.jsx @@ -6,6 +6,7 @@ import { GitHub, Menu, Plus, + Shield, Tool, } from "react-feather"; import IndexCount from "./IndexCount"; @@ -19,6 +20,7 @@ import NewWorkspaceModal, { import ActiveWorkspaces from "./ActiveWorkspaces"; import paths from "../../utils/paths"; import Discord from "../Icons/Discord"; +import useUser from "../../hooks/useUser"; export default function Sidebar() { const sidebarRef = useRef(null); @@ -47,6 +49,7 @@ export default function Sidebar() { AnythingLLM

    +
    - +
    ); } diff --git a/frontend/src/hooks/usePrefersDarkMode.js b/frontend/src/hooks/usePrefersDarkMode.js new file mode 100644 index 00000000000..2c14e810de0 --- /dev/null +++ b/frontend/src/hooks/usePrefersDarkMode.js @@ -0,0 +1,9 @@ +export default function usePrefersDarkMode() { + if (window?.matchMedia) { + if (window?.matchMedia("(prefers-color-scheme: dark)")?.matches) { + return true; + } + return false; + } + return false; +} diff --git a/frontend/src/index.css b/frontend/src/index.css index 1a7212337f0..672ce97c2d6 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -295,3 +295,23 @@ a { .grid-loader > circle { fill: #008eff; } + +dialog { + pointer-events: none; + opacity: 0; + transition: opacity 0.2s; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +dialog[open] { + opacity: 1; + pointer-events: inherit; +} + +dialog::backdrop { + background: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(2px); +} diff --git a/frontend/src/models/admin.js b/frontend/src/models/admin.js new file mode 100644 index 00000000000..49d6cc3ad5e --- /dev/null +++ b/frontend/src/models/admin.js @@ -0,0 +1,54 @@ +import { API_BASE } from "../utils/constants"; +import { baseHeaders } from "../utils/request"; + +const Admin = { + users: async () => { + return await fetch(`${API_BASE}/admin/users`, { + method: "GET", + headers: baseHeaders(), + }) + .then((res) => res.json()) + .then((res) => res?.users || []) + .catch((e) => { + console.error(e); + return []; + }); + }, + newUser: async (data) => { + return await fetch(`${API_BASE}/admin/users/new`, { + method: "POST", + headers: baseHeaders(), + body: JSON.stringify(data), + }) + .then((res) => res.json()) + .catch((e) => { + console.error(e); + return { user: null, error: e.message }; + }); + }, + updateUser: async (userId, data) => { + return await fetch(`${API_BASE}/admin/user/${userId}`, { + method: "POST", + headers: baseHeaders(), + body: JSON.stringify(data), + }) + .then((res) => res.json()) + .catch((e) => { + console.error(e); + return { success: false, error: e.message }; + }); + }, + deleteUser: async (userId) => { + return await fetch(`${API_BASE}/admin/user/${userId}`, { + method: "DELETE", + headers: baseHeaders(), + }) + .then((res) => res.json()) + .catch((e) => { + console.error(e); + return { success: false, error: e.message }; + }); + }, +}; + +export default Admin; diff --git a/frontend/src/models/workspace.js b/frontend/src/models/workspace.js index e8aa0bce1ae..ac61c71863a 100644 --- a/frontend/src/models/workspace.js +++ b/frontend/src/models/workspace.js @@ -49,6 +49,7 @@ const Workspace = { }, chatHistory: async function (slug) { const history = await fetch(`${API_BASE}/workspace/${slug}/chats`, { + method: "GET", headers: baseHeaders(), }) .then((res) => res.json()) @@ -71,7 +72,10 @@ const Workspace = { return chatResult; }, all: async function () { - const workspaces = await fetch(`${API_BASE}/workspaces`) + const workspaces = await fetch(`${API_BASE}/workspaces`, { + method: "GET", + headers: baseHeaders(), + }) .then((res) => res.json()) .then((res) => res.workspaces || []) .catch(() => []); diff --git a/frontend/src/pages/Admin/Users/NewUserModal/index.jsx b/frontend/src/pages/Admin/Users/NewUserModal/index.jsx new file mode 100644 index 00000000000..f705ea38365 --- /dev/null +++ b/frontend/src/pages/Admin/Users/NewUserModal/index.jsx @@ -0,0 +1,126 @@ +import React, { useState } from "react"; +import { X } from "react-feather"; +import Admin from "../../../../models/admin"; +const DIALOG_ID = `new-user-modal`; + +function hideModal() { + document.getElementById(DIALOG_ID)?.close(); +} + +export const NewUserModalId = DIALOG_ID; +export default function NewUserModal() { + const [error, setError] = useState(null); + const handleCreate = async (e) => { + setError(null); + e.preventDefault(); + const data = {}; + const form = new FormData(e.target); + for (var [key, value] of form.entries()) data[key] = value; + const { user, error } = await Admin.newUser(data); + if (!!user) window.location.reload(); + setError(error); + }; + + return ( + +
    +
    +
    +

    + Add user to instance +

    + +
    +
    +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    + {error && ( +

    + Error: {error} +

    + )} +

    + After creating a user they will need to login with their + initial login to get access. +

    +
    +
    +
    + + +
    +
    +
    +
    +
    + ); +} diff --git a/frontend/src/pages/Admin/Users/UserRow/EditUserModal/index.jsx b/frontend/src/pages/Admin/Users/UserRow/EditUserModal/index.jsx new file mode 100644 index 00000000000..833a926e98e --- /dev/null +++ b/frontend/src/pages/Admin/Users/UserRow/EditUserModal/index.jsx @@ -0,0 +1,123 @@ +import React, { useState } from "react"; +import { X } from "react-feather"; +import Admin from "../../../../../models/admin"; + +export const EditUserModalId = (user) => `edit-user-${user.id}-modal`; +export default function EditUserModal({ user }) { + const [error, setError] = useState(null); + const hideModal = () => { + document.getElementById(EditUserModalId(user)).close(); + }; + const handleUpdate = async (e) => { + setError(null); + e.preventDefault(); + const data = {}; + const form = new FormData(e.target); + for (var [key, value] of form.entries()) { + if (!value || value === null) continue; + data[key] = value; + } + const { success, error } = await Admin.updateUser(user.id, data); + if (success) window.location.reload(); + setError(error); + }; + + return ( + +
    +
    +
    +

    + Edit {user.username} +

    + +
    +
    +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    + {error && ( +

    + Error: {error} +

    + )} +
    +
    +
    + + +
    +
    +
    +
    +
    + ); +} diff --git a/frontend/src/pages/Admin/Users/UserRow/index.jsx b/frontend/src/pages/Admin/Users/UserRow/index.jsx new file mode 100644 index 00000000000..585a8b20580 --- /dev/null +++ b/frontend/src/pages/Admin/Users/UserRow/index.jsx @@ -0,0 +1,52 @@ +import { useRef } from "react"; +import { titleCase } from "text-case"; +import Admin from "../../../../models/admin"; +import EditUserModal, { EditUserModalId } from "./EditUserModal"; + +export default function UserRow({ currUser, user }) { + const rowRef = useRef(null); + const handleDelete = async () => { + if ( + !window.confirm( + `Are you sure you want to delete ${user.username}?\nAfter you do this they will be logged out and unable to use this instance of AnythingLLM.\n\nThis action is irreversible.` + ) + ) + return false; + rowRef?.current?.remove(); + await Admin.deleteUser(user.id); + }; + + return ( + <> + + + {user.username} + + {titleCase(user.role)} + {user.createdAt} + + + {currUser.id !== user.id && ( + + )} + + + + + ); +} diff --git a/frontend/src/pages/Admin/Users/index.jsx b/frontend/src/pages/Admin/Users/index.jsx new file mode 100644 index 00000000000..b7873bcbb44 --- /dev/null +++ b/frontend/src/pages/Admin/Users/index.jsx @@ -0,0 +1,104 @@ +import { useEffect, useState } from "react"; +import Sidebar, { SidebarMobileHeader } from "../../../components/AdminSidebar"; +import { isMobile } from "react-device-detect"; +import * as Skeleton from "react-loading-skeleton"; +import "react-loading-skeleton/dist/skeleton.css"; +import { UserPlus } from "react-feather"; +import usePrefersDarkMode from "../../../hooks/usePrefersDarkMode"; +import Admin from "../../../models/admin"; +import UserRow from "./UserRow"; +import useUser from "../../../hooks/useUser"; +import NewUserModal, { NewUserModalId } from "./NewUserModal"; + +export default function AdminUsers() { + return ( +
    + {!isMobile && } +
    + {isMobile && } +
    +
    +
    +

    + Instance users +

    + +
    +

    + These are all the accounts which have an account on this instance. + Removing an account will instantly remove their access to this + instance. +

    +
    + +
    + +
    +
    + ); +} + +function UsersContainer() { + const { user: currUser } = useUser(); + const darkMode = usePrefersDarkMode(); + const [loading, setLoading] = useState(true); + const [users, setUsers] = useState([]); + useEffect(() => { + async function fetchUsers() { + const _users = await Admin.users(); + setUsers(_users); + setLoading(false); + } + fetchUsers(); + }, []); + + if (loading) { + return ( + + ); + } + + return ( + + + + + + + + + + + {users.map((user) => ( + + ))} + +
    + Username + + Role + + Created On + + Actions +
    + ); +} diff --git a/server/endpoints/admin.js b/server/endpoints/admin.js new file mode 100644 index 00000000000..c85a113dda1 --- /dev/null +++ b/server/endpoints/admin.js @@ -0,0 +1,86 @@ +const { User } = require("../models/user"); +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(); + } + } + ); +} + +module.exports = { adminEndpoints }; diff --git a/server/endpoints/chat.js b/server/endpoints/chat.js index 7c3036434b9..3f34892d1af 100644 --- a/server/endpoints/chat.js +++ b/server/endpoints/chat.js @@ -1,34 +1,43 @@ 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"); 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}'`); + + if (!workspace) { + response.sendStatus(400).end(); + return; + } - 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, - }); + 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/system.js b/server/endpoints/system.js index b3a8ed4715f..2cd57017350 100644 --- a/server/endpoints/system.js +++ b/server/endpoints/system.js @@ -11,11 +11,17 @@ const { const { purgeDocument } = require("../utils/files/purgeDocument"); const { getVectorDbClass } = require("../utils/helpers"); const { updateENV } = require("../utils/helpers/updateENV"); -const { reqBody, makeJWT, userFromSession } = 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) { @@ -32,9 +38,6 @@ function systemEndpoints(app) { app.get("/setup-complete", async (_, response) => { try { - const multiUserMode = - (await SystemSettings.get(`label = 'multi_user_mode'`))?.value || - "false"; const vectorDB = process.env.VECTOR_DB || "pinecone"; const results = { CanDebug: !!!process.env.NO_DEBUG, @@ -45,7 +48,7 @@ function systemEndpoints(app) { AuthToken: !!process.env.AUTH_TOKEN, JWTSecret: !!process.env.JWT_SECRET, StorageDir: process.env.STORAGE_DIR, - MultiUserMode: multiUserMode === "true", + MultiUserMode: await SystemSettings.isMultiUserMode(), ...(vectorDB === "pinecone" ? { PineConeEnvironment: process.env.PINECONE_ENVIRONMENT, @@ -66,38 +69,36 @@ function systemEndpoints(app) { } }); - app.get("/system/check-token", async (request, response) => { - try { - const multiUserMode = - (await SystemSettings.get("label = 'multi_user_mode'"))?.value === - "true"; - if (multiUserMode) { - const user = await userFromSession(request); - if (!user) { - response.sendStatus(403).end(); + app.get( + "/system/check-token", + [validatedRequest], + async (request, response) => { + try { + if (multiUserMode(response)) { + const user = await userFromSession(request, response); + if (!user) { + response.sendStatus(403).end(); + return; + } + + response.sendStatus(200).end(); return; } response.sendStatus(200).end(); - return; + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); } - - response.sendStatus(200).end(); - } catch (e) { - console.log(e.message, e); - response.sendStatus(500).end(); } - }); + ); app.post("/request-token", async (request, response) => { try { - const multiUserMode = - (await SystemSettings.get("label = 'multi_user_mode'"))?.value === - "true"; - if (multiUserMode) { + if (await SystemSettings.isMultiUserMode()) { const { username, password } = reqBody(request); - const existingUser = await User.get(`username = '${username}'`); + if (!existingUser) { response.status(200).json({ user: null, @@ -152,7 +153,7 @@ function systemEndpoints(app) { } }); - app.get("/system/system-vectors", async (_, response) => { + app.get("/system/system-vectors", [validatedRequest], async (_, response) => { try { const VectorDb = getVectorDbClass(); const vectorCount = await VectorDb.totalIndicies(); @@ -163,18 +164,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 }); @@ -184,88 +189,104 @@ 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/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(); + } } - }); + ); - app.get("/system/accepted-document-types", async (_, response) => { - try { - const types = await acceptedFileTypes(); - if (!types) { - response.sendStatus(404).end(); - return; - } + 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(); + 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.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.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-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", async (request, response) => { - try { - const { username, password } = reqBody(request); - const multiUserModeEnabled = - (await SystemSettings.get(`label = 'multi_user_mode'`))?.value === - "true"; - if (multiUserModeEnabled) { - response - .status(200) - .json({ + 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; - } + return; + } - const { user, error } = await User.create({ - username, - password, - role: "admin", - }); - await SystemSettings.updateSettings({ multi_user_mode: true }); - 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(); + const { user, error } = await User.create({ + username, + password, + role: "admin", + }); + await SystemSettings.updateSettings({ multi_user_mode: true }); + 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", async (_, response) => { + app.get("/system/data-export", [validatedRequest], async (_, response) => { try { const { filename, error } = await exportData(); response.status(200).json({ filename, error }); @@ -275,18 +296,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 3473c6d329d..283d0514dfb 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,17 @@ const { checkPythonAppAlive, processDocument, } = require("../utils/files/documentProcessor"); +const { validatedRequest } = require("../utils/middleware/validatedRequest"); 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 +31,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}'`); - - if (!currWorkspace) { - response.sendStatus(400).end(); - return; + 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; + } + + 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 +90,75 @@ 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; + app.post( + "/workspace/:slug/update-embeddings", + [validatedRequest], + async (request, response) => { + try { + 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}'`); + + 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(); } - - 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.delete( + "/workspace/:slug", + [validatedRequest], + async (request, response) => { try { - await VectorDb["delete-namespace"]({ namespace: slug }); + 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; + } + + 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.error(e.message); + console.log(e.message, e); + response.sendStatus(500).end(); } - response.sendStatus(200).end(); - } catch (e) { - console.log(e.message, e); - response.sendStatus(500).end(); } - }); + ); - app.get("/workspaces", async (_, response) => { + 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 +166,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 +181,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 WorkspaceChats.forWorkspace(workspace.id); + + 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(); } - - 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(); } - }); + ); } module.exports = { workspaceEndpoints }; diff --git a/server/index.js b/server/index.js index 83ff994da44..a2adf32fa62 100644 --- a/server/index.js +++ b/server/index.js @@ -7,13 +7,13 @@ 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 app = express(); const apiRouter = express.Router(); @@ -26,12 +26,11 @@ app.use( }) ); -apiRouter.use("/system/*", validatedRequest); +app.use("/api", apiRouter); systemEndpoints(apiRouter); - -apiRouter.use("/workspace/*", validatedRequest); workspaceEndpoints(apiRouter); chatEndpoints(apiRouter); +adminEndpoints(apiRouter); apiRouter.post("/v/:command", async (request, response) => { try { @@ -61,8 +60,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 777bd7175ac..0de83bcd560 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/systemSettings.js b/server/models/systemSettings.js index 4425d938b41..d2e151ddd78 100644 --- a/server/models/systemSettings.js +++ b/server/models/systemSettings.js @@ -32,7 +32,7 @@ const SystemSettings = { }); 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)); @@ -109,6 +109,9 @@ const SystemSettings = { } 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 index df72673cb4e..15b73d277ff 100644 --- a/server/models/user.js +++ b/server/models/user.js @@ -6,6 +6,7 @@ const User = { 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 `, @@ -30,7 +31,7 @@ const User = { }); 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)); @@ -64,15 +65,88 @@ const User = { 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 } = updates; + const toUpdate = {}; + + 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} WHERE ${clause}`) + .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}`); diff --git a/server/models/vectors.js b/server/models/vectors.js index 9e1a8dd4253..e568097be8e 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 fed92434b68..72480e327fd 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,40 @@ 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; + }, }; module.exports = { Workspace }; diff --git a/server/models/workspaceChats.js b/server/models/workspaceChats.js index 7a2aafb8555..85c9a0b4a47 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,11 +93,13 @@ 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(); diff --git a/server/models/workspaceUsers.js b/server/models/workspaceUsers.js new file mode 100644 index 00000000000..aa0e1646e42 --- /dev/null +++ b/server/models/workspaceUsers.js @@ -0,0 +1,117 @@ +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; + }, + 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 53fa3c58ccf..094dbb4e161 100644 --- a/server/package.json +++ b/server/package.json @@ -23,7 +23,7 @@ "chromadb": "^1.5.2", "cors": "^2.8.5", "dotenv": "^16.0.3", - "express": "^4.18.2", + "express": ">=5.0.0-beta.1", "extract-zip": "^2.0.1", "jsonwebtoken": "^8.5.1", "langchain": "^0.0.90", @@ -42,4 +42,4 @@ "nodemon": "^2.0.22", "prettier": "^2.4.1" } -} +} \ No newline at end of file diff --git a/server/utils/chats/commands/reset.js b/server/utils/chats/commands/reset.js index 59f9448e77b..8851efdf19f 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 800003e2118..5e099dad049 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 755c640e29d..b01ffff2bb4 100644 --- a/server/utils/database/index.js +++ b/server/utils/database/index.js @@ -1,5 +1,3 @@ -const { SystemSettings } = require("../../models/systemSettings"); - function checkColumnTemplate(tablename = null, column = null) { if (!tablename || !column) throw new Error(`Migration Error`, { tablename, column }); @@ -52,13 +50,17 @@ 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"); await SystemSettings.migrateTable(); + await User.migrateTable(); await Workspace.migrateTable(); + await WorkspaceUser.migrateTable(); await Document.migrateTable(); await DocumentVectors.migrateTable(); await WorkspaceChats.migrateTable(); diff --git a/server/utils/http/index.js b/server/utils/http/index.js index 41a3fc92286..3543a36b3b0 100644 --- a/server/utils/http/index.js +++ b/server/utils/http/index.js @@ -20,7 +20,11 @@ function makeJWT(info = {}, expiry = "30d") { return JWT.sign(info, process.env.JWT_SECRET, { expiresIn: expiry }); } -async function userFromSession(request) { +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; @@ -41,11 +45,16 @@ function decodeJWT(jwtToken) { try { return JWT.verify(jwtToken, process.env.JWT_SECRET); } catch {} - return { id: null, username: null }; + return { p: null, id: null, username: null }; +} + +function multiUserMode(response) { + return response?.locals?.multiUserMode; } module.exports = { reqBody, + multiUserMode, queryParams, makeJWT, decodeJWT, diff --git a/server/utils/middleware/validatedRequest.js b/server/utils/middleware/validatedRequest.js index 4e7c519a81d..1ff13f3d0db 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 863b6f1304b..801a41db83f 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 3315028a7bd..ddc184693c7 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 0c03e75bbb4..e34391b1907 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 6f8ccca3e4c..d354e8c3ac8 100644 --- a/server/yarn.lock +++ b/server/yarn.lock @@ -128,7 +128,7 @@ abbrev@1: resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== -accepts@~1.3.4, accepts@~1.3.8: +accepts@~1.3.4, accepts@~1.3.7: version "1.3.8" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== @@ -266,10 +266,10 @@ array-back@^4.0.1, array-back@^4.0.2: resolved "https://registry.yarnpkg.com/array-back/-/array-back-4.0.2.tgz#8004e999a6274586beeb27342168652fdb89fa1e" integrity sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg== -array-flatten@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" - integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== +array-flatten@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-3.0.0.tgz#6428ca2ee52c7b823192ec600fa3ed2f157cd541" + integrity sha512-zPMVc3ZYlGLNk4mpK1NzP2wg0ml9t7fUgDsayR5Y5rSzxQilzR9FGu/EH2jQOcKSAeAfWeylyW8juy3OkWRvNA== arrify@^2.0.0: version "2.0.1" @@ -340,23 +340,21 @@ bl@^4.0.3: inherits "^2.0.4" readable-stream "^3.4.0" -body-parser@1.20.1: - version "1.20.1" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668" - integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw== +body-parser@2.0.0-beta.1: + version "2.0.0-beta.1" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-2.0.0-beta.1.tgz#d4ed97d6ed51f6040b967db0db2252a0b235a661" + integrity sha512-I1v2bt2OdYqtmk8nEFZuEf+9Opb30DphYwTPDbgg/OorSAoJOuTpWyDrZaSWQw7FdoevbBRCP2+9z/halXSWcA== dependencies: - bytes "3.1.2" + bytes "3.1.1" content-type "~1.0.4" debug "2.6.9" - depd "2.0.0" - destroy "1.2.0" - http-errors "2.0.0" + depd "~1.1.2" + http-errors "1.8.1" iconv-lite "0.4.24" - on-finished "2.4.1" - qs "6.11.0" - raw-body "2.5.1" + on-finished "~2.3.0" + qs "6.9.6" + raw-body "2.4.2" type-is "~1.6.18" - unpipe "1.0.0" body-parser@^1.20.2: version "1.20.2" @@ -428,6 +426,11 @@ busboy@^1.0.0: dependencies: streamsearch "^1.1.0" +bytes@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.1.tgz#3f018291cb4cbad9accb6e6970bca9c8889e879a" + integrity sha512-dWe4nWO/ruEOY7HkUJ5gFt1DCFV9zPRoJr8pV0/ASQermOZjtq8jMjOprC0Kd10GLN+l7xaUPvxzJFWtxGu8Fg== + bytes@3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" @@ -597,10 +600,10 @@ cookie-signature@1.0.6: resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== -cookie@0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" - integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== +cookie@0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1" + integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA== core-util-is@~1.0.0: version "1.0.3" @@ -642,6 +645,13 @@ debug@2.6.9: dependencies: ms "2.0.0" +debug@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== + dependencies: + ms "2.0.0" + debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.3: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" @@ -686,6 +696,11 @@ destroy@1.2.0: resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== +destroy@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" + integrity sha512-3NdhDuEXnfun/z7x9GOElY49LoqVHoGScmOKwmxhsS8N5Y+Z8KyPPDnaSzqWgYt/ji4mqwfTS34Htrk0zPIXVg== + detect-libc@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.2.tgz#8ccf2ba9315350e1241b88d0ac3b0e1fbd99605d" @@ -767,39 +782,40 @@ expr-eval@^2.0.2: resolved "https://registry.yarnpkg.com/expr-eval/-/expr-eval-2.0.2.tgz#fa6f044a7b0c93fde830954eb9c5b0f7fbc7e201" integrity sha512-4EMSHGOPSwAfBiibw3ndnP0AvjDWLsMvGOvWEZ2F96IGk0bIVdjQisOHxReSkE13mHcfbuCiXw+G4y0zv6N8Eg== -express@^4.18.2: - version "4.18.2" - resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59" - integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ== +express@>=5.0.0-beta.1: + version "5.0.0-beta.1" + resolved "https://registry.yarnpkg.com/express/-/express-5.0.0-beta.1.tgz#efbfd372e4650a48e417b1adbaf43599092ddc8f" + integrity sha512-KPtBrlZoQu2Ps0Ce/Imqtq73AB0KBJ8Gx59yZQ3pmDJU2/LhcoZETo03oSgtTQufbcLXt/WBITk/jMjl/WMyrQ== dependencies: - accepts "~1.3.8" - array-flatten "1.1.1" - body-parser "1.20.1" + accepts "~1.3.7" + array-flatten "3.0.0" + body-parser "2.0.0-beta.1" content-disposition "0.5.4" content-type "~1.0.4" - cookie "0.5.0" + cookie "0.4.1" cookie-signature "1.0.6" - debug "2.6.9" - depd "2.0.0" + debug "3.1.0" + depd "~1.1.2" encodeurl "~1.0.2" escape-html "~1.0.3" etag "~1.8.1" - finalhandler "1.2.0" + finalhandler "~1.1.2" fresh "0.5.2" - http-errors "2.0.0" merge-descriptors "1.0.1" methods "~1.1.2" - on-finished "2.4.1" + mime-types "~2.1.34" + on-finished "~2.3.0" parseurl "~1.3.3" - path-to-regexp "0.1.7" + path-is-absolute "1.0.1" proxy-addr "~2.0.7" - qs "6.11.0" + qs "6.9.6" range-parser "~1.2.1" + router "2.0.0-beta.1" safe-buffer "5.2.1" - send "0.18.0" - serve-static "1.15.0" + send "1.0.0-beta.1" + serve-static "2.0.0-beta.1" setprototypeof "1.2.0" - statuses "2.0.1" + statuses "~1.5.0" type-is "~1.6.18" utils-merge "1.0.1" vary "~1.1.2" @@ -839,17 +855,17 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" -finalhandler@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" - integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== +finalhandler@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" + integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== dependencies: debug "2.6.9" encodeurl "~1.0.2" escape-html "~1.0.3" - on-finished "2.4.1" + on-finished "~2.3.0" parseurl "~1.3.3" - statuses "2.0.1" + statuses "~1.5.0" unpipe "~1.0.0" find-replace@^3.0.0: @@ -1083,6 +1099,17 @@ http-cache-semantics@^4.1.0: resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a" integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ== +http-errors@1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.8.1.tgz#7c3f28577cbc8a207388455dbd62295ed07bd68c" + integrity sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g== + dependencies: + depd "~1.1.2" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.1" + http-errors@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" @@ -1488,11 +1515,6 @@ mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.24, mime-types@~2.1.34: dependencies: mime-db "1.52.0" -mime@1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" - integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== - minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" @@ -1777,6 +1799,13 @@ on-finished@2.4.1: dependencies: ee-first "1.1.1" +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + integrity sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww== + dependencies: + ee-first "1.1.1" + once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -1839,15 +1868,15 @@ parseurl@~1.3.2, parseurl@~1.3.3: resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== -path-is-absolute@^1.0.0: +path-is-absolute@1.0.1, path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== -path-to-regexp@0.1.7: - version "0.1.7" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" - integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== +path-to-regexp@3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-3.2.0.tgz#fa7877ecbc495c601907562222453c43cc204a5f" + integrity sha512-jczvQbCUS7XmS7o+y1aEO9OBVFeZBQ1MDSEqmO7xSoPgOPoowY/SxLpZ6Vh97/8qHZOteiCKb7gkG9gA2ZUxJA== pend@~1.2.0: version "1.2.0" @@ -1917,6 +1946,11 @@ qs@6.11.0: dependencies: side-channel "^1.0.4" +qs@6.9.6: + version "6.9.6" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.6.tgz#26ed3c8243a431b2924aca84cc90471f35d5a0ee" + integrity sha512-TIRk4aqYLNoJUbd+g2lEdz5kLWIuTMRagAXxl78Q0RiVjAOugHmeKNGdd3cwo/ktpf9aL9epCfFqWDEKysUlLQ== + qs@^6.7.0: version "6.11.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.2.tgz#64bea51f12c1f5da1bc01496f48ffcff7c69d7d9" @@ -1929,13 +1963,13 @@ range-parser@~1.2.1: resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== -raw-body@2.5.1: - version "2.5.1" - resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857" - integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig== +raw-body@2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.2.tgz#baf3e9c21eebced59dd6533ac872b71f7b61cb32" + integrity sha512-RPMAFUJP19WIet/99ngh6Iv8fzAbqum4Li7AD6DtGaW2RpMB/11xDoalPiJMTbu6I3hkbMVkATvZrqb9EEqeeQ== dependencies: - bytes "3.1.2" - http-errors "2.0.0" + bytes "3.1.1" + http-errors "1.8.1" iconv-lite "0.4.24" unpipe "1.0.0" @@ -2012,6 +2046,18 @@ rimraf@^3.0.2: dependencies: glob "^7.1.3" +router@2.0.0-beta.1: + version "2.0.0-beta.1" + resolved "https://registry.yarnpkg.com/router/-/router-2.0.0-beta.1.tgz#86fb03143cee259497d8b658c7f13b1d20049331" + integrity sha512-GLoYgkhAGAiwVda5nt6Qd4+5RAPuQ4WIYLlZ+mxfYICI+22gnIB3eCfmhgV8+uJNPS1/39DOYi/vdrrz0/ouKA== + dependencies: + array-flatten "3.0.0" + methods "~1.1.2" + parseurl "~1.3.3" + path-to-regexp "3.2.0" + setprototypeof "1.2.0" + utils-merge "1.0.1" + safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" @@ -2049,24 +2095,23 @@ semver@~7.0.0: resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== -send@0.18.0: - version "0.18.0" - resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" - integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== +send@1.0.0-beta.1: + version "1.0.0-beta.1" + resolved "https://registry.yarnpkg.com/send/-/send-1.0.0-beta.1.tgz#9db741443b0b25771cfd38adc833376ae2d606fe" + integrity sha512-OKTRokcl/oo34O8+6aUpj8Jf2Bjw2D0tZzmX0/RvyfVC9ZOZW+HPAWAlhS817IsRaCnzYX1z++h2kHFr2/KNRg== dependencies: - debug "2.6.9" - depd "2.0.0" - destroy "1.2.0" + debug "3.1.0" + destroy "~1.0.4" encodeurl "~1.0.2" escape-html "~1.0.3" etag "~1.8.1" fresh "0.5.2" - http-errors "2.0.0" - mime "1.6.0" + http-errors "1.8.1" + mime-types "~2.1.34" ms "2.1.3" - on-finished "2.4.1" + on-finished "~2.3.0" range-parser "~1.2.1" - statuses "2.0.1" + statuses "~1.5.0" serve-index@^1.9.1: version "1.9.1" @@ -2081,15 +2126,15 @@ serve-index@^1.9.1: mime-types "~2.1.17" parseurl "~1.3.2" -serve-static@1.15.0: - version "1.15.0" - resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" - integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== +serve-static@2.0.0-beta.1: + version "2.0.0-beta.1" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-2.0.0-beta.1.tgz#003bc35db8b37905e1f6d380a4ccc0a34f30ec1d" + integrity sha512-DEJ9on/tQeFO2Omj7ovT02lCp1YgP4Kb8W2lv2o/4keTFAbgc8HtH3yPd47++2wv9lvQeqiA7FHFDe5+8c4XpA== dependencies: encodeurl "~1.0.2" escape-html "~1.0.3" parseurl "~1.3.3" - send "0.18.0" + send "1.0.0-beta.1" set-blocking@^2.0.0: version "2.0.0" @@ -2182,7 +2227,7 @@ statuses@2.0.1: resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== -"statuses@>= 1.4.0 < 2": +"statuses@>= 1.4.0 < 2", "statuses@>= 1.5.0 < 2", statuses@~1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== From d42ac065692b17ec5f2d9a47dbc366d7033b623c Mon Sep 17 00:00:00 2001 From: timothycarambat Date: Mon, 24 Jul 2023 11:11:00 -0700 Subject: [PATCH 03/15] invitation mgmt --- frontend/src/App.jsx | 7 + .../src/components/AdminSidebar/index.jsx | 7 +- frontend/src/models/admin.js | 37 ++++ frontend/src/models/invite.js | 27 +++ .../Admin/Invitations/InviteRow/index.jsx | 80 ++++++++ .../Invitations/NewInviteModal/index.jsx | 113 +++++++++++ .../src/pages/Admin/Invitations/index.jsx | 104 ++++++++++ .../pages/Admin/Users/NewUserModal/index.jsx | 2 +- .../src/pages/Invite/NewUserModal/index.jsx | 90 +++++++++ frontend/src/pages/Invite/index.jsx | 53 +++++ frontend/src/utils/paths.js | 3 + server/endpoints/admin.js | 58 ++++++ server/endpoints/invite.js | 63 ++++++ server/index.js | 2 + server/models/invite.js | 191 ++++++++++++++++++ server/package.json | 3 +- server/utils/database/index.js | 3 + server/yarn.lock | 30 +++ 18 files changed, 870 insertions(+), 3 deletions(-) create mode 100644 frontend/src/models/invite.js create mode 100644 frontend/src/pages/Admin/Invitations/InviteRow/index.jsx create mode 100644 frontend/src/pages/Admin/Invitations/NewInviteModal/index.jsx create mode 100644 frontend/src/pages/Admin/Invitations/index.jsx create mode 100644 frontend/src/pages/Invite/NewUserModal/index.jsx create mode 100644 frontend/src/pages/Invite/index.jsx create mode 100644 server/endpoints/invite.js create mode 100644 server/models/invite.js diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 6a0204a1265..ae9a65a7d7d 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -2,10 +2,12 @@ import React, { lazy, Suspense } from "react"; import { Routes, Route } from "react-router-dom"; import { ContextWrapper } from "./AuthContext"; import PrivateRoute, { AdminRoute } from "./components/PrivateRoute"; +import InvitePage from "./pages/Invite"; const Main = lazy(() => import("./pages/Main")); const WorkspaceChat = lazy(() => import("./pages/WorkspaceChat")); const AdminUsers = lazy(() => import("./pages/Admin/Users")); +const AdminInvites = lazy(() => import("./pages/Admin/Invitations")); export default function App() { return ( @@ -17,8 +19,13 @@ export default function App() { path="/workspace/:slug" element={} /> + } /> {/* Admin Routes */} + } + /> } diff --git a/frontend/src/components/AdminSidebar/index.jsx b/frontend/src/components/AdminSidebar/index.jsx index 3b736890f58..e7036068222 100644 --- a/frontend/src/components/AdminSidebar/index.jsx +++ b/frontend/src/components/AdminSidebar/index.jsx @@ -1,5 +1,5 @@ import React, { useEffect, useRef, useState } from "react"; -import { BookOpen, GitHub, Menu, Users, X } from "react-feather"; +import { BookOpen, GitHub, Mail, Menu, Users, X } from "react-feather"; import IndexCount from "../Sidebar/IndexCount"; import LLMStatus from "../Sidebar/LLMStatus"; import paths from "../../utils/paths"; @@ -34,6 +34,11 @@ export default function AdminSidebar() {
    +
    diff --git a/frontend/src/index.css b/frontend/src/index.css index 672ce97c2d6..225a8d45724 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -256,42 +256,6 @@ a { height: 100px !important; } -.blink { - animation: blink 1.5s steps(1) infinite; -} - -@keyframes blink { - 0% { - opacity: 0; - } - - 50% { - opacity: 1; - } - - 100% { - opacity: 0; - } -} - -.background-animate { - background-size: 400%; - -webkit-animation: bgAnimate 10s ease infinite; - -moz-animation: bgAnimate 10s ease infinite; - animation: bgAnimate 10s ease infinite; -} - -@keyframes bgAnimate { - 0%, - 100% { - background-position: 0% 50%; - } - - 50% { - background-position: 100% 50%; - } -} - .grid-loader > circle { fill: #008eff; } @@ -299,7 +263,6 @@ a { dialog { pointer-events: none; opacity: 0; - transition: opacity 0.2s; display: flex; flex-direction: column; align-items: center; diff --git a/frontend/src/models/admin.js b/frontend/src/models/admin.js index 29a1e57cc35..4afa3a6209d 100644 --- a/frontend/src/models/admin.js +++ b/frontend/src/models/admin.js @@ -86,6 +86,58 @@ const Admin = { return { success: false, error: e.message }; }); }, + + // Workspaces Mgmt + workspaces: async () => { + return await fetch(`${API_BASE}/admin/workspaces`, { + method: "GET", + headers: baseHeaders(), + }) + .then((res) => res.json()) + .then((res) => res?.workspaces || []) + .catch((e) => { + console.error(e); + return []; + }); + }, + newWorkspace: async (name) => { + return await fetch(`${API_BASE}/admin/workspaces/new`, { + method: "POST", + headers: baseHeaders(), + body: JSON.stringify({ name }), + }) + .then((res) => res.json()) + .catch((e) => { + console.error(e); + return { workspace: null, error: e.message }; + }); + }, + updateUsersInWorkspace: async (workspaceId, userIds = []) => { + return await fetch( + `${API_BASE}/admin/workspaces/${workspaceId}/update-users`, + { + method: "POST", + headers: baseHeaders(), + body: JSON.stringify({ userIds }), + } + ) + .then((res) => res.json()) + .catch((e) => { + console.error(e); + return { success: false, error: e.message }; + }); + }, + deleteWorkspace: async (workspaceId) => { + return await fetch(`${API_BASE}/admin/workspaces/${workspaceId}`, { + method: "DELETE", + headers: baseHeaders(), + }) + .then((res) => res.json()) + .catch((e) => { + console.error(e); + return { success: false, error: e.message }; + }); + }, }; export default Admin; diff --git a/frontend/src/pages/Admin/Workspaces/NewWorkspaceModal/index.jsx b/frontend/src/pages/Admin/Workspaces/NewWorkspaceModal/index.jsx new file mode 100644 index 00000000000..bc38c191037 --- /dev/null +++ b/frontend/src/pages/Admin/Workspaces/NewWorkspaceModal/index.jsx @@ -0,0 +1,90 @@ +import React, { useState } from "react"; +import { X } from "react-feather"; +import Admin from "../../../../models/admin"; +const DIALOG_ID = `new-workspace-modal`; + +function hideModal() { + document.getElementById(DIALOG_ID)?.close(); +} + +export const NewWorkspaceModalId = DIALOG_ID; +export default function NewWorkspaceModal() { + const [error, setError] = useState(null); + const handleCreate = async (e) => { + setError(null); + e.preventDefault(); + const form = new FormData(e.target); + const { workspace, error } = await Admin.newWorkspace(form.get("name")); + if (!!workspace) window.location.reload(); + setError(error); + }; + + return ( + +
    +
    +
    +

    + Add workspace to Instance +

    + +
    +
    +
    +
    +
    + + +
    + {error && ( +

    + Error: {error} +

    + )} +

    + After creating this workspace only admins will be able to see + it. You can add users after it has been created. +

    +
    +
    +
    + + +
    +
    +
    +
    +
    + ); +} diff --git a/frontend/src/pages/Admin/Workspaces/WorkspaceRow/EditWorkspaceUsersModal/index.jsx b/frontend/src/pages/Admin/Workspaces/WorkspaceRow/EditWorkspaceUsersModal/index.jsx new file mode 100644 index 00000000000..c363dbed311 --- /dev/null +++ b/frontend/src/pages/Admin/Workspaces/WorkspaceRow/EditWorkspaceUsersModal/index.jsx @@ -0,0 +1,155 @@ +import React, { useState } from "react"; +import { X } from "react-feather"; +import Admin from "../../../../../models/admin"; +import { titleCase } from "text-case"; + +export const EditWorkspaceUsersModalId = (workspace) => + `edit-workspace-${workspace.id}-modal`; +export default function EditWorkspaceUsersModal({ workspace, users }) { + const [error, setError] = useState(null); + const hideModal = () => { + document.getElementById(EditWorkspaceUsersModalId(workspace)).close(); + }; + const handleUpdate = async (e) => { + setError(null); + e.preventDefault(); + const data = { + userIds: [], + }; + const form = new FormData(e.target); + for (var [key, value] of form.entries()) { + if (key.includes("user-") && value === "yes") { + const [_, id] = key.split(`-`); + data.userIds.push(+id); + } + } + const { success, error } = await Admin.updateUsersInWorkspace( + workspace.id, + data.userIds + ); + if (success) window.location.reload(); + setError(error); + }; + + return ( + +
    +
    +
    +

    + Edit {workspace.name} +

    + +
    +
    +
    +
    + {users + .filter((user) => user.role !== "admin") + .map((user) => { + return ( +
    { + document + .getElementById( + `workspace-${workspace.id}-user-${user.id}` + ) + ?.click(); + }} + > + + +
    + ); + })} +
    + + +
    + {error && ( +

    + Error: {error} +

    + )} +
    +
    +
    + + +
    +
    +
    +
    +
    + ); +} diff --git a/frontend/src/pages/Admin/Workspaces/WorkspaceRow/index.jsx b/frontend/src/pages/Admin/Workspaces/WorkspaceRow/index.jsx new file mode 100644 index 00000000000..762e2089bac --- /dev/null +++ b/frontend/src/pages/Admin/Workspaces/WorkspaceRow/index.jsx @@ -0,0 +1,63 @@ +import { useRef } from "react"; +import Admin from "../../../../models/admin"; +import paths from "../../../../utils/paths"; +import EditWorkspaceUsersModal, { + EditWorkspaceUsersModalId, +} from "./EditWorkspaceUsersModal"; + +export default function WorkspaceRow({ workspace, users }) { + const rowRef = useRef(null); + const handleDelete = async () => { + if ( + !window.confirm( + `Are you sure you want to delete ${workspace.name}?\nAfter you do this it will be unavailable in this instance of AnythingLLM.\n\nThis action is irreversible.` + ) + ) + return false; + rowRef?.current?.remove(); + await Admin.deleteWorkspace(workspace.id); + }; + + return ( + <> + + + {workspace.name} + + + + {workspace.slug} + + + {workspace.userIds?.length} + {workspace.createdAt} + + + + + + + + ); +} diff --git a/frontend/src/pages/Admin/Workspaces/index.jsx b/frontend/src/pages/Admin/Workspaces/index.jsx new file mode 100644 index 00000000000..ff3cb45a22d --- /dev/null +++ b/frontend/src/pages/Admin/Workspaces/index.jsx @@ -0,0 +1,112 @@ +import { useEffect, useState } from "react"; +import Sidebar, { SidebarMobileHeader } from "../../../components/AdminSidebar"; +import { isMobile } from "react-device-detect"; +import * as Skeleton from "react-loading-skeleton"; +import "react-loading-skeleton/dist/skeleton.css"; +import { BookOpen } from "react-feather"; +import usePrefersDarkMode from "../../../hooks/usePrefersDarkMode"; +import Admin from "../../../models/admin"; +import WorkspaceRow from "./WorkspaceRow"; +import NewWorkspaceModal, { NewWorkspaceModalId } from "./NewWorkspaceModal"; + +export default function AdminWorkspaces() { + return ( +
    + {!isMobile && } +
    + {isMobile && } +
    +
    +
    +

    + Instance workspaces +

    + +
    +

    + These are all the workspaces that exist on this instance. Removing + a workspace will delete all of it's associated chats and settings. +

    +
    + +
    + +
    +
    + ); +} + +function WorkspacesContainer() { + const darkMode = usePrefersDarkMode(); + const [loading, setLoading] = useState(true); + const [users, setUsers] = useState([]); + const [workspaces, setWorkspaces] = useState([]); + + useEffect(() => { + async function fetchData() { + const _users = await Admin.users(); + const _workspaces = await Admin.workspaces(); + setUsers(_users); + setWorkspaces(_workspaces); + setLoading(false); + } + fetchData(); + }, []); + + if (loading) { + return ( + + ); + } + + return ( + + + + + + + + + + + + {workspaces.map((workspace) => ( + + ))} + +
    + Name + + Link + + Users + + Created On + + Actions +
    + ); +} diff --git a/frontend/src/utils/paths.js b/frontend/src/utils/paths.js index 0748f6badaf..3c61c3406ad 100644 --- a/frontend/src/utils/paths.js +++ b/frontend/src/utils/paths.js @@ -34,5 +34,8 @@ export default { invites: () => { return `/admin/invites`; }, + workspaces: () => { + return `/admin/workspaces`; + }, }, }; diff --git a/server/endpoints/admin.js b/server/endpoints/admin.js index b072b7319f8..56951228650 100644 --- a/server/endpoints/admin.js +++ b/server/endpoints/admin.js @@ -1,5 +1,6 @@ const { Invite } = require("../models/invite"); const { User } = require("../models/user"); +const { Workspace } = require("../models/workspace"); const { userFromSession, reqBody } = require("../utils/http"); const { validatedRequest } = require("../utils/middleware/validatedRequest"); @@ -139,6 +140,94 @@ function adminEndpoints(app) { } } ); + + 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; + await Workspace.delete(`id = ${id}`); + response.status(200).json({ success, error }); + } catch (e) { + console.error(e); + response.sendStatus(500).end(); + } + } + ); } module.exports = { adminEndpoints }; diff --git a/server/models/workspace.js b/server/models/workspace.js index 72480e327fd..5049a2baeca 100644 --- a/server/models/workspace.js +++ b/server/models/workspace.js @@ -218,6 +218,21 @@ const Workspace = { 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/workspaceUsers.js b/server/models/workspaceUsers.js index aa0e1646e42..8dacbac113a 100644 --- a/server/models/workspaceUsers.js +++ b/server/models/workspaceUsers.js @@ -54,6 +54,21 @@ const WorkspaceUser = { 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 From 8f16d986103809a88a97ad3da9254be492f3acc5 Mon Sep 17 00:00:00 2001 From: timothycarambat Date: Mon, 24 Jul 2023 15:25:32 -0700 Subject: [PATCH 06/15] manage chats --- frontend/src/App.jsx | 7 +- .../src/components/AdminSidebar/index.jsx | 15 +- frontend/src/models/admin.js | 25 +++ .../src/pages/Admin/Chats/ChatRow/index.jsx | 95 ++++++++++++ frontend/src/pages/Admin/Chats/index.jsx | 142 ++++++++++++++++++ frontend/src/utils/paths.js | 3 + server/endpoints/admin.js | 43 ++++++ server/models/workspaceChats.js | 38 ++++- 8 files changed, 363 insertions(+), 5 deletions(-) create mode 100644 frontend/src/pages/Admin/Chats/ChatRow/index.jsx create mode 100644 frontend/src/pages/Admin/Chats/index.jsx diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index e7a06aa7060..f67b5b2c5a7 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -3,12 +3,13 @@ import { Routes, Route } from "react-router-dom"; import { ContextWrapper } from "./AuthContext"; import PrivateRoute, { AdminRoute } from "./components/PrivateRoute"; import InvitePage from "./pages/Invite"; -import AdminWorkspaces from "./pages/Admin/Workspaces"; const Main = lazy(() => import("./pages/Main")); const WorkspaceChat = lazy(() => import("./pages/WorkspaceChat")); const AdminUsers = lazy(() => import("./pages/Admin/Users")); const AdminInvites = lazy(() => import("./pages/Admin/Invitations")); +const AdminWorkspaces = lazy(() => import("./pages/Admin/Workspaces")); +const AdminChats = lazy(() => import("./pages/Admin/Chats")); export default function App() { return ( @@ -35,6 +36,10 @@ export default function App() { path="/admin/workspaces" element={} /> + } + /> diff --git a/frontend/src/components/AdminSidebar/index.jsx b/frontend/src/components/AdminSidebar/index.jsx index 25f423f2b70..2800f12424e 100644 --- a/frontend/src/components/AdminSidebar/index.jsx +++ b/frontend/src/components/AdminSidebar/index.jsx @@ -1,5 +1,13 @@ import React, { useEffect, useRef, useState } from "react"; -import { BookOpen, GitHub, Mail, Menu, Users, X } from "react-feather"; +import { + BookOpen, + GitHub, + Mail, + Menu, + MessageSquare, + Users, + X, +} from "react-feather"; import IndexCount from "../Sidebar/IndexCount"; import LLMStatus from "../Sidebar/LLMStatus"; import paths from "../../utils/paths"; @@ -49,6 +57,11 @@ export default function AdminSidebar() { btnText="Workspace Management" icon={} /> +
    diff --git a/frontend/src/models/admin.js b/frontend/src/models/admin.js index 4afa3a6209d..f05a76ded31 100644 --- a/frontend/src/models/admin.js +++ b/frontend/src/models/admin.js @@ -138,6 +138,31 @@ const Admin = { return { success: false, error: e.message }; }); }, + + // Workspace Chats Mgmt + chats: async (offset = 0) => { + return await fetch(`${API_BASE}/admin/workspace-chats`, { + method: "POST", + headers: baseHeaders(), + body: JSON.stringify({ offset }), + }) + .then((res) => res.json()) + .catch((e) => { + console.error(e); + return []; + }); + }, + deleteChat: async (chatId) => { + return await fetch(`${API_BASE}/admin/workspace-chats/${chatId}`, { + method: "DELETE", + headers: baseHeaders(), + }) + .then((res) => res.json()) + .catch((e) => { + console.error(e); + return { success: false, error: e.message }; + }); + }, }; export default Admin; diff --git a/frontend/src/pages/Admin/Chats/ChatRow/index.jsx b/frontend/src/pages/Admin/Chats/ChatRow/index.jsx new file mode 100644 index 00000000000..2cdf630d707 --- /dev/null +++ b/frontend/src/pages/Admin/Chats/ChatRow/index.jsx @@ -0,0 +1,95 @@ +import { useRef, useState } from "react"; +import Admin from "../../../../models/admin"; +import truncate from "truncate"; +import { X } from "react-feather"; + +export default function ChatRow({ chat }) { + const rowRef = useRef(null); + const handleDelete = async () => { + if ( + !window.confirm( + `Are you sure you want to delete this chat?\n\nThis action is irreversible.` + ) + ) + return false; + rowRef?.current?.remove(); + await Admin.deleteChat(chat.id); + }; + + return ( + <> + + + {chat.id} + + + {chat.user?.username} + + {chat.workspace?.name} + { + document.getElementById(`chat-${chat.id}-prompt`)?.showModal(); + }} + className="px-6 py-4 hover:dark:bg-stone-700 hover:bg-gray-100 cursor-pointer" + > + {truncate(chat.prompt, 40)} + + { + document.getElementById(`chat-${chat.id}-response`)?.showModal(); + }} + className="px-6 py-4 hover:dark:bg-stone-600 hover:bg-gray-100 cursor-pointer" + > + {truncate(JSON.parse(chat.response)?.text, 40)} + + {chat.createdAt} + + + + + + + + ); +} + +function hideModal(modalName) { + document.getElementById(modalName)?.close(); +} + +const TextPreview = ({ text, modalName }) => { + return ( + +
    +
    +
    +

    + Viewing Text +

    + +
    +
    +
    +              {text}
    +            
    +
    +
    +
    +
    + ); +}; diff --git a/frontend/src/pages/Admin/Chats/index.jsx b/frontend/src/pages/Admin/Chats/index.jsx new file mode 100644 index 00000000000..8c439bb70cd --- /dev/null +++ b/frontend/src/pages/Admin/Chats/index.jsx @@ -0,0 +1,142 @@ +import { useEffect, useState } from "react"; +import Sidebar, { SidebarMobileHeader } from "../../../components/AdminSidebar"; +import { isMobile } from "react-device-detect"; +import * as Skeleton from "react-loading-skeleton"; +import "react-loading-skeleton/dist/skeleton.css"; +import usePrefersDarkMode from "../../../hooks/usePrefersDarkMode"; +import Admin from "../../../models/admin"; +import useQuery from "../../../hooks/useQuery"; +import ChatRow from "./ChatRow"; + +export default function AdminChats() { + return ( +
    + {!isMobile && } +
    + {isMobile && } +
    +
    +
    +

    + Workspace Chats +

    +
    +

    + These are all the recorded chats and messages that have been sent + by users ordered by their creation date. +

    +
    + +
    +
    +
    + ); +} + +function ChatsContainer() { + const query = useQuery(); + const darkMode = usePrefersDarkMode(); + const [loading, setLoading] = useState(true); + const [chats, setChats] = useState([]); + const [offset, setOffset] = useState(Number(query.get("offset") || 0)); + const [canNext, setCanNext] = useState(false); + + const handlePrevious = () => { + if (chats.length === 0) { + setOffset(0); + return; + } + + const chat = chats.at(-1); + if (chat.id - 20 <= 0) { + setOffset(0); + return; + } + + setOffset(chat.id - 1); + }; + const handleNext = () => { + setOffset(chats[0].id + 1); + }; + + useEffect(() => { + async function fetchChats() { + const { chats: _chats, hasPages = false } = await Admin.chats(offset); + setChats(_chats); + setCanNext(hasPages); + setLoading(false); + } + fetchChats(); + }, [offset]); + + if (loading) { + return ( + + ); + } + + return ( + <> + + + + + + + + + + + + + + {chats.map((chat) => ( + + ))} + +
    + Id + + Sent By + + Workspace + + Prompt + + Response + + Sent At + + Actions +
    +
    + + +
    + + ); +} diff --git a/frontend/src/utils/paths.js b/frontend/src/utils/paths.js index 3c61c3406ad..e59fcae2600 100644 --- a/frontend/src/utils/paths.js +++ b/frontend/src/utils/paths.js @@ -37,5 +37,8 @@ export default { workspaces: () => { return `/admin/workspaces`; }, + chats: () => { + return "/admin/workspace-chats"; + }, }, }; diff --git a/server/endpoints/admin.js b/server/endpoints/admin.js index 56951228650..33cdb1cb91f 100644 --- a/server/endpoints/admin.js +++ b/server/endpoints/admin.js @@ -1,6 +1,7 @@ const { Invite } = require("../models/invite"); const { User } = require("../models/user"); const { Workspace } = require("../models/workspace"); +const { WorkspaceChats } = require("../models/workspaceChats"); const { userFromSession, reqBody } = require("../utils/http"); const { validatedRequest } = require("../utils/middleware/validatedRequest"); @@ -228,6 +229,48 @@ function adminEndpoints(app) { } } ); + + 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(); + } + } + ); } module.exports = { adminEndpoints }; diff --git a/server/models/workspaceChats.js b/server/models/workspaceChats.js index 85c9a0b4a47..027448bff55 100644 --- a/server/models/workspaceChats.js +++ b/server/models/workspaceChats.js @@ -106,10 +106,14 @@ const WorkspaceChats = { 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(); @@ -128,10 +132,38 @@ 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; }, }; From 3a1c7d13fb4cdfd5df5213c67890878f061c0560 Mon Sep 17 00:00:00 2001 From: timothycarambat Date: Mon, 24 Jul 2023 15:39:44 -0700 Subject: [PATCH 07/15] manage chats --- frontend/src/pages/Admin/Chats/ChatRow/index.jsx | 2 +- server/endpoints/admin.js | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/frontend/src/pages/Admin/Chats/ChatRow/index.jsx b/frontend/src/pages/Admin/Chats/ChatRow/index.jsx index 2cdf630d707..ca0d5c78129 100644 --- a/frontend/src/pages/Admin/Chats/ChatRow/index.jsx +++ b/frontend/src/pages/Admin/Chats/ChatRow/index.jsx @@ -1,4 +1,4 @@ -import { useRef, useState } from "react"; +import { useRef } from "react"; import Admin from "../../../../models/admin"; import truncate from "truncate"; import { X } from "react-feather"; diff --git a/server/endpoints/admin.js b/server/endpoints/admin.js index 33cdb1cb91f..78bb3bdbc78 100644 --- a/server/endpoints/admin.js +++ b/server/endpoints/admin.js @@ -221,7 +221,23 @@ function adminEndpoints(app) { } 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); From b135bcaa8cfe6eecdba4a49fb7149f93971b69bb Mon Sep 17 00:00:00 2001 From: timothycarambat Date: Tue, 25 Jul 2023 01:59:14 -0700 Subject: [PATCH 08/15] add Support for admin system settings for users to delete workspaces and limit chats per user --- frontend/src/App.jsx | 5 + .../src/components/AdminSidebar/index.jsx | 7 + frontend/src/components/Sidebar/index.jsx | 2 +- frontend/src/models/admin.js | 25 +++ frontend/src/pages/Admin/System/index.jsx | 155 ++++++++++++++++++ frontend/src/utils/paths.js | 3 + server/endpoints/admin.js | 56 +++++++ server/endpoints/chat.js | 30 ++++ server/endpoints/system.js | 7 +- server/endpoints/workspaces.js | 11 ++ server/models/systemSettings.js | 7 +- 11 files changed, 305 insertions(+), 3 deletions(-) create mode 100644 frontend/src/pages/Admin/System/index.jsx diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index f67b5b2c5a7..482bb08577f 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -10,6 +10,7 @@ const AdminUsers = lazy(() => import("./pages/Admin/Users")); const AdminInvites = lazy(() => import("./pages/Admin/Invitations")); const AdminWorkspaces = lazy(() => import("./pages/Admin/Workspaces")); const AdminChats = lazy(() => import("./pages/Admin/Chats")); +const AdminSystem = lazy(() => import("./pages/Admin/System")); export default function App() { return ( @@ -24,6 +25,10 @@ export default function App() { } /> {/* Admin Routes */} + } + /> } diff --git a/frontend/src/components/AdminSidebar/index.jsx b/frontend/src/components/AdminSidebar/index.jsx index 2800f12424e..6c8b8f8c8fd 100644 --- a/frontend/src/components/AdminSidebar/index.jsx +++ b/frontend/src/components/AdminSidebar/index.jsx @@ -1,10 +1,12 @@ import React, { useEffect, useRef, useState } from "react"; import { BookOpen, + Database, GitHub, Mail, Menu, MessageSquare, + Settings, Users, X, } from "react-feather"; @@ -42,6 +44,11 @@ export default function AdminSidebar() {
    +
    - +
    ); } diff --git a/frontend/src/models/admin.js b/frontend/src/models/admin.js index 245db35e25b..8aedf810bc2 100644 --- a/frontend/src/models/admin.js +++ b/frontend/src/models/admin.js @@ -2,7 +2,7 @@ import { API_BASE } from "../utils/constants"; import { baseHeaders } from "../utils/request"; const Admin = { - // User Mangement + // User Management users: async () => { return await fetch(`${API_BASE}/admin/users`, { method: "GET", diff --git a/frontend/src/pages/Admin/Users/NewUserModal/index.jsx b/frontend/src/pages/Admin/Users/NewUserModal/index.jsx index a7cfd7d1ba4..ac957d73b78 100644 --- a/frontend/src/pages/Admin/Users/NewUserModal/index.jsx +++ b/frontend/src/pages/Admin/Users/NewUserModal/index.jsx @@ -52,7 +52,7 @@ export default function NewUserModal() { name="username" type="text" className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-stone-600 dark:border-stone-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" - placeholder="My username" + placeholder="User's username" minLength={2} required={true} autoComplete="off" diff --git a/frontend/src/pages/Admin/Users/UserRow/EditUserModal/index.jsx b/frontend/src/pages/Admin/Users/UserRow/EditUserModal/index.jsx index 833a926e98e..cdab66a67a5 100644 --- a/frontend/src/pages/Admin/Users/UserRow/EditUserModal/index.jsx +++ b/frontend/src/pages/Admin/Users/UserRow/EditUserModal/index.jsx @@ -53,7 +53,7 @@ export default function EditUserModal({ user }) { name="username" type="text" className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-stone-600 dark:border-stone-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" - placeholder="My Workspace" + placeholder="User's username" minLength={2} defaultValue={user.username} required={true} diff --git a/server/package.json b/server/package.json index b2d673ea6aa..24b84210a5f 100644 --- a/server/package.json +++ b/server/package.json @@ -23,7 +23,7 @@ "chromadb": "^1.5.2", "cors": "^2.8.5", "dotenv": "^16.0.3", - "express": ">=5.0.0-beta.1", + "express": "^4.18.2", "extract-zip": "^2.0.1", "jsonwebtoken": "^8.5.1", "langchain": "^0.0.90", @@ -43,4 +43,4 @@ "nodemon": "^2.0.22", "prettier": "^2.4.1" } -} +} \ No newline at end of file diff --git a/server/yarn.lock b/server/yarn.lock index 1cc0eeb2fba..1a82497edca 100644 --- a/server/yarn.lock +++ b/server/yarn.lock @@ -128,7 +128,7 @@ abbrev@1: resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== -accepts@~1.3.4, accepts@~1.3.7: +accepts@~1.3.4, accepts@~1.3.8: version "1.3.8" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== @@ -266,10 +266,10 @@ array-back@^4.0.1, array-back@^4.0.2: resolved "https://registry.yarnpkg.com/array-back/-/array-back-4.0.2.tgz#8004e999a6274586beeb27342168652fdb89fa1e" integrity sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg== -array-flatten@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-3.0.0.tgz#6428ca2ee52c7b823192ec600fa3ed2f157cd541" - integrity sha512-zPMVc3ZYlGLNk4mpK1NzP2wg0ml9t7fUgDsayR5Y5rSzxQilzR9FGu/EH2jQOcKSAeAfWeylyW8juy3OkWRvNA== +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== arrify@^2.0.0: version "2.0.1" @@ -340,21 +340,23 @@ bl@^4.0.3: inherits "^2.0.4" readable-stream "^3.4.0" -body-parser@2.0.0-beta.1: - version "2.0.0-beta.1" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-2.0.0-beta.1.tgz#d4ed97d6ed51f6040b967db0db2252a0b235a661" - integrity sha512-I1v2bt2OdYqtmk8nEFZuEf+9Opb30DphYwTPDbgg/OorSAoJOuTpWyDrZaSWQw7FdoevbBRCP2+9z/halXSWcA== +body-parser@1.20.1: + version "1.20.1" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668" + integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw== dependencies: - bytes "3.1.1" + bytes "3.1.2" content-type "~1.0.4" debug "2.6.9" - depd "~1.1.2" - http-errors "1.8.1" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" iconv-lite "0.4.24" - on-finished "~2.3.0" - qs "6.9.6" - raw-body "2.4.2" + on-finished "2.4.1" + qs "6.11.0" + raw-body "2.5.1" type-is "~1.6.18" + unpipe "1.0.0" body-parser@^1.20.2: version "1.20.2" @@ -426,11 +428,6 @@ busboy@^1.0.0: dependencies: streamsearch "^1.1.0" -bytes@3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.1.tgz#3f018291cb4cbad9accb6e6970bca9c8889e879a" - integrity sha512-dWe4nWO/ruEOY7HkUJ5gFt1DCFV9zPRoJr8pV0/ASQermOZjtq8jMjOprC0Kd10GLN+l7xaUPvxzJFWtxGu8Fg== - bytes@3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" @@ -610,10 +607,10 @@ cookie-signature@1.0.6: resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== -cookie@0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1" - integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA== +cookie@0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" + integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== core-util-is@~1.0.0: version "1.0.3" @@ -655,13 +652,6 @@ debug@2.6.9: dependencies: ms "2.0.0" -debug@3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" - integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== - dependencies: - ms "2.0.0" - debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.3: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" @@ -706,11 +696,6 @@ destroy@1.2.0: resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== -destroy@~1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" - integrity sha512-3NdhDuEXnfun/z7x9GOElY49LoqVHoGScmOKwmxhsS8N5Y+Z8KyPPDnaSzqWgYt/ji4mqwfTS34Htrk0zPIXVg== - detect-libc@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.2.tgz#8ccf2ba9315350e1241b88d0ac3b0e1fbd99605d" @@ -797,40 +782,39 @@ expr-eval@^2.0.2: resolved "https://registry.yarnpkg.com/expr-eval/-/expr-eval-2.0.2.tgz#fa6f044a7b0c93fde830954eb9c5b0f7fbc7e201" integrity sha512-4EMSHGOPSwAfBiibw3ndnP0AvjDWLsMvGOvWEZ2F96IGk0bIVdjQisOHxReSkE13mHcfbuCiXw+G4y0zv6N8Eg== -express@>=5.0.0-beta.1: - version "5.0.0-beta.1" - resolved "https://registry.yarnpkg.com/express/-/express-5.0.0-beta.1.tgz#efbfd372e4650a48e417b1adbaf43599092ddc8f" - integrity sha512-KPtBrlZoQu2Ps0Ce/Imqtq73AB0KBJ8Gx59yZQ3pmDJU2/LhcoZETo03oSgtTQufbcLXt/WBITk/jMjl/WMyrQ== +express@^4.18.2: + version "4.18.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59" + integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ== dependencies: - accepts "~1.3.7" - array-flatten "3.0.0" - body-parser "2.0.0-beta.1" + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "1.20.1" content-disposition "0.5.4" content-type "~1.0.4" - cookie "0.4.1" + cookie "0.5.0" cookie-signature "1.0.6" - debug "3.1.0" - depd "~1.1.2" + debug "2.6.9" + depd "2.0.0" encodeurl "~1.0.2" escape-html "~1.0.3" etag "~1.8.1" - finalhandler "~1.1.2" + finalhandler "1.2.0" fresh "0.5.2" + http-errors "2.0.0" merge-descriptors "1.0.1" methods "~1.1.2" - mime-types "~2.1.34" - on-finished "~2.3.0" + on-finished "2.4.1" parseurl "~1.3.3" - path-is-absolute "1.0.1" + path-to-regexp "0.1.7" proxy-addr "~2.0.7" - qs "6.9.6" + qs "6.11.0" range-parser "~1.2.1" - router "2.0.0-beta.1" safe-buffer "5.2.1" - send "1.0.0-beta.1" - serve-static "2.0.0-beta.1" + send "0.18.0" + serve-static "1.15.0" setprototypeof "1.2.0" - statuses "~1.5.0" + statuses "2.0.1" type-is "~1.6.18" utils-merge "1.0.1" vary "~1.1.2" @@ -870,17 +854,17 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" -finalhandler@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" - integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== +finalhandler@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" + integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== dependencies: debug "2.6.9" encodeurl "~1.0.2" escape-html "~1.0.3" - on-finished "~2.3.0" + on-finished "2.4.1" parseurl "~1.3.3" - statuses "~1.5.0" + statuses "2.0.1" unpipe "~1.0.0" find-replace@^3.0.0: @@ -1114,17 +1098,6 @@ http-cache-semantics@^4.1.0: resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a" integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ== -http-errors@1.8.1: - version "1.8.1" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.8.1.tgz#7c3f28577cbc8a207388455dbd62295ed07bd68c" - integrity sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g== - dependencies: - depd "~1.1.2" - inherits "2.0.4" - setprototypeof "1.2.0" - statuses ">= 1.5.0 < 2" - toidentifier "1.0.1" - http-errors@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" @@ -1530,6 +1503,11 @@ mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.24, mime-types@~2.1.34: dependencies: mime-db "1.52.0" +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" @@ -1814,13 +1792,6 @@ on-finished@2.4.1: dependencies: ee-first "1.1.1" -on-finished@~2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" - integrity sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww== - dependencies: - ee-first "1.1.1" - once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -1883,15 +1854,15 @@ parseurl@~1.3.2, parseurl@~1.3.3: resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== -path-is-absolute@1.0.1, path-is-absolute@^1.0.0: +path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== -path-to-regexp@3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-3.2.0.tgz#fa7877ecbc495c601907562222453c43cc204a5f" - integrity sha512-jczvQbCUS7XmS7o+y1aEO9OBVFeZBQ1MDSEqmO7xSoPgOPoowY/SxLpZ6Vh97/8qHZOteiCKb7gkG9gA2ZUxJA== +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== pend@~1.2.0: version "1.2.0" @@ -1961,11 +1932,6 @@ qs@6.11.0: dependencies: side-channel "^1.0.4" -qs@6.9.6: - version "6.9.6" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.6.tgz#26ed3c8243a431b2924aca84cc90471f35d5a0ee" - integrity sha512-TIRk4aqYLNoJUbd+g2lEdz5kLWIuTMRagAXxl78Q0RiVjAOugHmeKNGdd3cwo/ktpf9aL9epCfFqWDEKysUlLQ== - qs@^6.7.0: version "6.11.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.2.tgz#64bea51f12c1f5da1bc01496f48ffcff7c69d7d9" @@ -1978,13 +1944,13 @@ range-parser@~1.2.1: resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== -raw-body@2.4.2: - version "2.4.2" - resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.2.tgz#baf3e9c21eebced59dd6533ac872b71f7b61cb32" - integrity sha512-RPMAFUJP19WIet/99ngh6Iv8fzAbqum4Li7AD6DtGaW2RpMB/11xDoalPiJMTbu6I3hkbMVkATvZrqb9EEqeeQ== +raw-body@2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857" + integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig== dependencies: - bytes "3.1.1" - http-errors "1.8.1" + bytes "3.1.2" + http-errors "2.0.0" iconv-lite "0.4.24" unpipe "1.0.0" @@ -2061,18 +2027,6 @@ rimraf@^3.0.2: dependencies: glob "^7.1.3" -router@2.0.0-beta.1: - version "2.0.0-beta.1" - resolved "https://registry.yarnpkg.com/router/-/router-2.0.0-beta.1.tgz#86fb03143cee259497d8b658c7f13b1d20049331" - integrity sha512-GLoYgkhAGAiwVda5nt6Qd4+5RAPuQ4WIYLlZ+mxfYICI+22gnIB3eCfmhgV8+uJNPS1/39DOYi/vdrrz0/ouKA== - dependencies: - array-flatten "3.0.0" - methods "~1.1.2" - parseurl "~1.3.3" - path-to-regexp "3.2.0" - setprototypeof "1.2.0" - utils-merge "1.0.1" - safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" @@ -2110,23 +2064,24 @@ semver@~7.0.0: resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== -send@1.0.0-beta.1: - version "1.0.0-beta.1" - resolved "https://registry.yarnpkg.com/send/-/send-1.0.0-beta.1.tgz#9db741443b0b25771cfd38adc833376ae2d606fe" - integrity sha512-OKTRokcl/oo34O8+6aUpj8Jf2Bjw2D0tZzmX0/RvyfVC9ZOZW+HPAWAlhS817IsRaCnzYX1z++h2kHFr2/KNRg== +send@0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" + integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== dependencies: - debug "3.1.0" - destroy "~1.0.4" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" encodeurl "~1.0.2" escape-html "~1.0.3" etag "~1.8.1" fresh "0.5.2" - http-errors "1.8.1" - mime-types "~2.1.34" + http-errors "2.0.0" + mime "1.6.0" ms "2.1.3" - on-finished "~2.3.0" + on-finished "2.4.1" range-parser "~1.2.1" - statuses "~1.5.0" + statuses "2.0.1" serve-index@^1.9.1: version "1.9.1" @@ -2141,15 +2096,15 @@ serve-index@^1.9.1: mime-types "~2.1.17" parseurl "~1.3.2" -serve-static@2.0.0-beta.1: - version "2.0.0-beta.1" - resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-2.0.0-beta.1.tgz#003bc35db8b37905e1f6d380a4ccc0a34f30ec1d" - integrity sha512-DEJ9on/tQeFO2Omj7ovT02lCp1YgP4Kb8W2lv2o/4keTFAbgc8HtH3yPd47++2wv9lvQeqiA7FHFDe5+8c4XpA== +serve-static@1.15.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" + integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== dependencies: encodeurl "~1.0.2" escape-html "~1.0.3" parseurl "~1.3.3" - send "1.0.0-beta.1" + send "0.18.0" set-blocking@^2.0.0: version "2.0.0" @@ -2242,7 +2197,7 @@ statuses@2.0.1: resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== -"statuses@>= 1.4.0 < 2", "statuses@>= 1.5.0 < 2", statuses@~1.5.0: +"statuses@>= 1.4.0 < 2": version "1.5.0" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== From 6f29e00ddb20538ea9c6609f2e95d33660b843c5 Mon Sep 17 00:00:00 2001 From: timothycarambat Date: Tue, 25 Jul 2023 09:33:28 -0700 Subject: [PATCH 11/15] wrong method --- server/endpoints/workspaces.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/endpoints/workspaces.js b/server/endpoints/workspaces.js index 4bfcd282f1c..f103a1c59c5 100644 --- a/server/endpoints/workspaces.js +++ b/server/endpoints/workspaces.js @@ -201,7 +201,7 @@ function workspaceEndpoints(app) { const user = await userFromSession(request, response); const workspace = multiUserMode(response) ? await Workspace.getWithUser(user, `slug = '${slug}'`) - : await WorkspaceChats.forWorkspace(workspace.id); + : await Workspace.get(`slug = '${slug}'`); if (!workspace) { response.sendStatus(400).end(); From 7e6ffb7bd18f264426f556a09d0c7f42f822c4f1 Mon Sep 17 00:00:00 2001 From: timothycarambat Date: Tue, 25 Jul 2023 09:50:00 -0700 Subject: [PATCH 12/15] update readme --- README.md | 39 ++++++++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index ceca8dbed8c..5df4d873257 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,24 @@ -# 🤖 AnythingLLM: A full-stack personalized AI assistant - -[![Twitter](https://img.shields.io/twitter/url/https/twitter.com/tim.svg?style=social&label=Follow%20%40Timothy%20Carambat)](https://twitter.com/tcarambat) [![](https://dcbadge.vercel.app/api/server/6UyHPeGZAC?compact=true&style=flat)](https://discord.gg/6UyHPeGZAC) - -A full-stack application and tool suite that enables you to turn any document, resource, or piece of content into a piece of data that any LLM can use as reference during chatting. This application runs with very minimal overhead as by default the LLM and vectorDB are hosted remotely, but can be swapped for local instances. Currently this project supports [Pinecone](https://pinecone.io), [ChromaDB](https://trychroma.com) & more for vector storage and [OpenAI](https://openai.com) for LLM/chatting. +

    + AnythingLLM: A full-stack personalized AI assistant.
    + A hyper-efficient and open-source document chatbot solution for all. +

    + +

    + + Twitter + | + + Discord + | + + License + | + + Usage Docs + +

    + +A full-stack application that enables you to turn any document, resource, or piece of content into context that any LLM can use as references during chatting. This application allows you to pick and choose which LLM or Vector Database you want to use. Currently this project supports [Pinecone](https://pinecone.io), [ChromaDB](https://trychroma.com) & more for vector storage and [OpenAI](https://openai.com) for LLM/chatting. ![Chatting](/images/screenshots/chat.png) @@ -14,20 +30,21 @@ A full-stack application and tool suite that enables you to turn any document, r ### Product Overview -AnythingLLM aims to be a full-stack application where you can use commercial off-the-shelf LLMs with Long-term-memory solutions or use popular open source LLM and vectorDB solutions. +AnythingLLM aims to be a full-stack application where you can use commercial off-the-shelf LLMs or popular open source LLMs and vectorDB solutions. Anything LLM is a full-stack product that you can run locally as well as host remotely and be able to chat intelligently with any documents you provide it. AnythingLLM divides your documents into objects called `workspaces`. A Workspace functions a lot like a thread, but with the addition of containerization of your documents. Workspaces can share documents, but they do not talk to each other so you can keep your context for each workspace clean. Some cool features of AnythingLLM -- Atomically manage documents to be used in long-term-memory from a simple UI +- Multi-user instance support and oversight +- Atomically manage documents in your vector database from a simple UI - Two chat modes `conversation` and `query`. Conversation retains previous questions and amendments. Query is simple QA against your documents - Each chat response contains a citation that is linked to the original content - Simple technology stack for fast iteration -- Fully capable of being hosted remotely -- "Bring your own LLM" model and vector solution. _still in progress_ -- Extremely efficient cost-saving measures for managing very large documents. you'll never pay to embed a massive document or transcript more than once. 90% more cost effective than other LTM chatbots +- 100% Cloud deployment ready. +- "Bring your own LLM" model. _still in progress - openai support only currently_ +- Extremely efficient cost-saving measures for managing very large documents. You'll never pay to embed a massive document or transcript more than once. 90% more cost effective than other document chatbot solutions. ### Technical Overview This monorepo consists of three main sections: @@ -38,7 +55,7 @@ This monorepo consists of three main sections: ### Requirements - `yarn` and `node` on your machine - `python` 3.8+ for running scripts in `collector/`. -- access to an LLM like `GPT-3.5`, `GPT-4`*. +- access to an LLM like `GPT-3.5`, `GPT-4`. - a [Pinecone.io](https://pinecone.io) free account*. *you can use drop in replacements for these. This is just the easiest to get up and running fast. We support multiple vector database providers. From aef2c1329515659bddf66eebd96f59a299469f13 Mon Sep 17 00:00:00 2001 From: timothycarambat Date: Tue, 25 Jul 2023 09:50:53 -0700 Subject: [PATCH 13/15] update readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5df4d873257..da8c94fde5c 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

    - AnythingLLM: A full-stack personalized AI assistant.
    + 🤖 AnythingLLM: A full-stack personalized AI assistant.
    A hyper-efficient and open-source document chatbot solution for all.

    @@ -14,7 +14,7 @@ License | - Usage Docs + Docs

    From f00579ea3f2b836ef8e1957cfd9c36afe4fc5e31 Mon Sep 17 00:00:00 2001 From: timothycarambat Date: Tue, 25 Jul 2023 09:51:46 -0700 Subject: [PATCH 14/15] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index da8c94fde5c..899b96f3884 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ This monorepo consists of three main sections: ### Requirements - `yarn` and `node` on your machine -- `python` 3.8+ for running scripts in `collector/`. +- `python` 3.9+ for running scripts in `collector/`. - access to an LLM like `GPT-3.5`, `GPT-4`. - a [Pinecone.io](https://pinecone.io) free account*. *you can use drop in replacements for these. This is just the easiest to get up and running fast. We support multiple vector database providers. From 2a75b7fad05696c60c99afef5dd3b7670618ebe7 Mon Sep 17 00:00:00 2001 From: timothycarambat Date: Tue, 25 Jul 2023 10:36:13 -0700 Subject: [PATCH 15/15] bump version to 0.1.0 --- frontend/package.json | 3 +-- package.json | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index a5aad549d30..41ef5032436 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,6 @@ { "name": "anything-llm-frontend", "private": false, - "version": "0.0.1-beta", "type": "module", "license": "MIT", "scripts": { @@ -44,4 +43,4 @@ "tailwindcss": "^3.3.1", "vite": "^4.3.0" } -} +} \ No newline at end of file diff --git a/package.json b/package.json index 7df9953fb6e..12d46fe3e74 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",