update loyout
This commit is contained in:
@@ -1,8 +1,4 @@
|
|||||||
interface Env {
|
async function verifyToken(token, env) {
|
||||||
JWT_SECRET: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function verifyToken(token: string, env: Env) {
|
|
||||||
try {
|
try {
|
||||||
const [headerB64, payloadB64, signature] = token.split('.');
|
const [headerB64, payloadB64, signature] = token.split('.');
|
||||||
|
|
||||||
@@ -49,12 +45,32 @@ export async function verifyToken(token: string, env: Env) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 中间件函数
|
// 中间件函数
|
||||||
export const authMiddleware = async (request: Request, env: Env) => {
|
export async function onRequest(context) {
|
||||||
|
try {
|
||||||
|
const request = context.request;
|
||||||
|
const env = context.env;
|
||||||
|
//跳过登录页面
|
||||||
|
if (request.url.includes('/login') || request.url.includes('/sendcode') || request.url.includes('/login')) {
|
||||||
|
return await context.next();
|
||||||
|
}
|
||||||
const authHeader = request.headers.get('Authorization');
|
const authHeader = request.headers.get('Authorization');
|
||||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||||
throw new Error('No token provided');
|
throw new Error('No token provided');
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = authHeader.split(' ')[1];
|
const token = authHeader.split(' ')[1];
|
||||||
return await verifyToken(token, env);
|
const payload = await verifyToken(token, env);
|
||||||
};
|
|
||||||
|
// 将用户信息添加到上下文中
|
||||||
|
context.user = payload;
|
||||||
|
|
||||||
|
return await context.next();
|
||||||
|
} catch (error) {
|
||||||
|
return new Response(JSON.stringify({ error: error.message }), {
|
||||||
|
status: 401,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
62
package-lock.json
generated
62
package-lock.json
generated
@@ -30,6 +30,7 @@
|
|||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-github-btn": "^1.4.0",
|
"react-github-btn": "^1.4.0",
|
||||||
"react-markdown": "^9.0.3",
|
"react-markdown": "^9.0.3",
|
||||||
|
"react-router-dom": "^7.4.0",
|
||||||
"rehype-katex": "^7.0.1",
|
"rehype-katex": "^7.0.1",
|
||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
"remark-math": "^6.0.0",
|
"remark-math": "^6.0.0",
|
||||||
@@ -2405,6 +2406,11 @@
|
|||||||
"@babel/types": "^7.20.7"
|
"@babel/types": "^7.20.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/cookie": {
|
||||||
|
"version": "0.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
|
||||||
|
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="
|
||||||
|
},
|
||||||
"node_modules/@types/debug": {
|
"node_modules/@types/debug": {
|
||||||
"version": "4.1.12",
|
"version": "4.1.12",
|
||||||
"resolved": "https://registry.npmmirror.com/@types/debug/-/debug-4.1.12.tgz",
|
"resolved": "https://registry.npmmirror.com/@types/debug/-/debug-4.1.12.tgz",
|
||||||
@@ -6060,6 +6066,52 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-router": {
|
||||||
|
"version": "7.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.4.0.tgz",
|
||||||
|
"integrity": "sha512-Y2g5ObjkvX3VFeVt+0CIPuYd9PpgqCslG7ASSIdN73LwA1nNWzcMLaoMRJfP3prZFI92svxFwbn7XkLJ+UPQ6A==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/cookie": "^0.6.0",
|
||||||
|
"cookie": "^1.0.1",
|
||||||
|
"set-cookie-parser": "^2.6.0",
|
||||||
|
"turbo-stream": "2.4.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=18",
|
||||||
|
"react-dom": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-router-dom": {
|
||||||
|
"version": "7.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.4.0.tgz",
|
||||||
|
"integrity": "sha512-VlksBPf3n2bijPvnA7nkTsXxMAKOj+bWp4R9c3i+bnwlSOFAGOkJkKhzy/OsRkWaBMICqcAl1JDzh9ZSOze9CA==",
|
||||||
|
"dependencies": {
|
||||||
|
"react-router": "7.4.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=18",
|
||||||
|
"react-dom": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-router/node_modules/cookie": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-style-singleton": {
|
"node_modules/react-style-singleton": {
|
||||||
"version": "2.2.3",
|
"version": "2.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
|
||||||
@@ -6449,6 +6501,11 @@
|
|||||||
"semver": "bin/semver.js"
|
"semver": "bin/semver.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/set-cookie-parser": {
|
||||||
|
"version": "2.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
|
||||||
|
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ=="
|
||||||
|
},
|
||||||
"node_modules/sharp": {
|
"node_modules/sharp": {
|
||||||
"version": "0.33.5",
|
"version": "0.33.5",
|
||||||
"resolved": "https://registry.npmmirror.com/sharp/-/sharp-0.33.5.tgz",
|
"resolved": "https://registry.npmmirror.com/sharp/-/sharp-0.33.5.tgz",
|
||||||
@@ -6918,6 +6975,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
|
||||||
},
|
},
|
||||||
|
"node_modules/turbo-stream": {
|
||||||
|
"version": "2.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz",
|
||||||
|
"integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g=="
|
||||||
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "5.7.3",
|
"version": "5.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-github-btn": "^1.4.0",
|
"react-github-btn": "^1.4.0",
|
||||||
"react-markdown": "^9.0.3",
|
"react-markdown": "^9.0.3",
|
||||||
|
"react-router-dom": "^7.4.0",
|
||||||
"rehype-katex": "^7.0.1",
|
"rehype-katex": "^7.0.1",
|
||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
"remark-math": "^6.0.0",
|
"remark-math": "^6.0.0",
|
||||||
|
|||||||
82
src/App.tsx
82
src/App.tsx
@@ -1,83 +1,11 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import { RouterProvider } from 'react-router-dom';
|
||||||
import ChatUI from './components/ChatUI';
|
import { router } from './routes';
|
||||||
import PhoneLogin from './components/PhoneLogin';
|
|
||||||
import './App.css';
|
|
||||||
import Layout from './components/Layout';
|
|
||||||
|
|
||||||
const App: React.FC = () => {
|
function App() {
|
||||||
const [isLoggedIn, setIsLoggedIn] = useState<boolean>(false);
|
console.log("App rendering"); // 添加日志
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
|
||||||
|
|
||||||
// 在组件加载时检查登录状态
|
|
||||||
useEffect(() => {
|
|
||||||
checkLoginStatus();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// 检查登录状态
|
|
||||||
const checkLoginStatus = async () => {
|
|
||||||
try {
|
|
||||||
// 从 localStorage 获取 token
|
|
||||||
const token = localStorage.getItem('token');
|
|
||||||
if (!token) {
|
|
||||||
setIsLoggedIn(false);
|
|
||||||
setIsLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证 token 有效性
|
|
||||||
const response = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/verify-token`, {
|
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${token}`
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
setIsLoggedIn(true);
|
|
||||||
} else {
|
|
||||||
// token 无效,清除存储的 token
|
|
||||||
localStorage.removeItem('token');
|
|
||||||
setIsLoggedIn(false);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('验证登录状态失败:', error);
|
|
||||||
setIsLoggedIn(false);
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
// 处理登出
|
|
||||||
const handleLogout = () => {
|
|
||||||
localStorage.removeItem('token');
|
|
||||||
setIsLoggedIn(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-gradient-to-br from-orange-50 via-orange-50/70 to-orange-100 flex items-center justify-center">
|
<RouterProvider router={router} />
|
||||||
<div className="w-8 h-8 animate-spin rounded-full border-4 border-orange-500 border-t-transparent"></div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{isLoggedIn ? (
|
|
||||||
<div className="relative">
|
|
||||||
<ChatUI />
|
|
||||||
<button
|
|
||||||
onClick={handleLogout}
|
|
||||||
className="absolute top-4 right-4 px-4 py-2 bg-red-500 text-white rounded-md hover:bg-red-600 transition-colors"
|
|
||||||
>
|
|
||||||
退出登录
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<PhoneLogin/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
16
src/components/AuthGuard.jsx
Normal file
16
src/components/AuthGuard.jsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { Navigate, useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
|
export default function AuthGuard({ children }) {
|
||||||
|
const location = useLocation();
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
|
||||||
|
if (!token && location.pathname !== '/login') {
|
||||||
|
return <Navigate to="/login" state={{ from: location }} replace />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token && location.pathname === '/login') {
|
||||||
|
return <Navigate to="/" replace />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return children;
|
||||||
|
}
|
||||||
22
src/layouts/BasicLayout.tsx
Normal file
22
src/layouts/BasicLayout.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { Outlet, useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
export default function BasicLayout() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
navigate('/login');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="layout">
|
||||||
|
<header className="header">
|
||||||
|
<div className="logo">AI Chat</div>
|
||||||
|
<button onClick={handleLogout}>退出登录</button>
|
||||||
|
</header>
|
||||||
|
<main className="main">
|
||||||
|
<Outlet />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { cn } from '../lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
@@ -17,8 +17,8 @@ import ReactMarkdown from 'react-markdown';
|
|||||||
import remarkGfm from 'remark-gfm'
|
import remarkGfm from 'remark-gfm'
|
||||||
import remarkMath from 'remark-math'
|
import remarkMath from 'remark-math'
|
||||||
import rehypeKatex from 'rehype-katex'
|
import rehypeKatex from 'rehype-katex'
|
||||||
import { SharePoster } from '@/components/SharePoster';
|
import { SharePoster } from '@/pages/chat/components/SharePoster';
|
||||||
import { MembersManagement } from '@/components/MembersManagement';
|
import { MembersManagement } from '@/pages/chat/components/MembersManagement';
|
||||||
import Sidebar from './Sidebar';
|
import Sidebar from './Sidebar';
|
||||||
import { AdBanner, AdBannerMobile } from './AdSection';
|
import { AdBanner, AdBannerMobile } from './AdSection';
|
||||||
// 使用本地头像数据,避免外部依赖
|
// 使用本地头像数据,避免外部依赖
|
||||||
7
src/pages/chat/index.tsx
Normal file
7
src/pages/chat/index.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import ChatUI from './components/ChatUI';
|
||||||
|
|
||||||
|
export default function Chat() {
|
||||||
|
return (
|
||||||
|
<ChatUI />
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import { Input } from "@/components/ui/input";
|
|||||||
interface PhoneLoginProps {
|
interface PhoneLoginProps {
|
||||||
onLogin: (phone: string, code: string) => void;
|
onLogin: (phone: string, code: string) => void;
|
||||||
}
|
}
|
||||||
|
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '';
|
||||||
|
|
||||||
const PhoneLogin: React.FC<PhoneLoginProps> = ({ onLogin }) => {
|
const PhoneLogin: React.FC<PhoneLoginProps> = ({ onLogin }) => {
|
||||||
const [phone, setPhone] = useState('');
|
const [phone, setPhone] = useState('');
|
||||||
@@ -21,7 +22,7 @@ const PhoneLogin: React.FC<PhoneLoginProps> = ({ onLogin }) => {
|
|||||||
|
|
||||||
//setIsLoading(true);
|
//setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/sendcode`, {
|
const response = await fetch(`${API_BASE_URL}/api/sendcode`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -64,7 +65,7 @@ const PhoneLogin: React.FC<PhoneLoginProps> = ({ onLogin }) => {
|
|||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/login`, {
|
const response = await fetch(`${API_BASE_URL}/api/login`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
17
src/pages/login/index.jsx
Normal file
17
src/pages/login/index.jsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import PhoneLogin from './comonents/PhoneLogin';
|
||||||
|
|
||||||
|
export default function Login() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const handleLoginSuccess = (token) => {
|
||||||
|
localStorage.setItem('token', token);
|
||||||
|
navigate('/');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="login-container">
|
||||||
|
<PhoneLogin onSuccess={handleLoginSuccess} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
26
src/routes.tsx
Normal file
26
src/routes.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { createBrowserRouter } from 'react-router-dom';
|
||||||
|
import Login from './pages/login';
|
||||||
|
import Chat from './pages/chat';
|
||||||
|
import BasicLayout from './layouts/BasicLayout';
|
||||||
|
import AuthGuard from './components/AuthGuard';
|
||||||
|
|
||||||
|
export const router = createBrowserRouter([
|
||||||
|
{
|
||||||
|
path: '/login',
|
||||||
|
element: <Login />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
element: (
|
||||||
|
<AuthGuard>
|
||||||
|
<BasicLayout />
|
||||||
|
</AuthGuard>
|
||||||
|
),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
element: <Chat />,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
36
src/utils/request.ts
Normal file
36
src/utils/request.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
export async function request(url: string, options: RequestInit = {}) {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...options.headers,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
headers['Authorization'] = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
...options,
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 如果返回 401,清除 token 并跳转到登录页
|
||||||
|
if (response.status === 401) {
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
window.location.href = '/login';
|
||||||
|
throw new Error('Unauthorized');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Request failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
} catch (error) {
|
||||||
|
// 如果是网络错误或其他错误,也可以处理
|
||||||
|
console.error('Request error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
[observability]
|
|
||||||
enabled = true
|
|
||||||
head_sampling_rate = 1 # optional. default = 1.
|
|
||||||
Reference in New Issue
Block a user