feat: setup dashboard
- Setup basic code structure - Add routes - Add layout components for authentication and dashboard pages - Add session handling - Add login, signup and session
This commit is contained in:
parent
f9ed91934e
commit
8bee841d66
33
dashboard/package-lock.json
generated
33
dashboard/package-lock.json
generated
|
@ -781,6 +781,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz",
|
||||||
"integrity": "sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA=="
|
"integrity": "sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA=="
|
||||||
},
|
},
|
||||||
|
"@graphql-typed-document-node/core": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-NQ17ii0rK1b34VZonlmT2QMJFI70m0TRwbknO/ihlbatXyaktDhN/98vBiUU6kNBPljqGqyIrl2T4nY2RpFANg=="
|
||||||
|
},
|
||||||
"@popperjs/core": {
|
"@popperjs/core": {
|
||||||
"version": "2.11.0",
|
"version": "2.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.0.tgz",
|
||||||
|
@ -891,6 +896,15 @@
|
||||||
"resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.0.tgz",
|
||||||
"integrity": "sha1-DSUBJorY+ZYrdA04fEZU9fjiPlI="
|
"integrity": "sha1-DSUBJorY+ZYrdA04fEZU9fjiPlI="
|
||||||
},
|
},
|
||||||
|
"@urql/core": {
|
||||||
|
"version": "2.3.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@urql/core/-/core-2.3.6.tgz",
|
||||||
|
"integrity": "sha512-PUxhtBh7/8167HJK6WqBv6Z0piuiaZHQGYbhwpNL9aIQmLROPEdaUYkY4wh45wPQXcTpnd11l0q3Pw+TI11pdw==",
|
||||||
|
"requires": {
|
||||||
|
"@graphql-typed-document-node/core": "^3.1.0",
|
||||||
|
"wonka": "^4.0.14"
|
||||||
|
}
|
||||||
|
},
|
||||||
"ansi-styles": {
|
"ansi-styles": {
|
||||||
"version": "3.2.1",
|
"version": "3.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
|
||||||
|
@ -1232,6 +1246,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
|
||||||
"integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="
|
"integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="
|
||||||
},
|
},
|
||||||
|
"graphql": {
|
||||||
|
"version": "16.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/graphql/-/graphql-16.2.0.tgz",
|
||||||
|
"integrity": "sha512-MuQd7XXrdOcmfwuLwC2jNvx0n3rxIuNYOxUtiee5XOmfrWo613ar2U8pE7aHAKh8VwfpifubpD9IP+EdEAEOsA=="
|
||||||
|
},
|
||||||
"has": {
|
"has": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
|
||||||
|
@ -1611,6 +1630,15 @@
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.4.tgz",
|
||||||
"integrity": "sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg=="
|
"integrity": "sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg=="
|
||||||
},
|
},
|
||||||
|
"urql": {
|
||||||
|
"version": "2.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/urql/-/urql-2.0.6.tgz",
|
||||||
|
"integrity": "sha512-ovK9mx7YxD/CKUwVZGbEDBzZjbCcNsr1990nIhDCKe3Ij/0gNcIa+0EIyXKceOPuYDYKavIoaNQV2kOZjQxFcw==",
|
||||||
|
"requires": {
|
||||||
|
"@urql/core": "^2.3.6",
|
||||||
|
"wonka": "^4.0.14"
|
||||||
|
}
|
||||||
|
},
|
||||||
"use-callback-ref": {
|
"use-callback-ref": {
|
||||||
"version": "1.2.5",
|
"version": "1.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.2.5.tgz",
|
||||||
|
@ -1640,6 +1668,11 @@
|
||||||
"loose-envify": "^1.0.0"
|
"loose-envify": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"wonka": {
|
||||||
|
"version": "4.0.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/wonka/-/wonka-4.0.15.tgz",
|
||||||
|
"integrity": "sha512-U0IUQHKXXn6PFo9nqsHphVCE5m3IntqZNB9Jjn7EB1lrR7YTDY3YWgFvEvwniTzXSvOH/XMzAZaIfJF/LvHYXg=="
|
||||||
|
},
|
||||||
"yaml": {
|
"yaml": {
|
||||||
"version": "1.10.2",
|
"version": "1.10.2",
|
||||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
|
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
|
||||||
|
|
|
@ -19,10 +19,12 @@
|
||||||
"@types/react-router-dom": "^5.3.2",
|
"@types/react-router-dom": "^5.3.2",
|
||||||
"esbuild": "^0.14.9",
|
"esbuild": "^0.14.9",
|
||||||
"framer-motion": "^5.5.5",
|
"framer-motion": "^5.5.5",
|
||||||
|
"graphql": "^16.2.0",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-icons": "^4.3.1",
|
"react-icons": "^4.3.1",
|
||||||
"react-router-dom": "^6.2.1",
|
"react-router-dom": "^6.2.1",
|
||||||
"typescript": "^4.5.4"
|
"typescript": "^4.5.4",
|
||||||
|
"urql": "^2.0.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,44 @@
|
||||||
import * as React from 'react';
|
import * as React from "react";
|
||||||
import { Text, ChakraProvider } from '@chakra-ui/react';
|
import { ChakraProvider, extendTheme } from "@chakra-ui/react";
|
||||||
import { MdStar } from 'react-icons/md';
|
import { BrowserRouter } from "react-router-dom";
|
||||||
import { BrowserRouter } from 'react-router-dom';
|
import { createClient, Provider } from "urql";
|
||||||
|
import {AppRoutes} from './routes'
|
||||||
|
import { AuthContainer } from "./containers/AuthContainer";
|
||||||
|
|
||||||
export default function Example() {
|
const queryClient = createClient({
|
||||||
|
url: "/graphql",
|
||||||
|
fetchOptions: () => {
|
||||||
|
return {
|
||||||
|
credentials: "include",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const theme = extendTheme({
|
||||||
|
styles: {
|
||||||
|
global: {
|
||||||
|
"html, body, #root": {
|
||||||
|
height: "100%",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
blue: {
|
||||||
|
500: "rgb(59,130,246)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
<ChakraProvider>
|
<ChakraProvider theme={theme}>
|
||||||
<BrowserRouter>
|
<Provider value={queryClient}>
|
||||||
<h1>Dashboard</h1>
|
<BrowserRouter basename="/dashboard">
|
||||||
|
<AuthContainer>
|
||||||
|
<AppRoutes />
|
||||||
|
</AuthContainer>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
|
</Provider>
|
||||||
</ChakraProvider>
|
</ChakraProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
54
dashboard/src/components/Sidebar.tsx
Normal file
54
dashboard/src/components/Sidebar.tsx
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
import { Box, Image, Link, Text } from "@chakra-ui/react";
|
||||||
|
import { NavLink, useLocation } from "react-router-dom";
|
||||||
|
import React from "react";
|
||||||
|
import { LOGO_URL } from "../constants";
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
route: "/users",
|
||||||
|
name: "Users",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
route: "/settings",
|
||||||
|
name: "Settings",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const Sidebar = () => {
|
||||||
|
const { pathname } = useLocation();
|
||||||
|
return (
|
||||||
|
<Box as="nav" h="100%">
|
||||||
|
<NavLink to="/">
|
||||||
|
<Box d="flex" alignItems="center" p="4" mt="4" mb="10">
|
||||||
|
<Image w="8" src={LOGO_URL} alt="" />
|
||||||
|
<Text
|
||||||
|
color="white"
|
||||||
|
casing="uppercase"
|
||||||
|
fontSize="1xl"
|
||||||
|
ml="3"
|
||||||
|
letterSpacing="1.5px"
|
||||||
|
>
|
||||||
|
Authorizer
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</NavLink>
|
||||||
|
{routes.map(({ route, name }: any) => (
|
||||||
|
<Link
|
||||||
|
color={pathname === route ? "blue.500" : "white"}
|
||||||
|
transition="all ease-in 0.2s"
|
||||||
|
_hover={{ color: pathname === route ? "blue.200" : "whiteAlpha.700" }}
|
||||||
|
px="4"
|
||||||
|
py="2"
|
||||||
|
bg={pathname === route ? "white" : ""}
|
||||||
|
fontWeight="bold"
|
||||||
|
display="block"
|
||||||
|
as={NavLink}
|
||||||
|
key={name}
|
||||||
|
to={route}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,5 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
export default function AuthLayout() {
|
|
||||||
return <h1>Auth Layout</h1>;
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
export default function DefaultLayout() {
|
|
||||||
return <h1>Default Layout</h1>;
|
|
||||||
}
|
|
1
dashboard/src/constants.ts
Normal file
1
dashboard/src/constants.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export const LOGO_URL = "https://user-images.githubusercontent.com/6964334/147834043-fc384cab-e7ca-40f8-9663-38fc25fd5f3a.png"
|
37
dashboard/src/containers/AuthContainer.tsx
Normal file
37
dashboard/src/containers/AuthContainer.tsx
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import { Center, Spinner } from "@chakra-ui/react";
|
||||||
|
import React from "react";
|
||||||
|
import { Navigate, Route, Routes } from "react-router-dom";
|
||||||
|
import { useLocation } from "react-router-dom";
|
||||||
|
import { useQuery } from "urql";
|
||||||
|
import { AdminSessionQuery } from "../graphql/queries";
|
||||||
|
import { hasAdminSecret } from "../utils";
|
||||||
|
|
||||||
|
export const AuthContainer = ({ children }: { children: any }) => {
|
||||||
|
const { pathname } = useLocation();
|
||||||
|
const isOnboardingComplete = hasAdminSecret();
|
||||||
|
const [result] = useQuery({
|
||||||
|
query: AdminSessionQuery,
|
||||||
|
pause: !isOnboardingComplete,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.fetching) {
|
||||||
|
return (
|
||||||
|
<Center>
|
||||||
|
<Spinner />
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
result?.error?.message.includes("unauthorized") &&
|
||||||
|
pathname !== "/login"
|
||||||
|
) {
|
||||||
|
return <Navigate to="login" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isOnboardingComplete && pathname !== "/setup") {
|
||||||
|
return <Navigate to="setup" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return children;
|
||||||
|
};
|
15
dashboard/src/graphql/mutation/index.ts
Normal file
15
dashboard/src/graphql/mutation/index.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
export const AdminSignup = `
|
||||||
|
mutation adminSignup($secret: String!) {
|
||||||
|
_admin_signup (params: {admin_secret: $secret}) {
|
||||||
|
message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const AdminLogin = `
|
||||||
|
mutation adminLogin($secret: String!){
|
||||||
|
_admin_login(params: { admin_secret: $secret }) {
|
||||||
|
message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
7
dashboard/src/graphql/queries/index.ts
Normal file
7
dashboard/src/graphql/queries/index.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
export const AdminSessionQuery = `
|
||||||
|
query {
|
||||||
|
_admin_session{
|
||||||
|
message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
29
dashboard/src/layouts/AuthLayout.tsx
Normal file
29
dashboard/src/layouts/AuthLayout.tsx
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import { Box, Center, Flex, Image, Text } from "@chakra-ui/react";
|
||||||
|
import React from "react";
|
||||||
|
import { LOGO_URL } from "../constants";
|
||||||
|
|
||||||
|
export function AuthLayout({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<Flex flexWrap="wrap" h="100%">
|
||||||
|
<Center h="100%" flex="3" bg="blue.500" flexDirection="column">
|
||||||
|
<Image
|
||||||
|
src={LOGO_URL}
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Text
|
||||||
|
color="white"
|
||||||
|
casing="uppercase"
|
||||||
|
fontSize="3xl"
|
||||||
|
mt="2"
|
||||||
|
letterSpacing="2.25px"
|
||||||
|
>
|
||||||
|
Authorizer
|
||||||
|
</Text>
|
||||||
|
</Center>
|
||||||
|
<Center h="100%" flex="2">
|
||||||
|
{children}
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
14
dashboard/src/layouts/DashboardLayout.tsx
Normal file
14
dashboard/src/layouts/DashboardLayout.tsx
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import { Box, Flex } from "@chakra-ui/react";
|
||||||
|
import React from "react";
|
||||||
|
import { Sidebar } from "../components/Sidebar";
|
||||||
|
|
||||||
|
export function DashboardLayout({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<Flex flexWrap="wrap" h="100%">
|
||||||
|
<Box maxW="72" bg="blue.500" flex="1">
|
||||||
|
<Sidebar />
|
||||||
|
</Box>
|
||||||
|
<Box as="main" flex="2" p="10">{children}</Box>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
90
dashboard/src/pages/Auth.tsx
Normal file
90
dashboard/src/pages/Auth.tsx
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
FormControl,
|
||||||
|
FormLabel,
|
||||||
|
Input,
|
||||||
|
useToast,
|
||||||
|
VStack,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import React, { useEffect } from "react";
|
||||||
|
import { useMutation } from "urql";
|
||||||
|
import { AuthLayout } from "../layouts/AuthLayout";
|
||||||
|
import { AdminLogin, AdminSignup } from "../graphql/mutation";
|
||||||
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
export const Auth = () => {
|
||||||
|
const [loginResult, login] = useMutation(AdminLogin);
|
||||||
|
const [signUpResult, signup] = useMutation(AdminSignup);
|
||||||
|
|
||||||
|
const toast = useToast();
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const { pathname } = useLocation();
|
||||||
|
const isLogin = pathname === "/login";
|
||||||
|
|
||||||
|
const handleAdminSecret = (e: any) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const formValues = [...e.target.elements].reduce((agg: any, elem: any) => {
|
||||||
|
if (elem.id) {
|
||||||
|
return {
|
||||||
|
...agg,
|
||||||
|
[elem.id]: elem.value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return agg;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
(isLogin ? login : signup)({
|
||||||
|
secret: formValues["admin-secret"],
|
||||||
|
}).then((res) => {
|
||||||
|
if (!res.error?.name) {
|
||||||
|
navigate("/");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const errors = isLogin ? loginResult.error : signUpResult.error;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (errors?.graphQLErrors) {
|
||||||
|
(errors?.graphQLErrors || []).map((error: any) => {
|
||||||
|
toast({
|
||||||
|
title: error.message,
|
||||||
|
isClosable: true,
|
||||||
|
status: "error",
|
||||||
|
position:"bottom-right"
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [errors])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthLayout>
|
||||||
|
<form onSubmit={handleAdminSecret}>
|
||||||
|
<VStack spacing="2.5" justify="space-between">
|
||||||
|
<FormControl isRequired>
|
||||||
|
<FormLabel htmlFor="admin-secret">
|
||||||
|
{isLogin ? "Enter" : "Setup"} Admin Secret
|
||||||
|
</FormLabel>
|
||||||
|
<Input
|
||||||
|
size="lg"
|
||||||
|
id="admin-secret"
|
||||||
|
placeholder="Admin secret"
|
||||||
|
minLength={6}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<Button
|
||||||
|
isLoading={signUpResult.fetching || loginResult.fetching}
|
||||||
|
colorScheme="blue"
|
||||||
|
size="lg"
|
||||||
|
w="100%"
|
||||||
|
d="block"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
{isLogin ? "Login" : "Sign up"}
|
||||||
|
</Button>
|
||||||
|
</VStack>
|
||||||
|
</form>
|
||||||
|
</AuthLayout>
|
||||||
|
);
|
||||||
|
};
|
6
dashboard/src/pages/Home.tsx
Normal file
6
dashboard/src/pages/Home.tsx
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import { Box } from "@chakra-ui/react";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export function Home() {
|
||||||
|
return <Box>Welcome to Authorizer dashboard!</Box>;
|
||||||
|
}
|
6
dashboard/src/pages/Users.tsx
Normal file
6
dashboard/src/pages/Users.tsx
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import { Box } from "@chakra-ui/react";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export function Users() {
|
||||||
|
return <Box>users</Box>;
|
||||||
|
}
|
26
dashboard/src/routes/index.tsx
Normal file
26
dashboard/src/routes/index.tsx
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import React from "react";
|
||||||
|
import { Outlet, Route, Routes } from "react-router-dom";
|
||||||
|
import { DashboardLayout } from "../layouts/DashboardLayout";
|
||||||
|
import { Auth } from "../pages/Auth";
|
||||||
|
|
||||||
|
import { Home } from "../pages/Home";
|
||||||
|
import { Users } from "../pages/Users";
|
||||||
|
|
||||||
|
export const AppRoutes = () => {
|
||||||
|
return (
|
||||||
|
<Routes>
|
||||||
|
<Route path="login" element={<Auth />} />
|
||||||
|
<Route path="setup" element={<Auth />} />
|
||||||
|
<Route
|
||||||
|
element={
|
||||||
|
<DashboardLayout>
|
||||||
|
<Outlet />
|
||||||
|
</DashboardLayout>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Route path="/" element={<Home />} />
|
||||||
|
<Route path="users" element={<Users />} />
|
||||||
|
</Route>
|
||||||
|
</Routes>
|
||||||
|
);
|
||||||
|
};
|
3
dashboard/src/utils/index.ts
Normal file
3
dashboard/src/utils/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export const hasAdminSecret = () => {
|
||||||
|
return (<any>window)["__authorizer__"].isOnboardingCompleted === true
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user