♻️ 按照 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 LandingPage from "@/pages/landing.tsx"; | ||||||
| import SignUpPage from "@/pages/auth/sign-up.tsx"; | import SignUpPage from "@/pages/auth/sign-up.tsx"; | ||||||
| import SignInPage from "@/pages/auth/sign-in.tsx"; | import SignInPage from "@/pages/auth/sign-in.tsx"; | ||||||
|  | import OauthConnectPage from "@/pages/auth/connect.tsx"; | ||||||
| import DashboardPage from "@/pages/users/dashboard.tsx"; | import DashboardPage from "@/pages/users/dashboard.tsx"; | ||||||
| import ErrorBoundary from "@/error.tsx"; | import ErrorBoundary from "@/error.tsx"; | ||||||
| import AppLoader from "@/components/AppLoader.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 SecurityPage from "@/pages/users/security.tsx"; | ||||||
| import { UserinfoProvider } from "@/stores/userinfo.tsx"; | import { UserinfoProvider } from "@/stores/userinfo.tsx"; | ||||||
| import { WellKnownProvider } from "@/stores/wellKnown.tsx"; | import { WellKnownProvider } from "@/stores/wellKnown.tsx"; | ||||||
|  | import AuthLayout from "@/pages/auth/layout.tsx"; | ||||||
|  | import AuthGuard from "@/pages/guard.tsx"; | ||||||
|  |  | ||||||
| declare const __GARFISH_EXPORTS__: { | declare const __GARFISH_EXPORTS__: { | ||||||
|   provider: Object; |   provider: Object; | ||||||
| @@ -44,6 +47,10 @@ const router = createBrowserRouter([ | |||||||
|     errorElement: <ErrorBoundary />, |     errorElement: <ErrorBoundary />, | ||||||
|     children: [ |     children: [ | ||||||
|       { path: "/", element: <LandingPage /> }, |       { path: "/", element: <LandingPage /> }, | ||||||
|  |       { | ||||||
|  |         path: "/", | ||||||
|  |         element: <AuthGuard />, | ||||||
|  |         children: [ | ||||||
|           { |           { | ||||||
|             path: "/users", |             path: "/users", | ||||||
|             element: <UserLayout />, |             element: <UserLayout />, | ||||||
| @@ -55,9 +62,19 @@ const router = createBrowserRouter([ | |||||||
|             ] |             ] | ||||||
|           } |           } | ||||||
|         ] |         ] | ||||||
|  |       } | ||||||
|  |     ] | ||||||
|   }, |   }, | ||||||
|  |   { | ||||||
|  |     path: "/auth", | ||||||
|  |     element: <AuthLayout />, | ||||||
|  |     errorElement: <ErrorBoundary />, | ||||||
|  |     children: [ | ||||||
|       { path: "/auth/sign-up", element: <SignUpPage />, errorElement: <ErrorBoundary /> }, |       { path: "/auth/sign-up", element: <SignUpPage />, errorElement: <ErrorBoundary /> }, | ||||||
|   { path: "/auth/sign-in", element: <SignInPage />, errorElement: <ErrorBoundary /> } |       { path: "/auth/sign-in", element: <SignInPage />, errorElement: <ErrorBoundary /> }, | ||||||
|  |       { path: "/auth/o/connect", element: <OauthConnectPage />, errorElement: <ErrorBoundary /> } | ||||||
|  |     ] | ||||||
|  |   } | ||||||
| ]); | ]); | ||||||
|  |  | ||||||
| const element = ( | 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,10 +277,15 @@ export default function SignInPage() { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   return ( |   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>} | ||||||
|  |  | ||||||
|  |       <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> | ||||||
|  |  | ||||||
|       <Card variant="outlined"> |       <Card variant="outlined"> | ||||||
|         <Collapse in={loading}> |         <Collapse in={loading}> | ||||||
|           <LinearProgress /> |           <LinearProgress /> | ||||||
| @@ -321,7 +326,6 @@ export default function SignInPage() { | |||||||
|           </Link> |           </Link> | ||||||
|         </Grid> |         </Grid> | ||||||
|       </Grid> |       </Grid> | ||||||
|       </Box> |     </> | ||||||
|     </Box> |  | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
| @@ -166,8 +166,7 @@ export default function SignUpPage() { | |||||||
|   ]; |   ]; | ||||||
|  |  | ||||||
|   return ( |   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"> |       <Card variant="outlined"> | ||||||
| @@ -194,7 +193,6 @@ export default function SignUpPage() { | |||||||
|           </Link> |           </Link> | ||||||
|         </Grid> |         </Grid> | ||||||
|       </Grid> |       </Grid> | ||||||
|       </Box> |     </> | ||||||
|     </Box> |  | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
							
								
								
									
										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"; | import { createContext, useContext, useState } from "react"; | ||||||
|  |  | ||||||
| export interface Userinfo { | export interface Userinfo { | ||||||
|  |   isReady: boolean, | ||||||
|   isLoggedIn: boolean, |   isLoggedIn: boolean, | ||||||
|   displayName: string, |   displayName: string, | ||||||
|   data: any, |   data: any, | ||||||
| } | } | ||||||
|  |  | ||||||
| const defaultUserinfo: Userinfo = { | const defaultUserinfo: Userinfo = { | ||||||
|  |   isReady: false, | ||||||
|   isLoggedIn: false, |   isLoggedIn: false, | ||||||
|   displayName: "Citizen", |   displayName: "Citizen", | ||||||
|   data: null, |   data: null | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const UserinfoContext = createContext<any>({ userinfo: defaultUserinfo }); | const UserinfoContext = createContext<any>({ userinfo: defaultUserinfo }); | ||||||
| @@ -28,10 +30,15 @@ export function UserinfoProvider(props: any) { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   async function readProfiles() { |   async function readProfiles() { | ||||||
|     if (!checkLoggedIn()) return; |     if (!checkLoggedIn()) { | ||||||
|  |       setUserinfo((data) => { | ||||||
|  |         data.isReady = true; | ||||||
|  |         return data; | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     const res = await request("/api/users/me", { |     const res = await request("/api/users/me", { | ||||||
|       credentials: "include" |       headers: { "Authorization": `Bearer ${getAtk()}` } | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     if (res.status !== 200) { |     if (res.status !== 200) { | ||||||
| @@ -42,6 +49,7 @@ export function UserinfoProvider(props: any) { | |||||||
|     const data = await res.json(); |     const data = await res.json(); | ||||||
|  |  | ||||||
|     setUserinfo({ |     setUserinfo({ | ||||||
|  |       isReady: true, | ||||||
|       isLoggedIn: true, |       isLoggedIn: true, | ||||||
|       displayName: data["nick"], |       displayName: data["nick"], | ||||||
|       data: data |       data: data | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user