mirror of
https://github.com/Mintplex-Labs/anything-llm.git
synced 2024-11-19 04:30:10 +01:00
12 auth implementation (#13)
* Add Auth protection for cloud-based or private instances * skip check on local dev
This commit is contained in:
parent
fdacf4bb2e
commit
62e3f62e82
@ -1,10 +1,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { titleCase } from "text-case";
|
import { titleCase } from "text-case";
|
||||||
|
|
||||||
export default function CannotRemoveModal({
|
export default function CannotRemoveModal({ hideModal, vectordb }) {
|
||||||
hideModal,
|
|
||||||
vectordb,
|
|
||||||
}) {
|
|
||||||
return (
|
return (
|
||||||
<dialog
|
<dialog
|
||||||
open={true}
|
open={true}
|
||||||
@ -19,7 +16,11 @@ export default function CannotRemoveModal({
|
|||||||
|
|
||||||
<div className="flex flex-col gap-y-1">
|
<div className="flex flex-col gap-y-1">
|
||||||
<p className="text-base mt-4">
|
<p className="text-base mt-4">
|
||||||
{titleCase(vectordb)} does not support atomic removal of documents.<br />Unfortunately, you will have to delete the entire workspace to remove this document from being referenced.
|
{titleCase(vectordb)} does not support atomic removal of
|
||||||
|
documents.
|
||||||
|
<br />
|
||||||
|
Unfortunately, you will have to delete the entire workspace to
|
||||||
|
remove this document from being referenced.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full justify-center items-center mt-4">
|
<div className="flex w-full justify-center items-center mt-4">
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { X } from 'react-feather';
|
import { X } from "react-feather";
|
||||||
import System from "../../../models/system";
|
import System from "../../../models/system";
|
||||||
import Workspace from "../../../models/workspace";
|
import Workspace from "../../../models/workspace";
|
||||||
import paths from "../../../utils/paths";
|
import paths from "../../../utils/paths";
|
||||||
@ -18,7 +18,7 @@ export default function ManageWorkspace({ hideModal = noop, workspace }) {
|
|||||||
const [originalDocuments, setOriginalDocuments] = useState([]);
|
const [originalDocuments, setOriginalDocuments] = useState([]);
|
||||||
const [selectedFiles, setSelectFiles] = useState([]);
|
const [selectedFiles, setSelectFiles] = useState([]);
|
||||||
const [vectordb, setVectorDB] = useState(null);
|
const [vectordb, setVectorDB] = useState(null);
|
||||||
const [showingNoRemovalModal, setShowingNoRemovalModal] = useState(false)
|
const [showingNoRemovalModal, setShowingNoRemovalModal] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function fetchKeys() {
|
async function fetchKeys() {
|
||||||
@ -29,7 +29,7 @@ export default function ManageWorkspace({ hideModal = noop, workspace }) {
|
|||||||
setDirectories(localFiles);
|
setDirectories(localFiles);
|
||||||
setOriginalDocuments([...originalDocs]);
|
setOriginalDocuments([...originalDocs]);
|
||||||
setSelectFiles([...originalDocs]);
|
setSelectFiles([...originalDocs]);
|
||||||
setVectorDB(settings?.VectorDB)
|
setVectorDB(settings?.VectorDB);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
fetchKeys();
|
fetchKeys();
|
||||||
@ -99,7 +99,7 @@ export default function ManageWorkspace({ hideModal = noop, workspace }) {
|
|||||||
return isFolder
|
return isFolder
|
||||||
? originalDocuments.some((doc) => doc.includes(filepath.split("/")[0]))
|
? originalDocuments.some((doc) => doc.includes(filepath.split("/")[0]))
|
||||||
: originalDocuments.some((doc) => doc.includes(filepath));
|
: originalDocuments.some((doc) => doc.includes(filepath));
|
||||||
}
|
};
|
||||||
|
|
||||||
const toggleSelection = (filepath) => {
|
const toggleSelection = (filepath) => {
|
||||||
const isFolder = !filepath.includes("/");
|
const isFolder = !filepath.includes("/");
|
||||||
@ -108,7 +108,7 @@ export default function ManageWorkspace({ hideModal = noop, workspace }) {
|
|||||||
if (isSelected(filepath)) {
|
if (isSelected(filepath)) {
|
||||||
// Certain vector DBs do not contain the ability to delete vectors
|
// Certain vector DBs do not contain the ability to delete vectors
|
||||||
// so we cannot remove from these. The user will have to clear the entire workspace.
|
// so we cannot remove from these. The user will have to clear the entire workspace.
|
||||||
if (['lancedb'].includes(vectordb) && isOriginalDoc(filepath)) {
|
if (["lancedb"].includes(vectordb) && isOriginalDoc(filepath)) {
|
||||||
setShowingNoRemovalModal(true);
|
setShowingNoRemovalModal(true);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
119
frontend/src/components/Modals/Password.jsx
Normal file
119
frontend/src/components/Modals/Password.jsx
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
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 (
|
||||||
|
<div class="fixed top-0 left-0 right-0 z-50 w-full p-4 overflow-x-hidden overflow-y-auto md:inset-0 h-[calc(100%-1rem)] h-full bg-black bg-opacity-50 flex items-center justify-center">
|
||||||
|
<div className="flex fixed top-0 left-0 right-0 w-full h-full" />
|
||||||
|
<div class="relative w-full max-w-2xl max-h-full">
|
||||||
|
<form ref={formEl} onSubmit={handleLogin}>
|
||||||
|
<div class="relative bg-white rounded-lg shadow dark:bg-stone-700">
|
||||||
|
<div class="flex items-start justify-between p-4 border-b rounded-t dark:border-gray-600">
|
||||||
|
<h3 class="text-xl font-semibold text-gray-900 dark:text-white">
|
||||||
|
This workspace is password protected.
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="p-6 space-y-6 flex h-full w-full">
|
||||||
|
<div className="w-full flex flex-col gap-y-4">
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="password"
|
||||||
|
class="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
|
||||||
|
>
|
||||||
|
Workspace Password
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
id="password"
|
||||||
|
class="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"
|
||||||
|
required={true}
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{error && (
|
||||||
|
<p className="text-red-600 dark:text-red-400 text-sm">
|
||||||
|
Error: {error}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<p className="text-gray-800 dark:text-slate-200 text-sm">
|
||||||
|
You will only have to enter this password once. After
|
||||||
|
successful login it will be stored in your browser.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-end p-6 space-x-2 border-t border-gray-200 rounded-b dark:border-gray-600">
|
||||||
|
<button
|
||||||
|
disabled={loading}
|
||||||
|
type="submit"
|
||||||
|
class="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-blue-300 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600"
|
||||||
|
>
|
||||||
|
{loading ? "Validating..." : "Submit"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 };
|
||||||
|
}
|
@ -51,7 +51,8 @@ export default function ActiveWorkspaces() {
|
|||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
href={isActive ? null : paths.workspace.chat(workspace.slug)}
|
href={isActive ? null : paths.workspace.chat(workspace.slug)}
|
||||||
className={`flex flex-grow w-[75%] h-[36px] gap-x-2 py-[5px] px-4 border border-slate-400 rounded-lg text-slate-800 dark:text-slate-200 justify-start items-center ${isActive
|
className={`flex flex-grow w-[75%] h-[36px] gap-x-2 py-[5px] px-4 border border-slate-400 rounded-lg text-slate-800 dark:text-slate-200 justify-start items-center ${
|
||||||
|
isActive
|
||||||
? "bg-gray-100 dark:bg-stone-600"
|
? "bg-gray-100 dark:bg-stone-600"
|
||||||
: "hover:bg-slate-100 dark:hover:bg-stone-900 "
|
: "hover:bg-slate-100 dark:hover:bg-stone-900 "
|
||||||
}`}
|
}`}
|
||||||
|
134
frontend/src/components/Sidebar/Placeholder/index.jsx
Normal file
134
frontend/src/components/Sidebar/Placeholder/index.jsx
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
import React, { useRef } from "react";
|
||||||
|
import {
|
||||||
|
BookOpen,
|
||||||
|
Briefcase,
|
||||||
|
Cpu,
|
||||||
|
GitHub,
|
||||||
|
Key,
|
||||||
|
Plus,
|
||||||
|
AlertCircle,
|
||||||
|
} from "react-feather";
|
||||||
|
import * as Skeleton from "react-loading-skeleton";
|
||||||
|
import "react-loading-skeleton/dist/skeleton.css";
|
||||||
|
import paths from "../../../utils/paths";
|
||||||
|
|
||||||
|
export default function Sidebar() {
|
||||||
|
const sidebarRef = useRef(null);
|
||||||
|
|
||||||
|
// const handleWidthToggle = () => {
|
||||||
|
// if (!sidebarRef.current) return false;
|
||||||
|
// sidebarRef.current.classList.add('translate-x-[-100%]')
|
||||||
|
// }
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
ref={sidebarRef}
|
||||||
|
style={{ height: "calc(100% - 32px)" }}
|
||||||
|
className="transition-all duration-500 relative m-[16px] rounded-[26px] bg-white dark:bg-black-900 min-w-[15.5%] p-[18px] "
|
||||||
|
>
|
||||||
|
{/* <button onClick={handleWidthToggle} className='absolute -right-[13px] top-[35%] bg-white w-auto h-auto bg-transparent flex items-center'>
|
||||||
|
<svg width="16" height="96" viewBox="0 0 16 96" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="#141414"><path d="M2.5 0H3C3 20 15 12 15 32V64C15 84 3 76 3 96H2.5V0Z" fill="black" fill-opacity="0.12" stroke="transparent" stroke-width="0px"></path><path d="M0 0H2.5C2.5 20 14.5 12 14.5 32V64C14.5 84 2.5 76 2.5 96H0V0Z" fill="#141414"></path></svg>
|
||||||
|
<ChevronLeft className='absolute h-4 w-4 text-white mr-1' />
|
||||||
|
</button> */}
|
||||||
|
|
||||||
|
<div className="w-full h-full flex flex-col overflow-x-hidden items-between">
|
||||||
|
{/* Header Information */}
|
||||||
|
<div className="flex w-full items-center justify-between">
|
||||||
|
<p className="text-xl font-base text-slate-600 dark:text-slate-200">
|
||||||
|
AnythingLLM
|
||||||
|
</p>
|
||||||
|
<div className="flex gap-x-2 items-center text-slate-500">
|
||||||
|
<button className="transition-all duration-300 p-2 rounded-full bg-slate-200 text-slate-400 dark:bg-stone-800 hover:bg-slate-800 hover:text-slate-200 dark:hover:text-slate-200">
|
||||||
|
<Key className="h-4 w-4 " />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Primary Body */}
|
||||||
|
<div className="h-[100%] flex flex-col w-full justify-between pt-4 overflow-y-hidden">
|
||||||
|
<div className="h-auto sidebar-items dark:sidebar-items">
|
||||||
|
<div className="flex flex-col gap-y-4 h-[65vh] pb-8 overflow-y-scroll no-scroll">
|
||||||
|
<div className="flex gap-x-2 items-center justify-between">
|
||||||
|
<button className="flex flex-grow w-[75%] h-[36px] gap-x-2 py-[5px] px-4 border border-slate-400 rounded-lg text-slate-800 dark:text-slate-200 justify-start items-center hover:bg-slate-100 dark:hover:bg-stone-900">
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
<p className="text-slate-800 dark:text-slate-200 text-xs leading-loose font-semibold">
|
||||||
|
New workspace
|
||||||
|
</p>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<Skeleton.default
|
||||||
|
height={36}
|
||||||
|
width="100%"
|
||||||
|
count={3}
|
||||||
|
baseColor="#292524"
|
||||||
|
highlightColor="#4c4948"
|
||||||
|
enableAnimation={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="flex flex-col gap-y-2">
|
||||||
|
<div className="w-full flex items-center justify-start">
|
||||||
|
<div className="flex w-fit items-center justify-end gap-x-2">
|
||||||
|
<p className="text-slate-400 leading-loose text-sm">LLM</p>
|
||||||
|
<div className="flex items-center gap-x-1 border border-red-400 px-2 bg-red-200 rounded-full">
|
||||||
|
<p className="text-red-700 leading-tight text-sm">
|
||||||
|
offline
|
||||||
|
</p>
|
||||||
|
<AlertCircle className="h-3 w-3 stroke-red-100 fill-red-400" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
href={paths.hosting()}
|
||||||
|
target="_blank"
|
||||||
|
className="flex flex-grow w-[100%] h-[36px] gap-x-2 py-[5px] px-4 border border-slate-400 dark:border-transparent rounded-lg text-slate-800 dark:text-slate-200 justify-center items-center hover:bg-slate-100 dark:bg-stone-800 dark:hover:bg-stone-900"
|
||||||
|
>
|
||||||
|
<Cpu className="h-4 w-4" />
|
||||||
|
<p className="text-slate-800 dark:text-slate-200 text-xs leading-loose font-semibold">
|
||||||
|
Managed cloud hosting
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href={paths.hosting()}
|
||||||
|
target="_blank"
|
||||||
|
className="flex flex-grow w-[100%] h-[36px] gap-x-2 py-[5px] px-4 border border-slate-400 dark:border-transparent rounded-lg text-slate-800 dark:text-slate-200 justify-center items-center hover:bg-slate-100 dark:bg-stone-800 dark:hover:bg-stone-900"
|
||||||
|
>
|
||||||
|
<Briefcase className="h-4 w-4" />
|
||||||
|
<p className="text-slate-800 dark:text-slate-200 text-xs leading-loose font-semibold">
|
||||||
|
Enterprise Installation
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<div className="flex items-end justify-between mt-2">
|
||||||
|
<div className="flex gap-x-1 items-center">
|
||||||
|
<a
|
||||||
|
href={paths.github()}
|
||||||
|
className="transition-all duration-300 p-2 rounded-full bg-slate-200 text-slate-400 dark:bg-slate-800 hover:bg-slate-800 hover:text-slate-200 dark:hover:text-slate-200"
|
||||||
|
>
|
||||||
|
<GitHub className="h-4 w-4 " />
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href={paths.docs()}
|
||||||
|
className="transition-all duration-300 p-2 rounded-full bg-slate-200 text-slate-400 dark:bg-slate-800 hover:bg-slate-800 hover:text-slate-200 dark:hover:text-slate-200"
|
||||||
|
>
|
||||||
|
<BookOpen className="h-4 w-4 " />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
href={paths.mailToMintplex()}
|
||||||
|
className="transition-all duration-300 text-xs text-slate-200 dark:text-slate-600 hover:text-blue-600 dark:hover:text-blue-400"
|
||||||
|
>
|
||||||
|
@MintplexLabs
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
import { API_BASE } from "../utils/constants";
|
import { API_BASE } from "../utils/constants";
|
||||||
|
import { baseHeaders } from "../utils/request";
|
||||||
|
|
||||||
const System = {
|
const System = {
|
||||||
ping: async function () {
|
ping: async function () {
|
||||||
@ -7,7 +8,9 @@ const System = {
|
|||||||
.catch(() => false);
|
.catch(() => false);
|
||||||
},
|
},
|
||||||
totalIndexes: async function () {
|
totalIndexes: async function () {
|
||||||
return await fetch(`${API_BASE}/system-vectors`)
|
return await fetch(`${API_BASE}/system/system-vectors`, {
|
||||||
|
headers: baseHeaders(),
|
||||||
|
})
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (!res.ok) throw new Error("Could not find indexes.");
|
if (!res.ok) throw new Error("Could not find indexes.");
|
||||||
return res.json();
|
return res.json();
|
||||||
@ -25,7 +28,9 @@ const System = {
|
|||||||
.catch(() => null);
|
.catch(() => null);
|
||||||
},
|
},
|
||||||
localFiles: async function () {
|
localFiles: async function () {
|
||||||
return await fetch(`${API_BASE}/local-files`)
|
return await fetch(`${API_BASE}/system/local-files`, {
|
||||||
|
headers: baseHeaders(),
|
||||||
|
})
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (!res.ok) throw new Error("Could not find setup information.");
|
if (!res.ok) throw new Error("Could not find setup information.");
|
||||||
return res.json();
|
return res.json();
|
||||||
@ -33,6 +38,27 @@ const System = {
|
|||||||
.then((res) => res.localFiles)
|
.then((res) => res.localFiles)
|
||||||
.catch(() => null);
|
.catch(() => null);
|
||||||
},
|
},
|
||||||
|
checkAuth: async function (currentToken = null) {
|
||||||
|
return await fetch(`${API_BASE}/system/check-token`, {
|
||||||
|
headers: baseHeaders(currentToken),
|
||||||
|
})
|
||||||
|
.then((res) => res.ok)
|
||||||
|
.catch(() => false);
|
||||||
|
},
|
||||||
|
requestToken: async function (body) {
|
||||||
|
return await fetch(`${API_BASE}/request-token`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ ...body }),
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
if (!res.ok) throw new Error("Could not validate login.");
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.then((res) => res)
|
||||||
|
.catch((e) => {
|
||||||
|
return { valid: false, message: e.message };
|
||||||
|
});
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default System;
|
export default System;
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
import { API_BASE } from "../utils/constants";
|
import { API_BASE } from "../utils/constants";
|
||||||
|
import { baseHeaders } from "../utils/request";
|
||||||
|
|
||||||
const Workspace = {
|
const Workspace = {
|
||||||
new: async function (data = {}) {
|
new: async function (data = {}) {
|
||||||
const { workspace, message } = await fetch(`${API_BASE}/workspace/new`, {
|
const { workspace, message } = await fetch(`${API_BASE}/workspace/new`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
|
headers: baseHeaders(),
|
||||||
})
|
})
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
@ -19,6 +21,7 @@ const Workspace = {
|
|||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify(changes), // contains 'adds' and 'removes' keys that are arrays of filepaths
|
body: JSON.stringify(changes), // contains 'adds' and 'removes' keys that are arrays of filepaths
|
||||||
|
headers: baseHeaders(),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
@ -29,7 +32,9 @@ const Workspace = {
|
|||||||
return { workspace, message };
|
return { workspace, message };
|
||||||
},
|
},
|
||||||
chatHistory: async function (slug) {
|
chatHistory: async function (slug) {
|
||||||
const history = await fetch(`${API_BASE}/workspace/${slug}/chats`)
|
const history = await fetch(`${API_BASE}/workspace/${slug}/chats`, {
|
||||||
|
headers: baseHeaders(),
|
||||||
|
})
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
.then((res) => res.history || [])
|
.then((res) => res.history || [])
|
||||||
.catch(() => []);
|
.catch(() => []);
|
||||||
@ -39,6 +44,7 @@ const Workspace = {
|
|||||||
const chatResult = await fetch(`${API_BASE}/workspace/${slug}/chat`, {
|
const chatResult = await fetch(`${API_BASE}/workspace/${slug}/chat`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({ message, mode }),
|
body: JSON.stringify({ message, mode }),
|
||||||
|
headers: baseHeaders(),
|
||||||
})
|
})
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
@ -57,7 +63,9 @@ const Workspace = {
|
|||||||
return workspaces;
|
return workspaces;
|
||||||
},
|
},
|
||||||
bySlug: async function (slug = "") {
|
bySlug: async function (slug = "") {
|
||||||
const workspace = await fetch(`${API_BASE}/workspace/${slug}`)
|
const workspace = await fetch(`${API_BASE}/workspace/${slug}`, {
|
||||||
|
headers: baseHeaders(),
|
||||||
|
})
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
.then((res) => res.workspace)
|
.then((res) => res.workspace)
|
||||||
.catch(() => null);
|
.catch(() => null);
|
||||||
@ -66,6 +74,7 @@ const Workspace = {
|
|||||||
delete: async function (slug) {
|
delete: async function (slug) {
|
||||||
const result = await fetch(`${API_BASE}/workspace/${slug}`, {
|
const result = await fetch(`${API_BASE}/workspace/${slug}`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
|
headers: baseHeaders(),
|
||||||
})
|
})
|
||||||
.then((res) => res.ok)
|
.then((res) => res.ok)
|
||||||
.catch(() => false);
|
.catch(() => false);
|
||||||
|
@ -1,8 +1,26 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import DefaultChatContainer from "../../components/DefaultChat";
|
import DefaultChatContainer from "../../components/DefaultChat";
|
||||||
import Sidebar from "../../components/Sidebar";
|
import Sidebar from "../../components/Sidebar";
|
||||||
|
import SidebarPlaceholder from "../../components/Sidebar/Placeholder";
|
||||||
|
import ChatPlaceholder from "../../components/WorkspaceChat/LoadingChat";
|
||||||
|
import PasswordModal, {
|
||||||
|
usePasswordModal,
|
||||||
|
} from "../../components/Modals/Password";
|
||||||
|
|
||||||
export default function Main() {
|
export default function Main() {
|
||||||
|
const { requiresAuth } = usePasswordModal();
|
||||||
|
if (requiresAuth === null || requiresAuth) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{requiresAuth && <PasswordModal />}
|
||||||
|
<div className="w-screen h-screen overflow-hidden bg-orange-100 dark:bg-stone-700 flex">
|
||||||
|
<SidebarPlaceholder />
|
||||||
|
<ChatPlaceholder />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-screen h-screen overflow-hidden bg-orange-100 dark:bg-stone-700 flex">
|
<div className="w-screen h-screen overflow-hidden bg-orange-100 dark:bg-stone-700 flex">
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
|
@ -3,8 +3,30 @@ import { default as WorkspaceChatContainer } from "../../components/WorkspaceCha
|
|||||||
import Sidebar from "../../components/Sidebar";
|
import Sidebar from "../../components/Sidebar";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import Workspace from "../../models/workspace";
|
import Workspace from "../../models/workspace";
|
||||||
|
import SidebarPlaceholder from "../../components/Sidebar/Placeholder";
|
||||||
|
import ChatPlaceholder from "../../components/WorkspaceChat/LoadingChat";
|
||||||
|
import PasswordModal, {
|
||||||
|
usePasswordModal,
|
||||||
|
} from "../../components/Modals/Password";
|
||||||
|
|
||||||
export default function WorkspaceChat() {
|
export default function WorkspaceChat() {
|
||||||
|
const { requiresAuth } = usePasswordModal();
|
||||||
|
if (requiresAuth === null || requiresAuth) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{requiresAuth && <PasswordModal />}
|
||||||
|
<div className="w-screen h-screen overflow-hidden bg-orange-100 dark:bg-stone-700 flex">
|
||||||
|
<SidebarPlaceholder />
|
||||||
|
<ChatPlaceholder />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <ShowWorkspaceChat />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ShowWorkspaceChat() {
|
||||||
const { slug } = useParams();
|
const { slug } = useParams();
|
||||||
const [workspace, setWorkspace] = useState(null);
|
const [workspace, setWorkspace] = useState(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
9
frontend/src/utils/request.js
Normal file
9
frontend/src/utils/request.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
// 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 baseHeaders(providedToken = null) {
|
||||||
|
const token =
|
||||||
|
providedToken || window.localStorage.getItem("anythingllm_authtoken");
|
||||||
|
return {
|
||||||
|
Authorization: token ? `Bearer ${token}` : null,
|
||||||
|
};
|
||||||
|
}
|
@ -18,4 +18,5 @@ PINECONE_INDEX=
|
|||||||
|
|
||||||
# CLOUD DEPLOYMENT VARIRABLES ONLY
|
# CLOUD DEPLOYMENT VARIRABLES ONLY
|
||||||
# AUTH_TOKEN="hunter2" # This is the password to your application if remote hosting.
|
# 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
|
# STORAGE_DIR= # absolute filesystem path with no trailing slash
|
@ -3,6 +3,7 @@ process.env.NODE_ENV === "development"
|
|||||||
: require("dotenv").config();
|
: require("dotenv").config();
|
||||||
const { viewLocalFiles } = require("../utils/files");
|
const { viewLocalFiles } = require("../utils/files");
|
||||||
const { getVectorDbClass } = require("../utils/helpers");
|
const { getVectorDbClass } = require("../utils/helpers");
|
||||||
|
const { reqBody, makeJWT } = require("../utils/http");
|
||||||
|
|
||||||
function systemEndpoints(app) {
|
function systemEndpoints(app) {
|
||||||
if (!app) return;
|
if (!app) return;
|
||||||
@ -15,6 +16,7 @@ function systemEndpoints(app) {
|
|||||||
try {
|
try {
|
||||||
const vectorDB = process.env.VECTOR_DB || "pinecone";
|
const vectorDB = process.env.VECTOR_DB || "pinecone";
|
||||||
const results = {
|
const results = {
|
||||||
|
RequiresAuth: !!process.env.AUTH_TOKEN,
|
||||||
VectorDB: vectorDB,
|
VectorDB: vectorDB,
|
||||||
OpenAiKey: !!process.env.OPEN_AI_KEY,
|
OpenAiKey: !!process.env.OPEN_AI_KEY,
|
||||||
OpenAiModelPref: process.env.OPEN_MODEL_PREF || "gpt-3.5-turbo",
|
OpenAiModelPref: process.env.OPEN_MODEL_PREF || "gpt-3.5-turbo",
|
||||||
@ -38,7 +40,37 @@ function systemEndpoints(app) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get("/system-vectors", async (_, response) => {
|
app.get("/system/check-token", (_, response) => {
|
||||||
|
response.sendStatus(200).end();
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post("/request-token", (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",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
response.status(200).json({
|
||||||
|
valid: true,
|
||||||
|
token: makeJWT({ p: password }, "30d"),
|
||||||
|
message: null,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e.message, e);
|
||||||
|
response.sendStatus(500).end();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/system/system-vectors", async (_, response) => {
|
||||||
try {
|
try {
|
||||||
const VectorDb = getVectorDbClass();
|
const VectorDb = getVectorDbClass();
|
||||||
const vectorCount = await VectorDb.totalIndicies();
|
const vectorCount = await VectorDb.totalIndicies();
|
||||||
@ -49,7 +81,7 @@ function systemEndpoints(app) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get("/local-files", async (_, response) => {
|
app.get("/system/local-files", async (_, response) => {
|
||||||
try {
|
try {
|
||||||
const localFiles = await viewLocalFiles();
|
const localFiles = await viewLocalFiles();
|
||||||
response.status(200).json({ localFiles });
|
response.status(200).json({ localFiles });
|
||||||
|
@ -14,7 +14,6 @@ const { getVectorDbClass } = require("./utils/helpers");
|
|||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
app.use(cors({ origin: true }));
|
app.use(cors({ origin: true }));
|
||||||
app.use(validatedRequest);
|
|
||||||
app.use(bodyParser.text());
|
app.use(bodyParser.text());
|
||||||
app.use(bodyParser.json());
|
app.use(bodyParser.json());
|
||||||
app.use(
|
app.use(
|
||||||
@ -23,6 +22,8 @@ app.use(
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
app.use("/system/*", validatedRequest);
|
||||||
|
app.use("/workspace/*", validatedRequest);
|
||||||
systemEndpoints(app);
|
systemEndpoints(app);
|
||||||
workspaceEndpoints(app);
|
workspaceEndpoints(app);
|
||||||
chatEndpoints(app);
|
chatEndpoints(app);
|
||||||
|
@ -30,6 +30,7 @@
|
|||||||
"sqlite": "^4.2.1",
|
"sqlite": "^4.2.1",
|
||||||
"sqlite3": "^5.1.6",
|
"sqlite3": "^5.1.6",
|
||||||
"uuid": "^9.0.0",
|
"uuid": "^9.0.0",
|
||||||
|
"jsonwebtoken": "^8.5.1",
|
||||||
"vectordb": "0.1.5-beta"
|
"vectordb": "0.1.5-beta"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -1,3 +1,9 @@
|
|||||||
|
process.env.NODE_ENV === "development"
|
||||||
|
? require("dotenv").config({ path: `.env.${process.env.NODE_ENV}` })
|
||||||
|
: require("dotenv").config();
|
||||||
|
const JWT = require("jsonwebtoken");
|
||||||
|
const SECRET = process.env.JWT_SECRET;
|
||||||
|
|
||||||
function reqBody(request) {
|
function reqBody(request) {
|
||||||
return typeof request.body === "string"
|
return typeof request.body === "string"
|
||||||
? JSON.parse(request.body)
|
? JSON.parse(request.body)
|
||||||
@ -8,7 +14,21 @@ function queryParams(request) {
|
|||||||
return request.query;
|
return request.query;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function makeJWT(info = {}, expiry = "30d") {
|
||||||
|
if (!SECRET) throw new Error("Cannot create JWT as JWT_SECRET is unset.");
|
||||||
|
return JWT.sign(info, SECRET, { expiresIn: expiry });
|
||||||
|
}
|
||||||
|
|
||||||
|
function decodeJWT(jwtToken) {
|
||||||
|
try {
|
||||||
|
return JWT.verify(jwtToken, SECRET);
|
||||||
|
} catch {}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
reqBody,
|
reqBody,
|
||||||
queryParams,
|
queryParams,
|
||||||
|
makeJWT,
|
||||||
|
decodeJWT,
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,13 @@
|
|||||||
|
const { decodeJWT } = require("../http");
|
||||||
|
|
||||||
function validatedRequest(request, response, next) {
|
function validatedRequest(request, response, next) {
|
||||||
// When in development passthrough auth token for ease of development.
|
// When in development passthrough auth token for ease of development.
|
||||||
if (process.env.NODE_ENV === "development" || !process.env.AUTH_TOKEN) {
|
// Or if the user simply did not set an Auth token or JWT Secret
|
||||||
|
if (
|
||||||
|
process.env.NODE_ENV === "development" ||
|
||||||
|
!process.env.AUTH_TOKEN ||
|
||||||
|
!process.env.JWT_SECRET
|
||||||
|
) {
|
||||||
next();
|
next();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -22,7 +29,8 @@ function validatedRequest(request, response, next) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (token !== process.env.AUTH_TOKEN) {
|
const { p } = decodeJWT(token);
|
||||||
|
if (p !== process.env.AUTH_TOKEN) {
|
||||||
response.status(403).json({
|
response.status(403).json({
|
||||||
error: "Invalid auth token found.",
|
error: "Invalid auth token found.",
|
||||||
});
|
});
|
||||||
|
@ -26,7 +26,8 @@ function curateLanceSources(sources = []) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const LanceDb = {
|
const LanceDb = {
|
||||||
uri: `${!!process.env.STORAGE_DIR ? `${process.env.STORAGE_DIR}/` : "./"
|
uri: `${
|
||||||
|
!!process.env.STORAGE_DIR ? `${process.env.STORAGE_DIR}/` : "./"
|
||||||
}lancedb`,
|
}lancedb`,
|
||||||
name: "LanceDb",
|
name: "LanceDb",
|
||||||
connect: async function () {
|
connect: async function () {
|
||||||
@ -282,4 +283,4 @@ const LanceDb = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports.LanceDb = LanceDb
|
module.exports.LanceDb = LanceDb;
|
||||||
|
Loading…
Reference in New Issue
Block a user