mirror of
https://github.com/pomerium/pomerium.git
synced 2025-06-12 15:52:53 +02:00
frontend: react+mui (#3004)
* mui v5 wip * wip * wip * wip * use compressor for all controlplane endpoints * wip * wip * add deps * fix authenticate URL * fix test * fix test * fix build * maybe fix build * fix integration test * remove image asset test * add yarn.lock
This commit is contained in:
parent
64d8748251
commit
2824faecbf
84 changed files with 13373 additions and 1455 deletions
47
ui/src/App.tsx
Normal file
47
ui/src/App.tsx
Normal file
|
@ -0,0 +1,47 @@
|
|||
import DeviceEnrolledPage from "./components/DeviceEnrolledPage";
|
||||
import ErrorPage from "./components/ErrorPage";
|
||||
import Footer from "./components/Footer";
|
||||
import Header from "./components/Header";
|
||||
import UserInfoPage from "./components/UserInfoPage";
|
||||
import WebAuthnRegistrationPage from "./components/WebAuthnRegistrationPage";
|
||||
import { createTheme } from "./theme";
|
||||
import { PageData } from "./types";
|
||||
import Container from "@mui/material/Container";
|
||||
import CssBaseline from "@mui/material/CssBaseline";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import { ThemeProvider } from "@mui/material/styles";
|
||||
import React, { FC } from "react";
|
||||
|
||||
const theme = createTheme();
|
||||
|
||||
const App: FC = () => {
|
||||
const data = (window["POMERIUM_DATA"] || {}) as PageData;
|
||||
let body: React.ReactNode = <></>;
|
||||
switch (data?.page) {
|
||||
case "DeviceEnrolled":
|
||||
body = <DeviceEnrolledPage data={data} />;
|
||||
break;
|
||||
case "Error":
|
||||
body = <ErrorPage data={data} />;
|
||||
break;
|
||||
case "UserInfo":
|
||||
body = <UserInfoPage data={data} />;
|
||||
break;
|
||||
case "WebAuthnRegistration":
|
||||
body = <WebAuthnRegistrationPage data={data} />;
|
||||
break;
|
||||
}
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
<Container maxWidth="md" disableGutters>
|
||||
<Stack spacing={3}>
|
||||
<Header csrfToken={data?.csrfToken} signOutUrl={data?.signOutUrl} />
|
||||
{body}
|
||||
<Footer />
|
||||
</Stack>
|
||||
</Container>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
export default App;
|
30
ui/src/components/AlertDialog.tsx
Normal file
30
ui/src/components/AlertDialog.tsx
Normal file
|
@ -0,0 +1,30 @@
|
|||
import Alert, { AlertColor } from "@mui/material/Alert";
|
||||
import Dialog, { DialogProps } from "@mui/material/Dialog";
|
||||
import DialogActions from "@mui/material/DialogActions";
|
||||
import DialogContent from "@mui/material/DialogContent";
|
||||
import DialogTitle from "@mui/material/DialogTitle";
|
||||
import React, { FC } from "react";
|
||||
|
||||
export type AlertDialogProps = DialogProps & {
|
||||
title?: React.ReactNode;
|
||||
severity?: AlertColor;
|
||||
actions?: React.ReactNode;
|
||||
};
|
||||
export const AlertDialog: FC<AlertDialogProps> = ({
|
||||
title,
|
||||
severity,
|
||||
children,
|
||||
actions,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<Dialog transitionDuration={{ exit: 0 }} {...props}>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Alert severity={severity || "info"}>{children}</Alert>
|
||||
</DialogContent>
|
||||
{actions ? <DialogActions>{actions}</DialogActions> : <></>}
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
export default AlertDialog;
|
24
ui/src/components/ClaimValue.tsx
Normal file
24
ui/src/components/ClaimValue.tsx
Normal file
|
@ -0,0 +1,24 @@
|
|||
import IDField from "./IDField";
|
||||
import { DateTime } from "luxon";
|
||||
import React, { FC } from "react";
|
||||
|
||||
const unixSecondTimestampFields = new Set(["exp", "iat", "nbf", "auth_time"]);
|
||||
|
||||
const idFields = new Set(["groups", "jti", "oid", "tid", "wids"]);
|
||||
|
||||
type ClaimValueProps = {
|
||||
claimKey: string;
|
||||
claimValue: unknown;
|
||||
};
|
||||
const ClaimValue: FC<ClaimValueProps> = ({ claimKey, claimValue }) => {
|
||||
if (unixSecondTimestampFields.has(claimKey)) {
|
||||
return <>{DateTime.fromMillis((claimValue as number) * 1000).toISO()}</>;
|
||||
}
|
||||
|
||||
if (idFields.has(claimKey)) {
|
||||
return <IDField value={`${claimValue}`} />;
|
||||
}
|
||||
|
||||
return <>{`${claimValue}`}</>;
|
||||
};
|
||||
export default ClaimValue;
|
57
ui/src/components/ClaimsTable.tsx
Normal file
57
ui/src/components/ClaimsTable.tsx
Normal file
|
@ -0,0 +1,57 @@
|
|||
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;
|
9
ui/src/components/CsrfInput.tsx
Normal file
9
ui/src/components/CsrfInput.tsx
Normal file
|
@ -0,0 +1,9 @@
|
|||
import React, { FC } from "react";
|
||||
|
||||
export type CsrfInputProps = {
|
||||
csrfToken: string;
|
||||
};
|
||||
export const CsrfInput: FC<CsrfInputProps> = ({ csrfToken }) => {
|
||||
return <input type="hidden" name="_pomerium_csrf" value={csrfToken} />;
|
||||
};
|
||||
export default CsrfInput;
|
72
ui/src/components/DeviceCredentialsTable.tsx
Normal file
72
ui/src/components/DeviceCredentialsTable.tsx
Normal file
|
@ -0,0 +1,72 @@
|
|||
import IDField from "./IDField";
|
||||
import Alert from "@mui/material/Alert";
|
||||
import Button from "@mui/material/Button";
|
||||
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";
|
||||
|
||||
export type DeviceCredentialsTableProps = {
|
||||
csrfToken: string;
|
||||
ids: string[];
|
||||
webAuthnUrl: string;
|
||||
};
|
||||
export const DeviceCredentialsTable: FC<DeviceCredentialsTableProps> = ({
|
||||
csrfToken,
|
||||
ids,
|
||||
webAuthnUrl
|
||||
}) => {
|
||||
return (
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>ID</TableCell>
|
||||
<TableCell></TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{ids?.length > 0 ? (
|
||||
ids?.map((id) => (
|
||||
<TableRow key={id}>
|
||||
<TableCell>
|
||||
<IDField value={id} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<form action={webAuthnUrl} method="POST">
|
||||
<input
|
||||
type="hidden"
|
||||
name="_pomerium_csrf"
|
||||
value={csrfToken}
|
||||
/>
|
||||
<input type="hidden" name="action" value="unregister" />
|
||||
<input
|
||||
type="hidden"
|
||||
name="pomerium_device_credential_id"
|
||||
value={id}
|
||||
/>
|
||||
<Button size="small" type="submit" variant="contained">
|
||||
Delete
|
||||
</Button>
|
||||
</form>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={2} padding="none">
|
||||
<Alert severity="warning" square>
|
||||
No device credentials found.
|
||||
</Alert>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
};
|
||||
export default DeviceCredentialsTable;
|
19
ui/src/components/DeviceEnrolledPage.tsx
Normal file
19
ui/src/components/DeviceEnrolledPage.tsx
Normal file
|
@ -0,0 +1,19 @@
|
|||
import HeroSection from "./HeroSection";
|
||||
import Container from "@mui/material/Container";
|
||||
import React, { FC } from "react";
|
||||
import { DeviceEnrolledPageData } from "src/types";
|
||||
|
||||
type DeviceEnrolledPageProps = {
|
||||
data: DeviceEnrolledPageData;
|
||||
};
|
||||
const DeviceEnrolledPage: FC<DeviceEnrolledPageProps> = () => {
|
||||
return (
|
||||
<Container>
|
||||
<HeroSection
|
||||
title="Device Enrolled"
|
||||
text="Device Successfully Enrolled"
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
export default DeviceEnrolledPage;
|
44
ui/src/components/ErrorPage.tsx
Normal file
44
ui/src/components/ErrorPage.tsx
Normal file
|
@ -0,0 +1,44 @@
|
|||
import { ErrorPageData } from "../types";
|
||||
import SectionFooter from "./SectionFooter";
|
||||
import Alert from "@mui/material/Alert";
|
||||
import AlertTitle from "@mui/material/AlertTitle";
|
||||
import Box from "@mui/material/Box";
|
||||
import Container from "@mui/material/Container";
|
||||
import Paper from "@mui/material/Paper";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import React, { FC } from "react";
|
||||
|
||||
export type ErrorPageProps = {
|
||||
data: ErrorPageData;
|
||||
};
|
||||
export const ErrorPage: FC<ErrorPageProps> = ({ data }) => {
|
||||
return (
|
||||
<Container>
|
||||
<Paper sx={{ overflow: "hidden" }}>
|
||||
<Stack>
|
||||
<Box sx={{ padding: "16px" }}>
|
||||
<Alert severity="error">
|
||||
<AlertTitle>
|
||||
{data?.status || 500}{" "}
|
||||
{data?.statusText || "Internal Server Error"}
|
||||
</AlertTitle>
|
||||
{data?.error || "Internal Server Error"}
|
||||
</Alert>
|
||||
</Box>
|
||||
{data?.requestId ? (
|
||||
<SectionFooter>
|
||||
<Typography variant="caption">
|
||||
If you should have access, contact your administrator with your
|
||||
request id {data?.requestId}.
|
||||
</Typography>
|
||||
</SectionFooter>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
export default ErrorPage;
|
16
ui/src/components/ExperimentalIcon.tsx
Normal file
16
ui/src/components/ExperimentalIcon.tsx
Normal file
|
@ -0,0 +1,16 @@
|
|||
import createSvgIcon from "@mui/material/utils/createSvgIcon";
|
||||
import React from "react";
|
||||
|
||||
const ExperimentalIcon = createSvgIcon(
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||
<path
|
||||
d="M20.5 19.34 16.14 12a1 1 0 0 1-.14-.51V2.5a.5.5 0 0 1 .5-.5H18a1 1 0 0 0 0-2H6a1 1 0 0 0 0 2h1.5a.5.5 0 0 1 .5.5v9a1 1 0 0 1-.14.51l-4.32 7.27A3 3 0 0 0 6 24h12a3 3 0 0 0 2.49-4.66ZM8.67 16a.5.5 0 0 1-.43-.25.5.5 0 0 1 0-.5l1.62-2.74A1 1 0 0 0 10 12V2.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 .5.5v2.75a.5.5 0 0 1-.5.5h-1.32a.75.75 0 0 0 0 1.5h1.32a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1.32a.75.75 0 0 0 0 1.5h1.32a.5.5 0 0 1 .5.5V12a1 1 0 0 0 .14.51l1.61 2.74a.47.47 0 0 1 0 .5.52.52 0 0 1-.44.25Zm.82 5.82a1.5 1.5 0 1 1 1.5-1.5 1.5 1.5 0 0 1-1.5 1.5Zm4.22-3a1 1 0 0 1 0-2 1 1 0 0 1 0 2Zm2.49 3.09a1 1 0 1 1 1-1 1 1 0 0 1-1 .98Z"
|
||||
style={{
|
||||
fill: "#5e6a82"
|
||||
}}
|
||||
transform="scale(.83333)"
|
||||
/>
|
||||
</svg>,
|
||||
"Experimental"
|
||||
);
|
||||
export default ExperimentalIcon;
|
39
ui/src/components/Footer.tsx
Normal file
39
ui/src/components/Footer.tsx
Normal file
|
@ -0,0 +1,39 @@
|
|||
import Box from "@mui/material/Box";
|
||||
import Container from "@mui/material/Container";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import React, { FC } from "react";
|
||||
|
||||
const Footer: FC = () => {
|
||||
return (
|
||||
<Container component="footer">
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={2}
|
||||
sx={{
|
||||
fontSize: "0.85rem",
|
||||
padding: "16px"
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<a href="https://pomerium.com/">Home</a>
|
||||
</Box>
|
||||
<Box>
|
||||
<a href="https://pomerium.com/docs">Docs</a>
|
||||
</Box>
|
||||
<Box>
|
||||
<a href="https://pomerium.com/docs/community/">Support</a>
|
||||
</Box>
|
||||
<Box>
|
||||
<a href="https://github.com/pomerium">GitHub</a>
|
||||
</Box>
|
||||
<Box>
|
||||
<a href="https://twitter.com/pomerium_io">@pomerium_io</a>
|
||||
</Box>
|
||||
<Box flexGrow={1} sx={{ textAlign: "right" }}>
|
||||
© Pomerium, Inc.
|
||||
</Box>
|
||||
</Stack>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
export default Footer;
|
41
ui/src/components/GroupDetails.tsx
Normal file
41
ui/src/components/GroupDetails.tsx
Normal file
|
@ -0,0 +1,41 @@
|
|||
import { Group } from "../types";
|
||||
import IDField from "./IDField";
|
||||
import Section from "./Section";
|
||||
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";
|
||||
|
||||
export type GroupDetailsProps = {
|
||||
groups: Group[];
|
||||
};
|
||||
export const GroupDetails: FC<GroupDetailsProps> = ({ groups }) => {
|
||||
return (
|
||||
<Section title="Groups">
|
||||
<TableContainer>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>ID</TableCell>
|
||||
<TableCell>Name</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{groups?.map((group) => (
|
||||
<TableRow key={group?.id}>
|
||||
<TableCell>
|
||||
<IDField value={group?.id} />
|
||||
</TableCell>
|
||||
<TableCell>{group?.name}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
export default GroupDetails;
|
35
ui/src/components/Header.tsx
Normal file
35
ui/src/components/Header.tsx
Normal file
|
@ -0,0 +1,35 @@
|
|||
import CsrfInput from "./CsrfInput";
|
||||
import Logo from "./Logo";
|
||||
import AppBar from "@mui/material/AppBar";
|
||||
import Box from "@mui/material/Box";
|
||||
import Button from "@mui/material/Button";
|
||||
import Toolbar from "@mui/material/Toolbar";
|
||||
import React, { FC } from "react";
|
||||
|
||||
type HeaderProps = {
|
||||
csrfToken: string;
|
||||
signOutUrl: string;
|
||||
};
|
||||
const Header: FC<HeaderProps> = ({ csrfToken, signOutUrl }) => {
|
||||
return (
|
||||
<AppBar position="sticky">
|
||||
<Toolbar>
|
||||
<a href="/.pomerium">
|
||||
<Logo />
|
||||
</a>
|
||||
<Box flexGrow={1} />
|
||||
{signOutUrl ? (
|
||||
<form action={signOutUrl}>
|
||||
<CsrfInput csrfToken={csrfToken} />
|
||||
<Button variant="text" color="inherit" type="submit">
|
||||
Logout
|
||||
</Button>
|
||||
</form>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
);
|
||||
};
|
||||
export default Header;
|
24
ui/src/components/HeroSection.tsx
Normal file
24
ui/src/components/HeroSection.tsx
Normal file
|
@ -0,0 +1,24 @@
|
|||
import Paper from "@mui/material/Paper";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import React, { FC } from "react";
|
||||
|
||||
export type HeroSectionProps = {
|
||||
icon?: React.ReactNode;
|
||||
title: React.ReactNode;
|
||||
text?: React.ReactNode;
|
||||
};
|
||||
export const HeroSection: FC<HeroSectionProps> = ({ icon, title, text }) => {
|
||||
return (
|
||||
<Paper sx={{ padding: "16px" }}>
|
||||
<Stack direction="row" spacing={2}>
|
||||
{icon}
|
||||
<Stack>
|
||||
<Typography variant="h1">{title}</Typography>
|
||||
{text ? <Typography>{text}</Typography> : <></>}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
export default HeroSection;
|
19
ui/src/components/IDField.tsx
Normal file
19
ui/src/components/IDField.tsx
Normal file
|
@ -0,0 +1,19 @@
|
|||
import Box from "@mui/material/Box";
|
||||
import React, { FC } from "react";
|
||||
|
||||
export type IDFieldProps = {
|
||||
value: string;
|
||||
};
|
||||
export const IDField: FC<IDFieldProps> = ({ value }) => {
|
||||
return (
|
||||
<Box component="span" sx={{ fontFamily: '"DM Mono"', fontSize: "12px" }}>
|
||||
{value?.split("")?.map((str, idx) => (
|
||||
<React.Fragment key={idx}>
|
||||
{str}
|
||||
<wbr />
|
||||
</React.Fragment>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
export default IDField;
|
59
ui/src/components/JwtIcon.tsx
Normal file
59
ui/src/components/JwtIcon.tsx
Normal file
|
@ -0,0 +1,59 @@
|
|||
import createSvgIcon from "@mui/material/utils/createSvgIcon";
|
||||
import React from "react";
|
||||
|
||||
const JwtIcon = createSvgIcon(
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 15 15"
|
||||
>
|
||||
<path
|
||||
style={{
|
||||
stroke: "none",
|
||||
fillRule: "evenodd",
|
||||
fill: "#fff",
|
||||
fillOpacity: 1
|
||||
}}
|
||||
d="M8.586 4.04 8.57.042H6.34l.015 3.996L7.47 5.57ZM6.355 10.887v4.008h2.23v-4.008L7.47 9.355Zm0 0"
|
||||
/>
|
||||
<path
|
||||
style={{
|
||||
stroke: "none",
|
||||
fillRule: "evenodd",
|
||||
fill: "#00f2e6",
|
||||
fillOpacity: 1
|
||||
}}
|
||||
d="m8.586 10.887 2.344 3.238 1.797-1.309-2.344-3.238L8.586 9ZM6.355 4.04 3.996.8 2.2 2.11l2.344 3.238 1.812.578Zm0 0"
|
||||
/>
|
||||
<path
|
||||
style={{
|
||||
stroke: "none",
|
||||
fillRule: "evenodd",
|
||||
fill: "#00b9f1",
|
||||
fillOpacity: 1
|
||||
}}
|
||||
d="m4.543 5.348-3.8-1.235-.684 2.11 3.804 1.246 1.797-.594ZM9.266 8.05l1.117 1.528 3.8 1.235.684-2.11-3.805-1.234Zm0 0"
|
||||
/>
|
||||
<path
|
||||
style={{
|
||||
stroke: "none",
|
||||
fillRule: "evenodd",
|
||||
fill: "#d63aff",
|
||||
fillOpacity: 1
|
||||
}}
|
||||
d="m11.063 7.469 3.804-1.246-.683-2.11-3.801 1.235-1.117 1.527ZM3.863 7.469.06 8.703l.683 2.11 3.801-1.235L5.66 8.051Zm0 0"
|
||||
/>
|
||||
<path
|
||||
style={{
|
||||
stroke: "none",
|
||||
fillRule: "evenodd",
|
||||
fill: "#fb015b",
|
||||
fillOpacity: 1
|
||||
}}
|
||||
d="m4.543 9.578-2.344 3.238 1.797 1.309 2.36-3.238V9ZM10.383 5.348l2.344-3.239L10.93.801 8.586 4.039v1.887Zm0 0"
|
||||
/>
|
||||
</svg>,
|
||||
"JWT"
|
||||
);
|
||||
export default JwtIcon;
|
9
ui/src/components/Logo.tsx
Normal file
9
ui/src/components/Logo.tsx
Normal file
|
@ -0,0 +1,9 @@
|
|||
import LogoURL from "../static/logo_white.svg";
|
||||
import React from "react";
|
||||
import type { FC } from "react";
|
||||
|
||||
const Logo: FC = () => {
|
||||
return <img alt="Logo" src={LogoURL} height="30px" />;
|
||||
};
|
||||
|
||||
export default Logo;
|
10
ui/src/components/PersonIcon.tsx
Normal file
10
ui/src/components/PersonIcon.tsx
Normal file
|
@ -0,0 +1,10 @@
|
|||
import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon";
|
||||
import React, { FC } from "react";
|
||||
import { User } from "react-feather";
|
||||
|
||||
export const PersonIcon: FC<SvgIconProps> = (props) => (
|
||||
<SvgIcon {...props}>
|
||||
<User />
|
||||
</SvgIcon>
|
||||
);
|
||||
export default PersonIcon;
|
41
ui/src/components/Section.tsx
Normal file
41
ui/src/components/Section.tsx
Normal file
|
@ -0,0 +1,41 @@
|
|||
import SectionFooter from "./SectionFooter";
|
||||
import Box from "@mui/material/Box";
|
||||
import Paper from "@mui/material/Paper";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Toolbar from "@mui/material/Toolbar";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import React, { FC } from "react";
|
||||
|
||||
export type SectionProps = React.PropsWithChildren<{
|
||||
title: React.ReactNode;
|
||||
icon?: React.ReactNode;
|
||||
footer?: React.ReactNode;
|
||||
}>;
|
||||
export const Section: FC<SectionProps> = ({
|
||||
title,
|
||||
icon,
|
||||
children,
|
||||
footer
|
||||
}) => {
|
||||
return (
|
||||
<Paper sx={{ overflow: "hidden" }}>
|
||||
<Stack>
|
||||
<Toolbar>
|
||||
<Typography variant="h4" flexGrow={1}>
|
||||
{title}
|
||||
</Typography>
|
||||
{icon ? <Box>{icon}</Box> : <></>}
|
||||
</Toolbar>
|
||||
<Box sx={{ padding: 3, paddingTop: 0 }}>{children}</Box>
|
||||
{footer ? (
|
||||
<SectionFooter>
|
||||
<Typography variant="caption">{footer}</Typography>
|
||||
</SectionFooter>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</Stack>
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
export default Section;
|
9
ui/src/components/SectionFooter.tsx
Normal file
9
ui/src/components/SectionFooter.tsx
Normal file
|
@ -0,0 +1,9 @@
|
|||
import Box from "@mui/material/Box";
|
||||
import styled from "@mui/material/styles/styled";
|
||||
import React, { FC } from "react";
|
||||
|
||||
export const SectionFooter = styled(Box)(({ theme }) => ({
|
||||
backgroundColor: theme.palette.grey[100],
|
||||
padding: theme.spacing(3)
|
||||
}));
|
||||
export default SectionFooter;
|
47
ui/src/components/SessionDetails.tsx
Normal file
47
ui/src/components/SessionDetails.tsx
Normal file
|
@ -0,0 +1,47 @@
|
|||
import { Session } from "../types";
|
||||
import ClaimsTable from "./ClaimsTable";
|
||||
import IDField from "./IDField";
|
||||
import Section from "./Section";
|
||||
import Stack from "@mui/material/Stack";
|
||||
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 TableRow from "@mui/material/TableRow";
|
||||
import React, { FC } from "react";
|
||||
|
||||
export type SessionDetailsProps = {
|
||||
session: Session;
|
||||
};
|
||||
export const SessionDetails: FC<SessionDetailsProps> = ({ session }) => {
|
||||
return (
|
||||
<Section title="Session Details">
|
||||
<Stack spacing={3}>
|
||||
<TableContainer>
|
||||
<Table size="small">
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>ID</TableCell>
|
||||
<TableCell>
|
||||
<IDField value={session?.id} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>User ID</TableCell>
|
||||
<TableCell>
|
||||
<IDField value={session?.userId} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>Expires At</TableCell>
|
||||
<TableCell>{session?.expiresAt || ""}</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<ClaimsTable claims={session?.claims} />
|
||||
</Stack>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
export default SessionDetails;
|
81
ui/src/components/SessionDeviceCredentials.tsx
Normal file
81
ui/src/components/SessionDeviceCredentials.tsx
Normal file
|
@ -0,0 +1,81 @@
|
|||
import DeviceCredentialsTable from "../components/DeviceCredentialsTable";
|
||||
import SectionFooter from "../components/SectionFooter";
|
||||
import { Session, User } from "../types";
|
||||
import Box from "@mui/material/Box";
|
||||
import Paper from "@mui/material/Paper";
|
||||
import Stack from "@mui/material/Stack";
|
||||
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 Toolbar from "@mui/material/Toolbar";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import React, { FC } from "react";
|
||||
|
||||
export type SessionDeviceCredentialsProps = {
|
||||
csrfToken: string;
|
||||
user: User;
|
||||
session: Session;
|
||||
webAuthnUrl: string;
|
||||
};
|
||||
export const SessionDeviceCredentials: FC<SessionDeviceCredentialsProps> = ({
|
||||
csrfToken,
|
||||
user,
|
||||
session,
|
||||
webAuthnUrl
|
||||
}) => {
|
||||
const currentSessionDeviceCredentialIds = [];
|
||||
const otherDeviceCredentialIds = [];
|
||||
user?.deviceCredentialIds?.forEach((id) => {
|
||||
if (session?.deviceCredentials?.find((cred) => cred?.id === id)) {
|
||||
currentSessionDeviceCredentialIds.push(id);
|
||||
} else {
|
||||
otherDeviceCredentialIds.push(id);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Paper sx={{ overflow: "hidden" }}>
|
||||
<Stack>
|
||||
<Toolbar>
|
||||
<Typography variant="h4" flexGrow={1}>
|
||||
Current Session Device Credentials
|
||||
</Typography>
|
||||
</Toolbar>
|
||||
<Box sx={{ padding: 3, paddingTop: 0 }}>
|
||||
<DeviceCredentialsTable
|
||||
csrfToken={csrfToken}
|
||||
ids={currentSessionDeviceCredentialIds}
|
||||
webAuthnUrl={webAuthnUrl}
|
||||
/>
|
||||
</Box>
|
||||
{otherDeviceCredentialIds?.length > 0 ? (
|
||||
<>
|
||||
<Toolbar>
|
||||
<Typography variant="h4" flexGrow={1}>
|
||||
Other Device Credentials
|
||||
</Typography>
|
||||
</Toolbar>
|
||||
<Box sx={{ padding: 3, paddingTop: 0 }}>
|
||||
<DeviceCredentialsTable
|
||||
csrfToken={csrfToken}
|
||||
ids={otherDeviceCredentialIds}
|
||||
webAuthnUrl={webAuthnUrl}
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<SectionFooter>
|
||||
<Typography variant="caption">
|
||||
Register device with <a href={webAuthnUrl}>WebAuthn</a>.
|
||||
</Typography>
|
||||
</SectionFooter>
|
||||
</Stack>
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
export default SessionDeviceCredentials;
|
17
ui/src/components/UserClaims.tsx
Normal file
17
ui/src/components/UserClaims.tsx
Normal file
|
@ -0,0 +1,17 @@
|
|||
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;
|
55
ui/src/components/UserInfoPage.tsx
Normal file
55
ui/src/components/UserInfoPage.tsx
Normal file
|
@ -0,0 +1,55 @@
|
|||
import GroupDetails from "./GroupDetails";
|
||||
import HeroSection from "./HeroSection";
|
||||
import PersonIcon from "./PersonIcon";
|
||||
import SessionDetails from "./SessionDetails";
|
||||
import SessionDeviceCredentials from "./SessionDeviceCredentials";
|
||||
import UserClaims from "./UserClaims";
|
||||
import MuiAvatar from "@mui/material/Avatar";
|
||||
import Container from "@mui/material/Container";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import styled from "@mui/material/styles/styled";
|
||||
import React, { FC } from "react";
|
||||
import { UserInfoPageData } from "src/types";
|
||||
|
||||
const Avatar = styled(MuiAvatar)(({ theme }) => ({
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
height: 48,
|
||||
width: 48
|
||||
}));
|
||||
|
||||
type UserInfoPageProps = {
|
||||
data: UserInfoPageData;
|
||||
};
|
||||
const UserInfoPage: FC<UserInfoPageProps> = ({ data }) => {
|
||||
const name = data?.user?.claims?.given_name?.[0] || data?.user?.name;
|
||||
return (
|
||||
<Container>
|
||||
<Stack spacing={3}>
|
||||
<HeroSection
|
||||
icon={
|
||||
<Avatar>
|
||||
<PersonIcon />
|
||||
</Avatar>
|
||||
}
|
||||
title={<>Hi {name}!</>}
|
||||
text={
|
||||
<>
|
||||
Welcome to the user info endpoint. Here you can view your current
|
||||
session details, and authorization context.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<SessionDetails session={data?.session} />
|
||||
<UserClaims user={data?.user} />
|
||||
<GroupDetails groups={data?.directoryGroups} />
|
||||
<SessionDeviceCredentials
|
||||
csrfToken={data?.csrfToken}
|
||||
session={data?.session}
|
||||
user={data?.user}
|
||||
webAuthnUrl={data?.webAuthnUrl}
|
||||
/>
|
||||
</Stack>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
export default UserInfoPage;
|
210
ui/src/components/WebAuthnRegistrationPage.tsx
Normal file
210
ui/src/components/WebAuthnRegistrationPage.tsx
Normal file
|
@ -0,0 +1,210 @@
|
|||
import { decode, encodeUrl } from "../util/base64";
|
||||
import AlertDialog from "./AlertDialog";
|
||||
import ExperimentalIcon from "./ExperimentalIcon";
|
||||
import HeroSection from "./HeroSection";
|
||||
import Button from "@mui/material/Button";
|
||||
import Container from "@mui/material/Container";
|
||||
import Paper from "@mui/material/Paper";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import React, { FC, useRef, useState } from "react";
|
||||
import {
|
||||
WebAuthnCreationOptions,
|
||||
WebAuthnRegistrationPageData,
|
||||
WebAuthnRequestOptions
|
||||
} from "src/types";
|
||||
|
||||
type CredentialForAuthenticate = {
|
||||
id: string;
|
||||
type: string;
|
||||
rawId: ArrayBuffer;
|
||||
response: {
|
||||
authenticatorData: ArrayBuffer;
|
||||
clientDataJSON: ArrayBuffer;
|
||||
signature: ArrayBuffer;
|
||||
userHandle: ArrayBuffer;
|
||||
};
|
||||
};
|
||||
|
||||
async function authenticateCredential(
|
||||
requestOptions: WebAuthnRequestOptions
|
||||
): Promise<CredentialForAuthenticate> {
|
||||
const credential = await navigator.credentials.get({
|
||||
publicKey: {
|
||||
allowCredentials: requestOptions?.allowCredentials?.map((c) => ({
|
||||
type: c.type,
|
||||
id: decode(c.id)
|
||||
})),
|
||||
challenge: decode(requestOptions?.challenge),
|
||||
timeout: requestOptions?.timeout,
|
||||
userVerification: requestOptions?.userVerification
|
||||
}
|
||||
});
|
||||
return credential as CredentialForAuthenticate;
|
||||
}
|
||||
|
||||
type CredentialForCreate = {
|
||||
id: string;
|
||||
type: string;
|
||||
rawId: ArrayBuffer;
|
||||
response: {
|
||||
attestationObject: ArrayBuffer;
|
||||
clientDataJSON: ArrayBuffer;
|
||||
};
|
||||
};
|
||||
|
||||
async function createCredential(
|
||||
creationOptions: WebAuthnCreationOptions
|
||||
): Promise<CredentialForCreate> {
|
||||
const credential = await navigator.credentials.create({
|
||||
publicKey: {
|
||||
attestation: creationOptions?.attestation || undefined,
|
||||
authenticatorSelection: {
|
||||
authenticatorAttachment:
|
||||
creationOptions?.authenticatorSelection?.authenticatorAttachment ||
|
||||
undefined,
|
||||
requireResidentKey:
|
||||
creationOptions?.authenticatorSelection?.requireResidentKey ||
|
||||
undefined,
|
||||
residentKey: creationOptions?.authenticatorSelection?.residentKey,
|
||||
userVerification:
|
||||
creationOptions?.authenticatorSelection?.userVerification || undefined
|
||||
},
|
||||
challenge: decode(creationOptions?.challenge),
|
||||
pubKeyCredParams: creationOptions?.pubKeyCredParams?.map((p) => ({
|
||||
type: p.type,
|
||||
alg: p.alg
|
||||
})),
|
||||
rp: {
|
||||
name: creationOptions?.rp?.name
|
||||
},
|
||||
timeout: creationOptions?.timeout,
|
||||
user: {
|
||||
id: decode(creationOptions?.user?.id),
|
||||
name: creationOptions?.user?.name,
|
||||
displayName: creationOptions?.user?.displayName
|
||||
}
|
||||
}
|
||||
});
|
||||
return credential as CredentialForCreate;
|
||||
}
|
||||
|
||||
type WebAuthnRegistrationPageProps = {
|
||||
data: WebAuthnRegistrationPageData;
|
||||
};
|
||||
const WebAuthnRegistrationPage: FC<WebAuthnRegistrationPageProps> = ({
|
||||
data
|
||||
}) => {
|
||||
const authenticateFormRef = useRef<HTMLFormElement>();
|
||||
const authenticateResponseRef = useRef<HTMLInputElement>();
|
||||
const registerFormRef = useRef<HTMLFormElement>();
|
||||
const registerResponseRef = useRef<HTMLInputElement>();
|
||||
|
||||
const [error, setError] = useState<string>(null);
|
||||
|
||||
const enableAuthenticate = data?.requestOptions?.allowCredentials?.length > 0;
|
||||
|
||||
function handleClickAuthenticate(evt: React.MouseEvent): void {
|
||||
evt.preventDefault();
|
||||
|
||||
void (async () => {
|
||||
try {
|
||||
const credential = await authenticateCredential(data?.requestOptions);
|
||||
authenticateResponseRef.current.value = JSON.stringify({
|
||||
id: credential.id,
|
||||
type: credential.type,
|
||||
rawId: encodeUrl(credential.rawId),
|
||||
response: {
|
||||
authenticatorData: encodeUrl(credential.response.authenticatorData),
|
||||
clientDataJSON: encodeUrl(credential.response.clientDataJSON),
|
||||
signature: encodeUrl(credential.response.signature),
|
||||
userHandle: encodeUrl(credential.response.userHandle)
|
||||
}
|
||||
});
|
||||
authenticateFormRef.current.submit();
|
||||
} catch (e) {
|
||||
setError(`${e}`);
|
||||
}
|
||||
})();
|
||||
}
|
||||
function handleClickDialogOK(evt: React.MouseEvent): void {
|
||||
evt.preventDefault();
|
||||
setError(null);
|
||||
}
|
||||
function handleClickRegister(evt: React.MouseEvent): void {
|
||||
evt.preventDefault();
|
||||
|
||||
void (async () => {
|
||||
try {
|
||||
const credential = await createCredential(data?.creationOptions);
|
||||
registerResponseRef.current.value = JSON.stringify({
|
||||
id: credential.id,
|
||||
type: credential.type,
|
||||
rawId: encodeUrl(credential.rawId),
|
||||
response: {
|
||||
attestationObject: encodeUrl(credential.response.attestationObject),
|
||||
clientDataJSON: encodeUrl(credential.response.clientDataJSON)
|
||||
}
|
||||
});
|
||||
registerFormRef.current.submit();
|
||||
} catch (e) {
|
||||
setError(`${e}`);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Stack spacing={3}>
|
||||
<HeroSection
|
||||
title={
|
||||
<>
|
||||
WebAuthn Registration <ExperimentalIcon />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Paper sx={{ padding: "16px" }}>
|
||||
<Stack direction="row" justifyContent="center" spacing={3}>
|
||||
<Button onClick={handleClickRegister} variant="contained">
|
||||
Register New Device
|
||||
</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}
|
||||
/>
|
||||
</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
|
||||
title="Error"
|
||||
severity="error"
|
||||
open={!!error}
|
||||
actions={<Button onClick={handleClickDialogOK}>OK</Button>}
|
||||
>
|
||||
{error}
|
||||
</AlertDialog>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
export default WebAuthnRegistrationPage;
|
1
ui/src/globals.d.ts
vendored
Normal file
1
ui/src/globals.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
declare module "*.svg";
|
5
ui/src/index.tsx
Normal file
5
ui/src/index.tsx
Normal file
|
@ -0,0 +1,5 @@
|
|||
import App from "./App";
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
|
||||
ReactDOM.render(<App />, document.getElementById("root"));
|
23
ui/src/static/logo_white.svg
Normal file
23
ui/src/static/logo_white.svg
Normal file
|
@ -0,0 +1,23 @@
|
|||
<svg width="161" height="28" viewBox="0 0 161 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0)">
|
||||
<path d="M5.71009 27.3796C5.13899 27.3067 4.37349 27.6226 4.03326 27.2216C3.70518 26.8328 3.9482 26.0916 3.93605 25.5083C3.93605 21.1583 3.93605 16.8083 3.9482 12.4461C3.9482 11.3525 3.76594 10.3075 3.10979 9.39618C2.47794 8.52132 1.62737 7.92592 0.485183 7.87732C0.0842016 7.86516 -0.0130059 7.71935 -0.000854965 7.34267C0.0234469 6.67437 0.0234469 6.00607 -0.000854965 5.33777C-0.0130059 4.99754 0.0720507 4.82743 0.436579 4.79098C2.36858 4.59656 4.30058 4.37784 6.22043 4.15913C8.97869 3.85535 11.7248 3.53943 14.4831 3.23566C17.1927 2.93188 19.9024 2.64026 22.6242 2.33649C25.2974 2.04486 27.9706 1.75324 30.6438 1.44947C33.5965 1.12139 36.5492 0.781165 39.5019 0.45309C39.5748 0.440939 39.6477 0.489543 39.7206 0.513845C39.9636 0.732562 40.0122 1.02418 40.0244 1.34011C40.0244 2.00841 40.0365 2.67671 40.0244 3.34501C40.0001 4.23203 39.915 4.34139 39.0766 4.57226C38.3475 4.76667 37.6428 5.00969 37.0231 5.44713C35.2733 6.66222 34.3377 8.3269 34.3256 10.4655C34.3013 15.9091 34.3256 21.3406 34.3134 26.7842C34.3134 27.1973 34.4106 27.6469 34.0218 27.975C32.8067 27.9628 31.5916 27.9385 30.3765 27.9507C29.8783 27.9628 29.7811 27.7684 29.7811 27.3188C29.7933 21.7537 29.7811 16.1886 29.7933 10.6234C29.7933 9.05596 29.2951 7.74366 28.1164 6.68652C27.8856 6.49211 27.6668 6.28554 27.3631 6.20049C26.4396 6.00607 25.5161 5.7995 24.5684 5.90886C22.284 6.16403 20.3034 8.08388 20.0117 10.3561C19.9145 11.073 19.8659 11.8021 19.8659 12.5311C19.8781 17.2214 19.8781 21.9117 19.8659 26.6019C19.8659 26.9907 19.9753 27.4282 19.55 27.7077C19.4771 27.7077 19.4164 27.7077 19.3434 27.7077C18.5658 27.5861 17.7881 27.7441 17.0105 27.6226C15.9655 27.5861 15.9655 27.5861 15.9655 26.5533C15.9655 21.5593 15.9533 16.5652 15.9655 11.5834C15.9655 10.0037 15.4794 8.65498 14.2279 7.6343C14.1185 7.54924 13.9849 7.48849 13.8634 7.42773C12.5997 7.13611 11.3238 6.88094 10.0966 7.53709C8.37115 8.43626 7.50843 9.93083 7.48413 11.8628C7.44767 16.5166 7.47198 21.1583 7.47198 25.8121C7.47198 26.1159 7.47198 26.4197 7.45983 26.7234C7.44767 26.9786 7.38692 27.1973 7.1682 27.3674C6.68216 27.4768 6.19613 27.4282 5.71009 27.3796Z" fill="white"/>
|
||||
<path d="M79.5888 14.7074C79.686 17.1619 79.443 19.6164 79.7468 22.0952C78.6775 22.0952 77.5353 22.0952 76.4053 22.0952C76.709 20.1511 76.5389 18.1826 76.5389 16.2142C76.5389 14.2457 76.6969 12.2651 76.4053 10.2237C77.7176 10.2237 79.1271 10.2237 80.5244 10.2237C80.6217 10.2237 80.731 10.2237 80.731 10.3695C80.731 11.0743 81.1684 11.6211 81.4479 12.2165C82.3106 14.0634 83.1855 15.9104 84.0604 17.7452C84.1454 17.9153 84.1697 18.134 84.4128 18.2677C84.7287 17.6237 85.0324 17.004 85.3362 16.3721C86.1746 14.6224 87.0738 12.9091 87.8029 11.1108C87.9122 10.8434 87.8879 10.3817 88.058 10.2845C88.3497 10.1387 88.7628 10.2116 89.1273 10.2116C90.1844 10.2116 91.2294 10.2116 92.2866 10.2116C92.323 10.2116 92.3595 10.2359 92.4202 10.248C92.2015 10.6733 92.1894 11.1472 92.1894 11.6089C92.1894 14.671 92.1894 17.7209 92.1894 20.7829C92.1894 21.2203 92.2015 21.6699 92.4202 22.1074C91.2537 22.1074 90.1237 22.1074 88.9937 22.1074C89.1759 21.2689 89.2731 15.8618 89.1152 14.7196C88.9086 14.7439 88.9086 14.9383 88.8478 15.072C87.8636 16.9675 86.8915 18.8752 86.0045 20.8194C85.9316 20.9895 85.798 21.1596 85.798 21.3175C85.8223 22.241 85.1904 22.0952 84.595 22.1438C83.6472 22.2046 83.0883 22.1074 82.6873 20.9652C81.9097 18.7902 80.7675 16.7488 79.7832 14.6588C79.7103 14.6953 79.6496 14.6953 79.5888 14.7074Z" fill="white"/>
|
||||
<path d="M153.138 18.3527C154.317 15.8496 155.641 13.5044 156.565 10.9892C156.82 10.2966 157.112 10.2115 157.707 10.2237C158.837 10.2601 159.967 10.2358 160.988 10.2358C160.915 14.1971 160.915 18.134 161 22.1195C160.016 22.1195 158.898 22.1195 157.78 22.1195C158.072 19.6771 157.829 17.2105 157.938 14.7438C157.683 14.7438 157.683 14.9018 157.634 14.999C156.699 16.8824 155.763 18.7537 154.84 20.6371C154.742 20.8315 154.633 21.038 154.572 21.2446C154.487 21.5241 154.499 21.998 154.341 22.0709C154.013 22.2167 153.6 22.1438 153.211 22.1559C152.786 22.1559 152.312 22.2653 151.948 22.1195C151.583 21.9737 151.717 21.4269 151.547 21.0745C150.526 18.9481 149.53 16.8095 148.448 14.6952C148.424 17.174 148.242 19.6285 148.485 22.1316C147.427 22.1316 146.285 22.1316 145.265 22.1316C145.35 18.1947 145.362 14.2457 145.252 10.248C146.48 10.248 147.865 10.248 149.25 10.248C149.42 10.248 149.481 10.2844 149.493 10.4789C149.517 10.7583 149.602 11.0378 149.724 11.293C150.793 13.5774 151.887 15.8496 152.968 18.134C152.993 18.1583 153.041 18.2069 153.138 18.3527Z" fill="white"/>
|
||||
<path d="M95.2515 10.2358C96.3694 10.2358 97.633 10.2358 98.9089 10.2358C101.23 10.2358 103.563 10.2479 105.884 10.2236C106.357 10.2236 106.54 10.2965 106.564 10.8312C106.588 11.5724 106.771 12.3014 106.892 13.1034C105.738 12.7632 104.62 12.5445 103.478 12.4716C101.947 12.3743 100.403 12.4351 98.8724 12.4351C98.5444 12.4351 98.3986 12.5931 98.3986 12.9211C98.4107 13.4679 98.4107 14.0147 98.3986 14.5615C98.3864 14.9625 98.5687 15.1083 98.9453 15.0962C100.877 15.0354 102.822 15.2906 104.766 14.7681C104.656 15.5336 104.547 16.2627 104.462 17.0039C104.425 17.3562 104.17 17.2833 103.964 17.2833C102.36 17.2833 100.768 17.3076 99.1641 17.2712C98.5322 17.259 98.3378 17.4777 98.3864 18.0731C98.4229 18.5592 98.3986 19.0452 98.3986 19.5313C98.3986 19.7864 98.4958 19.9565 98.7874 19.9565C101.108 19.9079 103.441 20.078 105.762 19.8593C106.297 19.8107 106.758 19.5556 107.293 19.2639C107.159 20.2239 107.062 21.123 106.916 21.9979C106.868 22.2774 106.576 22.1437 106.406 22.1437C103.794 22.1559 101.181 22.1437 98.5808 22.1437C97.4265 22.1437 96.2843 22.1437 95.2636 22.1437C95.3365 18.1582 95.3365 14.2091 95.2515 10.2358Z" fill="white"/>
|
||||
<path d="M130.112 10.248C131.267 10.248 132.397 10.248 133.527 10.248C133.259 11.6697 133.43 13.1157 133.393 14.5495C133.369 15.5216 133.381 16.4936 133.405 17.4657C133.454 19.2154 134.219 20.0053 135.933 20.1632C136.479 20.2118 137.026 20.1511 137.561 20.0174C138.472 19.7987 139.08 19.0332 139.104 17.8667C139.153 15.558 139.128 13.2615 139.116 10.9528C139.116 10.7219 139.019 10.4911 138.958 10.248C140.088 10.248 141.218 10.248 142.397 10.248C142.008 11.3052 142.202 12.3988 142.166 13.468C142.117 15.0598 142.312 16.6637 142.069 18.2555C141.729 20.467 140.392 21.7793 138.205 22.2046C136.735 22.4962 135.264 22.4719 133.818 22.0952C131.728 21.5484 130.416 19.8351 130.367 17.5872C130.331 15.6431 130.355 13.6989 130.355 11.7669C130.367 11.2444 130.331 10.7462 130.112 10.248Z" fill="white"/>
|
||||
<path d="M34.0225 27.9636C34.0225 22.1433 34.0346 16.323 34.0346 10.4906C34.0346 7.42853 36.0517 4.91329 39.053 4.26929C39.539 4.15993 39.7213 3.98982 39.7091 3.47948C39.6726 2.49525 39.7213 1.49887 39.7334 0.514648C40.3045 0.575403 40.8148 0.830573 41.3616 0.964233C41.629 1.02499 41.7019 1.2194 41.6897 1.46242C41.6897 2.4102 41.6776 3.37012 41.6897 4.31789C41.7019 4.77963 41.3981 4.81608 41.07 4.88899C38.8342 5.44793 37.2425 6.74808 36.4283 8.93525C36.1732 9.62786 36.076 10.3448 36.076 11.0738C36.0881 16.4567 36.076 21.8396 36.076 27.2224C36.076 28.0001 36.076 28.0001 35.2862 27.9636C34.873 27.9758 34.4477 27.9758 34.0225 27.9636Z" fill="#C5B7DD"/>
|
||||
<path d="M19.5508 27.6962C19.648 27.4167 19.6237 27.1373 19.6237 26.8578C19.6237 21.7544 19.6358 16.651 19.6237 11.5476C19.6237 9.53053 20.3284 7.878 21.9202 6.62646C23.5363 5.36276 25.7721 5.19264 27.376 6.18902C24.4476 6.07966 21.665 8.40049 21.6529 12.1065C21.6407 17.0398 21.6529 21.961 21.6529 26.8942C21.6529 27.7326 21.6529 27.7326 20.8266 27.7205C20.4013 27.7083 19.9761 27.7083 19.5508 27.6962Z" fill="#C5B7DD"/>
|
||||
<path d="M7.16896 27.3558C7.1568 25.1079 7.14465 22.8721 7.14465 20.6242C7.14465 17.6958 7.12035 14.7553 7.15681 11.8269C7.18111 9.49392 8.74858 7.41611 10.875 6.91792C11.9321 6.66275 12.965 6.74781 13.8884 7.41611C11.1545 7.50116 9.56269 9.72479 9.25892 11.8998C9.17386 12.5438 9.14956 13.1878 9.14956 13.8318C9.16171 18.1089 9.14956 22.3982 9.14956 26.6754C9.14956 27.4652 9.14956 27.4652 8.38405 27.4409C7.98307 27.4409 7.58209 27.3923 7.16896 27.3558Z" fill="#C5B7DD"/>
|
||||
<path d="M127.463 22.1312C126.26 22.1312 125.142 22.1312 124.158 22.1312C124.207 18.17 124.207 14.2331 124.158 10.2598C125.142 10.2598 126.285 10.2598 127.475 10.2598C127.208 10.7701 127.22 11.3169 127.22 11.8515C127.22 14.7678 127.22 17.684 127.22 20.6002C127.208 21.1106 127.22 21.6087 127.463 22.1312Z" fill="white"/>
|
||||
<path d="M17.0112 27.624C17.7889 27.5875 18.5665 27.5754 19.3442 27.709C18.5665 27.7576 17.7767 27.7819 17.0112 27.624Z" fill="#C5B7DD"/>
|
||||
<path d="M5.71094 27.3807C6.19697 27.3686 6.68301 27.3686 7.16905 27.3564C6.69516 27.6116 6.19697 27.4658 5.71094 27.3807Z" fill="#C5B7DD"/>
|
||||
<path d="M122.141 22.107C120.683 20.5638 119.893 18.4495 117.816 17.4531C117.998 17.3681 118.107 17.3195 118.216 17.2709C119.699 16.639 120.477 15.351 120.343 13.7471C120.185 11.8516 119.432 10.8187 117.682 10.4664C114.887 9.90741 112.044 10.3691 109.371 10.199C109.541 14.2088 109.492 18.1457 109.395 22.107C110.416 22.107 111.558 22.107 112.7 22.107C112.348 20.8676 112.566 19.6282 112.518 18.4009C112.506 18.0485 112.773 17.9999 113.064 17.9878C115.191 17.8541 116.698 18.8019 117.658 20.8676C117.852 21.2685 117.986 21.6695 117.779 22.107C119.261 22.107 120.707 22.107 122.141 22.107ZM116.236 15.8006C115.106 16.1165 113.952 15.9464 112.797 15.9343C112.518 15.9343 112.506 15.7034 112.506 15.4968C112.506 14.6463 112.506 13.7957 112.506 12.9573C112.506 12.6535 112.627 12.5077 112.931 12.5077C113.417 12.5077 113.903 12.5077 114.389 12.5077C114.389 12.5199 114.389 12.532 114.389 12.532C114.899 12.532 115.397 12.4956 115.896 12.5442C116.758 12.6171 117.22 13.1031 117.317 13.9294C117.415 14.8164 117.014 15.5819 116.236 15.8006Z" fill="white"/>
|
||||
<path d="M73.7928 14.1721C73.2825 11.9363 71.8608 10.6483 69.6372 10.2352C68.0333 9.93141 66.4172 9.93141 64.8133 10.2716C62.7112 10.7212 61.3138 11.9728 60.7913 14.087C60.4511 15.4601 60.4511 16.8574 60.7913 18.2305C61.3138 20.3448 62.6018 21.6935 64.7768 22.1431C65.6152 22.3132 66.4536 22.3983 67.3771 22.374C67.9847 22.4104 68.6408 22.3375 69.3091 22.2525C71.5327 21.9851 73.1488 20.6364 73.7199 18.4857C74.1087 17.064 74.133 15.6181 73.7928 14.1721ZM70.7308 18.1454C70.4756 19.0203 69.9288 19.64 69.0539 19.9073C67.8389 20.284 66.6238 20.284 65.433 19.8344C64.6918 19.555 64.1693 19.0325 63.9262 18.2791C63.4888 16.9425 63.4645 15.5816 63.8776 14.2328C64.145 13.3337 64.7404 12.7383 65.6274 12.4588C66.6359 12.1429 67.6687 12.1307 68.6773 12.3616C70.1111 12.6897 70.8158 13.6496 70.9616 15.3264C70.9859 15.6059 70.9616 15.8975 70.9616 16.177C70.9981 16.8453 70.913 17.5014 70.7308 18.1454Z" fill="white"/>
|
||||
<path d="M58.689 13.4924C58.4945 11.9979 57.5832 10.9043 56.1373 10.5033C55.4933 10.321 54.8371 10.236 54.1567 10.236C52.1518 10.236 50.1469 10.236 48.142 10.236C47.9719 10.236 47.7896 10.1752 47.6924 10.236C48.0326 12.253 47.8382 14.2336 47.8503 16.2021C47.8503 18.1706 48.0205 20.1512 47.7167 22.1196C48.8346 22.1196 49.9646 22.1196 51.1432 22.1196C50.7544 21.026 50.9853 19.9689 50.9124 18.9239C50.8759 18.4743 51.0096 18.3407 51.4713 18.3407C52.8079 18.3407 54.1445 18.4257 55.469 18.2435C57.1336 18.0126 58.3609 16.919 58.6525 15.2908C58.7619 14.6954 58.774 14.1 58.689 13.4924ZM54.7035 15.9226C53.5856 16.2872 52.4312 16.1899 51.2769 16.1656C50.961 16.1535 50.9124 15.8983 50.9124 15.631C50.9124 15.1814 50.9124 14.744 50.9124 14.2944C50.9124 13.8448 50.9124 13.4074 50.9124 12.9578C50.9124 12.7148 50.9731 12.5082 51.2769 12.5082C52.3826 12.4839 53.5005 12.3867 54.5941 12.6662C55.3353 12.8606 55.6755 13.3466 55.7241 14.185C55.7727 15.0599 55.4204 15.6918 54.7035 15.9226Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0">
|
||||
<rect width="161" height="27.5462" fill="white" transform="translate(0 0.454102)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 12 KiB |
161
ui/src/theme/index.ts
Normal file
161
ui/src/theme/index.ts
Normal file
|
@ -0,0 +1,161 @@
|
|||
import { softShadows } from "./shadows";
|
||||
import "@fontsource/dm-mono";
|
||||
import "@fontsource/dm-sans";
|
||||
import common from "@mui/material/colors/common";
|
||||
import muiCreateTheme, {
|
||||
Theme as MuiTheme,
|
||||
} from "@mui/material/styles/createTheme";
|
||||
|
||||
export const createTheme = (): MuiTheme =>
|
||||
muiCreateTheme({
|
||||
components: {
|
||||
MuiBackdrop: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
backgroundColor: "rgba(68, 56, 102, 0.8)",
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiBreadcrumbs: {
|
||||
styleOverrides: {
|
||||
separator: {
|
||||
opacity: "30%",
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiChip: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
backgroundColor: "rgba(0,0,0,0.075)",
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiDialog: {
|
||||
styleOverrides: {
|
||||
paper: {
|
||||
padding: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiDialogActions: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
padding: "16px",
|
||||
display: "flex",
|
||||
flexFlow: "row nowrap",
|
||||
justifyContent: "flex-end",
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiDialogContent: {
|
||||
styleOverrides: {
|
||||
root: { padding: "16px" },
|
||||
},
|
||||
},
|
||||
MuiDialogTitle: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
display: "flex",
|
||||
flexFlow: "row nowrap",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
padding: "16px",
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiFilledInput: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
borderRadius: "4px",
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiLinearProgress: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
borderRadius: 3,
|
||||
overflow: "hidden",
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiListItemIcon: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
minWidth: 32,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiOutlinedInput: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
borderRadius: "4px",
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiTableCell: {
|
||||
styleOverrides: {
|
||||
head: {
|
||||
fontWeight: 600,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
palette: {
|
||||
action: {
|
||||
active: "#39256C",
|
||||
},
|
||||
background: {
|
||||
default: "#FBFBFB",
|
||||
paper: common.white,
|
||||
},
|
||||
primary: {
|
||||
main: "#6F43E7",
|
||||
},
|
||||
secondary: {
|
||||
main: "#49AAA1",
|
||||
},
|
||||
},
|
||||
shadows: softShadows,
|
||||
shape: {
|
||||
borderRadius: "16px",
|
||||
},
|
||||
typography: {
|
||||
fontFamily: [
|
||||
'"DM Sans"',
|
||||
"-apple-system",
|
||||
"BlinkMacSystemFont",
|
||||
'"Segoe UI"',
|
||||
"Roboto",
|
||||
'"Helvetica Neue"',
|
||||
"Arial",
|
||||
"sans-serif",
|
||||
'"Apple Color Emoji"',
|
||||
'"Segoe UI Emoji"',
|
||||
'"Segoe UI Symbol"',
|
||||
].join(","),
|
||||
h1: {
|
||||
fontSize: "3.052rem",
|
||||
fontWeight: 550,
|
||||
},
|
||||
h2: {
|
||||
fontSize: "2.441rem",
|
||||
fontWeight: 550,
|
||||
},
|
||||
h3: {
|
||||
fontSize: "1.953rem",
|
||||
fontWeight: 550,
|
||||
},
|
||||
h4: {
|
||||
fontSize: "1.563rem",
|
||||
fontWeight: 550,
|
||||
},
|
||||
h5: {
|
||||
fontSize: "1.25rem",
|
||||
fontWeight: 550,
|
||||
},
|
||||
h6: {
|
||||
fontSize: "1rem",
|
||||
fontWeight: 550,
|
||||
},
|
||||
},
|
||||
});
|
57
ui/src/theme/shadows.ts
Normal file
57
ui/src/theme/shadows.ts
Normal file
|
@ -0,0 +1,57 @@
|
|||
import type { Shadows } from "@mui/material/styles/shadows";
|
||||
|
||||
export const softShadows: Shadows = [
|
||||
"none",
|
||||
"0 0 0 1px rgba(63,63,68,0.05), 0 1px 2px 0 rgba(63,63,68,0.15)",
|
||||
"0 0 1px 0 rgba(0,0,0,0.31), 0 2px 2px -2px rgba(0,0,0,0.25)",
|
||||
"0 0 1px 0 rgba(0,0,0,0.31), 0 3px 4px -2px rgba(0,0,0,0.25)",
|
||||
"0 0 1px 0 rgba(0,0,0,0.31), 0 3px 4px -2px rgba(0,0,0,0.25)",
|
||||
"0 0 1px 0 rgba(0,0,0,0.31), 0 4px 6px -2px rgba(0,0,0,0.25)",
|
||||
"0 0 1px 0 rgba(0,0,0,0.31), 0 4px 6px -2px rgba(0,0,0,0.25)",
|
||||
"0 0 1px 0 rgba(0,0,0,0.31), 0 4px 8px -2px rgba(0,0,0,0.25)",
|
||||
"0 0 1px 0 rgba(0,0,0,0.31), 0 5px 8px -2px rgba(0,0,0,0.25)",
|
||||
"0 0 1px 0 rgba(0,0,0,0.31), 0 6px 12px -4px rgba(0,0,0,0.25)",
|
||||
"0 0 1px 0 rgba(0,0,0,0.31), 0 7px 12px -4px rgba(0,0,0,0.25)",
|
||||
"0 0 1px 0 rgba(0,0,0,0.31), 0 6px 16px -4px rgba(0,0,0,0.25)",
|
||||
"0 0 1px 0 rgba(0,0,0,0.31), 0 7px 16px -4px rgba(0,0,0,0.25)",
|
||||
"0 0 1px 0 rgba(0,0,0,0.31), 0 8px 18px -8px rgba(0,0,0,0.25)",
|
||||
"0 0 1px 0 rgba(0,0,0,0.31), 0 9px 18px -8px rgba(0,0,0,0.25)",
|
||||
"0 0 1px 0 rgba(0,0,0,0.31), 0 10px 20px -8px rgba(0,0,0,0.25)",
|
||||
"0 0 1px 0 rgba(0,0,0,0.31), 0 11px 20px -8px rgba(0,0,0,0.25)",
|
||||
"0 0 1px 0 rgba(0,0,0,0.31), 0 12px 22px -8px rgba(0,0,0,0.25)",
|
||||
"0 0 1px 0 rgba(0,0,0,0.31), 0 13px 22px -8px rgba(0,0,0,0.25)",
|
||||
"0 0 1px 0 rgba(0,0,0,0.31), 0 14px 24px -8px rgba(0,0,0,0.25)",
|
||||
"0 0 1px 0 rgba(0,0,0,0.31), 0 16px 28px -8px rgba(0,0,0,0.25)",
|
||||
"0 0 1px 0 rgba(0,0,0,0.31), 0 18px 30px -8px rgba(0,0,0,0.25)",
|
||||
"0 0 1px 0 rgba(0,0,0,0.31), 0 20px 32px -8px rgba(0,0,0,0.25)",
|
||||
"0 0 1px 0 rgba(0,0,0,0.31), 0 22px 34px -8px rgba(0,0,0,0.25)",
|
||||
"0 0 1px 0 rgba(0,0,0,0.31), 0 24px 36px -8px rgba(0,0,0,0.25)",
|
||||
];
|
||||
|
||||
export const strongShadows: Shadows = [
|
||||
"none",
|
||||
"0 0 1px 0 rgba(0,0,0,0.70), 0 3px 4px -2px rgba(0,0,0,0.50)",
|
||||
"0 0 1px 0 rgba(0,0,0,0.70), 0 2px 2px -2px rgba(0,0,0,0.50)",
|
||||
"0 0 1px 0 rgba(0,0,0,0.70), 0 3px 4px -2px rgba(0,0,0,0.50)",
|
||||
"0 0 1px 0 rgba(0,0,0,0.70), 0 3px 4px -2px rgba(0,0,0,0.50)",
|
||||
"0 0 1px 0 rgba(0,0,0,0.70), 0 4px 6px -2px rgba(0,0,0,0.50)",
|
||||
"0 0 1px 0 rgba(0,0,0,0.70), 0 4px 6px -2px rgba(0,0,0,0.50)",
|
||||
"0 0 1px 0 rgba(0,0,0,0.70), 0 4px 8px -2px rgba(0,0,0,0.50)",
|
||||
"0 0 1px 0 rgba(0,0,0,0.70), 0 5px 8px -2px rgba(0,0,0,0.50)",
|
||||
"0 0 1px 0 rgba(0,0,0,0.70), 0 6px 12px -4px rgba(0,0,0,0.50)",
|
||||
"0 0 1px 0 rgba(0,0,0,0.70), 0 7px 12px -4px rgba(0,0,0,0.50)",
|
||||
"0 0 1px 0 rgba(0,0,0,0.70), 0 6px 16px -4px rgba(0,0,0,0.50)",
|
||||
"0 0 1px 0 rgba(0,0,0,0.70), 0 7px 16px -4px rgba(0,0,0,0.50)",
|
||||
"0 0 1px 0 rgba(0,0,0,0.70), 0 8px 18px -8px rgba(0,0,0,0.50)",
|
||||
"0 0 1px 0 rgba(0,0,0,0.70), 0 9px 18px -8px rgba(0,0,0,0.50)",
|
||||
"0 0 1px 0 rgba(0,0,0,0.70), 0 10px 20px -8px rgba(0,0,0,0.50)",
|
||||
"0 0 1px 0 rgba(0,0,0,0.70), 0 11px 20px -8px rgba(0,0,0,0.50)",
|
||||
"0 0 1px 0 rgba(0,0,0,0.70), 0 12px 22px -8px rgba(0,0,0,0.50)",
|
||||
"0 0 1px 0 rgba(0,0,0,0.70), 0 13px 22px -8px rgba(0,0,0,0.50)",
|
||||
"0 0 1px 0 rgba(0,0,0,0.70), 0 14px 24px -8px rgba(0,0,0,0.50)",
|
||||
"0 0 1px 0 rgba(0,0,0,0.70), 0 16px 28px -8px rgba(0,0,0,0.50)",
|
||||
"0 0 1px 0 rgba(0,0,0,0.70), 0 18px 30px -8px rgba(0,0,0,0.50)",
|
||||
"0 0 1px 0 rgba(0,0,0,0.70), 0 20px 32px -8px rgba(0,0,0,0.50)",
|
||||
"0 0 1px 0 rgba(0,0,0,0.70), 0 22px 34px -8px rgba(0,0,0,0.50)",
|
||||
"0 0 1px 0 rgba(0,0,0,0.70), 0 24px 36px -8px rgba(0,0,0,0.50)",
|
||||
];
|
127
ui/src/types/index.ts
Normal file
127
ui/src/types/index.ts
Normal file
|
@ -0,0 +1,127 @@
|
|||
export type Claims = Record<string, unknown[]>;
|
||||
|
||||
export type DirectoryUser = {
|
||||
displayName: string;
|
||||
email: string;
|
||||
groupIds: string[];
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type Group = {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type Session = {
|
||||
audience: string[];
|
||||
claims: Claims;
|
||||
deviceCredentials: Array<{
|
||||
typeId: string;
|
||||
id: string;
|
||||
}>;
|
||||
expiresAt: string;
|
||||
id: string;
|
||||
idToken: {
|
||||
expiresAt: string;
|
||||
issuedAt: string;
|
||||
issuer: string;
|
||||
raw: string;
|
||||
subject: string;
|
||||
};
|
||||
issuedAt: string;
|
||||
oauthToken: {
|
||||
accessToken: string;
|
||||
expiresAt: string;
|
||||
refreshToken: string;
|
||||
tokenType: string;
|
||||
};
|
||||
userId: string;
|
||||
};
|
||||
|
||||
export type User = {
|
||||
claims: Claims;
|
||||
deviceCredentialIds: string[];
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type WebAuthnCreationOptions = {
|
||||
attestation: AttestationConveyancePreference;
|
||||
authenticatorSelection: {
|
||||
authenticatorAttachment?: AuthenticatorAttachment;
|
||||
requireResidentKey?: boolean;
|
||||
residentKey?: ResidentKeyRequirement;
|
||||
userVerification?: UserVerificationRequirement;
|
||||
};
|
||||
challenge: string;
|
||||
pubKeyCredParams: PublicKeyCredentialParameters[];
|
||||
rp: {
|
||||
name: string;
|
||||
};
|
||||
timeout: number;
|
||||
user: {
|
||||
displayName: string;
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type WebAuthnRequestOptions = {
|
||||
allowCredentials: Array<{
|
||||
type: "public-key";
|
||||
id: string;
|
||||
}>;
|
||||
challenge: string;
|
||||
timeout: number;
|
||||
userVerification: UserVerificationRequirement;
|
||||
};
|
||||
|
||||
// page data
|
||||
|
||||
type BasePageData = {
|
||||
csrfToken?: string;
|
||||
signOutUrl?: string;
|
||||
};
|
||||
|
||||
export type ErrorPageData = BasePageData & {
|
||||
page: "Error";
|
||||
|
||||
canDebug?: boolean;
|
||||
debugUrl?: string;
|
||||
error?: string;
|
||||
requestId?: string;
|
||||
status?: number;
|
||||
statusText?: string;
|
||||
version?: string;
|
||||
};
|
||||
|
||||
export type DeviceEnrolledPageData = BasePageData & {
|
||||
page: "DeviceEnrolled";
|
||||
};
|
||||
|
||||
export type UserInfoPageData = BasePageData & {
|
||||
page: "UserInfo";
|
||||
|
||||
csrfToken: string;
|
||||
directoryGroups?: Group[];
|
||||
directoryUser?: DirectoryUser;
|
||||
session?: Session;
|
||||
user?: User;
|
||||
webAuthnUrl?: string;
|
||||
};
|
||||
|
||||
export type WebAuthnRegistrationPageData = BasePageData & {
|
||||
page: "WebAuthnRegistration";
|
||||
|
||||
creationOptions?: WebAuthnCreationOptions;
|
||||
csrfToken: string;
|
||||
requestOptions?: WebAuthnRequestOptions;
|
||||
selfUrl: string;
|
||||
};
|
||||
|
||||
export type PageData =
|
||||
| ErrorPageData
|
||||
| DeviceEnrolledPageData
|
||||
| UserInfoPageData
|
||||
| WebAuthnRegistrationPageData;
|
94
ui/src/util/base64.ts
Normal file
94
ui/src/util/base64.ts
Normal file
|
@ -0,0 +1,94 @@
|
|||
// The MIT License (MIT)
|
||||
|
||||
// Copyright (c) 2020 Blake Embrey (hello@blakeembrey.com)
|
||||
// Copyright (c) 2012 Niklas von Hertzen (https://github.com/niklasvh/base64-arraybuffer)
|
||||
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
/**
|
||||
* Original source: https://github.com/niklasvh/base64-arraybuffer.
|
||||
*/
|
||||
const base64Chars =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
const base64UrlChars =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
||||
const base64Lookup = new Uint8Array(256);
|
||||
for (let i = 0; i < base64Chars.length; i++) {
|
||||
base64Lookup[base64Chars.charCodeAt(i)] = i;
|
||||
}
|
||||
// Support base64url.
|
||||
base64Lookup[45 /* - */] = 62;
|
||||
base64Lookup[95 /* _ */] = 63;
|
||||
/**
|
||||
* Encode an `ArrayBuffer` to base64.
|
||||
*/
|
||||
export function encode(
|
||||
buffer: ArrayBuffer,
|
||||
chars = base64Chars,
|
||||
padding = "="
|
||||
): string {
|
||||
const bytes = new Uint8Array(buffer);
|
||||
const length = bytes.length;
|
||||
let base64 = "";
|
||||
for (let i = 0; i < length; i += 3) {
|
||||
base64 += chars[bytes[i] >> 2];
|
||||
base64 += chars[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)];
|
||||
base64 += chars[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)];
|
||||
base64 += chars[bytes[i + 2] & 63];
|
||||
}
|
||||
if (length % 3 === 2) {
|
||||
base64 = base64.slice(0, base64.length - 1) + padding;
|
||||
} else if (length % 3 === 1) {
|
||||
base64 = base64.slice(0, base64.length - 2) + padding + padding;
|
||||
}
|
||||
return base64;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode using the base64url variant.
|
||||
*/
|
||||
export function encodeUrl(buffer: ArrayBuffer): string {
|
||||
return encode(buffer, base64UrlChars, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a base64 encoded string.
|
||||
*/
|
||||
export function decode(base64: string, lookup = base64Lookup): ArrayBuffer {
|
||||
const length = base64.length;
|
||||
let bufferLength = Math.floor(base64.length * 0.75);
|
||||
let p = 0;
|
||||
if (base64[length - 1] === "=") {
|
||||
bufferLength--;
|
||||
if (base64[length - 2] === "=") {
|
||||
bufferLength--;
|
||||
}
|
||||
}
|
||||
const bytes = new Uint8Array(bufferLength);
|
||||
for (let i = 0; i < length; i += 4) {
|
||||
const encoded1 = lookup[base64.charCodeAt(i)];
|
||||
const encoded2 = lookup[base64.charCodeAt(i + 1)];
|
||||
const encoded3 = lookup[base64.charCodeAt(i + 2)];
|
||||
const encoded4 = lookup[base64.charCodeAt(i + 3)];
|
||||
bytes[p++] = (encoded1 << 2) | (encoded2 >> 4);
|
||||
bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2);
|
||||
bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63);
|
||||
}
|
||||
return bytes;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue