Merge b16f2516a6
into 1680a1786e
This commit is contained in:
commit
c2f261212d
17 changed files with 13339 additions and 1297 deletions
|
@ -49,4 +49,4 @@ networks:
|
||||||
perplexica-network:
|
perplexica-network:
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
backend-dbstore:
|
backend-dbstore:
|
22
drizzle/0000_exotic_zeigeist.sql
Normal file
22
drizzle/0000_exotic_zeigeist.sql
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
CREATE TABLE `auth_settings` (
|
||||||
|
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
`is_enabled` integer DEFAULT false NOT NULL,
|
||||||
|
`username` text,
|
||||||
|
`password` text
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `chats` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`title` text NOT NULL,
|
||||||
|
`createdAt` text NOT NULL,
|
||||||
|
`focusMode` text NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `messages` (
|
||||||
|
`id` integer PRIMARY KEY NOT NULL,
|
||||||
|
`content` text NOT NULL,
|
||||||
|
`chatId` text NOT NULL,
|
||||||
|
`messageId` text NOT NULL,
|
||||||
|
`type` text,
|
||||||
|
`metadata` text
|
||||||
|
);
|
143
drizzle/meta/0000_snapshot.json
Normal file
143
drizzle/meta/0000_snapshot.json
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
{
|
||||||
|
"version": "6",
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"id": "1f74ebbe-03cb-4768-9e38-c59596de9938",
|
||||||
|
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||||
|
"tables": {
|
||||||
|
"auth_settings": {
|
||||||
|
"name": "auth_settings",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"is_enabled": {
|
||||||
|
"name": "is_enabled",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"username": {
|
||||||
|
"name": "username",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"name": "password",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
},
|
||||||
|
"chats": {
|
||||||
|
"name": "chats",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"name": "title",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"name": "createdAt",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"focusMode": {
|
||||||
|
"name": "focusMode",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"name": "messages",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"name": "content",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"chatId": {
|
||||||
|
"name": "chatId",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"messageId": {
|
||||||
|
"name": "messageId",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "type",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"name": "metadata",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {},
|
||||||
|
"_meta": {
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {},
|
||||||
|
"columns": {}
|
||||||
|
},
|
||||||
|
"internal": {
|
||||||
|
"indexes": {}
|
||||||
|
}
|
||||||
|
}
|
13
drizzle/meta/_journal.json
Normal file
13
drizzle/meta/_journal.json
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"entries": [
|
||||||
|
{
|
||||||
|
"idx": 0,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1728069023714,
|
||||||
|
"tag": "0000_exotic_zeigeist",
|
||||||
|
"breakpoints": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
5214
package-lock.json
generated
Normal file
5214
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
|
@ -39,6 +39,7 @@
|
||||||
"drizzle-orm": "^0.31.2",
|
"drizzle-orm": "^0.31.2",
|
||||||
"express": "^4.19.2",
|
"express": "^4.19.2",
|
||||||
"html-to-text": "^9.0.5",
|
"html-to-text": "^9.0.5",
|
||||||
|
"js-cookie": "^3.0.5",
|
||||||
"langchain": "^0.1.30",
|
"langchain": "^0.1.30",
|
||||||
"pdf-parse": "^1.1.1",
|
"pdf-parse": "^1.1.1",
|
||||||
"winston": "^3.13.0",
|
"winston": "^3.13.0",
|
||||||
|
|
|
@ -5,6 +5,7 @@ import http from 'http';
|
||||||
import routes from './routes';
|
import routes from './routes';
|
||||||
import { getPort } from './config';
|
import { getPort } from './config';
|
||||||
import logger from './utils/logger';
|
import logger from './utils/logger';
|
||||||
|
import authSettingsRouter from './routes/auth-settings';
|
||||||
|
|
||||||
const port = getPort();
|
const port = getPort();
|
||||||
|
|
||||||
|
@ -19,6 +20,7 @@ app.use(cors(corsOptions));
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
|
||||||
app.use('/api', routes);
|
app.use('/api', routes);
|
||||||
|
app.use('/api/auth-settings', authSettingsRouter);
|
||||||
app.get('/api', (_, res) => {
|
app.get('/api', (_, res) => {
|
||||||
res.status(200).json({ status: 'ok' });
|
res.status(200).json({ status: 'ok' });
|
||||||
});
|
});
|
||||||
|
@ -35,4 +37,4 @@ process.on('uncaughtException', (err, origin) => {
|
||||||
|
|
||||||
process.on('unhandledRejection', (reason, promise) => {
|
process.on('unhandledRejection', (reason, promise) => {
|
||||||
logger.error(`Unhandled Rejection at: ${promise}, reason: ${reason}`);
|
logger.error(`Unhandled Rejection at: ${promise}, reason: ${reason}`);
|
||||||
});
|
});
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { timestamp } from 'drizzle-orm/pg-core';
|
||||||
import { text, integer, sqliteTable } from 'drizzle-orm/sqlite-core';
|
import { text, integer, sqliteTable } from 'drizzle-orm/sqlite-core';
|
||||||
|
|
||||||
export const messages = sqliteTable('messages', {
|
export const messages = sqliteTable('messages', {
|
||||||
|
@ -17,3 +18,10 @@ export const chats = sqliteTable('chats', {
|
||||||
createdAt: text('createdAt').notNull(),
|
createdAt: text('createdAt').notNull(),
|
||||||
focusMode: text('focusMode').notNull(),
|
focusMode: text('focusMode').notNull(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const authSettings = sqliteTable('auth_settings', {
|
||||||
|
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||||
|
isEnabled: integer('is_enabled', { mode: 'boolean' }).notNull().default(false),
|
||||||
|
username: text('username'),
|
||||||
|
password: text('password'),
|
||||||
|
});
|
31
src/routes/auth-settings.ts
Normal file
31
src/routes/auth-settings.ts
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import express from 'express';
|
||||||
|
import db from '../db';
|
||||||
|
import { authSettings } from '../db/schema';
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.get('/', async (req, res) => {
|
||||||
|
const settings = await db.select().from(authSettings).limit(1);
|
||||||
|
res.json(settings[0] || { isEnabled: false, username: null, password: null });
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/', async (req, res) => {
|
||||||
|
const { isEnabled, username, password } = req.body;
|
||||||
|
|
||||||
|
if (isEnabled && (!username || !password)) {
|
||||||
|
return res.status(400).json({ message: 'Username and password are required when enabling authentication.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.insert(authSettings).values({
|
||||||
|
isEnabled,
|
||||||
|
username: isEnabled ? username : null,
|
||||||
|
password: isEnabled ? password : null,
|
||||||
|
}).onConflictDoUpdate({
|
||||||
|
target: authSettings.id,
|
||||||
|
set: { isEnabled, username: isEnabled ? username : null, password: isEnabled ? password : null },
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).json({ message: 'Authentication settings updated' });
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
|
@ -1,4 +1,5 @@
|
||||||
import ChatWindow from '@/components/ChatWindow';
|
import ChatWindow from '@/components/ChatWindow';
|
||||||
|
import AuthSettingsHandler from '@/components/AuthSettingsHandler';
|
||||||
import { Metadata } from 'next';
|
import { Metadata } from 'next';
|
||||||
import { Suspense } from 'react';
|
import { Suspense } from 'react';
|
||||||
|
|
||||||
|
@ -7,14 +8,13 @@ export const metadata: Metadata = {
|
||||||
description: 'Chat with the internet, chat with Perplexica.',
|
description: 'Chat with the internet, chat with Perplexica.',
|
||||||
};
|
};
|
||||||
|
|
||||||
const Home = () => {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
<AuthSettingsHandler />
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<ChatWindow />
|
<ChatWindow />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
export default Home;
|
|
35
ui/components/AuthSettingsHandler.tsx
Normal file
35
ui/components/AuthSettingsHandler.tsx
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
import Cookies from 'js-cookie'
|
||||||
|
|
||||||
|
export default function AuthSettingsHandler() {
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchAuthSettings = async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/auth-settings`, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const data = await res.json()
|
||||||
|
|
||||||
|
if (data.isEnabled) {
|
||||||
|
Cookies.set('authEnabled', 'true', { expires: 7, path: '/' })
|
||||||
|
Cookies.set('authUsername', data.username, { expires: 7, path: '/' })
|
||||||
|
Cookies.set('authPassword', data.password, { expires: 7, path: '/' })
|
||||||
|
} else {
|
||||||
|
Cookies.remove('authEnabled', { path: '/' })
|
||||||
|
Cookies.remove('authUsername', { path: '/' })
|
||||||
|
Cookies.remove('authPassword', { path: '/' })
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching auth settings:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchAuthSettings()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
|
@ -8,6 +8,8 @@ import React, {
|
||||||
type SelectHTMLAttributes,
|
type SelectHTMLAttributes,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import ThemeSwitcher from './theme/Switcher';
|
import ThemeSwitcher from './theme/Switcher';
|
||||||
|
import { Switch } from '@headlessui/react';
|
||||||
|
import Cookies from 'js-cookie';
|
||||||
|
|
||||||
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
|
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||||
|
|
||||||
|
@ -36,13 +38,11 @@ export const Select = ({ className, options, ...restProps }: SelectProps) => {
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{options.map(({ label, value, disabled }) => {
|
{options.map((option) => (
|
||||||
return (
|
<option key={option.value} value={option.value} disabled={option.disabled}>
|
||||||
<option key={value} value={value} disabled={disabled}>
|
{option.label}
|
||||||
{label}
|
</option>
|
||||||
</option>
|
))}
|
||||||
);
|
|
||||||
})}
|
|
||||||
</select>
|
</select>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -60,6 +60,12 @@ interface SettingsType {
|
||||||
ollamaApiUrl: string;
|
ollamaApiUrl: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface AuthSettings {
|
||||||
|
isEnabled: boolean;
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
const SettingsDialog = ({
|
const SettingsDialog = ({
|
||||||
isOpen,
|
isOpen,
|
||||||
setIsOpen,
|
setIsOpen,
|
||||||
|
@ -87,6 +93,11 @@ const SettingsDialog = ({
|
||||||
const [customOpenAIBaseURL, setCustomOpenAIBaseURL] = useState<string>('');
|
const [customOpenAIBaseURL, setCustomOpenAIBaseURL] = useState<string>('');
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [isUpdating, setIsUpdating] = useState(false);
|
const [isUpdating, setIsUpdating] = useState(false);
|
||||||
|
const [authSettings, setAuthSettings] = useState<AuthSettings>({
|
||||||
|
isEnabled: false,
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
|
@ -150,44 +161,52 @@ const SettingsDialog = ({
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
|
const handleAuthToggle = () => {
|
||||||
|
setAuthSettings((prev) => ({
|
||||||
|
...prev,
|
||||||
|
isEnabled: !prev.isEnabled,
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
setIsUpdating(true);
|
setIsUpdating(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await fetch(`${process.env.NEXT_PUBLIC_API_URL}/config`, {
|
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/auth-settings`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify(config),
|
body: JSON.stringify(authSettings),
|
||||||
});
|
});
|
||||||
|
|
||||||
localStorage.setItem('chatModelProvider', selectedChatModelProvider!);
|
if (res.ok) {
|
||||||
localStorage.setItem('chatModel', selectedChatModel!);
|
if (authSettings.isEnabled) {
|
||||||
localStorage.setItem(
|
Cookies.set('authEnabled', 'true');
|
||||||
'embeddingModelProvider',
|
Cookies.set('authUsername', authSettings.username);
|
||||||
selectedEmbeddingModelProvider!,
|
Cookies.set('authPassword', authSettings.password);
|
||||||
);
|
} else {
|
||||||
localStorage.setItem('embeddingModel', selectedEmbeddingModel!);
|
Cookies.remove('authEnabled');
|
||||||
localStorage.setItem('openAIApiKey', customOpenAIApiKey!);
|
Cookies.remove('authUsername');
|
||||||
localStorage.setItem('openAIBaseURL', customOpenAIBaseURL!);
|
Cookies.remove('authPassword');
|
||||||
} catch (err) {
|
}
|
||||||
console.log(err);
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
const errorData = await res.json();
|
||||||
|
alert(errorData.message || 'Failed to update settings.');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating auth settings:', error);
|
||||||
|
alert('An error occurred while updating settings.');
|
||||||
} finally {
|
} finally {
|
||||||
setIsUpdating(false);
|
setIsUpdating(false);
|
||||||
setIsOpen(false);
|
|
||||||
|
|
||||||
window.location.reload();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Transition appear show={isOpen} as={Fragment}>
|
<Transition appear show={isOpen} as={Fragment}>
|
||||||
<Dialog
|
<Dialog as="div" className="relative z-10" onClose={() => setIsOpen(false)}>
|
||||||
as="div"
|
|
||||||
className="relative z-50"
|
|
||||||
onClose={() => setIsOpen(false)}
|
|
||||||
>
|
|
||||||
<Transition.Child
|
<Transition.Child
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
enter="ease-out duration-300"
|
enter="ease-out duration-300"
|
||||||
|
@ -197,21 +216,25 @@ const SettingsDialog = ({
|
||||||
leaveFrom="opacity-100"
|
leaveFrom="opacity-100"
|
||||||
leaveTo="opacity-0"
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
<div className="fixed inset-0 bg-white/50 dark:bg-black/50" />
|
<div className="fixed inset-0 bg-black bg-opacity-25" />
|
||||||
</Transition.Child>
|
</Transition.Child>
|
||||||
|
|
||||||
<div className="fixed inset-0 overflow-y-auto">
|
<div className="fixed inset-0 overflow-y-auto">
|
||||||
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
||||||
<Transition.Child
|
<Transition.Child
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
enter="ease-out duration-200"
|
enter="ease-out duration-300"
|
||||||
enterFrom="opacity-0 scale-95"
|
enterFrom="opacity-0 scale-95"
|
||||||
enterTo="opacity-100 scale-100"
|
enterTo="opacity-100 scale-100"
|
||||||
leave="ease-in duration-100"
|
leave="ease-in duration-200"
|
||||||
leaveFrom="opacity-100 scale-200"
|
leaveFrom="opacity-100 scale-100"
|
||||||
leaveTo="opacity-0 scale-95"
|
leaveTo="opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Dialog.Panel className="w-full max-w-md transform rounded-2xl bg-light-secondary dark:bg-dark-secondary border border-light-200 dark:border-dark-200 p-6 text-left align-middle shadow-xl transition-all">
|
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-2xl bg-white dark:bg-gray-800 p-6 text-left align-middle shadow-xl transition-all">
|
||||||
<Dialog.Title className="text-xl font-medium leading-6 dark:text-white">
|
<Dialog.Title
|
||||||
|
as="h3"
|
||||||
|
className="text-lg font-medium leading-6 text-gray-900 dark:text-white"
|
||||||
|
>
|
||||||
Settings
|
Settings
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
{config && !isLoading && (
|
{config && !isLoading && (
|
||||||
|
@ -468,6 +491,63 @@ const SettingsDialog = ({
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{/* Authentication Toggle */}
|
||||||
|
<div className="mt-6 border-t border-gray-200 dark:border-gray-700 pt-6">
|
||||||
|
<h4 className="text-md font-semibold text-gray-700 dark:text-gray-200 mb-4">
|
||||||
|
HTTP Basic Authentication
|
||||||
|
</h4>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-gray-700 dark:text-gray-200">Enable Authentication</span>
|
||||||
|
<Switch
|
||||||
|
checked={authSettings.isEnabled}
|
||||||
|
onChange={handleAuthToggle}
|
||||||
|
className={`${
|
||||||
|
authSettings.isEnabled ? 'bg-blue-600' : 'bg-gray-200'
|
||||||
|
} relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2`}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`${
|
||||||
|
authSettings.isEnabled ? 'translate-x-6' : 'translate-x-1'
|
||||||
|
} inline-block h-4 w-4 transform rounded-full bg-white transition-transform`}
|
||||||
|
/>
|
||||||
|
</Switch>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{authSettings.isEnabled && (
|
||||||
|
<div className="mt-4 space-y-4">
|
||||||
|
<div className="flex flex-col space-y-1">
|
||||||
|
<p className="text-black/70 dark:text-white/70 text-sm">Username</p>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="Username"
|
||||||
|
value={authSettings.username}
|
||||||
|
onChange={(e) =>
|
||||||
|
setAuthSettings({
|
||||||
|
...authSettings,
|
||||||
|
username: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
required={authSettings.isEnabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col space-y-1">
|
||||||
|
<p className="text-black/70 dark:text-white/70 text-sm">Password</p>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
placeholder="Password"
|
||||||
|
value={authSettings.password}
|
||||||
|
onChange={(e) =>
|
||||||
|
setAuthSettings({
|
||||||
|
...authSettings,
|
||||||
|
password: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
required={authSettings.isEnabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
|
@ -489,6 +569,7 @@ const SettingsDialog = ({
|
||||||
) : (
|
) : (
|
||||||
<CloudUpload size={20} />
|
<CloudUpload size={20} />
|
||||||
)}
|
)}
|
||||||
|
<span>Confirm</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</Dialog.Panel>
|
</Dialog.Panel>
|
||||||
|
@ -500,4 +581,4 @@ const SettingsDialog = ({
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SettingsDialog;
|
export default SettingsDialog;
|
69
ui/middleware.ts
Normal file
69
ui/middleware.ts
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import type { NextRequest } from 'next/server';
|
||||||
|
|
||||||
|
const decodeBase64 = (encoded: string): { username: string; password: string } | null => {
|
||||||
|
try {
|
||||||
|
const decoded = Buffer.from(encoded, 'base64').toString('utf-8');
|
||||||
|
const [username, password] = decoded.split(':');
|
||||||
|
if (username && password) {
|
||||||
|
return { username, password };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const verifyPassword = (inputPassword: string, storedPassword: string): boolean => {
|
||||||
|
return inputPassword === storedPassword;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function middleware(request: NextRequest) {
|
||||||
|
const response = NextResponse.next();
|
||||||
|
|
||||||
|
// Extract cookies from the request
|
||||||
|
const cookies = request.headers.get('cookie');
|
||||||
|
let authEnabled = false;
|
||||||
|
let authUsername = '';
|
||||||
|
let authPassword = '';
|
||||||
|
|
||||||
|
if (cookies) {
|
||||||
|
const parsedCookies = Object.fromEntries(cookies.split('; ').map(c => c.split('=')));
|
||||||
|
authEnabled = parsedCookies['authEnabled'] === 'true';
|
||||||
|
authUsername = parsedCookies['authUsername'] || '';
|
||||||
|
authPassword = parsedCookies['authPassword'] || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authEnabled) {
|
||||||
|
const authHeader = request.headers.get('authorization');
|
||||||
|
|
||||||
|
if (!authHeader || !authHeader.startsWith('Basic ')) {
|
||||||
|
const headers = new Headers();
|
||||||
|
headers.set('WWW-Authenticate', 'Basic realm="Restricted Area"');
|
||||||
|
return new NextResponse('Authentication Required', { status: 401, headers });
|
||||||
|
}
|
||||||
|
|
||||||
|
const encodedCredentials = authHeader.split(' ')[1];
|
||||||
|
const credentials = decodeBase64(encodedCredentials);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!credentials ||
|
||||||
|
credentials.username !== authUsername ||
|
||||||
|
!verifyPassword(credentials.password, authPassword)
|
||||||
|
) {
|
||||||
|
const headers = new Headers();
|
||||||
|
headers.set('WWW-Authenticate', 'Basic realm="Restricted Area"');
|
||||||
|
return new NextResponse('Invalid Credentials', { status: 401, headers });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Credentials are valid; allow access
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authentication not enabled; allow access
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
matcher: '/:path*',
|
||||||
|
};
|
6676
ui/package-lock.json
generated
Normal file
6676
ui/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
|
@ -16,6 +16,7 @@
|
||||||
"@langchain/openai": "^0.0.25",
|
"@langchain/openai": "^0.0.25",
|
||||||
"@tailwindcss/typography": "^0.5.12",
|
"@tailwindcss/typography": "^0.5.12",
|
||||||
"clsx": "^2.1.0",
|
"clsx": "^2.1.0",
|
||||||
|
"js-cookie": "^3.0.5",
|
||||||
"langchain": "^0.1.30",
|
"langchain": "^0.1.30",
|
||||||
"lucide-react": "^0.363.0",
|
"lucide-react": "^0.363.0",
|
||||||
"markdown-to-jsx": "^7.4.5",
|
"markdown-to-jsx": "^7.4.5",
|
||||||
|
@ -31,6 +32,7 @@
|
||||||
"zod": "^3.22.4"
|
"zod": "^3.22.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/js-cookie": "^3.0.6",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^18",
|
"@types/react": "^18",
|
||||||
"@types/react-dom": "^18",
|
"@types/react-dom": "^18",
|
||||||
|
|
1057
ui/yarn.lock
1057
ui/yarn.lock
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue