anything-llm/server/utils/middleware/embedMiddleware.js
Sean Hatfield 1846a99b93
[FEAT] Embedded AnythingLLM (#656)
* WIP embedded app

* WIP got response from backend in embedded app

* WIP streaming prints to embedded app

* implemented streaming and tailwind min for styling into embedded app

* WIP embedded app history functional

* load params from script tag into embedded app

* rough in modularization of embed chat
cleanup dev process for easier dev support
move all chat to components
todo: build process
todo: backend support

* remove eslint config

* Implement models and cleanup embed chat endpoints
Improve build process for embed
prod minification and bundle size awareness
WIP

* forgot files

* rename to embed folder

* introduce chat modal styles

* add middleware validations on embed chat

* auto open param and default greeting

* reset chat history

* Admin embed config page

* Admin Embed Chats mgmt page

* update embed

* nonpriv

* more style support
reopen if chat was last opened

* update comments

* remove unused imports

* allow change of workspace for embedconfig

* update failure to lookup message

* update reset script

* update instructions

* Add more styling options
Add sponsor text at bottom
Support dynamic container height
Loading animations

* publish new embed script

* Add back syntax highlighting and keep bundle small via dynamic script build

* add hint

* update readme

* update copy model for snippet with link to styles

---------

Co-authored-by: timothycarambat <rambat1010@gmail.com>
2024-02-05 14:21:34 -08:00

152 lines
3.7 KiB
JavaScript

const { v4: uuidv4 } = require("uuid");
const { VALID_CHAT_MODE } = require("../chats/stream");
const { EmbedChats } = require("../../models/embedChats");
const { EmbedConfig } = require("../../models/embedConfig");
const { reqBody } = require("../http");
// Finds or Aborts request for a /:embedId/ url. This should always
// be the first middleware and the :embedID should be in the URL.
async function validEmbedConfig(request, response, next) {
const { embedId } = request.params;
const embed = await EmbedConfig.getWithWorkspace({ uuid: embedId });
if (!embed) {
response.sendStatus(404).end();
return;
}
response.locals.embedConfig = embed;
next();
}
function setConnectionMeta(request, response, next) {
response.locals.connection = {
host: request.headers?.origin,
ip: request?.ip,
};
next();
}
async function validEmbedConfigId(request, response, next) {
const { embedId } = request.params;
const embed = await EmbedConfig.get({ id: Number(embedId) });
if (!embed) {
response.sendStatus(404).end();
return;
}
response.locals.embedConfig = embed;
next();
}
async function canRespond(request, response, next) {
const embed = response.locals.embedConfig;
if (!embed) {
response.sendStatus(404).end();
return;
}
// Block if disabled by admin.
if (!embed.enabled) {
response.status(503).json({
id: uuidv4(),
type: "abort",
textResponse: null,
sources: [],
close: true,
error:
"This chat has been disabled by the administrator - try again later.",
});
return;
}
// Check if requester hostname is in the valid allowlist of domains.
const host = request.headers.origin ?? "";
const allowedHosts = EmbedConfig.parseAllowedHosts(embed);
if (allowedHosts !== null && !allowedHosts.includes(host)) {
response.status(401).json({
id: uuidv4(),
type: "abort",
textResponse: null,
sources: [],
close: true,
error: "Invalid request.",
});
return;
}
const { sessionId, message } = reqBody(request);
if (!message?.length || !VALID_CHAT_MODE.includes(embed.chat_mode)) {
response.status(400).json({
id: uuidv4(),
type: "abort",
textResponse: null,
sources: [],
close: true,
error: !message?.length
? "Message is empty."
: `${embed.chat_mode} is not a valid mode.`,
});
return;
}
if (!isNaN(embed.max_chats_per_day) && Number(embed.max_chats_per_day) > 0) {
const dailyChatCount = await EmbedChats.count({
embed_id: embed.id,
createdAt: {
gte: new Date(new Date() - 24 * 60 * 60 * 1000),
},
});
if (dailyChatCount >= Number(embed.max_chats_per_day)) {
response.status(429).json({
id: uuidv4(),
type: "abort",
textResponse: null,
sources: [],
close: true,
error:
"The quota for this chat has been reached. Try again later or contact the site owner.",
});
return;
}
}
if (
!isNaN(embed.max_chats_per_session) &&
Number(embed.max_chats_per_session) > 0
) {
const dailySessionCount = await EmbedChats.count({
embed_id: embed.id,
session_id: sessionId,
createdAt: {
gte: new Date(new Date() - 24 * 60 * 60 * 1000),
},
});
if (dailySessionCount >= Number(embed.max_chats_per_session)) {
response.status(429).json({
id: uuidv4(),
type: "abort",
textResponse: null,
sources: [],
close: true,
error:
"Your quota for this chat has been reached. Try again later or contact the site owner.",
});
return;
}
}
next();
}
module.exports = {
setConnectionMeta,
validEmbedConfig,
validEmbedConfigId,
canRespond,
};