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:
Yash Joshi 2022-01-15 21:15:46 +05:30
parent f9ed91934e
commit 8bee841d66
19 changed files with 366 additions and 23 deletions

View File

@ -781,6 +781,11 @@
"resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz",
"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": {
"version": "2.11.0",
"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",
"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": {
"version": "3.2.1",
"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",
"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": {
"version": "1.0.3",
"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",
"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": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.2.5.tgz",
@ -1640,6 +1668,11 @@
"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": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",

View File

@ -19,10 +19,12 @@
"@types/react-router-dom": "^5.3.2",
"esbuild": "^0.14.9",
"framer-motion": "^5.5.5",
"graphql": "^16.2.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-icons": "^4.3.1",
"react-router-dom": "^6.2.1",
"typescript": "^4.5.4"
"typescript": "^4.5.4",
"urql": "^2.0.6"
}
}

View File

@ -1,14 +1,44 @@
import * as React from 'react';
import { Text, ChakraProvider } from '@chakra-ui/react';
import { MdStar } from 'react-icons/md';
import { BrowserRouter } from 'react-router-dom';
import * as React from "react";
import { ChakraProvider, extendTheme } from "@chakra-ui/react";
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 (
<ChakraProvider>
<BrowserRouter>
<h1>Dashboard</h1>
<ChakraProvider theme={theme}>
<Provider value={queryClient}>
<BrowserRouter basename="/dashboard">
<AuthContainer>
<AppRoutes />
</AuthContainer>
</BrowserRouter>
</Provider>
</ChakraProvider>
);
}

View 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>
);
};

View File

@ -1,5 +0,0 @@
import React from 'react';
export default function AuthLayout() {
return <h1>Auth Layout</h1>;
}

View File

@ -1,5 +0,0 @@
import React from 'react';
export default function DefaultLayout() {
return <h1>Default Layout</h1>;
}

View File

@ -0,0 +1 @@
export const LOGO_URL = "https://user-images.githubusercontent.com/6964334/147834043-fc384cab-e7ca-40f8-9663-38fc25fd5f3a.png"

View 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;
};

View 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
}
}
`

View File

@ -0,0 +1,7 @@
export const AdminSessionQuery = `
query {
_admin_session{
message
}
}
`;

View 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>
);
}

View 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>
);
}

View 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>
);
};

View 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>;
}

View File

@ -0,0 +1,6 @@
import { Box } from "@chakra-ui/react";
import React from "react";
export function Users() {
return <Box>users</Box>;
}

View 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>
);
};

View File

@ -0,0 +1,3 @@
export const hasAdminSecret = () => {
return (<any>window)["__authorizer__"].isOnboardingCompleted === true
}