anything-llm/server/endpoints/browserExtension.js
Timothy Carambat 29df483a27
AnythingLLM Chrome Extension (#2066)
* initial commit for chrome extension

* wip browser extension backend

* wip frontend browser extension settings

* fix typo for browserExtension route

* implement verification codes + frontend panel for browser extension keys

* reorganize + state management for all connection states

* implement embed to workspace

* add send page to anythingllm extension option + refactor

* refactor connection string auth + update context menus + organize background.js into models

* popup extension from main app and save if successful

* fix hebrew translation misspelling

* fetch custom logo inside chrome extension

* delete api keys on disconnect of extension

* use correct apiUrl constant in frontend + remove unneeded comments

* remove upload-link endpoint and send inner text html to raw text collector endpoint

* update readme

* fix readme link

* fix readme typo

* update readme

* handle deletion of browser keys with key id and DELETE endpoint

* move event string to constant

* remove tablename and writable fields from BrowserExtensionApiKey backend model

* add border-none to all buttons and inputs for desktop compatibility

* patch prisma injections

* update delete endpoints to delete keys by id

* remove unused prop

* add button to attempt browser extension connection + remove max active keys

* wip multi user mode support

* multi user mode support

* clean up backend + show created by in frotend browser extension page

* show multi user warning message on key creation + hide context menus when no workspaces

* show browser extension options to managers

* small backend changes and refactors

* extension cleanup

* rename submodule

* extension updates & docs

* dev docker build

---------

Co-authored-by: shatfield4 <seanhatfield5@gmail.com>
2024-08-27 14:58:47 -07:00

225 lines
6.8 KiB
JavaScript

const { Workspace } = require("../models/workspace");
const { BrowserExtensionApiKey } = require("../models/browserExtensionApiKey");
const { Document } = require("../models/documents");
const {
validBrowserExtensionApiKey,
} = require("../utils/middleware/validBrowserExtensionApiKey");
const { CollectorApi } = require("../utils/collectorApi");
const { reqBody, multiUserMode, userFromSession } = require("../utils/http");
const { validatedRequest } = require("../utils/middleware/validatedRequest");
const {
flexUserRoleValid,
ROLES,
} = require("../utils/middleware/multiUserProtected");
const { Telemetry } = require("../models/telemetry");
function browserExtensionEndpoints(app) {
if (!app) return;
app.get(
"/browser-extension/check",
[validBrowserExtensionApiKey],
async (request, response) => {
try {
const user = await userFromSession(request, response);
const workspaces = multiUserMode(response)
? await Workspace.whereWithUser(user)
: await Workspace.where();
const apiKeyId = response.locals.apiKey.id;
response.status(200).json({
connected: true,
workspaces,
apiKeyId,
});
} catch (error) {
console.error(error);
response
.status(500)
.json({ connected: false, error: "Failed to fetch workspaces" });
}
}
);
app.delete(
"/browser-extension/disconnect",
[validBrowserExtensionApiKey],
async (_request, response) => {
try {
const apiKeyId = response.locals.apiKey.id;
const { success, error } =
await BrowserExtensionApiKey.delete(apiKeyId);
if (!success) throw new Error(error);
response.status(200).json({ success: true });
} catch (error) {
console.error(error);
response
.status(500)
.json({ error: "Failed to disconnect and revoke API key" });
}
}
);
app.get(
"/browser-extension/workspaces",
[validBrowserExtensionApiKey],
async (request, response) => {
try {
const user = await userFromSession(request, response);
const workspaces = multiUserMode(response)
? await Workspace.whereWithUser(user)
: await Workspace.where();
response.status(200).json({ workspaces });
} catch (error) {
console.error(error);
response.status(500).json({ error: "Failed to fetch workspaces" });
}
}
);
app.post(
"/browser-extension/embed-content",
[validBrowserExtensionApiKey],
async (request, response) => {
try {
const { workspaceId, textContent, metadata } = reqBody(request);
const user = await userFromSession(request, response);
const workspace = multiUserMode(response)
? await Workspace.getWithUser(user, { id: parseInt(workspaceId) })
: await Workspace.get({ id: parseInt(workspaceId) });
if (!workspace) {
response.status(404).json({ error: "Workspace not found" });
return;
}
const Collector = new CollectorApi();
const { success, reason, documents } = await Collector.processRawText(
textContent,
metadata
);
if (!success) {
response.status(500).json({ success: false, error: reason });
return;
}
const { failedToEmbed = [], errors = [] } = await Document.addDocuments(
workspace,
[documents[0].location],
user?.id
);
if (failedToEmbed.length > 0) {
response.status(500).json({ success: false, error: errors[0] });
return;
}
await Telemetry.sendTelemetry("browser_extension_embed_content");
response.status(200).json({ success: true });
} catch (error) {
console.error(error);
response.status(500).json({ error: "Failed to embed content" });
}
}
);
app.post(
"/browser-extension/upload-content",
[validBrowserExtensionApiKey],
async (request, response) => {
try {
const { textContent, metadata } = reqBody(request);
const Collector = new CollectorApi();
const { success, reason } = await Collector.processRawText(
textContent,
metadata
);
if (!success) {
response.status(500).json({ success: false, error: reason });
return;
}
await Telemetry.sendTelemetry("browser_extension_upload_content");
response.status(200).json({ success: true });
} catch (error) {
console.error(error);
response.status(500).json({ error: "Failed to embed content" });
}
}
);
// Internal endpoints for managing API keys
app.get(
"/browser-extension/api-keys",
[validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
async (request, response) => {
try {
const user = await userFromSession(request, response);
const apiKeys = multiUserMode(response)
? await BrowserExtensionApiKey.whereWithUser(user)
: await BrowserExtensionApiKey.where();
response.status(200).json({ success: true, apiKeys });
} catch (error) {
console.error(error);
response
.status(500)
.json({ success: false, error: "Failed to fetch API keys" });
}
}
);
app.post(
"/browser-extension/api-keys/new",
[validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
async (request, response) => {
try {
const user = await userFromSession(request, response);
const { apiKey, error } = await BrowserExtensionApiKey.create(
user?.id || null
);
if (error) throw new Error(error);
response.status(200).json({
apiKey: apiKey.key,
});
} catch (error) {
console.error(error);
response.status(500).json({ error: "Failed to create API key" });
}
}
);
app.delete(
"/browser-extension/api-keys/:id",
[validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
async (request, response) => {
try {
const { id } = request.params;
const user = await userFromSession(request, response);
if (multiUserMode(response) && user.role !== ROLES.admin) {
const apiKey = await BrowserExtensionApiKey.get({
id: parseInt(id),
user_id: user?.id,
});
if (!apiKey) {
return response.status(403).json({ error: "Unauthorized" });
}
}
const { success, error } = await BrowserExtensionApiKey.delete(id);
if (!success) throw new Error(error);
response.status(200).json({ success: true });
} catch (error) {
console.error(error);
response.status(500).json({ error: "Failed to revoke API key" });
}
}
);
}
module.exports = { browserExtensionEndpoints };