fix(auth): 修复登录持久化和路由重定向问题
- 注册成功后直接跳转首页,无需重新登录 - 优化useAuthLoader使用useRef避免闪烁 - 统一错误处理格式 - 修复HTML标签嵌套错误 - 添加XSS防护(rehype-sanitize) - 修复API credentials配置 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
ccfa763657
commit
a118346238
@ -18,7 +18,7 @@ export function LoginPage() {
|
||||
|
||||
const { error } = await login(email, password);
|
||||
if (error) {
|
||||
setError(error.message || '登录失败');
|
||||
setError(error);
|
||||
} else {
|
||||
navigate('/');
|
||||
}
|
||||
|
||||
@ -12,16 +12,33 @@ export function RegisterPage() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
// 密码强度验证
|
||||
const passwordRequirements = [
|
||||
{ label: '至少8个字符', met: password.length >= 8 },
|
||||
{ label: '包含大写字母', met: /[A-Z]/.test(password) },
|
||||
{ label: '包含小写字母', met: /[a-z]/.test(password) },
|
||||
{ label: '包含数字', met: /\d/.test(password) },
|
||||
];
|
||||
|
||||
const meetsAllRequirements = passwordRequirements.every((req) => req.met);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setError('');
|
||||
|
||||
if (!meetsAllRequirements) {
|
||||
setError('密码不符合要求,请检查密码格式');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const { error } = await register(email, password, nickname);
|
||||
if (error) {
|
||||
setError(error.message || '注册失败');
|
||||
setError(error);
|
||||
} else {
|
||||
navigate('/login');
|
||||
// 注册成功后直接跳转到首页(已登录状态)
|
||||
navigate('/');
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
@ -48,6 +65,7 @@ export function RegisterPage() {
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<div>
|
||||
<TextInput
|
||||
label="密码"
|
||||
type="password"
|
||||
@ -56,8 +74,22 @@ export function RegisterPage() {
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
{/* 密码要求提示 */}
|
||||
<Stack gap={4} mt={6}>
|
||||
{passwordRequirements.map((req, index) => (
|
||||
<Text
|
||||
key={index}
|
||||
size="xs"
|
||||
c={req.met ? 'green' : 'dimmed'}
|
||||
style={{ display: 'flex', alignItems: 'center', gap: 4 }}
|
||||
>
|
||||
{req.met ? '✓' : '○'} {req.label}
|
||||
</Text>
|
||||
))}
|
||||
</Stack>
|
||||
</div>
|
||||
{error && <Text c="red" size="sm">{error}</Text>}
|
||||
<Button type="submit" loading={loading} fullWidth>
|
||||
<Button type="submit" loading={loading} fullWidth disabled={!meetsAllRequirements && password.length > 0}>
|
||||
注册
|
||||
</Button>
|
||||
</Stack>
|
||||
|
||||
@ -1,23 +1,76 @@
|
||||
import { createBrowserRouter } from 'react-router-dom';
|
||||
import { createBrowserRouter, Navigate } from 'react-router-dom';
|
||||
import { LandingPage } from './pages/LandingPage';
|
||||
import { LoginPage } from './pages/LoginPage';
|
||||
import { RegisterPage } from './pages/RegisterPage';
|
||||
import { HomePage } from './pages/HomePage';
|
||||
import { useAppStore } from './stores';
|
||||
import { useEffect } from 'react';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
|
||||
// Protected Route wrapper
|
||||
function ProtectedRoute({ children }: { children: React.ReactNode }) {
|
||||
// Loading spinner component
|
||||
function AuthLoading() {
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
height: '100vh',
|
||||
background: 'linear-gradient(135deg, #4A90D9 0%, #7AB8F5 100%)',
|
||||
}}>
|
||||
<div style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
border: '3px solid rgba(255,255,255,0.3)',
|
||||
borderTopColor: 'white',
|
||||
borderRadius: '50%',
|
||||
animation: 'spin 1s linear infinite',
|
||||
}} />
|
||||
<style>{`
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Auth loader - handles checkAuth and loading state
|
||||
function useAuthLoader() {
|
||||
const isAuthenticated = useAppStore((state) => state.isAuthenticated);
|
||||
const checkAuth = useAppStore((state) => state.checkAuth);
|
||||
const isLoading = useAppStore((state) => state.isLoading);
|
||||
|
||||
// 使用 ref 跟踪是否已检查过,避免重复检查
|
||||
const checkedRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
// 标记已检查,但只在未登录时调用 checkAuth
|
||||
if (!checkedRef.current) {
|
||||
checkedRef.current = true;
|
||||
// 如果未登录,才需要检查
|
||||
if (!isAuthenticated) {
|
||||
checkAuth();
|
||||
}, [checkAuth]);
|
||||
}
|
||||
}
|
||||
}, [checkAuth, isAuthenticated]);
|
||||
|
||||
// 如果已登录,直接返回,不再显示 Loading
|
||||
if (isAuthenticated) {
|
||||
return { isAuthenticated: true, isLoading: false };
|
||||
}
|
||||
|
||||
return { isAuthenticated: false, isLoading };
|
||||
}
|
||||
|
||||
// Protected Route wrapper
|
||||
function ProtectedRoute({ children }: { children: React.ReactNode }) {
|
||||
const { isAuthenticated, isLoading } = useAuthLoader();
|
||||
|
||||
if (isLoading) {
|
||||
return null; // Or a loading spinner
|
||||
return <AuthLoading />;
|
||||
}
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return <Navigate to="/login" replace />;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
@ -25,15 +78,14 @@ function ProtectedRoute({ children }: { children: React.ReactNode }) {
|
||||
|
||||
// Public Route wrapper (redirects to home if authenticated)
|
||||
function PublicRoute({ children }: { children: React.ReactNode }) {
|
||||
const isAuthenticated = useAppStore((state) => state.isAuthenticated);
|
||||
const checkAuth = useAppStore((state) => state.checkAuth);
|
||||
const { isAuthenticated, isLoading } = useAuthLoader();
|
||||
|
||||
useEffect(() => {
|
||||
checkAuth();
|
||||
}, [checkAuth]);
|
||||
if (isLoading) {
|
||||
return <AuthLoading />;
|
||||
}
|
||||
|
||||
if (isAuthenticated) {
|
||||
return <HomePage />;
|
||||
return <Navigate to="/home" replace />;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
@ -42,6 +94,10 @@ function PublicRoute({ children }: { children: React.ReactNode }) {
|
||||
export const router = createBrowserRouter([
|
||||
{
|
||||
path: '/',
|
||||
element: <Navigate to="/home" replace />,
|
||||
},
|
||||
{
|
||||
path: '/landing',
|
||||
element: (
|
||||
<PublicRoute>
|
||||
<LandingPage />
|
||||
|
||||
@ -93,7 +93,7 @@ export const useAppStore = create<AppState>()(
|
||||
|
||||
saveNotes: async (content) => {
|
||||
try {
|
||||
const notes = await api.notes.save(content);
|
||||
const notes = await api.notes.update(content);
|
||||
set({ notes });
|
||||
} catch (error) {
|
||||
console.error('Failed to save notes:', error);
|
||||
@ -143,7 +143,9 @@ export const useAppStore = create<AppState>()(
|
||||
set({ user, isAuthenticated: true });
|
||||
return { error: null };
|
||||
} catch (error: any) {
|
||||
return { error: error.message || '登录失败' };
|
||||
// 确保返回字符串错误信息
|
||||
const errorMessage = error.message || '登录失败,请检查邮箱和密码';
|
||||
return { error: errorMessage };
|
||||
}
|
||||
},
|
||||
|
||||
@ -154,7 +156,9 @@ export const useAppStore = create<AppState>()(
|
||||
set({ user, isAuthenticated: true });
|
||||
return { error: null };
|
||||
} catch (error: any) {
|
||||
return { error: error.message || '注册失败' };
|
||||
// 确保返回字符串错误信息
|
||||
const errorMessage = error.message || '注册失败,请稍后重试';
|
||||
return { error: errorMessage };
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user