diff --git a/dashboard/src/App.tsx b/dashboard/src/App.tsx index 4effe54..24fe062 100644 --- a/dashboard/src/App.tsx +++ b/dashboard/src/App.tsx @@ -1,44 +1,44 @@ -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"; +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 { AuthContextProvider } from './contexts/AuthContext'; const queryClient = createClient({ - url: "/graphql", - fetchOptions: () => { - return { - credentials: "include", - }; - }, + url: '/graphql', + fetchOptions: () => { + return { + credentials: 'include', + }; + }, }); const theme = extendTheme({ - styles: { - global: { - "html, body, #root": { - height: "100%", - }, - }, - }, - colors: { - blue: { - 500: "rgb(59,130,246)", - }, - }, + styles: { + global: { + 'html, body, #root': { + height: '100%', + }, + }, + }, + colors: { + blue: { + 500: 'rgb(59,130,246)', + }, + }, }); export default function App() { - return ( - - - - - - - - - - ); + return ( + + + + + + + + + + ); } diff --git a/dashboard/src/components/Sidebar.tsx b/dashboard/src/components/Sidebar.tsx index d4fbb13..ab82762 100644 --- a/dashboard/src/components/Sidebar.tsx +++ b/dashboard/src/components/Sidebar.tsx @@ -1,54 +1,78 @@ -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"; +import { Box, Image, Link, Text, Button } from '@chakra-ui/react'; +import { NavLink, useLocation, useNavigate } from 'react-router-dom'; +import React from 'react'; +import { LOGO_URL } from '../constants'; +import { useMutation } from 'urql'; +import { AdminLogout } from '../graphql/mutation'; +import { useAuthContext } from '../contexts/AuthContext'; const routes = [ - { - route: "/users", - name: "Users", - }, - { - route: "/settings", - name: "Settings", - }, + { + route: '/users', + name: 'Users', + }, + { + route: '/', + name: 'Environment Variable', + }, ]; export const Sidebar = () => { - const { pathname } = useLocation(); - return ( - - - - - - Authorizer - - - - {routes.map(({ route, name }: any) => ( - - {name} - - ))} - - ); + const { pathname } = useLocation(); + const [_, logout] = useMutation(AdminLogout); + const { setIsLoggedIn } = useAuthContext(); + const navigate = useNavigate(); + + const handleLogout = async () => { + await logout(); + setIsLoggedIn(false); + navigate('/', { replace: true }); + }; + + return ( + + + + + + Authorizer + + + + {routes.map(({ route, name }: any) => ( + + {name} + + ))} + + + + + + ); }; diff --git a/dashboard/src/components/layouts/AuthLayout.tsx b/dashboard/src/components/layouts/AuthLayout.tsx deleted file mode 100644 index 0ba7800..0000000 --- a/dashboard/src/components/layouts/AuthLayout.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import React from 'react'; - -export default function AuthLayout() { - return

Auth Layout

; -} diff --git a/dashboard/src/components/layouts/DefaultLayout.tsx b/dashboard/src/components/layouts/DefaultLayout.tsx deleted file mode 100644 index 505b6c5..0000000 --- a/dashboard/src/components/layouts/DefaultLayout.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import React from 'react'; - -export default function DefaultLayout() { - return

Default Layout

; -} diff --git a/dashboard/src/containers/AuthContainer.tsx b/dashboard/src/containers/AuthContainer.tsx deleted file mode 100644 index e8aad71..0000000 --- a/dashboard/src/containers/AuthContainer.tsx +++ /dev/null @@ -1,37 +0,0 @@ -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 ( -
- -
- ); - } - - if ( - result?.error?.message.includes("unauthorized") && - pathname !== "/login" - ) { - return ; - } - - if (!isOnboardingComplete && pathname !== "/setup") { - return ; - } - - return children; -}; diff --git a/dashboard/src/contexts/AuthContext.tsx b/dashboard/src/contexts/AuthContext.tsx index e69de29..6a1c57e 100644 --- a/dashboard/src/contexts/AuthContext.tsx +++ b/dashboard/src/contexts/AuthContext.tsx @@ -0,0 +1,49 @@ +import React, { createContext, useState, useContext, useEffect } from 'react'; +import { Center, Spinner } from '@chakra-ui/react'; +import { useQuery } from 'urql'; +import { useLocation, useNavigate } from 'react-router-dom'; + +import { AdminSessionQuery } from '../graphql/queries'; +import { hasAdminSecret } from '../utils'; + +const AuthContext = createContext({ + isLoggedIn: false, + setIsLoggedIn: (data: boolean) => {}, +}); + +export const AuthContextProvider = ({ children }: { children: any }) => { + const [isLoggedIn, setIsLoggedIn] = useState(false); + + const { pathname } = useLocation(); + const navigate = useNavigate(); + + const isOnboardingComplete = hasAdminSecret(); + const [{ fetching, data, error }] = useQuery({ + query: AdminSessionQuery, + }); + + useEffect(() => { + if (!fetching && !error) { + setIsLoggedIn(true); + if (pathname === '/login' || pathname === 'signup') { + navigate('/', { replace: true }); + } + } + }, [fetching, error]); + + if (fetching) { + return ( +
+ +
+ ); + } + + return ( + + {children} + + ); +}; + +export const useAuthContext = () => useContext(AuthContext); diff --git a/dashboard/src/graphql/mutation/index.ts b/dashboard/src/graphql/mutation/index.ts index 01fcacc..1f070f2 100644 --- a/dashboard/src/graphql/mutation/index.ts +++ b/dashboard/src/graphql/mutation/index.ts @@ -12,4 +12,12 @@ mutation adminLogin($secret: String!){ message } } -` \ No newline at end of file +`; + +export const AdminLogout = ` + mutation adminLogout { + _admin_logout { + message + } + } +`; diff --git a/dashboard/src/layouts/AuthLayout.tsx b/dashboard/src/layouts/AuthLayout.tsx index 10c73bd..4ce69e5 100644 --- a/dashboard/src/layouts/AuthLayout.tsx +++ b/dashboard/src/layouts/AuthLayout.tsx @@ -1,29 +1,26 @@ -import { Box, Center, Flex, Image, Text } from "@chakra-ui/react"; -import React from "react"; -import { LOGO_URL } from "../constants"; +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 ( - -
- + return ( + +
+ - - Authorizer - -
-
- {children} -
-
- ); + + Authorizer + +
+
+ {children} +
+
+ ); } diff --git a/dashboard/src/layouts/DashboardLayout.tsx b/dashboard/src/layouts/DashboardLayout.tsx index d3b663f..03ce24f 100644 --- a/dashboard/src/layouts/DashboardLayout.tsx +++ b/dashboard/src/layouts/DashboardLayout.tsx @@ -1,14 +1,16 @@ -import { Box, Flex } from "@chakra-ui/react"; -import React from "react"; -import { Sidebar } from "../components/Sidebar"; +import { Box, Flex } from '@chakra-ui/react'; +import React from 'react'; +import { Sidebar } from '../components/Sidebar'; export function DashboardLayout({ children }: { children: React.ReactNode }) { - return ( - - - - - {children} - - ); + return ( + + + + + + {children} + + + ); } diff --git a/dashboard/src/pages/Auth.tsx b/dashboard/src/pages/Auth.tsx index 0db905f..f8f7574 100644 --- a/dashboard/src/pages/Auth.tsx +++ b/dashboard/src/pages/Auth.tsx @@ -1,90 +1,95 @@ 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"; + 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 { useNavigate } from 'react-router-dom'; +import { useAuthContext } from '../contexts/AuthContext'; +import { capitalizeFirstLetter, hasAdminSecret } from '../utils'; export const Auth = () => { - const [loginResult, login] = useMutation(AdminLogin); - const [signUpResult, signup] = useMutation(AdminSignup); + const [loginResult, login] = useMutation(AdminLogin); + const [signUpResult, signup] = useMutation(AdminSignup); + const { setIsLoggedIn } = useAuthContext(); - const toast = useToast(); - const navigate = useNavigate() - const { pathname } = useLocation(); - const isLogin = pathname === "/login"; + const toast = useToast(); + const navigate = useNavigate(); + const isLogin = hasAdminSecret(); - 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, - }; - } + const handleSubmit = (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; - }, {}); + return agg; + }, {}); - (isLogin ? login : signup)({ - secret: formValues["admin-secret"], - }).then((res) => { - if (!res.error?.name) { - navigate("/"); - } - }); - }; + (isLogin ? login : signup)({ + secret: formValues['admin-secret'], + }).then((res) => { + setIsLoggedIn(true); + if (res.data) { + navigate('/', { replace: true }); + } + }); + }; - const errors = isLogin ? loginResult.error : signUpResult.error; + 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]) + useEffect(() => { + if (errors?.graphQLErrors) { + (errors?.graphQLErrors || []).map((error: any) => { + toast({ + title: capitalizeFirstLetter(error.message), + isClosable: true, + status: 'error', + position: 'bottom-right', + }); + }); + } + }, [errors]); - return ( - -
- - - - {isLogin ? "Enter" : "Setup"} Admin Secret - - - - - -
-
- ); + return ( + +
+ + + + {isLogin ? 'Enter' : 'Setup'} Admin Secret + + + + + +
+
+ ); }; diff --git a/dashboard/src/routes/index.tsx b/dashboard/src/routes/index.tsx index 3c1e2f9..a967160 100644 --- a/dashboard/src/routes/index.tsx +++ b/dashboard/src/routes/index.tsx @@ -1,26 +1,34 @@ -import React from "react"; -import { Outlet, Route, Routes } from "react-router-dom"; -import { DashboardLayout } from "../layouts/DashboardLayout"; -import { Auth } from "../pages/Auth"; +import React from 'react'; +import { Outlet, Route, Routes } from 'react-router-dom'; -import { Home } from "../pages/Home"; -import { Users } from "../pages/Users"; +import { useAuthContext } from '../contexts/AuthContext'; +import { DashboardLayout } from '../layouts/DashboardLayout'; +import { Auth } from '../pages/Auth'; +import { Home } from '../pages/Home'; +import { Users } from '../pages/Users'; export const AppRoutes = () => { - return ( - - } /> - } /> - - - - } - > - } /> - } /> - - - ); + const { isLoggedIn } = useAuthContext(); + + if (isLoggedIn) { + return ( + + + + + } + > + } /> + } /> + + + ); + } + return ( + + } /> + + ); }; diff --git a/dashboard/src/utils/index.ts b/dashboard/src/utils/index.ts index cab1ebf..465d658 100644 --- a/dashboard/src/utils/index.ts +++ b/dashboard/src/utils/index.ts @@ -1,3 +1,6 @@ export const hasAdminSecret = () => { - return (window)["__authorizer__"].isOnboardingCompleted === true -} \ No newline at end of file + return (window)['__authorizer__'].isOnboardingCompleted === true; +}; + +export const capitalizeFirstLetter = (data: string): string => + data.charAt(0).toUpperCase() + data.slice(1); diff --git a/server/routes/routes.go b/server/routes/routes.go index fdab544..0a8f938 100644 --- a/server/routes/routes.go +++ b/server/routes/routes.go @@ -39,6 +39,7 @@ func InitRouter() *gin.Engine { { app.Static("/build", "dashboard/build") app.GET("/", handlers.DashboardHandler()) + app.GET("/:page", handlers.DashboardHandler()) } return router }