mirror of
https://github.com/pomerium/pomerium.git
synced 2025-06-01 10:22:43 +02:00
Style update for User Info Endpoint (#3055)
* style changes from mui 5 * fix spacing issue on small screen * remove unneeded import * add default exports * make linter happy * more style changes * use startCase from lodash Co-authored-by: Caleb Doxsey <cdoxsey@pomerium.com>
This commit is contained in:
parent
f0843d6f44
commit
fd8ec0099e
16 changed files with 400 additions and 214 deletions
|
@ -6,11 +6,12 @@ import UserInfoPage from "./components/UserInfoPage";
|
||||||
import WebAuthnRegistrationPage from "./components/WebAuthnRegistrationPage";
|
import WebAuthnRegistrationPage from "./components/WebAuthnRegistrationPage";
|
||||||
import { createTheme } from "./theme";
|
import { createTheme } from "./theme";
|
||||||
import { PageData } from "./types";
|
import { PageData } from "./types";
|
||||||
import Container from "@mui/material/Container";
|
|
||||||
import CssBaseline from "@mui/material/CssBaseline";
|
import CssBaseline from "@mui/material/CssBaseline";
|
||||||
import Stack from "@mui/material/Stack";
|
|
||||||
import { ThemeProvider } from "@mui/material/styles";
|
import { ThemeProvider } from "@mui/material/styles";
|
||||||
import React, { FC } from "react";
|
import React, { FC } from "react";
|
||||||
|
import {ToolbarOffset} from "./components/ToolbarOffset";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import {SubpageContextProvider} from "./context/Subpage";
|
||||||
|
|
||||||
const theme = createTheme();
|
const theme = createTheme();
|
||||||
|
|
||||||
|
@ -34,13 +35,22 @@ const App: FC = () => {
|
||||||
return (
|
return (
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
<Container maxWidth="md" disableGutters>
|
<SubpageContextProvider>
|
||||||
<Stack spacing={3}>
|
<Header
|
||||||
<Header csrfToken={data?.csrfToken} signOutUrl={data?.signOutUrl} />
|
includeSidebar={data?.page === "UserInfo"}
|
||||||
{body}
|
data={data}
|
||||||
<Footer />
|
/>
|
||||||
</Stack>
|
<ToolbarOffset />
|
||||||
</Container>
|
<Box
|
||||||
|
sx={{overflow: 'hidden', height: 'calc(100vh - 120px)'}}
|
||||||
|
>
|
||||||
|
<Box sx={{overflow: 'auto', height: '100%', paddingTop: theme.spacing(5)}}>
|
||||||
|
{body}
|
||||||
|
<ToolbarOffset />
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Footer />
|
||||||
|
</SubpageContextProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
20
ui/src/components/Avatar.tsx
Normal file
20
ui/src/components/Avatar.tsx
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import React from 'react';
|
||||||
|
import {User} from "react-feather";
|
||||||
|
import MuiAvatar from "@mui/material/Avatar";
|
||||||
|
|
||||||
|
type AvatarProps = {
|
||||||
|
name: string;
|
||||||
|
url?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Avatar = ({url, name}:AvatarProps): JSX.Element => {
|
||||||
|
if (url === 'https://graph.microsoft.com/v1.0/me/photo/$value') {
|
||||||
|
url = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return url ? (
|
||||||
|
<MuiAvatar alt={name} src={url} />
|
||||||
|
) : (
|
||||||
|
<User />
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,57 +0,0 @@
|
||||||
import { Claims } from "../types";
|
|
||||||
import ClaimValue from "./ClaimValue";
|
|
||||||
import Alert from "@mui/material/Alert";
|
|
||||||
import Table from "@mui/material/Table";
|
|
||||||
import TableBody from "@mui/material/TableBody";
|
|
||||||
import TableCell from "@mui/material/TableCell";
|
|
||||||
import TableContainer from "@mui/material/TableContainer";
|
|
||||||
import TableHead from "@mui/material/TableHead";
|
|
||||||
import TableRow from "@mui/material/TableRow";
|
|
||||||
import React, { FC } from "react";
|
|
||||||
|
|
||||||
type ClaimsTableProps = {
|
|
||||||
claims: Claims;
|
|
||||||
};
|
|
||||||
const ClaimsTable: FC<ClaimsTableProps> = ({ claims }) => {
|
|
||||||
const entries = Object.entries(claims || {});
|
|
||||||
entries.sort(([a], [b]) => a.localeCompare(b));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableContainer>
|
|
||||||
<Table size="small">
|
|
||||||
<TableHead>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell variant="head">Claims</TableCell>
|
|
||||||
<TableCell variant="head"></TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableHead>
|
|
||||||
<TableBody>
|
|
||||||
{entries.length > 0 ? (
|
|
||||||
entries.map(([key, values]) => (
|
|
||||||
<TableRow key={key}>
|
|
||||||
<TableCell>{key}</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
{values?.map((v, i) => (
|
|
||||||
<React.Fragment key={`${v}`}>
|
|
||||||
{i > 0 ? <br /> : <></>}
|
|
||||||
<ClaimValue claimKey={key} claimValue={v} />
|
|
||||||
</React.Fragment>
|
|
||||||
))}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<TableRow>
|
|
||||||
<TableCell colSpan={2} padding="none">
|
|
||||||
<Alert severity="warning" square={true}>
|
|
||||||
No Claims Found
|
|
||||||
</Alert>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
)}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</TableContainer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
export default ClaimsTable;
|
|
|
@ -8,7 +8,7 @@ type DeviceEnrolledPageProps = {
|
||||||
};
|
};
|
||||||
const DeviceEnrolledPage: FC<DeviceEnrolledPageProps> = () => {
|
const DeviceEnrolledPage: FC<DeviceEnrolledPageProps> = () => {
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container maxWidth={false}>
|
||||||
<HeroSection
|
<HeroSection
|
||||||
title="Device Enrolled"
|
title="Device Enrolled"
|
||||||
text="Device Successfully Enrolled"
|
text="Device Successfully Enrolled"
|
||||||
|
|
|
@ -14,7 +14,7 @@ export type ErrorPageProps = {
|
||||||
};
|
};
|
||||||
export const ErrorPage: FC<ErrorPageProps> = ({ data }) => {
|
export const ErrorPage: FC<ErrorPageProps> = ({ data }) => {
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container maxWidth={false}>
|
||||||
<Paper sx={{ overflow: "hidden" }}>
|
<Paper sx={{ overflow: "hidden" }}>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Box sx={{ padding: "16px" }}>
|
<Box sx={{ padding: "16px" }}>
|
||||||
|
|
|
@ -1,39 +1,51 @@
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Container from "@mui/material/Container";
|
|
||||||
import Stack from "@mui/material/Stack";
|
import Stack from "@mui/material/Stack";
|
||||||
import React, { FC } from "react";
|
import React, { FC } from "react";
|
||||||
|
import {FooterLink} from "./FooterLink";
|
||||||
|
import AppBar from "@mui/material/AppBar";
|
||||||
|
|
||||||
const Footer: FC = () => {
|
const Footer: FC = () => {
|
||||||
return (
|
return (
|
||||||
<Container component="footer">
|
<AppBar
|
||||||
|
position="fixed"
|
||||||
|
sx={{
|
||||||
|
zIndex: (theme) => theme.zIndex.drawer + 1,
|
||||||
|
top: 'auto',
|
||||||
|
bottom: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Stack
|
<Stack
|
||||||
direction="row"
|
direction="row"
|
||||||
spacing={2}
|
spacing={8}
|
||||||
|
justifyContent="center"
|
||||||
sx={{
|
sx={{
|
||||||
fontSize: "0.85rem",
|
fontSize: "0.85rem",
|
||||||
padding: "16px"
|
padding: "16px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box>
|
<Box>
|
||||||
<a href="https://pomerium.com/">Home</a>
|
<FooterLink
|
||||||
|
href="https://pomerium.com/"
|
||||||
|
>
|
||||||
|
Home
|
||||||
|
</FooterLink>
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box>
|
||||||
<a href="https://pomerium.com/docs">Docs</a>
|
<FooterLink
|
||||||
|
href="https://pomerium.com/docs"
|
||||||
|
>
|
||||||
|
Docs
|
||||||
|
</FooterLink>
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box>
|
||||||
<a href="https://pomerium.com/docs/community/">Support</a>
|
<FooterLink
|
||||||
</Box>
|
href="https://discuss.pomerium.com"
|
||||||
<Box>
|
>
|
||||||
<a href="https://github.com/pomerium">GitHub</a>
|
Support
|
||||||
</Box>
|
</FooterLink>
|
||||||
<Box>
|
|
||||||
<a href="https://twitter.com/pomerium_io">@pomerium_io</a>
|
|
||||||
</Box>
|
|
||||||
<Box flexGrow={1} sx={{ textAlign: "right" }}>
|
|
||||||
© Pomerium, Inc.
|
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Container>
|
</AppBar>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
export default Footer;
|
export default Footer;
|
||||||
|
|
10
ui/src/components/FooterLink.tsx
Normal file
10
ui/src/components/FooterLink.tsx
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { styled } from '@mui/system';
|
||||||
|
import {Link} from "@mui/material";
|
||||||
|
|
||||||
|
export const FooterLink = styled(Link)(({ theme }) => ({
|
||||||
|
fontSize: '1.25rem',
|
||||||
|
fontWeight: `bold`,
|
||||||
|
color: theme.palette.background.default,
|
||||||
|
}));
|
||||||
|
export default FooterLink;
|
|
@ -1,32 +1,124 @@
|
||||||
import CsrfInput from "./CsrfInput";
|
|
||||||
import Logo from "./Logo";
|
import Logo from "./Logo";
|
||||||
import AppBar from "@mui/material/AppBar";
|
import AppBar from "@mui/material/AppBar";
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Button from "@mui/material/Button";
|
|
||||||
import Toolbar from "@mui/material/Toolbar";
|
import Toolbar from "@mui/material/Toolbar";
|
||||||
import React, { FC } from "react";
|
import React, {FC, useState} from "react";
|
||||||
|
import {useTheme} from "@mui/material/styles";
|
||||||
|
import {Drawer, IconButton, Menu, MenuItem, useMediaQuery} from "@mui/material";
|
||||||
|
import {ToolbarOffset} from "./ToolbarOffset";
|
||||||
|
import UserSidebarContent from "./UserSidebarContent";
|
||||||
|
import {ChevronLeft, ChevronRight, Menu as MenuIcon} from "react-feather";
|
||||||
|
import styled from "@mui/material/styles/styled";
|
||||||
|
import {Avatar} from "./Avatar";
|
||||||
|
import {PageData} from "../types";
|
||||||
|
import {get} from 'lodash';
|
||||||
|
|
||||||
|
const DrawerHeader = styled('div')(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: theme.spacing(0, 1),
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
}));
|
||||||
|
|
||||||
type HeaderProps = {
|
type HeaderProps = {
|
||||||
csrfToken: string;
|
includeSidebar: boolean;
|
||||||
signOutUrl: string;
|
data: PageData;
|
||||||
};
|
};
|
||||||
const Header: FC<HeaderProps> = ({ csrfToken, signOutUrl }) => {
|
const Header: FC<HeaderProps> = ({ includeSidebar, data }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const mdUp = useMediaQuery(() => theme.breakpoints.up('md'), {
|
||||||
|
defaultMatches: true,
|
||||||
|
noSsr: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||||
|
const [anchorEl, setAnchorEl] = React.useState(null);
|
||||||
|
const handleMenuOpen = e => {
|
||||||
|
setAnchorEl(e.currentTarget);
|
||||||
|
};
|
||||||
|
const handleMenuClose = () => {
|
||||||
|
setAnchorEl(null);
|
||||||
|
};
|
||||||
|
const userName = get(data, 'user.name') || get(data, 'user.claims.given_name');
|
||||||
|
|
||||||
|
const handleDrawerOpen = () => {
|
||||||
|
setDrawerOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDrawerClose = ():void => {
|
||||||
|
setDrawerOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLogout = (evt: React.MouseEvent):void => {
|
||||||
|
evt.preventDefault();
|
||||||
|
location.href = "/.pomerium/sign_out";
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppBar position="sticky">
|
<AppBar
|
||||||
|
position="fixed"
|
||||||
|
sx={{ zIndex: (theme) => theme.zIndex.drawer + 1 }}
|
||||||
|
>
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
<a href="/.pomerium">
|
{!mdUp && includeSidebar ? (
|
||||||
<Logo />
|
<>
|
||||||
</a>
|
<IconButton
|
||||||
<Box flexGrow={1} />
|
color="inherit"
|
||||||
{signOutUrl ? (
|
aria-label="open drawer"
|
||||||
<form action={signOutUrl}>
|
onClick={handleDrawerOpen}
|
||||||
<CsrfInput csrfToken={csrfToken} />
|
edge="start"
|
||||||
<Button variant="text" color="inherit" type="submit">
|
sx={{ mr: 2, ...(drawerOpen && { display: 'none' }) }}
|
||||||
Logout
|
>
|
||||||
</Button>
|
<MenuIcon />
|
||||||
</form>
|
</IconButton>
|
||||||
|
<Drawer
|
||||||
|
sx={{
|
||||||
|
width: 256,
|
||||||
|
flexShrink: 0,
|
||||||
|
'& .MuiDrawer-paper': {
|
||||||
|
width: 256,
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
backgroundColor: 'neutral.900',
|
||||||
|
height: '100vh',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
variant="persistent"
|
||||||
|
anchor="left"
|
||||||
|
open={drawerOpen}
|
||||||
|
>
|
||||||
|
<DrawerHeader>
|
||||||
|
<IconButton onClick={handleDrawerClose}>
|
||||||
|
{theme.direction === 'ltr' ? <ChevronLeft /> : <ChevronRight />}
|
||||||
|
</IconButton>
|
||||||
|
</DrawerHeader>
|
||||||
|
<UserSidebarContent close={handleDrawerClose}/>
|
||||||
|
<ToolbarOffset />
|
||||||
|
</Drawer>
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<></>
|
<a href="/.pomerium">
|
||||||
|
<Logo />
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
<Box flexGrow={1} />
|
||||||
|
{userName && (
|
||||||
|
<>
|
||||||
|
<IconButton color="inherit" onClick={handleMenuOpen}>
|
||||||
|
<Avatar name={userName} url={get(data, 'claims.picture', null)} />
|
||||||
|
</IconButton>
|
||||||
|
<Menu
|
||||||
|
onClose={handleMenuClose}
|
||||||
|
anchorOrigin={{
|
||||||
|
vertical: 'bottom',
|
||||||
|
horizontal: 'center'
|
||||||
|
}}
|
||||||
|
keepMounted
|
||||||
|
open={!!anchorEl}
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
>
|
||||||
|
<MenuItem onClick={handleLogout}>Logout</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
</AppBar>
|
</AppBar>
|
||||||
|
|
|
@ -21,10 +21,10 @@ export const Section: FC<SectionProps> = ({
|
||||||
<Paper sx={{ overflow: "hidden" }}>
|
<Paper sx={{ overflow: "hidden" }}>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
<Typography variant="h4" flexGrow={1}>
|
<Typography variant="h4">
|
||||||
{title}
|
{title}
|
||||||
</Typography>
|
</Typography>
|
||||||
{icon ? <Box>{icon}</Box> : <></>}
|
{!!icon && (<Box sx={{marginLeft: (theme) => theme.spacing(3)}}>{icon}</Box>)}
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
<Box sx={{ padding: 3, paddingTop: 0 }}>{children}</Box>
|
<Box sx={{ padding: 3, paddingTop: 0 }}>{children}</Box>
|
||||||
{footer ? (
|
{footer ? (
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { Session } from "../types";
|
import { Session } from "../types";
|
||||||
import ClaimsTable from "./ClaimsTable";
|
|
||||||
import IDField from "./IDField";
|
import IDField from "./IDField";
|
||||||
import Section from "./Section";
|
import Section from "./Section";
|
||||||
import Stack from "@mui/material/Stack";
|
import Stack from "@mui/material/Stack";
|
||||||
|
@ -9,37 +8,52 @@ import TableCell from "@mui/material/TableCell";
|
||||||
import TableContainer from "@mui/material/TableContainer";
|
import TableContainer from "@mui/material/TableContainer";
|
||||||
import TableRow from "@mui/material/TableRow";
|
import TableRow from "@mui/material/TableRow";
|
||||||
import React, { FC } from "react";
|
import React, { FC } from "react";
|
||||||
|
import ClaimValue from "./ClaimValue";
|
||||||
|
import {startCase} from "lodash";
|
||||||
|
|
||||||
export type SessionDetailsProps = {
|
export type SessionDetailsProps = {
|
||||||
session: Session;
|
session: Session;
|
||||||
};
|
};
|
||||||
export const SessionDetails: FC<SessionDetailsProps> = ({ session }) => {
|
export const SessionDetails: FC<SessionDetailsProps> = ({ session }) => {
|
||||||
return (
|
return (
|
||||||
<Section title="Session Details">
|
<Section title="User Details">
|
||||||
<Stack spacing={3}>
|
<Stack spacing={3}>
|
||||||
<TableContainer>
|
<TableContainer>
|
||||||
<Table size="small">
|
<Table size="small">
|
||||||
<TableBody>
|
<TableBody>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell>ID</TableCell>
|
<TableCell width={'18%'} variant="head">Session ID</TableCell>
|
||||||
<TableCell>
|
<TableCell align="left">
|
||||||
<IDField value={session?.id} />
|
<IDField value={session?.id} />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell>User ID</TableCell>
|
<TableCell variant="head">User ID</TableCell>
|
||||||
<TableCell>
|
<TableCell align="left">
|
||||||
<IDField value={session?.userId} />
|
<IDField value={session?.userId} />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell>Expires At</TableCell>
|
<TableCell variant="head">Expires At</TableCell>
|
||||||
<TableCell>{session?.expiresAt || ""}</TableCell>
|
<TableCell align="left">{session?.expiresAt || ""}</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
{Object.entries(session?.claims || {}).map(
|
||||||
|
([key, values]) => (
|
||||||
|
<TableRow key={key}>
|
||||||
|
<TableCell variant="head">{startCase(key)}</TableCell>
|
||||||
|
<TableCell align="left">
|
||||||
|
{values?.map((v, i) => (
|
||||||
|
<React.Fragment key={`${v}`}>
|
||||||
|
{i > 0 ? <br /> : <></>}
|
||||||
|
<ClaimValue claimKey={key} claimValue={v} />
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
<ClaimsTable claims={session?.claims} />
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Section>
|
</Section>
|
||||||
);
|
);
|
||||||
|
|
6
ui/src/components/ToolbarOffset.tsx
Normal file
6
ui/src/components/ToolbarOffset.tsx
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import styled from "@mui/material/styles/styled";
|
||||||
|
import {BaseCSSProperties} from "@mui/material/styles/createMixins";
|
||||||
|
|
||||||
|
export const ToolbarOffset = styled('div')(({ theme }) => ({
|
||||||
|
...(theme.mixins.toolbar as BaseCSSProperties),
|
||||||
|
}));
|
|
@ -1,17 +0,0 @@
|
||||||
import { User } from "../types";
|
|
||||||
import ClaimsTable from "./ClaimsTable";
|
|
||||||
import JwtIcon from "./JwtIcon";
|
|
||||||
import Section from "./Section";
|
|
||||||
import React, { FC } from "react";
|
|
||||||
|
|
||||||
export type UserClaimsProps = {
|
|
||||||
user: User;
|
|
||||||
};
|
|
||||||
export const UserClaims: FC<UserClaimsProps> = ({ user }) => {
|
|
||||||
return (
|
|
||||||
<Section title="User Claims" icon={<JwtIcon />}>
|
|
||||||
<ClaimsTable claims={user?.claims} />
|
|
||||||
</Section>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
export default UserClaims;
|
|
|
@ -1,53 +1,70 @@
|
||||||
import GroupDetails from "./GroupDetails";
|
import GroupDetails from "./GroupDetails";
|
||||||
import HeroSection from "./HeroSection";
|
|
||||||
import PersonIcon from "./PersonIcon";
|
|
||||||
import SessionDetails from "./SessionDetails";
|
import SessionDetails from "./SessionDetails";
|
||||||
import SessionDeviceCredentials from "./SessionDeviceCredentials";
|
import SessionDeviceCredentials from "./SessionDeviceCredentials";
|
||||||
import UserClaims from "./UserClaims";
|
|
||||||
import MuiAvatar from "@mui/material/Avatar";
|
|
||||||
import Container from "@mui/material/Container";
|
import Container from "@mui/material/Container";
|
||||||
import Stack from "@mui/material/Stack";
|
import React, {FC, useContext} from "react";
|
||||||
import styled from "@mui/material/styles/styled";
|
|
||||||
import React, { FC } from "react";
|
|
||||||
import { UserInfoPageData } from "src/types";
|
import { UserInfoPageData } from "src/types";
|
||||||
|
import {Drawer, useMediaQuery} from "@mui/material";
|
||||||
const Avatar = styled(MuiAvatar)(({ theme }) => ({
|
import { useTheme } from '@mui/material/styles';
|
||||||
backgroundColor: theme.palette.primary.main,
|
import { ToolbarOffset } from "./ToolbarOffset";
|
||||||
height: 48,
|
import {UserSidebarContent} from "./UserSidebarContent";
|
||||||
width: 48
|
import {SubpageContext} from "../context/Subpage";
|
||||||
}));
|
import Stack from "@mui/material/Stack";
|
||||||
|
|
||||||
type UserInfoPageProps = {
|
type UserInfoPageProps = {
|
||||||
data: UserInfoPageData;
|
data: UserInfoPageData;
|
||||||
};
|
};
|
||||||
const UserInfoPage: FC<UserInfoPageProps> = ({ data }) => {
|
const UserInfoPage: FC<UserInfoPageProps> = ({ data }) => {
|
||||||
const name = data?.user?.claims?.given_name?.[0] || data?.user?.name;
|
const theme = useTheme();
|
||||||
|
const mdUp = useMediaQuery(() => theme.breakpoints.up('md'), {
|
||||||
|
defaultMatches: true,
|
||||||
|
noSsr: false
|
||||||
|
});
|
||||||
|
const {subpage} = useContext(SubpageContext);
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container maxWidth={false}>
|
||||||
<Stack spacing={3}>
|
{mdUp && (
|
||||||
<HeroSection
|
<Drawer
|
||||||
icon={
|
anchor="left"
|
||||||
<Avatar>
|
open
|
||||||
<PersonIcon />
|
PaperProps={{
|
||||||
</Avatar>
|
sx: {
|
||||||
}
|
backgroundColor: 'neutral.900',
|
||||||
title={<>Hi {name}!</>}
|
width: 256,
|
||||||
text={
|
height: '100vh',
|
||||||
<>
|
}
|
||||||
Welcome to the user info endpoint. Here you can view your current
|
}}
|
||||||
session details, and authorization context.
|
variant="persistent"
|
||||||
</>
|
>
|
||||||
}
|
<ToolbarOffset />
|
||||||
/>
|
<UserSidebarContent close={null}/>
|
||||||
<SessionDetails session={data?.session} />
|
<ToolbarOffset />
|
||||||
<UserClaims user={data?.user} />
|
</Drawer>
|
||||||
<GroupDetails groups={data?.directoryGroups} />
|
)}
|
||||||
<SessionDeviceCredentials
|
<Stack
|
||||||
csrfToken={data?.csrfToken}
|
spacing={3}
|
||||||
session={data?.session}
|
sx={{
|
||||||
user={data?.user}
|
marginLeft: mdUp ? '256px' : '0px',
|
||||||
webAuthnUrl={data?.webAuthnUrl}
|
}}>
|
||||||
/>
|
|
||||||
|
{subpage === 'User' && (
|
||||||
|
<SessionDetails session={data?.session} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{subpage === 'Groups Info' && (
|
||||||
|
<GroupDetails groups={data?.directoryGroups} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{subpage === 'Devices Info' && (
|
||||||
|
<SessionDeviceCredentials
|
||||||
|
csrfToken={data?.csrfToken}
|
||||||
|
session={data?.session}
|
||||||
|
user={data?.user}
|
||||||
|
webAuthnUrl={data?.webAuthnUrl}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
|
|
55
ui/src/components/UserSidebarContent.tsx
Normal file
55
ui/src/components/UserSidebarContent.tsx
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
import React, {FC, ReactNode, useContext} from "react";
|
||||||
|
import {SubpageContext} from "../context/Subpage";
|
||||||
|
import {List, ListItemButton, ListItemIcon, ListItemText} from "@mui/material";
|
||||||
|
import {User, Users} from "react-feather";
|
||||||
|
import {Devices} from "@mui/icons-material";
|
||||||
|
|
||||||
|
export interface Subpage {
|
||||||
|
icon: ReactNode;
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const sectionList: Subpage[] = [
|
||||||
|
{
|
||||||
|
title: 'User',
|
||||||
|
icon: <User />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Groups Info',
|
||||||
|
icon: <Users />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Devices Info',
|
||||||
|
icon: <Devices />
|
||||||
|
},
|
||||||
|
]
|
||||||
|
type UserSidebarContent = {
|
||||||
|
close: () => void | null;
|
||||||
|
};
|
||||||
|
export const UserSidebarContent:FC<UserSidebarContent> = ({close}:UserSidebarContent):JSX.Element => {
|
||||||
|
|
||||||
|
const info = useContext(SubpageContext);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<List>
|
||||||
|
{sectionList.map(({title, icon}) => {
|
||||||
|
return (
|
||||||
|
<ListItemButton
|
||||||
|
key={'tab ' + title}
|
||||||
|
selected={title === info.subpage}
|
||||||
|
onClick={() => {
|
||||||
|
info.setSubpage(title)
|
||||||
|
!!close && close();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ListItemIcon>
|
||||||
|
{icon}
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary={title} />
|
||||||
|
</ListItemButton>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</List>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export default UserSidebarContent;
|
|
@ -6,13 +6,15 @@ import Button from "@mui/material/Button";
|
||||||
import Container from "@mui/material/Container";
|
import Container from "@mui/material/Container";
|
||||||
import Paper from "@mui/material/Paper";
|
import Paper from "@mui/material/Paper";
|
||||||
import Stack from "@mui/material/Stack";
|
import Stack from "@mui/material/Stack";
|
||||||
import Typography from "@mui/material/Typography";
|
|
||||||
import React, { FC, useRef, useState } from "react";
|
import React, { FC, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
WebAuthnCreationOptions,
|
WebAuthnCreationOptions,
|
||||||
WebAuthnRegistrationPageData,
|
WebAuthnRegistrationPageData,
|
||||||
WebAuthnRequestOptions
|
WebAuthnRequestOptions
|
||||||
} from "src/types";
|
} from "src/types";
|
||||||
|
import JwtIcon from "./JwtIcon";
|
||||||
|
import ClaimsTable from "./ClaimsTable";
|
||||||
|
import Section from "./Section";
|
||||||
|
|
||||||
type CredentialForAuthenticate = {
|
type CredentialForAuthenticate = {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -154,48 +156,39 @@ const WebAuthnRegistrationPage: FC<WebAuthnRegistrationPageProps> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Section title="WebAuthn Registration" icon={<ExperimentalIcon />}>
|
||||||
<Stack spacing={3}>
|
<Paper sx={{ padding: "16px" }}>
|
||||||
<HeroSection
|
<Stack direction="row" justifyContent="center" spacing={3}>
|
||||||
title={
|
<Button onClick={handleClickRegister} variant="contained">
|
||||||
<>
|
Register New Device
|
||||||
WebAuthn Registration <ExperimentalIcon />
|
</Button>
|
||||||
</>
|
<Button
|
||||||
}
|
onClick={handleClickAuthenticate}
|
||||||
|
variant="contained"
|
||||||
|
disabled={!enableAuthenticate}
|
||||||
|
>
|
||||||
|
Authenticate Existing Device
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
<form ref={authenticateFormRef} method="post" action={data?.selfUrl}>
|
||||||
|
<input type="hidden" name="_pomerium_csrf" value={data?.csrfToken} />
|
||||||
|
<input type="hidden" name="action" value="authenticate" />
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
name="authenticate_response"
|
||||||
|
ref={authenticateResponseRef}
|
||||||
/>
|
/>
|
||||||
<Paper sx={{ padding: "16px" }}>
|
</form>
|
||||||
<Stack direction="row" justifyContent="center" spacing={3}>
|
<form ref={registerFormRef} method="POST" action={data?.selfUrl}>
|
||||||
<Button onClick={handleClickRegister} variant="contained">
|
<input type="hidden" name="_pomerium_csrf" value={data?.csrfToken} />
|
||||||
Register New Device
|
<input type="hidden" name="action" value="register" />
|
||||||
</Button>
|
<input
|
||||||
<Button
|
type="hidden"
|
||||||
onClick={handleClickAuthenticate}
|
name="register_response"
|
||||||
variant="contained"
|
ref={registerResponseRef}
|
||||||
disabled={!enableAuthenticate}
|
/>
|
||||||
>
|
</form>
|
||||||
Authenticate Existing Device
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
|
||||||
</Paper>
|
|
||||||
<form ref={authenticateFormRef} method="post" action={data?.selfUrl}>
|
|
||||||
<input type="hidden" name="_pomerium_csrf" value={data?.csrfToken} />
|
|
||||||
<input type="hidden" name="action" value="authenticate" />
|
|
||||||
<input
|
|
||||||
type="hidden"
|
|
||||||
name="authenticate_response"
|
|
||||||
ref={authenticateResponseRef}
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
<form ref={registerFormRef} method="POST" action={data?.selfUrl}>
|
|
||||||
<input type="hidden" name="_pomerium_csrf" value={data?.csrfToken} />
|
|
||||||
<input type="hidden" name="action" value="register" />
|
|
||||||
<input
|
|
||||||
type="hidden"
|
|
||||||
name="register_response"
|
|
||||||
ref={registerResponseRef}
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
</Stack>
|
|
||||||
<AlertDialog
|
<AlertDialog
|
||||||
title="Error"
|
title="Error"
|
||||||
severity="error"
|
severity="error"
|
||||||
|
@ -204,7 +197,7 @@ const WebAuthnRegistrationPage: FC<WebAuthnRegistrationPageProps> = ({
|
||||||
>
|
>
|
||||||
{error}
|
{error}
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
</Container>
|
</Section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
export default WebAuthnRegistrationPage;
|
export default WebAuthnRegistrationPage;
|
||||||
|
|
31
ui/src/context/Subpage.tsx
Normal file
31
ui/src/context/Subpage.tsx
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import React, {createContext, FC, useState} from 'react'
|
||||||
|
|
||||||
|
export interface SubpageContextValue {
|
||||||
|
subpage: string;
|
||||||
|
setSubpage: (subpage: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SubpageContext = createContext<SubpageContextValue>({
|
||||||
|
subpage: "User",
|
||||||
|
setSubpage: (_: string) => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const SubpageContextProvider:FC = ({children}) => {
|
||||||
|
|
||||||
|
const setSubpage = (subpage: string) => {
|
||||||
|
setState({...state, subpage})
|
||||||
|
}
|
||||||
|
|
||||||
|
const initState = {
|
||||||
|
subpage: "User",
|
||||||
|
setSubpage
|
||||||
|
}
|
||||||
|
|
||||||
|
const [state, setState] = useState(initState)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SubpageContext.Provider value={state}>
|
||||||
|
{children}
|
||||||
|
</SubpageContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue