Skip to content

Commit 9f6f93e

Browse files
awesomeYGawesomeYG
andauthored
feat: 重构认证系统并新增时间显示和错误边界组件 (#45)
- 新增 TimeDisplay 组件,支持相对时间和绝对时间显示 - 新增 ServerErrorBoundary 组件,用于处理服务端渲染错误 - 重构认证系统,移除 publicAccess 相关功能 - 优化 httpClient 认证逻辑,改进 clearAuthData 函数 - 移动 proxy.ts 文件到根目录 - 优化论坛、登录、注册等页面的UI组件 - 改进错误处理和用户体验 Co-authored-by: awesomeYG <gang.yang@chaitin.com>
1 parent fbdc374 commit 9f6f93e

File tree

18 files changed

+507
-231
lines changed

18 files changed

+507
-231
lines changed

ui/front/src/proxy.ts renamed to ui/front/proxy.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
import { NextResponse } from 'next/server';
88
import type { NextRequest } from 'next/server';
9-
import { getServerPublicAccessStatus } from '@/utils/serverAuthConfig';
9+
import { getServerPublicAccessStatus } from './src/utils/serverAuthConfig';
1010

1111
// 需要认证的路由
1212
const PROTECTED_ROUTES = [
@@ -19,7 +19,7 @@ const PROTECTED_ROUTES = [
1919
const AUTH_ROUTES = ['/login', '/register'];
2020

2121
// 可能受public_access控制的路由(首页和discuss页面)
22-
const CONDITIONAL_PUBLIC_ROUTES = ['/', '/forum'];
22+
const CONDITIONAL_PUBLIC_ROUTES = ['/', '/forum*'];
2323

2424
/**
2525
* 检查路由是否匹配
@@ -186,4 +186,3 @@ export const config = {
186186
'/((?!api|_next/static|_next/image|favicon.ico|.*\\..*|font).*)',
187187
],
188188
};
189-

ui/front/src/api/httpClient.ts

Lines changed: 80 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,6 @@ import alert from "@/components/alert";
1414
import { clearCache, generateCacheKey, retryRequest } from "@/lib/api-cache";
1515
import { API_CONSTANTS } from "@/lib/constants";
1616
import { clearAllAuthCookies } from "@/utils/cookie";
17-
import {
18-
clearPublicAccessCache,
19-
getPublicAccessStatus,
20-
} from "@/utils/publicAccess";
2117
import type {
2218
AxiosInstance,
2319
AxiosRequestConfig,
@@ -123,37 +119,54 @@ export const clearCsrfTokenCache = () => {
123119
};
124120

125121
// 导出公共访问状态获取函数,供其他组件使用
126-
export const checkPublicAccess = getPublicAccessStatus;
127122

128123
// 清除所有认证信息的函数
129-
export const clearAuthData = async () => {
124+
export const clearAuthData = async (callLogoutAPI: boolean = true) => {
130125
if (typeof window !== "undefined") {
131126
console.log("Clearing all authentication data...");
132127

133128
// 清除本地存储的认证信息
134129
localStorage.removeItem("auth_token");
135130
localStorage.removeItem("user");
136131
localStorage.removeItem("userInfo");
132+
133+
// 设置明确的空值,避免后续检查时误判
134+
localStorage.setItem("auth_token", "");
135+
localStorage.setItem("user", "");
136+
localStorage.setItem("userInfo", "");
137137

138138
// 使用工具函数清除所有认证相关的cookie,包括auth_token
139139
clearAllAuthCookies();
140140

141141
// 清除CSRF token缓存
142142
clearCsrfTokenCache();
143-
clearPublicAccessCache();
144143

145-
// 调用服务端 logout API 来清理服务端 cookie
146-
try {
147-
await fetch('/api/logout', {
148-
method: 'POST',
149-
credentials: 'include', // 确保发送 cookie
150-
});
151-
console.log("Server-side cookies cleared successfully");
152-
} catch (error) {
153-
console.warn("Failed to clear server-side cookies:", error);
154-
// 即使服务端清理失败,客户端清理仍然有效
144+
// 根据参数决定是否调用服务端登出API
145+
if (callLogoutAPI) {
146+
try {
147+
await fetch('/api/user/logout', {
148+
method: 'POST',
149+
credentials: 'include', // 确保发送 cookie
150+
});
151+
console.log("Server-side cookies cleared successfully");
152+
} catch (error) {
153+
console.warn("Failed to clear server-side cookies:", error);
154+
// 即使服务端清理失败,客户端清理仍然有效
155+
}
155156
}
156157

158+
// 清除所有待处理的用户相关请求
159+
if (typeof window !== 'undefined' && window.httpClientInstance) {
160+
window.httpClientInstance.clearPendingRequestsByPath('/user');
161+
}
162+
163+
// 强制刷新页面状态,确保所有组件重新初始化
164+
// 使用 setTimeout 避免在清理过程中立即刷新
165+
setTimeout(() => {
166+
// 触发自定义事件,通知所有组件认证状态已清除
167+
window.dispatchEvent(new CustomEvent('auth:cleared'));
168+
}, 100);
169+
157170
console.log("Authentication data cleared successfully");
158171
}
159172
};
@@ -278,9 +291,28 @@ export class HttpClient<SecurityDataType = unknown> {
278291

279292
// 清除所有待处理的请求
280293
public clearPendingRequests = () => {
294+
console.log(`[HttpClient] Clearing ${this.pendingRequests.size} pending requests`);
281295
this.pendingRequests.clear();
282296
};
283297

298+
// 清除特定类型的待处理请求
299+
public clearPendingRequestsByPath = (pathPattern: string) => {
300+
const keysToDelete: string[] = [];
301+
for (const [key] of this.pendingRequests) {
302+
if (key.includes(pathPattern)) {
303+
keysToDelete.push(key);
304+
}
305+
}
306+
307+
keysToDelete.forEach(key => {
308+
this.pendingRequests.delete(key);
309+
});
310+
311+
if (keysToDelete.length > 0) {
312+
console.log(`[HttpClient] Cleared ${keysToDelete.length} pending requests for path: ${pathPattern}`);
313+
}
314+
};
315+
284316
protected mergeRequestParams(
285317
params1: AxiosRequestConfig,
286318
params2?: AxiosRequestConfig,
@@ -345,9 +377,19 @@ export class HttpClient<SecurityDataType = unknown> {
345377

346378
// 检查是否有相同的请求正在进行中(请求去重)
347379
if (this.pendingRequests.has(requestKey)) {
380+
console.log(`[HttpClient] Request already pending: ${requestKey}`);
348381
return this.pendingRequests.get(requestKey);
349382
}
350383

384+
// 对于 /api/user 请求,添加额外的去重逻辑
385+
if (path === '/user' && method === 'GET') {
386+
const userRequestKey = 'GET:/user';
387+
if (this.pendingRequests.has(userRequestKey)) {
388+
console.log('[HttpClient] User request already pending, reusing result');
389+
return this.pendingRequests.get(userRequestKey);
390+
}
391+
}
392+
351393
const secureParams =
352394
((typeof secure === "boolean" ? secure : this.secure) &&
353395
this.securityWorker &&
@@ -442,17 +484,17 @@ export class HttpClient<SecurityDataType = unknown> {
442484
requestConfig.headers.Cookie = allCookies;
443485
}
444486

445-
if (process.env.NODE_ENV === "development") {
446-
console.log(`[SSR] API Request to ${path}`);
447-
console.log(`[SSR] Cookies available:`, !!allCookies);
448-
console.log(`[SSR] Authorization token:`, !!Authorization);
449-
if (allCookies) {
450-
console.log(
451-
`[SSR] Cookie header:`,
452-
allCookies.substring(0, 100) + "...",
453-
);
454-
}
455-
}
487+
// if (process.env.NODE_ENV === "development") {
488+
// console.log(`[SSR] API Request to ${path}`);
489+
// console.log(`[SSR] Cookies available:`, !!allCookies);
490+
// console.log(`[SSR] Authorization token:`, !!Authorization);
491+
// if (allCookies) {
492+
// console.log(
493+
// `[SSR] Cookie header:`,
494+
// allCookies.substring(0, 100) + "...",
495+
// );
496+
// }
497+
// }
456498
} catch (error) {
457499
// 在某些情况下cookies可能不可用,忽略错误
458500
console.warn("Failed to get cookies in SSR:", error);
@@ -486,7 +528,15 @@ export class HttpClient<SecurityDataType = unknown> {
486528
};
487529
}
488530

489-
export default new HttpClient({
531+
// 创建全局 httpClient 实例
532+
const httpClientInstance = new HttpClient({
490533
format: "json",
491534
baseURL: (process.env.TARGET || "") + "/api",
492-
}).request;
535+
});
536+
537+
// 在客户端环境中将实例挂载到 window 对象上,方便全局访问
538+
if (typeof window !== "undefined") {
539+
(window as any).httpClientInstance = httpClientInstance;
540+
}
541+
542+
export default httpClientInstance.request;

ui/front/src/app/forum/[forum_id]/discuss/[id]/ui/content.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
import { Card, MarkDown } from '@/components'
2121
import { AuthContext } from '@/components/authProvider'
2222
// import { Avatar } from '@/components/discussion'
23+
import { TimeDisplayWithTag } from '@/components/TimeDisplay'
2324
import EditorWrap from '@/components/editor/edit/Wrap'
2425
import Modal from '@/components/modal'
2526
import { useAuthCheck } from '@/hooks/useAuthCheck'
@@ -274,12 +275,10 @@ const BaseDiscussCard = (props: {
274275
color: 'rgba(0,0,0,0.5)',
275276
}}
276277
>
277-
<time
278-
dateTime={dayjs.unix(data.updated_at!).format()}
278+
<TimeDisplayWithTag
279+
timestamp={data.updated_at!}
279280
title={dayjs.unix(data.updated_at!).format('YYYY-MM-DD HH:mm:ss')}
280-
>
281-
更新于 {dayjs.unix(data.updated_at!).fromNow()}
282-
</time>
281+
/>
283282
</Typography>
284283
<Stack direction='row' gap={2} alignItems='center' sx={{ display: { xs: 'none', sm: 'flex' } }}>
285284
{!isReply && (

ui/front/src/app/forum/[forum_id]/discuss/[id]/ui/titleCard.tsx

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { ModelDiscussionDetail, ModelDiscussionType, ModelUserRole } from '@/api
99
import { Card } from '@/components'
1010
import { AuthContext } from '@/components/authProvider'
1111
import { ReleaseModal, Tag } from '@/components/discussion'
12+
import { TimeDisplayWithTag } from '@/components/TimeDisplay'
1213
import EditorWrap from '@/components/editor/edit/Wrap'
1314
import EditorContent from '@/components/EditorContent'
1415
import Modal from '@/components/modal'
@@ -258,8 +259,22 @@ const TitleCard = ({ data }: { data: ModelDiscussionDetail }) => {
258259
>
259260
{data.user_name}
260261
{data.updated_at && data.updated_at !== data.created_at
261-
? `更新于 ${dayjs.unix(data.updated_at).fromNow()}`
262-
: `发布于 ${dayjs.unix(data.created_at!).fromNow()}`}
262+
? (
263+
<>
264+
更新于 <TimeDisplayWithTag
265+
timestamp={data.updated_at}
266+
title={dayjs.unix(data.updated_at).format('YYYY-MM-DD HH:mm:ss')}
267+
/>
268+
</>
269+
)
270+
: (
271+
<>
272+
发布于 <TimeDisplayWithTag
273+
timestamp={data.created_at!}
274+
title={dayjs.unix(data.created_at!).format('YYYY-MM-DD HH:mm:ss')}
275+
/>
276+
</>
277+
)}
263278
</Typography>
264279
<Stack direction='row' alignItems='flex-end' gap={2} justifyContent='space-between' sx={{ my: 1 }}>
265280
<Stack direction='row' flexWrap='wrap' gap='8px 16px'>
@@ -311,8 +326,22 @@ const TitleCard = ({ data }: { data: ModelDiscussionDetail }) => {
311326
>
312327
{data.user_name}{' '}
313328
{data.updated_at && data.updated_at !== data.created_at
314-
? `更新于 ${dayjs.unix(data.updated_at).fromNow()}`
315-
: `发布于 ${dayjs.unix(data.created_at!).fromNow()}`}
329+
? (
330+
<>
331+
更新于 <TimeDisplayWithTag
332+
timestamp={data.updated_at}
333+
title={dayjs.unix(data.updated_at).format('YYYY-MM-DD HH:mm:ss')}
334+
/>
335+
</>
336+
)
337+
: (
338+
<>
339+
发布于 <TimeDisplayWithTag
340+
timestamp={data.created_at!}
341+
title={dayjs.unix(data.created_at!).format('YYYY-MM-DD HH:mm:ss')}
342+
/>
343+
</>
344+
)}
316345
</time>
317346
</Typography>
318347
</Stack>

ui/front/src/app/forum/[forum_id]/ui/article.tsx

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,11 @@ const Article = ({
246246
setArticleData(data)
247247
}, [data])
248248

249+
// 当URL参数变化时重置页码
250+
useEffect(() => {
251+
setPage(1)
252+
}, [status, type, topics])
253+
249254
// 更新搜索引用
250255
useEffect(() => {
251256
searchRef.current = search
@@ -467,8 +472,10 @@ const Article = ({
467472
value={type || 'qa'}
468473
onChange={(value: string) => {
469474
// 只有在状态真正变化时才更新 URL
470-
const query = createQueryString('type', value)
471-
router.replace(`/?${query}`)
475+
if (value !== (type || 'qa')) {
476+
const query = createQueryString('type', value)
477+
router.replace(`/?${query}`)
478+
}
472479
}}
473480
list={TYPE_LIST}
474481
/>
@@ -613,8 +620,10 @@ const Article = ({
613620
value={status}
614621
onChange={(value: Status) => {
615622
// 只有在状态真正变化时才更新 URL
616-
const query = createQueryString('sort', value)
617-
router.replace(`/?${query}`)
623+
if (value !== status) {
624+
const query = createQueryString('sort', value)
625+
router.replace(`/?${query}`)
626+
}
618627
}}
619628
list={getStatusLabels()}
620629
/>

ui/front/src/app/forum/[forum_id]/ui/discussCard.tsx

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Card, MatchedString, Title } from '@/app/(banner)/s/ui/common'
33
import { Icon } from '@/components'
44
import { CommonContext } from '@/components/commonProvider'
55
import { Avatar, Tag } from '@/components/discussion'
6+
import { TimeDisplay, TimeDisplayWithTag } from '@/components/TimeDisplay'
67
import { formatNumber } from '@/lib/utils'
78
import CheckCircleIcon from '@mui/icons-material/CheckCircle'
89
import { Box, Chip, Stack, SxProps, Typography } from '@mui/material'
@@ -178,7 +179,7 @@ const DiscussCard = ({ data, keywords: _keywords, showType = false, sx }: { data
178179
variant='body2'
179180
sx={{ fontSize: 12, color: 'rgba(0,0,0,0.5)', whiteSpace: 'nowrap', flexShrink: 0 }}
180181
>
181-
{dayjs.unix(it.updated_at!).fromNow()}
182+
<TimeDisplay timestamp={it.updated_at!} />
182183
</Typography>
183184
</Stack>
184185
</Stack>
@@ -364,12 +365,10 @@ export const DiscussCardMobile = ({ data, keywords, showType = false, sx }: { da
364365
variant='body2'
365366
sx={{ fontSize: 12, lineHeight: 1, color: 'rgba(0,0,0,0.5)', whiteSpace: 'nowrap', flexShrink: 0 }}
366367
>
367-
<time
368-
dateTime={dayjs.unix(it.updated_at!).format()}
368+
<TimeDisplayWithTag
369+
timestamp={it.updated_at!}
369370
title={dayjs.unix(it.updated_at!).format('YYYY-MM-DD HH:mm:ss')}
370-
>
371-
更新于 {dayjs.unix(it.updated_at!).fromNow()}
372-
</time>
371+
/>
373372
</Typography>
374373
<Stack direction='row' alignItems='center' gap={1} sx={{ minWidth: 0, flex: 1 }}>
375374
{it.user_avatar ? (

0 commit comments

Comments
 (0)