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);
|
const { error } = await login(email, password);
|
||||||
if (error) {
|
if (error) {
|
||||||
setError(error.message || '登录失败');
|
setError(error);
|
||||||
} else {
|
} else {
|
||||||
navigate('/');
|
navigate('/');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,16 +12,33 @@ export function RegisterPage() {
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState('');
|
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) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError('');
|
setError('');
|
||||||
|
|
||||||
|
if (!meetsAllRequirements) {
|
||||||
|
setError('密码不符合要求,请检查密码格式');
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const { error } = await register(email, password, nickname);
|
const { error } = await register(email, password, nickname);
|
||||||
if (error) {
|
if (error) {
|
||||||
setError(error.message || '注册失败');
|
setError(error);
|
||||||
} else {
|
} else {
|
||||||
navigate('/login');
|
// 注册成功后直接跳转到首页(已登录状态)
|
||||||
|
navigate('/');
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
@ -48,6 +65,7 @@ export function RegisterPage() {
|
|||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
<div>
|
||||||
<TextInput
|
<TextInput
|
||||||
label="密码"
|
label="密码"
|
||||||
type="password"
|
type="password"
|
||||||
@ -56,8 +74,22 @@ export function RegisterPage() {
|
|||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
required
|
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>}
|
{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>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@ -1,23 +1,76 @@
|
|||||||
import { createBrowserRouter } from 'react-router-dom';
|
import { createBrowserRouter, Navigate } from 'react-router-dom';
|
||||||
import { LandingPage } from './pages/LandingPage';
|
import { LandingPage } from './pages/LandingPage';
|
||||||
import { LoginPage } from './pages/LoginPage';
|
import { LoginPage } from './pages/LoginPage';
|
||||||
import { RegisterPage } from './pages/RegisterPage';
|
import { RegisterPage } from './pages/RegisterPage';
|
||||||
import { HomePage } from './pages/HomePage';
|
import { HomePage } from './pages/HomePage';
|
||||||
import { useAppStore } from './stores';
|
import { useAppStore } from './stores';
|
||||||
import { useEffect } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
|
|
||||||
// Protected Route wrapper
|
// Loading spinner component
|
||||||
function ProtectedRoute({ children }: { children: React.ReactNode }) {
|
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 isAuthenticated = useAppStore((state) => state.isAuthenticated);
|
||||||
const checkAuth = useAppStore((state) => state.checkAuth);
|
const checkAuth = useAppStore((state) => state.checkAuth);
|
||||||
const isLoading = useAppStore((state) => state.isLoading);
|
const isLoading = useAppStore((state) => state.isLoading);
|
||||||
|
|
||||||
|
// 使用 ref 跟踪是否已检查过,避免重复检查
|
||||||
|
const checkedRef = useRef(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// 标记已检查,但只在未登录时调用 checkAuth
|
||||||
|
if (!checkedRef.current) {
|
||||||
|
checkedRef.current = true;
|
||||||
|
// 如果未登录,才需要检查
|
||||||
|
if (!isAuthenticated) {
|
||||||
checkAuth();
|
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) {
|
if (isLoading) {
|
||||||
return null; // Or a loading spinner
|
return <AuthLoading />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
return <Navigate to="/login" replace />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
@ -25,15 +78,14 @@ function ProtectedRoute({ children }: { children: React.ReactNode }) {
|
|||||||
|
|
||||||
// Public Route wrapper (redirects to home if authenticated)
|
// Public Route wrapper (redirects to home if authenticated)
|
||||||
function PublicRoute({ children }: { children: React.ReactNode }) {
|
function PublicRoute({ children }: { children: React.ReactNode }) {
|
||||||
const isAuthenticated = useAppStore((state) => state.isAuthenticated);
|
const { isAuthenticated, isLoading } = useAuthLoader();
|
||||||
const checkAuth = useAppStore((state) => state.checkAuth);
|
|
||||||
|
|
||||||
useEffect(() => {
|
if (isLoading) {
|
||||||
checkAuth();
|
return <AuthLoading />;
|
||||||
}, [checkAuth]);
|
}
|
||||||
|
|
||||||
if (isAuthenticated) {
|
if (isAuthenticated) {
|
||||||
return <HomePage />;
|
return <Navigate to="/home" replace />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
@ -42,6 +94,10 @@ function PublicRoute({ children }: { children: React.ReactNode }) {
|
|||||||
export const router = createBrowserRouter([
|
export const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
|
element: <Navigate to="/home" replace />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/landing',
|
||||||
element: (
|
element: (
|
||||||
<PublicRoute>
|
<PublicRoute>
|
||||||
<LandingPage />
|
<LandingPage />
|
||||||
|
|||||||
@ -93,7 +93,7 @@ export const useAppStore = create<AppState>()(
|
|||||||
|
|
||||||
saveNotes: async (content) => {
|
saveNotes: async (content) => {
|
||||||
try {
|
try {
|
||||||
const notes = await api.notes.save(content);
|
const notes = await api.notes.update(content);
|
||||||
set({ notes });
|
set({ notes });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to save notes:', error);
|
console.error('Failed to save notes:', error);
|
||||||
@ -143,7 +143,9 @@ export const useAppStore = create<AppState>()(
|
|||||||
set({ user, isAuthenticated: true });
|
set({ user, isAuthenticated: true });
|
||||||
return { error: null };
|
return { error: null };
|
||||||
} catch (error: any) {
|
} 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 });
|
set({ user, isAuthenticated: true });
|
||||||
return { error: null };
|
return { error: null };
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
return { error: error.message || '注册失败' };
|
// 确保返回字符串错误信息
|
||||||
|
const errorMessage = error.message || '注册失败,请稍后重试';
|
||||||
|
return { error: errorMessage };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user