diff --git a/package-lock.json b/package-lock.json index 91053c7c..7563c3d4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42077,7 +42077,7 @@ "@graphql-codegen/cli": "^5.0.0", "@graphql-codegen/client-preset": "^4.1.0", "@graphql-codegen/near-operation-file-preset": "^2.5.0", - "@graphql-codegen/typescript-react-apollo": "*", + "@graphql-codegen/typescript-react-apollo": "^3.3.7", "@jsonforms/core": "^3.1.0", "@jsonforms/material-renderers": "^3.1.0", "@jsonforms/react": "^3.1.0", diff --git a/packages/client/.gitignore b/packages/client/.gitignore index a547bf36..7ceb59f8 100644 --- a/packages/client/.gitignore +++ b/packages/client/.gitignore @@ -22,3 +22,4 @@ dist-ssr *.njsproj *.sln *.sw? +.env diff --git a/packages/client/src/App.tsx b/packages/client/src/App.tsx index 69e9a9f7..95bbcd4f 100644 --- a/packages/client/src/App.tsx +++ b/packages/client/src/App.tsx @@ -21,34 +21,66 @@ import { EnvironmentContextProvider } from './context/EnvironmentContext'; import { AuthProvider } from './context/AuthContext'; import { AdminGuard } from './guards/AdminGuard'; import { LogoutPage } from './pages/LogoutPage'; +import { CssBaseline, Box, styled } from '@mui/material'; +import { useState } from 'react'; +import { SideBar } from './components/SideBar'; + +const drawerWidth = 240; + +const Main = styled('main', { shouldForwardProp: (prop) => prop !== 'open' })<{ open?: boolean; }>(({ theme, open }) => ({ + flexGrow: 1, + padding: theme.spacing(3), + transition: theme.transitions.create('margin', { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.leavingScreen, + }), + marginRight: -drawerWidth, + ...(open && { + transition: theme.transitions.create('margin', { + easing: theme.transitions.easing.easeOut, + duration: theme.transitions.duration.enteringScreen, + }), + marginRight: 0, + }), +})); function App() { + const [drawerOpen, setDrawerOpen] = useState(true); + return ( - - - } /> - } /> - } /> - }> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - + + + +
+ + + + } /> + } /> + } /> + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + +
+
diff --git a/packages/client/src/components/Navigation.tsx b/packages/client/src/components/Navigation.tsx deleted file mode 100644 index cd80444d..00000000 --- a/packages/client/src/components/Navigation.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { Box } from '@mui/material'; -import { DropdownComponent } from './Dropdown'; -import { useEffect, useState } from 'react'; - -export const Navigation: React.FC = () => { - const [study] = useState('study name'); - - const [names, setNames] = useState([ - { - name: 'Projects', - sublinks: [ - { title: 'New Project', link: 'newproject' }, - { title: 'Project Control', link: 'projectcontrol' }, - { title: 'User Permissions', link: 'userpermissions' } - ] - }, - { - name: 'Studies', - sublinks: [ - { title: 'Create New Study', link: 'newstudy' }, - { title: 'Study Control', link: 'studycontrol' }, - { title: 'User Permissions', link: 'studyuserpermissions' }, - { title: 'Entry Controls', link: 'entrycontrols' }, - { title: 'Download Tags', link: 'downloadtags' } - ] - }, - { - name: 'Datasets', - sublinks: [ - { title: 'Dataset Controls', link: 'datasetcontrols' }, - { title: 'Project Access', link: 'projectaccess' } - ] - } - ]); - - useEffect(() => { - if (study) { - setNames((names) => [...names, { name: 'Contribute', sublinks: [{ title: 'Contribute to a Study', link: 'contribute' }] }]); - } - }, []); - - return ( - - - - ); -}; diff --git a/packages/client/src/components/NavigationBar.tsx b/packages/client/src/components/NavigationBar.tsx index 8fde4ef0..b0f71835 100644 --- a/packages/client/src/components/NavigationBar.tsx +++ b/packages/client/src/components/NavigationBar.tsx @@ -1,43 +1,31 @@ -import { AppBar, Toolbar, CssBaseline, Typography, Link } from '@mui/material'; -import { SideBar } from './SideBar'; -import { Divider } from '@mui/material'; -import { useAuth } from '../context/AuthContext'; -import { useNavigate } from 'react-router-dom'; +import { AppBar, Toolbar, Typography } from '@mui/material'; +import { FC, Dispatch, SetStateAction } from 'react'; +import { IconButton } from '@mui/material'; +import { Menu } from '@mui/icons-material'; -function NavBar() { - const { token, initialized } = useAuth(); - const navigate = useNavigate(); +export interface NavBarProps { + drawerOpen: boolean; + setDrawerOpen: Dispatch>; +} + +export const NavBar: FC = ({ drawerOpen, setDrawerOpen }) => { return ( - - - + + setDrawerOpen(!drawerOpen)}> + + ASL-LEX SignLab - - {!token || !initialized ? ( -
- - Log In - - - Sign Up - -
- ) : ( - navigate('/logoutpage')}> - Log Out - - )} ); } -export { NavBar }; diff --git a/packages/client/src/components/SideBar.tsx b/packages/client/src/components/SideBar.tsx index 5256032d..66d08766 100644 --- a/packages/client/src/components/SideBar.tsx +++ b/packages/client/src/components/SideBar.tsx @@ -1,45 +1,117 @@ -import { useState } from 'react'; -import { Divider, Drawer, IconButton, List, Typography, Link } from '@mui/material'; -import MenuIcon from '@mui/icons-material/Menu'; -import { Environment } from './Environment'; -import { Navigation } from './Navigation'; +import { FC, ReactNode, useState } from 'react'; +import { Collapse, Divider, Drawer, List, ListItem, ListItemButton, ListItemIcon, ListItemText } from '@mui/material'; +import { ExpandMore, ExpandLess, School, Dataset, Work, Logout } from '@mui/icons-material'; import { useAuth } from '../context/AuthContext'; +import {useNavigate} from 'react-router-dom'; -function SideBar() { - const [openDrawer, setOpenDrawer] = useState(false); - const { token, initialized } = useAuth(); +interface SideBarProps { + open: boolean; + drawerWidth: number; +} + +export const SideBar: FC = ({ open, drawerWidth }) => { + const { logout } = useAuth(); + const navigate = useNavigate(); + + const navItems: NavItemProps[] = [ + { + name: 'Projects', + icon: , + action: () => {}, + subItems: [ + { name: 'New Project', action: () => navigate('/project/new') }, + { name: 'Project Control', action: () => navigate('/project/controls') }, + { name: 'User Permissions', action: () => navigate('/project/permissions') }, + ] + }, + { + name: 'Studies', + action: () => {}, + icon: , + subItems: [ + { name: 'New Study', action: () => navigate('/study/new') }, + { name: 'Study Control', action: () => navigate('/study/contols') }, + { name: 'User Permissions', action: () => navigate('/study/permissions') }, + { name: 'Entry Controls', action: () => navigate('/study/controls') }, + { name: 'Download Tags', action: () => navigate('/study/tags') } + ] + }, + { + name: 'Datasets', + action: () => {}, + icon: , + subItems: [ + { name: 'Dataset Control', action: () => navigate('/dataset/controls') }, + { name: 'Project Access', action: () => navigate('/dataset/projectaccess') } + ] + }, + { + name: 'Logout', + action: logout, + icon: + } + ]; return ( -
- setOpenDrawer(false)}> - - setOpenDrawer(false)} - > - Home - - - - {token && initialized && ( -
- Environment - - - Navigation - -
- )} -
- setOpenDrawer(!openDrawer)}> - - -
+ + + {navItems.map((navItem) => )} + + ); +}; + +interface NavItemProps { + action: () => void; + name: string; + icon?: ReactNode; + subItems?: NavItemProps[] } -export { SideBar }; + +const NavItem: FC = ({ action, name, icon, subItems }) => { + const isExpandable = subItems && subItems.length > 0; + const [open, setOpen] = useState(false); + + const handleClick = () => { + setOpen(!open); + action(); + }; + + const menuItemChildren = isExpandable ? ( + + + + {subItems.map((item, index) => )} + + + ) : null; + + return ( + <> + + + {icon && {icon}} + + {isExpandable && !open && } + {isExpandable && open && } + + + {menuItemChildren} + + ); +}; diff --git a/packages/client/src/context/AuthContext.tsx b/packages/client/src/context/AuthContext.tsx index 0502386e..508dae5a 100644 --- a/packages/client/src/context/AuthContext.tsx +++ b/packages/client/src/context/AuthContext.tsx @@ -1,7 +1,9 @@ -import React, { createContext, FC, useContext, useEffect, useState } from 'react'; +import { createContext, FC, useContext, useEffect, useState, ReactNode } from 'react'; import jwt_decode from 'jwt-decode'; import { useNavigate } from 'react-router-dom'; +const AUTH_TOKEN_STR = 'token'; + export interface DecodedToken { id: string; projectId: string; @@ -10,77 +12,95 @@ export interface DecodedToken { } export interface AuthContextProps { - initialized: boolean; - setInitialized: (initialized: boolean) => void; - token?: string; - decoded_token?: DecodedToken; + authenticated: boolean; + setAuthenticated: (authenticated: boolean) => void; + token: string | null; + decodedToken: DecodedToken | null; setToken: (token: string) => void; - setRefreshToken: (token: string) => void; + login: (token: string) => void; + logout: () => void; } const AuthContext = createContext({} as AuthContextProps); export interface AuthProviderProps { - children: React.ReactNode; + children: ReactNode; } export const AuthProvider: FC = (props) => { - const [initialized, setInitialized] = useState(false); - const [token, setToken] = useState(); - const [refreshToken, setRefreshToken] = useState(); - const [decoded_token, setDecodedToken] = useState(); + const [authenticated, setAuthenticated] = useState(false); + const [token, setToken] = useState(null); + const [decodedToken, setDecodedToken] = useState(null); const navigate = useNavigate(); + const setUnautheticated = () => { + clearToken(); + setAuthenticated(false); + setToken(null); + setDecodedToken(null); + }; + + const makeAuthenticated = (token: string, tokenPayload: DecodedToken) => { + setAuthenticated(true); + setToken(token); + setDecodedToken(tokenPayload); + saveToken(token); + }; + + const logout = () => { + setUnautheticated(); + navigate('/loginpage'); + }; + + const login = (token: string) => { + makeAuthenticated(token, jwt_decode(token)); + navigate('/'); + }; + useEffect(() => { const token = restoreToken(); - const refreshToken = restoreRefreshToken(); - if (token) { - const decoded_token: DecodedToken = jwt_decode(token); - const current_time = new Date().getTime() / 1000; - if (current_time > decoded_token.exp) { - navigate('/logout'); - } - setToken(token); - setDecodedToken(decoded_token); - } else { - localStorage.removeItem('token'); + + // If not token present, redirect to login + if (!token) { + setUnautheticated(); + navigate('/login'); + return; } - if (refreshToken) { - setRefreshToken(refreshToken); + + // Decode the current token payload + const decodedToken: DecodedToken = jwt_decode(token); + const currentTime = new Date().getTime() / 1000; + + // Handle expired token + if (currentTime > decodedToken.exp) { + setUnautheticated(); + navigate('/login'); + return; } - setInitialized(true); - }, [token]); + + // User is authenticated with presoent token + makeAuthenticated(token, decodedToken); + }, []); useEffect(() => { if (token) { - saveToken(token); - setDecodedToken(jwt_decode(token)); + makeAuthenticated(token, jwt_decode(token)); } }, [token]); - useEffect(() => { - if (refreshToken) { - saveRefreshToken(refreshToken); - } - }, [refreshToken]); - - return ; + return ; }; const saveToken = (token: string) => { - localStorage.setItem('token', token); + localStorage.setItem(AUTH_TOKEN_STR, token); }; const restoreToken = (): string | null => { - return localStorage.getItem('token'); + return localStorage.getItem(AUTH_TOKEN_STR); }; -const saveRefreshToken = (token: string) => { - localStorage.setItem('refreshToken', token); -}; - -const restoreRefreshToken = (): string | null => { - return localStorage.getItem('refreshToken'); -}; +const clearToken = (): void => { + localStorage.removeItem(AUTH_TOKEN_STR); +} export const useAuth = () => useContext(AuthContext); diff --git a/packages/client/src/pages/AuthCallback.tsx b/packages/client/src/pages/AuthCallback.tsx index f0978b0a..00b5a571 100644 --- a/packages/client/src/pages/AuthCallback.tsx +++ b/packages/client/src/pages/AuthCallback.tsx @@ -1,26 +1,17 @@ import { CircularProgress, Stack } from '@mui/material'; import { useEffect } from 'react'; import { useAuth } from '../context/AuthContext'; -import { useNavigate } from 'react-router-dom'; export const AuthCallback: React.FC = () => { - const { token, setToken } = useAuth(); - const navigate = useNavigate(); + const { login } = useAuth(); useEffect(() => { const token = new URLSearchParams(window.location.search).get('token'); if (token) { - console.log('we got token: ', token); - setToken(token); + login(token); } }, []); - useEffect(() => { - if (token) { - navigate('/'); - } - }, [token]); - return ( { - const { token, initialized } = useAuth(); + const { token, authenticated } = useAuth(); return (
Welcome to SignLab - {initialized && token ?

You are signed in

:

Please login to continue

} + {authenticated && token ?

You are signed in

:

Please login to continue

}
); }; diff --git a/packages/client/src/pages/LoginPage.tsx b/packages/client/src/pages/LoginPage.tsx index 55f38b9d..9333197a 100644 --- a/packages/client/src/pages/LoginPage.tsx +++ b/packages/client/src/pages/LoginPage.tsx @@ -1,7 +1,29 @@ import LockOutlinedIcon from '@mui/icons-material/LockOutlined'; import { Avatar, Box, Container, Link, Typography } from '@mui/material'; +import { FC, useEffect } from 'react'; +import { useAuth } from '../context/AuthContext'; +import {useNavigate} from 'react-router-dom'; + +export const LoginPage: FC = () => { + // Construct the Auth URL + const authUrlBase = import.meta.env.VITE_AUTH_LOGIN_URL; + const projectId = import.meta.env.VITE_AUTH_PROJECT_ID; + const redirectUrl = encodeURIComponent(window.location.origin + '/callback'); + const authUrl = `${authUrlBase}/?projectId=${projectId}&redirectUrl=${redirectUrl}`; + console.log(authUrl); + + const { authenticated } = useAuth(); + const navigate = useNavigate(); + + useEffect(() => { + console.log(authenticated); + if (authenticated) { + navigate('/'); + } else { + window.location.href = authUrl; + } + }, []); -export const LoginPage = () => { return ( { by following this link diff --git a/packages/client/tsconfig.json b/packages/client/tsconfig.json index a7fc6fbf..6c93135e 100644 --- a/packages/client/tsconfig.json +++ b/packages/client/tsconfig.json @@ -7,7 +7,7 @@ "skipLibCheck": true, /* Bundler mode */ - "moduleResolution": "bundler", + "moduleResolution": "Node", "allowImportingTsExtensions": true, "resolveJsonModule": true, "isolatedModules": true,