update loyout

This commit is contained in:
hobby
2025-03-25 08:25:40 +08:00
parent 6c495ac631
commit 487621a645
19 changed files with 229 additions and 100 deletions

View File

@@ -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
View File

@@ -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",

View File

@@ -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",

View File

@@ -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;

View 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;
}

View 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>
);
}

View File

@@ -1,5 +1,5 @@
import React, { useState } from 'react';
import { cn } from '../lib/utils';
import { cn } from '@/lib/utils';
import {
Popover,
PopoverContent,

View File

@@ -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
View File

@@ -0,0 +1,7 @@
import ChatUI from './components/ChatUI';
export default function Chat() {
return (
<ChatUI />
);
}

View File

@@ -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
View 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
View 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
View 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;
}
}

View File

@@ -1,3 +0,0 @@
[observability]
enabled = true
head_sampling_rate = 1 # optional. default = 1.