update loyout
This commit is contained in:
@@ -1,8 +1,4 @@
|
||||
interface Env {
|
||||
JWT_SECRET: string;
|
||||
}
|
||||
|
||||
export async function verifyToken(token: string, env: Env) {
|
||||
async function verifyToken(token, env) {
|
||||
try {
|
||||
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) => {
|
||||
const authHeader = request.headers.get('Authorization');
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
throw new Error('No token provided');
|
||||
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');
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
throw new Error('No token provided');
|
||||
}
|
||||
|
||||
const token = authHeader.split(' ')[1];
|
||||
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'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const token = authHeader.split(' ')[1];
|
||||
return await verifyToken(token, env);
|
||||
};
|
||||
}
|
||||
62
package-lock.json
generated
62
package-lock.json
generated
@@ -30,6 +30,7 @@
|
||||
"react-dom": "^18.2.0",
|
||||
"react-github-btn": "^1.4.0",
|
||||
"react-markdown": "^9.0.3",
|
||||
"react-router-dom": "^7.4.0",
|
||||
"rehype-katex": "^7.0.1",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"remark-math": "^6.0.0",
|
||||
@@ -2405,6 +2406,11 @@
|
||||
"@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": {
|
||||
"version": "4.1.12",
|
||||
"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": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
|
||||
@@ -6449,6 +6501,11 @@
|
||||
"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": {
|
||||
"version": "0.33.5",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "5.7.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
"react-dom": "^18.2.0",
|
||||
"react-github-btn": "^1.4.0",
|
||||
"react-markdown": "^9.0.3",
|
||||
"react-router-dom": "^7.4.0",
|
||||
"rehype-katex": "^7.0.1",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"remark-math": "^6.0.0",
|
||||
|
||||
86
src/App.tsx
86
src/App.tsx
@@ -1,83 +1,11 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import ChatUI from './components/ChatUI';
|
||||
import PhoneLogin from './components/PhoneLogin';
|
||||
import './App.css';
|
||||
import Layout from './components/Layout';
|
||||
|
||||
const App: React.FC = () => {
|
||||
const [isLoggedIn, setIsLoggedIn] = useState<boolean>(false);
|
||||
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 (
|
||||
<div className="fixed inset-0 bg-gradient-to-br from-orange-50 via-orange-50/70 to-orange-100 flex items-center justify-center">
|
||||
<div className="w-8 h-8 animate-spin rounded-full border-4 border-orange-500 border-t-transparent"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
import { RouterProvider } from 'react-router-dom';
|
||||
import { router } from './routes';
|
||||
|
||||
function App() {
|
||||
console.log("App rendering"); // 添加日志
|
||||
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/>
|
||||
)}
|
||||
</>
|
||||
<RouterProvider router={router} />
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
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 { cn } from '../lib/utils';
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
@@ -17,8 +17,8 @@ import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import remarkMath from 'remark-math'
|
||||
import rehypeKatex from 'rehype-katex'
|
||||
import { SharePoster } from '@/components/SharePoster';
|
||||
import { MembersManagement } from '@/components/MembersManagement';
|
||||
import { SharePoster } from '@/pages/chat/components/SharePoster';
|
||||
import { MembersManagement } from '@/pages/chat/components/MembersManagement';
|
||||
import Sidebar from './Sidebar';
|
||||
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 {
|
||||
onLogin: (phone: string, code: string) => void;
|
||||
}
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '';
|
||||
|
||||
const PhoneLogin: React.FC<PhoneLoginProps> = ({ onLogin }) => {
|
||||
const [phone, setPhone] = useState('');
|
||||
@@ -21,7 +22,7 @@ const PhoneLogin: React.FC<PhoneLoginProps> = ({ onLogin }) => {
|
||||
|
||||
//setIsLoading(true);
|
||||
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',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -64,7 +65,7 @@ const PhoneLogin: React.FC<PhoneLoginProps> = ({ onLogin }) => {
|
||||
|
||||
setIsLoading(true);
|
||||
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',
|
||||
headers: {
|
||||
'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