From 9f51767981cba150e2ac9a7a4719f09c4fac57bf Mon Sep 17 00:00:00 2001 From: Audrius Mecionis Date: Tue, 15 Dec 2020 16:11:31 +0100 Subject: [PATCH 01/13] ui: split Signin component into two separate components addresses https://github.com/reanahub/reana/issues/469 --- reana-ui/src/components/App.js | 3 +- reana-ui/src/hooks.js | 32 +++++ reana-ui/src/pages/signin/Signin.js | 125 ++++++------------ reana-ui/src/pages/signin/Signup.js | 51 +++++++ .../pages/signin/components/SignContainer.js | 47 +++++++ .../SignContainer.module.scss} | 0 6 files changed, 170 insertions(+), 88 deletions(-) create mode 100644 reana-ui/src/hooks.js create mode 100644 reana-ui/src/pages/signin/Signup.js create mode 100644 reana-ui/src/pages/signin/components/SignContainer.js rename reana-ui/src/pages/signin/{Signin.module.scss => components/SignContainer.module.scss} (100%) diff --git a/reana-ui/src/components/App.js b/reana-ui/src/components/App.js index 2ed08b65..a85a84ea 100644 --- a/reana-ui/src/components/App.js +++ b/reana-ui/src/components/App.js @@ -15,6 +15,7 @@ import { Dimmer, Loader } from "semantic-ui-react"; import { getUserFetchError, isSignedIn, loadingUser } from "../selectors"; import Signin from "../pages/signin/Signin"; +import Signup from "../pages/signin/Signup"; import WorkflowList from "../pages/workflowList/WorkflowList"; import WorkflowDetails from "../pages/workflowDetails/WorkflowDetails"; import Profile from "../pages/profile/Profile"; @@ -62,7 +63,7 @@ export default function App() { /> (signedIn ? : )} + render={() => (signedIn ? : )} /> diff --git a/reana-ui/src/hooks.js b/reana-ui/src/hooks.js new file mode 100644 index 00000000..2a772946 --- /dev/null +++ b/reana-ui/src/hooks.js @@ -0,0 +1,32 @@ +/* + -*- coding: utf-8 -*- + + This file is part of REANA. + Copyright (C) 2020 CERN. + + REANA is free software; you can redistribute it and/or modify it + under the terms of the MIT License; see LICENSE file for more details. +*/ + +import { useDispatch } from "react-redux"; +import { useHistory, useLocation } from "react-router-dom"; + +export function useSubmit(action) { + const dispatch = useDispatch(); + const history = useHistory(); + const location = useLocation(); + + const handleSubmit = (event, formData, setFormData) => { + const { from } = location.state || { from: { pathname: "/" } }; + dispatch(action(formData)).then((res) => { + if (res.isAxiosError ?? false) { + setFormData({ ...formData, password: "" }); + } else { + history.replace(from); + } + }); + event.preventDefault(); + }; + + return handleSubmit; +} diff --git a/reana-ui/src/pages/signin/Signin.js b/reana-ui/src/pages/signin/Signin.js index 61f7feb7..c32d2ed0 100644 --- a/reana-ui/src/pages/signin/Signin.js +++ b/reana-ui/src/pages/signin/Signin.js @@ -9,110 +9,61 @@ */ import React, { useState } from "react"; -import { useDispatch, useSelector } from "react-redux"; -import { Button, Divider, Grid, Image, Segment } from "semantic-ui-react"; -import { Link, useHistory, useLocation } from "react-router-dom"; -import PropTypes from "prop-types"; +import { useSelector } from "react-redux"; +import { Button, Divider, Segment } from "semantic-ui-react"; +import { Link } from "react-router-dom"; -import { getConfig } from "../../selectors"; import SignForm from "./components/SignForm"; +import SignContainer from "./components/SignContainer"; import { api } from "../../config"; -import { userSignup, userSignin } from "../../actions"; -import LogoImg from "../../images/logo-reana.svg"; - -import styles from "./Signin.module.scss"; +import { getConfig } from "../../selectors"; +import { userSignin } from "../../actions"; +import { useSubmit } from "../../hooks"; -export default function Signin({ signup }) { +export default function Signin() { + const handleSubmit = useSubmit(userSignin); const config = useSelector(getConfig); const [formData, setFormData] = useState({ email: "", password: "" }); - const dispatch = useDispatch(); - const history = useHistory(); - const location = useLocation(); const handleClick = () => { window.location.href = api + "/oauth/login/cern"; }; - const handleSubmit = (event, action) => { - const { from } = location.state || { from: { pathname: "/" } }; - dispatch(action(formData)).then((res) => { - if (res.isAxiosError ?? false) { - setFormData({ ...formData, password: "" }); - } else { - history.replace(from); - } - }); - event.preventDefault(); - }; - const handleInputChange = (event) => { - const target = event.target; + const { target } = event; setFormData({ ...formData, [target.name]: target.value }); }; return ( -
- - - - - {config.cernSSO && !signup && ( - <> - - {config.localUsers && ( - - or - - )} - - )} + + + {config.cernSSO && ( + <> + {config.localUsers && ( - - handleSubmit(e, signup ? userSignup : userSignin) - } - formData={formData} - handleInputChange={handleInputChange} - /> + + or + )} - - {config.localUsers && ( - <> - {!signup ? ( -

- Don't have an account? Sign up -

- ) : ( -

- Already signed up? Go to Sign In -

- )} - - )} -
-
-
+ + )} + {config.localUsers && ( + handleSubmit(e, formData, setFormData)} + formData={formData} + handleInputChange={handleInputChange} + /> + )} + + {config.localUsers && ( +

+ If you do not have an account yet, please + Sign up here +

+ )} + ); } - -Signin.propTypes = { - /** Whether to show signup instead of signin form. */ - signup: PropTypes.bool, -}; - -Signin.defatultProps = { - signup: false, -}; diff --git a/reana-ui/src/pages/signin/Signup.js b/reana-ui/src/pages/signin/Signup.js new file mode 100644 index 00000000..26dbbe76 --- /dev/null +++ b/reana-ui/src/pages/signin/Signup.js @@ -0,0 +1,51 @@ +/* + -*- coding: utf-8 -*- + + This file is part of REANA. + Copyright (C) 2020 CERN. + + REANA is free software; you can redistribute it and/or modify it + under the terms of the MIT License; see LICENSE file for more details. +*/ + +import React, { useState } from "react"; +import { useSelector } from "react-redux"; +import { Segment } from "semantic-ui-react"; +import { Link } from "react-router-dom"; + +import SignForm from "./components/SignForm"; +import SignContainer from "./components/SignContainer"; +import { getConfig } from "../../selectors"; +import { userSignup } from "../../actions"; +import { useSubmit } from "../../hooks"; + +export default function Signup() { + const handleSubmit = useSubmit(userSignup); + const config = useSelector(getConfig); + const [formData, setFormData] = useState({ email: "", password: "" }); + + const handleInputChange = (event) => { + const { target } = event; + setFormData({ ...formData, [target.name]: target.value }); + }; + + return ( + + + {config.localUsers && ( + handleSubmit(e, formData, setFormData)} + formData={formData} + handleInputChange={handleInputChange} + /> + )} + + {config.localUsers && ( +

+ Already signed up? Go to Sign In +

+ )} +
+ ); +} diff --git a/reana-ui/src/pages/signin/components/SignContainer.js b/reana-ui/src/pages/signin/components/SignContainer.js new file mode 100644 index 00000000..babd141c --- /dev/null +++ b/reana-ui/src/pages/signin/components/SignContainer.js @@ -0,0 +1,47 @@ +/* + -*- coding: utf-8 -*- + + This file is part of REANA. + Copyright (C) 2020 CERN. + + REANA is free software; you can redistribute it and/or modify it + under the terms of the MIT License; see LICENSE file for more details. +*/ + +import React from "react"; +import { Grid, Image } from "semantic-ui-react"; +import PropTypes from "prop-types"; + +import LogoImg from "../../../images/logo-reana.svg"; + +import styles from "./SignContainer.module.scss"; + +export default function SignContainer({ children }) { + return ( +
+ + + + {children} + + +
+ ); +} + +SignContainer.propTypes = { + children: PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.node), + PropTypes.node, + ]).isRequired, +}; diff --git a/reana-ui/src/pages/signin/Signin.module.scss b/reana-ui/src/pages/signin/components/SignContainer.module.scss similarity index 100% rename from reana-ui/src/pages/signin/Signin.module.scss rename to reana-ui/src/pages/signin/components/SignContainer.module.scss From cbb8ce0a0ed49362ab2d1e916aad46e6f0e59c9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Vidal=20Garc=C3=ADa?= Date: Thu, 19 Nov 2020 18:29:55 +0100 Subject: [PATCH 02/13] ui: show more meaningful errors --- reana-ui/src/actions.js | 14 ++++++++++++-- reana-ui/src/components/App.js | 5 +++-- reana-ui/src/reducers.js | 5 ++++- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/reana-ui/src/actions.js b/reana-ui/src/actions.js index f8ca27d0..5713d1a9 100644 --- a/reana-ui/src/actions.js +++ b/reana-ui/src/actions.js @@ -103,8 +103,18 @@ export function loadUser() { .get(USER_INFO_URL, { withCredentials: true }) .then((resp) => dispatch({ type: USER_RECEIVED, ...resp.data })) .catch((err) => { - dispatch(errorActionCreator(err, USER_INFO_URL)); - dispatch({ type: USER_FETCH_ERROR }); + // 403 Forbidden, user token was revoked. + // 401 Unauthorized, user did not sign in, we fail silently. + let errorData; + if (err.response.status !== 401) { + const { + statusText, + data: { message }, + } = err.response; + errorData = { statusText, message }; + dispatch(errorActionCreator(err, USER_INFO_URL)); + } + dispatch({ type: USER_FETCH_ERROR, ...errorData }); }); }; } diff --git a/reana-ui/src/components/App.js b/reana-ui/src/components/App.js index a85a84ea..81dfc18e 100644 --- a/reana-ui/src/components/App.js +++ b/reana-ui/src/components/App.js @@ -9,6 +9,7 @@ */ import React from "react"; +import isEmpty from "lodash/isEmpty"; import { Redirect, BrowserRouter, Route, Switch } from "react-router-dom"; import { useSelector } from "react-redux"; import { Dimmer, Loader } from "semantic-ui-react"; @@ -45,8 +46,8 @@ export default function App() { const loading = useSelector(loadingUser); const signedIn = useSelector(isSignedIn); const error = useSelector(getUserFetchError); - if (error) { - return ; + if (!isEmpty(error)) { + return ; } return ( diff --git a/reana-ui/src/reducers.js b/reana-ui/src/reducers.js index 74a637f8..979f4fdf 100644 --- a/reana-ui/src/reducers.js +++ b/reana-ui/src/reducers.js @@ -130,9 +130,12 @@ const auth = (state = authInitialState, action) => { loadingUser: false, }; case USER_FETCH_ERROR: + const { type, ...errorData } = action; return { ...state, - error: { [USER_ERROR.fetch]: action.message }, + error: { + [USER_ERROR.fetch]: errorData, + }, loadingUser: false, }; case USER_SIGNEDOUT: From 451c8179a3c4df3125a4735438255d622d7be39f Mon Sep 17 00:00:00 2001 From: Audrius Mecionis Date: Thu, 17 Dec 2020 09:45:25 +0100 Subject: [PATCH 03/13] ui: control signup form visibility with configs addresses https://github.com/reanahub/reana/issues/469 --- reana-ui/src/components/App.js | 18 ++++++++++++++---- reana-ui/src/pages/signin/Signin.js | 8 +++++++- reana-ui/src/pages/signin/Signup.js | 2 +- reana-ui/src/reducers.js | 4 ++++ reana-ui/src/selectors.js | 2 ++ 5 files changed, 28 insertions(+), 6 deletions(-) diff --git a/reana-ui/src/components/App.js b/reana-ui/src/components/App.js index 81dfc18e..0c11f310 100644 --- a/reana-ui/src/components/App.js +++ b/reana-ui/src/components/App.js @@ -14,7 +14,13 @@ import { Redirect, BrowserRouter, Route, Switch } from "react-router-dom"; import { useSelector } from "react-redux"; import { Dimmer, Loader } from "semantic-ui-react"; -import { getUserFetchError, isSignedIn, loadingUser } from "../selectors"; +import { + getUserFetchError, + isSignedIn, + isSignupHidden, + loadingUser, + loadingConfig, +} from "../selectors"; import Signin from "../pages/signin/Signin"; import Signup from "../pages/signin/Signup"; import WorkflowList from "../pages/workflowList/WorkflowList"; @@ -43,13 +49,15 @@ function ProtectedRoute(props) { } export default function App() { - const loading = useSelector(loadingUser); + const userLoading = useSelector(loadingUser); + const configLoading = useSelector(loadingConfig); + const loading = userLoading || configLoading; const signedIn = useSelector(isSignedIn); + const signupHidden = useSelector(isSignupHidden); const error = useSelector(getUserFetchError); if (!isEmpty(error)) { return ; } - return ( {loading ? ( @@ -64,7 +72,9 @@ export default function App() { /> (signedIn ? : )} + render={() => + signedIn || signupHidden ? : + } /> diff --git a/reana-ui/src/pages/signin/Signin.js b/reana-ui/src/pages/signin/Signin.js index c32d2ed0..370816b8 100644 --- a/reana-ui/src/pages/signin/Signin.js +++ b/reana-ui/src/pages/signin/Signin.js @@ -58,7 +58,13 @@ export default function Signin() { /> )} - {config.localUsers && ( + {config.hideSignup && ( +

+ If you do not have an account yet, please contact + REANA administrators +

+ )} + {!config.hideSignup && config.localUsers && (

If you do not have an account yet, please Sign up here diff --git a/reana-ui/src/pages/signin/Signup.js b/reana-ui/src/pages/signin/Signup.js index 26dbbe76..29fc7fe9 100644 --- a/reana-ui/src/pages/signin/Signup.js +++ b/reana-ui/src/pages/signin/Signup.js @@ -43,7 +43,7 @@ export default function Signup() { {config.localUsers && (

- Already signed up? Go to Sign In + Already signed up? Go to Sign in

)} diff --git a/reana-ui/src/reducers.js b/reana-ui/src/reducers.js index 979f4fdf..8b005ce1 100644 --- a/reana-ui/src/reducers.js +++ b/reana-ui/src/reducers.js @@ -45,7 +45,9 @@ export const configInitialState = { forumURL: null, chatURL: null, cernSSO: false, + adminEmail: null, localUsers: false, + hideSignup: false, isLoaded: false, loading: false, }; @@ -100,7 +102,9 @@ const config = (state = configInitialState, action) => { forumURL: action.forum_url, chatURL: action.chat_url, cernSSO: action.cern_sso, + adminEmail: action.admin_email, localUsers: action.local_users, + hideSignup: action.hide_signup, isLoaded: true, loading: false, }; diff --git a/reana-ui/src/selectors.js b/reana-ui/src/selectors.js index 78ff483b..4744e357 100644 --- a/reana-ui/src/selectors.js +++ b/reana-ui/src/selectors.js @@ -16,6 +16,8 @@ export const getError = (state) => state.error; // Config export const getConfig = (state) => state.config; export const isConfigLoaded = (state) => state.config.isLoaded; +export const isSignupHidden = (state) => state.config.hideSignup; +export const loadingConfig = (state) => state.config.loading; // Auth export const isSignedIn = (state) => !!state.auth.email; From da189aa8735c567d81e13f70d1f2a3188aa4a582 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Vidal=20Garc=C3=ADa?= Date: Thu, 1 Oct 2020 12:53:08 +0200 Subject: [PATCH 04/13] ui: make notifications more generic partially addresses #123 --- reana-ui/src/actions.js | 17 +++++++-- reana-ui/src/components/Notification.js | 38 ++++++++++++------- .../pages/workflowDetails/WorkflowDetails.js | 3 ++ reana-ui/src/reducers.js | 25 ++++++++---- reana-ui/src/selectors.js | 4 +- 5 files changed, 61 insertions(+), 26 deletions(-) diff --git a/reana-ui/src/actions.js b/reana-ui/src/actions.js index 5713d1a9..4ffbc90f 100644 --- a/reana-ui/src/actions.js +++ b/reana-ui/src/actions.js @@ -20,7 +20,8 @@ import { } from "./selectors"; export const ERROR = "Error"; -export const CLEAR_ERROR = "Clear error"; +export const NOTIFICATION = "Notification"; +export const CLEAR_NOTIFICATION = "Clear notification"; export const CONFIG_FETCH = "Fetch app config info"; export const CONFIG_RECEIVED = "App config info received"; @@ -78,10 +79,20 @@ const WORKFLOW_FILES_URL = (id, { page = 1, size }) => { function errorActionCreator(error, name) { const { status, data } = error?.response; const { message } = data; - return { type: ERROR, name, status, message }; + return { + type: ERROR, + name, + status, + message, + header: "An error has occurred", + }; +} + +export function triggerNotification(header, message) { + return { type: NOTIFICATION, header, message }; } -export const clearError = { type: CLEAR_ERROR }; +export const clearNotification = { type: CLEAR_NOTIFICATION }; export function loadConfig() { return async (dispatch) => { diff --git a/reana-ui/src/components/Notification.js b/reana-ui/src/components/Notification.js index 428bbbb0..2d45a2a4 100644 --- a/reana-ui/src/components/Notification.js +++ b/reana-ui/src/components/Notification.js @@ -13,36 +13,44 @@ import { useDispatch, useSelector } from "react-redux"; import { Container, Message, Transition } from "semantic-ui-react"; import PropTypes from "prop-types"; -import { clearError } from "../actions"; -import { getError } from "../selectors"; +import { clearNotification } from "../actions"; +import { getNotification } from "../selectors"; import styles from "./Notification.module.scss"; const AUTO_CLOSE_TIMEOUT = 16000; -export default function Notification({ icon, header, message, closable }) { +export default function Notification({ + icon, + header, + message, + closable, + error, + success, +}) { const dispatch = useDispatch(); - const error = useSelector(getError); + const notification = useSelector(getNotification); const timer = useRef(null); - const hide = () => dispatch(clearError); - const visible = message || error ? true : false; + const hide = () => dispatch(clearNotification); + const visible = message || notification ? true : false; + const actionIcon = notification?.isError ? "warning sign" : "info circle"; if (closable && visible) { clearTimeout(timer.current); timer.current = setTimeout(() => hide(), AUTO_CLOSE_TIMEOUT); } - return ( @@ -54,11 +62,15 @@ Notification.propTypes = { header: PropTypes.string, message: PropTypes.string, closable: PropTypes.bool, + error: PropTypes.bool, + success: PropTypes.bool, }; Notification.defaultProps = { - icon: "warning sign", - header: "An error has occurred", + icon: null, + header: null, message: null, closable: true, + error: false, + success: false, }; diff --git a/reana-ui/src/pages/workflowDetails/WorkflowDetails.js b/reana-ui/src/pages/workflowDetails/WorkflowDetails.js index 1b36dda9..1a508622 100644 --- a/reana-ui/src/pages/workflowDetails/WorkflowDetails.js +++ b/reana-ui/src/pages/workflowDetails/WorkflowDetails.js @@ -59,8 +59,11 @@ function WorkflowDetails() { if (!workflow) { return ( ); } diff --git a/reana-ui/src/reducers.js b/reana-ui/src/reducers.js index 8b005ce1..00894a9c 100644 --- a/reana-ui/src/reducers.js +++ b/reana-ui/src/reducers.js @@ -11,7 +11,8 @@ import { combineReducers } from "redux"; import { ERROR, - CLEAR_ERROR, + NOTIFICATION, + CLEAR_NOTIFICATION, CONFIG_FETCH, CONFIG_RECEIVED, CONFIG_ERROR, @@ -35,7 +36,7 @@ import { } from "./actions"; import { USER_ERROR } from "./errors"; -const errorInitialState = null; +const notificationInitialState = null; export const configInitialState = { announcement: null, @@ -76,13 +77,21 @@ const detailsInitialState = { loadingDetails: false, }; -const error = (state = errorInitialState, action) => { - const { name, status, message } = action; +const notification = (state = notificationInitialState, action) => { + const { name, status, message, header } = action; switch (action.type) { case ERROR: - return { ...state, name, status, message }; - case CLEAR_ERROR: - return errorInitialState; + case NOTIFICATION: + return { + ...state, + name, + status, + message, + header, + isError: action.type === ERROR, + }; + case CLEAR_NOTIFICATION: + return notificationInitialState; default: return state; } @@ -243,7 +252,7 @@ const details = (state = detailsInitialState, action) => { }; const reanaApp = combineReducers({ - error, + notification, config, auth, workflows, diff --git a/reana-ui/src/selectors.js b/reana-ui/src/selectors.js index 4744e357..9600a2e9 100644 --- a/reana-ui/src/selectors.js +++ b/reana-ui/src/selectors.js @@ -10,8 +10,8 @@ import { USER_ERROR } from "./errors"; -// Error -export const getError = (state) => state.error; +// Notification +export const getNotification = (state) => state.notification; // Config export const getConfig = (state) => state.config; From e4b508f0fde5530f16ffe07c362c757c4f3349e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Vidal=20Garc=C3=ADa?= Date: Fri, 18 Dec 2020 18:45:39 +0100 Subject: [PATCH 05/13] auth: show proper notifications when signing in/up - sign up: success, remember to confirm your email. - sign in: cannot sign in, email confirmation needed. --- reana-ui/src/actions.js | 15 ++++++++++++++- .../src/pages/signin/components/SignContainer.js | 3 +++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/reana-ui/src/actions.js b/reana-ui/src/actions.js index 4ffbc90f..bec96443 100644 --- a/reana-ui/src/actions.js +++ b/reana-ui/src/actions.js @@ -141,10 +141,23 @@ function userSignFactory(initAction, succeedAction, actionURL, body) { .then((resp) => { dispatch({ type: succeedAction }); dispatch(loadUser()); + if (initAction === USER_SIGNUP) { + dispatch( + triggerNotification( + "Success!", + "User registered. Please confirm your email by clicking on the link we sent you." + ) + ); + } return resp; }) .catch((err) => { - dispatch({ type: USER_SIGN_ERROR, ...err.response.data }); + // validation errors + if (err.response.data.errors) { + dispatch({ type: USER_SIGN_ERROR, ...err.response.data }); + } else { + dispatch(errorActionCreator(err, USER_SIGN_ERROR)); + } return err; }); }; diff --git a/reana-ui/src/pages/signin/components/SignContainer.js b/reana-ui/src/pages/signin/components/SignContainer.js index babd141c..edb4dc03 100644 --- a/reana-ui/src/pages/signin/components/SignContainer.js +++ b/reana-ui/src/pages/signin/components/SignContainer.js @@ -12,6 +12,8 @@ import React from "react"; import { Grid, Image } from "semantic-ui-react"; import PropTypes from "prop-types"; +import Notification from "../../../components/Notification"; + import LogoImg from "../../../images/logo-reana.svg"; import styles from "./SignContainer.module.scss"; @@ -19,6 +21,7 @@ import styles from "./SignContainer.module.scss"; export default function SignContainer({ children }) { return (
+ Date: Mon, 21 Dec 2020 21:25:26 +0100 Subject: [PATCH 06/13] auth: add route to handle email confirmation --- reana-ui/src/actions.js | 35 ++++++++++++++++++++++++++++ reana-ui/src/components/App.js | 2 ++ reana-ui/src/pages/signin/Confirm.js | 27 +++++++++++++++++++++ 3 files changed, 64 insertions(+) create mode 100644 reana-ui/src/pages/signin/Confirm.js diff --git a/reana-ui/src/actions.js b/reana-ui/src/actions.js index bec96443..c37f69de 100644 --- a/reana-ui/src/actions.js +++ b/reana-ui/src/actions.js @@ -40,6 +40,9 @@ export const USER_SIGNEDOUT = "User signed out"; export const USER_REQUEST_TOKEN = "Request user token"; export const USER_TOKEN_REQUESTED = "User token requested"; export const USER_TOKEN_ERROR = "User token error"; +export const USER_EMAIL_CONFIRMATION = "User request email confirmation"; +export const USER_EMAIL_CONFIRMED = "User email confirmed"; +export const USER_EMAIL_CONFIRMATION_ERROR = "User email confirmation error"; export const WORKFLOWS_FETCH = "Fetch workflows info"; export const WORKFLOWS_RECEIVED = "Workflows info received"; @@ -58,6 +61,7 @@ const USER_SIGNUP_URL = `${api}/api/register`; const USER_SIGNIN_URL = `${api}/api/login`; const USER_SIGNOUT_URL = `${api}/api/logout`; const USER_REQUEST_TOKEN_URL = `${api}/api/token`; +const USER_CONFIRM_EMAIL_URL = `${api}/api/confirm-email`; const WORKFLOWS_URL = ({ page = 1, size }) => { let url = `${api}/api/workflows`; if (size) { @@ -198,6 +202,37 @@ export function requestToken() { }; } +export function confirmUserEmail(token) { + return async (dispatch) => { + dispatch({ type: USER_EMAIL_CONFIRMATION }); + return await axios + .post( + USER_CONFIRM_EMAIL_URL, + { token: token }, + { + withCredentials: true, + headers: { "Content-Type": "application/json" }, + } + ) + .then((resp) => { + dispatch({ type: USER_EMAIL_CONFIRMED }); + dispatch( + triggerNotification( + "Success!", + "User email confirmed. You can sign in now." + ) + ); + return resp; + }) + .catch((err) => { + // adapt error format (remove array) + err.response.data.message = err.response.data.message[0]; + dispatch(errorActionCreator(err, USER_EMAIL_CONFIRMATION_ERROR)); + return err; + }); + }; +} + export function fetchWorkflows(pagination) { return async (dispatch) => { dispatch({ type: WORKFLOWS_FETCH }); diff --git a/reana-ui/src/components/App.js b/reana-ui/src/components/App.js index 0c11f310..71163dc2 100644 --- a/reana-ui/src/components/App.js +++ b/reana-ui/src/components/App.js @@ -21,6 +21,7 @@ import { loadingUser, loadingConfig, } from "../selectors"; +import Confirm from "../pages/signin/Confirm"; import Signin from "../pages/signin/Signin"; import Signup from "../pages/signin/Signup"; import WorkflowList from "../pages/workflowList/WorkflowList"; @@ -76,6 +77,7 @@ export default function App() { signedIn || signupHidden ? : } /> + diff --git a/reana-ui/src/pages/signin/Confirm.js b/reana-ui/src/pages/signin/Confirm.js new file mode 100644 index 00000000..fdcbb417 --- /dev/null +++ b/reana-ui/src/pages/signin/Confirm.js @@ -0,0 +1,27 @@ +/* + -*- coding: utf-8 -*- + + This file is part of REANA. + Copyright (C) 2020 CERN. + + REANA is free software; you can redistribute it and/or modify it + under the terms of the MIT License; see LICENSE file for more details. +*/ + +import { useEffect } from "react"; +import { useDispatch } from "react-redux"; +import { useHistory, useParams } from "react-router-dom"; + +import { confirmUserEmail } from "../../actions"; + +export default function Confirm() { + const { token } = useParams(); + const dispatch = useDispatch(); + const history = useHistory(); + + useEffect(() => { + dispatch(confirmUserEmail(token)).then(() => history.replace("/")); + }, [dispatch, token, history]); + + return null; +} From 4dbe6199bfbcc1ee117b1a082c69362c332d0dd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Vidal=20Garc=C3=ADa?= Date: Tue, 22 Dec 2020 13:08:16 +0100 Subject: [PATCH 07/13] auth: display notification when signing up via SSO --- reana-ui/src/pages/signin/Signin.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/reana-ui/src/pages/signin/Signin.js b/reana-ui/src/pages/signin/Signin.js index 370816b8..0fb70c43 100644 --- a/reana-ui/src/pages/signin/Signin.js +++ b/reana-ui/src/pages/signin/Signin.js @@ -9,7 +9,7 @@ */ import React, { useState } from "react"; -import { useSelector } from "react-redux"; +import { useDispatch, useSelector } from "react-redux"; import { Button, Divider, Segment } from "semantic-ui-react"; import { Link } from "react-router-dom"; @@ -17,16 +17,26 @@ import SignForm from "./components/SignForm"; import SignContainer from "./components/SignContainer"; import { api } from "../../config"; import { getConfig } from "../../selectors"; -import { userSignin } from "../../actions"; +import { triggerNotification, userSignin } from "../../actions"; import { useSubmit } from "../../hooks"; export default function Signin() { const handleSubmit = useSubmit(userSignin); const config = useSelector(getConfig); + const dispatch = useDispatch(); const [formData, setFormData] = useState({ email: "", password: "" }); const handleClick = () => { window.location.href = api + "/oauth/login/cern"; + // FIXME: We assume that the sign-up went successfully but we actually don't know. + // We should upgrade Invenio-OAuthClient to latest version that supports REST apps + // and adapt the whole workflow. + dispatch( + triggerNotification( + "Success!", + "User registered. Please confirm your email by clicking on the link we sent you." + ) + ); }; const handleInputChange = (event) => { From 81d8329f9566e140d63f7eea021956688c8a03ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Vidal=20Garc=C3=ADa?= Date: Thu, 7 Jan 2021 14:42:11 +0100 Subject: [PATCH 08/13] auth: display server confirmation success message --- reana-ui/src/actions.js | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/reana-ui/src/actions.js b/reana-ui/src/actions.js index c37f69de..797ca74b 100644 --- a/reana-ui/src/actions.js +++ b/reana-ui/src/actions.js @@ -216,19 +216,14 @@ export function confirmUserEmail(token) { ) .then((resp) => { dispatch({ type: USER_EMAIL_CONFIRMED }); - dispatch( - triggerNotification( - "Success!", - "User email confirmed. You can sign in now." - ) - ); - return resp; + dispatch(triggerNotification("Success!", resp.data?.message)); }) .catch((err) => { - // adapt error format (remove array) - err.response.data.message = err.response.data.message[0]; + // adapt error format coming from invenio-accounts (remove array) + if (Array.isArray(err.response?.data?.message)) { + err.response.data.message = err.response?.data?.message[0]; + } dispatch(errorActionCreator(err, USER_EMAIL_CONFIRMATION_ERROR)); - return err; }); }; } From 751d49172d007e55aadf58b486db66776ba530e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Vidal=20Garc=C3=ADa?= Date: Thu, 7 Jan 2021 14:44:07 +0100 Subject: [PATCH 09/13] auth: clear notifications before sign in/up --- reana-ui/src/actions.js | 1 + 1 file changed, 1 insertion(+) diff --git a/reana-ui/src/actions.js b/reana-ui/src/actions.js index 797ca74b..17046d7e 100644 --- a/reana-ui/src/actions.js +++ b/reana-ui/src/actions.js @@ -143,6 +143,7 @@ function userSignFactory(initAction, succeedAction, actionURL, body) { headers: { "Content-Type": "application/json" }, }) .then((resp) => { + dispatch(clearNotification); dispatch({ type: succeedAction }); dispatch(loadUser()); if (initAction === USER_SIGNUP) { From 11e08707a9c76d3b2d9f847574af70216fad00da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Vidal=20Garc=C3=ADa?= Date: Fri, 8 Jan 2021 16:41:35 +0100 Subject: [PATCH 10/13] auth: adjust success messages to user confirmation --- reana-ui/src/actions.js | 12 ++++++++++-- reana-ui/src/pages/signin/Signin.js | 6 +++++- reana-ui/src/reducers.js | 2 ++ 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/reana-ui/src/actions.js b/reana-ui/src/actions.js index 17046d7e..217efdb8 100644 --- a/reana-ui/src/actions.js +++ b/reana-ui/src/actions.js @@ -14,6 +14,7 @@ import axios from "axios"; import { api } from "./config"; import { parseWorkflows, parseLogs, parseFiles } from "./util"; import { + getConfig, getWorkflow, getWorkflowLogs, getWorkflowSpecification, @@ -135,7 +136,10 @@ export function loadUser() { } function userSignFactory(initAction, succeedAction, actionURL, body) { - return async (dispatch) => { + return async (dispatch, getStore) => { + const state = getStore(); + const { userConfirmation } = getConfig(state); + dispatch({ type: initAction }); return await axios .post(actionURL, body, { @@ -150,7 +154,11 @@ function userSignFactory(initAction, succeedAction, actionURL, body) { dispatch( triggerNotification( "Success!", - "User registered. Please confirm your email by clicking on the link we sent you." + `User registered. ${ + userConfirmation + ? "Please confirm your email by clicking on the link we sent you." + : "" + }` ) ); } diff --git a/reana-ui/src/pages/signin/Signin.js b/reana-ui/src/pages/signin/Signin.js index 0fb70c43..cd40ba77 100644 --- a/reana-ui/src/pages/signin/Signin.js +++ b/reana-ui/src/pages/signin/Signin.js @@ -34,7 +34,11 @@ export default function Signin() { dispatch( triggerNotification( "Success!", - "User registered. Please confirm your email by clicking on the link we sent you." + `User registered. ${ + config.userConfirmation + ? "Please confirm your email by clicking on the link we sent you." + : "" + }` ) ); }; diff --git a/reana-ui/src/reducers.js b/reana-ui/src/reducers.js index 00894a9c..8f31db33 100644 --- a/reana-ui/src/reducers.js +++ b/reana-ui/src/reducers.js @@ -50,6 +50,7 @@ export const configInitialState = { localUsers: false, hideSignup: false, isLoaded: false, + userConfirmation: true, loading: false, }; @@ -114,6 +115,7 @@ const config = (state = configInitialState, action) => { adminEmail: action.admin_email, localUsers: action.local_users, hideSignup: action.hide_signup, + userConfirmation: action.user_confirmation, isLoaded: true, loading: false, }; From 5b14feb985f9bc1996e4a952cb07cd45581ef6d0 Mon Sep 17 00:00:00 2001 From: Audrius Mecionis Date: Tue, 2 Feb 2021 13:25:45 +0100 Subject: [PATCH 11/13] ui: added privacy policy page with RoPo content closes #172 --- reana-ui/src/components/App.js | 2 + reana-ui/src/components/Footer.js | 8 +- .../src/pages/privacyNotice/PrivacyNotice.js | 326 ++++++++++++++++++ .../privacyNotice/PrivacyNotice.module.scss | 16 + reana-ui/src/reducers.js | 2 + 5 files changed, 353 insertions(+), 1 deletion(-) create mode 100644 reana-ui/src/pages/privacyNotice/PrivacyNotice.js create mode 100644 reana-ui/src/pages/privacyNotice/PrivacyNotice.module.scss diff --git a/reana-ui/src/components/App.js b/reana-ui/src/components/App.js index 71163dc2..70b07154 100644 --- a/reana-ui/src/components/App.js +++ b/reana-ui/src/components/App.js @@ -26,6 +26,7 @@ import Signin from "../pages/signin/Signin"; import Signup from "../pages/signin/Signup"; import WorkflowList from "../pages/workflowList/WorkflowList"; import WorkflowDetails from "../pages/workflowDetails/WorkflowDetails"; +import PrivacyNotice from "../pages/privacyNotice/PrivacyNotice"; import Profile from "../pages/profile/Profile"; import Error from "./Error"; @@ -78,6 +79,7 @@ export default function App() { } /> + diff --git a/reana-ui/src/components/Footer.js b/reana-ui/src/components/Footer.js index d28f6ff0..68eb06b9 100644 --- a/reana-ui/src/components/Footer.js +++ b/reana-ui/src/components/Footer.js @@ -9,8 +9,9 @@ */ import React from "react"; -import { Icon } from "semantic-ui-react"; import { useSelector } from "react-redux"; +import { Link } from "react-router-dom"; +import { Icon } from "semantic-ui-react"; import { getConfig } from "../selectors"; @@ -22,6 +23,11 @@ export default function Footer() {