Add Auth for WebPage and APIs
This commit is contained in:
parent
e6b87f89ec
commit
5e6d0e0ee6
27 changed files with 15384 additions and 1720 deletions
65
ui/components/AuthCheck.tsx
Normal file
65
ui/components/AuthCheck.tsx
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRouter, usePathname } from 'next/navigation';
|
||||
|
||||
export default function AuthCheck({ children }: { children: React.ReactNode }) {
|
||||
const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null);
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
useEffect(() => {
|
||||
// 检查用户是否已经通过认证
|
||||
const authToken = localStorage.getItem('authToken');
|
||||
|
||||
// 如果当前路径是认证页面,不需要重定向
|
||||
if (pathname === '/auth') {
|
||||
setIsAuthenticated(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果未认证且不在认证页面,重定向到认证页面
|
||||
if (!authToken) {
|
||||
router.push('/auth');
|
||||
setIsAuthenticated(false);
|
||||
} else {
|
||||
// 验证令牌有效性
|
||||
checkTokenValidity(authToken);
|
||||
}
|
||||
}, [pathname, router]);
|
||||
|
||||
// 验证令牌有效性
|
||||
const checkTokenValidity = async (token: string) => {
|
||||
try {
|
||||
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/auth/verify-token`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
setIsAuthenticated(true);
|
||||
} else {
|
||||
// 令牌无效,清除存储并重定向到登录页
|
||||
localStorage.removeItem('authToken');
|
||||
router.push('/auth');
|
||||
setIsAuthenticated(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('验证令牌时出错:', error);
|
||||
setIsAuthenticated(true); // 网络错误时暂时允许访问
|
||||
}
|
||||
};
|
||||
|
||||
// 如果认证状态尚未确定,显示加载状态
|
||||
if (isAuthenticated === null) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-light-primary dark:bg-dark-primary">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 如果在认证页面或已认证,显示子组件
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ import { getSuggestions } from '@/lib/actions';
|
|||
import { Settings } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import NextError from 'next/error';
|
||||
import { getApiUrl, get } from '@/lib/api';
|
||||
|
||||
export type Message = {
|
||||
messageId: string;
|
||||
|
|
@ -71,20 +72,10 @@ const useSocket = (
|
|||
localStorage.setItem('autoVideoSearch', 'false');
|
||||
}
|
||||
|
||||
const providers = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/models`,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
},
|
||||
).then(async (res) => {
|
||||
if (!res.ok)
|
||||
throw new Error(
|
||||
`Failed to fetch models: ${res.status} ${res.statusText}`,
|
||||
);
|
||||
return res.json();
|
||||
});
|
||||
const providers = await get<{
|
||||
chatModelProviders: Record<string, Record<string, any>>,
|
||||
embeddingModelProviders: Record<string, Record<string, any>>
|
||||
}>(getApiUrl('/models'));
|
||||
|
||||
if (
|
||||
!chatModel ||
|
||||
|
|
@ -201,6 +192,12 @@ const useSocket = (
|
|||
|
||||
searchParams.append('embeddingModel', embeddingModel!);
|
||||
searchParams.append('embeddingModelProvider', embeddingModelProvider);
|
||||
|
||||
// 添加认证令牌
|
||||
const authToken = localStorage.getItem('authToken');
|
||||
if (authToken) {
|
||||
searchParams.append('token', authToken);
|
||||
}
|
||||
|
||||
wsURL.search = searchParams.toString();
|
||||
|
||||
|
|
@ -315,55 +312,45 @@ const loadMessages = async (
|
|||
setFiles: (files: File[]) => void,
|
||||
setFileIds: (fileIds: string[]) => void,
|
||||
) => {
|
||||
const res = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/chats/${chatId}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
},
|
||||
);
|
||||
try {
|
||||
const data = await get<any>(getApiUrl(`/chats/${chatId}`));
|
||||
|
||||
const messages = data.messages.map((msg: any) => {
|
||||
return {
|
||||
...msg,
|
||||
...JSON.parse(msg.metadata),
|
||||
};
|
||||
}) as Message[];
|
||||
|
||||
if (res.status === 404) {
|
||||
setNotFound(true);
|
||||
setMessages(messages);
|
||||
|
||||
const history = messages.map((msg) => {
|
||||
return [msg.role, msg.content];
|
||||
}) as [string, string][];
|
||||
|
||||
console.debug(new Date(), 'app:messages_loaded');
|
||||
|
||||
document.title = messages[0].content;
|
||||
|
||||
const files = data.chat.files.map((file: any) => {
|
||||
return {
|
||||
fileName: file.name,
|
||||
fileExtension: file.name.split('.').pop(),
|
||||
fileId: file.fileId,
|
||||
};
|
||||
});
|
||||
|
||||
setFiles(files);
|
||||
setFileIds(files.map((file: File) => file.fileId));
|
||||
|
||||
setChatHistory(history);
|
||||
setFocusMode(data.chat.focusMode);
|
||||
setIsMessagesLoaded(true);
|
||||
return;
|
||||
} catch (error) {
|
||||
console.debug(new Date(), 'ws:error', error);
|
||||
setIsMessagesLoaded(true);
|
||||
setNotFound(true);
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
const messages = data.messages.map((msg: any) => {
|
||||
return {
|
||||
...msg,
|
||||
...JSON.parse(msg.metadata),
|
||||
};
|
||||
}) as Message[];
|
||||
|
||||
setMessages(messages);
|
||||
|
||||
const history = messages.map((msg) => {
|
||||
return [msg.role, msg.content];
|
||||
}) as [string, string][];
|
||||
|
||||
console.debug(new Date(), 'app:messages_loaded');
|
||||
|
||||
document.title = messages[0].content;
|
||||
|
||||
const files = data.chat.files.map((file: any) => {
|
||||
return {
|
||||
fileName: file.name,
|
||||
fileExtension: file.name.split('.').pop(),
|
||||
fileId: file.fileId,
|
||||
};
|
||||
});
|
||||
|
||||
setFiles(files);
|
||||
setFileIds(files.map((file: File) => file.fileId));
|
||||
|
||||
setChatHistory(history);
|
||||
setFocusMode(data.chat.focusMode);
|
||||
setIsMessagesLoaded(true);
|
||||
};
|
||||
|
||||
const ChatWindow = ({ id }: { id?: string }) => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Trash } from 'lucide-react';
|
||||
import { TrashIcon } from 'lucide-react';
|
||||
import {
|
||||
Description,
|
||||
Dialog,
|
||||
|
|
@ -11,6 +11,8 @@ import {
|
|||
import { Fragment, useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { Chat } from '@/app/library/page';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { getApiUrl, del } from '@/lib/api';
|
||||
|
||||
const DeleteChat = ({
|
||||
chatId,
|
||||
|
|
@ -25,36 +27,27 @@ const DeleteChat = ({
|
|||
}) => {
|
||||
const [confirmationDialogOpen, setConfirmationDialogOpen] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const router = useRouter();
|
||||
|
||||
const handleDelete = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/chats/${chatId}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (res.status != 200) {
|
||||
throw new Error('Failed to delete chat');
|
||||
}
|
||||
|
||||
const newChats = chats.filter((chat) => chat.id !== chatId);
|
||||
|
||||
setChats(newChats);
|
||||
|
||||
await del(getApiUrl(`/chats/${chatId}`));
|
||||
|
||||
// 从列表中移除聊天
|
||||
setChats(chats.filter((chat) => chat.id !== chatId));
|
||||
|
||||
if (redirect) {
|
||||
window.location.href = '/';
|
||||
router.push('/');
|
||||
}
|
||||
} catch (err: any) {
|
||||
toast.error(err.message);
|
||||
|
||||
toast.success('聊天已删除');
|
||||
} catch (error) {
|
||||
console.error('删除聊天失败:', error);
|
||||
toast.error('删除聊天失败');
|
||||
} finally {
|
||||
setConfirmationDialogOpen(false);
|
||||
setLoading(false);
|
||||
setConfirmationDialogOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -66,7 +59,7 @@ const DeleteChat = ({
|
|||
}}
|
||||
className="bg-transparent text-red-400 hover:scale-105 transition duration-200"
|
||||
>
|
||||
<Trash size={17} />
|
||||
<TrashIcon size={17} />
|
||||
</button>
|
||||
<Transition appear show={confirmationDialogOpen} as={Fragment}>
|
||||
<Dialog
|
||||
|
|
|
|||
|
|
@ -41,8 +41,18 @@ const Attach = ({
|
|||
data.append('embedding_model_provider', embeddingModelProvider!);
|
||||
data.append('embedding_model', embeddingModel!);
|
||||
|
||||
// 获取认证令牌
|
||||
const authToken = localStorage.getItem('authToken');
|
||||
|
||||
// 创建headers对象,添加认证令牌
|
||||
const headers: HeadersInit = {};
|
||||
if (authToken) {
|
||||
headers['Authorization'] = `Bearer ${authToken}`;
|
||||
}
|
||||
|
||||
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/uploads`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: data,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -39,8 +39,18 @@ const AttachSmall = ({
|
|||
data.append('embedding_model_provider', embeddingModelProvider!);
|
||||
data.append('embedding_model', embeddingModel!);
|
||||
|
||||
// 获取认证令牌
|
||||
const authToken = localStorage.getItem('authToken');
|
||||
|
||||
// 创建headers对象,添加认证令牌
|
||||
const headers: HeadersInit = {};
|
||||
if (authToken) {
|
||||
headers['Authorization'] = `Bearer ${authToken}`;
|
||||
}
|
||||
|
||||
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/uploads`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: data,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
/* eslint-disable @next/next/no-img-element */
|
||||
import { ImagesIcon, PlusIcon } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import Lightbox from 'yet-another-react-lightbox';
|
||||
import 'yet-another-react-lightbox/styles.css';
|
||||
import { Message } from './ChatWindow';
|
||||
import { getApiUrl, post } from '@/lib/api';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
type Image = {
|
||||
url: string;
|
||||
|
|
@ -37,34 +39,25 @@ const SearchImages = ({
|
|||
const customOpenAIBaseURL = localStorage.getItem('openAIBaseURL');
|
||||
const customOpenAIKey = localStorage.getItem('openAIApiKey');
|
||||
|
||||
const res = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/images`,
|
||||
const data = await post<{ images: Image[] }>(
|
||||
getApiUrl('/images'),
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
query,
|
||||
chatHistory,
|
||||
chatModel: {
|
||||
provider: chatModelProvider,
|
||||
model: chatModel,
|
||||
...(chatModelProvider === 'custom_openai' && {
|
||||
customOpenAIKey,
|
||||
customOpenAIBaseURL,
|
||||
}),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query: query,
|
||||
chatHistory: chatHistory,
|
||||
chatModel: {
|
||||
provider: chatModelProvider,
|
||||
model: chatModel,
|
||||
...(chatModelProvider === 'custom_openai' && {
|
||||
customOpenAIBaseURL: customOpenAIBaseURL,
|
||||
customOpenAIKey: customOpenAIKey,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
const images = data.images ?? [];
|
||||
setImages(images);
|
||||
setImages(data.images);
|
||||
setSlides(
|
||||
images.map((image: Image) => {
|
||||
data.images.map((image: Image) => {
|
||||
return {
|
||||
src: image.img_src,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { useRef, useState } from 'react';
|
|||
import Lightbox, { GenericSlide, VideoSlide } from 'yet-another-react-lightbox';
|
||||
import 'yet-another-react-lightbox/styles.css';
|
||||
import { Message } from './ChatWindow';
|
||||
import { getApiUrl, post } from '@/lib/api';
|
||||
|
||||
type Video = {
|
||||
url: string;
|
||||
|
|
@ -52,40 +53,31 @@ const Searchvideos = ({
|
|||
const customOpenAIBaseURL = localStorage.getItem('openAIBaseURL');
|
||||
const customOpenAIKey = localStorage.getItem('openAIApiKey');
|
||||
|
||||
const res = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/videos`,
|
||||
const data = await post<{ videos: Video[] }>(
|
||||
getApiUrl('/videos'),
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
query,
|
||||
chatHistory,
|
||||
chatModel: {
|
||||
provider: chatModelProvider,
|
||||
model: chatModel,
|
||||
...(chatModelProvider === 'custom_openai' && {
|
||||
customOpenAIKey,
|
||||
customOpenAIBaseURL,
|
||||
}),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query: query,
|
||||
chatHistory: chatHistory,
|
||||
chatModel: {
|
||||
provider: chatModelProvider,
|
||||
model: chatModel,
|
||||
...(chatModelProvider === 'custom_openai' && {
|
||||
customOpenAIBaseURL: customOpenAIBaseURL,
|
||||
customOpenAIKey: customOpenAIKey,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
const videos = data.videos ?? [];
|
||||
setVideos(videos);
|
||||
setVideos(data.videos || []);
|
||||
setSlides(
|
||||
videos.map((video: Video) => {
|
||||
data.videos.map((video: Video) => {
|
||||
return {
|
||||
type: 'video-slide',
|
||||
iframe_src: video.iframe_src,
|
||||
src: video.img_src,
|
||||
};
|
||||
}),
|
||||
})
|
||||
);
|
||||
setLoading(false);
|
||||
}}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue