Skip to content

Commit

Permalink
Auth0 Integration (#378)
Browse files Browse the repository at this point in the history
* Add Auth0 React SDK

* Activate Auth0 context (with values from environment)

* Make use of refresh tokens

* Stop stripping query parameters off of the URL upon redirect

* Initial elements to allow for login and display avatar

* Use Github icon for login button

* Confirm non-null state before dereferencing

* Add context menu to display name and logout button

* Change logout to roll to configured URL or fallback to window origin

* Use Octokit mock only when we're in a testing environment

* Update yarn lockfile

* Default arguments to empty object

* Add Accept: header for Github API calls

* Add HTTP request header for authorization

* Use access token to make request for comments

* Do not raise a popup for retrieving Auth0 access token

* Send access token on remote call

TODO: Refactor into single function to avoid eliminate duplication

* Fix arguments passed to Octokit

* Allow GitHub base URL to be set via an environment variable

* No-op commit

* Remove commented out code (Octokit instantiation)

* Update esbuild configuration to pass along null if no redirect URI provided

* Update size of GitHub avatar to be in line with new UI

* Update dependency lock file

* Correct capitalization of GitHub

* Create store member for GitHub access token

* Use access token from internal store

* Retrieve access token upon application load (if available)

* Remove debug console logging

* No need to destructure Zustand state value

* Conditionally set authorization when requesting IFC

* Add missing useEffect dependency

* New function to parse GitHub repository URLs

* Do not prepend leading slash when returning path

* New function to get object direct download URL

* Ensure exceptions are bubbled up from Octokit

* Use boolean variable to reflect whether this is a new file

* Add utility functions to get target IFC URL

* Perform IFC URL "pre-resolution" only if we're logged in

* Let downstream handle mapping of target GitHub host

* Move repository URL resolution functions outside of component

* Remove access token comparison from conditional

* Add escape hatch for IFCs hosted in this repository

* Add missing hook dependency for access token

* Add missing argument in child function

* Don't attempt to load viewer why authentication is happening

* Do not allow cached responses for download URL retrieval

Related to octokit/discussions#13

* Replace existing path and reload page upon Auth0 login

* Add existing path for login redirect

* Fix ESLint errors

* Replace ESLint rule suppression with newlines

* Reintroduce URL transform logic

* Migrate to v2 provider syntax

* Change Auth0 scope request to offline access

* Disable refresh tokens (temporarily)

* Remove duplicate Share button introduced by merge

* Use leading slash on file paths in tests

* Fix inconsistent labeling and title of login button

* Create set of mocks for authentication-based rendering

* Add unit tests for rendering the authentication navigation component

* Replace external image with inlined data
  • Loading branch information
oo-bldrs committed Feb 6, 2023
1 parent 94247e9 commit c9e7d80
Show file tree
Hide file tree
Showing 20 changed files with 725 additions and 35 deletions.
4 changes: 4 additions & 0 deletions config/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ export const build = {
target: ['chrome58', 'firefox57', 'safari11', 'edge18'],
logLevel: 'info',
define: {
'process.env.OAUTH2_CLIENT_ID': JSON.stringify(process.env.OAUTH2_CLIENT_ID),
'process.env.OAUTH2_REDIRECT_URI': JSON.stringify(process.env.OAUTH2_REDIRECT_URI || null),
'process.env.AUTH0_DOMAIN': JSON.stringify(process.env.AUTH0_DOMAIN),
'process.env.GITHUB_BASE_URL': JSON.stringify(process.env.GITHUB_BASE_URL || 'https://api.github.com'),
'process.env.SENTRY_DSN': JSON.stringify(process.env.SENTRY_DSN || null),
'process.env.SENTRY_ENVIRONMENT': JSON.stringify(process.env.SENTRY_ENVIRONMENT || process.env.NODE_ENV),
},
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"write-new-version": "node src/utils/version.mjs > package.json.new && shx mv package.json.new package.json"
},
"dependencies": {
"@auth0/auth0-react": "^2.0.0",
"@babel/core": "^7.18.10",
"@bldrs-ai/ifclib": "^5.3.3",
"@emotion/react": "^11.10.0",
Expand All @@ -42,6 +43,7 @@
"@sentry/react": "^7.31.1",
"@sentry/tracing": "^7.31.1",
"clsx": "^1.2.1",
"material-ui-popup-state": "^5.0.4",
"matrix-widget-api": "^1.1.1",
"normalize.css": "^8.0.1",
"prop-types": "^15.8.1",
Expand Down
35 changes: 33 additions & 2 deletions src/BaseRoutes.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import React, {useEffect} from 'react'
import {Outlet, Route, Routes, useLocation, useNavigate} from 'react-router-dom'
import ShareRoutes from './ShareRoutes'
import debug from './utils/debug'
import {useAuth0} from '@auth0/auth0-react'
import useStore from './store/useStore'
import * as Sentry from '@sentry/react'


Expand All @@ -27,14 +29,43 @@ export default function BaseRoutes({testElt = null}) {
const navigation = useNavigate()
const installPrefix = window.location.pathname.startsWith('/Share') ? '/Share' : ''
const basePath = `${installPrefix }/`
const {isLoading, isAuthenticated, getAccessTokenSilently} = useAuth0()
const setAccessToken = useStore((state) => state.setAccessToken)

useEffect(() => {
if (location.pathname === installPrefix ||
location.pathname === basePath) {
debug().log('BaseRoutes#useEffect[], forwarding to: ', `${installPrefix }/share`)
navigation(`${installPrefix }/share`)

let targetURL = `${installPrefix}/share`
if (location.search !== '') {
targetURL += location.search
}

if (location.hash !== '') {
targetURL += location.hash
}

navigation(targetURL)
}

if (!isLoading && isAuthenticated) {
getAccessTokenSilently({
authorizationParams: {
audience: 'https://api.github.com/',
scope: 'openid profile email offline_access repo',
},
}).then((token) => {
setAccessToken(token)
}).catch((err) => {
if (err.error !== 'login_required') {
throw err
}

console.log(err.error)
})
}
}, [basePath, installPrefix, location, navigation])
}, [basePath, installPrefix, location, navigation, getAccessTokenSilently, isAuthenticated, isLoading, setAccessToken])

return (
<SentryRoutes>
Expand Down
29 changes: 29 additions & 0 deletions src/Components/Auth0ProviderWithHistory.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React from 'react'
import {useNavigate} from 'react-router-dom'
import {Auth0Provider} from '@auth0/auth0-react'


export const Auth0ProviderWithHistory = ({children}) => {
const navigate = useNavigate()
const onRedirect = (state) => {
navigate(state && state.returnTo ? state.returnTo : window.location.pathname, {replace: true})
navigate(0)
}

return (
<Auth0Provider
domain={process.env.AUTH0_DOMAIN}
clientId={process.env.OAUTH2_CLIENT_ID}
authorizationParams={{
audience: 'https://api.github.com/',
scope: 'openid profile email offline_access repo',
redirect_uri: process.env.OAUTH2_REDIRECT_URI || window.location.origin,
}}
cacheLocation={'localstorage'}
onRedirectCallback={onRedirect}
useRefreshTokens={false}
>
{children}
</Auth0Provider>
)
}
20 changes: 20 additions & 0 deletions src/Components/AuthNav.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React from 'react'
import {useAuth0} from '@auth0/auth0-react'
import {CircularProgress} from '@material-ui/core'
import LoginButton from './LoginButton'
import UserProfile from './UserProfile'


const AuthNav = () => {
const {isLoading, isAuthenticated} = useAuth0()

if (isLoading) {
return <CircularProgress/>
}

return isAuthenticated ?
<UserProfile/> :
<LoginButton/>
}

export default AuthNav
23 changes: 23 additions & 0 deletions src/Components/AuthNav.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React from 'react'
import {render, screen} from '@testing-library/react'
import {mockedUseAuth0, mockedUserLoggedIn, mockedUserLoggedOut} from '../__mocks__/authentication'
import AuthNav from './AuthNav'


describe('AuthNav', () => {
it('renders the login button when not logged in', () => {
mockedUseAuth0.mockReturnValue(mockedUserLoggedOut)

render(<AuthNav/>)
const loginButton = screen.getByTitle(/Log in with GitHub/i)
expect(loginButton).toBeInTheDocument()
})

it('renders the user avatar when logged in', () => {
mockedUseAuth0.mockReturnValue(mockedUserLoggedIn)

render(<AuthNav/>)
const avatarImage = screen.getByAltText(/Unit Testing/i)
expect(avatarImage).toBeInTheDocument()
})
})
31 changes: 31 additions & 0 deletions src/Components/LoginButton.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React from 'react'
import {useAuth0} from '@auth0/auth0-react'
import {TooltipIconButton} from './Buttons'
import GitHubIcon from '@mui/icons-material/GitHub'


const LoginButton = ({
title = 'Log in with GitHub',
placement = 'right',
...props
}) => {
const {loginWithRedirect} = useAuth0()

const onClick = async () => {
await loginWithRedirect({
appState: {
returnTo: window.location.pathname,
},
})
}

return (
<TooltipIconButton
title={'Log in with GitHub'}
icon={<GitHubIcon/>}
onClick={onClick}
/>
)
}

export default LoginButton
9 changes: 5 additions & 4 deletions src/Components/Notes/Notes.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export default function Notes() {
const filteredNote = (notes && selectedNoteId) ?
notes.filter((issue) => issue.id === selectedNoteId)[0] : null
const repository = useStore((state) => state.repository)
const accessToken = useStore((state) => state.accessToken)


useEffect(() => {
Expand All @@ -33,7 +34,7 @@ export default function Notes() {
const fetchNotes = async () => {
try {
const fetchedNotes = []
const issuesData = await getIssues(repository)
const issuesData = await getIssues(repository, accessToken)
let issueIndex = 0
issuesData.data.slice(0).reverse().map((issue, index) => {
if (issue.body === null) {
Expand Down Expand Up @@ -63,7 +64,7 @@ export default function Notes() {
}

fetchNotes()
}, [setNotes, repository])
}, [setNotes, repository, accessToken])


useEffect(() => {
Expand All @@ -76,7 +77,7 @@ export default function Notes() {
try {
const commentsArr = []

const commentsData = await getComments(repository, selectedNote.number)
const commentsData = await getComments(repository, selectedNote.number, accessToken)
if (commentsData) {
commentsData.map((comment) => {
commentsArr.push({
Expand All @@ -103,7 +104,7 @@ export default function Notes() {
// this useEffect runs every time notes are fetched to enable fetching the comments when the platform is open
// using the link
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [filteredNote, repository, setComments])
}, [filteredNote, repository, setComments, accessToken])

return (
<Paper
Expand Down
4 changes: 4 additions & 0 deletions src/Components/OperationsGroup.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import CameraControl from './CameraControl'
import CutPlaneMenu from './CutPlaneMenu'
import ShareControl from './ShareControl'
import {TooltipIconButton} from './Buttons'
import AuthNav from './AuthNav'
import ClearIcon from '../assets/icons/Clear.svg'
import ListIcon from '../assets/icons/List.svg'
import MoonIcon from '../assets/icons/Moon.svg'
Expand Down Expand Up @@ -71,6 +72,9 @@ export default function OperationsGroup({deselectItems}) {
},
}}
>
<AuthNav/>
<Divider/>

{isCollaborationGroupVisible &&
<ButtonGroup orientation="vertical" >
<ShareControl/>
Expand Down
85 changes: 85 additions & 0 deletions src/Components/UserProfile.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import React from 'react'
import {useAuth0} from '@auth0/auth0-react'
import Avatar from '@mui/material/Avatar'
import MenuItem from '@mui/material/MenuItem'
import ListItemIcon from '@mui/material/ListItemIcon'
import Divider from '@mui/material/Divider'
import Logout from '@mui/icons-material/Logout'
import Typography from '@mui/material/Typography'
import {
usePopupState,
bindTrigger,
bindMenu,
} from 'material-ui-popup-state/hooks'
import IconButton from '@mui/material/IconButton'
import Menu from '@mui/material/Menu'
import GitHubIcon from '@mui/icons-material/GitHub'


const UserProfile = ({size = 'medium'}) => {
const {user, isAuthenticated, logout} = useAuth0()
const popupState = usePopupState({
variant: 'popup',
popupId: 'user-profile',
})

return isAuthenticated && (
<>
<IconButton className={'no-hover'} {...bindTrigger(popupState)}>
<Avatar
alt={user.name}
src={user.picture}
sx={{width: 22, height: 22}}
/>
</IconButton>

<Menu
PaperProps={{
sx: {
'filter': 'drop-shadow(0px 2px 8px rgba(0,0,0,0.32))',
'mt': 1.5,
'& .MuiAvatar-root': {
width: 32,
height: 32,
ml: -0.5,
mr: 1,
},
'&:before': {
content: '""',
display: 'block',
position: 'absolute',
top: 0,
right: 14,
width: 10,
height: 10,
bgcolor: 'background.paper',
transform: 'translateY(-50%) rotate(45deg)',
zIndex: 0,
},
},
}}
{...bindMenu(popupState)}
>
<MenuItem>
<ListItemIcon sx={{display: 'flex', alignItems: 'center'}}>
<GitHubIcon/>
<Typography sx={{paddingLeft: '11px'}}>
Hi, {user.name}!
</Typography>
</ListItemIcon>
</MenuItem>
<Divider/>
<MenuItem onClick={() => logout({returnTo: process.env.OAUTH2_REDIRECT_URI || window.location.origin})}>
<ListItemIcon>
<Logout/>
<Typography sx={{paddingLeft: '11px'}}>
Logout
</Typography>
</ListItemIcon>
</MenuItem>
</Menu>
</>
)
}

export default UserProfile

0 comments on commit c9e7d80

Please sign in to comment.