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 { 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<LoginProps> = ({ 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<LoginProps> = ({ 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);

View File

@@ -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<boolean>(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<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 {
// 直接从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 (

View File

@@ -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<AxiosResponse<LoginResponse>> =>
apiClient.post('/api/auth/login', credentials),
verify: (headers?: Record<string, string>): Promise<AxiosResponse> =>
apiClient.get('/api/auth/verify', headers ? { headers } : undefined),
register: (data: { email: string; password: string; name: string }): Promise<AxiosResponse> =>
apiClient.post('/api/auth/register', data),
refreshToken: (): Promise<AxiosResponse<{token: string}>> =>
apiClient.post('/api/auth/refresh-token'),
// 保留 verify 方法用于与后端验证
verify: async (): Promise<AxiosResponse> => {
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

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;