mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-24 04:10:15 +01:00
Feat/startup config api (#518)
* feat: add api for config * feat: add data service to client * feat: update client pages with values from config endpoint * test: update tests * Update configurations and documentation to remove VITE_SHOW_GOOGLE_LOGIN_OPTION and change VITE_APP_TITLE to APP_TITLE * include APP_TITLE with startup config * Add test for new route * update backend-review pipeline * comment out test until we can figure out testing routes in CI * update: .env.example --------- Co-authored-by: fuegovic <32828263+fuegovic@users.noreply.github.com>
This commit is contained in:
parent
2da81db440
commit
3634d8691a
25 changed files with 419 additions and 75 deletions
|
|
@ -2,10 +2,11 @@ import { useEffect } from 'react';
|
|||
import LoginForm from './LoginForm';
|
||||
import { useAuthContext } from '~/hooks/AuthContext';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { SHOW_GOOGLE_LOGIN_OPTION, ALLOW_REGISTRATION, DOMAIN_SERVER } from "~/utils/envConstants";
|
||||
import { useGetStartupConfig } from '~/data-provider';
|
||||
|
||||
function Login() {
|
||||
const { login, error, isAuthenticated } = useAuthContext();
|
||||
const { data: startupConfig } = useGetStartupConfig();
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
|
|
@ -29,7 +30,7 @@ function Login() {
|
|||
</div>
|
||||
)}
|
||||
<LoginForm onSubmit={login} />
|
||||
{ALLOW_REGISTRATION && (
|
||||
{startupConfig?.registrationEnabled && (
|
||||
<p className="my-4 text-center text-sm font-light text-gray-700">
|
||||
{' '}
|
||||
Don't have an account?{' '}
|
||||
|
|
@ -38,7 +39,7 @@ function Login() {
|
|||
</a>
|
||||
</p>
|
||||
)}
|
||||
{SHOW_GOOGLE_LOGIN_OPTION && (
|
||||
{startupConfig?.googleLoginEnabled && (
|
||||
<>
|
||||
<div className="relative mt-6 flex w-full items-center justify-center border border-t uppercase">
|
||||
<div className="absolute bg-white px-3 text-xs">Or</div>
|
||||
|
|
@ -47,7 +48,7 @@ function Login() {
|
|||
<a
|
||||
aria-label="Login with Google"
|
||||
className="justify-left flex w-full items-center space-x-3 rounded-md border border-gray-300 px-5 py-3 hover:bg-gray-50 focus:ring-2 focus:ring-violet-600 focus:ring-offset-1"
|
||||
href={`${DOMAIN_SERVER}/oauth/google`}
|
||||
href={`${startupConfig.serverDomain}/oauth/google`}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useRegisterUserMutation, TRegisterUser } from '~/data-provider';
|
||||
import { SHOW_GOOGLE_LOGIN_OPTION, DOMAIN_SERVER } from '~/utils/envConstants';
|
||||
import { useRegisterUserMutation, TRegisterUser, useGetStartupConfig } from '~/data-provider';
|
||||
|
||||
function Registration() {
|
||||
const navigate = useNavigate();
|
||||
const { data: startupConfig } = useGetStartupConfig();
|
||||
|
||||
const {
|
||||
register,
|
||||
|
|
@ -34,6 +34,12 @@ function Registration() {
|
|||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (startupConfig?.registrationEnabled === false) {
|
||||
navigate('/login');
|
||||
}
|
||||
}, [startupConfig, navigate]);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col items-center justify-center bg-white pt-6 sm:pt-0">
|
||||
<div className="mt-6 w-96 overflow-hidden bg-white px-6 py-4 sm:max-w-md sm:rounded-lg">
|
||||
|
|
@ -266,7 +272,7 @@ function Registration() {
|
|||
Login
|
||||
</a>
|
||||
</p>
|
||||
{SHOW_GOOGLE_LOGIN_OPTION && (
|
||||
{startupConfig?.googleLoginEnabled && (
|
||||
<>
|
||||
<div className="relative mt-6 flex w-full items-center justify-center border border-t uppercase">
|
||||
<div className="absolute bg-white px-3 text-xs">Or</div>
|
||||
|
|
@ -275,7 +281,7 @@ function Registration() {
|
|||
<div className="mt-4 flex gap-x-2">
|
||||
<a
|
||||
aria-label="Login with Google"
|
||||
href={`${DOMAIN_SERVER}/oauth/google`}
|
||||
href={`${startupConfig.serverDomain}/oauth/google`}
|
||||
className="justify-left flex w-full items-center space-x-3 rounded-md border border-gray-300 px-5 py-3 hover:bg-gray-50 focus:ring-2 focus:ring-violet-600 focus:ring-offset-1"
|
||||
>
|
||||
<svg
|
||||
|
|
|
|||
|
|
@ -3,12 +3,6 @@ import userEvent from '@testing-library/user-event';
|
|||
import Login from '../Login';
|
||||
import * as mockDataProvider from '~/data-provider';
|
||||
|
||||
jest.mock('~/utils/envConstants', () => ({
|
||||
DOMAIN_SERVER: 'mock-server',
|
||||
SHOW_GOOGLE_LOGIN_OPTION: true,
|
||||
ALLOW_REGISTRATION: true
|
||||
}));
|
||||
|
||||
jest.mock('~/data-provider');
|
||||
|
||||
const setup = ({
|
||||
|
|
@ -23,6 +17,15 @@ const setup = ({
|
|||
mutate: jest.fn(),
|
||||
data: {},
|
||||
isSuccess: false
|
||||
},
|
||||
useGetStartupCongfigReturnValue = {
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
data: {
|
||||
googleLoginEnabled: true,
|
||||
registrationEnabled: true,
|
||||
serverDomain: 'mock-server'
|
||||
}
|
||||
}
|
||||
} = {}) => {
|
||||
const mockUseLoginUser = jest
|
||||
|
|
@ -33,12 +36,16 @@ const setup = ({
|
|||
.spyOn(mockDataProvider, 'useGetUserQuery')
|
||||
//@ts-ignore - we don't need all parameters of the QueryObserverSuccessResult
|
||||
.mockReturnValue(useGetUserQueryReturnValue);
|
||||
const mockUseGetStartupConfig = jest
|
||||
.spyOn(mockDataProvider, 'useGetStartupConfig')
|
||||
//@ts-ignore - we don't need all parameters of the QueryObserverSuccessResult
|
||||
.mockReturnValue(useGetStartupCongfigReturnValue);
|
||||
const renderResult = render(<Login />);
|
||||
|
||||
return {
|
||||
...renderResult,
|
||||
mockUseLoginUser,
|
||||
mockUseGetUserQuery
|
||||
mockUseGetUserQuery,
|
||||
mockUseGetStartupConfig
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -3,11 +3,6 @@ import userEvent from '@testing-library/user-event';
|
|||
import Registration from '../Registration';
|
||||
import * as mockDataProvider from '~/data-provider';
|
||||
|
||||
jest.mock('~/utils/envConstants', () => ({
|
||||
DOMAIN_SERVER: 'mock-server',
|
||||
SHOW_GOOGLE_LOGIN_OPTION: true
|
||||
}));
|
||||
|
||||
jest.mock('~/data-provider');
|
||||
|
||||
const setup = ({
|
||||
|
|
@ -22,6 +17,15 @@ const setup = ({
|
|||
mutate: jest.fn(),
|
||||
data: {},
|
||||
isSuccess: false
|
||||
},
|
||||
useGetStartupCongfigReturnValue = {
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
data: {
|
||||
googleLoginEnabled: true,
|
||||
registrationEnabled: true,
|
||||
serverDomain: 'mock-server'
|
||||
}
|
||||
}
|
||||
} = {}) => {
|
||||
const mockUseRegisterUserMutation = jest
|
||||
|
|
@ -32,13 +36,18 @@ const setup = ({
|
|||
.spyOn(mockDataProvider, 'useGetUserQuery')
|
||||
//@ts-ignore - we don't need all parameters of the QueryObserverSuccessResult
|
||||
.mockReturnValue(useGetUserQueryReturnValue);
|
||||
const mockUseGetStartupConfig = jest
|
||||
.spyOn(mockDataProvider, 'useGetStartupConfig')
|
||||
//@ts-ignore - we don't need all parameters of the QueryObserverSuccessResult
|
||||
.mockReturnValue(useGetStartupCongfigReturnValue);
|
||||
|
||||
const renderResult = render(<Registration />);
|
||||
|
||||
return {
|
||||
...renderResult,
|
||||
mockUseRegisterUserMutation,
|
||||
mockUseGetUserQuery
|
||||
mockUseGetUserQuery,
|
||||
mockUseGetStartupConfig
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import React from 'react';
|
||||
import { useGetStartupConfig } from '~/data-provider';
|
||||
|
||||
export default function Footer() {
|
||||
const { data: config } = useGetStartupConfig();
|
||||
return (
|
||||
<div className="hidden px-3 pb-1 pt-2 text-center text-xs text-black/50 dark:text-white/50 md:block md:px-4 md:pb-4 md:pt-3">
|
||||
<a
|
||||
|
|
@ -9,7 +11,7 @@ export default function Footer() {
|
|||
rel="noreferrer"
|
||||
className="underline"
|
||||
>
|
||||
{import.meta.env.VITE_APP_TITLE || 'LibreChat'}
|
||||
{config?.appTitle || 'LibreChat'}
|
||||
</a>
|
||||
. Serves and searches all conversations reliably. All AI convos under one house. Pay per call
|
||||
and not per month (cents compared to dollars).
|
||||
|
|
@ -1,20 +1,24 @@
|
|||
import React from 'react';
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import useDocumentTitle from '~/hooks/useDocumentTitle';
|
||||
import SunIcon from '../svg/SunIcon';
|
||||
import LightningIcon from '../svg/LightningIcon';
|
||||
import CautionIcon from '../svg/CautionIcon';
|
||||
import store from '~/store';
|
||||
import { useGetStartupConfig } from '~/data-provider';
|
||||
|
||||
export default function Landing() {
|
||||
const { data: config } = useGetStartupConfig();
|
||||
const setText = useSetRecoilState(store.text);
|
||||
const conversation = useRecoilValue(store.conversation);
|
||||
// @ts-ignore TODO: Fix anti-pattern - requires refactoring conversation store
|
||||
const { title = 'New Chat' } = conversation || {};
|
||||
|
||||
useDocumentTitle(title);
|
||||
|
||||
const clickHandler = (e) => {
|
||||
const clickHandler = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault();
|
||||
const { innerText } = e.target;
|
||||
const { innerText } = e.target as HTMLButtonElement;
|
||||
const quote = innerText.split('"')[1].trim();
|
||||
setText(quote);
|
||||
};
|
||||
|
|
@ -26,7 +30,7 @@ export default function Landing() {
|
|||
id="landing-title"
|
||||
className="mb-10 ml-auto mr-auto mt-6 flex items-center justify-center gap-2 text-center text-4xl font-semibold sm:mb-16 md:mt-[10vh]"
|
||||
>
|
||||
{import.meta.env.VITE_APP_TITLE || 'LibreChat'}
|
||||
{config?.appTitle || 'LibreChat'}
|
||||
</h1>
|
||||
<div className="items-start gap-3.5 text-center md:flex">
|
||||
<div className="mb-8 flex flex-1 flex-col gap-3.5 md:mb-auto">
|
||||
|
|
@ -89,3 +89,7 @@ export const resetPassword = () => {
|
|||
export const plugins = () => {
|
||||
return '/api/plugins';
|
||||
};
|
||||
|
||||
export const config = () => {
|
||||
return '/api/config';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -111,3 +111,7 @@ export const getAvailablePlugins = (): Promise<t.TPlugin[]> => {
|
|||
export const updateUserPlugins = (payload: t.TUpdateUserPlugins) => {
|
||||
return request.post(endpoints.userPlugins(), payload);
|
||||
};
|
||||
|
||||
export const getStartupConfig = (): Promise<t.TStartupConfig> => {
|
||||
return request.get(endpoints.config());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,8 @@ export enum QueryKeys {
|
|||
presets = 'presets',
|
||||
searchResults = 'searchResults',
|
||||
tokenCount = 'tokenCount',
|
||||
availablePlugins = 'availablePlugins'
|
||||
availablePlugins = 'availablePlugins',
|
||||
startupConfig = 'startupConfig',
|
||||
}
|
||||
|
||||
export const useAbortRequestWithMessage = (): UseMutationResult<
|
||||
|
|
@ -336,3 +337,11 @@ export const useUpdateUserPluginsMutation = (): UseMutationResult<
|
|||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetStartupConfig = (): QueryObserverResult<t.TStartupConfig> => {
|
||||
return useQuery<t.TStartupConfig>([QueryKeys.startupConfig], () => dataService.getStartupConfig(), {
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchOnMount: false
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -233,3 +233,10 @@ export type TResetPassword = {
|
|||
token: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
export type TStartupConfig = {
|
||||
appTitle: boolean;
|
||||
googleLoginEnabled: boolean;
|
||||
serverDomain: string;
|
||||
registrationEnabled: boolean;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,11 @@ import Messages from '../components/Messages';
|
|||
import TextChat from '../components/Input';
|
||||
|
||||
import store from '~/store';
|
||||
import { useGetMessagesByConvoId, useGetConversationByIdMutation } from '~/data-provider';
|
||||
import {
|
||||
useGetMessagesByConvoId,
|
||||
useGetConversationByIdMutation,
|
||||
useGetStartupConfig
|
||||
} from '~/data-provider';
|
||||
|
||||
export default function Chat() {
|
||||
const searchQuery = useRecoilValue(store.searchQuery);
|
||||
|
|
@ -21,6 +25,7 @@ export default function Chat() {
|
|||
//disabled by default, we only enable it when messagesTree is null
|
||||
const messagesQuery = useGetMessagesByConvoId(conversationId, { enabled: false });
|
||||
const getConversationMutation = useGetConversationByIdMutation(conversationId);
|
||||
const { data: config } = useGetStartupConfig();
|
||||
|
||||
// when conversation changed or conversationId (in url) changed
|
||||
useEffect(() => {
|
||||
|
|
@ -53,8 +58,8 @@ export default function Chat() {
|
|||
// conversationId (in url) should always follow conversation?.conversationId, unless conversation is null
|
||||
navigate(`/chat/${conversation?.conversationId}`);
|
||||
}
|
||||
document.title = conversation?.title || import.meta.env.VITE_APP_TITLE || 'Chat';
|
||||
}, [conversation, conversationId]);
|
||||
document.title = conversation?.title || config?.appTitle || 'Chat';
|
||||
}, [conversation, conversationId, config]);
|
||||
|
||||
useEffect(() => {
|
||||
if (messagesTree === null && conversation?.conversationId) {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import Search from './Search';
|
|||
import { Login, Registration, RequestPasswordReset, ResetPassword } from '../components/Auth';
|
||||
import { AuthContextProvider } from '../hooks/AuthContext';
|
||||
import ApiErrorWatcher from '../components/Auth/ApiErrorWatcher';
|
||||
import { ALLOW_REGISTRATION } from '../utils/envConstants';
|
||||
|
||||
const AuthLayout = () => (
|
||||
<AuthContextProvider>
|
||||
|
|
@ -17,7 +16,7 @@ const AuthLayout = () => (
|
|||
export const router = createBrowserRouter([
|
||||
{
|
||||
path: 'register',
|
||||
element: ALLOW_REGISTRATION ? <Registration /> : <Navigate to="/login" replace={true} />
|
||||
element: <Registration />
|
||||
},
|
||||
{
|
||||
path: 'forgot-password',
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
const ALLOW_REGISTRATION = import.meta.env.ALLOW_REGISTRATION === 'true';
|
||||
const DOMAIN_SERVER = import.meta.env.DOMAIN_SERVER;
|
||||
const SHOW_GOOGLE_LOGIN_OPTION = import.meta.env.VITE_SHOW_GOOGLE_LOGIN_OPTION === 'true';
|
||||
|
||||
export {
|
||||
ALLOW_REGISTRATION,
|
||||
DOMAIN_SERVER,
|
||||
SHOW_GOOGLE_LOGIN_OPTION
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue