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:
Timothy Carambat 2023-08-23 19:15:07 -07:00 committed by GitHub
parent bdf9529e80
commit defe6054b3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 6098 additions and 57 deletions

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

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

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

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

View File

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

View File

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

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

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

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

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

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

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

View File

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

View File

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

View File

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

@ -0,0 +1,6 @@
{
"events": {
"start": "yarn swagger",
"restart": "yarn swagger"
}
}

View File

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

File diff suppressed because it is too large Load Diff

3
server/swagger/index.css Normal file
View File

@ -0,0 +1,3 @@
.schemes.wrapper>div:first-of-type {
display: none;
}

28
server/swagger/index.js Normal file
View 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
View 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

File diff suppressed because it is too large Load Diff

52
server/swagger/utils.js Normal file
View 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 }

View File

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

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

View File

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