diff --git a/web/src/components/Login.tsx b/web/src/components/Login.tsx index 0054294..86162b8 100644 --- a/web/src/components/Login.tsx +++ b/web/src/components/Login.tsx @@ -3,9 +3,7 @@ import { useNavigate } from 'react-router-dom'; import axios from 'axios'; import { User } from '../context/AuthContext'; import { useAuth } from '../context/AuthContext'; -import { Form, Input, Button, Card, Alert, Checkbox } from 'antd'; -import { LockOutlined, MailOutlined } from '@ant-design/icons'; -import { authApi } from '../utils/api'; +import supabase from '../utils/supabase'; interface LoginProps { onLoginSuccess: (token: string, user: User) => void; @@ -26,7 +24,10 @@ const Login: React.FC = ({ onLoginSuccess }) => { console.log('Login - User already authenticated, redirecting to dashboard'); navigate('/', { replace: true }); } - }, [isAuthenticated, navigate]); + + // 保存记住我设置 + localStorage.setItem('remember_me', rememberMe ? 'true' : 'false'); + }, [isAuthenticated, navigate, rememberMe]); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -34,23 +35,38 @@ const Login: React.FC = ({ onLoginSuccess }) => { setError(''); try { - const response = await authApi.login({ email, password }); + // 使用 Supabase 进行登录 + const { data, error } = await supabase.auth.signInWithPassword({ + email, + password + }); - if (response.data.token) { - // 直接设置身份验证状态 - onLoginSuccess(response.data.token, response.data.user); + if (error) { + throw error; + } + + if (data.session) { + // 创建用户对象 + const userData: User = { + id: data.user.id, + email: data.user.email || '', + name: data.user.user_metadata?.name + }; - // 直接导航到仪表板,不做任何额外的检查或延迟 + // 调用登录成功回调 + onLoginSuccess(data.session.access_token, userData); + + // 导航到仪表板 navigate('/dashboard'); } else { - setError('登录失败:未收到有效令牌'); + setError('登录失败:未收到有效会话'); } } catch (error) { console.error('登录失败:', error); - if (axios.isAxiosError(error) && error.response) { - setError(`登录失败:${error.response.data.error || '服务器错误'}`); + if (error instanceof Error) { + setError(`登录失败:${error.message}`); } else { - setError('登录失败:网络错误或服务器无响应'); + setError('登录失败:未知错误'); } } finally { setLoading(false); diff --git a/web/src/context/AuthContext.tsx b/web/src/context/AuthContext.tsx index 7f49019..9571fb0 100644 --- a/web/src/context/AuthContext.tsx +++ b/web/src/context/AuthContext.tsx @@ -1,5 +1,6 @@ import React, { createContext, useState, useContext, useEffect, ReactNode } from 'react'; import { authApi } from '../utils/api'; +import supabase from '../utils/supabase'; export interface User { id: string; @@ -36,21 +37,27 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => const [loading, setLoading] = useState(true); // 检查是否有保存的登录状态 - const loadSavedSession = () => { + const loadSavedSession = async () => { try { - const savedAuth = localStorage.getItem('auth_state'); - if (savedAuth) { - const { user: savedUser, timestamp } = JSON.parse(savedAuth); - // 检查保存的状态是否在一周内 - const now = Date.now(); - const validDuration = 7 * 24 * 60 * 60 * 1000; // 一周 - if (now - timestamp < validDuration) { - setUser(savedUser); - setIsAuthenticated(true); - - // 如果认证状态有效,但我们仍需验证token - return true; - } + // 从 Supabase 获取当前会话 + const { data: { session }, error } = await supabase.auth.getSession(); + + if (error) { + console.error('Failed to load Supabase session:', error); + return false; + } + + if (session) { + // 设置用户信息 + const userData: User = { + id: session.user.id, + email: session.user.email || '', + name: session.user.user_metadata?.name + }; + + setUser(userData); + setIsAuthenticated(true); + return true; } } catch (error) { console.error('Failed to load saved authentication state:', error); @@ -76,17 +83,13 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => const initAuth = async () => { console.log('Checking authentication status...'); - // 先尝试加载保存的会话 - const hasSavedSession = loadSavedSession(); + // 尝试加载 Supabase 会话 + const hasSavedSession = await loadSavedSession(); - // 即使有保存的会话,仍然需要验证token - const isValid = await checkAuth(); - - // 如果验证失败,清除保存的会话 - if (!isValid && hasSavedSession) { + // 如果没有有效会话,确保用户未认证 + if (!hasSavedSession) { setIsAuthenticated(false); setUser(null); - localStorage.removeItem('auth_state'); } setLoading(false); @@ -94,116 +97,101 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => initAuth(); - // 设置定时刷新token - const refreshInterval = setInterval(() => { - // 检查是否已认证 - if (isAuthenticated) { - // 静默尝试刷新token - refreshToken(); + // 设置 Supabase 认证状态变化监听 + const { data: { subscription } } = supabase.auth.onAuthStateChange( + async (event, session) => { + console.log('Supabase auth state changed:', event); + + if (event === 'SIGNED_IN' && session) { + // 用户登录 + const userData: User = { + id: session.user.id, + email: session.user.email || '', + name: session.user.user_metadata?.name + }; + + setUser(userData); + setIsAuthenticated(true); + saveAuthState(userData); + } else if (event === 'SIGNED_OUT') { + // 用户登出 + setUser(null); + setIsAuthenticated(false); + localStorage.removeItem('auth_state'); + } } - }, REFRESH_THRESHOLD); + ); - return () => clearInterval(refreshInterval); - }, [isAuthenticated]); + // 清理订阅 + return () => { + subscription.unsubscribe(); + }; + }, []); - // 尝试刷新token + // 尝试刷新token - 使用 Supabase 自动处理 const refreshToken = async () => { - try { - // 检查是否启用了"记住我" - const rememberMe = localStorage.getItem('remember_me'); - - // 如果没有启用"记住我",则使用会话存储 - if (rememberMe !== 'true') { - // 短期会话不需要刷新token - return; - } - - const token = localStorage.getItem('auth_token'); - if (!token) return; - - // 调用刷新token的API - const response = await authApi.refreshToken(); - - if (response.data.token) { - // 更新token - localStorage.setItem('auth_token', response.data.token); - console.log('Token refreshed successfully'); - } - } catch (error) { - console.error('Failed to refresh token:', error); - // 如果刷新失败,不要强制登出用户,让下一次API调用来决定 - } + // Supabase 会自动处理 token 刷新,不需要手动实现 }; // Check if the token is valid const checkAuth = async (): Promise => { - // 简单检查本地存储中是否有token,不再发送API请求 - const savedToken = localStorage.getItem('auth_token'); - const savedUser = localStorage.getItem('user'); - - if (!savedToken || !savedUser) { - setIsAuthenticated(false); - setUser(null); - setLoading(false); - return false; - } - try { - // 直接从localStorage恢复会话状态,不设置token状态(没有找到setToken函数) - setUser(JSON.parse(savedUser)); + // 从 Supabase 获取当前会话 + const { data: { session }, error } = await supabase.auth.getSession(); + + if (error || !session) { + setIsAuthenticated(false); + setUser(null); + return false; + } + + // 设置用户信息 + const userData: User = { + id: session.user.id, + email: session.user.email || '', + name: session.user.user_metadata?.name + }; + + setUser(userData); setIsAuthenticated(true); - setLoading(false); return true; } catch (error) { - console.error('Error parsing stored user data:', error); + console.error('Error checking auth:', error); setIsAuthenticated(false); setUser(null); - setLoading(false); return false; + } finally { + setLoading(false); } }; - // Login function - const login = (token: string, userData: User) => { + // Login function - 使用 Supabase 登录 + const login = async (token: string, userData: User) => { console.log('Logging in user, setting isAuthenticated=true', userData); - // 检查是否需要记住登录状态 - const rememberMe = localStorage.getItem('remember_me'); - console.log('Remember me setting:', rememberMe); - - if (rememberMe === 'true') { - // 长期存储在localStorage - localStorage.setItem('auth_token', token); - // 保存认证状态 - saveAuthState(userData); - console.log('Saved auth state to localStorage (long term)'); - } else { - // 短期存储在sessionStorage(浏览器关闭后清除) - sessionStorage.setItem('auth_token', token); - sessionStorage.setItem('user', JSON.stringify(userData)); - console.log('Saved auth state to sessionStorage (session only)'); - } - - // 先设置用户信息,再设置认证状态(顺序很重要) + // 保存用户信息 setUser(userData); setIsAuthenticated(true); + + // 保存认证状态 + saveAuthState(userData); }; - // Logout function - const logout = () => { + // Logout function - 使用 Supabase 登出 + const logout = async () => { console.log('Logging out user'); - // 清除localStorage中的认证数据 - localStorage.removeItem('auth_token'); - localStorage.removeItem('auth_state'); - // 保留remember_me设置,以便下次登录时使用相同设置 + // 调用 Supabase 登出 + const { error } = await supabase.auth.signOut(); - // 清除sessionStorage中的认证数据 - sessionStorage.removeItem('auth_token'); - sessionStorage.removeItem('user'); + if (error) { + console.error('Error signing out:', error); + } + // 清除本地状态 setUser(null); setIsAuthenticated(false); + localStorage.removeItem('auth_state'); }; return ( diff --git a/web/src/utils/api.ts b/web/src/utils/api.ts index 44beffa..4c5c054 100644 --- a/web/src/utils/api.ts +++ b/web/src/utils/api.ts @@ -1,4 +1,5 @@ import axios, { AxiosInstance, AxiosResponse } from 'axios'; +import supabase from './supabase'; // Type definitions interface LoginCredentials { @@ -27,10 +28,13 @@ const apiClient: AxiosInstance = axios.create({ // Request interceptor for adding auth token apiClient.interceptors.request.use( - (config) => { - const token = localStorage.getItem('auth_token'); - if (token) { - config.headers.Authorization = `Bearer ${token}`; + async (config) => { + // 从 Supabase 获取当前会话 + const { data } = await supabase.auth.getSession(); + const session = data.session; + + if (session) { + config.headers.Authorization = `Bearer ${session.access_token}`; } return config; }, @@ -44,18 +48,32 @@ apiClient.interceptors.response.use( (response) => { return response; }, - (error) => { + async (error) => { // Handle errors globally if (error.response) { // Server responded with error status (4xx, 5xx) if (error.response.status === 401) { - // Unauthorized - clear local storage - localStorage.removeItem('auth_token'); - localStorage.removeItem('user'); - - // Redirect to login page if not already there - if (window.location.pathname !== '/login') { - window.location.href = '/login'; + // Unauthorized - 可能是 token 过期,尝试刷新 + try { + const { data, error: refreshError } = await supabase.auth.refreshSession(); + + if (refreshError || !data.session) { + // 刷新失败,重定向到登录页面 + if (window.location.pathname !== '/login') { + window.location.href = '/login'; + } + } else { + // 刷新成功,重试请求 + const originalRequest = error.config; + originalRequest.headers.Authorization = `Bearer ${data.session.access_token}`; + return axios(originalRequest); + } + } catch (refreshError) { + console.error('Failed to refresh token:', refreshError); + // 重定向到登录页面 + if (window.location.pathname !== '/login') { + window.location.href = '/login'; + } } } } @@ -63,16 +81,23 @@ apiClient.interceptors.response.use( } ); -// Auth API +// Auth API - 不再需要大部分方法,因为现在直接使用 Supabase export const authApi = { - login: (credentials: LoginCredentials): Promise> => - apiClient.post('/api/auth/login', credentials), - verify: (headers?: Record): Promise => - apiClient.get('/api/auth/verify', headers ? { headers } : undefined), - register: (data: { email: string; password: string; name: string }): Promise => - apiClient.post('/api/auth/register', data), - refreshToken: (): Promise> => - apiClient.post('/api/auth/refresh-token'), + // 保留 verify 方法用于与后端验证 + verify: async (): Promise => { + const { data } = await supabase.auth.getSession(); + const session = data.session; + + if (!session) { + throw new Error('No active session'); + } + + return apiClient.get('/api/auth/verify', { + headers: { + Authorization: `Bearer ${session.access_token}` + } + }); + } }; // Comments API diff --git a/web/src/utils/supabase.ts b/web/src/utils/supabase.ts new file mode 100644 index 0000000..1dd7dbc --- /dev/null +++ b/web/src/utils/supabase.ts @@ -0,0 +1,10 @@ +import { createClient } from '@supabase/supabase-js'; + +// 使用环境变量或直接使用 URL 和 Key(生产环境中应使用环境变量) +const supabaseUrl = import.meta.env.VITE_SUPABASE_URL || 'http://your-supabase-url'; +const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY || 'your-supabase-anon-key'; + +// 创建 Supabase 客户端 +const supabase = createClient(supabaseUrl, supabaseAnonKey); + +export default supabase; \ No newline at end of file