diff --git a/Makefile b/Makefile index efcb32a8..e8305c9d 100644 --- a/Makefile +++ b/Makefile @@ -69,6 +69,9 @@ lint-react: lint-react-fix: $(NPM) lint-fix +mail: + docker run -d -e "MH_STORAGE=maildir" -v /tmp/maildir:/maildir -p 1025:1025 -p 8025:8025 mailhog/mailhog + migrate-db: $(FLASK) db migrate --directory $(MIGRATIONS) diff --git a/fittrackee_client/src/actions/user.js b/fittrackee_client/src/actions/user.js index 706a7857..95ede0d6 100644 --- a/fittrackee_client/src/actions/user.js +++ b/fittrackee_client/src/actions/user.js @@ -46,15 +46,27 @@ export const getProfile = () => dispatch => throw error }) -export const loginOrRegister = (target, formData) => dispatch => - FitTrackeeApi.loginOrRegister(target, formData) +export const loginOrRegisterOrPasswordReset = (target, formData) => dispatch => + FitTrackeeApi.loginOrRegisterOrPasswordReset(target, formData) .then(ret => { if (ret.status === 'success') { - window.localStorage.setItem('authToken', ret.auth_token) - if (target === 'register') { - dispatch(getAppData('config')) + if (target === 'password/reset-request') { + return history.push({ + pathname: '/password-reset/sent', + }) + } + if (target === 'password/update') { + return history.push({ + pathname: '/updated-password', + }) + } + if (target === 'login' || target === 'register') { + window.localStorage.setItem('authToken', ret.auth_token) + if (target === 'register') { + dispatch(getAppData('config')) + } + return dispatch(getProfile()) } - return dispatch(getProfile()) } return dispatch(AuthError(ret.message)) }) @@ -62,9 +74,12 @@ export const loginOrRegister = (target, formData) => dispatch => throw error }) -const RegisterFormControl = formData => { +const RegisterFormControl = (formData, onlyPasswords = false) => { const errMsg = [] - if (formData.username.length < 3 || formData.username.length > 12) { + if ( + !onlyPasswords && + (formData.username.length < 3 || formData.username.length > 12) + ) { errMsg.push('3 to 12 characters required for username.') } if (formData.password !== formData.password_conf) { @@ -77,13 +92,13 @@ const RegisterFormControl = formData => { } export const handleUserFormSubmit = (formData, formType) => dispatch => { - if (formType === 'register') { - const ret = RegisterFormControl(formData) + if (formType === 'register' || formType === 'password/update') { + const ret = RegisterFormControl(formData, formType === 'password/update') if (ret.length > 0) { return dispatch(AuthErrors(generateIds(ret))) } } - return dispatch(loginOrRegister(formType, formData)) + return dispatch(loginOrRegisterOrPasswordReset(formType, formData)) } export const handleProfileFormSubmit = formData => dispatch => { diff --git a/fittrackee_client/src/components/App.css b/fittrackee_client/src/components/App.css index 2b8fb6d7..62d13fe6 100644 --- a/fittrackee_client/src/components/App.css +++ b/fittrackee_client/src/components/App.css @@ -312,7 +312,6 @@ label { .dropdown-item { cursor: default; font-size: 0.9em; - } .dropdown-item-selected { @@ -437,6 +436,12 @@ label { text-align: center; } +.password-forget { + margin: 10px; + font-size: .9em; + font-style: italic; +} + .radioLabel { text-align: center; } @@ -467,6 +472,14 @@ label { pointer-events: none; } +.svg-icon { + fill: #405976; + height: 70px; + margin-left: auto; + margin-right: auto; + width: 70px; +} + .time-frames { align-items: center; display: inline-flex; diff --git a/fittrackee_client/src/components/App.jsx b/fittrackee_client/src/components/App.jsx index c97c9ff4..e857db53 100644 --- a/fittrackee_client/src/components/App.jsx +++ b/fittrackee_client/src/components/App.jsx @@ -12,6 +12,7 @@ import Footer from './Footer' import Logout from './User/Logout' import NavBar from './NavBar' import NotFound from './Others/NotFound' +import PasswordReset from './User/PasswordReset' import ProfileEdit from './User/ProfileEdit' import Statistics from './Statistics' import UserForm from './User/UserForm' @@ -43,6 +44,27 @@ class App extends React.Component { path="/login" render={() => } /> + } + /> + } + /> + } + /> + } + /> + diff --git a/fittrackee_client/src/components/NavBar/index.jsx b/fittrackee_client/src/components/NavBar/index.jsx index 5dcec83a..b4f576d9 100644 --- a/fittrackee_client/src/components/NavBar/index.jsx +++ b/fittrackee_client/src/components/NavBar/index.jsx @@ -101,7 +101,7 @@ class NavBar extends React.PureComponent { pathname: '/register', }} > - {t('common:Register')} + {t('user:Register')} )} @@ -113,7 +113,7 @@ class NavBar extends React.PureComponent { pathname: '/login', }} > - {t('common:Login')} + {t('user:Login')} )} @@ -148,7 +148,7 @@ class NavBar extends React.PureComponent { pathname: '/logout', }} > - {t('common:Logout')} + {t('user:Logout')} diff --git a/fittrackee_client/src/components/User/Form.jsx b/fittrackee_client/src/components/User/Form.jsx index 77e369b8..a833d47c 100644 --- a/fittrackee_client/src/components/User/Form.jsx +++ b/fittrackee_client/src/components/User/Form.jsx @@ -1,12 +1,13 @@ import React from 'react' import { useTranslation } from 'react-i18next' import { Helmet } from 'react-helmet' +import { Link } from 'react-router-dom' import { history } from '../../index' export default function Form(props) { const { t } = useTranslation() - const pageTitle = `common:${props.formType + const pageTitle = `user:${props.formType .charAt(0) .toUpperCase()}${props.formType.slice(1)}` return ( @@ -34,65 +35,86 @@ export default function Form(props) { ) : ( -
- props.handleUserFormSubmit(event, props.formType) - } - > - {props.formType === 'register' && ( -
- -
- )} -
+ <> + + props.handleUserFormSubmit(event, props.formType) + } + > + {props.formType === 'register' && ( +
+ +
+ )} + {props.formType !== 'password reset' && ( +
+ +
+ )} + {props.formType !== 'reset your password' && ( + <> +
+ +
+ {props.formType !== 'login' && ( +
+ +
+ )} + + )} -
-
- -
- {props.formType === 'register' && ( -
- -
- )} - -
+ +

+ {props.formType === 'login' && ( + + {t('user:Forgot password?')} + + )} +

+ )}
diff --git a/fittrackee_client/src/components/User/PasswordReset.jsx b/fittrackee_client/src/components/User/PasswordReset.jsx new file mode 100644 index 00000000..5589df43 --- /dev/null +++ b/fittrackee_client/src/components/User/PasswordReset.jsx @@ -0,0 +1,48 @@ +import React from 'react' +import { Trans, useTranslation } from 'react-i18next' +import { Link } from 'react-router-dom' + +import { ReactComponent as Password } from '../../images/password.svg' +import { ReactComponent as MailSend } from '../../images/mail-send.svg' + +export default function PasswordReset(props) { + const { t } = useTranslation() + const { action } = props + return ( +
+
+
+
+
+
+ {action === 'sent' && ( + <> +
+ +
+ {t( + // eslint-disable-next-line max-len + "user:Check your email. If your address is in our database, you'll received an email with a link to reset your password." + )} + + )} + {action === 'updated' && ( + <> +
+ +
+ + {/* prettier-ignore */} + Your password have been updated. Click + here to log in. + + + )} +
+
+
+
+
+
+ ) +} diff --git a/fittrackee_client/src/components/User/UserForm.jsx b/fittrackee_client/src/components/User/UserForm.jsx index 85616e67..7f64ebd4 100644 --- a/fittrackee_client/src/components/User/UserForm.jsx +++ b/fittrackee_client/src/components/User/UserForm.jsx @@ -49,9 +49,10 @@ class UserForm extends React.Component { t, } = this.props const { formData } = this.state + const { token } = this.props.location.query return (
- {isLoggedIn() ? ( + {isLoggedIn() || (formType === 'password reset' && !token) ? ( ) : (
@@ -63,6 +64,9 @@ class UserForm extends React.Component { onHandleFormChange={event => this.onHandleFormChange(event)} handleUserFormSubmit={event => { event.preventDefault() + if (formType === 'password reset') { + formData.token = token + } onHandleUserFormSubmit(formData, formType) }} /> @@ -82,6 +86,12 @@ export default withTranslation()( }), dispatch => ({ onHandleUserFormSubmit: (formData, formType) => { + formType = + formType === 'password reset' + ? 'password/update' + : formType === 'reset your password' + ? 'password/reset-request' + : formType dispatch(handleUserFormSubmit(formData, formType)) }, }) diff --git a/fittrackee_client/src/fitTrackeeApi/auth.js b/fittrackee_client/src/fitTrackeeApi/auth.js index 199b4ec0..72c6731c 100644 --- a/fittrackee_client/src/fitTrackeeApi/auth.js +++ b/fittrackee_client/src/fitTrackeeApi/auth.js @@ -1,7 +1,7 @@ import { createApiRequest } from '../utils' export default class FitTrackeeApi { - static loginOrRegister(target, data) { + static loginOrRegisterOrPasswordReset(target, data) { const params = { url: `auth/${target}`, method: 'POST', diff --git a/fittrackee_client/src/images/mail-send.svg b/fittrackee_client/src/images/mail-send.svg new file mode 100644 index 00000000..d58f098a --- /dev/null +++ b/fittrackee_client/src/images/mail-send.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + diff --git a/fittrackee_client/src/images/password.svg b/fittrackee_client/src/images/password.svg new file mode 100644 index 00000000..92777002 --- /dev/null +++ b/fittrackee_client/src/images/password.svg @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fittrackee_client/src/locales/en/common.json b/fittrackee_client/src/locales/en/common.json index 04d9bfaf..3a898677 100644 --- a/fittrackee_client/src/locales/en/common.json +++ b/fittrackee_client/src/locales/en/common.json @@ -12,8 +12,6 @@ "Edit": "Edit", "day": "day", "days": "days", - "Login": "Login", - "Logout": "Logout", "Next": "Next", "No": "No", "no": "no", @@ -21,7 +19,6 @@ "No workouts.": "No workouts.", "Page not found": "Page not found", "Previous": "Prev", - "Register": "Register", "registration date": "registration date", "Sort": "Sort", "Sort by": "Sort by", diff --git a/fittrackee_client/src/locales/en/user.json b/fittrackee_client/src/locales/en/user.json index ddf580bb..568de1e6 100644 --- a/fittrackee_client/src/locales/en/user.json +++ b/fittrackee_client/src/locales/en/user.json @@ -4,6 +4,7 @@ "Are you sure you want to delete your account? All data will be deleted, this cannot be undone.": "Are you sure you want to delete your account? All data will be deleted, this cannot be undone.", "Bio": "Bio", "Birth Date": "Birth Date", + "Check your email. If your address is in our database, you'll received an email with a link to reset your password.": "Check your email. If your address is in our database, you'll received an email with a link to reset your password.", "Delete my account": "Delete my account", "Delete picture": "Delete picture", "Delete user account": "Delete user account", @@ -15,20 +16,30 @@ "Enter the password confirmation": "Enter the password confirmation", "First day of week": "First day of week", "First Name": "First Name", + "Forgot password?": "Forgot password?", + "Invalid token. Please request a new token.": "Invalid token. Please request a new token.", "Language": "Language", "Last Name": "Last Name", "Location": "Location", "loggedOut": "You are now logged out. Click <1>here to log back in.", + "Login": "Login", "login": "login", + "Logout": "Logout", "Monday": "Monday", "Password": "Password", "Password Confirmation": "Password Confirmation", + "Password reset": "Password reset", + "password reset": "password reset", "Profile": "Profile", "Profile Edition": "Profile Edition", + "Register": "Register", "register": "register", "Registration Date": "Registration Date", + "Reset your password": "Reset your password", + "reset your password": "reset your password", "Send": "Send", "Sunday": "Sunday", "Timezone": "Timezone", + "updatedPasswordText": "Your password have been updated. Click <1>here to log in." , "Username": "Username" } diff --git a/fittrackee_client/src/locales/fr/common.json b/fittrackee_client/src/locales/fr/common.json index b0a31eb2..76d30a79 100644 --- a/fittrackee_client/src/locales/fr/common.json +++ b/fittrackee_client/src/locales/fr/common.json @@ -12,8 +12,6 @@ "Edit": "Modifier", "day": "jour", "days": "jours", - "Login": "Se connecter", - "Logout": "Se déconnecter", "Next": "Page suivante", "No": "Non", "no": "non", @@ -21,7 +19,6 @@ "No workouts.": "Pas d'activités.", "Page not found": "Page introuvable", "Previous": "Page précédente", - "Register": "S'inscrire", "registration date": "date d'inscription", "Sort": "Tri", "Sort by": "Trier par", diff --git a/fittrackee_client/src/locales/fr/user.json b/fittrackee_client/src/locales/fr/user.json index 5b891b72..7dc52704 100644 --- a/fittrackee_client/src/locales/fr/user.json +++ b/fittrackee_client/src/locales/fr/user.json @@ -4,6 +4,7 @@ "Are you sure you want to delete your account? All data will be deleted, this cannot be undone.": "Etes-vous sûr de vouloir supprimer votre compte ? Toutes les données seront définitivement effacés.", "Bio": "Bio", "Birth Date": "Date de naissance", + "Check your email. If your address is in our database, you'll received an email with a link to reset your password.": "Vérifiez vore boite mail. Si vote adresse est dans notre base de données, vous recevrez un email avec un lien pour réinitialiser votre mot de passe", "Delete my account": "Supprimer mon compte", "Delete picture": "Supprimer l'image", "Delete user account": "Supprimer le compte", @@ -15,20 +16,30 @@ "Enter the password confirmation": "Confirmer le mot de passe", "First day of week": "Premier jour de la semaine", "First Name": "Prénom", + "Forgot password?": "Mot de passe oublié ?", + "Invalid token. Please request a new token.": "Token invalid. Veuillez demander un nouveau token.", "Language": "Langue", "Last Name": "Nom", "Location": "Lieu", "loggedOut": "Vous êtes déconnecté. Cliquez <1>ici pour vous reconnecter.", + "Login": "Se connecter", "login": "se connecter", + "Logout": "Se déconnecter", "Monday": "Lundi", "Password": "Mot de passe", "Password Confirmation": "Confirmation du mot de passe", + "Password reset": "Réinitialiser votre mot de passe", + "password reset": "réinitialiser votre mot de passe", "Profile": "Profil", "Profile Edition": "Edition du profil", + "Register": "S'inscrire", "register": "s'inscrire", "Registration Date": "Date d'inscription", + "Reset your password": "Réinitialiser votre mot de passe", + "reset your password": "réinitialiser votre mot de passe", "Send": "Envoyer", "Sunday": "Dimanche", "Timezone": "Fuseau horaire", + "updatedPasswordText": "Votre mot de passe a été mis à jour. Cliquez <1>ici pour vous connecter.", "Username": "Nom d'utilisateur" } diff --git a/fittrackee_client/src/utils/history.js b/fittrackee_client/src/utils/history.js index 5d2209af..15b06b67 100644 --- a/fittrackee_client/src/utils/history.js +++ b/fittrackee_client/src/utils/history.js @@ -1,3 +1,12 @@ +const routesWithoutAuthentication = [ + '/login', + '/register', + '/password-reset', + '/password-reset/request', + '/password-reset/sent', + '/updated-password', +] + const updatePath = (toPath, newPath) => { if (typeof toPath === 'string' || toPath instanceof String) { toPath = newPath @@ -10,13 +19,13 @@ const updatePath = (toPath, newPath) => { const pathInterceptor = toPath => { if ( !window.localStorage.authToken && - !['/login', '/register'].includes(toPath.pathname) + !routesWithoutAuthentication.includes(toPath.pathname) ) { toPath = updatePath(toPath, '/login') } if ( window.localStorage.authToken && - ['/login', '/register'].includes(toPath.pathname) + routesWithoutAuthentication.includes(toPath.pathname) ) { toPath = updatePath(toPath, '/') }