Skip to content

Commit ede70a1

Browse files
awesomeYGawesomeYG
andauthored
fix: editor update and open the error check in product env (#43)
* fix: editor update and open the error check in product env * fix: 登录状态 --------- Co-authored-by: awesomeYG <gang.yang@chaitin.com>
1 parent 76de08d commit ede70a1

File tree

11 files changed

+387
-62
lines changed

11 files changed

+387
-62
lines changed

ui/front/src/api/httpClient.ts

Lines changed: 43 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ export const clearCsrfTokenCache = () => {
126126
export const checkPublicAccess = getPublicAccessStatus;
127127

128128
// 清除所有认证信息的函数
129-
export const clearAuthData = () => {
129+
export const clearAuthData = async () => {
130130
if (typeof window !== "undefined") {
131131
console.log("Clearing all authentication data...");
132132

@@ -142,7 +142,17 @@ export const clearAuthData = () => {
142142
clearCsrfTokenCache();
143143
clearPublicAccessCache();
144144

145-
// 清除认证配置缓存
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+
// 即使服务端清理失败,客户端清理仍然有效
155+
}
146156

147157
console.log("Authentication data cleared successfully");
148158
}
@@ -206,22 +216,37 @@ export class HttpClient<SecurityDataType = unknown> {
206216
error.response,
207217
);
208218

209-
// 立即清除所有认证信息,包括cookie中的auth_token
210-
clearAuthData();
211-
212-
const currentPath = window.location.pathname;
213-
const isAuthPage =
214-
currentPath.startsWith("/login") ||
215-
currentPath.startsWith("/register");
216-
217-
// 如果不在认证页面,直接重定向到登录页
218-
// middleware已经处理了public_access的检查,这里不需要重复检查
219-
if (!isAuthPage) {
220-
const fullPath =
221-
window.location.pathname + window.location.search;
222-
const loginUrl = `/login?redirect=${encodeURIComponent(fullPath)}`;
223-
window.location.href = loginUrl;
224-
}
219+
// 异步清除所有认证信息,包括cookie中的auth_token
220+
clearAuthData().then(() => {
221+
const currentPath = window.location.pathname;
222+
const isAuthPage =
223+
currentPath.startsWith("/login") ||
224+
currentPath.startsWith("/register");
225+
226+
// 如果不在认证页面,直接重定向到登录页
227+
// middleware已经处理了public_access的检查,这里不需要重复检查
228+
if (!isAuthPage) {
229+
const fullPath =
230+
window.location.pathname + window.location.search;
231+
const loginUrl = `/login?redirect=${encodeURIComponent(fullPath)}`;
232+
window.location.href = loginUrl;
233+
}
234+
}).catch((clearError) => {
235+
console.error("Failed to clear auth data on 401:", clearError);
236+
// 即使清理失败,也要重定向到登录页
237+
const currentPath = window.location.pathname;
238+
const isAuthPage =
239+
currentPath.startsWith("/login") ||
240+
currentPath.startsWith("/register");
241+
242+
if (!isAuthPage) {
243+
const fullPath =
244+
window.location.pathname + window.location.search;
245+
const loginUrl = `/login?redirect=${encodeURIComponent(fullPath)}`;
246+
window.location.href = loginUrl;
247+
}
248+
});
249+
225250
return Promise.reject(error.response);
226251
}
227252
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { NextRequest, NextResponse } from 'next/server';
2+
import { clearAllAuthCookies } from '@/utils/cookie';
3+
4+
/**
5+
* 退出登录 API 路由
6+
* 处理 SSR 和客户端的 cookie 清理
7+
*/
8+
export async function POST(request: NextRequest) {
9+
try {
10+
// 创建响应
11+
const response = NextResponse.json(
12+
{ success: true, message: '退出登录成功' },
13+
{ status: 200 }
14+
);
15+
16+
// 清除所有认证相关的 cookie
17+
const cookiesToClear = [
18+
'auth_token',
19+
'session_id',
20+
'koala_session',
21+
'csrf_token',
22+
'_vercel_jwt',
23+
'_pw_auth_session',
24+
];
25+
26+
// 在服务端清除 cookie
27+
cookiesToClear.forEach(cookieName => {
28+
// 清除不同路径和域名的 cookie
29+
const paths = ['/', '/api'];
30+
const domains = [request.nextUrl.hostname];
31+
32+
// 添加子域名(如果不是 localhost)
33+
if (request.nextUrl.hostname !== "localhost") {
34+
domains.push(`.${request.nextUrl.hostname}`);
35+
}
36+
37+
paths.forEach(path => {
38+
domains.forEach(domain => {
39+
// 清除带域名的 cookie
40+
response.cookies.set(cookieName, '', {
41+
expires: new Date(0),
42+
path: path,
43+
domain: domain,
44+
sameSite: 'lax',
45+
secure: process.env.NODE_ENV === 'production',
46+
httpOnly: true,
47+
});
48+
49+
// 清除不带域名的 cookie
50+
response.cookies.set(cookieName, '', {
51+
expires: new Date(0),
52+
path: path,
53+
sameSite: 'lax',
54+
secure: process.env.NODE_ENV === 'production',
55+
httpOnly: true,
56+
});
57+
});
58+
});
59+
});
60+
61+
if (process.env.NODE_ENV === 'development') {
62+
console.log('[Logout API] Cleared all authentication cookies');
63+
}
64+
65+
return response;
66+
} catch (error) {
67+
console.error('[Logout API] Error during logout:', error);
68+
return NextResponse.json(
69+
{ success: false, message: '退出登录失败' },
70+
{ status: 500 }
71+
);
72+
}
73+
}
74+
75+
// 支持 GET 请求(用于直接访问)
76+
export async function GET(request: NextRequest) {
77+
return POST(request);
78+
}

ui/front/src/app/error.tsx

Lines changed: 115 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
import Link from 'next/link';
33
import Image from 'next/image';
44
import error from '@/asset/img/500.png';
5-
import { Box, Stack, Button, Typography, Collapse } from '@mui/material';
5+
import { Box, Stack, Button, Typography, Collapse, Chip, IconButton, Tooltip, Alert } from '@mui/material';
66
import { useState } from 'react';
7+
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
78

89
export default function Error({
910
error: err,
@@ -12,7 +13,37 @@ export default function Error({
1213
error: Error & { digest?: string };
1314
reset: () => void;
1415
}) {
15-
const [showDetail, setShowDetail] = useState(false);
16+
// 检测是否为生产环境
17+
const isProduction = process.env.NODE_ENV === 'production';
18+
// 生产环境默认显示详情,开发环境默认隐藏
19+
const [showDetail, setShowDetail] = useState(isProduction);
20+
const [copyStatus, setCopyStatus] = useState<'idle' | 'success' | 'error'>('idle');
21+
22+
// 复制错误信息到剪贴板
23+
const copyErrorInfo = async () => {
24+
const errorInfo = {
25+
message: err?.message,
26+
stack: err?.stack,
27+
digest: err?.digest,
28+
timestamp: new Date().toISOString(),
29+
userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : 'N/A',
30+
url: typeof window !== 'undefined' ? window.location.href : 'N/A',
31+
environment: process.env.NODE_ENV,
32+
...(err as any),
33+
};
34+
35+
try {
36+
await navigator.clipboard.writeText(JSON.stringify(errorInfo, null, 2));
37+
setCopyStatus('success');
38+
// 3秒后重置状态
39+
setTimeout(() => setCopyStatus('idle'), 3000);
40+
} catch (error) {
41+
console.error('复制失败:', error);
42+
setCopyStatus('error');
43+
// 3秒后重置状态
44+
setTimeout(() => setCopyStatus('idle'), 3000);
45+
}
46+
};
1647

1748
return (
1849
<Box
@@ -36,7 +67,15 @@ export default function Error({
3667
>
3768
<Image src={error} alt='500'></Image>
3869
<Stack alignItems='center' gap={1} sx={{ maxWidth: 900, mx: 'auto', px: 2 }}>
39-
<Typography variant='h6'>发生错误</Typography>
70+
<Stack direction='row' alignItems='center' gap={1}>
71+
<Typography variant='h6'>发生错误</Typography>
72+
<Chip
73+
label={isProduction ? '生产环境' : '开发环境'}
74+
size='small'
75+
color={isProduction ? 'error' : 'warning'}
76+
variant='outlined'
77+
/>
78+
</Stack>
4079
{(() => {
4180
// 优先展示后端返回的 message/status/code
4281
const errWithData = err as { isBackend?: boolean; data?: unknown; status?: number; code?: number; url?: string };
@@ -77,26 +116,89 @@ export default function Error({
77116
digest: {err.digest}
78117
</Typography>
79118
)}
80-
<Button size='small' onClick={() => setShowDetail(v => !v)}>
81-
{showDetail ? '隐藏详情' : '显示详情'}
82-
</Button>
119+
<Stack direction='row' gap={1} alignItems='center'>
120+
<Button size='small' onClick={() => setShowDetail(v => !v)}>
121+
{showDetail ? '隐藏详情' : '显示详情'}
122+
</Button>
123+
<Tooltip title='复制错误信息'>
124+
<IconButton size='small' onClick={copyErrorInfo}>
125+
<ContentCopyIcon fontSize='small' />
126+
</IconButton>
127+
</Tooltip>
128+
</Stack>
129+
130+
{/* 复制状态提示 */}
131+
{copyStatus !== 'idle' && (
132+
<Alert
133+
severity={copyStatus === 'success' ? 'success' : 'error'}
134+
sx={{ width: '100%', maxWidth: 400 }}
135+
>
136+
{copyStatus === 'success' ? '错误信息已复制到剪贴板' : '复制失败,请手动复制'}
137+
</Alert>
138+
)}
83139
<Collapse in={showDetail} unmountOnExit sx={{ width: '100%' }}>
84140
<Box
85-
component='pre'
86141
sx={{
87142
width: '100%',
88-
whiteSpace: 'pre-wrap',
89-
wordBreak: 'break-word',
90-
p: 2,
91143
bgcolor: 'background.paper',
92144
borderRadius: 2,
93145
border: '1px solid',
94146
borderColor: 'divider',
95-
fontSize: 12,
96-
lineHeight: 1.5,
147+
overflow: 'hidden',
97148
}}
98149
>
99-
{process.env.NODE_ENV === 'development' ? err?.stack || '' : '生产环境默认隐藏堆栈信息'}
150+
{/* 错误堆栈信息 */}
151+
{err?.stack && (
152+
<Box sx={{ p: 2, borderBottom: '1px solid', borderColor: 'divider' }}>
153+
<Typography variant='subtitle2' gutterBottom>
154+
错误堆栈:
155+
</Typography>
156+
<Box
157+
component='pre'
158+
sx={{
159+
whiteSpace: 'pre-wrap',
160+
wordBreak: 'break-word',
161+
fontSize: 12,
162+
lineHeight: 1.5,
163+
m: 0,
164+
fontFamily: 'monospace',
165+
}}
166+
>
167+
{err.stack}
168+
</Box>
169+
</Box>
170+
)}
171+
172+
{/* 详细错误信息 */}
173+
<Box sx={{ p: 2 }}>
174+
<Typography variant='subtitle2' gutterBottom>
175+
详细错误信息:
176+
</Typography>
177+
<Box
178+
component='pre'
179+
sx={{
180+
whiteSpace: 'pre-wrap',
181+
wordBreak: 'break-word',
182+
fontSize: 12,
183+
lineHeight: 1.5,
184+
m: 0,
185+
fontFamily: 'monospace',
186+
bgcolor: 'grey.50',
187+
p: 1,
188+
borderRadius: 1,
189+
}}
190+
>
191+
{JSON.stringify({
192+
message: err?.message,
193+
digest: err?.digest,
194+
timestamp: new Date().toISOString(),
195+
url: typeof window !== 'undefined' ? window.location.href : 'N/A',
196+
userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : 'N/A',
197+
environment: process.env.NODE_ENV,
198+
...(err as any),
199+
}, null, 2)}
200+
</Box>
201+
</Box>
100202
</Box>
101203
</Collapse>
102204
</Stack>

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

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { getDiscussionDiscId } from '@/api'
22
import { Metadata } from 'next'
33
import { Suspense } from 'react'
4-
import { Box, Stack } from '@mui/material'
4+
import { Box, Stack, Alert, Typography } from '@mui/material'
55
import TitleCard from './ui/titleCard'
66
import Content from './ui/content'
77
import ScrollAnimation from '@/components/ScrollAnimation'
@@ -15,10 +15,14 @@ export const metadata: Metadata = {
1515
async function fetchDiscussionDetail(discId: string) {
1616
try {
1717
const discussion = await getDiscussionDiscId({ discId })
18-
return discussion
18+
return { success: true, data: discussion, error: null }
1919
} catch (error) {
2020
console.error('Failed to fetch discussion detail:', error)
21-
return null
21+
return {
22+
success: false,
23+
data: null,
24+
error: error instanceof Error ? error.message : '获取讨论详情失败'
25+
}
2226
}
2327
}
2428

@@ -41,7 +45,38 @@ const DiscussDetailPage = async (props: { params: Promise<{ forum_id: string; id
4145
const { id } = await props.params
4246

4347
// 获取讨论详情
44-
const discussion = await fetchDiscussionDetail(id)
48+
const result = await fetchDiscussionDetail(id)
49+
50+
// 处理错误情况
51+
if (!result.success) {
52+
return (
53+
<Box
54+
sx={{
55+
minHeight: '100vh',
56+
display: 'flex',
57+
alignItems: 'center',
58+
justifyContent: 'center',
59+
px: 2,
60+
}}
61+
>
62+
<Box sx={{ maxWidth: 600, width: '100%' }}>
63+
<Alert severity="error" sx={{ mb: 2 }}>
64+
<Typography variant="h6" gutterBottom>
65+
获取讨论详情失败
66+
</Typography>
67+
<Typography variant="body2" color="text.secondary">
68+
{result.error}
69+
</Typography>
70+
</Alert>
71+
<Typography variant="body2" color="text.disabled" textAlign="center">
72+
请检查网络连接或稍后重试
73+
</Typography>
74+
</Box>
75+
</Box>
76+
)
77+
}
78+
79+
const discussion = result.data
4580

4681
if (!discussion) {
4782
return (

0 commit comments

Comments
 (0)