mirror of
https://github.com/Mintplex-Labs/anything-llm.git
synced 2024-11-16 11:20:10 +01:00
Full developer api (#221)
* Autodocument Swagger API with JSDocs on /v1/ endpoints for API access implement single-player API keys WIP Admin API Keys * Create new api keys as both single and multi-user * Add boot and telem * Complete Admin API * Complete endpoints dark mode swagger * update docs * undo debug * update docs and readme
This commit is contained in:
parent
bdf9529e80
commit
defe6054b3
@ -49,6 +49,7 @@ Some cool features of AnythingLLM
|
||||
- 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.
|
||||
- Full Developer API for custom integrations!
|
||||
|
||||
### Technical Overview
|
||||
This monorepo consists of three main sections:
|
||||
|
BIN
frontend/public/anything-llm-dark.png
Normal file
BIN
frontend/public/anything-llm-dark.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.2 KiB |
BIN
frontend/public/anything-llm-light.png
Normal file
BIN
frontend/public/anything-llm-light.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.2 KiB |
@ -14,6 +14,7 @@ const AdminWorkspaces = lazy(() => import("./pages/Admin/Workspaces"));
|
||||
const AdminChats = lazy(() => import("./pages/Admin/Chats"));
|
||||
const AdminSystem = lazy(() => import("./pages/Admin/System"));
|
||||
const AdminAppearance = lazy(() => import("./pages/Admin/Appearance"));
|
||||
const AdminApiKeys = lazy(() => import("./pages/Admin/ApiKeys"));
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
@ -52,6 +53,10 @@ export default function App() {
|
||||
path="/admin/appearance"
|
||||
element={<AdminRoute Component={AdminAppearance} />}
|
||||
/>
|
||||
<Route
|
||||
path="/admin/api-keys"
|
||||
element={<AdminRoute Component={AdminApiKeys} />}
|
||||
/>
|
||||
</Routes>
|
||||
<ToastContainer />
|
||||
</ContextWrapper>
|
||||
|
@ -3,6 +3,7 @@ import {
|
||||
BookOpen,
|
||||
Eye,
|
||||
GitHub,
|
||||
Key,
|
||||
Mail,
|
||||
Menu,
|
||||
MessageSquare,
|
||||
@ -82,6 +83,11 @@ export default function AdminSidebar() {
|
||||
btnText="Appearance"
|
||||
icon={<Eye className="h-4 w-4 flex-shrink-0" />}
|
||||
/>
|
||||
<Option
|
||||
href={paths.admin.apiKeys()}
|
||||
btnText="API Keys"
|
||||
icon={<Key className="h-4 w-4 flex-shrink-0" />}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
@ -242,6 +248,11 @@ export function SidebarMobileHeader() {
|
||||
btnText="Appearance"
|
||||
icon={<Eye className="h-4 w-4 flex-shrink-0" />}
|
||||
/>
|
||||
<Option
|
||||
href={paths.admin.apiKeys()}
|
||||
btnText="API Keys"
|
||||
icon={<Key className="h-4 w-4 flex-shrink-0" />}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
|
198
frontend/src/components/Modals/Settings/ApiKey/index.jsx
Normal file
198
frontend/src/components/Modals/Settings/ApiKey/index.jsx
Normal file
@ -0,0 +1,198 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import System from "../../../../models/system";
|
||||
import PreLoader from "../../../Preloader";
|
||||
import paths from "../../../../utils/paths";
|
||||
import showToast from "../../../../utils/toast";
|
||||
import { CheckCircle, Copy, RefreshCcw, Trash } from "react-feather";
|
||||
|
||||
export default function ApiKey() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [generating, setGenerating] = useState(false);
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
const [apiKey, setApiKey] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchExistingApiKey() {
|
||||
const { apiKey: _apiKey } = await System.getApiKey();
|
||||
setApiKey(_apiKey);
|
||||
setLoading(false);
|
||||
}
|
||||
fetchExistingApiKey();
|
||||
}, []);
|
||||
|
||||
const generateApiKey = async () => {
|
||||
setGenerating(true);
|
||||
const isRefresh = !!apiKey;
|
||||
const { apiKey: newApiKey, error } = await System.generateApiKey();
|
||||
if (!!error) {
|
||||
showToast(error, "error");
|
||||
} else {
|
||||
showToast(
|
||||
isRefresh ? "API key regenerated!" : "API key generated!",
|
||||
"info"
|
||||
);
|
||||
setApiKey(newApiKey);
|
||||
}
|
||||
setGenerating(false);
|
||||
};
|
||||
|
||||
const removeApiKey = async () => {
|
||||
setDeleting(true);
|
||||
const ok = await System.deleteApiKey();
|
||||
if (ok) {
|
||||
showToast("API key deleted from instance.", "info");
|
||||
setApiKey(null);
|
||||
} else {
|
||||
showToast("API key could not be deleted.", "error");
|
||||
}
|
||||
setDeleting(false);
|
||||
};
|
||||
|
||||
const copyToClipboard = async () => {
|
||||
window.navigator.clipboard.writeText(apiKey.secret);
|
||||
showToast("API key copied to clipboard!", "info");
|
||||
setCopied(true);
|
||||
setTimeout(() => {
|
||||
setCopied(false);
|
||||
}, 1200);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="relative w-full w-full max-h-full">
|
||||
<div className="relative bg-white rounded-lg shadow dark:bg-stone-700">
|
||||
<div className="flex items-start justify-between px-6 py-4">
|
||||
<p className="text-gray-800 dark:text-stone-200 text-base ">
|
||||
Generate an API Key for your AnythingLLM instance.
|
||||
</p>
|
||||
</div>
|
||||
<div className="px-1 md:px-8 pb-10 ">
|
||||
<PreLoader />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!apiKey) {
|
||||
return (
|
||||
<div className="relative w-full w-full max-h-full">
|
||||
<div className="relative bg-white rounded-lg shadow dark:bg-stone-700">
|
||||
<div className="flex items-start justify-between px-6 py-4">
|
||||
<p className="text-gray-800 dark:text-stone-200 text-base ">
|
||||
Generate an API Key for your AnythingLLM instance.
|
||||
</p>
|
||||
</div>
|
||||
<div className="md:px-8 pb-10 ">
|
||||
<div className="flex flex-col gap-y-1 text-gray-800 dark:text-stone-200 mb-2">
|
||||
<p>
|
||||
No api key for this instance exists. Create one by clicking the
|
||||
button below.
|
||||
</p>
|
||||
<a
|
||||
href={paths.apiDocs()}
|
||||
target="_blank"
|
||||
className="dark:text-blue-300 text-blue-600 hover:underline"
|
||||
>
|
||||
View endpoint documentation →
|
||||
</a>
|
||||
</div>
|
||||
<button
|
||||
disabled={generating}
|
||||
type="button"
|
||||
onClick={generateApiKey}
|
||||
className="w-full 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"
|
||||
>
|
||||
{generating ? "Generating..." : "Generate new API key"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative w-full w-full max-h-full">
|
||||
<div className="relative bg-white rounded-lg shadow dark:bg-stone-700">
|
||||
<div className="flex flex-col items-start justify-between px-6 py-4">
|
||||
<p className="text-gray-800 dark:text-stone-200 text-base ">
|
||||
Use this API key for interacting with your AnythingLLM instance
|
||||
programmatically.
|
||||
</p>
|
||||
<a
|
||||
href={paths.apiDocs()}
|
||||
target="_blank"
|
||||
className="dark:text-blue-300 text-blue-600 hover:underline"
|
||||
>
|
||||
View endpoint documentation →
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="md:px-8 pb-10">
|
||||
<div className="mb-6">
|
||||
<div className="flex flex-col md:flex-row items-center">
|
||||
<div className="flex md:flex-row flex-col gap-y-2 w-full gap-x-2 items-center px-4 md:px-0">
|
||||
<input
|
||||
key={apiKey.secret}
|
||||
type="text"
|
||||
disabled={true}
|
||||
className="w-full md:w-1/2 bg-gray-50 border border-gray-500 text-gray-900 placeholder-gray-500 text-sm rounded-lg dark:bg-stone-700 focus:border-stone-500 block p-2.5 dark:text-slate-200 dark:placeholder-stone-500 dark:border-slate-200"
|
||||
defaultValue={apiKey.secret}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
/>
|
||||
<button
|
||||
onClick={copyToClipboard}
|
||||
disabled={copied}
|
||||
className="w-full flex justify-center items-center gap-x-2 md:w-fit disabled:bg-green-300 dark:disabled:bg-green-600 bg-gray-50 border border-gray-500 text-gray-900 placeholder-gray-500 text-sm rounded-lg dark:bg-stone-700 focus:border-stone-500 block p-2.5 dark:text-slate-200 dark:placeholder-stone-500 dark:border-slate-200 group hover:bg-gray-100 dark:hover:bg-stone-600"
|
||||
>
|
||||
{copied ? (
|
||||
<CheckCircle className="stroke-green-800 dark:stroke-green-300" />
|
||||
) : (
|
||||
<Copy />
|
||||
)}
|
||||
<p className="block md:hidden text-base">Copy API Key</p>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (
|
||||
!confirm(
|
||||
"Are you sure you want to refresh the API key? The old key will no longer work!"
|
||||
)
|
||||
)
|
||||
return false;
|
||||
generateApiKey();
|
||||
}}
|
||||
disabled={generating}
|
||||
className="w-full flex justify-center items-center gap-x-2 md:w-fit disabled:bg-green-300 dark:disabled:bg-green-600 bg-gray-50 border border-gray-500 text-gray-900 placeholder-gray-500 text-sm rounded-lg dark:bg-stone-700 focus:border-stone-500 block p-2.5 dark:text-slate-200 dark:placeholder-stone-500 dark:border-slate-200 group hover:bg-gray-100 dark:hover:bg-stone-600"
|
||||
>
|
||||
<RefreshCcw />
|
||||
<p className="block md:hidden text-base">
|
||||
Regenerate API Key
|
||||
</p>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (
|
||||
!confirm(
|
||||
"Are you sure you want to delete the API key? All API keys will be deleted."
|
||||
)
|
||||
)
|
||||
return false;
|
||||
removeApiKey();
|
||||
}}
|
||||
disabled={deleting}
|
||||
className="w-full flex justify-center items-center gap-x-2 md:w-fit disabled:bg-red-300 dark:disabled:bg-red-600 border border-red-500 text-red-900 placeholder-red-500 text-sm rounded-lg dark:bg-transparent focus:border-red-500 block p-2.5 dark:text-red-200 dark:placeholder-red-500 dark:border-red-200 group hover:bg-red-100 dark:hover:bg-red-600"
|
||||
>
|
||||
<Trash />
|
||||
<p className="block md:hidden text-base">Delete API Key</p>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -8,6 +8,7 @@ import useUser from "../../../hooks/useUser";
|
||||
import VectorDBSelection from "./VectorDbs";
|
||||
import LLMSelection from "./LLMSelection";
|
||||
import Appearance from "./Appearance";
|
||||
import ApiKey from "./ApiKey";
|
||||
|
||||
export const TABS = {
|
||||
llm: LLMSelection,
|
||||
@ -16,6 +17,7 @@ export const TABS = {
|
||||
multiuser: MultiUserMode,
|
||||
vectordb: VectorDBSelection,
|
||||
appearance: Appearance,
|
||||
apikey: ApiKey,
|
||||
};
|
||||
|
||||
const noop = () => false;
|
||||
|
@ -7,6 +7,7 @@ import {
|
||||
Database,
|
||||
MessageSquare,
|
||||
Eye,
|
||||
Key,
|
||||
} from "react-feather";
|
||||
import SystemSettingsModal, {
|
||||
useSystemSettingsModal,
|
||||
@ -127,6 +128,12 @@ export default function SettingsOverlay() {
|
||||
isActive={tab === "multiuser"}
|
||||
onClick={() => selectTab("multiuser")}
|
||||
/>
|
||||
<Option
|
||||
btnText="API Key"
|
||||
icon={<Key className="h-4 w-4 flex-shrink-0" />}
|
||||
isActive={tab === "apikey"}
|
||||
onClick={() => selectTab("apikey")}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
@ -232,6 +232,51 @@ const Admin = {
|
||||
return { success: false, error: e.message };
|
||||
});
|
||||
},
|
||||
|
||||
// API Keys
|
||||
getApiKeys: async function () {
|
||||
return fetch(`${API_BASE}/admin/api-keys`, {
|
||||
method: "GET",
|
||||
headers: baseHeaders(),
|
||||
})
|
||||
.then((res) => {
|
||||
if (!res.ok) {
|
||||
throw new Error(res.statusText || "Error fetching api keys.");
|
||||
}
|
||||
return res.json();
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
return { apiKeys: [], error: e.message };
|
||||
});
|
||||
},
|
||||
generateApiKey: async function () {
|
||||
return fetch(`${API_BASE}/admin/generate-api-key`, {
|
||||
method: "POST",
|
||||
headers: baseHeaders(),
|
||||
})
|
||||
.then((res) => {
|
||||
if (!res.ok) {
|
||||
throw new Error(res.statusText || "Error generating api key.");
|
||||
}
|
||||
return res.json();
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
return { apiKey: null, error: e.message };
|
||||
});
|
||||
},
|
||||
deleteApiKey: async function (apiKeyId = "") {
|
||||
return fetch(`${API_BASE}/admin/delete-api-key/${apiKeyId}`, {
|
||||
method: "DELETE",
|
||||
headers: baseHeaders(),
|
||||
})
|
||||
.then((res) => res.ok)
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
return false;
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default Admin;
|
||||
|
@ -220,6 +220,49 @@ const System = {
|
||||
return { success: false, error: e.message };
|
||||
});
|
||||
},
|
||||
getApiKey: async function () {
|
||||
return fetch(`${API_BASE}/system/api-key`, {
|
||||
method: "GET",
|
||||
headers: baseHeaders(),
|
||||
})
|
||||
.then((res) => {
|
||||
if (!res.ok) {
|
||||
throw new Error(res.statusText || "Error fetching api key.");
|
||||
}
|
||||
return res.json();
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
return { apiKey: null, error: e.message };
|
||||
});
|
||||
},
|
||||
generateApiKey: async function () {
|
||||
return fetch(`${API_BASE}/system/generate-api-key`, {
|
||||
method: "POST",
|
||||
headers: baseHeaders(),
|
||||
})
|
||||
.then((res) => {
|
||||
if (!res.ok) {
|
||||
throw new Error(res.statusText || "Error generating api key.");
|
||||
}
|
||||
return res.json();
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
return { apiKey: null, error: e.message };
|
||||
});
|
||||
},
|
||||
deleteApiKey: async function () {
|
||||
return fetch(`${API_BASE}/system/api-key`, {
|
||||
method: "DELETE",
|
||||
headers: baseHeaders(),
|
||||
})
|
||||
.then((res) => res.ok)
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
return false;
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default System;
|
||||
|
69
frontend/src/pages/Admin/ApiKeys/ApiKeyRow/index.jsx
Normal file
69
frontend/src/pages/Admin/ApiKeys/ApiKeyRow/index.jsx
Normal file
@ -0,0 +1,69 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import Admin from "../../../../models/admin";
|
||||
import showToast from "../../../../utils/toast";
|
||||
|
||||
export default function ApiKeyRow({ apiKey }) {
|
||||
const rowRef = useRef(null);
|
||||
const [copied, setCopied] = useState(false);
|
||||
const handleDelete = async () => {
|
||||
if (
|
||||
!window.confirm(
|
||||
`Are you sure you want to deactivate this api key?\nAfter you do this it will not longer be useable.\n\nThis action is irreversible.`
|
||||
)
|
||||
)
|
||||
return false;
|
||||
if (rowRef?.current) {
|
||||
rowRef.current.remove();
|
||||
}
|
||||
await Admin.deleteApiKey(apiKey.id);
|
||||
showToast("API Key permanently deleted", "info");
|
||||
};
|
||||
const copyApiKey = () => {
|
||||
if (!apiKey) return false;
|
||||
window.navigator.clipboard.writeText(apiKey.secret);
|
||||
showToast("API Key copied to clipboard", "success");
|
||||
setCopied(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
function resetStatus() {
|
||||
if (!copied) return false;
|
||||
setTimeout(() => {
|
||||
setCopied(false);
|
||||
}, 3000);
|
||||
}
|
||||
resetStatus();
|
||||
}, [copied]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<tr ref={rowRef} className="bg-transparent">
|
||||
<td
|
||||
scope="row"
|
||||
className="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white font-mono"
|
||||
>
|
||||
{apiKey.secret}
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
{apiKey.createdBy?.username || "unknown user"}
|
||||
</td>
|
||||
<td className="px-6 py-4">{apiKey.createdAt}</td>
|
||||
<td className="px-6 py-4 flex items-center gap-x-6">
|
||||
<button
|
||||
onClick={copyApiKey}
|
||||
disabled={copied}
|
||||
className="font-medium text-blue-600 dark:text-blue-300 px-2 py-1 rounded-lg hover:bg-blue-50 hover:dark:bg-blue-800 hover:dark:bg-opacity-20"
|
||||
>
|
||||
{copied ? "Copied" : "Copy API Key"}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleDelete}
|
||||
className="font-medium text-red-600 dark:text-red-300 px-2 py-1 rounded-lg hover:bg-red-50 hover:dark:bg-red-800 hover:dark:bg-opacity-20"
|
||||
>
|
||||
Deactivate API Key
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</>
|
||||
);
|
||||
}
|
118
frontend/src/pages/Admin/ApiKeys/NewApiKeyModal/index.jsx
Normal file
118
frontend/src/pages/Admin/ApiKeys/NewApiKeyModal/index.jsx
Normal file
@ -0,0 +1,118 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { X } from "react-feather";
|
||||
import Admin from "../../../../models/admin";
|
||||
import paths from "../../../../utils/paths";
|
||||
const DIALOG_ID = `new-api-key-modal`;
|
||||
|
||||
function hideModal() {
|
||||
document.getElementById(DIALOG_ID)?.close();
|
||||
}
|
||||
|
||||
export const NewApiKeyModalId = DIALOG_ID;
|
||||
export default function NewApiKeyModal() {
|
||||
const [apiKey, setApiKey] = useState(null);
|
||||
const [error, setError] = useState(null);
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const handleCreate = async (e) => {
|
||||
setError(null);
|
||||
e.preventDefault();
|
||||
const { apiKey: newApiKey, error } = await Admin.generateApiKey();
|
||||
if (!!newApiKey) setApiKey(newApiKey);
|
||||
setError(error);
|
||||
};
|
||||
const copyApiKey = () => {
|
||||
if (!apiKey) return false;
|
||||
window.navigator.clipboard.writeText(apiKey.secret);
|
||||
setCopied(true);
|
||||
};
|
||||
useEffect(() => {
|
||||
function resetStatus() {
|
||||
if (!copied) return false;
|
||||
setTimeout(() => {
|
||||
setCopied(false);
|
||||
}, 3000);
|
||||
}
|
||||
resetStatus();
|
||||
}, [copied]);
|
||||
|
||||
return (
|
||||
<dialog id={DIALOG_ID} className="bg-transparent outline-none">
|
||||
<div className="relative w-full max-w-2xl max-h-full">
|
||||
<div className="relative bg-white rounded-lg shadow dark:bg-stone-700">
|
||||
<div className="flex items-start justify-between p-4 border-b rounded-t dark:border-gray-600">
|
||||
<h3 className="text-xl font-semibold text-gray-900 dark:text-white">
|
||||
Create new API key
|
||||
</h3>
|
||||
<button
|
||||
onClick={hideModal}
|
||||
type="button"
|
||||
className="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center dark:hover:bg-gray-600 dark:hover:text-white"
|
||||
data-modal-hide="staticModal"
|
||||
>
|
||||
<X className="text-gray-300 text-lg" />
|
||||
</button>
|
||||
</div>
|
||||
<form onSubmit={handleCreate}>
|
||||
<div className="p-6 space-y-6 flex h-full w-full">
|
||||
<div className="w-full flex flex-col gap-y-4">
|
||||
{error && (
|
||||
<p className="text-red-600 dark:text-red-400 text-sm">
|
||||
Error: {error}
|
||||
</p>
|
||||
)}
|
||||
{apiKey && (
|
||||
<input
|
||||
type="text"
|
||||
defaultValue={`${apiKey.secret}`}
|
||||
disabled={true}
|
||||
className="rounded-lg px-4 py-2 text-gray-800 bg-gray-100 dark:text-slate-200 dark:bg-stone-800"
|
||||
/>
|
||||
)}
|
||||
<p className="text-gray-800 dark:text-slate-200 text-xs md:text-sm">
|
||||
Once created the API key can be used to programmatically
|
||||
access and configure this AnythingLLM instance.
|
||||
</p>
|
||||
<a
|
||||
href={paths.apiDocs()}
|
||||
target="_blank"
|
||||
className="text-blue-600 dark:text-blue-300 hover:underline"
|
||||
>
|
||||
Read the API documentation →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full justify-between items-center p-6 space-x-2 border-t border-gray-200 rounded-b dark:border-gray-600">
|
||||
{!apiKey ? (
|
||||
<>
|
||||
<button
|
||||
onClick={hideModal}
|
||||
type="button"
|
||||
className="text-gray-800 hover:bg-gray-100 px-4 py-1 rounded-lg dark:text-slate-200 dark:hover:bg-stone-900"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="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-black dark:text-slate-200 dark:border-transparent dark:hover:text-slate-200 dark:hover:bg-gray-900 dark:focus:ring-gray-800"
|
||||
>
|
||||
Create API key
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
onClick={copyApiKey}
|
||||
type="button"
|
||||
disabled={copied}
|
||||
className="w-full disabled:bg-green-200 disabled:text-green-600 text-gray-800 bg-gray-100 px-4 py-2 rounded-lg dark:text-slate-200 dark:bg-stone-900"
|
||||
>
|
||||
{copied ? "Copied API key" : "Copy API key"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
);
|
||||
}
|
109
frontend/src/pages/Admin/ApiKeys/index.jsx
Normal file
109
frontend/src/pages/Admin/ApiKeys/index.jsx
Normal file
@ -0,0 +1,109 @@
|
||||
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 { PlusCircle } from "react-feather";
|
||||
import usePrefersDarkMode from "../../../hooks/usePrefersDarkMode";
|
||||
import Admin from "../../../models/admin";
|
||||
import ApiKeyRow from "./ApiKeyRow";
|
||||
import NewApiKeyModal, { NewApiKeyModalId } from "./NewApiKeyModal";
|
||||
import paths from "../../../utils/paths";
|
||||
|
||||
export default function AdminApiKeys() {
|
||||
return (
|
||||
<div className="w-screen h-screen overflow-hidden bg-orange-100 dark:bg-stone-700 flex">
|
||||
{!isMobile && <Sidebar />}
|
||||
<div
|
||||
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
|
||||
className="transition-all duration-500 relative md:ml-[2px] md:mr-[8px] md:my-[16px] md:rounded-[26px] bg-white dark:bg-black-900 md:min-w-[82%] p-[18px] h-full overflow-y-scroll"
|
||||
>
|
||||
{isMobile && <SidebarMobileHeader />}
|
||||
<div className="flex flex-col w-full px-1 md:px-8">
|
||||
<div className="w-full flex flex-col gap-y-1">
|
||||
<div className="items-center flex gap-x-4">
|
||||
<p className="text-3xl font-semibold text-slate-600 dark:text-slate-200">
|
||||
API Keys
|
||||
</p>
|
||||
<button
|
||||
onClick={() =>
|
||||
document?.getElementById(NewApiKeyModalId)?.showModal()
|
||||
}
|
||||
className="border border-slate-800 dark:border-slate-200 px-4 py-1 rounded-lg text-slate-800 dark:text-slate-200 text-sm items-center flex gap-x-2 hover:bg-slate-800 hover:text-slate-100 dark:hover:bg-slate-200 dark:hover:text-slate-800"
|
||||
>
|
||||
<PlusCircle className="h-4 w-4" /> Generate New API Key
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-sm font-base text-slate-600 dark:text-slate-200">
|
||||
API keys allow the holder to programmatically access and manage
|
||||
this AnythingLLM instance.
|
||||
</p>
|
||||
<a
|
||||
href={paths.apiDocs()}
|
||||
target="_blank"
|
||||
className="text-blue-600 dark:text-blue-300 hover:underline"
|
||||
>
|
||||
Read the API documentation →
|
||||
</a>
|
||||
</div>
|
||||
<ApiKeysContainer />
|
||||
</div>
|
||||
<NewApiKeyModal />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ApiKeysContainer() {
|
||||
const darkMode = usePrefersDarkMode();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [apiKeys, setApiKeys] = useState([]);
|
||||
useEffect(() => {
|
||||
async function fetchExistingKeys() {
|
||||
const { apiKeys: foundKeys } = await Admin.getApiKeys();
|
||||
setApiKeys(foundKeys);
|
||||
setLoading(false);
|
||||
}
|
||||
fetchExistingKeys();
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Skeleton.default
|
||||
height="80vh"
|
||||
width="100%"
|
||||
baseColor={darkMode ? "#2a3a53" : null}
|
||||
highlightColor={darkMode ? "#395073" : null}
|
||||
count={1}
|
||||
className="w-full p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm mt-6"
|
||||
containerClassName="flex w-full"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<table className="md:w-3/4 w-full text-sm text-left text-gray-500 dark:text-gray-400 rounded-lg mt-5">
|
||||
<thead className="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-stone-800 dark:text-gray-400">
|
||||
<tr>
|
||||
<th scope="col" className="px-6 py-3">
|
||||
API Key
|
||||
</th>
|
||||
<th scope="col" className="px-6 py-3">
|
||||
Created By
|
||||
</th>
|
||||
<th scope="col" className="px-6 py-3">
|
||||
Created
|
||||
</th>
|
||||
<th scope="col" className="px-6 py-3 rounded-tr-lg">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{apiKeys.map((apiKey) => (
|
||||
<ApiKeyRow key={apiKey.id} apiKey={apiKey} />
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
@ -30,6 +30,9 @@ export default {
|
||||
exports: () => {
|
||||
return `${API_BASE.replace("/api", "")}/system/data-exports`;
|
||||
},
|
||||
apiDocs: () => {
|
||||
return `${API_BASE}/docs`;
|
||||
},
|
||||
admin: {
|
||||
system: () => {
|
||||
return `/admin/system-preferences`;
|
||||
@ -49,5 +52,8 @@ export default {
|
||||
appearance: () => {
|
||||
return "/admin/appearance";
|
||||
},
|
||||
apiKeys: () => {
|
||||
return "/admin/api-keys";
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -1,3 +1,4 @@
|
||||
const { ApiKey } = require("../models/apiKeys");
|
||||
const { Document } = require("../models/documents");
|
||||
const { Invite } = require("../models/invite");
|
||||
const { SystemSettings } = require("../models/systemSettings");
|
||||
@ -8,8 +9,6 @@ const { WorkspaceChats } = require("../models/workspaceChats");
|
||||
const { getVectorDbClass } = require("../utils/helpers");
|
||||
const { userFromSession, reqBody } = require("../utils/http");
|
||||
const { validatedRequest } = require("../utils/middleware/validatedRequest");
|
||||
const { setupLogoUploads } = require("../utils/files/multer");
|
||||
const { handleLogoUploads } = setupLogoUploads();
|
||||
|
||||
function adminEndpoints(app) {
|
||||
if (!app) return;
|
||||
@ -345,6 +344,72 @@ function adminEndpoints(app) {
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
app.get("/admin/api-keys", [validatedRequest], async (request, response) => {
|
||||
try {
|
||||
const user = await userFromSession(request, response);
|
||||
if (!user || user?.role !== "admin") {
|
||||
response.sendStatus(401).end();
|
||||
return;
|
||||
}
|
||||
|
||||
const apiKeys = await ApiKey.whereWithUser("id IS NOT NULL");
|
||||
return response.status(200).json({
|
||||
apiKeys,
|
||||
error: null,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
response.status(500).json({
|
||||
apiKey: null,
|
||||
error: "Could not find an API Keys.",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
app.post(
|
||||
"/admin/generate-api-key",
|
||||
[validatedRequest],
|
||||
async (request, response) => {
|
||||
try {
|
||||
const user = await userFromSession(request, response);
|
||||
if (!user || user?.role !== "admin") {
|
||||
response.sendStatus(401).end();
|
||||
return;
|
||||
}
|
||||
|
||||
const { apiKey, error } = await ApiKey.create(user.id);
|
||||
return response.status(200).json({
|
||||
apiKey,
|
||||
error,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
response.sendStatus(500).end();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
app.delete(
|
||||
"/admin/delete-api-key/:id",
|
||||
[validatedRequest],
|
||||
async (request, response) => {
|
||||
try {
|
||||
const { id } = request.params;
|
||||
const user = await userFromSession(request, response);
|
||||
if (!user || user?.role !== "admin") {
|
||||
response.sendStatus(401).end();
|
||||
return;
|
||||
}
|
||||
|
||||
await ApiKey.delete(`id = ${id}`);
|
||||
return response.status(200).end();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
response.sendStatus(500).end();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = { adminEndpoints };
|
||||
|
642
server/endpoints/api/admin/index.js
Normal file
642
server/endpoints/api/admin/index.js
Normal file
@ -0,0 +1,642 @@
|
||||
const { Invite } = require("../../../models/invite");
|
||||
const { SystemSettings } = require("../../../models/systemSettings");
|
||||
const { User } = require("../../../models/user");
|
||||
const { Workspace } = require("../../../models/workspace");
|
||||
const { WorkspaceChats } = require("../../../models/workspaceChats");
|
||||
const { multiUserMode, reqBody } = require("../../../utils/http");
|
||||
const { validApiKey } = require("../../../utils/middleware/validApiKey");
|
||||
|
||||
function apiAdminEndpoints(app) {
|
||||
if (!app) return;
|
||||
|
||||
app.get("/v1/admin/is-multi-user-mode", [validApiKey], (_, response) => {
|
||||
/*
|
||||
#swagger.tags = ['Admin']
|
||||
#swagger.description = 'Check to see if the instance is in multi-user-mode first. Methods are disabled until multi user mode is enabled via the UI.'
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: {
|
||||
type: 'object',
|
||||
example: {
|
||||
"isMultiUser": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#swagger.responses[403] = {
|
||||
schema: {
|
||||
"$ref": "#/definitions/InvalidAPIKey"
|
||||
}
|
||||
}
|
||||
*/
|
||||
const isMultiUser = multiUserMode(response);
|
||||
response.status(200).json({ isMultiUser });
|
||||
});
|
||||
|
||||
app.get("/v1/admin/users", [validApiKey], async (request, response) => {
|
||||
/*
|
||||
#swagger.tags = ['Admin']
|
||||
#swagger.description = 'Check to see if the instance is in multi-user-mode first. Methods are disabled until multi user mode is enabled via the UI.'
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: {
|
||||
type: 'object',
|
||||
example: {
|
||||
"users": [
|
||||
{
|
||||
username: "sample-sam",
|
||||
role: 'default',
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#swagger.responses[403] = {
|
||||
schema: {
|
||||
"$ref": "#/definitions/InvalidAPIKey"
|
||||
}
|
||||
}
|
||||
#swagger.responses[401] = {
|
||||
description: "Instance is not in Multi-User mode. Method denied",
|
||||
}
|
||||
*/
|
||||
try {
|
||||
if (!multiUserMode(response)) {
|
||||
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("/v1/admin/users/new", [validApiKey], async (request, response) => {
|
||||
/*
|
||||
#swagger.tags = ['Admin']
|
||||
#swagger.description = 'Create a new user with username and password. Methods are disabled until multi user mode is enabled via the UI.'
|
||||
#swagger.requestBody = {
|
||||
description: 'Key pair object that will define the new user to add to the system.',
|
||||
required: true,
|
||||
type: 'object',
|
||||
content: {
|
||||
"application/json": {
|
||||
example: {
|
||||
username: "sample-sam",
|
||||
password: 'hunter2',
|
||||
role: 'default | admin'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: {
|
||||
type: 'object',
|
||||
example: {
|
||||
user: {
|
||||
id: 1,
|
||||
username: 'sample-sam',
|
||||
role: 'default',
|
||||
},
|
||||
error: null,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#swagger.responses[403] = {
|
||||
schema: {
|
||||
"$ref": "#/definitions/InvalidAPIKey"
|
||||
}
|
||||
}
|
||||
#swagger.responses[401] = {
|
||||
description: "Instance is not in Multi-User mode. Method denied",
|
||||
}
|
||||
*/
|
||||
try {
|
||||
if (!multiUserMode(response)) {
|
||||
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("/v1/admin/users/:id", [validApiKey], async (request, response) => {
|
||||
/*
|
||||
#swagger.tags = ['Admin']
|
||||
#swagger.path = '/v1/admin/users/{id}'
|
||||
#swagger.parameters['id'] = {
|
||||
in: 'path',
|
||||
description: 'id of the user in the database.',
|
||||
required: true,
|
||||
type: 'string'
|
||||
}
|
||||
#swagger.description = 'Update existing user settings. Methods are disabled until multi user mode is enabled via the UI.'
|
||||
#swagger.requestBody = {
|
||||
description: 'Key pair object that will update the found user. All fields are optional and will not update unless specified.',
|
||||
required: true,
|
||||
type: 'object',
|
||||
content: {
|
||||
"application/json": {
|
||||
example: {
|
||||
username: "sample-sam",
|
||||
password: 'hunter2',
|
||||
role: 'default | admin',
|
||||
suspended: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: {
|
||||
type: 'object',
|
||||
example: {
|
||||
success: true,
|
||||
error: null,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#swagger.responses[403] = {
|
||||
schema: {
|
||||
"$ref": "#/definitions/InvalidAPIKey"
|
||||
}
|
||||
}
|
||||
#swagger.responses[401] = {
|
||||
description: "Instance is not in Multi-User mode. Method denied",
|
||||
}
|
||||
*/
|
||||
try {
|
||||
if (!multiUserMode(response)) {
|
||||
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(
|
||||
"/v1/admin/users/:id",
|
||||
[validApiKey],
|
||||
async (request, response) => {
|
||||
/*
|
||||
#swagger.tags = ['Admin']
|
||||
#swagger.description = 'Delete existing user by id. Methods are disabled until multi user mode is enabled via the UI.'
|
||||
#swagger.path = '/v1/admin/users/{id}'
|
||||
#swagger.parameters['id'] = {
|
||||
in: 'path',
|
||||
description: 'id of the user in the database.',
|
||||
required: true,
|
||||
type: 'string'
|
||||
}
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: {
|
||||
type: 'object',
|
||||
example: {
|
||||
success: true,
|
||||
error: null,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#swagger.responses[403] = {
|
||||
schema: {
|
||||
"$ref": "#/definitions/InvalidAPIKey"
|
||||
}
|
||||
}
|
||||
#swagger.responses[401] = {
|
||||
description: "Instance is not in Multi-User mode. Method denied",
|
||||
}
|
||||
*/
|
||||
try {
|
||||
if (!multiUserMode(response)) {
|
||||
response.sendStatus(401).end();
|
||||
return;
|
||||
}
|
||||
|
||||
const { id } = request.params;
|
||||
await User.delete(`id = ${id}`);
|
||||
response.status(200).json({ success: true, error: null });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
response.sendStatus(500).end();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
app.get("/v1/admin/invites", [validApiKey], async (request, response) => {
|
||||
/*
|
||||
#swagger.tags = ['Admin']
|
||||
#swagger.description = 'List all existing invitations to instance regardless of status. Methods are disabled until multi user mode is enabled via the UI.'
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: {
|
||||
type: 'object',
|
||||
example: {
|
||||
"invites": [
|
||||
{
|
||||
id: 1,
|
||||
status: "pending",
|
||||
code: 'abc-123',
|
||||
claimedBy: null
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#swagger.responses[403] = {
|
||||
schema: {
|
||||
"$ref": "#/definitions/InvalidAPIKey"
|
||||
}
|
||||
}
|
||||
#swagger.responses[401] = {
|
||||
description: "Instance is not in Multi-User mode. Method denied",
|
||||
}
|
||||
*/
|
||||
try {
|
||||
if (!multiUserMode(response)) {
|
||||
response.sendStatus(401).end();
|
||||
return;
|
||||
}
|
||||
|
||||
const invites = await Invite.whereWithUsers();
|
||||
response.status(200).json({ invites });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
response.sendStatus(500).end();
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/v1/admin/invite/new", [validApiKey], async (request, response) => {
|
||||
/*
|
||||
#swagger.tags = ['Admin']
|
||||
#swagger.description = 'Create a new invite code for someone to use to register with instance. Methods are disabled until multi user mode is enabled via the UI.'
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: {
|
||||
type: 'object',
|
||||
example: {
|
||||
invite: {
|
||||
id: 1,
|
||||
status: "pending",
|
||||
code: 'abc-123',
|
||||
},
|
||||
error: null,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#swagger.responses[403] = {
|
||||
schema: {
|
||||
"$ref": "#/definitions/InvalidAPIKey"
|
||||
}
|
||||
}
|
||||
#swagger.responses[401] = {
|
||||
description: "Instance is not in Multi-User mode. Method denied",
|
||||
}
|
||||
*/
|
||||
try {
|
||||
if (!multiUserMode(response)) {
|
||||
response.sendStatus(401).end();
|
||||
return;
|
||||
}
|
||||
|
||||
const { invite, error } = await Invite.create();
|
||||
response.status(200).json({ invite, error });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
response.sendStatus(500).end();
|
||||
}
|
||||
});
|
||||
|
||||
app.delete(
|
||||
"/v1/admin/invite/:id",
|
||||
[validApiKey],
|
||||
async (request, response) => {
|
||||
/*
|
||||
#swagger.tags = ['Admin']
|
||||
#swagger.description = 'Deactivates (soft-delete) invite by id. Methods are disabled until multi user mode is enabled via the UI.'
|
||||
#swagger.path = '/v1/admin/invite/{id}'
|
||||
#swagger.parameters['id'] = {
|
||||
in: 'path',
|
||||
description: 'id of the invite in the database.',
|
||||
required: true,
|
||||
type: 'string'
|
||||
}
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: {
|
||||
type: 'object',
|
||||
example: {
|
||||
success: true,
|
||||
error: null,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#swagger.responses[403] = {
|
||||
schema: {
|
||||
"$ref": "#/definitions/InvalidAPIKey"
|
||||
}
|
||||
}
|
||||
#swagger.responses[401] = {
|
||||
description: "Instance is not in Multi-User mode. Method denied",
|
||||
}
|
||||
*/
|
||||
try {
|
||||
if (!multiUserMode(response)) {
|
||||
response.sendStatus(401).end();
|
||||
return;
|
||||
}
|
||||
|
||||
const { id } = request.params;
|
||||
const { success, error } = await Invite.deactivate(id);
|
||||
response.status(200).json({ success, error });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
response.sendStatus(500).end();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
app.post(
|
||||
"/v1/admin/workspaces/:workspaceId/update-users",
|
||||
[validApiKey],
|
||||
async (request, response) => {
|
||||
/*
|
||||
#swagger.tags = ['Admin']
|
||||
#swagger.path = '/v1/admin/workspaces/{workspaceId}/update-users'
|
||||
#swagger.parameters['workspaceId'] = {
|
||||
in: 'path',
|
||||
description: 'id of the workspace in the database.',
|
||||
required: true,
|
||||
type: 'string'
|
||||
}
|
||||
#swagger.description = 'Overwrite workspace permissions to only be accessible by the given user ids and admins. Methods are disabled until multi user mode is enabled via the UI.'
|
||||
#swagger.requestBody = {
|
||||
description: 'Entire array of user ids who can access the workspace. All fields are optional and will not update unless specified.',
|
||||
required: true,
|
||||
type: 'object',
|
||||
content: {
|
||||
"application/json": {
|
||||
example: {
|
||||
userIds: [1,2,4,12],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: {
|
||||
type: 'object',
|
||||
example: {
|
||||
success: true,
|
||||
error: null,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#swagger.responses[403] = {
|
||||
schema: {
|
||||
"$ref": "#/definitions/InvalidAPIKey"
|
||||
}
|
||||
}
|
||||
#swagger.responses[401] = {
|
||||
description: "Instance is not in Multi-User mode. Method denied",
|
||||
}
|
||||
*/
|
||||
try {
|
||||
if (!multiUserMode(response)) {
|
||||
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.post(
|
||||
"/v1/admin/workspace-chats",
|
||||
[validApiKey],
|
||||
async (request, response) => {
|
||||
/*
|
||||
#swagger.tags = ['Admin']
|
||||
#swagger.description = 'All chats in the system ordered by most recent. Methods are disabled until multi user mode is enabled via the UI.'
|
||||
#swagger.requestBody = {
|
||||
description: 'Page offset to show of workspace chats. All fields are optional and will not update unless specified.',
|
||||
required: false,
|
||||
type: 'integer',
|
||||
content: {
|
||||
"application/json": {
|
||||
example: {
|
||||
offset: 2,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: {
|
||||
type: 'object',
|
||||
example: {
|
||||
success: true,
|
||||
error: null,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#swagger.responses[403] = {
|
||||
schema: {
|
||||
"$ref": "#/definitions/InvalidAPIKey"
|
||||
}
|
||||
}
|
||||
#swagger.responses[401] = {
|
||||
description: "Instance is not in Multi-User mode. Method denied",
|
||||
}
|
||||
*/
|
||||
try {
|
||||
if (!multiUserMode(response)) {
|
||||
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.get("/v1/admin/preferences", [validApiKey], async (request, response) => {
|
||||
/*
|
||||
#swagger.tags = ['Admin']
|
||||
#swagger.description = 'Show all multi-user preferences for instance. Methods are disabled until multi user mode is enabled via the UI.'
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: {
|
||||
type: 'object',
|
||||
example: {
|
||||
settings: {
|
||||
users_can_delete_workspaces: true,
|
||||
limit_user_messages: false,
|
||||
message_limit: 10,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#swagger.responses[403] = {
|
||||
schema: {
|
||||
"$ref": "#/definitions/InvalidAPIKey"
|
||||
}
|
||||
}
|
||||
#swagger.responses[401] = {
|
||||
description: "Instance is not in Multi-User mode. Method denied",
|
||||
}
|
||||
*/
|
||||
try {
|
||||
if (!multiUserMode(response)) {
|
||||
response.sendStatus(401).end();
|
||||
return;
|
||||
}
|
||||
|
||||
const settings = {
|
||||
users_can_delete_workspaces:
|
||||
(await SystemSettings.get(`label = 'users_can_delete_workspaces'`))
|
||||
?.value === "true",
|
||||
limit_user_messages:
|
||||
(await SystemSettings.get(`label = 'limit_user_messages'`))?.value ===
|
||||
"true",
|
||||
message_limit:
|
||||
Number(
|
||||
(await SystemSettings.get(`label = 'message_limit'`))?.value
|
||||
) || 10,
|
||||
};
|
||||
response.status(200).json({ settings });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
response.sendStatus(500).end();
|
||||
}
|
||||
});
|
||||
|
||||
app.post(
|
||||
"/v1/admin/preferences",
|
||||
[validApiKey],
|
||||
async (request, response) => {
|
||||
/*
|
||||
#swagger.tags = ['Admin']
|
||||
#swagger.description = 'Update multi-user preferences for instance. Methods are disabled until multi user mode is enabled via the UI.'
|
||||
#swagger.requestBody = {
|
||||
description: 'Object with setting key and new value to set. All keys are optional and will not update unless specified.',
|
||||
required: true,
|
||||
type: 'object',
|
||||
content: {
|
||||
"application/json": {
|
||||
example: {
|
||||
users_can_delete_workspaces: false,
|
||||
limit_user_messages: true,
|
||||
message_limit: 5,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: {
|
||||
type: 'object',
|
||||
example: {
|
||||
success: true,
|
||||
error: null,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#swagger.responses[403] = {
|
||||
schema: {
|
||||
"$ref": "#/definitions/InvalidAPIKey"
|
||||
}
|
||||
}
|
||||
#swagger.responses[401] = {
|
||||
description: "Instance is not in Multi-User mode. Method denied",
|
||||
}
|
||||
*/
|
||||
try {
|
||||
if (!multiUserMode(response)) {
|
||||
response.sendStatus(401).end();
|
||||
return;
|
||||
}
|
||||
|
||||
const updates = reqBody(request);
|
||||
await SystemSettings.updateSettings(updates);
|
||||
response.status(200).json({ success: true, error: null });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
response.sendStatus(500).end();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = { apiAdminEndpoints };
|
33
server/endpoints/api/auth/index.js
Normal file
33
server/endpoints/api/auth/index.js
Normal file
@ -0,0 +1,33 @@
|
||||
const { validApiKey } = require("../../../utils/middleware/validApiKey");
|
||||
|
||||
function apiAuthEndpoints(app) {
|
||||
if (!app) return;
|
||||
|
||||
app.get("/v1/auth", [validApiKey], (_, response) => {
|
||||
/*
|
||||
#swagger.tags = ['Authentication']
|
||||
#swagger.description = 'Verify the attached Authentication header contains a valid API token.'
|
||||
#swagger.responses[200] = {
|
||||
description: 'Valid auth token was found.',
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: {
|
||||
type: 'object',
|
||||
example: {
|
||||
authenticated: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#swagger.responses[403] = {
|
||||
schema: {
|
||||
"$ref": "#/definitions/InvalidAPIKey"
|
||||
}
|
||||
}
|
||||
*/
|
||||
response.status(200).json({ authenticated: true });
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { apiAuthEndpoints };
|
194
server/endpoints/api/document/index.js
Normal file
194
server/endpoints/api/document/index.js
Normal file
@ -0,0 +1,194 @@
|
||||
const { Telemetry } = require("../../../models/telemetry");
|
||||
const { validApiKey } = require("../../../utils/middleware/validApiKey");
|
||||
const { setupMulter } = require("../../../utils/files/multer");
|
||||
const {
|
||||
checkPythonAppAlive,
|
||||
acceptedFileTypes,
|
||||
processDocument,
|
||||
} = require("../../../utils/files/documentProcessor");
|
||||
const { viewLocalFiles } = require("../../../utils/files");
|
||||
const { handleUploads } = setupMulter();
|
||||
|
||||
function apiDocumentEndpoints(app) {
|
||||
if (!app) return;
|
||||
|
||||
app.post(
|
||||
"/v1/document/upload",
|
||||
[validApiKey],
|
||||
handleUploads.single("file"),
|
||||
async (request, response) => {
|
||||
/*
|
||||
#swagger.tags = ['Documents']
|
||||
#swagger.description = 'Upload a new file to AnythingLLM to be parsed and prepared for embedding.'
|
||||
|
||||
#swagger.requestBody = {
|
||||
description: 'File to be uploaded.',
|
||||
required: true,
|
||||
type: 'file',
|
||||
content: {
|
||||
"multipart/form-data": {
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
file: {
|
||||
type: 'string',
|
||||
format: 'binary',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: {
|
||||
type: 'object',
|
||||
example: {
|
||||
success: true,
|
||||
error: null,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#swagger.responses[403] = {
|
||||
schema: {
|
||||
"$ref": "#/definitions/InvalidAPIKey"
|
||||
}
|
||||
}
|
||||
*/
|
||||
try {
|
||||
const { originalname } = request.file;
|
||||
const processingOnline = await checkPythonAppAlive();
|
||||
|
||||
if (!processingOnline) {
|
||||
response
|
||||
.status(500)
|
||||
.json({
|
||||
success: false,
|
||||
error: `Python processing API is not online. Document ${originalname} will not be processed automatically.`,
|
||||
})
|
||||
.end();
|
||||
}
|
||||
|
||||
const { success, reason } = await processDocument(originalname);
|
||||
if (!success) {
|
||||
response.status(500).json({ success: false, error: reason }).end();
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Document ${originalname} uploaded processed and successfully. It is now available in documents.`
|
||||
);
|
||||
await Telemetry.sendTelemetry("document_uploaded");
|
||||
response.status(200).json({ success: true, error: null });
|
||||
} catch (e) {
|
||||
console.log(e.message, e);
|
||||
response.sendStatus(500).end();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
app.get("/v1/documents", [validApiKey], async (_, response) => {
|
||||
/*
|
||||
#swagger.tags = ['Documents']
|
||||
#swagger.description = 'List of all locally-stored documents in instance'
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: {
|
||||
type: 'object',
|
||||
example: {
|
||||
"localFiles": {
|
||||
"name": "documents",
|
||||
"type": "folder",
|
||||
items: [
|
||||
{
|
||||
"name": "my-stored-document.json",
|
||||
"type": "file",
|
||||
"id": "bb07c334-4dab-4419-9462-9d00065a49a1",
|
||||
"url": "file://my-stored-document.txt",
|
||||
"title": "my-stored-document.txt",
|
||||
"cached": false
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#swagger.responses[403] = {
|
||||
schema: {
|
||||
"$ref": "#/definitions/InvalidAPIKey"
|
||||
}
|
||||
}
|
||||
*/
|
||||
try {
|
||||
const localFiles = await viewLocalFiles();
|
||||
response.status(200).json({ localFiles });
|
||||
} catch (e) {
|
||||
console.log(e.message, e);
|
||||
response.sendStatus(500).end();
|
||||
}
|
||||
});
|
||||
|
||||
app.get(
|
||||
"/v1/document/accepted-file-types",
|
||||
[validApiKey],
|
||||
async (_, response) => {
|
||||
/*
|
||||
#swagger.tags = ['Documents']
|
||||
#swagger.description = 'Check available filetypes and MIMEs that can be uploaded.'
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: {
|
||||
type: 'object',
|
||||
example: {
|
||||
"types": {
|
||||
"application/mbox": [
|
||||
".mbox"
|
||||
],
|
||||
"application/pdf": [
|
||||
".pdf"
|
||||
],
|
||||
"application/vnd.oasis.opendocument.text": [
|
||||
".odt"
|
||||
],
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": [
|
||||
".docx"
|
||||
],
|
||||
"text/plain": [
|
||||
".txt",
|
||||
".md"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#swagger.responses[403] = {
|
||||
schema: {
|
||||
"$ref": "#/definitions/InvalidAPIKey"
|
||||
}
|
||||
}
|
||||
*/
|
||||
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();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = { apiDocumentEndpoints };
|
21
server/endpoints/api/index.js
Normal file
21
server/endpoints/api/index.js
Normal file
@ -0,0 +1,21 @@
|
||||
const { useSwagger } = require("../../swagger/utils");
|
||||
const { apiAdminEndpoints } = require("./admin");
|
||||
const { apiAuthEndpoints } = require("./auth");
|
||||
const { apiDocumentEndpoints } = require("./document");
|
||||
const { apiSystemEndpoints } = require("./system");
|
||||
const { apiWorkspaceEndpoints } = require("./workspace");
|
||||
|
||||
// All endpoints must be documented and pass through the validApiKey Middleware.
|
||||
// How to JSDoc an endpoint
|
||||
// https://www.npmjs.com/package/swagger-autogen#openapi-3x
|
||||
function developerEndpoints(app, router) {
|
||||
if (!router) return;
|
||||
useSwagger(app);
|
||||
apiAuthEndpoints(router);
|
||||
apiAdminEndpoints(router);
|
||||
apiSystemEndpoints(router);
|
||||
apiWorkspaceEndpoints(router);
|
||||
apiDocumentEndpoints(router);
|
||||
}
|
||||
|
||||
module.exports = { developerEndpoints };
|
153
server/endpoints/api/system/index.js
Normal file
153
server/endpoints/api/system/index.js
Normal file
@ -0,0 +1,153 @@
|
||||
const { SystemSettings } = require("../../../models/systemSettings");
|
||||
const { getVectorDbClass } = require("../../../utils/helpers");
|
||||
const { dumpENV, updateENV } = require("../../../utils/helpers/updateENV");
|
||||
const { reqBody } = require("../../../utils/http");
|
||||
const { validApiKey } = require("../../../utils/middleware/validApiKey");
|
||||
|
||||
function apiSystemEndpoints(app) {
|
||||
if (!app) return;
|
||||
|
||||
app.get("/v1/system/env-dump", async (_, response) => {
|
||||
/*
|
||||
#swagger.tags = ['System Settings']
|
||||
#swagger.description = 'Dump all settings to file storage'
|
||||
#swagger.responses[403] = {
|
||||
schema: {
|
||||
"$ref": "#/definitions/InvalidAPIKey"
|
||||
}
|
||||
}
|
||||
*/
|
||||
try {
|
||||
if (process.env.NODE_ENV !== "production")
|
||||
return response.sendStatus(200).end();
|
||||
await dumpENV();
|
||||
response.sendStatus(200).end();
|
||||
} catch (e) {
|
||||
console.log(e.message, e);
|
||||
response.sendStatus(500).end();
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/v1/system", [validApiKey], async (_, response) => {
|
||||
/*
|
||||
#swagger.tags = ['System Settings']
|
||||
#swagger.description = 'Get all current system settings that are defined.'
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: {
|
||||
type: 'object',
|
||||
example: {
|
||||
"settings": {
|
||||
"VectorDB": "pinecone",
|
||||
"PineConeEnvironment": "us-west4-gcp-free",
|
||||
"PineConeKey": true,
|
||||
"PineConeIndex": "my-pinecone-index",
|
||||
"LLMProvider": "azure",
|
||||
"[KEY_NAME]": "KEY_VALUE",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#swagger.responses[403] = {
|
||||
schema: {
|
||||
"$ref": "#/definitions/InvalidAPIKey"
|
||||
}
|
||||
}
|
||||
*/
|
||||
try {
|
||||
const settings = await SystemSettings.currentSettings();
|
||||
response.status(200).json({ settings });
|
||||
} catch (e) {
|
||||
console.log(e.message, e);
|
||||
response.sendStatus(500).end();
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/v1/system/vector-count", [validApiKey], async (_, response) => {
|
||||
/*
|
||||
#swagger.tags = ['System Settings']
|
||||
#swagger.description = 'Number of all vectors in connected vector database'
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: {
|
||||
type: 'object',
|
||||
example: {
|
||||
"vectorCount": 5450
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#swagger.responses[403] = {
|
||||
schema: {
|
||||
"$ref": "#/definitions/InvalidAPIKey"
|
||||
}
|
||||
}
|
||||
*/
|
||||
try {
|
||||
const VectorDb = getVectorDbClass();
|
||||
const vectorCount = await VectorDb.totalIndicies();
|
||||
response.status(200).json({ vectorCount });
|
||||
} catch (e) {
|
||||
console.log(e.message, e);
|
||||
response.sendStatus(500).end();
|
||||
}
|
||||
});
|
||||
|
||||
app.post(
|
||||
"/v1/system/update-env",
|
||||
[validApiKey],
|
||||
async (request, response) => {
|
||||
/*
|
||||
#swagger.tags = ['System Settings']
|
||||
#swagger.description = 'Update a system setting or preference.'
|
||||
#swagger.requestBody = {
|
||||
description: 'Key pair object that matches a valid setting and value. Get keys from GET /v1/system or refer to codebase.',
|
||||
required: true,
|
||||
type: 'object',
|
||||
content: {
|
||||
"application/json": {
|
||||
example: {
|
||||
VectorDB: "lancedb",
|
||||
AnotherKey: "updatedValue"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: {
|
||||
type: 'object',
|
||||
example: {
|
||||
newValues: {"[ENV_KEY]": 'Value'},
|
||||
error: 'error goes here, otherwise null'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#swagger.responses[403] = {
|
||||
schema: {
|
||||
"$ref": "#/definitions/InvalidAPIKey"
|
||||
}
|
||||
}
|
||||
*/
|
||||
try {
|
||||
const body = reqBody(request);
|
||||
const { newValues, error } = updateENV(body);
|
||||
if (process.env.NODE_ENV === "production") await dumpENV();
|
||||
response.status(200).json({ newValues, error });
|
||||
} catch (e) {
|
||||
console.log(e.message, e);
|
||||
response.sendStatus(500).end();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = { apiSystemEndpoints };
|
430
server/endpoints/api/workspace/index.js
Normal file
430
server/endpoints/api/workspace/index.js
Normal file
@ -0,0 +1,430 @@
|
||||
const { Document } = require("../../../models/documents");
|
||||
const { Telemetry } = require("../../../models/telemetry");
|
||||
const { DocumentVectors } = require("../../../models/vectors");
|
||||
const { Workspace } = require("../../../models/workspace");
|
||||
const { WorkspaceChats } = require("../../../models/workspaceChats");
|
||||
const { convertToChatHistory } = require("../../../utils/chats");
|
||||
const { getVectorDbClass } = require("../../../utils/helpers");
|
||||
const { multiUserMode, reqBody } = require("../../../utils/http");
|
||||
const { validApiKey } = require("../../../utils/middleware/validApiKey");
|
||||
|
||||
function apiWorkspaceEndpoints(app) {
|
||||
if (!app) return;
|
||||
|
||||
app.post("/v1/workspace/new", [validApiKey], async (request, response) => {
|
||||
/*
|
||||
#swagger.tags = ['Workspaces']
|
||||
#swagger.description = 'Create a new workspace'
|
||||
#swagger.requestBody = {
|
||||
description: 'JSON object containing new display name of workspace.',
|
||||
required: true,
|
||||
type: 'object',
|
||||
content: {
|
||||
"application/json": {
|
||||
example: {
|
||||
name: "My New Workspace",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: {
|
||||
type: 'object',
|
||||
example: {
|
||||
workspace: {
|
||||
"id": 79,
|
||||
"name": "Sample workspace",
|
||||
"slug": "sample-workspace",
|
||||
"createdAt": "2023-08-17 00:45:03",
|
||||
"openAiTemp": null,
|
||||
"lastUpdatedAt": "2023-08-17 00:45:03",
|
||||
"openAiHistory": 20,
|
||||
"openAiPrompt": null
|
||||
},
|
||||
message: 'Workspace created'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#swagger.responses[403] = {
|
||||
schema: {
|
||||
"$ref": "#/definitions/InvalidAPIKey"
|
||||
}
|
||||
}
|
||||
*/
|
||||
try {
|
||||
const { name = null } = reqBody(request);
|
||||
const { workspace, message } = await Workspace.new(name);
|
||||
await Telemetry.sendTelemetry("workspace_created", {
|
||||
multiUserMode: multiUserMode(response),
|
||||
LLMSelection: process.env.LLM_PROVIDER || "openai",
|
||||
VectorDbSelection: process.env.VECTOR_DB || "pinecone",
|
||||
});
|
||||
response.status(200).json({ workspace, message });
|
||||
} catch (e) {
|
||||
console.log(e.message, e);
|
||||
response.sendStatus(500).end();
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/v1/workspaces", [validApiKey], async (request, response) => {
|
||||
/*
|
||||
#swagger.tags = ['Workspaces']
|
||||
#swagger.description = 'List all current workspaces'
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: {
|
||||
type: 'object',
|
||||
example: {
|
||||
workspaces: [
|
||||
{
|
||||
"id": 79,
|
||||
"name": "Sample workspace",
|
||||
"slug": "sample-workspace",
|
||||
"createdAt": "2023-08-17 00:45:03",
|
||||
"openAiTemp": null,
|
||||
"lastUpdatedAt": "2023-08-17 00:45:03",
|
||||
"openAiHistory": 20,
|
||||
"openAiPrompt": null
|
||||
}
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#swagger.responses[403] = {
|
||||
schema: {
|
||||
"$ref": "#/definitions/InvalidAPIKey"
|
||||
}
|
||||
}
|
||||
*/
|
||||
try {
|
||||
const workspaces = await Workspace.where();
|
||||
response.status(200).json({ workspaces });
|
||||
} catch (e) {
|
||||
console.log(e.message, e);
|
||||
response.sendStatus(500).end();
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/v1/workspace/:slug", [validApiKey], async (request, response) => {
|
||||
/*
|
||||
#swagger.tags = ['Workspaces']
|
||||
#swagger.description = 'Get a workspace by its unique slug.'
|
||||
#swagger.path = '/v1/workspace/{slug}'
|
||||
#swagger.parameters['slug'] = {
|
||||
in: 'path',
|
||||
description: 'Unique slug of workspace to find',
|
||||
required: true,
|
||||
type: 'string'
|
||||
}
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: {
|
||||
type: 'object',
|
||||
example: {
|
||||
workspace: {
|
||||
"id": 79,
|
||||
"name": "My workspace",
|
||||
"slug": "my-workspace-123",
|
||||
"createdAt": "2023-08-17 00:45:03",
|
||||
"openAiTemp": null,
|
||||
"lastUpdatedAt": "2023-08-17 00:45:03",
|
||||
"openAiHistory": 20,
|
||||
"openAiPrompt": null,
|
||||
"documents": []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#swagger.responses[403] = {
|
||||
schema: {
|
||||
"$ref": "#/definitions/InvalidAPIKey"
|
||||
}
|
||||
}
|
||||
*/
|
||||
try {
|
||||
const { slug } = request.params;
|
||||
const workspace = await Workspace.get(`slug = '${slug}'`);
|
||||
response.status(200).json({ workspace });
|
||||
} catch (e) {
|
||||
console.log(e.message, e);
|
||||
response.sendStatus(500).end();
|
||||
}
|
||||
});
|
||||
|
||||
app.delete(
|
||||
"/v1/workspace/:slug",
|
||||
[validApiKey],
|
||||
async (request, response) => {
|
||||
/*
|
||||
#swagger.tags = ['Workspaces']
|
||||
#swagger.description = 'Deletes a workspace by its slug.'
|
||||
#swagger.path = '/v1/workspace/{slug}'
|
||||
#swagger.parameters['slug'] = {
|
||||
in: 'path',
|
||||
description: 'Unique slug of workspace to delete',
|
||||
required: true,
|
||||
type: 'string'
|
||||
}
|
||||
#swagger.responses[403] = {
|
||||
schema: {
|
||||
"$ref": "#/definitions/InvalidAPIKey"
|
||||
}
|
||||
}
|
||||
*/
|
||||
try {
|
||||
const { slug = "" } = request.params;
|
||||
const VectorDb = getVectorDbClass();
|
||||
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)}`);
|
||||
try {
|
||||
await VectorDb["delete-namespace"]({ namespace: slug });
|
||||
} catch (e) {
|
||||
console.error(e.message);
|
||||
}
|
||||
response.sendStatus(200).end();
|
||||
} catch (e) {
|
||||
console.log(e.message, e);
|
||||
response.sendStatus(500).end();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
app.post(
|
||||
"/v1/workspace/:slug/update",
|
||||
[validApiKey],
|
||||
async (request, response) => {
|
||||
/*
|
||||
#swagger.tags = ['Workspaces']
|
||||
#swagger.description = 'Update workspace settings by its unique slug.'
|
||||
#swagger.path = '/v1/workspace/{slug}/update'
|
||||
#swagger.parameters['slug'] = {
|
||||
in: 'path',
|
||||
description: 'Unique slug of workspace to find',
|
||||
required: true,
|
||||
type: 'string'
|
||||
}
|
||||
#swagger.requestBody = {
|
||||
description: 'JSON object containing new settings to update a workspace. All keys are optional and will not update unless provided',
|
||||
required: true,
|
||||
type: 'object',
|
||||
content: {
|
||||
"application/json": {
|
||||
example: {
|
||||
"name": 'Updated Workspace Name',
|
||||
"openAiTemp": 0.2,
|
||||
"openAiHistory": 20,
|
||||
"openAiPrompt": "Respond to all inquires and questions in binary - do not respond in any other format."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: {
|
||||
type: 'object',
|
||||
example: {
|
||||
workspace: {
|
||||
"id": 79,
|
||||
"name": "My workspace",
|
||||
"slug": "my-workspace-123",
|
||||
"createdAt": "2023-08-17 00:45:03",
|
||||
"openAiTemp": null,
|
||||
"lastUpdatedAt": "2023-08-17 00:45:03",
|
||||
"openAiHistory": 20,
|
||||
"openAiPrompt": null,
|
||||
"documents": []
|
||||
},
|
||||
message: null,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#swagger.responses[403] = {
|
||||
schema: {
|
||||
"$ref": "#/definitions/InvalidAPIKey"
|
||||
}
|
||||
}
|
||||
*/
|
||||
try {
|
||||
const { slug = null } = request.params;
|
||||
const data = reqBody(request);
|
||||
const currWorkspace = 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();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
app.get(
|
||||
"/v1/workspace/:slug/chats",
|
||||
[validApiKey],
|
||||
async (request, response) => {
|
||||
/*
|
||||
#swagger.tags = ['Workspaces']
|
||||
#swagger.description = 'Get a workspaces chats regardless of user by its unique slug.'
|
||||
#swagger.path = '/v1/workspace/{slug}/chats'
|
||||
#swagger.parameters['slug'] = {
|
||||
in: 'path',
|
||||
description: 'Unique slug of workspace to find',
|
||||
required: true,
|
||||
type: 'string'
|
||||
}
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: {
|
||||
type: 'object',
|
||||
example: {
|
||||
history: [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "What is AnythingLLM?",
|
||||
"sentAt": 1692851630
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "AnythingLLM is a platform that allows you to convert notes, PDFs, and other source materials into a chatbot. It ensures privacy, cites its answers, and allows multiple people to interact with the same documents simultaneously. It is particularly useful for businesses to enhance the visibility and readability of various written communications such as SOPs, contracts, and sales calls. You can try it out with a free trial to see if it meets your business needs.",
|
||||
"sources": [{"source": "object about source document and snippets used"}]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#swagger.responses[403] = {
|
||||
schema: {
|
||||
"$ref": "#/definitions/InvalidAPIKey"
|
||||
}
|
||||
}
|
||||
*/
|
||||
try {
|
||||
const { slug } = request.params;
|
||||
const workspace = await Workspace.get(`slug = '${slug}'`);
|
||||
|
||||
if (!workspace) {
|
||||
response.sendStatus(400).end();
|
||||
return;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
app.post(
|
||||
"/v1/workspace/:slug/update-embeddings",
|
||||
[validApiKey],
|
||||
async (request, response) => {
|
||||
/*
|
||||
#swagger.tags = ['Workspaces']
|
||||
#swagger.description = 'Add or remove documents from a workspace by its unique slug.'
|
||||
#swagger.path = '/v1/workspace/{slug}/update-embeddings'
|
||||
#swagger.parameters['slug'] = {
|
||||
in: 'path',
|
||||
description: 'Unique slug of workspace to find',
|
||||
required: true,
|
||||
type: 'string'
|
||||
}
|
||||
#swagger.requestBody = {
|
||||
description: 'JSON object of additions and removals of documents to add to update a workspace. The value should be the folder + filename with the exclusions of the top-level documents path.',
|
||||
required: true,
|
||||
type: 'object',
|
||||
content: {
|
||||
"application/json": {
|
||||
example: {
|
||||
adds: [],
|
||||
deletes: ["custom-documents/anythingllm-hash.json"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: {
|
||||
type: 'object',
|
||||
example: {
|
||||
workspace: {
|
||||
"id": 79,
|
||||
"name": "My workspace",
|
||||
"slug": "my-workspace-123",
|
||||
"createdAt": "2023-08-17 00:45:03",
|
||||
"openAiTemp": null,
|
||||
"lastUpdatedAt": "2023-08-17 00:45:03",
|
||||
"openAiHistory": 20,
|
||||
"openAiPrompt": null,
|
||||
"documents": []
|
||||
},
|
||||
message: null,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#swagger.responses[403] = {
|
||||
schema: {
|
||||
"$ref": "#/definitions/InvalidAPIKey"
|
||||
}
|
||||
}
|
||||
*/
|
||||
try {
|
||||
const { slug = null } = request.params;
|
||||
const { adds = [], deletes = [] } = reqBody(request);
|
||||
const currWorkspace = await Workspace.get(`slug = '${slug}'`);
|
||||
|
||||
if (!currWorkspace) {
|
||||
response.sendStatus(400).end();
|
||||
return;
|
||||
}
|
||||
|
||||
await Document.removeDocuments(currWorkspace, deletes);
|
||||
await Document.addDocuments(currWorkspace, adds);
|
||||
const updatedWorkspace = await Workspace.get(`slug = '${slug}'`);
|
||||
response.status(200).json({ workspace: updatedWorkspace });
|
||||
} catch (e) {
|
||||
console.log(e.message, e);
|
||||
response.sendStatus(500).end();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = { apiWorkspaceEndpoints };
|
@ -36,6 +36,7 @@ const {
|
||||
} = require("../utils/files/logo");
|
||||
const { Telemetry } = require("../models/telemetry");
|
||||
const { WelcomeMessages } = require("../models/welcomeMessages");
|
||||
const { ApiKey } = require("../models/apiKeys");
|
||||
|
||||
function systemEndpoints(app) {
|
||||
if (!app) return;
|
||||
@ -58,57 +59,7 @@ function systemEndpoints(app) {
|
||||
|
||||
app.get("/setup-complete", async (_, response) => {
|
||||
try {
|
||||
const llmProvider = process.env.LLM_PROVIDER || "openai";
|
||||
const vectorDB = process.env.VECTOR_DB || "pinecone";
|
||||
const results = {
|
||||
CanDebug: !!!process.env.NO_DEBUG,
|
||||
RequiresAuth: !!process.env.AUTH_TOKEN,
|
||||
AuthToken: !!process.env.AUTH_TOKEN,
|
||||
JWTSecret: !!process.env.JWT_SECRET,
|
||||
StorageDir: process.env.STORAGE_DIR,
|
||||
MultiUserMode: await SystemSettings.isMultiUserMode(),
|
||||
VectorDB: vectorDB,
|
||||
...(vectorDB === "pinecone"
|
||||
? {
|
||||
PineConeEnvironment: process.env.PINECONE_ENVIRONMENT,
|
||||
PineConeKey: !!process.env.PINECONE_API_KEY,
|
||||
PineConeIndex: process.env.PINECONE_INDEX,
|
||||
}
|
||||
: {}),
|
||||
...(vectorDB === "chroma"
|
||||
? {
|
||||
ChromaEndpoint: process.env.CHROMA_ENDPOINT,
|
||||
}
|
||||
: {}),
|
||||
...(vectorDB === "weaviate"
|
||||
? {
|
||||
WeaviateEndpoint: process.env.WEAVIATE_ENDPOINT,
|
||||
WeaviateApiKey: process.env.WEAVIATE_API_KEY,
|
||||
}
|
||||
: {}),
|
||||
...(vectorDB === "qdrant"
|
||||
? {
|
||||
QdrantEndpoint: process.env.QDRANT_ENDPOINT,
|
||||
QdrantApiKey: process.env.QDRANT_API_KEY,
|
||||
}
|
||||
: {}),
|
||||
LLMProvider: llmProvider,
|
||||
...(llmProvider === "openai"
|
||||
? {
|
||||
OpenAiKey: !!process.env.OPEN_AI_KEY,
|
||||
OpenAiModelPref: process.env.OPEN_MODEL_PREF || "gpt-3.5-turbo",
|
||||
}
|
||||
: {}),
|
||||
|
||||
...(llmProvider === "azure"
|
||||
? {
|
||||
AzureOpenAiEndpoint: process.env.AZURE_OPENAI_ENDPOINT,
|
||||
AzureOpenAiKey: !!process.env.AZURE_OPENAI_KEY,
|
||||
AzureOpenAiModelPref: process.env.OPEN_MODEL_PREF,
|
||||
AzureOpenAiEmbeddingModelPref: process.env.EMBEDDING_MODEL_PREF,
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
const results = await SystemSettings.currentSettings();
|
||||
response.status(200).json({ results });
|
||||
} catch (e) {
|
||||
console.log(e.message, e);
|
||||
@ -526,6 +477,65 @@ function systemEndpoints(app) {
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
app.get("/system/api-key", [validatedRequest], async (_, response) => {
|
||||
try {
|
||||
if (response.locals.multiUserMode) {
|
||||
return response.sendStatus(401).end();
|
||||
}
|
||||
|
||||
const apiKey = await ApiKey.get("id IS NOT NULL");
|
||||
return response.status(200).json({
|
||||
apiKey,
|
||||
error: null,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
response.status(500).json({
|
||||
apiKey: null,
|
||||
error: "Could not find an API Key.",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
app.post(
|
||||
"/system/generate-api-key",
|
||||
[validatedRequest],
|
||||
async (_, response) => {
|
||||
try {
|
||||
if (response.locals.multiUserMode) {
|
||||
return response.sendStatus(401).end();
|
||||
}
|
||||
|
||||
await ApiKey.delete();
|
||||
const { apiKey, error } = await ApiKey.create();
|
||||
return response.status(200).json({
|
||||
apiKey,
|
||||
error,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
response.status(500).json({
|
||||
apiKey: null,
|
||||
error: "Error generating api key.",
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
app.delete("/system/api-key", [validatedRequest], async (_, response) => {
|
||||
try {
|
||||
if (response.locals.multiUserMode) {
|
||||
return response.sendStatus(401).end();
|
||||
}
|
||||
|
||||
await ApiKey.delete();
|
||||
return response.status(200).end();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
response.status(500).end();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { systemEndpoints };
|
||||
|
@ -17,6 +17,7 @@ const { adminEndpoints } = require("./endpoints/admin");
|
||||
const { inviteEndpoints } = require("./endpoints/invite");
|
||||
const { utilEndpoints } = require("./endpoints/utils");
|
||||
const { Telemetry } = require("./models/telemetry");
|
||||
const { developerEndpoints } = require("./endpoints/api");
|
||||
const app = express();
|
||||
const apiRouter = express.Router();
|
||||
const FILE_LIMIT = "3GB";
|
||||
@ -38,6 +39,7 @@ chatEndpoints(apiRouter);
|
||||
adminEndpoints(apiRouter);
|
||||
inviteEndpoints(apiRouter);
|
||||
utilEndpoints(apiRouter);
|
||||
developerEndpoints(app, apiRouter);
|
||||
|
||||
apiRouter.post("/v/:command", async (request, response) => {
|
||||
try {
|
||||
|
133
server/models/apiKeys.js
Normal file
133
server/models/apiKeys.js
Normal file
@ -0,0 +1,133 @@
|
||||
const { Telemetry } = require("./telemetry");
|
||||
|
||||
const ApiKey = {
|
||||
tablename: "api_keys",
|
||||
writable: [],
|
||||
colsInit: `
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
secret TEXT UNIQUE,
|
||||
createdBy INTEGER DEFAULT NULL,
|
||||
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 ApiKey migrations`);
|
||||
const db = await this.db(false);
|
||||
await checkForMigrations(this, db);
|
||||
},
|
||||
migrations: function () {
|
||||
return [];
|
||||
},
|
||||
makeSecret: () => {
|
||||
const uuidAPIKey = require("uuid-apikey");
|
||||
return uuidAPIKey.create().apiKey;
|
||||
},
|
||||
db: async function (tracing = true) {
|
||||
const sqlite3 = require("sqlite3").verbose();
|
||||
const { open } = require("sqlite");
|
||||
|
||||
const db = await open({
|
||||
filename: `${
|
||||
!!process.env.STORAGE_DIR ? `${process.env.STORAGE_DIR}/` : "storage/"
|
||||
}anythingllm.db`,
|
||||
driver: sqlite3.Database,
|
||||
});
|
||||
|
||||
await db.exec(
|
||||
`PRAGMA foreign_keys = ON;CREATE TABLE IF NOT EXISTS ${this.tablename} (${this.colsInit})`
|
||||
);
|
||||
|
||||
if (tracing) db.on("trace", (sql) => console.log(sql));
|
||||
return db;
|
||||
},
|
||||
create: async function (createdByUserId = null) {
|
||||
const db = await this.db();
|
||||
const { id, success, message } = await db
|
||||
.run(`INSERT INTO ${this.tablename} (secret, createdBy) VALUES(?, ?)`, [
|
||||
this.makeSecret(),
|
||||
createdByUserId,
|
||||
])
|
||||
.then((res) => {
|
||||
return { id: res.lastID, success: true, message: null };
|
||||
})
|
||||
.catch((error) => {
|
||||
return { id: null, success: false, message: error.message };
|
||||
});
|
||||
|
||||
if (!success) {
|
||||
db.close();
|
||||
console.error("FAILED TO CREATE API KEY.", message);
|
||||
return { apiKey: null, error: message };
|
||||
}
|
||||
|
||||
const apiKey = await db.get(
|
||||
`SELECT * FROM ${this.tablename} WHERE id = ${id} `
|
||||
);
|
||||
db.close();
|
||||
await Telemetry.sendTelemetry("api_key_created");
|
||||
return { apiKey, error: null };
|
||||
},
|
||||
get: async function (clause = "") {
|
||||
const db = await this.db();
|
||||
const result = await db
|
||||
.get(
|
||||
`SELECT * FROM ${this.tablename} ${clause ? `WHERE ${clause}` : clause}`
|
||||
)
|
||||
.then((res) => res || null);
|
||||
if (!result) return null;
|
||||
db.close();
|
||||
return { ...result };
|
||||
},
|
||||
count: async function (clause = null) {
|
||||
const db = await this.db();
|
||||
const { count } = await db.get(
|
||||
`SELECT COUNT(*) as count FROM ${this.tablename} ${
|
||||
clause ? `WHERE ${clause}` : ""
|
||||
} `
|
||||
);
|
||||
db.close();
|
||||
|
||||
return count;
|
||||
},
|
||||
delete: async function (clause = "") {
|
||||
const db = await this.db();
|
||||
await db.get(
|
||||
`DELETE FROM ${this.tablename} ${clause ? `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;
|
||||
},
|
||||
whereWithUser: async function (clause = "", limit = null) {
|
||||
const { User } = require("./user");
|
||||
const apiKeys = await this.where(clause, limit);
|
||||
|
||||
for (const apiKey of apiKeys) {
|
||||
if (!apiKey.createdBy) continue;
|
||||
const user = await User.get(`id = ${apiKey.createdBy}`);
|
||||
if (!user) continue;
|
||||
|
||||
apiKey.createdBy = {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
role: user.role,
|
||||
};
|
||||
}
|
||||
|
||||
return apiKeys;
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = { ApiKey };
|
@ -1,3 +1,7 @@
|
||||
process.env.NODE_ENV === "development"
|
||||
? require("dotenv").config({ path: `.env.${process.env.NODE_ENV}` })
|
||||
: require("dotenv").config();
|
||||
|
||||
const SystemSettings = {
|
||||
supportedFields: [
|
||||
"multi_user_mode",
|
||||
@ -45,6 +49,59 @@ const SystemSettings = {
|
||||
if (tracing) db.on("trace", (sql) => console.log(sql));
|
||||
return db;
|
||||
},
|
||||
currentSettings: async function () {
|
||||
const llmProvider = process.env.LLM_PROVIDER || "openai";
|
||||
const vectorDB = process.env.VECTOR_DB || "pinecone";
|
||||
return {
|
||||
CanDebug: !!!process.env.NO_DEBUG,
|
||||
RequiresAuth: !!process.env.AUTH_TOKEN,
|
||||
AuthToken: !!process.env.AUTH_TOKEN,
|
||||
JWTSecret: !!process.env.JWT_SECRET,
|
||||
StorageDir: process.env.STORAGE_DIR,
|
||||
MultiUserMode: await this.isMultiUserMode(),
|
||||
VectorDB: vectorDB,
|
||||
...(vectorDB === "pinecone"
|
||||
? {
|
||||
PineConeEnvironment: process.env.PINECONE_ENVIRONMENT,
|
||||
PineConeKey: !!process.env.PINECONE_API_KEY,
|
||||
PineConeIndex: process.env.PINECONE_INDEX,
|
||||
}
|
||||
: {}),
|
||||
...(vectorDB === "chroma"
|
||||
? {
|
||||
ChromaEndpoint: process.env.CHROMA_ENDPOINT,
|
||||
}
|
||||
: {}),
|
||||
...(vectorDB === "weaviate"
|
||||
? {
|
||||
WeaviateEndpoint: process.env.WEAVIATE_ENDPOINT,
|
||||
WeaviateApiKey: process.env.WEAVIATE_API_KEY,
|
||||
}
|
||||
: {}),
|
||||
...(vectorDB === "qdrant"
|
||||
? {
|
||||
QdrantEndpoint: process.env.QDRANT_ENDPOINT,
|
||||
QdrantApiKey: process.env.QDRANT_API_KEY,
|
||||
}
|
||||
: {}),
|
||||
LLMProvider: llmProvider,
|
||||
...(llmProvider === "openai"
|
||||
? {
|
||||
OpenAiKey: !!process.env.OPEN_AI_KEY,
|
||||
OpenAiModelPref: process.env.OPEN_MODEL_PREF || "gpt-3.5-turbo",
|
||||
}
|
||||
: {}),
|
||||
|
||||
...(llmProvider === "azure"
|
||||
? {
|
||||
AzureOpenAiEndpoint: process.env.AZURE_OPENAI_ENDPOINT,
|
||||
AzureOpenAiKey: !!process.env.AZURE_OPENAI_KEY,
|
||||
AzureOpenAiModelPref: process.env.OPEN_MODEL_PREF,
|
||||
AzureOpenAiEmbeddingModelPref: process.env.EMBEDDING_MODEL_PREF,
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
},
|
||||
get: async function (clause = "") {
|
||||
const db = await this.db();
|
||||
const result = await db
|
||||
|
6
server/nodemon.json
Normal file
6
server/nodemon.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"events": {
|
||||
"start": "yarn swagger",
|
||||
"restart": "yarn swagger"
|
||||
}
|
||||
}
|
@ -10,9 +10,10 @@
|
||||
"node": ">=18.12.1"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "NODE_ENV=development nodemon --ignore documents --ignore vector-cache --ignore storage --trace-warnings index.js",
|
||||
"dev": "NODE_ENV=development nodemon --ignore documents --ignore vector-cache --ignore storage --ignore swagger --trace-warnings index.js",
|
||||
"start": "NODE_ENV=production node index.js",
|
||||
"lint": "yarn prettier --write ./endpoints ./models ./utils index.js"
|
||||
"lint": "yarn prettier --write ./endpoints ./models ./utils index.js",
|
||||
"swagger": "node ./swagger/init.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@azure/openai": "^1.0.0-beta.3",
|
||||
@ -41,6 +42,8 @@
|
||||
"slugify": "^1.6.6",
|
||||
"sqlite": "^4.2.1",
|
||||
"sqlite3": "^5.1.6",
|
||||
"swagger-autogen": "^2.23.5",
|
||||
"swagger-ui-express": "^5.0.0",
|
||||
"uuid": "^9.0.0",
|
||||
"uuid-apikey": "^1.5.3",
|
||||
"vectordb": "0.1.12",
|
||||
@ -50,4 +53,4 @@
|
||||
"nodemon": "^2.0.22",
|
||||
"prettier": "^2.4.1"
|
||||
}
|
||||
}
|
||||
}
|
1722
server/swagger/dark-swagger.css
Normal file
1722
server/swagger/dark-swagger.css
Normal file
File diff suppressed because it is too large
Load Diff
3
server/swagger/index.css
Normal file
3
server/swagger/index.css
Normal file
@ -0,0 +1,3 @@
|
||||
.schemes.wrapper>div:first-of-type {
|
||||
display: none;
|
||||
}
|
28
server/swagger/index.js
Normal file
28
server/swagger/index.js
Normal file
@ -0,0 +1,28 @@
|
||||
function waitForElm(selector) {
|
||||
return new Promise(resolve => {
|
||||
if (document.querySelector(selector)) {
|
||||
return resolve(document.querySelector(selector));
|
||||
}
|
||||
|
||||
const observer = new MutationObserver(mutations => {
|
||||
if (document.querySelector(selector)) {
|
||||
resolve(document.querySelector(selector));
|
||||
observer.disconnect();
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Force change the Swagger logo in the header
|
||||
waitForElm('img[alt="Swagger UI"]').then((elm) => {
|
||||
if (window.SWAGGER_DOCS_ENV === 'development') {
|
||||
elm.src = 'http://localhost:3000/public/anything-llm-light.png'
|
||||
} else {
|
||||
elm.src = `${window.location.origin}/anything-llm-light.png`
|
||||
}
|
||||
});
|
37
server/swagger/init.js
Normal file
37
server/swagger/init.js
Normal file
@ -0,0 +1,37 @@
|
||||
const swaggerAutogen = require('swagger-autogen')({ openapi: '3.0.0' });
|
||||
|
||||
const doc = {
|
||||
info: {
|
||||
version: '1.0.0',
|
||||
title: 'AnythingLLM Developer API',
|
||||
description: 'API endpoints that enable programmatic reading, writing, and updating of your AnythingLLM instance. UI supplied by Swagger.io.',
|
||||
},
|
||||
host: '/api',
|
||||
schemes: ['http'],
|
||||
securityDefinitions: {
|
||||
BearerAuth: {
|
||||
type: 'http',
|
||||
scheme: 'bearer',
|
||||
bearerFormat: 'JWT'
|
||||
}
|
||||
},
|
||||
security: [
|
||||
{ BearerAuth: [] }
|
||||
],
|
||||
definitions: {
|
||||
InvalidAPIKey: {
|
||||
message: 'Invalid API Key',
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
const outputFile = './openapi.json';
|
||||
const endpointsFiles = [
|
||||
'../endpoints/api/auth/index.js',
|
||||
'../endpoints/api/admin/index.js',
|
||||
'../endpoints/api/document/index.js',
|
||||
'../endpoints/api/workspace/index.js',
|
||||
'../endpoints/api/system/index.js',
|
||||
];
|
||||
|
||||
swaggerAutogen(outputFile, endpointsFiles, doc)
|
1767
server/swagger/openapi.json
Normal file
1767
server/swagger/openapi.json
Normal file
File diff suppressed because it is too large
Load Diff
52
server/swagger/utils.js
Normal file
52
server/swagger/utils.js
Normal file
@ -0,0 +1,52 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const swaggerUi = require('swagger-ui-express');
|
||||
|
||||
function faviconUrl() {
|
||||
return process.env.NODE_ENV === "production" ?
|
||||
'/public/favicon.png' :
|
||||
'http://localhost:3000/public/favicon.png'
|
||||
}
|
||||
|
||||
function useSwagger(app) {
|
||||
app.use('/api/docs', swaggerUi.serve);
|
||||
const options = {
|
||||
customCss: [
|
||||
fs.readFileSync(path.resolve(__dirname, 'index.css')),
|
||||
fs.readFileSync(path.resolve(__dirname, 'dark-swagger.css'))
|
||||
].join('\n\n\n'),
|
||||
customSiteTitle: 'AnythingLLM Developer API Documentation',
|
||||
customfavIcon: faviconUrl(),
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
const swaggerDocument = require('./openapi.json');
|
||||
app.get('/api/docs', swaggerUi.setup(
|
||||
swaggerDocument,
|
||||
{
|
||||
...options,
|
||||
customJsStr: 'window.SWAGGER_DOCS_ENV = "production";\n\n' + fs.readFileSync(path.resolve(__dirname, 'index.js'), 'utf8'),
|
||||
},
|
||||
));
|
||||
} else {
|
||||
// we regenerate the html page only in development mode to ensure it is up-to-date when the code is hot-reloaded.
|
||||
app.get(
|
||||
"/api/docs",
|
||||
async (_, response) => {
|
||||
// #swagger.ignore = true
|
||||
const swaggerDocument = require('./openapi.json');
|
||||
return response.send(
|
||||
swaggerUi.generateHTML(
|
||||
swaggerDocument,
|
||||
{
|
||||
...options,
|
||||
customJsStr: 'window.SWAGGER_DOCS_ENV = "development";\n\n' + fs.readFileSync(path.resolve(__dirname, 'index.js'), 'utf8'),
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { faviconUrl, useSwagger }
|
@ -62,6 +62,7 @@ async function validateTablePragmas(force = false) {
|
||||
const { WorkspaceChats } = require("../../models/workspaceChats");
|
||||
const { Invite } = require("../../models/invite");
|
||||
const { WelcomeMessages } = require("../../models/welcomeMessages");
|
||||
const { ApiKey } = require("../../models/apiKeys");
|
||||
|
||||
await SystemSettings.migrateTable();
|
||||
await User.migrateTable();
|
||||
@ -72,6 +73,7 @@ async function validateTablePragmas(force = false) {
|
||||
await WorkspaceChats.migrateTable();
|
||||
await Invite.migrateTable();
|
||||
await WelcomeMessages.migrateTable();
|
||||
await ApiKey.migrateTable();
|
||||
} catch (e) {
|
||||
console.error(`validateTablePragmas: Migrations failed`, e);
|
||||
}
|
||||
|
30
server/utils/middleware/validApiKey.js
Normal file
30
server/utils/middleware/validApiKey.js
Normal file
@ -0,0 +1,30 @@
|
||||
const { ApiKey } = require("../../models/apiKeys");
|
||||
const { SystemSettings } = require("../../models/systemSettings");
|
||||
|
||||
async function validApiKey(request, response, next) {
|
||||
const multiUserMode = await SystemSettings.isMultiUserMode();
|
||||
response.locals.multiUserMode = multiUserMode;
|
||||
|
||||
const auth = request.header("Authorization");
|
||||
const bearerKey = auth ? auth.split(" ")[1] : null;
|
||||
if (!bearerKey) {
|
||||
response.status(403).json({
|
||||
error: "No valid api key found.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const apiKey = await ApiKey.get(`secret = '${bearerKey}'`);
|
||||
if (!apiKey) {
|
||||
response.status(403).json({
|
||||
error: "No valid api key found.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
validApiKey,
|
||||
};
|
@ -252,6 +252,11 @@ accepts@~1.3.4, accepts@~1.3.8:
|
||||
mime-types "~2.1.34"
|
||||
negotiator "0.6.3"
|
||||
|
||||
acorn@^7.4.1:
|
||||
version "7.4.1"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
|
||||
integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
|
||||
|
||||
agent-base@6, agent-base@^6.0.2:
|
||||
version "6.0.2"
|
||||
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
|
||||
@ -800,6 +805,11 @@ deep-extend@~0.6.0:
|
||||
resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac"
|
||||
integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==
|
||||
|
||||
deepmerge@^4.2.2:
|
||||
version "4.3.1"
|
||||
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a"
|
||||
integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==
|
||||
|
||||
delayed-stream@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
|
||||
@ -1149,7 +1159,7 @@ glob-parent@~5.1.2:
|
||||
dependencies:
|
||||
is-glob "^4.0.1"
|
||||
|
||||
glob@^7.1.3, glob@^7.1.4:
|
||||
glob@^7.1.3, glob@^7.1.4, glob@^7.1.7:
|
||||
version "7.2.3"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
|
||||
integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==
|
||||
@ -1458,6 +1468,11 @@ json-bignum@^0.0.3:
|
||||
resolved "https://registry.yarnpkg.com/json-bignum/-/json-bignum-0.0.3.tgz#41163b50436c773d82424dbc20ed70db7604b8d7"
|
||||
integrity sha512-2WHyXj3OfHSgNyuzDbSxI1w2jgw5gkWSWhS7Qg4bWXx1nLk3jnbwfUeS0PSba3IzpTUWdHxBieELUzXRjQB2zg==
|
||||
|
||||
json5@^2.2.3:
|
||||
version "2.2.3"
|
||||
resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283"
|
||||
integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==
|
||||
|
||||
jsonpointer@^5.0.1:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-5.0.1.tgz#2110e0af0900fd37467b5907ecd13a7884a1b559"
|
||||
@ -2429,6 +2444,28 @@ supports-color@^5.3.0, supports-color@^5.5.0:
|
||||
dependencies:
|
||||
has-flag "^3.0.0"
|
||||
|
||||
swagger-autogen@^2.23.5:
|
||||
version "2.23.5"
|
||||
resolved "https://registry.yarnpkg.com/swagger-autogen/-/swagger-autogen-2.23.5.tgz#fe86bde66daf991a2e9064ec83f2136319d19258"
|
||||
integrity sha512-4Tl2+XhZMyHoBYkABnScHtQE0lKPKUD3NBt09mClrI6UKOUYljKlYw1xiFVwsHCTGR2hAXmhT4PpgjruCtt1ZA==
|
||||
dependencies:
|
||||
acorn "^7.4.1"
|
||||
deepmerge "^4.2.2"
|
||||
glob "^7.1.7"
|
||||
json5 "^2.2.3"
|
||||
|
||||
swagger-ui-dist@>=5.0.0:
|
||||
version "5.4.2"
|
||||
resolved "https://registry.yarnpkg.com/swagger-ui-dist/-/swagger-ui-dist-5.4.2.tgz#ff7b936bdfc84673a1823a0f05f3a933ba7ccd4c"
|
||||
integrity sha512-vT5QxP/NOr9m4gLZl+SpavWI3M9Fdh30+Sdw9rEtZbkqNmNNEPhjXas2xTD9rsJYYdLzAiMfwXvtooWH3xbLJA==
|
||||
|
||||
swagger-ui-express@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/swagger-ui-express/-/swagger-ui-express-5.0.0.tgz#7a00a18dd909574cb0d628574a299b9ba53d4d49"
|
||||
integrity sha512-tsU9tODVvhyfkNSvf03E6FAk+z+5cU3lXAzMy6Pv4av2Gt2xA0++fogwC4qo19XuFf6hdxevPuVCSKFuMHJhFA==
|
||||
dependencies:
|
||||
swagger-ui-dist ">=5.0.0"
|
||||
|
||||
table-layout@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/table-layout/-/table-layout-1.0.2.tgz#c4038a1853b0136d63365a734b6931cf4fad4a04"
|
||||
|
Loading…
Reference in New Issue
Block a user