web only use supabase auth
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
|
||||||
// 检查保存的状态是否在一周内
|
|
||||||
const now = Date.now();
|
|
||||||
const validDuration = 7 * 24 * 60 * 60 * 1000; // 一周
|
|
||||||
if (now - timestamp < validDuration) {
|
|
||||||
setUser(savedUser);
|
|
||||||
setIsAuthenticated(true);
|
|
||||||
|
|
||||||
// 如果认证状态有效,但我们仍需验证token
|
if (error) {
|
||||||
return true;
|
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) {
|
} 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();
|
||||||
setIsAuthenticated(true);
|
|
||||||
setLoading(false);
|
if (error || !session) {
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error parsing stored user data:', error);
|
|
||||||
setIsAuthenticated(false);
|
setIsAuthenticated(false);
|
||||||
setUser(null);
|
setUser(null);
|
||||||
setLoading(false);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 设置用户信息
|
||||||
|
const userData: User = {
|
||||||
|
id: session.user.id,
|
||||||
|
email: session.user.email || '',
|
||||||
|
name: session.user.user_metadata?.name
|
||||||
};
|
};
|
||||||
|
|
||||||
// Login function
|
|
||||||
const login = (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);
|
setUser(userData);
|
||||||
setIsAuthenticated(true);
|
setIsAuthenticated(true);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking auth:', error);
|
||||||
|
setIsAuthenticated(false);
|
||||||
|
setUser(null);
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Logout function
|
// Login function - 使用 Supabase 登录
|
||||||
const logout = () => {
|
const login = async (token: string, userData: User) => {
|
||||||
|
console.log('Logging in user, setting isAuthenticated=true', userData);
|
||||||
|
|
||||||
|
// 保存用户信息
|
||||||
|
setUser(userData);
|
||||||
|
setIsAuthenticated(true);
|
||||||
|
|
||||||
|
// 保存认证状态
|
||||||
|
saveAuthState(userData);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Logout function - 使用 Supabase 登出
|
||||||
|
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 (
|
||||||
|
|||||||
@@ -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,35 +48,56 @@ 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') {
|
if (window.location.pathname !== '/login') {
|
||||||
window.location.href = '/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';
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// 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
10
web/src/utils/supabase.ts
Normal 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;
|
||||||
Reference in New Issue
Block a user