diff --git a/pkg/views/bun.lockb b/pkg/views/bun.lockb index 20e87a6..b20726d 100755 Binary files a/pkg/views/bun.lockb and b/pkg/views/bun.lockb differ diff --git a/pkg/views/src/main.tsx b/pkg/views/src/main.tsx index 942fdfb..e65d710 100644 --- a/pkg/views/src/main.tsx +++ b/pkg/views/src/main.tsx @@ -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: }, { - path: "/users", - element: , + path: "/", + element: , children: [ - { path: "/users", element: }, - { path: "/users/notifications", element: }, - { path: "/users/personalize", element: }, - { path: "/users/security", element: } + { + path: "/users", + element: , + children: [ + { path: "/users", element: }, + { path: "/users/notifications", element: }, + { path: "/users/personalize", element: }, + { path: "/users/security", element: } + ] + } ] } ] }, - { path: "/auth/sign-up", element: , errorElement: }, - { path: "/auth/sign-in", element: , errorElement: } + { + path: "/auth", + element: , + errorElement: , + children: [ + { path: "/auth/sign-up", element: , errorElement: }, + { path: "/auth/sign-in", element: , errorElement: }, + { path: "/auth/o/connect", element: , errorElement: } + ] + } ]); const element = ( diff --git a/pkg/views/src/pages/auth/connect.tsx b/pkg/views/src/pages/auth/connect.tsx new file mode 100644 index 0000000..567a3ed --- /dev/null +++ b/pkg/views/src/pages/auth/connect.tsx @@ -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(null); + const [loading, setLoading] = useState(false); + + const [client, setClient] = useState(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 = [ + ( + <> + + + + + Sign in to {client?.name} + + + + + About this app + {client?.description} + + + Make you trust this app + + 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. + + + + + + + + + + + + ), + ( + <> + + + + + Authorized + + + + + Now Redirecting... + Hold on a second, we are going to redirect you to the target. + + + + + ) + ]; + + return ( + <> + {error && {error}} + + + + + + + + {elements[panel]} + + + + ); +} \ No newline at end of file diff --git a/pkg/views/src/pages/auth/layout.tsx b/pkg/views/src/pages/auth/layout.tsx new file mode 100644 index 0000000..f9dec4c --- /dev/null +++ b/pkg/views/src/pages/auth/layout.tsx @@ -0,0 +1,12 @@ +import { Box } from "@mui/material"; +import { Outlet } from "react-router-dom"; + +export default function AuthLayout() { + return ( + + + + + + ) +} \ No newline at end of file diff --git a/pkg/views/src/pages/auth/sign-in.tsx b/pkg/views/src/pages/auth/sign-in.tsx index 2173d10..22a62a7 100644 --- a/pkg/views/src/pages/auth/sign-in.tsx +++ b/pkg/views/src/pages/auth/sign-in.tsx @@ -277,51 +277,55 @@ export default function SignInPage() { } return ( - - - {error && {error}} + <> + {error && {error}} - - - - + + + You need sign in before take an action. After that, we will take you back to your work. + + - - {elements[panel]} - + + + + - - - - - Risk {challenge?.risk_level}  - Progress {challenge?.progress}/{challenge?.requirements} - - - - - - + + {elements[panel]} + - - - - Haven't an account? Sign up! - - + + + + + Risk {challenge?.risk_level}  + Progress {challenge?.progress}/{challenge?.requirements} + + + + + + + + + + + Haven't an account? Sign up! + - - + + ); } \ No newline at end of file diff --git a/pkg/views/src/pages/auth/sign-up.tsx b/pkg/views/src/pages/auth/sign-up.tsx index ba3dc8b..0af8031 100644 --- a/pkg/views/src/pages/auth/sign-up.tsx +++ b/pkg/views/src/pages/auth/sign-up.tsx @@ -166,35 +166,33 @@ export default function SignUpPage() { ]; return ( - - - {error && {error}} + <> + {error && {error}} - - - - + + + + - - {!done ? elements[0] : elements[1]} - - + + {!done ? elements[0] : elements[1]} + + - - - - Already have an account? Sign in! - - + + + + Already have an account? Sign in! + - - + + ); } \ No newline at end of file diff --git a/pkg/views/src/pages/guard.tsx b/pkg/views/src/pages/guard.tsx new file mode 100644 index 0000000..ce39a4c --- /dev/null +++ b/pkg/views/src/pages/guard.tsx @@ -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 ? ( + + + + + + ) : ; +} \ No newline at end of file diff --git a/pkg/views/src/stores/userinfo.tsx b/pkg/views/src/stores/userinfo.tsx index e3400f7..4f70622 100644 --- a/pkg/views/src/stores/userinfo.tsx +++ b/pkg/views/src/stores/userinfo.tsx @@ -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({ 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