✨ OAuth & Auth Guard
This commit is contained in:
parent
e4ace4324a
commit
0a9369aba5
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;
|
||||
@ -44,6 +47,10 @@ const router = createBrowserRouter([
|
||||
errorElement: <ErrorBoundary />,
|
||||
children: [
|
||||
{ path: "/", element: <LandingPage /> },
|
||||
{
|
||||
path: "/",
|
||||
element: <AuthGuard />,
|
||||
children: [
|
||||
{
|
||||
path: "/users",
|
||||
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-in", element: <SignInPage />, 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,10 +277,15 @@ 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>}
|
||||
|
||||
<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">
|
||||
<Collapse in={loading}>
|
||||
<LinearProgress />
|
||||
@ -321,7 +326,6 @@ export default function SignInPage() {
|
||||
</Link>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
@ -166,8 +166,7 @@ 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>}
|
||||
|
||||
<Card variant="outlined">
|
||||
@ -194,7 +193,6 @@ export default function SignUpPage() {
|
||||
</Link>
|
||||
</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";
|
||||
|
||||
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
|
||||
|
Loading…
Reference in New Issue
Block a user