Handle identity token reset on 2004
This commit is contained in:
@@ -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>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user