Add Auth for WebPage and APIs
This commit is contained in:
parent
e6b87f89ec
commit
5e6d0e0ee6
27 changed files with 15384 additions and 1720 deletions
62
ui/app/auth/page.tsx
Normal file
62
ui/app/auth/page.tsx
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
'use client';
|
||||
|
||||
import { useState, FormEvent } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
export default function AuthPage() {
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const router = useRouter();
|
||||
|
||||
const handleSubmit = async (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/auth/verify`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ password }),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
localStorage.setItem('authToken', data.token);
|
||||
router.push('/');
|
||||
} else {
|
||||
setError('密码错误');
|
||||
}
|
||||
} catch (err) {
|
||||
setError('验证失败,请重试');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-light-primary dark:bg-dark-primary">
|
||||
<div className="w-full max-w-md p-8 space-y-6 bg-light-secondary dark:bg-dark-secondary rounded-lg shadow-lg">
|
||||
<h1 className="text-2xl font-bold text-center">请输入访问密码</h1>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="w-full p-3 border rounded-md bg-light-primary dark:bg-dark-primary"
|
||||
placeholder="输入密码"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
{error && (
|
||||
<p className="text-red-500 text-sm text-center">{error}</p>
|
||||
)}
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full p-3 text-white bg-blue-600 rounded-md hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
确认
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import { Search } from 'lucide-react';
|
|||
import { useEffect, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { toast } from 'sonner';
|
||||
import { getApiUrl, get } from '@/lib/api';
|
||||
|
||||
interface Discover {
|
||||
title: string;
|
||||
|
|
@ -19,25 +20,11 @@ const Page = () => {
|
|||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/discover`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(data.message);
|
||||
}
|
||||
|
||||
data.blogs = data.blogs.filter((blog: Discover) => blog.thumbnail);
|
||||
|
||||
setDiscover(data.blogs);
|
||||
} catch (err: any) {
|
||||
console.error('Error fetching data:', err.message);
|
||||
toast.error('Error fetching data');
|
||||
const data = await get<{ blogs: Discover[] }>(getApiUrl('/discover'));
|
||||
setDiscover(data.blogs.filter(blog => blog.thumbnail));
|
||||
} catch (error) {
|
||||
console.error('Unable to fetch discovers:', error);
|
||||
toast.error('Unable to fetch discovers');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
|
@ -88,9 +75,17 @@ const Page = () => {
|
|||
<img
|
||||
className="object-cover w-full aspect-video"
|
||||
src={
|
||||
new URL(item.thumbnail).origin +
|
||||
new URL(item.thumbnail).pathname +
|
||||
`?id=${new URL(item.thumbnail).searchParams.get('id')}`
|
||||
(() => {
|
||||
try {
|
||||
if (!item.thumbnail) return '/placeholder.jpg';
|
||||
const url = new URL(item.thumbnail);
|
||||
return url.origin + url.pathname +
|
||||
(url.searchParams.get('id') ?
|
||||
`?id=${url.searchParams.get('id')}` : '');
|
||||
} catch (e) {
|
||||
return item.thumbnail || '/placeholder.jpg';
|
||||
}
|
||||
})()
|
||||
}
|
||||
alt={item.title}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { cn } from '@/lib/utils';
|
|||
import Sidebar from '@/components/Sidebar';
|
||||
import { Toaster } from 'sonner';
|
||||
import ThemeProvider from '@/components/theme/Provider';
|
||||
import AuthCheck from '@/components/AuthCheck';
|
||||
|
||||
const montserrat = Montserrat({
|
||||
weight: ['300', '400', '500', '700'],
|
||||
|
|
@ -28,7 +29,9 @@ export default function RootLayout({
|
|||
<html className="h-full" lang="en" suppressHydrationWarning>
|
||||
<body className={cn('h-full', montserrat.className)}>
|
||||
<ThemeProvider>
|
||||
<Sidebar>{children}</Sidebar>
|
||||
<AuthCheck>
|
||||
<Sidebar>{children}</Sidebar>
|
||||
</AuthCheck>
|
||||
<Toaster
|
||||
toastOptions={{
|
||||
unstyled: true,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import { cn, formatTimeDifference } from '@/lib/utils';
|
|||
import { BookOpenText, ClockIcon, Delete, ScanEye } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { getApiUrl, get } from '@/lib/api';
|
||||
|
||||
export interface Chat {
|
||||
id: string;
|
||||
|
|
@ -21,17 +23,15 @@ const Page = () => {
|
|||
const fetchChats = async () => {
|
||||
setLoading(true);
|
||||
|
||||
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/chats`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
setChats(data.chats);
|
||||
setLoading(false);
|
||||
try {
|
||||
const data = await get<{ chats: Chat[] }>(getApiUrl('/chats'));
|
||||
setChats(data.chats);
|
||||
} catch (error) {
|
||||
console.error('获取聊天记录失败:', error);
|
||||
toast.error('获取聊天记录失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchChats();
|
||||
|
|
|
|||
|
|
@ -7,13 +7,15 @@ import { Switch } from '@headlessui/react';
|
|||
import ThemeSwitcher from '@/components/theme/Switcher';
|
||||
import { ImagesIcon, VideoIcon } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { toast } from 'sonner';
|
||||
import { getApiUrl, get, post } from '@/lib/api';
|
||||
|
||||
interface SettingsType {
|
||||
chatModelProviders: {
|
||||
[key: string]: [Record<string, any>];
|
||||
[key: string]: Record<string, any>[];
|
||||
};
|
||||
embeddingModelProviders: {
|
||||
[key: string]: [Record<string, any>];
|
||||
[key: string]: Record<string, any>[];
|
||||
};
|
||||
openaiApiKey: string;
|
||||
groqApiKey: string;
|
||||
|
|
@ -116,63 +118,62 @@ const Page = () => {
|
|||
useEffect(() => {
|
||||
const fetchConfig = async () => {
|
||||
setIsLoading(true);
|
||||
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/config`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
try {
|
||||
const data = await get<SettingsType>(getApiUrl('/config'));
|
||||
setConfig(data);
|
||||
|
||||
const data = (await res.json()) as SettingsType;
|
||||
setConfig(data);
|
||||
const chatModelProvidersKeys = Object.keys(data.chatModelProviders || {});
|
||||
const embeddingModelProvidersKeys = Object.keys(
|
||||
data.embeddingModelProviders || {},
|
||||
);
|
||||
|
||||
const chatModelProvidersKeys = Object.keys(data.chatModelProviders || {});
|
||||
const embeddingModelProvidersKeys = Object.keys(
|
||||
data.embeddingModelProviders || {},
|
||||
);
|
||||
const defaultChatModelProvider =
|
||||
chatModelProvidersKeys.length > 0 ? chatModelProvidersKeys[0] : '';
|
||||
const defaultEmbeddingModelProvider =
|
||||
embeddingModelProvidersKeys.length > 0
|
||||
? embeddingModelProvidersKeys[0]
|
||||
: '';
|
||||
|
||||
const defaultChatModelProvider =
|
||||
chatModelProvidersKeys.length > 0 ? chatModelProvidersKeys[0] : '';
|
||||
const defaultEmbeddingModelProvider =
|
||||
embeddingModelProvidersKeys.length > 0
|
||||
? embeddingModelProvidersKeys[0]
|
||||
: '';
|
||||
const chatModelProvider =
|
||||
localStorage.getItem('chatModelProvider') ||
|
||||
defaultChatModelProvider ||
|
||||
'';
|
||||
const chatModel =
|
||||
localStorage.getItem('chatModel') ||
|
||||
(data.chatModelProviders &&
|
||||
data.chatModelProviders[chatModelProvider]?.length > 0
|
||||
? data.chatModelProviders[chatModelProvider][0].name
|
||||
: undefined) ||
|
||||
'';
|
||||
const embeddingModelProvider =
|
||||
localStorage.getItem('embeddingModelProvider') ||
|
||||
defaultEmbeddingModelProvider ||
|
||||
'';
|
||||
const embeddingModel =
|
||||
localStorage.getItem('embeddingModel') ||
|
||||
(data.embeddingModelProviders &&
|
||||
data.embeddingModelProviders[embeddingModelProvider]?.[0].name) ||
|
||||
'';
|
||||
|
||||
const chatModelProvider =
|
||||
localStorage.getItem('chatModelProvider') ||
|
||||
defaultChatModelProvider ||
|
||||
'';
|
||||
const chatModel =
|
||||
localStorage.getItem('chatModel') ||
|
||||
(data.chatModelProviders &&
|
||||
data.chatModelProviders[chatModelProvider]?.length > 0
|
||||
? data.chatModelProviders[chatModelProvider][0].name
|
||||
: undefined) ||
|
||||
'';
|
||||
const embeddingModelProvider =
|
||||
localStorage.getItem('embeddingModelProvider') ||
|
||||
defaultEmbeddingModelProvider ||
|
||||
'';
|
||||
const embeddingModel =
|
||||
localStorage.getItem('embeddingModel') ||
|
||||
(data.embeddingModelProviders &&
|
||||
data.embeddingModelProviders[embeddingModelProvider]?.[0].name) ||
|
||||
'';
|
||||
setSelectedChatModelProvider(chatModelProvider);
|
||||
setSelectedChatModel(chatModel);
|
||||
setSelectedEmbeddingModelProvider(embeddingModelProvider);
|
||||
setSelectedEmbeddingModel(embeddingModel);
|
||||
setChatModels(data.chatModelProviders || {});
|
||||
setEmbeddingModels(data.embeddingModelProviders || {});
|
||||
|
||||
setSelectedChatModelProvider(chatModelProvider);
|
||||
setSelectedChatModel(chatModel);
|
||||
setSelectedEmbeddingModelProvider(embeddingModelProvider);
|
||||
setSelectedEmbeddingModel(embeddingModel);
|
||||
setChatModels(data.chatModelProviders || {});
|
||||
setEmbeddingModels(data.embeddingModelProviders || {});
|
||||
|
||||
setAutomaticImageSearch(
|
||||
localStorage.getItem('autoImageSearch') === 'true',
|
||||
);
|
||||
setAutomaticVideoSearch(
|
||||
localStorage.getItem('autoVideoSearch') === 'true',
|
||||
);
|
||||
|
||||
setIsLoading(false);
|
||||
setAutomaticImageSearch(
|
||||
localStorage.getItem('autoImageSearch') === 'true',
|
||||
);
|
||||
setAutomaticVideoSearch(
|
||||
localStorage.getItem('autoVideoSearch') === 'true',
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('获取配置失败:', error);
|
||||
toast.error('获取配置失败');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchConfig();
|
||||
|
|
@ -187,39 +188,15 @@ const Page = () => {
|
|||
[key]: value,
|
||||
} as SettingsType;
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/config`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(updatedConfig),
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to update config');
|
||||
}
|
||||
|
||||
await post(getApiUrl('/config'), updatedConfig);
|
||||
|
||||
setConfig(updatedConfig);
|
||||
|
||||
if (
|
||||
key.toLowerCase().includes('api') ||
|
||||
key.toLowerCase().includes('url')
|
||||
) {
|
||||
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/config`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to fetch updated config');
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
const data = await get<SettingsType>(getApiUrl('/config'));
|
||||
setChatModels(data.chatModelProviders || {});
|
||||
setEmbeddingModels(data.embeddingModelProviders || {});
|
||||
|
||||
|
|
@ -332,13 +309,11 @@ const Page = () => {
|
|||
} else if (key === 'embeddingModel') {
|
||||
localStorage.setItem('embeddingModel', value);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to save:', err);
|
||||
setConfig((prev) => ({ ...prev! }));
|
||||
} catch (error) {
|
||||
console.error('保存配置失败:', error);
|
||||
toast.error('保存配置失败');
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
setSavingStates((prev) => ({ ...prev, [key]: false }));
|
||||
}, 500);
|
||||
setSavingStates((prev) => ({ ...prev, [key]: false }));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue