From 68d9003f8d8d2adcba43cad6366eca5365110e48 Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Tue, 5 Apr 2022 21:15:57 +0200 Subject: feat(web/ui): Add support for unauthenticated user access This change updates the web UI and API to support unauthenticated user access. Such functionality is helpful when there is just a single user that wants to try OpenDC. --- opendc-web/opendc-web-ui/public/img/avatar.svg | 18 ++++++++++ opendc-web/opendc-web-ui/src/api/index.js | 17 +++++---- opendc-web/opendc-web-ui/src/auth.js | 42 +++++++++++++++------- .../opendc-web-ui/src/components/AppHeaderTools.js | 16 ++++++--- .../topologies/sidebar/rack/MachineComponent.js | 7 +--- opendc-web/opendc-web-ui/src/config.js | 41 +++++++++++++++++++++ opendc-web/opendc-web-ui/src/data/project.js | 2 +- opendc-web/opendc-web-ui/src/pages/_app.js | 18 +++++----- .../opendc-web-ui/src/pages/projects/index.js | 17 +++------ opendc-web/opendc-web-ui/src/redux/index.js | 3 +- 10 files changed, 126 insertions(+), 55 deletions(-) create mode 100644 opendc-web/opendc-web-ui/public/img/avatar.svg create mode 100644 opendc-web/opendc-web-ui/src/config.js (limited to 'opendc-web/opendc-web-ui') diff --git a/opendc-web/opendc-web-ui/public/img/avatar.svg b/opendc-web/opendc-web-ui/public/img/avatar.svg new file mode 100644 index 00000000..73726f9b --- /dev/null +++ b/opendc-web/opendc-web-ui/public/img/avatar.svg @@ -0,0 +1,18 @@ + + + + + + + + + + diff --git a/opendc-web/opendc-web-ui/src/api/index.js b/opendc-web/opendc-web-ui/src/api/index.js index 1a9877d0..75751658 100644 --- a/opendc-web/opendc-web-ui/src/api/index.js +++ b/opendc-web/opendc-web-ui/src/api/index.js @@ -20,7 +20,7 @@ * SOFTWARE. */ -const apiUrl = process.env.NEXT_PUBLIC_API_BASE_URL +import { apiUrl } from '../config' /** * Send the specified request to the OpenDC API. @@ -31,14 +31,19 @@ const apiUrl = process.env.NEXT_PUBLIC_API_BASE_URL * @param body The body of the request. */ export async function request(auth, path, method = 'GET', body) { + const headers = { + 'Content-Type': 'application/json', + } + const { getAccessTokenSilently } = auth - const token = await getAccessTokenSilently() + if (getAccessTokenSilently) { + const token = await getAccessTokenSilently() + headers['Authorization'] = `Bearer ${token}` + } + const response = await fetch(`${apiUrl}/${path}`, { method: method, - headers: { - Authorization: `Bearer ${token}`, - 'Content-Type': 'application/json', - }, + headers: headers, body: body && JSON.stringify(body), }) const json = await response.json() diff --git a/opendc-web/opendc-web-ui/src/auth.js b/opendc-web/opendc-web-ui/src/auth.js index e670476c..3d6cf87c 100644 --- a/opendc-web/opendc-web-ui/src/auth.js +++ b/opendc-web/opendc-web-ui/src/auth.js @@ -23,14 +23,26 @@ import PropTypes from 'prop-types' import { Auth0Provider, useAuth0 } from '@auth0/auth0-react' import { useEffect } from 'react' +import { auth } from './config' /** - * Obtain the authentication context. + * Helper function to provide the authentication context in case Auth0 is not + * configured. */ -export function useAuth() { - return useAuth0() +function useAuthDev() { + return { + isAuthenticated: false, + isLoading: false, + logout: () => {}, + loginWithRedirect: () => {}, + } } +/** + * Obtain the authentication context. + */ +export const useAuth = auth.domain ? useAuth0 : useAuthDev + /** * Force the user to be authenticated or redirect to the homepage. */ @@ -51,16 +63,20 @@ export function useRequireAuth() { * AuthProvider which provides an authentication context. */ export function AuthProvider({ children }) { - return ( - - {children} - - ) + if (auth.domain) { + return ( + + {children} + + ) + } + + return children } AuthProvider.propTypes = { diff --git a/opendc-web/opendc-web-ui/src/components/AppHeaderTools.js b/opendc-web/opendc-web-ui/src/components/AppHeaderTools.js index 02e5d265..3e58b209 100644 --- a/opendc-web/opendc-web-ui/src/components/AppHeaderTools.js +++ b/opendc-web/opendc-web-ui/src/components/AppHeaderTools.js @@ -39,7 +39,9 @@ import { useAuth } from '../auth' import { GithubIcon, HelpIcon } from '@patternfly/react-icons' function AppHeaderTools() { - const auth = useAuth() + const { logout, user, isAuthenticated, isLoading } = useAuth() + const username = isAuthenticated || isLoading ? user?.name : 'Anonymous' + const avatar = isAuthenticated || isLoading ? user?.picture : '/img/avatar.svg' const [isKebabDropdownOpen, setKebabDropdownOpen] = useState(false) const kebabDropdownItems = [ @@ -56,7 +58,11 @@ function AppHeaderTools() { const [isDropdownOpen, setDropdownOpen] = useState(false) const userDropdownItems = [ - auth.logout({ returnTo: window.location.origin })}> + logout({ returnTo: window.location.origin })} + > Logout , @@ -105,7 +111,7 @@ function AppHeaderTools() { isOpen={isDropdownOpen} toggle={ setDropdownOpen(!isDropdownOpen)}> - {auth?.user?.name ?? ( + {username ?? ( - {auth?.user?.picture ? ( - + {avatar ? ( + ) : ( )} diff --git a/opendc-web/opendc-web-ui/src/components/topologies/sidebar/rack/MachineComponent.js b/opendc-web/opendc-web-ui/src/components/topologies/sidebar/rack/MachineComponent.js index 8897f2d0..18c3db3c 100644 --- a/opendc-web/opendc-web-ui/src/components/topologies/sidebar/rack/MachineComponent.js +++ b/opendc-web/opendc-web-ui/src/components/topologies/sidebar/rack/MachineComponent.js @@ -5,12 +5,7 @@ import { Machine } from '../../../../shapes' const UnitIcon = ({ id, type }) => ( // eslint-disable-next-line @next/next/no-img-element - {'Machine + {'Machine ) UnitIcon.propTypes = { diff --git a/opendc-web/opendc-web-ui/src/config.js b/opendc-web/opendc-web-ui/src/config.js new file mode 100644 index 00000000..52952d69 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/config.js @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2022 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +/** + * URL to OpenDC API. + */ +export const apiUrl = process.env.NEXT_PUBLIC_API_BASE_URL + +/** + * Authentication configuration. + */ +export const auth = { + domain: process.env.NEXT_PUBLIC_AUTH0_DOMAIN, + clientId: process.env.NEXT_PUBLIC_AUTH0_CLIENT_ID, + audience: process.env.NEXT_PUBLIC_AUTH0_AUDIENCE, + redirectUri: global.window && global.window.location.origin, +} + +/** + * Sentry DSN for web frontend. + */ +export const sentryDsn = process.env.NEXT_PUBLIC_SENTRY_DSN diff --git a/opendc-web/opendc-web-ui/src/data/project.js b/opendc-web/opendc-web-ui/src/data/project.js index b1db3da5..60a8fab6 100644 --- a/opendc-web/opendc-web-ui/src/data/project.js +++ b/opendc-web/opendc-web-ui/src/data/project.js @@ -23,7 +23,7 @@ import { useQuery, useMutation } from 'react-query' import { addProject, deleteProject, fetchProject, fetchProjects } from '../api/projects' import { addPortfolio, deletePortfolio, fetchPortfolio, fetchPortfolios } from '../api/portfolios' -import { addScenario, deleteScenario, fetchScenario, fetchScenariosOfPortfolio } from '../api/scenarios' +import { addScenario, deleteScenario, fetchScenario } from '../api/scenarios' /** * Configure the query defaults for the project endpoints. diff --git a/opendc-web/opendc-web-ui/src/pages/_app.js b/opendc-web/opendc-web-ui/src/pages/_app.js index 4861f5c1..bac9a5af 100644 --- a/opendc-web/opendc-web-ui/src/pages/_app.js +++ b/opendc-web/opendc-web-ui/src/pages/_app.js @@ -30,6 +30,7 @@ import { AuthProvider, useRequireAuth } from '../auth' import * as Sentry from '@sentry/react' import { Integrations } from '@sentry/tracing' import { QueryClientProvider } from 'react-query' +import { sentryDsn } from '../config' import '@patternfly/react-core/dist/styles/base.css' import '@patternfly/react-styles/css/utilities/Alignment/alignment.css' @@ -67,17 +68,14 @@ Inner.propTypes = { }).isRequired, } -const dsn = process.env.NEXT_PUBLIC_SENTRY_DSN // Initialize Sentry if the user has configured a DSN -if (process.browser && dsn) { - if (dsn) { - Sentry.init({ - environment: process.env.NODE_ENV, - dsn: dsn, - integrations: [new Integrations.BrowserTracing()], - tracesSampleRate: 0.1, - }) - } +if (process.browser && sentryDsn) { + Sentry.init({ + environment: process.env.NODE_ENV, + dsn: sentryDsn, + integrations: [new Integrations.BrowserTracing()], + tracesSampleRate: 0.1, + }) } export default function App(props) { diff --git a/opendc-web/opendc-web-ui/src/pages/projects/index.js b/opendc-web/opendc-web-ui/src/pages/projects/index.js index bb1fbd69..40792275 100644 --- a/opendc-web/opendc-web-ui/src/pages/projects/index.js +++ b/opendc-web/opendc-web-ui/src/pages/projects/index.js @@ -23,38 +23,29 @@ import React, { useMemo, useState } from 'react' import Head from 'next/head' import ProjectFilterPanel from '../../components/projects/FilterPanel' -import { useAuth } from '../../auth' import { AppPage } from '../../components/AppPage' import { PageSection, PageSectionVariants, Text, TextContent } from '@patternfly/react-core' import { useProjects, useDeleteProject } from '../../data/project' import ProjectTable from '../../components/projects/ProjectTable' import NewProject from '../../components/projects/NewProject' -const getVisibleProjects = (projects, filter, userId) => { +const getVisibleProjects = (projects, filter) => { switch (filter) { case 'SHOW_ALL': return projects case 'SHOW_OWN': - return projects.filter((project) => - project.authorizations.some((a) => a.userId === userId && a.level === 'OWN') - ) + return projects.filter((project) => project.role === 'OWNER') case 'SHOW_SHARED': - return projects.filter((project) => - project.authorizations.some((a) => a.userId === userId && a.level !== 'OWN') - ) + return projects.filter((project) => project.role !== 'OWNER') default: return projects } } function Projects() { - const { user } = useAuth() const { status, data: projects } = useProjects() const [filter, setFilter] = useState('SHOW_ALL') - const visibleProjects = useMemo( - () => getVisibleProjects(projects ?? [], filter, user?.sub), - [projects, filter, user?.sub] - ) + const visibleProjects = useMemo(() => getVisibleProjects(projects ?? [], filter), [projects, filter]) const { mutate: deleteProject } = useDeleteProject() diff --git a/opendc-web/opendc-web-ui/src/redux/index.js b/opendc-web/opendc-web-ui/src/redux/index.js index fa0c9d23..53cd9144 100644 --- a/opendc-web/opendc-web-ui/src/redux/index.js +++ b/opendc-web/opendc-web-ui/src/redux/index.js @@ -6,6 +6,7 @@ import thunk from 'redux-thunk' import rootReducer from './reducers' import rootSaga from './sagas' import { createReduxEnhancer } from '@sentry/react' +import { sentryDsn } from '../config' let store @@ -20,7 +21,7 @@ function initStore(initialState, ctx) { let middleware = applyMiddleware(...middlewares) - if (process.env.NEXT_PUBLIC_SENTRY_DSN) { + if (sentryDsn) { middleware = compose(middleware, createReduxEnhancer()) } -- cgit v1.2.3