From fdfe8d1f4112e1b5fbfdda848350b29abfd4959d Mon Sep 17 00:00:00 2001 From: ItzCrazyKns Date: Fri, 2 Aug 2024 19:32:38 +0530 Subject: [PATCH] feat(app): add password auth for settings --- sample.config.toml | 4 ++ src/config.ts | 13 ++++++ src/routes/config.ts | 40 ++++++++++++++++- src/routes/models.ts | 31 +++++++++++-- src/websocket/messageHandler.ts | 77 ++++++++++++++++++--------------- ui/app/library/layout.tsx | 26 ++++++++++- ui/app/library/page.tsx | 2 +- 7 files changed, 152 insertions(+), 41 deletions(-) diff --git a/sample.config.toml b/sample.config.toml index f6c6943..781761a 100644 --- a/sample.config.toml +++ b/sample.config.toml @@ -1,6 +1,10 @@ [GENERAL] PORT = 3001 # Port to run the server on SIMILARITY_MEASURE = "cosine" # "cosine" or "dot" +CONFIG_PASSWORD = "lorem_ipsum" # Password to access config +DISCOVER_ENABLED = true +LIBRARY_ENABLED = true +COPILOT_ENABLED = true [API_KEYS] OPENAI = "" # OpenAI API key - sk-1234567890abcdef1234567890abcdef diff --git a/src/config.ts b/src/config.ts index 9ebc182..c343d69 100644 --- a/src/config.ts +++ b/src/config.ts @@ -8,6 +8,10 @@ interface Config { GENERAL: { PORT: number; SIMILARITY_MEASURE: string; + CONFIG_PASSWORD: string; + DISCOVER_ENABLED: boolean; + LIBRARY_ENABLED: boolean; + COPILOT_ENABLED: boolean; }; API_KEYS: { OPENAI: string; @@ -34,6 +38,14 @@ export const getPort = () => loadConfig().GENERAL.PORT; export const getSimilarityMeasure = () => loadConfig().GENERAL.SIMILARITY_MEASURE; +export const getConfigPassword = () => loadConfig().GENERAL.CONFIG_PASSWORD; + +export const isDiscoverEnabled = () => loadConfig().GENERAL.DISCOVER_ENABLED; + +export const isLibraryEnabled = () => loadConfig().GENERAL.LIBRARY_ENABLED; + +export const isCopilotEnabled = () => loadConfig().GENERAL.COPILOT_ENABLED; + export const getOpenaiApiKey = () => loadConfig().API_KEYS.OPENAI; export const getGroqApiKey = () => loadConfig().API_KEYS.GROQ; @@ -53,6 +65,7 @@ export const updateConfig = (config: RecursivePartial) => { if (typeof currentConfig[key] === 'object' && currentConfig[key] !== null) { for (const nestedKey in currentConfig[key]) { if ( + typeof config[key][nestedKey] !== 'boolean' && !config[key][nestedKey] && currentConfig[key][nestedKey] && config[key][nestedKey] !== '' diff --git a/src/routes/config.ts b/src/routes/config.ts index f255560..8993715 100644 --- a/src/routes/config.ts +++ b/src/routes/config.ts @@ -9,11 +9,23 @@ import { getAnthropicApiKey, getOpenaiApiKey, updateConfig, + getConfigPassword, + isLibraryEnabled, + isCopilotEnabled, + isDiscoverEnabled, } from '../config'; const router = express.Router(); -router.get('/', async (_, res) => { +router.get('/', async (req, res) => { + const authHeader = req.headers['authorization']?.split(' ')[1]; + const password = getConfigPassword(); + + if (authHeader !== password) { + res.status(401).json({ message: 'Unauthorized' }); + return; + } + const config = {}; const [chatModelProviders, embeddingModelProviders] = await Promise.all([ @@ -40,14 +52,30 @@ router.get('/', async (_, res) => { config['ollamaApiUrl'] = getOllamaApiEndpoint(); config['anthropicApiKey'] = getAnthropicApiKey(); config['groqApiKey'] = getGroqApiKey(); + config['isLibraryEnabled'] = isLibraryEnabled(); + config['isCopilotEnabled'] = isCopilotEnabled(); + config['isDiscoverEnabled'] = isDiscoverEnabled(); res.status(200).json(config); }); router.post('/', async (req, res) => { + const authHeader = req.headers['authorization']?.split(' ')[1]; + const password = getConfigPassword(); + + if (authHeader !== password) { + res.status(401).json({ message: 'Unauthorized' }); + return; + } + const config = req.body; const updatedConfig = { + GENERAL: { + DISCOVER_ENABLED: config.isDiscoverEnabled, + LIBRARY_ENABLED: config.isLibraryEnabled, + COPILOT_ENABLED: config.isCopilotEnabled, + }, API_KEYS: { OPENAI: config.openaiApiKey, GROQ: config.groqApiKey, @@ -63,4 +91,14 @@ router.post('/', async (req, res) => { res.status(200).json({ message: 'Config updated' }); }); +router.get('/preferences', (_, res) => { + const preferences = { + isLibraryEnabled: isLibraryEnabled(), + isCopilotEnabled: isCopilotEnabled(), + isDiscoverEnabled: isDiscoverEnabled(), + }; + + res.status(200).json(preferences); +}); + export default router; diff --git a/src/routes/models.ts b/src/routes/models.ts index 36df25a..17b9629 100644 --- a/src/routes/models.ts +++ b/src/routes/models.ts @@ -9,10 +9,33 @@ const router = express.Router(); router.get('/', async (req, res) => { try { - const [chatModelProviders, embeddingModelProviders] = await Promise.all([ - getAvailableChatModelProviders(), - getAvailableEmbeddingModelProviders(), - ]); + const [chatModelProvidersRaw, embeddingModelProvidersRaw] = + await Promise.all([ + getAvailableChatModelProviders(), + getAvailableEmbeddingModelProviders(), + ]); + + const chatModelProviders = {}; + + const chatModelProvidersKeys = Object.keys(chatModelProvidersRaw); + chatModelProvidersKeys.forEach((provider) => { + chatModelProviders[provider] = {}; + const models = Object.keys(chatModelProvidersRaw[provider]); + models.forEach((model) => { + chatModelProviders[provider][model] = {}; + }); + }); + + const embeddingModelProviders = {}; + + const embeddingModelProvidersKeys = Object.keys(embeddingModelProvidersRaw); + embeddingModelProvidersKeys.forEach((provider) => { + embeddingModelProviders[provider] = {}; + const models = Object.keys(embeddingModelProvidersRaw[provider]); + models.forEach((model) => { + embeddingModelProviders[provider][model] = {}; + }); + }); res.status(200).json({ chatModelProviders, embeddingModelProviders }); } catch (err) { diff --git a/src/websocket/messageHandler.ts b/src/websocket/messageHandler.ts index 0afda9f..b0b2d4d 100644 --- a/src/websocket/messageHandler.ts +++ b/src/websocket/messageHandler.ts @@ -13,6 +13,7 @@ import db from '../db'; import { chats, messages } from '../db/schema'; import { eq } from 'drizzle-orm'; import crypto from 'crypto'; +import { isLibraryEnabled } from '../config'; type Message = { messageId: string; @@ -46,6 +47,8 @@ const handleEmitterEvents = ( let recievedMessage = ''; let sources = []; + const libraryEnabled = isLibraryEnabled(); + emitter.on('data', (data) => { const parsedData = JSON.parse(data); if (parsedData.type === 'response') { @@ -71,18 +74,20 @@ const handleEmitterEvents = ( emitter.on('end', () => { ws.send(JSON.stringify({ type: 'messageEnd', messageId: messageId })); - db.insert(messages) - .values({ - content: recievedMessage, - chatId: chatId, - messageId: messageId, - role: 'assistant', - metadata: JSON.stringify({ - createdAt: new Date(), - ...(sources && sources.length > 0 && { sources }), - }), - }) - .execute(); + if (libraryEnabled) { + db.insert(messages) + .values({ + content: recievedMessage, + chatId: chatId, + messageId: messageId, + role: 'assistant', + metadata: JSON.stringify({ + createdAt: new Date(), + ...(sources && sources.length > 0 && { sources }), + }), + }) + .execute(); + } }); emitter.on('error', (data) => { const parsedData = JSON.parse(data); @@ -132,6 +137,8 @@ export const handleMessage = async ( if (parsedWSMessage.type === 'message') { const handler = searchHandlers[parsedWSMessage.focusMode]; + const libraryEnabled = isLibraryEnabled(); + if (handler) { const emitter = handler( parsedMessage.content, @@ -142,34 +149,36 @@ export const handleMessage = async ( handleEmitterEvents(emitter, ws, id, parsedMessage.chatId); - const chat = await db.query.chats.findFirst({ - where: eq(chats.id, parsedMessage.chatId), - }); + if (libraryEnabled) { + const chat = await db.query.chats.findFirst({ + where: eq(chats.id, parsedMessage.chatId), + }); + + if (!chat) { + await db + .insert(chats) + .values({ + id: parsedMessage.chatId, + title: parsedMessage.content, + createdAt: new Date().toString(), + focusMode: parsedWSMessage.focusMode, + }) + .execute(); + } - if (!chat) { await db - .insert(chats) + .insert(messages) .values({ - id: parsedMessage.chatId, - title: parsedMessage.content, - createdAt: new Date().toString(), - focusMode: parsedWSMessage.focusMode, + content: parsedMessage.content, + chatId: parsedMessage.chatId, + messageId: id, + role: 'user', + metadata: JSON.stringify({ + createdAt: new Date(), + }), }) .execute(); } - - await db - .insert(messages) - .values({ - content: parsedMessage.content, - chatId: parsedMessage.chatId, - messageId: id, - role: 'user', - metadata: JSON.stringify({ - createdAt: new Date(), - }), - }) - .execute(); } else { ws.send( JSON.stringify({ diff --git a/ui/app/library/layout.tsx b/ui/app/library/layout.tsx index 00d4a3b..d1a1cf6 100644 --- a/ui/app/library/layout.tsx +++ b/ui/app/library/layout.tsx @@ -5,7 +5,31 @@ export const metadata: Metadata = { title: 'Library - Perplexica', }; -const Layout = ({ children }: { children: React.ReactNode }) => { +const Layout = async ({ children }: { children: React.ReactNode }) => { + const res = await fetch( + `${process.env.NEXT_PUBLIC_API_URL}/config/preferences`, + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }, + ); + + const data = await res.json(); + + const { isLibraryEnabled } = data; + + if (!isLibraryEnabled) { + return ( +
+

+ Library is disabled +

+
+ ); + } + return
{children}
; }; diff --git a/ui/app/library/page.tsx b/ui/app/library/page.tsx index 8294fc1..586765d 100644 --- a/ui/app/library/page.tsx +++ b/ui/app/library/page.tsx @@ -2,7 +2,7 @@ import DeleteChat from '@/components/DeleteChat'; import { formatTimeDifference } from '@/lib/utils'; -import { BookOpenText, ClockIcon, Delete, ScanEye } from 'lucide-react'; +import { BookOpenText, ClockIcon } from 'lucide-react'; import Link from 'next/link'; import { useEffect, useState } from 'react';