Handle identity token reset on 2004

This commit is contained in:
LeonspaceX
2026-01-27 15:57:53 +08:00
parent 0bce1d49da
commit 79f481df43
3 changed files with 110 additions and 11 deletions

View File

@@ -1,14 +1,15 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { tokens, useToastController, Toast, ToastTitle } from '@fluentui/react-components';
import { tokens, useToastController, Toast, ToastTitle, Dialog, DialogSurface, DialogBody, DialogTitle, DialogContent, DialogActions, Button, Input, Text } from '@fluentui/react-components';
import { MainLayout } from './layouts/MainLayout';
import About from './components/About';
import CreatePost from './components/CreatePost';
import PostCard from './components/PostCard';
import ImageViewer from './components/ImageViewer';
import { fetchArticles, type Article } from './api';
import { fetchArticles, reset_identity_token, type Article } from './api';
import { useLayout } from './context/LayoutContext';
import './App.css';
import { Eye24Regular, EyeOff24Regular, Copy24Regular } from '@fluentui/react-icons';
const Home: React.FC<{ onPreviewImage: (src: string, alt?: string) => void }> = ({ onPreviewImage }) => {
const { refreshTrigger } = useLayout();
@@ -154,6 +155,19 @@ function App() {
setImageViewer({ open: true, src, alt });
};
const closeImageViewer = () => setImageViewer({ open: false });
const [identityDialogOpen, setIdentityDialogOpen] = useState(false);
const [identityToken, setIdentityToken] = useState('');
const [identityVisible, setIdentityVisible] = useState(false);
useEffect(() => {
const handler = () => {
setIdentityToken(localStorage.getItem('identity_token') || '');
setIdentityVisible(false);
setIdentityDialogOpen(true);
};
window.addEventListener('identity_invalid', handler as EventListener);
return () => window.removeEventListener('identity_invalid', handler as EventListener);
}, []);
return (
<BrowserRouter>
@@ -162,10 +176,70 @@ function App() {
path="/"
element={
<MainLayout
imageViewer={
imageViewer.open && imageViewer.src ? (
<ImageViewer src={imageViewer.src!} alt={imageViewer.alt} onClose={closeImageViewer} />
) : null
overlays={
<>
{imageViewer.open && imageViewer.src ? (
<ImageViewer src={imageViewer.src!} alt={imageViewer.alt} onClose={closeImageViewer} />
) : null}
<Dialog open={identityDialogOpen} onOpenChange={(_, data) => setIdentityDialogOpen(!!data.open)}>
<DialogSurface>
<DialogBody>
<DialogTitle>Identity_token无效</DialogTitle>
<DialogContent>
<Text block style={{ color: tokens.colorPaletteRedForeground1 }}>
Identity_token后使使 identity
</Text>
<div style={{ marginTop: tokens.spacingVerticalS }}>
<Text block>Identity_token:</Text>
<div style={{ display: 'flex', gap: tokens.spacingHorizontalS, alignItems: 'center', marginTop: tokens.spacingVerticalXS }}>
<Input
type={identityVisible ? 'text' : 'password'}
value={identityToken}
readOnly
/>
<Button
appearance="secondary"
icon={identityVisible ? <EyeOff24Regular /> : <Eye24Regular />}
onClick={() => setIdentityVisible(v => !v)}
/>
<Button
appearance="secondary"
icon={<Copy24Regular />}
disabled={!identityVisible}
onClick={() => {
if (!identityVisible || !identityToken) return;
navigator.clipboard.writeText(identityToken).catch(() => {});
}}
/>
</div>
</div>
</DialogContent>
<DialogActions>
<Button
appearance="secondary"
onClick={() => setIdentityDialogOpen(false)}
>
</Button>
<Button
appearance="primary"
onClick={async () => {
try {
const token = await reset_identity_token();
setIdentityToken(token);
setIdentityDialogOpen(false);
} catch (err) {
console.error('Failed to reset identity token:', err);
}
}}
>
</Button>
</DialogActions>
</DialogBody>
</DialogSurface>
</Dialog>
</>
}
/>
}

View File

@@ -117,6 +117,26 @@ export const get_id_token = async (): Promise<string> => {
return token;
};
const notifyInvalidIdentity = () => {
if (typeof window !== 'undefined') {
window.dispatchEvent(new CustomEvent('identity_invalid'));
}
};
const handlePostApiCode = (json: any) => {
if (json && json.code === 2004) {
notifyInvalidIdentity();
throw new Error(json.data || 'Identity token invalid');
}
};
export const reset_identity_token = async (): Promise<string> => {
const token = await fetch_id_token();
localStorage.removeItem('identity_token');
localStorage.setItem('identity_token', token);
return token;
};
export interface CreatePostResponse {
code: number;
data: any;
@@ -156,7 +176,9 @@ export const createPost = async (content: string): Promise<CreatePostResponse> =
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
const json = await response.json();
handlePostApiCode(json);
return json;
} catch (error) {
console.error('Failed to create post:', error);
throw error;
@@ -286,6 +308,7 @@ export const postComment = async (commentData: PostCommentRequest): Promise<Post
}
const json = await response.json();
handlePostApiCode(json);
if (json.code === 1001 && json.data?.id !== undefined) {
return { id: Number(json.data.id) };
}
@@ -320,6 +343,7 @@ export const reportPost = async (reportData: { id: number; title: string; conten
throw new Error(`HTTP error! status: ${response.status}`);
}
const json = await response.json();
handlePostApiCode(json);
if (json.code === 1001 && json.data?.id !== undefined) {
return { id: Number(json.data.id) };
}
@@ -346,6 +370,7 @@ export const uploadImage = async (file: File): Promise<string> => {
throw new Error(`HTTP error! status: ${response.status}`);
}
const json = await response.json();
handlePostApiCode(json);
if (json.code === 1001 && typeof json.data === 'string') {
return json.data;
}

View File

@@ -52,7 +52,7 @@ const useStyles = makeStyles({
}
});
const LayoutContent = ({ toasterId, imageViewer }: { toasterId: string; imageViewer?: ReactNode }) => {
const LayoutContent = ({ toasterId, overlays }: { toasterId: string; overlays?: ReactNode }) => {
const styles = useStyles();
const { isDarkMode } = useLayout();
@@ -71,17 +71,17 @@ const LayoutContent = ({ toasterId, imageViewer }: { toasterId: string; imageVie
</div>
<Footer />
<Toaster toasterId={toasterId} position="top-end" />
{imageViewer}
{overlays}
</div>
</FluentProvider>
);
};
export const MainLayout = ({ imageViewer }: { imageViewer?: ReactNode }) => {
export const MainLayout = ({ overlays }: { overlays?: ReactNode }) => {
const toasterId = useId('toaster');
return (
<LayoutProvider toasterId={toasterId}>
<LayoutContent toasterId={toasterId} imageViewer={imageViewer} />
<LayoutContent toasterId={toasterId} overlays={overlays} />
</LayoutProvider>
);
};