12 auth implementation (#13)

* Add Auth protection for cloud-based or private instances

* skip check on local dev
This commit is contained in:
Timothy Carambat 2023-06-09 11:27:27 -07:00 committed by GitHub
parent fdacf4bb2e
commit 62e3f62e82
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 429 additions and 26 deletions

View File

@ -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">

View File

@ -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;
} }

View 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 };
}

View File

@ -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 "
}`} }`}

View 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>
</>
);
}

View File

@ -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;

View File

@ -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);

View File

@ -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 />

View File

@ -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);

View 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,
};
}

View File

@ -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

View File

@ -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 });

View File

@ -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);

View File

@ -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": {

View File

@ -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,
}; };

View File

@ -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.",
}); });

View File

@ -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;