♻️ 按照 Material Design + Reactjs 重构 #1
										
											Binary file not shown.
										
									
								
							| @@ -16,6 +16,7 @@ import AppShell from "@/components/AppShell.tsx"; | ||||
| import LandingPage from "@/pages/landing.tsx"; | ||||
| import SignUpPage from "@/pages/auth/sign-up.tsx"; | ||||
| import SignInPage from "@/pages/auth/sign-in.tsx"; | ||||
| import OauthConnectPage from "@/pages/auth/connect.tsx"; | ||||
| import DashboardPage from "@/pages/users/dashboard.tsx"; | ||||
| import ErrorBoundary from "@/error.tsx"; | ||||
| import AppLoader from "@/components/AppLoader.tsx"; | ||||
| @@ -25,6 +26,8 @@ import PersonalizePage from "@/pages/users/personalize.tsx"; | ||||
| import SecurityPage from "@/pages/users/security.tsx"; | ||||
| import { UserinfoProvider } from "@/stores/userinfo.tsx"; | ||||
| import { WellKnownProvider } from "@/stores/wellKnown.tsx"; | ||||
| import AuthLayout from "@/pages/auth/layout.tsx"; | ||||
| import AuthGuard from "@/pages/guard.tsx"; | ||||
|  | ||||
| declare const __GARFISH_EXPORTS__: { | ||||
|   provider: Object; | ||||
| @@ -45,19 +48,33 @@ const router = createBrowserRouter([ | ||||
|     children: [ | ||||
|       { path: "/", element: <LandingPage /> }, | ||||
|       { | ||||
|         path: "/users", | ||||
|         element: <UserLayout />, | ||||
|         path: "/", | ||||
|         element: <AuthGuard />, | ||||
|         children: [ | ||||
|           { path: "/users", element: <DashboardPage /> }, | ||||
|           { path: "/users/notifications", element: <NotificationsPage /> }, | ||||
|           { path: "/users/personalize", element: <PersonalizePage /> }, | ||||
|           { path: "/users/security", element: <SecurityPage /> } | ||||
|           { | ||||
|             path: "/users", | ||||
|             element: <UserLayout />, | ||||
|             children: [ | ||||
|               { path: "/users", element: <DashboardPage /> }, | ||||
|               { path: "/users/notifications", element: <NotificationsPage /> }, | ||||
|               { path: "/users/personalize", element: <PersonalizePage /> }, | ||||
|               { path: "/users/security", element: <SecurityPage /> } | ||||
|             ] | ||||
|           } | ||||
|         ] | ||||
|       } | ||||
|     ] | ||||
|   }, | ||||
|   { path: "/auth/sign-up", element: <SignUpPage />, errorElement: <ErrorBoundary /> }, | ||||
|   { path: "/auth/sign-in", element: <SignInPage />, errorElement: <ErrorBoundary /> } | ||||
|   { | ||||
|     path: "/auth", | ||||
|     element: <AuthLayout />, | ||||
|     errorElement: <ErrorBoundary />, | ||||
|     children: [ | ||||
|       { path: "/auth/sign-up", element: <SignUpPage />, errorElement: <ErrorBoundary /> }, | ||||
|       { path: "/auth/sign-in", element: <SignInPage />, errorElement: <ErrorBoundary /> }, | ||||
|       { path: "/auth/o/connect", element: <OauthConnectPage />, errorElement: <ErrorBoundary /> } | ||||
|     ] | ||||
|   } | ||||
| ]); | ||||
|  | ||||
| const element = ( | ||||
|   | ||||
							
								
								
									
										182
									
								
								pkg/views/src/pages/auth/connect.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										182
									
								
								pkg/views/src/pages/auth/connect.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,182 @@ | ||||
| import { useEffect, useState } from "react"; | ||||
| import { | ||||
|   Alert, | ||||
|   Avatar, | ||||
|   Box, | ||||
|   Button, | ||||
|   Card, | ||||
|   CardContent, | ||||
|   Collapse, | ||||
|   Grid, | ||||
|   LinearProgress, | ||||
|   Typography | ||||
| } from "@mui/material"; | ||||
| import { request } from "@/scripts/request.ts"; | ||||
| import { useUserinfo } from "@/stores/userinfo.tsx"; | ||||
| import { useSearchParams } from "react-router-dom"; | ||||
| import OutletIcon from "@mui/icons-material/Outlet"; | ||||
| import WhatshotIcon from "@mui/icons-material/Whatshot"; | ||||
|  | ||||
| export default function OauthConnectPage() { | ||||
|   const { getAtk } = useUserinfo(); | ||||
|  | ||||
|   const [panel, setPanel] = useState(0); | ||||
|   const [error, setError] = useState<string | null>(null); | ||||
|   const [loading, setLoading] = useState(false); | ||||
|  | ||||
|   const [client, setClient] = useState<any>(null); | ||||
|  | ||||
|   const [searchParams] = useSearchParams(); | ||||
|  | ||||
|   async function preconnect() { | ||||
|     const res = await request(`/api/auth/o/connect${location.search}`, { | ||||
|       headers: { "Authorization": `Bearer ${getAtk()}` } | ||||
|     }); | ||||
|  | ||||
|     if (res.status !== 200) { | ||||
|       setError(await res.text()); | ||||
|     } else { | ||||
|       const data = await res.json(); | ||||
|  | ||||
|       if (data["session"]) { | ||||
|         setPanel(1); | ||||
|         redirect(data["session"]); | ||||
|       } else { | ||||
|         setClient(data["client"]); | ||||
|         setLoading(false); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   useEffect(() => { | ||||
|     preconnect().then(() => console.log("Fetched metadata")); | ||||
|   }, []); | ||||
|  | ||||
|   function decline() { | ||||
|     if (window.history.length > 0) { | ||||
|       window.history.back(); | ||||
|     } else { | ||||
|       window.close(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   async function approve() { | ||||
|     setLoading(true); | ||||
|  | ||||
|     const res = await request("/api/auth/o/connect?" + new URLSearchParams({ | ||||
|       client_id: searchParams.get("client_id") as string, | ||||
|       redirect_uri: encodeURIComponent(searchParams.get("redirect_uri") as string), | ||||
|       response_type: "code", | ||||
|       scope: searchParams.get("scope") as string | ||||
|     }), { | ||||
|       method: "POST", | ||||
|       headers: { "Authorization": `Bearer ${getAtk()}` } | ||||
|     }); | ||||
|  | ||||
|     if (res.status !== 200) { | ||||
|       setError(await res.text()); | ||||
|       setLoading(false); | ||||
|     } else { | ||||
|       const data = await res.json(); | ||||
|       setPanel(1); | ||||
|       setTimeout(() => redirect(data["session"]), 1850); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   function redirect(session: any) { | ||||
|     const url = `${searchParams.get("redirect_uri")}?code=${session["grant_token"]}&state=${searchParams.get("state")}`; | ||||
|     window.open(url, "_self"); | ||||
|   } | ||||
|  | ||||
|   const elements = [ | ||||
|     ( | ||||
|       <> | ||||
|         <Avatar sx={{ m: 1, bgcolor: "secondary.main" }}> | ||||
|           <OutletIcon /> | ||||
|         </Avatar> | ||||
|         <Typography component="h1" variant="h5"> | ||||
|           Sign in to {client?.name} | ||||
|         </Typography> | ||||
|         <Box sx={{ mt: 3, width: "100%" }}> | ||||
|           <Grid container spacing={2}> | ||||
|             <Grid item xs={12}> | ||||
|               <Typography fontWeight="bold">About this app</Typography> | ||||
|               <Typography variant="body2">{client?.description}</Typography> | ||||
|             </Grid> | ||||
|             <Grid item xs={12}> | ||||
|               <Typography fontWeight="bold">Make you trust this app</Typography> | ||||
|               <Typography variant="body2"> | ||||
|                 After you click Approve button, you will share your basic personal information to this application | ||||
|                 developer. Some of them will leak your data. Think twice. | ||||
|               </Typography> | ||||
|             </Grid> | ||||
|             <Grid item xs={12} md={6}> | ||||
|               <Button | ||||
|                 fullWidth | ||||
|                 color="info" | ||||
|                 variant="outlined" | ||||
|                 disabled={loading} | ||||
|                 sx={{ mt: 3 }} | ||||
|                 onClick={() => decline()} | ||||
|               > | ||||
|                 Decline | ||||
|               </Button> | ||||
|             </Grid> | ||||
|             <Grid item xs={12} md={6}> | ||||
|               <Button | ||||
|                 fullWidth | ||||
|                 variant="outlined" | ||||
|                 disabled={loading} | ||||
|                 sx={{ mt: 3 }} | ||||
|                 onClick={() => approve()} | ||||
|               > | ||||
|                 Approve | ||||
|               </Button> | ||||
|             </Grid> | ||||
|           </Grid> | ||||
|         </Box> | ||||
|       </> | ||||
|     ), | ||||
|     ( | ||||
|       <> | ||||
|         <Avatar sx={{ m: 1, bgcolor: "secondary.main" }}> | ||||
|           <WhatshotIcon /> | ||||
|         </Avatar> | ||||
|         <Typography component="h1" variant="h5"> | ||||
|           Authorized | ||||
|         </Typography> | ||||
|         <Box sx={{ mt: 3, width: "100%", textAlign: "center" }}> | ||||
|           <Grid container spacing={2}> | ||||
|             <Grid item xs={12} sx={{ my: 8 }}> | ||||
|               <Typography variant="h6">Now Redirecting...</Typography> | ||||
|               <Typography>Hold on a second, we are going to redirect you to the target.</Typography> | ||||
|             </Grid> | ||||
|           </Grid> | ||||
|         </Box> | ||||
|       </> | ||||
|     ) | ||||
|   ]; | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       {error && <Alert severity="error" className="capitalize" sx={{ mb: 2 }}>{error}</Alert>} | ||||
|  | ||||
|       <Card variant="outlined"> | ||||
|         <Collapse in={loading}> | ||||
|           <LinearProgress /> | ||||
|         </Collapse> | ||||
|  | ||||
|         <CardContent | ||||
|           style={{ padding: "40px 48px 36px" }} | ||||
|           sx={{ | ||||
|             display: "flex", | ||||
|             flexDirection: "column", | ||||
|             alignItems: "center" | ||||
|           }} | ||||
|         > | ||||
|           {elements[panel]} | ||||
|         </CardContent> | ||||
|       </Card> | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										12
									
								
								pkg/views/src/pages/auth/layout.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								pkg/views/src/pages/auth/layout.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| import { Box } from "@mui/material"; | ||||
| import { Outlet } from "react-router-dom"; | ||||
|  | ||||
| export default function AuthLayout() { | ||||
|   return ( | ||||
|     <Box sx={{ height: "100vh", display: "flex", alignItems: "center", justifyContent: "center" }}> | ||||
|       <Box style={{ width: "100vw", maxWidth: "450px" }}> | ||||
|         <Outlet /> | ||||
|       </Box> | ||||
|     </Box> | ||||
|   ) | ||||
| } | ||||
| @@ -277,51 +277,55 @@ export default function SignInPage() { | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <Box sx={{ height: "100vh", display: "flex", alignItems: "center", justifyContent: "center" }}> | ||||
|       <Box style={{ width: "100vw", maxWidth: "450px" }}> | ||||
|         {error && <Alert severity="error" className="capitalize" sx={{ mb: 2 }}>{error}</Alert>} | ||||
|     <> | ||||
|       {error && <Alert severity="error" className="capitalize" sx={{ mb: 2 }}>{error}</Alert>} | ||||
|  | ||||
|         <Card variant="outlined"> | ||||
|           <Collapse in={loading}> | ||||
|             <LinearProgress /> | ||||
|           </Collapse> | ||||
|       <Collapse in={searchParams.has("redirect_uri")}> | ||||
|         <Alert severity="info" sx={{ mb: 2 }}> | ||||
|           You need sign in before take an action. After that, we will take you back to your work. | ||||
|         </Alert> | ||||
|       </Collapse> | ||||
|  | ||||
|           <CardContent | ||||
|             style={{ padding: "40px 48px 36px" }} | ||||
|             sx={{ | ||||
|               display: "flex", | ||||
|               flexDirection: "column", | ||||
|               alignItems: "center" | ||||
|             }} | ||||
|           > | ||||
|             {elements[panel]} | ||||
|           </CardContent> | ||||
|       <Card variant="outlined"> | ||||
|         <Collapse in={loading}> | ||||
|           <LinearProgress /> | ||||
|         </Collapse> | ||||
|  | ||||
|           <Collapse in={challenge != null} unmountOnExit> | ||||
|             <Box> | ||||
|               <Paper square sx={{ pt: 3, px: 5, textAlign: "center" }}> | ||||
|                 <Typography sx={{ mb: 2 }}> | ||||
|                   Risk <b className="font-mono">{challenge?.risk_level}</b>  | ||||
|                   Progress <b className="font-mono">{challenge?.progress}/{challenge?.requirements}</b> | ||||
|                 </Typography> | ||||
|                 <LinearProgress | ||||
|                   variant="determinate" | ||||
|                   value={challenge?.progress / challenge?.requirements * 100} | ||||
|                   sx={{ width: "calc(100%+5rem)", mt: 1, mx: -5 }} | ||||
|                 /> | ||||
|               </Paper> | ||||
|             </Box> | ||||
|           </Collapse> | ||||
|         </Card> | ||||
|         <CardContent | ||||
|           style={{ padding: "40px 48px 36px" }} | ||||
|           sx={{ | ||||
|             display: "flex", | ||||
|             flexDirection: "column", | ||||
|             alignItems: "center" | ||||
|           }} | ||||
|         > | ||||
|           {elements[panel]} | ||||
|         </CardContent> | ||||
|  | ||||
|         <Grid container justifyContent="center" sx={{ mt: 2 }}> | ||||
|           <Grid item> | ||||
|             <Link component={RouterLink} to="/auth/sign-up" variant="body2"> | ||||
|               Haven't an account? Sign up! | ||||
|             </Link> | ||||
|           </Grid> | ||||
|         <Collapse in={challenge != null} unmountOnExit> | ||||
|           <Box> | ||||
|             <Paper square sx={{ pt: 3, px: 5, textAlign: "center" }}> | ||||
|               <Typography sx={{ mb: 2 }}> | ||||
|                 Risk <b className="font-mono">{challenge?.risk_level}</b>  | ||||
|                 Progress <b className="font-mono">{challenge?.progress}/{challenge?.requirements}</b> | ||||
|               </Typography> | ||||
|               <LinearProgress | ||||
|                 variant="determinate" | ||||
|                 value={challenge?.progress / challenge?.requirements * 100} | ||||
|                 sx={{ width: "calc(100%+5rem)", mt: 1, mx: -5 }} | ||||
|               /> | ||||
|             </Paper> | ||||
|           </Box> | ||||
|         </Collapse> | ||||
|       </Card> | ||||
|  | ||||
|       <Grid container justifyContent="center" sx={{ mt: 2 }}> | ||||
|         <Grid item> | ||||
|           <Link component={RouterLink} to="/auth/sign-up" variant="body2"> | ||||
|             Haven't an account? Sign up! | ||||
|           </Link> | ||||
|         </Grid> | ||||
|       </Box> | ||||
|     </Box> | ||||
|       </Grid> | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
| @@ -166,35 +166,33 @@ export default function SignUpPage() { | ||||
|   ]; | ||||
|  | ||||
|   return ( | ||||
|     <Box sx={{ height: "100vh", display: "flex", alignItems: "center", justifyContent: "center" }}> | ||||
|       <Box style={{ width: "100vw", maxWidth: "450px" }}> | ||||
|         {error && <Alert severity="error" className="capitalize" sx={{ mb: 2 }}>{error}</Alert>} | ||||
|     <> | ||||
|       {error && <Alert severity="error" className="capitalize" sx={{ mb: 2 }}>{error}</Alert>} | ||||
|  | ||||
|         <Card variant="outlined"> | ||||
|           <Collapse in={loading}> | ||||
|             <LinearProgress /> | ||||
|           </Collapse> | ||||
|       <Card variant="outlined"> | ||||
|         <Collapse in={loading}> | ||||
|           <LinearProgress /> | ||||
|         </Collapse> | ||||
|  | ||||
|           <CardContent | ||||
|             style={{ padding: "40px 48px 36px" }} | ||||
|             sx={{ | ||||
|               display: "flex", | ||||
|               flexDirection: "column", | ||||
|               alignItems: "center" | ||||
|             }} | ||||
|           > | ||||
|             {!done ? elements[0] : elements[1]} | ||||
|           </CardContent> | ||||
|         </Card> | ||||
|         <CardContent | ||||
|           style={{ padding: "40px 48px 36px" }} | ||||
|           sx={{ | ||||
|             display: "flex", | ||||
|             flexDirection: "column", | ||||
|             alignItems: "center" | ||||
|           }} | ||||
|         > | ||||
|           {!done ? elements[0] : elements[1]} | ||||
|         </CardContent> | ||||
|       </Card> | ||||
|  | ||||
|         <Grid container justifyContent="center" sx={{ mt: 2 }}> | ||||
|           <Grid item> | ||||
|             <Link component={RouterLink} to="/auth/sign-in" variant="body2"> | ||||
|               Already have an account? Sign in! | ||||
|             </Link> | ||||
|           </Grid> | ||||
|       <Grid container justifyContent="center" sx={{ mt: 2 }}> | ||||
|         <Grid item> | ||||
|           <Link component={RouterLink} to="/auth/sign-in" variant="body2"> | ||||
|             Already have an account? Sign in! | ||||
|           </Link> | ||||
|         </Grid> | ||||
|       </Box> | ||||
|     </Box> | ||||
|       </Grid> | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										29
									
								
								pkg/views/src/pages/guard.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								pkg/views/src/pages/guard.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | ||||
| import { useEffect } from "react"; | ||||
| import { Box, CircularProgress } from "@mui/material"; | ||||
| import { Outlet, useLocation, useNavigate } from "react-router-dom"; | ||||
| import { useUserinfo } from "@/stores/userinfo.tsx"; | ||||
|  | ||||
| export default function AuthGuard() { | ||||
|   const { userinfo } = useUserinfo(); | ||||
|  | ||||
|   const navigate = useNavigate(); | ||||
|   const location = useLocation(); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     console.log(userinfo) | ||||
|     if (userinfo?.isReady) { | ||||
|       if (!userinfo?.isLoggedIn) { | ||||
|         const callback = location.pathname + location.search; | ||||
|         navigate({ pathname: "/auth/sign-in", search: `redirect_uri=${callback}` }); | ||||
|       } | ||||
|     } | ||||
|   }, [userinfo]); | ||||
|  | ||||
|   return !userinfo?.isReady ? ( | ||||
|     <Box sx={{ pt: 32, display: "flex", justifyContent: "center", alignItems: "center" }}> | ||||
|       <Box> | ||||
|         <CircularProgress /> | ||||
|       </Box> | ||||
|     </Box> | ||||
|   ) : <Outlet />; | ||||
| } | ||||
| @@ -3,15 +3,17 @@ import { request } from "../scripts/request.ts"; | ||||
| import { createContext, useContext, useState } from "react"; | ||||
|  | ||||
| export interface Userinfo { | ||||
|   isReady: boolean, | ||||
|   isLoggedIn: boolean, | ||||
|   displayName: string, | ||||
|   data: any, | ||||
| } | ||||
|  | ||||
| const defaultUserinfo: Userinfo = { | ||||
|   isReady: false, | ||||
|   isLoggedIn: false, | ||||
|   displayName: "Citizen", | ||||
|   data: null, | ||||
|   data: null | ||||
| }; | ||||
|  | ||||
| const UserinfoContext = createContext<any>({ userinfo: defaultUserinfo }); | ||||
| @@ -28,10 +30,15 @@ export function UserinfoProvider(props: any) { | ||||
|   } | ||||
|  | ||||
|   async function readProfiles() { | ||||
|     if (!checkLoggedIn()) return; | ||||
|     if (!checkLoggedIn()) { | ||||
|       setUserinfo((data) => { | ||||
|         data.isReady = true; | ||||
|         return data; | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     const res = await request("/api/users/me", { | ||||
|       credentials: "include" | ||||
|       headers: { "Authorization": `Bearer ${getAtk()}` } | ||||
|     }); | ||||
|  | ||||
|     if (res.status !== 200) { | ||||
| @@ -42,6 +49,7 @@ export function UserinfoProvider(props: any) { | ||||
|     const data = await res.json(); | ||||
|  | ||||
|     setUserinfo({ | ||||
|       isReady: true, | ||||
|       isLoggedIn: true, | ||||
|       displayName: data["nick"], | ||||
|       data: data | ||||
|   | ||||
		Reference in New Issue
	
	Block a user