diff options
| author | Fabian Mastenbroek <mail.fabianm@gmail.com> | 2021-05-16 23:18:02 +0200 |
|---|---|---|
| committer | Fabian Mastenbroek <mail.fabianm@gmail.com> | 2021-05-18 15:46:42 +0200 |
| commit | a6865b86cc8d710374fc0b6cfcbd2b863f1942a9 (patch) | |
| tree | 121fdb26827c5509a12a4427e8f9d881dbdefe82 /opendc-web | |
| parent | 6412610f38117e1ea0635a56fa023183723fa67a (diff) | |
ui: Migrate to Auth0 as Identity Provider
This change updates the frontend codebase to move away from the Google
login and instead use Auth0 as generic Identity Provider. This allows
users to login with other accounts as well.
Since Auth0 has a free tier, users can experiment themselves with OpenDC
locally without having to pay for the login functionality. The code has
been written so that we should be able to migrate away from Auth0 once
it is not a suitable Identity Provider for OpenDC anymore.
Diffstat (limited to 'opendc-web')
44 files changed, 423 insertions, 607 deletions
diff --git a/opendc-web/opendc-web-ui/package.json b/opendc-web/opendc-web-ui/package.json index f6917398..9c41c2e2 100644 --- a/opendc-web/opendc-web-ui/package.json +++ b/opendc-web/opendc-web-ui/package.json @@ -17,6 +17,7 @@ "license": "MIT", "private": true, "dependencies": { + "@auth0/auth0-react": "^1.5.0", "@sentry/react": "^5.30.0", "@sentry/tracing": "^5.30.0", "approximate-number": "~2.0.0", @@ -32,14 +33,12 @@ "react": "~17.0.2", "react-dom": "~17.0.2", "react-fontawesome": "~1.7.1", - "react-google-login": "~5.1.14", "react-hotkeys": "^2.0.0", "react-konva": "~17.0.2-0", "react-redux": "~7.2.0", "reactstrap": "^8.9.0", "recharts": "~2.0.9", "redux": "~4.0.5", - "redux-localstorage": "^0.4.1", "redux-logger": "~3.0.6", "redux-saga": "~1.1.3", "redux-thunk": "~2.3.0", diff --git a/opendc-web/opendc-web-ui/src/api/index.js b/opendc-web/opendc-web-ui/src/api/index.js index 65358745..680d49ce 100644 --- a/opendc-web/opendc-web-ui/src/api/index.js +++ b/opendc-web/opendc-web-ui/src/api/index.js @@ -20,30 +20,32 @@ * SOFTWARE. */ -import { getAuthToken } from '../auth' - const apiUrl = process.env.NEXT_PUBLIC_API_BASE_URL /** * Send the specified request to the OpenDC API. + * + * @param auth The authentication context. * @param path Relative path for the API. * @param method The method to use for the request. * @param body The body of the request. */ -export async function request(path, method = 'GET', body) { - const res = await fetch(`${apiUrl}/v2/${path}`, { +export async function request(auth, path, method = 'GET', body) { + const { getAccessTokenSilently } = auth + const token = await getAccessTokenSilently() + const response = await fetch(`${apiUrl}/${path}`, { method: method, headers: { - 'auth-token': getAuthToken(), + Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', }, body: body && JSON.stringify(body), }) - const { status, content } = await res.json() + const json = await response.json() - if (status.code !== 200) { - throw status + if (!response.ok) { + throw response.message } - return content + return json.data } diff --git a/opendc-web/opendc-web-ui/src/api/portfolios.js b/opendc-web/opendc-web-ui/src/api/portfolios.js index 6202e702..28898e6a 100644 --- a/opendc-web/opendc-web-ui/src/api/portfolios.js +++ b/opendc-web/opendc-web-ui/src/api/portfolios.js @@ -22,18 +22,18 @@ import { request } from './index' -export function addPortfolio(projectId, portfolio) { - return request(`projects/${projectId}/portfolios`, 'POST', { portfolio }) +export function addPortfolio(auth, projectId, portfolio) { + return request(auth, `projects/${projectId}/portfolios`, 'POST', { portfolio }) } -export function getPortfolio(portfolioId) { - return request(`portfolios/${portfolioId}`) +export function getPortfolio(auth, portfolioId) { + return request(auth, `portfolios/${portfolioId}`) } -export function updatePortfolio(portfolioId, portfolio) { - return request(`portfolios/${portfolioId}`, 'PUT', { portfolio }) +export function updatePortfolio(auth, portfolioId, portfolio) { + return request(auth, `portfolios/${portfolioId}`, 'PUT', { portfolio }) } -export function deletePortfolio(portfolioId) { - return request(`portfolios/${portfolioId}`, 'DELETE') +export function deletePortfolio(auth, portfolioId) { + return request(auth, `portfolios/${portfolioId}`, 'DELETE') } diff --git a/opendc-web/opendc-web-ui/src/api/prefabs.js b/opendc-web/opendc-web-ui/src/api/prefabs.js index a8bd3f3b..eb9aa23c 100644 --- a/opendc-web/opendc-web-ui/src/api/prefabs.js +++ b/opendc-web/opendc-web-ui/src/api/prefabs.js @@ -22,18 +22,18 @@ import { request } from './index' -export function getPrefab(prefabId) { - return request(`prefabs/${prefabId}`) +export function getPrefab(auth, prefabId) { + return request(auth, `prefabs/${prefabId}`) } -export function addPrefab(prefab) { - return request('prefabs', 'POST', { prefab }) +export function addPrefab(auth, prefab) { + return request(auth, 'prefabs/', 'POST', { prefab }) } -export function updatePrefab(prefab) { - return request(`prefabs/${prefab._id}`, 'PUT', { prefab }) +export function updatePrefab(auth, prefab) { + return request(auth, `prefabs/${prefab._id}`, 'PUT', { prefab }) } -export function deletePrefab(prefabId) { - return request(`prefabs/${prefabId}`, 'DELETE') +export function deletePrefab(auth, prefabId) { + return request(auth, `prefabs/${prefabId}`, 'DELETE') } diff --git a/opendc-web/opendc-web-ui/src/api/projects.js b/opendc-web/opendc-web-ui/src/api/projects.js index 9ff7deda..93052080 100644 --- a/opendc-web/opendc-web-ui/src/api/projects.js +++ b/opendc-web/opendc-web-ui/src/api/projects.js @@ -22,18 +22,22 @@ import { request } from './index' -export function getProject(projectId) { - return request(`projects/${projectId}`) +export function getProjects(auth) { + return request(auth, `projects/`) } -export function addProject(project) { - return request('projects', 'POST', { project }) +export function getProject(auth, projectId) { + return request(auth, `projects/${projectId}`) } -export function updateProject(project) { - return request(`projects/${project._id}`, 'PUT', { project }) +export function addProject(auth, project) { + return request(auth, 'projects/', 'POST', { project }) } -export function deleteProject(projectId) { - return request(`projects/${projectId}`, 'DELETE') +export function updateProject(auth, project) { + return request(auth, `projects/${project._id}`, 'PUT', { project }) +} + +export function deleteProject(auth, projectId) { + return request(auth, `projects/${projectId}`, 'DELETE') } diff --git a/opendc-web/opendc-web-ui/src/api/scenarios.js b/opendc-web/opendc-web-ui/src/api/scenarios.js index 9f8c717b..095aa788 100644 --- a/opendc-web/opendc-web-ui/src/api/scenarios.js +++ b/opendc-web/opendc-web-ui/src/api/scenarios.js @@ -22,18 +22,18 @@ import { request } from './index' -export function addScenario(portfolioId, scenario) { - return request(`portfolios/${portfolioId}/scenarios`, 'POST', { scenario }) +export function addScenario(auth, portfolioId, scenario) { + return request(auth, `portfolios/${portfolioId}/scenarios`, 'POST', { scenario }) } -export function getScenario(scenarioId) { - return request(`scenarios/${scenarioId}`) +export function getScenario(auth, scenarioId) { + return request(auth, `scenarios/${scenarioId}`) } -export function updateScenario(scenarioId, scenario) { - return request(`scenarios/${scenarioId}`, 'PUT', { scenario }) +export function updateScenario(auth, scenarioId, scenario) { + return request(auth, `scenarios/${scenarioId}`, 'PUT', { scenario }) } -export function deleteScenario(scenarioId) { - return request(`scenarios/${scenarioId}`, 'DELETE') +export function deleteScenario(auth, scenarioId) { + return request(auth, `scenarios/${scenarioId}`, 'DELETE') } diff --git a/opendc-web/opendc-web-ui/src/api/schedulers.js b/opendc-web/opendc-web-ui/src/api/schedulers.js index 7791e51e..1b69f1a1 100644 --- a/opendc-web/opendc-web-ui/src/api/schedulers.js +++ b/opendc-web/opendc-web-ui/src/api/schedulers.js @@ -22,6 +22,6 @@ import { request } from './index' -export function getAllSchedulers() { - return request('schedulers') +export function getAllSchedulers(auth) { + return request(auth, 'schedulers/') } diff --git a/opendc-web/opendc-web-ui/src/api/token-signin.js b/opendc-web/opendc-web-ui/src/api/token-signin.js deleted file mode 100644 index a3761fa1..00000000 --- a/opendc-web/opendc-web-ui/src/api/token-signin.js +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (c) 2021 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. - */ - -export function performTokenSignIn(token) { - const apiUrl = process.env.NEXT_PUBLIC_API_BASE_URL - - return fetch(`${apiUrl}/tokensignin`, { - method: 'POST', - body: new URLSearchParams({ - idtoken: token, - }), - }).then((res) => res.json()) -} diff --git a/opendc-web/opendc-web-ui/src/api/topologies.js b/opendc-web/opendc-web-ui/src/api/topologies.js index e6df73c7..c8744e6c 100644 --- a/opendc-web/opendc-web-ui/src/api/topologies.js +++ b/opendc-web/opendc-web-ui/src/api/topologies.js @@ -22,18 +22,18 @@ import { request } from './index' -export function addTopology(topology) { - return request(`projects/${topology.projectId}/topologies`, 'POST', { topology }) +export function addTopology(auth, topology) { + return request(auth, `projects/${topology.projectId}/topologies`, 'POST', { topology }) } -export function getTopology(topologyId) { - return request(`topologies/${topologyId}`) +export function getTopology(auth, topologyId) { + return request(auth, `topologies/${topologyId}`) } -export function updateTopology(topology) { - return request(`topologies/${topology._id}`, 'PUT', { topology }) +export function updateTopology(auth, topology) { + return request(auth, `topologies/${topology._id}`, 'PUT', { topology }) } -export function deleteTopology(topologyId) { - return request(`topologies/${topologyId}`, 'DELETE') +export function deleteTopology(auth, topologyId) { + return request(auth, `topologies/${topologyId}`, 'DELETE') } diff --git a/opendc-web/opendc-web-ui/src/api/traces.js b/opendc-web/opendc-web-ui/src/api/traces.js index 1c5cfa1d..df03a2dd 100644 --- a/opendc-web/opendc-web-ui/src/api/traces.js +++ b/opendc-web/opendc-web-ui/src/api/traces.js @@ -22,6 +22,6 @@ import { request } from './index' -export function getAllTraces() { - return request('traces') +export function getAllTraces(auth) { + return request(auth, 'traces/') } diff --git a/opendc-web/opendc-web-ui/src/auth.js b/opendc-web/opendc-web-ui/src/auth.js index faed9829..706151bf 100644 --- a/opendc-web/opendc-web-ui/src/auth.js +++ b/opendc-web/opendc-web-ui/src/auth.js @@ -1,83 +1,65 @@ -import { LOG_IN_SUCCEEDED, LOG_OUT } from './redux/actions/auth' -import { DELETE_CURRENT_USER_SUCCEEDED } from './redux/actions/users' -import { useEffect, useState } from 'react' -import { useRouter } from 'next/router' -import { useSelector } from 'react-redux' - -const getAuthObject = () => { - const authItem = global.localStorage && localStorage.getItem('auth') - if (!authItem || authItem === '{}') { - return undefined - } - return JSON.parse(authItem) -} - -export const userIsLoggedIn = () => { - const authObj = getAuthObject() - - if (!authObj || !authObj.googleId) { - return false - } - - const currentTime = new Date().getTime() - return parseInt(authObj.expiresAt, 10) - currentTime > 0 -} - -export const getAuthToken = () => { - const authObj = getAuthObject() - if (!authObj) { - return undefined - } - - return authObj.authToken -} - -export const saveAuthLocalStorage = (payload) => { - localStorage.setItem('auth', JSON.stringify(payload)) -} - -export const clearAuthLocalStorage = () => { - localStorage.setItem('auth', '') -} - -export const authRedirectMiddleware = (store) => (next) => (action) => { - switch (action.type) { - case LOG_IN_SUCCEEDED: - saveAuthLocalStorage(action.payload) - window.location.href = '/projects' - break - case LOG_OUT: - case DELETE_CURRENT_USER_SUCCEEDED: - clearAuthLocalStorage() - window.location.href = '/' - break - default: - next(action) - return - } - - next(action) -} +/* + * Copyright (c) 2021 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. + */ -export function useIsLoggedIn() { - const [isLoggedIn, setLoggedIn] = useState(false) - - useEffect(() => { - setLoggedIn(userIsLoggedIn()) - }, []) +import { Auth0Provider, useAuth0 } from '@auth0/auth0-react' +import { useEffect } from 'react' +import { useRouter } from 'next/router' - return isLoggedIn +/** + * Obtain the authentication context. + */ +export function useAuth() { + return useAuth0() } +/** + * Force the user to be authenticated or redirect to the homepage. + */ export function useRequireAuth() { + const auth = useAuth() const router = useRouter() + const { isLoading, isAuthenticated } = auth + useEffect(() => { - if (!userIsLoggedIn()) { + if (!isLoading && !isAuthenticated) { router.replace('/') } - }) + }, [isLoading, isAuthenticated]) + + return auth } -export function useUser() { - return useSelector((state) => state.auth) +/** + * AuthProvider which provides an authentication context. + */ +export function AuthProvider({ children }) { + return ( + <Auth0Provider + domain={process.env.NEXT_PUBLIC_AUTH0_DOMAIN} + clientId={process.env.NEXT_PUBLIC_AUTH0_CLIENT_ID} + redirectUri={global.window && global.window.location.origin} + audience={process.env.NEXT_PUBLIC_AUTH0_AUDIENCE} + > + {children} + </Auth0Provider> + ) } diff --git a/opendc-web/opendc-web-ui/src/components/navigation/Navbar.js b/opendc-web/opendc-web-ui/src/components/navigation/Navbar.js index f16a3feb..5c9ea1b8 100644 --- a/opendc-web/opendc-web-ui/src/components/navigation/Navbar.js +++ b/opendc-web/opendc-web-ui/src/components/navigation/Navbar.js @@ -15,7 +15,7 @@ import Login from '../../containers/auth/Login' import Logout from '../../containers/auth/Logout' import ProfileName from '../../containers/auth/ProfileName' import { login, navbar, opendcBrand } from './Navbar.module.scss' -import { useIsLoggedIn } from '../../auth' +import { useAuth } from '../../auth' export const NAVBAR_HEIGHT = 60 @@ -44,10 +44,10 @@ export const NavItem = ({ route, children }) => { export const LoggedInSection = () => { const router = useRouter() - const isLoggedIn = useIsLoggedIn() + const { isAuthenticated } = useAuth() return ( <Nav navbar className="auth-links"> - {isLoggedIn ? ( + {isAuthenticated ? ( [ router.asPath === '/' ? ( <NavItem route="/projects" key="projects"> @@ -58,12 +58,10 @@ export const LoggedInSection = () => { </Link> </NavItem> ) : ( - <NavItem route="/profile" key="profile"> - <Link href="/profile"> - <NavLink title="My Profile"> - <ProfileName /> - </NavLink> - </Link> + <NavItem key="profile"> + <NavLink title="My Profile"> + <ProfileName /> + </NavLink> </NavItem> ), <NavItem route="logout" key="logout"> diff --git a/opendc-web/opendc-web-ui/src/components/projects/ProjectList.js b/opendc-web/opendc-web-ui/src/components/projects/ProjectList.js index 90d42326..cb17b835 100644 --- a/opendc-web/opendc-web-ui/src/components/projects/ProjectList.js +++ b/opendc-web/opendc-web-ui/src/components/projects/ProjectList.js @@ -1,12 +1,12 @@ import PropTypes from 'prop-types' import React from 'react' -import { Authorization } from '../../shapes' +import { Project } from '../../shapes' import ProjectRow from './ProjectRow' -const ProjectList = ({ authorizations }) => { +const ProjectList = ({ projects }) => { return ( <div className="vertically-expanding-container"> - {authorizations.length === 0 ? ( + {projects.length === 0 ? ( <div className="alert alert-info"> <span className="info-icon fa fa-question-circle mr-2" /> <strong>No projects here yet...</strong> Add some with the 'New Project' button! @@ -22,8 +22,8 @@ const ProjectList = ({ authorizations }) => { </tr> </thead> <tbody> - {authorizations.map((authorization) => ( - <ProjectRow projectAuth={authorization} key={authorization.project._id} /> + {projects.map((project) => ( + <ProjectRow project={project} key={project._id} /> ))} </tbody> </table> @@ -33,7 +33,7 @@ const ProjectList = ({ authorizations }) => { } ProjectList.propTypes = { - authorizations: PropTypes.arrayOf(Authorization).isRequired, + projects: PropTypes.arrayOf(Project).isRequired, } export default ProjectList diff --git a/opendc-web/opendc-web-ui/src/components/projects/ProjectRow.js b/opendc-web/opendc-web-ui/src/components/projects/ProjectRow.js index a0aac098..9a95b777 100644 --- a/opendc-web/opendc-web-ui/src/components/projects/ProjectRow.js +++ b/opendc-web/opendc-web-ui/src/components/projects/ProjectRow.js @@ -1,24 +1,29 @@ import classNames from 'classnames' import React from 'react' import ProjectActions from '../../containers/projects/ProjectActions' -import { Authorization } from '../../shapes' +import { Project } from '../../shapes' import { AUTH_DESCRIPTION_MAP, AUTH_ICON_MAP } from '../../util/authorizations' import { parseAndFormatDateTime } from '../../util/date-time' +import { useAuth } from '../../auth' -const ProjectRow = ({ projectAuth }) => ( - <tr> - <td className="pt-3">{projectAuth.project.name}</td> - <td className="pt-3">{parseAndFormatDateTime(projectAuth.project.datetimeLastEdited)}</td> - <td className="pt-3"> - <span className={classNames('fa', 'fa-' + AUTH_ICON_MAP[projectAuth.authorizationLevel], 'mr-2')} /> - {AUTH_DESCRIPTION_MAP[projectAuth.authorizationLevel]} - </td> - <ProjectActions projectId={projectAuth.project._id} /> - </tr> -) +const ProjectRow = ({ project }) => { + const { user } = useAuth() + const { level } = project.authorizations.find((auth) => auth.userId === user.sub) + return ( + <tr> + <td className="pt-3">{project.name}</td> + <td className="pt-3">{parseAndFormatDateTime(project.datetimeLastEdited)}</td> + <td className="pt-3"> + <span className={classNames('fa', 'fa-' + AUTH_ICON_MAP[level], 'mr-2')} /> + {AUTH_DESCRIPTION_MAP[level]} + </td> + <ProjectActions projectId={project._id} /> + </tr> + ) +} ProjectRow.propTypes = { - projectAuth: Authorization.isRequired, + project: Project.isRequired, } export default ProjectRow diff --git a/opendc-web/opendc-web-ui/src/containers/app/sidebars/project/ScenarioListContainer.js b/opendc-web/opendc-web-ui/src/containers/app/sidebars/project/ScenarioListContainer.js index 7aaa1886..e1be51dc 100644 --- a/opendc-web/opendc-web-ui/src/containers/app/sidebars/project/ScenarioListContainer.js +++ b/opendc-web/opendc-web-ui/src/containers/app/sidebars/project/ScenarioListContainer.js @@ -60,7 +60,7 @@ const ScenarioListContainer = ({ portfolioId }) => { /> <NewScenarioModalComponent show={isVisible} - currentPortfolioId={currentProjectId} + currentPortfolioId={portfolioId} currentPortfolioScenarioIds={scenarios.map((s) => s._id)} traces={traces} schedulers={schedulers} diff --git a/opendc-web/opendc-web-ui/src/containers/auth/Login.js b/opendc-web/opendc-web-ui/src/containers/auth/Login.js index b0da0d0e..8459ef5f 100644 --- a/opendc-web/opendc-web-ui/src/containers/auth/Login.js +++ b/opendc-web/opendc-web-ui/src/containers/auth/Login.js @@ -1,43 +1,18 @@ import React from 'react' -import GoogleLogin from 'react-google-login' -import { useDispatch } from 'react-redux' -import { logIn } from '../../redux/actions/auth' import { Button } from 'reactstrap' +import { useAuth } from '../../auth' function Login({ visible, className }) { - const dispatch = useDispatch() - - const onLogin = (payload) => dispatch(logIn(payload)) - const onAuthResponse = (response) => { - onLogin({ - email: response.getBasicProfile().getEmail(), - givenName: response.getBasicProfile().getGivenName(), - familyName: response.getBasicProfile().getFamilyName(), - googleId: response.googleId, - authToken: response.getAuthResponse().id_token, - expiresAt: response.getAuthResponse().expires_at, - }) - } - const onAuthFailure = (error) => { - // TODO Show error alert - console.error(error) - } + const { loginWithRedirect } = useAuth() if (!visible) { return <span /> } return ( - <GoogleLogin - clientId={process.env.NEXT_PUBLIC_OAUTH_CLIENT_ID} - onSuccess={onAuthResponse} - onFailure={onAuthFailure} - render={(renderProps) => ( - <Button color="primary" onClick={renderProps.onClick} className={className}> - <span aria-hidden className="fa fa-google" /> Login with Google - </Button> - )} - /> + <Button color="primary" onClick={() => loginWithRedirect()} className={className}> + <span aria-hidden className="fa fa-sign-in" /> Sign In + </Button> ) } diff --git a/opendc-web/opendc-web-ui/src/containers/auth/Logout.js b/opendc-web/opendc-web-ui/src/containers/auth/Logout.js index 94d4d061..37705c5d 100644 --- a/opendc-web/opendc-web-ui/src/containers/auth/Logout.js +++ b/opendc-web/opendc-web-ui/src/containers/auth/Logout.js @@ -1,11 +1,10 @@ import React from 'react' -import { useDispatch } from 'react-redux' -import { logOut } from '../../redux/actions/auth' import LogoutButton from '../../components/navigation/LogoutButton' +import { useAuth } from '../../auth' const Logout = (props) => { - const dispatch = useDispatch() - return <LogoutButton {...props} onLogout={() => dispatch(logOut())} /> + const { logout } = useAuth() + return <LogoutButton {...props} onLogout={() => logout({ returnTo: window.location.origin })} /> } export default Logout diff --git a/opendc-web/opendc-web-ui/src/containers/auth/ProfileName.js b/opendc-web/opendc-web-ui/src/containers/auth/ProfileName.js index 3992c00f..70f5b884 100644 --- a/opendc-web/opendc-web-ui/src/containers/auth/ProfileName.js +++ b/opendc-web/opendc-web-ui/src/containers/auth/ProfileName.js @@ -1,13 +1,9 @@ import React from 'react' -import { useUser } from '../../auth' +import { useAuth } from '../../auth' function ProfileName() { - const user = useUser() - return ( - <span> - {user.givenName} {user.familyName} - </span> - ) + const { isLoading, user } = useAuth() + return isLoading ? <span>Loading...</span> : <span>{user.name}</span> } export default ProfileName diff --git a/opendc-web/opendc-web-ui/src/containers/projects/ProjectListContainer.js b/opendc-web/opendc-web-ui/src/containers/projects/ProjectListContainer.js new file mode 100644 index 00000000..6632a8b5 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/containers/projects/ProjectListContainer.js @@ -0,0 +1,34 @@ +import React from 'react' +import PropTypes from 'prop-types' +import ProjectList from '../../components/projects/ProjectList' +import { useAuth } from '../../auth' +import { useProjects } from '../../data/project' + +const getVisibleProjects = (projects, filter, userId) => { + switch (filter) { + case 'SHOW_ALL': + return projects + case 'SHOW_OWN': + return projects.filter((project) => + project.authorizations.some((a) => a.userId === userId && a.level === 'OWN') + ) + case 'SHOW_SHARED': + return projects.filter((project) => + project.authorizations.some((a) => a.userId === userId && a.level !== 'OWN') + ) + default: + return projects + } +} + +const ProjectListContainer = ({ filter }) => { + const { user } = useAuth() + const projects = useProjects() + return <ProjectList projects={getVisibleProjects(projects, filter, user?.sub)} /> +} + +ProjectListContainer.propTypes = { + filter: PropTypes.string.isRequired, +} + +export default ProjectListContainer diff --git a/opendc-web/opendc-web-ui/src/containers/projects/VisibleProjectAuthList.js b/opendc-web/opendc-web-ui/src/containers/projects/VisibleProjectAuthList.js deleted file mode 100644 index 8e1d063b..00000000 --- a/opendc-web/opendc-web-ui/src/containers/projects/VisibleProjectAuthList.js +++ /dev/null @@ -1,37 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' -import { useSelector } from 'react-redux' -import ProjectList from '../../components/projects/ProjectList' - -const getVisibleProjectAuths = (projectAuths, filter) => { - switch (filter) { - case 'SHOW_ALL': - return projectAuths - case 'SHOW_OWN': - return projectAuths.filter((projectAuth) => projectAuth.authorizationLevel === 'OWN') - case 'SHOW_SHARED': - return projectAuths.filter((projectAuth) => projectAuth.authorizationLevel !== 'OWN') - default: - return projectAuths - } -} - -const VisibleProjectAuthList = ({ filter }) => { - const authorizations = useSelector((state) => { - const denormalizedAuthorizations = state.projectList.authorizationsOfCurrentUser.map((authorizationIds) => { - const authorization = state.objects.authorization[authorizationIds] - authorization.user = state.objects.user[authorization.userId] - authorization.project = state.objects.project[authorization.projectId] - return authorization - }) - - return getVisibleProjectAuths(denormalizedAuthorizations, filter) - }) - return <ProjectList authorizations={authorizations} /> -} - -VisibleProjectAuthList.propTypes = { - filter: PropTypes.string.isRequired, -} - -export default VisibleProjectAuthList diff --git a/opendc-web/opendc-web-ui/src/data/project.js b/opendc-web/opendc-web-ui/src/data/project.js index 0db49fdd..de2bc0d3 100644 --- a/opendc-web/opendc-web-ui/src/data/project.js +++ b/opendc-web/opendc-web-ui/src/data/project.js @@ -23,6 +23,13 @@ import { useSelector } from 'react-redux' /** + * Return the available projects. + */ +export function useProjects() { + return useSelector((state) => state.projects) +} + +/** * Return the current active project. */ export function useActiveProject() { diff --git a/opendc-web/opendc-web-ui/src/pages/_app.js b/opendc-web/opendc-web-ui/src/pages/_app.js index 761ae0cd..f9727a7a 100644 --- a/opendc-web/opendc-web-ui/src/pages/_app.js +++ b/opendc-web/opendc-web-ui/src/pages/_app.js @@ -24,19 +24,29 @@ import Head from 'next/head' import { Provider } from 'react-redux' import { useStore } from '../redux' import '../index.scss' +import { AuthProvider, useAuth } from '../auth' -export default function App({ Component, pageProps }) { - const store = useStore(pageProps.initialReduxState) +// This setup is necessary to forward the Auth0 context to the Redux context +const Inner = ({ Component, pageProps }) => { + const auth = useAuth() + const store = useStore(pageProps.initialReduxState, { auth }) + return ( + <Provider store={store}> + <Component {...pageProps} /> + </Provider> + ) +} +export default function App(props) { return ( <> <Head> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" /> <meta name="theme-color" content="#00A6D6" /> </Head> - <Provider store={store}> - <Component {...pageProps} /> - </Provider> + <AuthProvider> + <Inner {...props} /> + </AuthProvider> </> ) } diff --git a/opendc-web/opendc-web-ui/src/api/users.js b/opendc-web/opendc-web-ui/src/pages/logout.js index 3da030ad..e96e0605 100644 --- a/opendc-web/opendc-web-ui/src/api/users.js +++ b/opendc-web/opendc-web-ui/src/pages/logout.js @@ -20,20 +20,20 @@ * SOFTWARE. */ -import { request } from './index' +import React from 'react' +import Head from 'next/head' +import AppNavbarContainer from '../containers/navigation/AppNavbarContainer' -export function getUserByEmail(email) { - return request(`users` + new URLSearchParams({ email })) +function Logout() { + return ( + <> + <Head> + <title>Logged Out - OpenDC</title> + </Head> + <AppNavbarContainer fullWidth={false} /> + <span>Logged out successfully</span> + </> + ) } -export function addUser(user) { - return request('users', 'POST', { user }) -} - -export function getUser(userId) { - return request(`users/${userId}`) -} - -export function deleteUser(userId) { - return request(`users/${userId}`, 'DELETE') -} +export default Logout diff --git a/opendc-web/opendc-web-ui/src/pages/profile.js b/opendc-web/opendc-web-ui/src/pages/profile.js deleted file mode 100644 index 97afc4d9..00000000 --- a/opendc-web/opendc-web-ui/src/pages/profile.js +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright (c) 2021 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. - */ - -import React, { useState } from 'react' -import Head from 'next/head' -import { useDispatch } from 'react-redux' -import AppNavbarContainer from '../containers/navigation/AppNavbarContainer' -import ConfirmationModal from '../components/modals/ConfirmationModal' -import { deleteCurrentUser } from '../redux/actions/users' -import { useRequireAuth } from '../auth' - -const Profile = () => { - useRequireAuth() - - const dispatch = useDispatch() - - const [isDeleteModalOpen, setDeleteModalOpen] = useState(false) - const onDelete = (isConfirmed) => { - if (isConfirmed) { - dispatch(deleteCurrentUser()) - } - setDeleteModalOpen(false) - } - - return ( - <> - <Head> - <title>My Profile - OpenDC</title> - </Head> - <div className="full-height"> - <AppNavbarContainer fullWidth={false} /> - <div className="container text-page-container full-height"> - <button - className="btn btn-danger mb-2 ml-auto mr-auto" - style={{ maxWidth: 300 }} - onClick={() => setDeleteModalOpen(true)} - > - Delete my account on OpenDC - </button> - <p className="text-muted text-center"> - This does not delete your Google account, but simply disconnects it from the OpenDC platform and - deletes any project info that is associated with you (projects you own and any authorizations - you may have on other projects). - </p> - </div> - <ConfirmationModal - title="Delete my account" - message="Are you sure you want to delete your OpenDC account?" - show={isDeleteModalOpen} - callback={onDelete} - /> - </div> - </> - ) -} - -export default Profile 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 05e9514f..8603c228 100644 --- a/opendc-web/opendc-web-ui/src/pages/projects/index.js +++ b/opendc-web/opendc-web-ui/src/pages/projects/index.js @@ -1,13 +1,13 @@ import React, { useEffect, useState } from 'react' import Head from 'next/head' import { useDispatch } from 'react-redux' -import { fetchAuthorizationsOfCurrentUser } from '../../redux/actions/users' import ProjectFilterPanel from '../../components/projects/FilterPanel' import NewProjectContainer from '../../containers/projects/NewProjectContainer' -import VisibleProjectList from '../../containers/projects/VisibleProjectAuthList' +import ProjectListContainer from '../../containers/projects/ProjectListContainer' import AppNavbarContainer from '../../containers/navigation/AppNavbarContainer' import { useRequireAuth } from '../../auth' import { Container } from 'reactstrap' +import { fetchProjects } from '../../redux/actions/projects' function Projects() { useRequireAuth() @@ -15,7 +15,7 @@ function Projects() { const dispatch = useDispatch() const [filter, setFilter] = useState('SHOW_ALL') - useEffect(() => dispatch(fetchAuthorizationsOfCurrentUser()), []) + useEffect(() => dispatch(fetchProjects()), []) return ( <> @@ -26,7 +26,7 @@ function Projects() { <AppNavbarContainer fullWidth={false} /> <Container className="text-page-container"> <ProjectFilterPanel onSelect={setFilter} activeFilter={filter} /> - <VisibleProjectList filter={filter} /> + <ProjectListContainer filter={filter} /> <NewProjectContainer /> </Container> </div> diff --git a/opendc-web/opendc-web-ui/src/redux/actions/auth.js b/opendc-web/opendc-web-ui/src/redux/actions/auth.js deleted file mode 100644 index 38c1a782..00000000 --- a/opendc-web/opendc-web-ui/src/redux/actions/auth.js +++ /dev/null @@ -1,23 +0,0 @@ -export const LOG_IN = 'LOG_IN' -export const LOG_IN_SUCCEEDED = 'LOG_IN_SUCCEEDED' -export const LOG_OUT = 'LOG_OUT' - -export function logIn(payload) { - return { - type: LOG_IN, - payload, - } -} - -export function logInSucceeded(payload) { - return { - type: LOG_IN_SUCCEEDED, - payload, - } -} - -export function logOut() { - return { - type: LOG_OUT, - } -} diff --git a/opendc-web/opendc-web-ui/src/redux/actions/projects.js b/opendc-web/opendc-web-ui/src/redux/actions/projects.js index 15158164..a6324c43 100644 --- a/opendc-web/opendc-web-ui/src/redux/actions/projects.js +++ b/opendc-web/opendc-web-ui/src/redux/actions/projects.js @@ -1,24 +1,35 @@ +export const FETCH_PROJECTS = 'FETCH_PROJECTS' +export const FETCH_PROJECTS_SUCCEEDED = 'FETCH_PROJECTS_SUCCEEDED' export const ADD_PROJECT = 'ADD_PROJECT' export const ADD_PROJECT_SUCCEEDED = 'ADD_PROJECT_SUCCEEDED' export const DELETE_PROJECT = 'DELETE_PROJECT' export const DELETE_PROJECT_SUCCEEDED = 'DELETE_PROJECT_SUCCEEDED' export const OPEN_PROJECT_SUCCEEDED = 'OPEN_PROJECT_SUCCEEDED' +export function fetchProjects() { + return { + type: FETCH_PROJECTS, + } +} + +export function fetchProjectsSucceeded(projects) { + return { + type: FETCH_PROJECTS_SUCCEEDED, + projects, + } +} + export function addProject(name) { - return (dispatch, getState) => { - const { auth } = getState() - dispatch({ - type: ADD_PROJECT, - name, - userId: auth.userId, - }) + return { + type: ADD_PROJECT, + name, } } -export function addProjectSucceeded(authorization) { +export function addProjectSucceeded(project) { return { type: ADD_PROJECT_SUCCEEDED, - authorization, + project, } } diff --git a/opendc-web/opendc-web-ui/src/redux/actions/users.js b/opendc-web/opendc-web-ui/src/redux/actions/users.js deleted file mode 100644 index 4868ac34..00000000 --- a/opendc-web/opendc-web-ui/src/redux/actions/users.js +++ /dev/null @@ -1,37 +0,0 @@ -export const FETCH_AUTHORIZATIONS_OF_CURRENT_USER = 'FETCH_AUTHORIZATIONS_OF_CURRENT_USER' -export const FETCH_AUTHORIZATIONS_OF_CURRENT_USER_SUCCEEDED = 'FETCH_AUTHORIZATIONS_OF_CURRENT_USER_SUCCEEDED' -export const DELETE_CURRENT_USER = 'DELETE_CURRENT_USER' -export const DELETE_CURRENT_USER_SUCCEEDED = 'DELETE_CURRENT_USER_SUCCEEDED' - -export function fetchAuthorizationsOfCurrentUser() { - return (dispatch, getState) => { - const { auth } = getState() - dispatch({ - type: FETCH_AUTHORIZATIONS_OF_CURRENT_USER, - userId: auth.userId, - }) - } -} - -export function fetchAuthorizationsOfCurrentUserSucceeded(authorizationsOfCurrentUser) { - return { - type: FETCH_AUTHORIZATIONS_OF_CURRENT_USER_SUCCEEDED, - authorizationsOfCurrentUser, - } -} - -export function deleteCurrentUser() { - return (dispatch, getState) => { - const { auth } = getState() - dispatch({ - type: DELETE_CURRENT_USER, - userId: auth.userId, - }) - } -} - -export function deleteCurrentUserSucceeded() { - return { - type: DELETE_CURRENT_USER_SUCCEEDED, - } -} diff --git a/opendc-web/opendc-web-ui/src/redux/index.js b/opendc-web/opendc-web-ui/src/redux/index.js index c706752b..ee6ca3f5 100644 --- a/opendc-web/opendc-web-ui/src/redux/index.js +++ b/opendc-web/opendc-web-ui/src/redux/index.js @@ -1,40 +1,32 @@ import { useMemo } from 'react' -import { applyMiddleware, compose, createStore } from 'redux' +import { applyMiddleware, createStore } from 'redux' import { createLogger } from 'redux-logger' -import persistState from 'redux-localstorage' import createSagaMiddleware from 'redux-saga' import thunk from 'redux-thunk' -import { authRedirectMiddleware } from '../auth' import rootReducer from './reducers' import rootSaga from './sagas' import { viewportAdjustmentMiddleware } from './middleware/viewport-adjustment' let store -function initStore(initialState) { - const sagaMiddleware = createSagaMiddleware() +function initStore(initialState, ctx) { + const sagaMiddleware = createSagaMiddleware({ context: ctx }) - const middlewares = [thunk, sagaMiddleware, authRedirectMiddleware, viewportAdjustmentMiddleware] + const middlewares = [thunk, sagaMiddleware, viewportAdjustmentMiddleware] if (process.env.NODE_ENV !== 'production') { middlewares.push(createLogger()) } - let enhancer = applyMiddleware(...middlewares) - - if (global.localStorage) { - enhancer = compose(persistState('auth'), enhancer) - } - - const configuredStore = createStore(rootReducer, enhancer) + const configuredStore = createStore(rootReducer, initialState, applyMiddleware(...middlewares)) sagaMiddleware.run(rootSaga) store = configuredStore return configuredStore } -export const initializeStore = (preloadedState) => { - let _store = store ?? initStore(preloadedState) +export const initializeStore = (preloadedState, ctx) => { + let _store = store ?? initStore(preloadedState, ctx) // After navigating to a page with an initial Redux state, merge that state // with the current state in the store, and create a new store @@ -55,6 +47,6 @@ export const initializeStore = (preloadedState) => { return _store } -export function useStore(initialState) { - return useMemo(() => initializeStore(initialState), [initialState]) +export function useStore(initialState, ctx) { + return useMemo(() => initializeStore(initialState, ctx), [initialState, ctx]) } diff --git a/opendc-web/opendc-web-ui/src/redux/reducers/auth.js b/opendc-web/opendc-web-ui/src/redux/reducers/auth.js deleted file mode 100644 index 399a4b10..00000000 --- a/opendc-web/opendc-web-ui/src/redux/reducers/auth.js +++ /dev/null @@ -1,12 +0,0 @@ -import { LOG_IN_SUCCEEDED, LOG_OUT } from '../actions/auth' - -export function auth(state = {}, action) { - switch (action.type) { - case LOG_IN_SUCCEEDED: - return action.payload - case LOG_OUT: - return {} - default: - return state - } -} diff --git a/opendc-web/opendc-web-ui/src/redux/reducers/index.js b/opendc-web/opendc-web-ui/src/redux/reducers/index.js index 9dff379b..b143d417 100644 --- a/opendc-web/opendc-web-ui/src/redux/reducers/index.js +++ b/opendc-web/opendc-web-ui/src/redux/reducers/index.js @@ -1,15 +1,14 @@ import { combineReducers } from 'redux' -import { auth } from './auth' import { construction } from './construction-mode' import { currentPortfolioId, currentProjectId, currentScenarioId, currentTopologyId } from './current-ids' import { interactionLevel } from './interaction-level' import { map } from './map' import { objects } from './objects' -import { projectList } from './project-list' +import { projects } from './projects' const rootReducer = combineReducers({ objects, - projectList, + projects, construction, map, currentProjectId, @@ -17,7 +16,6 @@ const rootReducer = combineReducers({ currentPortfolioId, currentScenarioId, interactionLevel, - auth, }) export default rootReducer diff --git a/opendc-web/opendc-web-ui/src/redux/reducers/project-list.js b/opendc-web/opendc-web-ui/src/redux/reducers/project-list.js deleted file mode 100644 index ad803db0..00000000 --- a/opendc-web/opendc-web-ui/src/redux/reducers/project-list.js +++ /dev/null @@ -1,18 +0,0 @@ -import { combineReducers } from 'redux' -import { ADD_PROJECT_SUCCEEDED, DELETE_PROJECT_SUCCEEDED } from '../actions/projects' -import { FETCH_AUTHORIZATIONS_OF_CURRENT_USER_SUCCEEDED } from '../actions/users' - -export function authorizationsOfCurrentUser(state = [], action) { - switch (action.type) { - case FETCH_AUTHORIZATIONS_OF_CURRENT_USER_SUCCEEDED: - return action.authorizationsOfCurrentUser - case ADD_PROJECT_SUCCEEDED: - return [...state, action.authorization] - case DELETE_PROJECT_SUCCEEDED: - return state.filter((authorization) => authorization[1] !== action.id) - default: - return state - } -} - -export const projectList = combineReducers({ authorizationsOfCurrentUser }) diff --git a/opendc-web/opendc-web-ui/src/redux/reducers/projects.js b/opendc-web/opendc-web-ui/src/redux/reducers/projects.js new file mode 100644 index 00000000..a920e47f --- /dev/null +++ b/opendc-web/opendc-web-ui/src/redux/reducers/projects.js @@ -0,0 +1,14 @@ +import { ADD_PROJECT_SUCCEEDED, DELETE_PROJECT_SUCCEEDED, FETCH_PROJECTS_SUCCEEDED } from '../actions/projects' + +export function projects(state = [], action) { + switch (action.type) { + case FETCH_PROJECTS_SUCCEEDED: + return action.projects + case ADD_PROJECT_SUCCEEDED: + return [...state, action.project] + case DELETE_PROJECT_SUCCEEDED: + return state.filter((project) => project._id !== action.id) + default: + return state + } +} diff --git a/opendc-web/opendc-web-ui/src/redux/sagas/index.js b/opendc-web/opendc-web-ui/src/redux/sagas/index.js index 6332b2fb..a8f44843 100644 --- a/opendc-web/opendc-web-ui/src/redux/sagas/index.js +++ b/opendc-web/opendc-web-ui/src/redux/sagas/index.js @@ -1,7 +1,6 @@ import { takeEvery } from 'redux-saga/effects' -import { LOG_IN } from '../actions/auth' import { ADD_PORTFOLIO, DELETE_PORTFOLIO, OPEN_PORTFOLIO_SUCCEEDED, UPDATE_PORTFOLIO } from '../actions/portfolios' -import { ADD_PROJECT, DELETE_PROJECT, OPEN_PROJECT_SUCCEEDED } from '../actions/projects' +import { ADD_PROJECT, DELETE_PROJECT, FETCH_PROJECTS, OPEN_PROJECT_SUCCEEDED } from '../actions/projects' import { ADD_TILE, CANCEL_NEW_ROOM_CONSTRUCTION, @@ -11,10 +10,8 @@ import { import { ADD_UNIT, DELETE_MACHINE, DELETE_UNIT } from '../actions/topology/machine' import { ADD_MACHINE, DELETE_RACK, EDIT_RACK_NAME } from '../actions/topology/rack' import { ADD_RACK_TO_TILE, DELETE_ROOM, EDIT_ROOM_NAME } from '../actions/topology/room' -import { DELETE_CURRENT_USER, FETCH_AUTHORIZATIONS_OF_CURRENT_USER } from '../actions/users' import { onAddPortfolio, onDeletePortfolio, onOpenPortfolioSucceeded, onUpdatePortfolio } from './portfolios' -import { onDeleteCurrentUser } from './profile' -import { onOpenProjectSucceeded, onProjectAdd, onProjectDelete } from './projects' +import { onFetchProjects, onOpenProjectSucceeded, onProjectAdd, onProjectDelete } from './projects' import { onAddMachine, onAddRackToTile, @@ -32,7 +29,6 @@ import { onEditRoomName, onStartNewRoomConstruction, } from './topology' -import { onFetchAuthorizationsOfCurrentUser, onFetchLoggedInUser } from './users' import { ADD_TOPOLOGY, DELETE_TOPOLOGY } from '../actions/topologies' import { ADD_SCENARIO, DELETE_SCENARIO, OPEN_SCENARIO_SUCCEEDED, UPDATE_SCENARIO } from '../actions/scenarios' import { onAddScenario, onDeleteScenario, onOpenScenarioSucceeded, onUpdateScenario } from './scenarios' @@ -40,14 +36,10 @@ import { onAddPrefab } from './prefabs' import { ADD_PREFAB } from '../actions/prefabs' export default function* rootSaga() { - yield takeEvery(LOG_IN, onFetchLoggedInUser) - - yield takeEvery(FETCH_AUTHORIZATIONS_OF_CURRENT_USER, onFetchAuthorizationsOfCurrentUser) + yield takeEvery(FETCH_PROJECTS, onFetchProjects) yield takeEvery(ADD_PROJECT, onProjectAdd) yield takeEvery(DELETE_PROJECT, onProjectDelete) - yield takeEvery(DELETE_CURRENT_USER, onDeleteCurrentUser) - yield takeEvery(OPEN_PROJECT_SUCCEEDED, onOpenProjectSucceeded) yield takeEvery(OPEN_PORTFOLIO_SUCCEEDED, onOpenPortfolioSucceeded) yield takeEvery(OPEN_SCENARIO_SUCCEEDED, onOpenScenarioSucceeded) diff --git a/opendc-web/opendc-web-ui/src/redux/sagas/objects.js b/opendc-web/opendc-web-ui/src/redux/sagas/objects.js index 82dbb935..e5fd092d 100644 --- a/opendc-web/opendc-web-ui/src/redux/sagas/objects.js +++ b/opendc-web/opendc-web-ui/src/redux/sagas/objects.js @@ -1,9 +1,8 @@ -import { call, put, select } from 'redux-saga/effects' +import { call, put, select, getContext } from 'redux-saga/effects' import { addToStore } from '../actions/objects' import { getAllSchedulers } from '../../api/schedulers' import { getProject } from '../../api/projects' import { getAllTraces } from '../../api/traces' -import { getUser } from '../../api/users' import { getTopology, updateTopology } from '../../api/topologies' import { uuid } from 'uuidv4' @@ -42,9 +41,10 @@ function* fetchAndStoreObjects(objectType, apiCall) { return objects } -export const fetchAndStoreProject = (id) => fetchAndStoreObject('project', id, call(getProject, id)) - -export const fetchAndStoreUser = (id) => fetchAndStoreObject('user', id, call(getUser, id)) +export const fetchAndStoreProject = function* (id) { + const auth = yield getContext('auth') + return yield fetchAndStoreObject('project', id, call(getProject, auth, id)) +} export const fetchAndStoreTopology = function* (id) { const topologyStore = yield select(OBJECT_SELECTORS['topology']) @@ -52,10 +52,11 @@ export const fetchAndStoreTopology = function* (id) { const tileStore = yield select(OBJECT_SELECTORS['tile']) const rackStore = yield select(OBJECT_SELECTORS['rack']) const machineStore = yield select(OBJECT_SELECTORS['machine']) + const auth = yield getContext('auth') let topology = topologyStore[id] if (!topology) { - const fullTopology = yield call(getTopology, id) + const fullTopology = yield call(getTopology, auth, id) for (let roomIdx in fullTopology.rooms) { const fullRoom = fullTopology.rooms[roomIdx] @@ -142,8 +143,8 @@ const generateIdIfNotPresent = (obj) => { export const updateTopologyOnServer = function* (id) { const topology = yield getTopologyAsObject(id, true) - - yield call(updateTopology, topology) + const auth = yield getContext('auth') + yield call(updateTopology, auth, topology) } export const getTopologyAsObject = function* (id, keepIds) { @@ -217,10 +218,14 @@ export const getRackById = function* (id, keepIds) { } } -export const fetchAndStoreAllTraces = () => fetchAndStoreObjects('trace', call(getAllTraces)) +export const fetchAndStoreAllTraces = function* () { + const auth = yield getContext('auth') + return yield fetchAndStoreObjects('trace', call(getAllTraces, auth)) +} export const fetchAndStoreAllSchedulers = function* () { - const objects = yield call(getAllSchedulers) + const auth = yield getContext('auth') + const objects = yield call(getAllSchedulers, auth) for (let object of objects) { object._id = object.name yield put(addToStore('scheduler', object)) diff --git a/opendc-web/opendc-web-ui/src/redux/sagas/portfolios.js b/opendc-web/opendc-web-ui/src/redux/sagas/portfolios.js index 8ddf888d..340cb490 100644 --- a/opendc-web/opendc-web-ui/src/redux/sagas/portfolios.js +++ b/opendc-web/opendc-web-ui/src/redux/sagas/portfolios.js @@ -1,4 +1,4 @@ -import { call, put, select, delay } from 'redux-saga/effects' +import { call, put, select, delay, getContext } from 'redux-saga/effects' import { addPropToStoreObject, addToStore } from '../actions/objects' import { addPortfolio, deletePortfolio, getPortfolio, updatePortfolio } from '../../api/portfolios' import { getProject } from '../../api/projects' @@ -8,7 +8,8 @@ import { getScenario } from '../../api/scenarios' export function* onOpenPortfolioSucceeded(action) { try { - const project = yield call(getProject, action.projectId) + const auth = yield getContext('auth') + const project = yield call(getProject, auth, action.projectId) yield put(addToStore('project', project)) yield fetchAndStoreAllTopologiesOfProject(project._id) yield fetchPortfoliosOfProject() @@ -66,11 +67,12 @@ export function* fetchPortfoliosOfProject() { export function* fetchPortfolioWithScenarios(portfolioId) { try { - const portfolio = yield call(getPortfolio, portfolioId) + const auth = yield getContext('auth') + const portfolio = yield call(getPortfolio, auth, portfolioId) yield put(addToStore('portfolio', portfolio)) for (let i in portfolio.scenarioIds) { - const scenario = yield call(getScenario, portfolio.scenarioIds[i]) + const scenario = yield call(getScenario, auth, portfolio.scenarioIds[i]) yield put(addToStore('scenario', scenario)) } return portfolio @@ -82,9 +84,10 @@ export function* fetchPortfolioWithScenarios(portfolioId) { export function* onAddPortfolio(action) { try { const currentProjectId = yield select((state) => state.currentProjectId) - + const auth = yield getContext('auth') const portfolio = yield call( addPortfolio, + auth, currentProjectId, Object.assign({}, action.portfolio, { projectId: currentProjectId, @@ -106,7 +109,8 @@ export function* onAddPortfolio(action) { export function* onUpdatePortfolio(action) { try { - const portfolio = yield call(updatePortfolio, action.portfolio._id, action.portfolio) + const auth = yield getContext('auth') + const portfolio = yield call(updatePortfolio, auth, action.portfolio._id, action.portfolio) yield put(addToStore('portfolio', portfolio)) } catch (error) { console.error(error) @@ -115,7 +119,8 @@ export function* onUpdatePortfolio(action) { export function* onDeletePortfolio(action) { try { - yield call(deletePortfolio, action.id) + const auth = yield getContext('auth') + yield call(deletePortfolio, auth, action.id) const currentProjectId = yield select((state) => state.currentProjectId) const portfolioIds = yield select((state) => state.objects.project[currentProjectId].portfolioIds) diff --git a/opendc-web/opendc-web-ui/src/redux/sagas/prefabs.js b/opendc-web/opendc-web-ui/src/redux/sagas/prefabs.js index 3158a219..ec679391 100644 --- a/opendc-web/opendc-web-ui/src/redux/sagas/prefabs.js +++ b/opendc-web/opendc-web-ui/src/redux/sagas/prefabs.js @@ -1,4 +1,4 @@ -import { call, put, select } from 'redux-saga/effects' +import { call, put, select, getContext } from 'redux-saga/effects' import { addToStore } from '../actions/objects' import { addPrefab } from '../../api/prefabs' import { getRackById } from './objects' @@ -7,7 +7,8 @@ export function* onAddPrefab(action) { try { const currentRackId = yield select((state) => state.objects.tile[state.interactionLevel.tileId].rackId) const currentRackJson = yield getRackById(currentRackId, false) - const prefab = yield call(addPrefab, { name: action.name, rack: currentRackJson }) + const auth = yield getContext('auth') + const prefab = yield call(addPrefab, auth, { name: action.name, rack: currentRackJson }) yield put(addToStore('prefab', prefab)) } catch (error) { console.error(error) diff --git a/opendc-web/opendc-web-ui/src/redux/sagas/profile.js b/opendc-web/opendc-web-ui/src/redux/sagas/profile.js deleted file mode 100644 index e187b765..00000000 --- a/opendc-web/opendc-web-ui/src/redux/sagas/profile.js +++ /dev/null @@ -1,12 +0,0 @@ -import { call, put } from 'redux-saga/effects' -import { deleteCurrentUserSucceeded } from '../actions/users' -import { deleteUser } from '../../api/users' - -export function* onDeleteCurrentUser(action) { - try { - yield call(deleteUser, action.userId) - yield put(deleteCurrentUserSucceeded()) - } catch (error) { - console.error(error) - } -} diff --git a/opendc-web/opendc-web-ui/src/redux/sagas/projects.js b/opendc-web/opendc-web-ui/src/redux/sagas/projects.js index ecd9a7c9..506df6ed 100644 --- a/opendc-web/opendc-web-ui/src/redux/sagas/projects.js +++ b/opendc-web/opendc-web-ui/src/redux/sagas/projects.js @@ -1,14 +1,15 @@ -import { call, put } from 'redux-saga/effects' +import { call, put, getContext } from 'redux-saga/effects' import { addToStore } from '../actions/objects' -import { addProjectSucceeded, deleteProjectSucceeded } from '../actions/projects' -import { addProject, deleteProject, getProject } from '../../api/projects' +import { addProjectSucceeded, deleteProjectSucceeded, fetchProjectsSucceeded } from '../actions/projects' +import { addProject, deleteProject, getProject, getProjects } from '../../api/projects' import { fetchAndStoreAllTopologiesOfProject } from './topology' import { fetchAndStoreAllSchedulers, fetchAndStoreAllTraces } from './objects' import { fetchPortfoliosOfProject } from './portfolios' export function* onOpenProjectSucceeded(action) { try { - const project = yield call(getProject, action.id) + const auth = yield getContext('auth') + const project = yield call(getProject, auth, action.id) yield put(addToStore('project', project)) yield fetchAndStoreAllTopologiesOfProject(action.id, true) @@ -22,17 +23,10 @@ export function* onOpenProjectSucceeded(action) { export function* onProjectAdd(action) { try { - const project = yield call(addProject, { name: action.name }) + const auth = yield getContext('auth') + const project = yield call(addProject, auth, { name: action.name }) yield put(addToStore('project', project)) - - const authorization = { - projectId: project._id, - userId: action.userId, - authorizationLevel: 'OWN', - project, - } - yield put(addToStore('authorization', authorization)) - yield put(addProjectSucceeded([authorization.userId, authorization.projectId])) + yield put(addProjectSucceeded(project)) } catch (error) { console.error(error) } @@ -40,9 +34,20 @@ export function* onProjectAdd(action) { export function* onProjectDelete(action) { try { - yield call(deleteProject, action.id) + const auth = yield getContext('auth') + yield call(deleteProject, auth, action.id) yield put(deleteProjectSucceeded(action.id)) } catch (error) { console.error(error) } } + +export function* onFetchProjects(action) { + try { + const auth = yield getContext('auth') + const projects = yield call(getProjects, auth) + yield put(fetchProjectsSucceeded(projects)) + } catch (error) { + console.error(error) + } +} diff --git a/opendc-web/opendc-web-ui/src/redux/sagas/scenarios.js b/opendc-web/opendc-web-ui/src/redux/sagas/scenarios.js index a5463fa6..bdb7c45d 100644 --- a/opendc-web/opendc-web-ui/src/redux/sagas/scenarios.js +++ b/opendc-web/opendc-web-ui/src/redux/sagas/scenarios.js @@ -1,4 +1,4 @@ -import { call, put, select } from 'redux-saga/effects' +import { call, put, select, getContext } from 'redux-saga/effects' import { addPropToStoreObject, addToStore } from '../actions/objects' import { getProject } from '../../api/projects' import { fetchAndStoreAllSchedulers, fetchAndStoreAllTraces } from './objects' @@ -8,7 +8,8 @@ import { fetchPortfolioWithScenarios, watchForPortfolioResults } from './portfol export function* onOpenScenarioSucceeded(action) { try { - const project = yield call(getProject, action.projectId) + const auth = yield getContext('auth') + const project = yield call(getProject, auth, action.projectId) yield put(addToStore('project', project)) yield fetchAndStoreAllTopologiesOfProject(project._id) yield fetchAndStoreAllSchedulers() @@ -23,7 +24,8 @@ export function* onOpenScenarioSucceeded(action) { export function* onAddScenario(action) { try { - const scenario = yield call(addScenario, action.scenario.portfolioId, action.scenario) + const auth = yield getContext('auth') + const scenario = yield call(addScenario, auth, action.scenario.portfolioId, action.scenario) yield put(addToStore('scenario', scenario)) const scenarioIds = yield select((state) => state.objects.portfolio[action.scenario.portfolioId].scenarioIds) @@ -40,7 +42,8 @@ export function* onAddScenario(action) { export function* onUpdateScenario(action) { try { - const scenario = yield call(updateScenario, action.scenario._id, action.scenario) + const auth = yield getContext('auth') + const scenario = yield call(updateScenario, auth, action.scenario._id, action.scenario) yield put(addToStore('scenario', scenario)) } catch (error) { console.error(error) @@ -49,7 +52,8 @@ export function* onUpdateScenario(action) { export function* onDeleteScenario(action) { try { - yield call(deleteScenario, action.id) + const auth = yield getContext('auth') + yield call(deleteScenario, auth, action.id) const currentPortfolioId = yield select((state) => state.currentPortfolioId) const scenarioIds = yield select((state) => state.objects.portfolio[currentPortfolioId].scenarioIds) diff --git a/opendc-web/opendc-web-ui/src/redux/sagas/topology.js b/opendc-web/opendc-web-ui/src/redux/sagas/topology.js index 65f97cc9..e5fd3d39 100644 --- a/opendc-web/opendc-web-ui/src/redux/sagas/topology.js +++ b/opendc-web/opendc-web-ui/src/redux/sagas/topology.js @@ -1,4 +1,4 @@ -import { call, put, select } from 'redux-saga/effects' +import { call, put, select, getContext } from 'redux-saga/effects' import { goDownOneInteractionLevel } from '../actions/interaction-level' import { addIdToStoreObjectListProp, @@ -50,8 +50,10 @@ export function* onAddTopology(action) { topologyToBeCreated = { name: action.name, rooms: [] } } + const auth = yield getContext('auth') const topology = yield call( addTopology, + auth, Object.assign({}, topologyToBeCreated, { projectId: currentProjectId, }) @@ -79,7 +81,8 @@ export function* onDeleteTopology(action) { yield put(setCurrentTopology(topologyIds.filter((t) => t !== action.id)[0])) } - yield call(deleteTopology, action.id) + const auth = yield getContext('auth') + yield call(deleteTopology, auth, action.id) yield put( addPropToStoreObject('project', currentProjectId, { diff --git a/opendc-web/opendc-web-ui/src/redux/sagas/users.js b/opendc-web/opendc-web-ui/src/redux/sagas/users.js deleted file mode 100644 index 88c424b5..00000000 --- a/opendc-web/opendc-web-ui/src/redux/sagas/users.js +++ /dev/null @@ -1,44 +0,0 @@ -import { call, put } from 'redux-saga/effects' -import { logInSucceeded } from '../actions/auth' -import { addToStore } from '../actions/objects' -import { fetchAuthorizationsOfCurrentUserSucceeded } from '../actions/users' -import { performTokenSignIn } from '../../api/token-signin' -import { addUser } from '../../api/users' -import { saveAuthLocalStorage } from '../../auth' -import { fetchAndStoreProject, fetchAndStoreUser } from './objects' - -export function* onFetchLoggedInUser(action) { - try { - const tokenResponse = yield call(performTokenSignIn, action.payload.authToken) - - let userId = tokenResponse.userId - - if (tokenResponse.isNewUser) { - saveAuthLocalStorage({ authToken: action.payload.authToken }) - const newUser = yield call(addUser, action.payload) - userId = newUser._id - } - - yield put(logInSucceeded(Object.assign({ userId }, action.payload))) - } catch (error) { - console.error(error) - } -} - -export function* onFetchAuthorizationsOfCurrentUser(action) { - try { - const user = yield call(fetchAndStoreUser, action.userId) - - for (const authorization of user.authorizations) { - authorization.userId = action.userId - yield put(addToStore('authorization', authorization)) - yield fetchAndStoreProject(authorization.projectId) - } - - const authorizationIds = user.authorizations.map((authorization) => [action.userId, authorization.projectId]) - - yield put(fetchAuthorizationsOfCurrentUserSucceeded(authorizationIds)) - } catch (error) { - console.error(error) - } -} diff --git a/opendc-web/opendc-web-ui/src/shapes.js b/opendc-web/opendc-web-ui/src/shapes.js index 9aeb99d8..6c29eab0 100644 --- a/opendc-web/opendc-web-ui/src/shapes.js +++ b/opendc-web/opendc-web-ui/src/shapes.js @@ -1,3 +1,25 @@ +/* + * Copyright (c) 2021 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. + */ + import PropTypes from 'prop-types' export const User = PropTypes.shape({ diff --git a/opendc-web/opendc-web-ui/yarn.lock b/opendc-web/opendc-web-ui/yarn.lock index 9aed2596..f8bfc180 100644 --- a/opendc-web/opendc-web-ui/yarn.lock +++ b/opendc-web/opendc-web-ui/yarn.lock @@ -2,6 +2,26 @@ # yarn lockfile v1 +"@auth0/auth0-react@^1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@auth0/auth0-react/-/auth0-react-1.5.0.tgz#f2dcfef1cab59f8555ade4e2b5ec13befce67395" + integrity sha512-LFJUd3V6aKCKxblJvrz3JIHzyBCz2X3ax5RdSjxZwGr5XxPwdhogeBWcyijnuB8moKD2ncl56OpOslbVGLIJ/w== + dependencies: + "@auth0/auth0-spa-js" "^1.15.0" + +"@auth0/auth0-spa-js@^1.15.0": + version "1.15.0" + resolved "https://registry.yarnpkg.com/@auth0/auth0-spa-js/-/auth0-spa-js-1.15.0.tgz#9fa563b7b2e49dc4c6a465a0c240078322e80159" + integrity sha512-d/crchAbhncl9irIMuw1zNSZgX+id0U7mzASQr2htMJ73JCYaAvBSdGXL0WcYS4yBm1Xsx1JYm3b5tEZ5p/ncg== + dependencies: + abortcontroller-polyfill "^1.7.1" + browser-tabs-lock "^1.2.13" + core-js "^3.11.0" + es-cookie "^1.3.2" + fast-text-encoding "^1.0.3" + promise-polyfill "^8.2.0" + unfetch "^4.2.0" + "@babel/code-frame@7.12.11": version "7.12.11" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f" @@ -325,6 +345,11 @@ resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.0.0.tgz#165aae4819ad2174a17476dbe66feebd549556c0" integrity sha512-xSQfNcvOiE5f9dyd4Kzxbof1aTrLobL278pGLKOZI6esGfZ7ts9Ka16CzIN6Y8hFHE1C7jIBZokULhK1bOgjRw== +abortcontroller-polyfill@^1.7.1: + version "1.7.3" + resolved "https://registry.yarnpkg.com/abortcontroller-polyfill/-/abortcontroller-polyfill-1.7.3.tgz#1b5b487bd6436b5b764fd52a612509702c3144b5" + integrity sha512-zetDJxd89y3X99Kvo4qFx8GKlt6GsvN3UcRZHwU6iFA/0KiOmhkTVhe8oRoTBiTVPZu09x3vCra47+w8Yz1+2Q== + aggregate-error@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" @@ -479,6 +504,13 @@ brorand@^1.0.1, brorand@^1.1.0: resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8= +browser-tabs-lock@^1.2.13: + version "1.2.14" + resolved "https://registry.yarnpkg.com/browser-tabs-lock/-/browser-tabs-lock-1.2.14.tgz#f4ba30810d20199a1858c102da1f91e339250728" + integrity sha512-ssSpCRcvFe4vc098LDnrJOQDfZiG35KhQGB9hthTbwJk5mmUkePwhcMlW61NH3YuIE2Y9uGLqf9yxEBKbaDlaw== + dependencies: + lodash ">=4.17.21" + browserify-aes@^1.0.0, browserify-aes@^1.0.4: version "1.2.0" resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48" @@ -755,6 +787,11 @@ convert-source-map@1.7.0: dependencies: safe-buffer "~5.1.1" +core-js@^3.11.0: + version "3.12.1" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.12.1.tgz#6b5af4ff55616c08a44d386f1f510917ff204112" + integrity sha512-Ne9DKPHTObRuB09Dru5AjwKjY4cJHVGu+y5f7coGn1E9Grkc3p2iBwE9AI/nJzsE29mQF7oq+mhYYRqOMFN1Bw== + core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" @@ -1104,6 +1141,11 @@ es-abstract@^1.18.0-next.1, es-abstract@^1.18.0-next.2: string.prototype.trimstart "^1.0.4" unbox-primitive "^1.0.0" +es-cookie@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/es-cookie/-/es-cookie-1.3.2.tgz#80e831597f72a25721701bdcb21d990319acd831" + integrity sha512-UTlYYhXGLOy05P/vKVT2Ui7WtC7NiRzGtJyAKKn32g5Gvcjn7KAClLPWlipCtxIus934dFg9o9jXiBL0nP+t9Q== + es-to-primitive@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" @@ -1181,6 +1223,11 @@ fast-equals@^2.0.0: resolved "https://registry.yarnpkg.com/fast-equals/-/fast-equals-2.0.2.tgz#f4029b437d9e8184b807233bdbcd371e3bc73cf7" integrity sha512-ehHsR38w6sqy5E0QrWQxclb+zl3ulNsgCVWt1cMoZ6QBFgtkr4lKZWpQP1kfEFn6bWnm78pmiDGak+zUvQ2/DQ== +fast-text-encoding@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz#ec02ac8e01ab8a319af182dae2681213cfe9ce53" + integrity sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig== + figures@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" @@ -1702,7 +1749,7 @@ lodash.throttle@^4.1.1: resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4" integrity sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ= -lodash@^4.17.13, lodash@^4.17.19: +lodash@>=4.17.21, lodash@^4.17.13, lodash@^4.17.19: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -2168,7 +2215,12 @@ process@0.11.10, process@^0.11.10: resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI= -prop-types@15.7.2, prop-types@^15.5.6, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@~15.7.2: +promise-polyfill@^8.2.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-8.2.0.tgz#367394726da7561457aba2133c9ceefbd6267da0" + integrity sha512-k/TC0mIcPVF6yHhUvwAp7cvL6I2fFV7TzF1DuGPI8mBh4QQazf36xCKEHKTZKRysEoTQoQdKyP25J8MPJp7j5g== + +prop-types@15.7.2, prop-types@^15.5.6, prop-types@^15.5.8, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@~15.7.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== @@ -2275,14 +2327,6 @@ react-fontawesome@~1.7.1: dependencies: prop-types "^15.5.6" -react-google-login@~5.1.14: - version "5.1.25" - resolved "https://registry.yarnpkg.com/react-google-login/-/react-google-login-5.1.25.tgz#6bf5b4c8ce4e9e64a4c80eb14b80c9c6bd07bf9b" - integrity sha512-N7SZkjTEX9NsC3hywXs68SPJWAHo6M19Bs1OIFPirG0yomGF7KnbKjSqoiIfxz1V7fKAt8bkfBAzogwnGWYTeQ== - dependencies: - "@types/react" "*" - prop-types "^15.6.0" - react-hotkeys@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/react-hotkeys/-/react-hotkeys-2.0.0.tgz#a7719c7340cbba888b0e9184f806a9ec0ac2c53f" @@ -2463,11 +2507,6 @@ reduce-css-calc@^2.1.8: css-unit-converter "^1.1.1" postcss-value-parser "^3.3.0" -redux-localstorage@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/redux-localstorage/-/redux-localstorage-0.4.1.tgz#faf6d719c581397294d811473ffcedee065c933c" - integrity sha1-+vbXGcWBOXKU2BFHP/zt7gZckzw= - redux-logger@~3.0.6: version "3.0.6" resolved "https://registry.yarnpkg.com/redux-logger/-/redux-logger-3.0.6.tgz#f7555966f3098f3c88604c449cf0baf5778274bf" @@ -2977,6 +3016,11 @@ unbox-primitive@^1.0.0: has-symbols "^1.0.2" which-boxed-primitive "^1.0.2" +unfetch@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/unfetch/-/unfetch-4.2.0.tgz#7e21b0ef7d363d8d9af0fb929a5555f6ef97a3be" + integrity sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA== + unpipe@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" |
