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 (
<>
+ {step === 2 && + !forgotPassword.request && + !forgotPassword.success ? ( +
{t('content.login.forgotPassword')}
) : null} + {step === 2 && forgotPassword.request ? ( ++ {t('content.login.forgotPasswordSuccessShort')} +
+ ) : null}