web only use supabase auth

This commit is contained in:
2025-03-10 14:21:58 +08:00
parent 4b5910be83
commit 755fb6ac04
4 changed files with 175 additions and 136 deletions

View File

@@ -3,9 +3,7 @@ import { useNavigate } from 'react-router-dom';
import axios from 'axios'; import axios from 'axios';
import { User } from '../context/AuthContext'; import { User } from '../context/AuthContext';
import { useAuth } from '../context/AuthContext'; import { useAuth } from '../context/AuthContext';
import { Form, Input, Button, Card, Alert, Checkbox } from 'antd'; import supabase from '../utils/supabase';
import { LockOutlined, MailOutlined } from '@ant-design/icons';
import { authApi } from '../utils/api';
interface LoginProps { interface LoginProps {
onLoginSuccess: (token: string, user: User) => void; onLoginSuccess: (token: string, user: User) => void;
@@ -26,7 +24,10 @@ const Login: React.FC<LoginProps> = ({ onLoginSuccess }) => {
console.log('Login - User already authenticated, redirecting to dashboard'); console.log('Login - User already authenticated, redirecting to dashboard');
navigate('/', { replace: true }); navigate('/', { replace: true });
} }
}, [isAuthenticated, navigate]);
// 保存记住我设置
localStorage.setItem('remember_me', rememberMe ? 'true' : 'false');
}, [isAuthenticated, navigate, rememberMe]);
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
@@ -34,23 +35,38 @@ const Login: React.FC<LoginProps> = ({ onLoginSuccess }) => {
setError(''); setError('');
try { try {
const response = await authApi.login({ email, password }); // 使用 Supabase 进行登录
const { data, error } = await supabase.auth.signInWithPassword({
email,
password
});
if (response.data.token) { if (error) {
// 直接设置身份验证状态 throw error;
onLoginSuccess(response.data.token, response.data.user); }
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'); navigate('/dashboard');
} else { } else {
setError('登录失败:未收到有效令牌'); setError('登录失败:未收到有效会话');
} }
} catch (error) { } catch (error) {
console.error('登录失败:', error); console.error('登录失败:', error);
if (axios.isAxiosError(error) && error.response) { if (error instanceof Error) {
setError(`登录失败:${error.response.data.error || '服务器错误'}`); setError(`登录失败:${error.message}`);
} else { } else {
setError('登录失败:网络错误或服务器无响应'); setError('登录失败:未知错误');
} }
} finally { } finally {
setLoading(false); setLoading(false);

View File

@@ -1,5 +1,6 @@
import React, { createContext, useState, useContext, useEffect, ReactNode } from 'react'; import React, { createContext, useState, useContext, useEffect, ReactNode } from 'react';
import { authApi } from '../utils/api'; import { authApi } from '../utils/api';
import supabase from '../utils/supabase';
export interface User { export interface User {
id: string; id: string;
@@ -36,21 +37,27 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
const [loading, setLoading] = useState<boolean>(true); const [loading, setLoading] = useState<boolean>(true);
// 检查是否有保存的登录状态 // 检查是否有保存的登录状态
const loadSavedSession = () => { const loadSavedSession = async () => {
try { try {
const savedAuth = localStorage.getItem('auth_state'); // 从 Supabase 获取当前会话
if (savedAuth) { const { data: { session }, error } = await supabase.auth.getSession();
const { user: savedUser, timestamp } = JSON.parse(savedAuth);
// 检查保存的状态是否在一周内 if (error) {
const now = Date.now(); console.error('Failed to load Supabase session:', error);
const validDuration = 7 * 24 * 60 * 60 * 1000; // 一周 return false;
if (now - timestamp < validDuration) { }
setUser(savedUser);
setIsAuthenticated(true); if (session) {
// 设置用户信息
// 如果认证状态有效但我们仍需验证token const userData: User = {
return true; id: session.user.id,
} email: session.user.email || '',
name: session.user.user_metadata?.name
};
setUser(userData);
setIsAuthenticated(true);
return true;
} }
} catch (error) { } catch (error) {
console.error('Failed to load saved authentication state:', 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 () => { const initAuth = async () => {
console.log('Checking authentication status...'); console.log('Checking authentication status...');
// 尝试加载保存的会话 // 尝试加载 Supabase 会话
const hasSavedSession = loadSavedSession(); const hasSavedSession = await loadSavedSession();
// 即使有保存的会话仍然需要验证token // 如果没有有效会话,确保用户未认证
const isValid = await checkAuth(); if (!hasSavedSession) {
// 如果验证失败,清除保存的会话
if (!isValid && hasSavedSession) {
setIsAuthenticated(false); setIsAuthenticated(false);
setUser(null); setUser(null);
localStorage.removeItem('auth_state');
} }
setLoading(false); setLoading(false);
@@ -94,116 +97,101 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
initAuth(); initAuth();
// 设置定时刷新token // 设置 Supabase 认证状态变化监听
const refreshInterval = setInterval(() => { const { data: { subscription } } = supabase.auth.onAuthStateChange(
// 检查是否已认证 async (event, session) => {
if (isAuthenticated) { console.log('Supabase auth state changed:', event);
// 静默尝试刷新token
refreshToken(); 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 () => { const refreshToken = async () => {
try { // Supabase 会自动处理 token 刷新,不需要手动实现
// 检查是否启用了"记住我"
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调用来决定
}
}; };
// Check if the token is valid // Check if the token is valid
const checkAuth = async (): Promise<boolean> => { const checkAuth = async (): Promise<boolean> => {
// 简单检查本地存储中是否有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 { try {
// 直接从localStorage恢复会话状态不设置token状态没有找到setToken函数 // 从 Supabase 获取当前会话
setUser(JSON.parse(savedUser)); 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); setIsAuthenticated(true);
setLoading(false);
return true; return true;
} catch (error) { } catch (error) {
console.error('Error parsing stored user data:', error); console.error('Error checking auth:', error);
setIsAuthenticated(false); setIsAuthenticated(false);
setUser(null); setUser(null);
setLoading(false);
return false; return false;
} finally {
setLoading(false);
} }
}; };
// Login function // Login function - 使用 Supabase 登录
const login = (token: string, userData: User) => { const login = async (token: string, userData: User) => {
console.log('Logging in user, setting isAuthenticated=true', userData); 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); setUser(userData);
setIsAuthenticated(true); setIsAuthenticated(true);
// 保存认证状态
saveAuthState(userData);
}; };
// Logout function // Logout function - 使用 Supabase 登出
const logout = () => { const logout = async () => {
console.log('Logging out user'); console.log('Logging out user');
// 清除localStorage中的认证数据 // 调用 Supabase 登出
localStorage.removeItem('auth_token'); const { error } = await supabase.auth.signOut();
localStorage.removeItem('auth_state');
// 保留remember_me设置以便下次登录时使用相同设置
// 清除sessionStorage中的认证数据 if (error) {
sessionStorage.removeItem('auth_token'); console.error('Error signing out:', error);
sessionStorage.removeItem('user'); }
// 清除本地状态
setUser(null); setUser(null);
setIsAuthenticated(false); setIsAuthenticated(false);
localStorage.removeItem('auth_state');
}; };
return ( return (

View File

@@ -1,4 +1,5 @@
import axios, { AxiosInstance, AxiosResponse } from 'axios'; import axios, { AxiosInstance, AxiosResponse } from 'axios';
import supabase from './supabase';
// Type definitions // Type definitions
interface LoginCredentials { interface LoginCredentials {
@@ -27,10 +28,13 @@ const apiClient: AxiosInstance = axios.create({
// Request interceptor for adding auth token // Request interceptor for adding auth token
apiClient.interceptors.request.use( apiClient.interceptors.request.use(
(config) => { async (config) => {
const token = localStorage.getItem('auth_token'); // 从 Supabase 获取当前会话
if (token) { const { data } = await supabase.auth.getSession();
config.headers.Authorization = `Bearer ${token}`; const session = data.session;
if (session) {
config.headers.Authorization = `Bearer ${session.access_token}`;
} }
return config; return config;
}, },
@@ -44,18 +48,32 @@ apiClient.interceptors.response.use(
(response) => { (response) => {
return response; return response;
}, },
(error) => { async (error) => {
// Handle errors globally // Handle errors globally
if (error.response) { if (error.response) {
// Server responded with error status (4xx, 5xx) // Server responded with error status (4xx, 5xx)
if (error.response.status === 401) { if (error.response.status === 401) {
// Unauthorized - clear local storage // Unauthorized - 可能是 token 过期,尝试刷新
localStorage.removeItem('auth_token'); try {
localStorage.removeItem('user'); const { data, error: refreshError } = await supabase.auth.refreshSession();
// Redirect to login page if not already there if (refreshError || !data.session) {
if (window.location.pathname !== '/login') { // 刷新失败,重定向到登录页面
window.location.href = '/login'; 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 = { export const authApi = {
login: (credentials: LoginCredentials): Promise<AxiosResponse<LoginResponse>> => // 保留 verify 方法用于与后端验证
apiClient.post('/api/auth/login', credentials), verify: async (): Promise<AxiosResponse> => {
verify: (headers?: Record<string, string>): Promise<AxiosResponse> => const { data } = await supabase.auth.getSession();
apiClient.get('/api/auth/verify', headers ? { headers } : undefined), const session = data.session;
register: (data: { email: string; password: string; name: string }): Promise<AxiosResponse> =>
apiClient.post('/api/auth/register', data), if (!session) {
refreshToken: (): Promise<AxiosResponse<{token: string}>> => throw new Error('No active session');
apiClient.post('/api/auth/refresh-token'), }
return apiClient.get('/api/auth/verify', {
headers: {
Authorization: `Bearer ${session.access_token}`
}
});
}
}; };
// Comments API // Comments API

10
web/src/utils/supabase.ts Normal file
View File

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