diff --git a/web/api/rest/routes.js b/web/api/rest/routes.js index a70ab65..351aaba 100644 --- a/web/api/rest/routes.js +++ b/web/api/rest/routes.js @@ -46,7 +46,9 @@ module.exports = (app) => { */ app.post('/api/verification', apiKeyAuth, async (req, res) => { const { email, givenName, familyName } = req.body; + let link; let results; + let user; if (!email || !validator.validate(email)) { return res.status(400).json({ message: 'Email missing' }); @@ -57,20 +59,16 @@ module.exports = (app) => { // If no user, create user if (results.totalResults === 0) { - const user = await AppIdManagement.createUser( - email, - givenName, - familyName, - ); - - const token = await jwt.encode({ id: user.id }); + user = await AppIdManagement.createUser(email, givenName, familyName); - const link = `${dashboardURL}/onboard?${qs.stringify({ - token, + link = `${dashboardURL}/onboard?${qs.stringify({ + token: await jwt.encode({ id: user.id }), })}`; return res.json({ verified: false, + new: true, + active: false, givenName, familyName, email, @@ -79,17 +77,26 @@ module.exports = (app) => { }); } + user = results.Resources[0]; + + // If user has account, but has not finished + // onboarding, generate new link & token + if (!user.active) { + link = `${dashboardURL}/onboard?${qs.stringify({ + token: await jwt.encode({ id: user.id }), + })}`; + } + + await jwt.encode({ id: user.id }); return res.json({ verified: true, - givenName: results.Resources[0].name - ? results.Resources[0].name.givenName - : null, - familyName: results.Resources[0].name - ? results.Resources[0].name.familyName - : null, - uuid: results.Resources[0].id, + new: false, + active: user.active, + givenName: user.name ? user.name.givenName : null, + familyName: user.name ? user.name.familyName : null, + uuid: user.id, email, - link: null, + link: !user.active ? link : null, }); } catch (e) { return res.status(500).json({ message: 'Error verifying user' }); @@ -255,6 +262,31 @@ module.exports = (app) => { })(req, res, next); }); + app.post('/api/forgot-password', async (req, res) => { + const { email } = req.body; + + if (!email) { + return res.status(400).json({ message: 'Missing email.' }); + } + + try { + await AppIdManagement.forgotPassword(email); + + return res.json({ success: true }); + } catch (e) { + // To not indicate whether a user exists in db, + // a success message is returned if the user is not found + if (e.message === 'user_not_found') { + return res.json({ success: true }); + } + + return res.status(500).json({ + message: 'Error processing forgot password.', + clientCode: 'error_forgot_password', + }); + } + }); + app.post('/api/logout', (req, res) => { req.session.destroy((err) => { if (err) { diff --git a/web/api/services/appId.js b/web/api/services/appId.js index bbbfe88..2f6712d 100644 --- a/web/api/services/appId.js +++ b/web/api/services/appId.js @@ -242,6 +242,42 @@ class AppIdManagement { return json; } + async forgotPassword(email) { + const iam = await this.iamAuth; + + const data = JSON.stringify({ + user: email, + }); + + const response = await fetch( + `https://us-south.appid.cloud.ibm.com/management/v4/${TENET_ID}/cloud_directory/forgot_password`, + { + method: 'POST', + headers: { + // eslint-disable-next-line quote-props + Accept: 'application/json', + // eslint-disable-next-line quote-props + Authorization: `Bearer ${iam.token}`, + 'Content-Type': 'application/json', + }, + body: data, + }, + ); + + if (!response.ok) { + const error = await response.json(); + console.error(error); + + if (error.error === 'user not found') { + throw new Error('user_not_found'); + } + + throw new Error('forgot_password_fail'); + } + + return response.ok; + } + async removeUser(id) { const iam = await this.iamAuth; diff --git a/web/client/src/components/LoginInput/index.js b/web/client/src/components/LoginInput/index.js index 7f80780..4239ec0 100644 --- a/web/client/src/components/LoginInput/index.js +++ b/web/client/src/components/LoginInput/index.js @@ -18,6 +18,8 @@ const LoginInput = ({ initLogin, loginId, setError, + setForgotPassword, + forgotPassword, }) => { const { t } = useContext(AppContext) const [attemptedSubmit, setAttemptedSubmit] = useState(false) @@ -27,6 +29,12 @@ const LoginInput = ({ const returnToEmail = () => { setError('') setStep(1) + setForgotPassword({ + email: '', + request: false, + success: false, + error: '', + }) } useEffect(() => { @@ -52,6 +60,10 @@ const LoginInput = ({ onSubmit={(values, { setSubmitting }) => { if (step === 1) { setLoginId(values.openeewId) + setForgotPassword({ + ...forgotPassword, + email: values.openeewId, + }) setStep(2) } else { diff --git a/web/client/src/components/SensorsInformationSidePanel/SensorsInformationSidePanel.scss b/web/client/src/components/SensorsInformationSidePanel/SensorsInformationSidePanel.scss index 24a18d9..e1b8881 100644 --- a/web/client/src/components/SensorsInformationSidePanel/SensorsInformationSidePanel.scss +++ b/web/client/src/components/SensorsInformationSidePanel/SensorsInformationSidePanel.scss @@ -4,7 +4,7 @@ bottom: 0; right: 0; padding: $layout-02 $layout-03; - background-color: #2C2C2C; + background-color: #2c2c2c; border-left: 1px solid #515151; .bx--toast-notification { @@ -12,14 +12,14 @@ color: #fff; background-color: #393939; - border-left: 3px solid #6D96DD; + border-left: 3px solid #6d96dd; .bx--toast-notification__icon { - fill: #6D96DD; + fill: #6d96dd; } a { - color: #6D96DD; + color: #6d96dd; } } @@ -33,7 +33,7 @@ } } .tag-owner { - margin-Left: 40px; + margin-left: 40px; } } @@ -78,9 +78,9 @@ text-align: left; cursor: pointer; - &[data-testing-sensor="true"] { - color: #FF5D5D; - border: 1px solid #FF5D5D; + &[data-testing-sensor='true'] { + color: #ff5d5d; + border: 1px solid #ff5d5d; } } @@ -91,10 +91,10 @@ .sensors-side-panel__restarting { display: flex; align-items: center; - position:absolute; + position: absolute; left: calc(24px); margin-top: 10px; - color: #9C9C9C; + color: #9c9c9c; p { margin-left: $spacing-02; diff --git a/web/client/src/content/Login/Login.scss b/web/client/src/content/Login/Login.scss index 92149a5..9a784d0 100644 --- a/web/client/src/content/Login/Login.scss +++ b/web/client/src/content/Login/Login.scss @@ -38,7 +38,8 @@ min-height: 1.5rem; } -.login__container .bx--inline-notification--error { +.login__container .bx--inline-notification--error, +.login__container .bx--inline-notification--success { background: #393939; color: white; } @@ -50,6 +51,16 @@ margin-right: $spacing-03; } +.login__forgotPasswordLoading { + position: relative; + left: -5px; +} + +.login__forgotPasswordSuccess span { + @include carbon--type-style('body-short-01'); + color: #5ec07a; +} + .login__supportingContainer { min-height: $layout-05; } @@ -105,4 +116,8 @@ .login__supportingContainer { min-height: $layout-06 / 2; } + + .bx--inline-notification { + max-width: none; + } } diff --git a/web/client/src/content/Login/index.js b/web/client/src/content/Login/index.js index 2849f54..2755550 100644 --- a/web/client/src/content/Login/index.js +++ b/web/client/src/content/Login/index.js @@ -1,6 +1,6 @@ -import React, { useContext, useCallback, useState, useEffect } from 'react' +import React, { useContext, useState, useEffect } from 'react' import { motion, AnimatePresence } from 'framer-motion' -import { InlineNotification } from 'carbon-components-react' +import { InlineNotification, InlineLoading } from 'carbon-components-react' import AppContext from '../../context/app' import LoginInput from '../../components/LoginInput' @@ -16,43 +16,16 @@ const Login = ({ history }) => { const [error, setError] = useState('') const [step, setStep] = useState(1) const [loginId, setLoginId] = useState('') + const [forgotPassword, setForgotPassword] = useState({ + email: '', + request: false, + success: false, + error: '', + }) const [activeAnimation, setActiveAnimation] = useState(() => window.innerWidth < 672 ? animation.mobile : animation.desktop ) - const initLogin = useCallback( - /** - * Init login and set user on success. Catch errors - * handled by Auth client and sets error state. - * @param {string} password - * @param {function} setSubmitting - */ - async (password, setSubmitting) => { - setSubmitting(true) - setError('') - - try { - const user = await AuthClient.login(loginId, password) - - setSubmitting(false) - - setCurrentUser({ - isAuth: true, - email: user.email, - firstName: user.givenName, - lastName: user.familyName, - }) - - return history.push('/events') - } catch (e) { - setSubmitting(false) - - return setError(e) - } - }, - [loginId, setCurrentUser, history] - ) - useEffect(() => { window.addEventListener('resize', () => setActiveAnimation( @@ -63,6 +36,58 @@ const Login = ({ history }) => { ) }, []) + /** + * Init login and set user on success. Catch errors + * handled by Auth client and sets error state. + * @param {string} password + * @param {function} setSubmitting + */ + const initLogin = async (password, setSubmitting) => { + setSubmitting(true) + setError('') + + try { + const user = await AuthClient.login(loginId, password) + + setSubmitting(false) + + setCurrentUser({ + isAuth: true, + email: user.email, + firstName: user.givenName, + lastName: user.familyName, + }) + + return history.push('/events') + } catch (e) { + setSubmitting(false) + + return setError(e) + } + } + + const initForgotPassword = async () => { + setForgotPassword({ ...forgotPassword, request: true }) + + try { + await AuthClient.resetPassword(forgotPassword.email) + + setForgotPassword({ + ...forgotPassword, + request: false, + success: true, + error: '', + }) + } catch (error) { + setForgotPassword({ + ...forgotPassword, + request: false, + success: false, + error, + }) + } + } + return ( <>
@@ -83,11 +108,31 @@ const Login = ({ history }) => { {...activeAnimation} >
- {step === 2 ? ( -

+ {step === 2 && + !forgotPassword.request && + !forgotPassword.success ? ( +

{t('content.login.forgotPassword')}

) : null} + {step === 2 && forgotPassword.request ? ( + + ) : null} + {step === 2 && forgotPassword.success ? ( +

+ {t('content.login.forgotPasswordSuccessShort')} +

+ ) : null}
{ initLogin={initLogin} loginId={loginId} setError={setError} + forgotPassword={forgotPassword} + setForgotPassword={setForgotPassword} /> {error ? ( { hideCloseButton={true} /> ) : null} + + {forgotPassword.success ? ( + {t(`content.login.forgotPasswordSuccessContent`)} + } + tabIndex={0} + title={t('content.login.forgotPasswordSuccessHeader')} + hideCloseButton={true} + /> + ) : null} + + {forgotPassword.error ? ( + {t(`content.login.forgotPasswordErrorContent`)} + } + tabIndex={0} + title={t('content.login.forgotPasswordErrorHeader')} + hideCloseButton={true} + /> + ) : null} diff --git a/web/client/src/locales/en.json b/web/client/src/locales/en.json index 901d29e..20eecdd 100644 --- a/web/client/src/locales/en.json +++ b/web/client/src/locales/en.json @@ -69,6 +69,12 @@ "login": { "title": "Log in to", "forgotPassword": "Forgot Password?", + "forgotPasswordSuccessShort": "Password reset sent", + "forgotPasswordSuccessHeader": "Check your email", + "forgotPasswordSuccessContent": "If there is an account associated with this address, you will receive an email that will allow you to update your password.", + "forgotPasswordLoading": "Sending request", + "forgotPasswordErrorContent": "There was error processing the forgot password request", + "forgotPasswordErrorHeader": "Reset Password Error", "continue": "Continue", "rememberMe": "Remember Me", "loggingIn": "Logging in", diff --git a/web/client/src/rest/auth.js b/web/client/src/rest/auth.js index bef3cc4..c32238e 100644 --- a/web/client/src/rest/auth.js +++ b/web/client/src/rest/auth.js @@ -87,6 +87,32 @@ class Auth { }) } + /** + * Init request for password reset + */ + resetPassword(email) { + return new Promise((resolve, reject) => { + fetch(`${env.base_url}/api/forgot-password`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + email, + }), + }) + .then(this.handleError) + .then((response) => response.json()) + .then(() => { + return resolve(true) + }) + .catch((e) => { + return reject(e.message) + }) + }) + } + /** * Retrieves a user using a JWT as authentication. Used * for onboarding a new account.