all repos — caroster @ db14df0f7a21cbdd9a57218dc259b4d58a79db87

[Octree] Group carpool to your event https://caroster.io

:sparkles: Resolve "Lors du login, pouvoir reset son mot de passe"
Hadrien Froger e3k8y9i0k3s8o9x4@octreea17.slack.com
Wed, 22 Jul 2020 09:47:34 +0000
commit

db14df0f7a21cbdd9a57218dc259b4d58a79db87

parent

e43e86fc1e36b2d54b730071eb9cc28e4b6dcaf5

M app/package-lock.jsonapp/package-lock.json

@@ -1784,9 +1784,9 @@ "resolved": "https://npm-8ee.hidora.com/@types%2fminimatch/-/minimatch-3.0.3.tgz",

"integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==" }, "@types/node": { - "version": "14.0.23", - "resolved": "https://npm-8ee.hidora.com/@types%2fnode/-/node-14.0.23.tgz", - "integrity": "sha512-Z4U8yDAl5TFkmYsZdFPdjeMa57NOvnaf1tljHzhouaPEp7LCj2JKkejpI1ODviIAQuW4CcQmxkQ77rnLsOOoKw==" + "version": "14.0.24", + "resolved": "https://npm-8ee.hidora.com/@types%2fnode/-/node-14.0.24.tgz", + "integrity": "sha512-btt/oNOiDWcSuI721MdL8VQGnjsKjlTMdrKyTcLCKeQp/n4AAMFJ961wMbp+09y8WuGPClDEv07RIItdXKIXAA==" }, "@types/parse-json": { "version": "4.0.0",

@@ -3455,9 +3455,9 @@ "lodash.uniq": "^4.5.0"

} }, "caniuse-lite": { - "version": "1.0.30001102", - "resolved": "https://npm-8ee.hidora.com/caniuse-lite/-/caniuse-lite-1.0.30001102.tgz", - "integrity": "sha512-fOjqRmHjRXv1H1YD6QVLb96iKqnu17TjcLSaX64TwhGYed0P1E1CCWZ9OujbbK4Z/7zax7zAzvQidzdtjx8RcA==" + "version": "1.0.30001104", + "resolved": "https://npm-8ee.hidora.com/caniuse-lite/-/caniuse-lite-1.0.30001104.tgz", + "integrity": "sha512-pkpCg7dmI/a7WcqM2yfdOiT4Xx5tzyoHAXWsX5/HxZ3TemwDZs0QXdqbE0UPLPVy/7BeK7693YfzfRYfu1YVpg==" }, "capture-exit": { "version": "2.0.0",

@@ -4785,9 +4785,9 @@ "resolved": "https://npm-8ee.hidora.com/ee-first/-/ee-first-1.1.1.tgz",

"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" }, "electron-to-chromium": { - "version": "1.3.499", - "resolved": "https://npm-8ee.hidora.com/electron-to-chromium/-/electron-to-chromium-1.3.499.tgz", - "integrity": "sha512-y7FwtQm/8xuLMnYQfBQDYzCpNn+VkSnf4c3Km5TWMNXg7JA5RQBuxmcLaKdDVcIK0K5xGIa7TlxpRt4BdNxNoA==" + "version": "1.3.502", + "resolved": "https://npm-8ee.hidora.com/electron-to-chromium/-/electron-to-chromium-1.3.502.tgz", + "integrity": "sha512-TIeXOaHAvfP7FemGUtAJxStmOc1YFGWFNqdey/4Nk41L9b1nMmDVDGNMIWhZJvOfJxix6Cv5FGEnBK+yvw3UTg==" }, "elliptic": { "version": "6.5.3",

@@ -12299,9 +12299,9 @@ "resolved": "https://npm-8ee.hidora.com/stealthy-require/-/stealthy-require-1.1.1.tgz",

"integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=" }, "strapi-react-context": { - "version": "0.2.7", - "resolved": "https://npm-8ee.hidora.com/strapi-react-context/-/strapi-react-context-0.2.7.tgz", - "integrity": "sha512-Dp8SfBlGVmSlAitAa7kvAtx0gJH5xLPLN5/lLi8Tjm6gcQepgP3Xny5ouNJZvvY6Pxrp/7x+iXnGFddeLTQUJw==", + "version": "0.3.2", + "resolved": "https://npm-8ee.hidora.com/strapi-react-context/-/strapi-react-context-0.3.2.tgz", + "integrity": "sha512-1co9TXuQRG74PDbJRxCpjIWu+88rK/jcF+VOQCFGsrJypZDgmkwSfvXx2Xx39C7UVA3YDXCtlU5gFRB9NpevJQ==", "requires": { "@testing-library/react-hooks": "^3.3.0" }

@@ -12315,23 +12315,28 @@ "inherits": "~2.0.1",

"readable-stream": "^2.0.2" }, "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://npm-8ee.hidora.com/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + "@babel/cli": { + "version": "7.10.5", + "resolved": "https://npm-8ee.hidora.com/@babel%2fcli/-/cli-7.10.5.tgz", + "integrity": "sha512-j9H9qSf3kLdM0Ao3aGPbGZ73mEA9XazuupcS6cDGWuiyAcANoguhP0r2Lx32H5JGw4sSSoHG3x/mxVnHgvOoyA==", + "requires": { + "chokidar": "^2.1.8", + "commander": "^4.0.1", + "convert-source-map": "^1.1.0", + "fs-readdir-recursive": "^1.1.0", + "glob": "^7.0.0", + "lodash": "^4.17.19", + "make-dir": "^2.1.0", + "slash": "^2.0.0", + "source-map": "^0.5.0" + } }, "readable-stream": { "version": "2.3.7", "resolved": "https://npm-8ee.hidora.com/readable-stream/-/readable-stream-2.3.7.tgz", "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" + "@babel/highlight": "^7.10.4" } }, "string_decoder": {

@@ -12339,7 +12344,9 @@ "version": "1.1.1",

"resolved": "https://npm-8ee.hidora.com/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "requires": { - "safe-buffer": "~5.1.0" + "browserslist": "^4.12.0", + "invariant": "^2.2.4", + "semver": "^5.5.0" } } }

@@ -12375,13 +12382,37 @@ "version": "2.3.7",

"resolved": "https://npm-8ee.hidora.com/readable-stream/-/readable-stream-2.3.7.tgz", "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" + "@babel/code-frame": "^7.10.4", + "@babel/generator": "^7.10.5", + "@babel/helper-module-transforms": "^7.10.5", + "@babel/helpers": "^7.10.4", + "@babel/parser": "^7.10.5", + "@babel/template": "^7.10.4", + "@babel/traverse": "^7.10.5", + "@babel/types": "^7.10.5", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.1", + "json5": "^2.1.2", + "lodash": "^4.17.19", + "resolve": "^1.3.2", + "semver": "^5.4.1", + "source-map": "^0.5.0" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://npm-8ee.hidora.com/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://npm-8ee.hidora.com/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } } }, "string_decoder": {

@@ -12389,7 +12420,9 @@ "version": "1.1.1",

"resolved": "https://npm-8ee.hidora.com/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "requires": { - "safe-buffer": "~5.1.0" + "@babel/types": "^7.10.5", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" } } }

@@ -12428,7 +12461,7 @@ "version": "4.0.0",

"resolved": "https://npm-8ee.hidora.com/strip-ansi/-/strip-ansi-4.0.0.tgz", "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", "requires": { - "ansi-regex": "^3.0.0" + "@babel/types": "^7.10.4" } } }

@@ -12453,7 +12486,8 @@ "version": "6.0.0",

"resolved": "https://npm-8ee.hidora.com/strip-ansi/-/strip-ansi-6.0.0.tgz", "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", "requires": { - "ansi-regex": "^5.0.0" + "@babel/helper-explode-assignable-expression": "^7.10.4", + "@babel/types": "^7.10.4" } } }
M app/package.jsonapp/package.json

@@ -21,7 +21,7 @@ "react-i18next": "^11.7.0",

"react-router-dom": "^5.2.0", "react-scripts": "3.4.1", "react-slick": "^0.26.1", - "strapi-react-context": "^0.2.7" + "strapi-react-context": "^0.3.2" }, "scripts": { "start": "react-scripts start",
M app/src/Router.jsapp/src/Router.js

@@ -12,6 +12,8 @@ import SignUp from './pages/SignUp';

import SignUpSuccess from './pages/SignUpSuccess'; import NotConfirmed from './pages/SignUpSuccess'; import SignIn from './pages/SignIn'; +import LostPassword from './pages/LostPassword.js'; +import ResetPassword from './pages/ResetPassword.js'; const Router = () => { useGTM();

@@ -23,6 +25,8 @@ <Route path="/" exact component={Home} />

<Route path="/new" exact component={Home} /> <Route path="/register/success" exact component={SignUpSuccess} /> <Route path="/register" exact component={SignUp} /> + <Route path="/lost-password" exact component={LostPassword} /> + <Route path="/reset-password" exact component={ResetPassword} /> <Route path="/login" exact component={SignIn} /> <Route path="/dashboard" exact component={Dashboard} /> <Route path="/confirm" exact component={NotConfirmed} />
A app/src/containers/LostPassword/Success.js

@@ -0,0 +1,50 @@

+import React from 'react'; +import {useTranslation} from 'react-i18next'; +import Button from '@material-ui/core/Button'; + +import Icon from '@material-ui/core/Icon'; +import CardContent from '@material-ui/core/CardContent'; +import Card from '@material-ui/core/Card'; +import Typography from '@material-ui/core/Typography'; +import CardActions from '@material-ui/core/CardActions'; +import {makeStyles} from '@material-ui/core/styles'; + +const Success = ({email}) => { + const {t} = useTranslation(); + const classes = useStyles(); + + return ( + <Card className={classes.successCard}> + <CardContent> + <Icon size="large" color="primary" className={classes.successIcon}> + done + </Icon> + </CardContent> + <CardContent> + <Typography variant="body1" gutterBottom> + {t('lost_password.sent', {email})} + </Typography> + </CardContent> + <CardActions> + <Button + id="LostPasswordRegister" + href="/login" + color="secondary" + variant="contained" + > + {t('lost_password.actions.login')} + </Button> + </CardActions> + </Card> + ); +}; + +const useStyles = makeStyles(theme => ({ + successCard: { + textAlign: 'center', + }, + successIcon: { + fontSize: theme.spacing(14), + }, +})); +export default Success;
A app/src/containers/LostPassword/index.js

@@ -0,0 +1,126 @@

+import React, {useCallback, useState} from 'react'; +import {Redirect} from 'react-router-dom'; +import {useTranslation} from 'react-i18next'; +import {useAuth} from 'strapi-react-context'; +import TextField from '@material-ui/core/TextField'; +import Button from '@material-ui/core/Button'; + +import CardContent from '@material-ui/core/CardContent'; +import Card from '@material-ui/core/Card'; +import CircularProgress from '@material-ui/core/CircularProgress'; +import CardActions from '@material-ui/core/CardActions'; +import Link from '@material-ui/core/Link'; +import {useToast} from '../../contexts/Toast'; +import {makeStyles} from '@material-ui/core/styles'; +import LostPasswordSuccess from './Success'; + +const LostPassword = () => { + const {t} = useTranslation(); + const classes = useStyles(); + + const {token, authState, sendPasswordReset} = useAuth(); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(''); + const [email, setEmail] = useState(''); + const [isSent, setIsSent] = useState(false); + const {addToast} = useToast(); + + const canSubmit = () => email.length < 4; + + const onSubmit = useCallback( + async evt => { + if (evt.preventDefault) evt.preventDefault(); + if (isLoading) { + return; + } + setIsLoading(true); + try { + await sendPasswordReset(email); + setIsSent(true); + } catch (error) { + if (error.kind === 'bad_data') { + addToast(t('lost_password.error')); + setError(t('lost_password.error')); + } else { + addToast(t('generic.errors.unknown')); + } + } + setIsLoading(false); + return false; + }, + [sendPasswordReset, email, isLoading, addToast, t] + ); + + if (token) { + return <Redirect to="/dashboard" />; + } + if (authState && authState.user && !authState.user.confirmed) { + return <Redirect to="/confirm" />; + } + + if (!isLoading && isSent) { + return <LostPasswordSuccess email={email} />; + } + + return ( + <form onSubmit={onSubmit}> + <Card> + <CardContent> + <TextField + label={t('lost_password.email')} + fullWidth + required={true} + margin="dense" + value={email} + onChange={({target: {value = ''}}) => setEmail(value)} + id="LostPasswordEmail" + name="email" + type="email" + error={!!error} + helperText={ + error && ( + <> + {error}&nbsp; + <Link href="/register"> + {t('lost_password.actions.register')} + </Link> + </> + ) + } + /> + </CardContent> + <CardActions> + <Button + id="LostPasswordRegister" + href="/login" + color="secondary" + variant="contained" + > + {t('lost_password.actions.cancel')} + </Button> + + <Button + color="primary" + variant="contained" + type="submit" + disabled={!canSubmit} + aria-disabled={!canSubmit} + id="LostPasswordSubmit" + > + {t('lost_password.actions.send')} + {isLoading && ( + <CircularProgress className={classes.loader} size={20} /> + )} + </Button> + </CardActions> + </Card> + </form> + ); +}; + +const useStyles = makeStyles(theme => ({ + loader: { + marginLeft: theme.spacing(4), + }, +})); +export default LostPassword;
D app/src/containers/Profile/Profile.js

@@ -1,163 +0,0 @@

-import React, {useState} from 'react'; -import Card from '@material-ui/core/Card'; -import CardHeader from '@material-ui/core/CardHeader'; -import CardContent from '@material-ui/core/CardContent'; -import IconButton from '@material-ui/core/IconButton'; -import Icon from '@material-ui/core/Icon'; -import CardActions from '@material-ui/core/CardActions'; -import Button from '@material-ui/core/Button'; -import {useTranslation} from 'react-i18next'; -import EditPassword from './EditPassword'; -import {useToast} from '../../contexts/Toast'; -import ProfileField from './ProfileField'; -import {makeStyles} from '@material-ui/core'; - -const Profile = ({profile, updateProfile, logout}) => { - const {t} = useTranslation(); - const {addToast} = useToast(); - const classes = useStyles(); - const [isEditing, setIsEditing] = useState(false); - const [isEditingPassword, setIsEditingPassword] = useState(false); - const [firstName, setFirstName] = useState(profile.firstName); - const [lastName, setLastName] = useState(profile.lastName); - const [email, setEmail] = useState(profile.email); - const [oldPassword, setOldPassword] = useState(''); - const [newPassword, setNewPassword] = useState(''); - const [errorPassword, setErrorPassword] = useState(''); - - const savePassword = async () => { - try { - await updateProfile({ - old_password: oldPassword, - password: newPassword, - }); - addToast(t('profile.password_changed')); - } catch (err) { - if (err.kind === 'bad_data') { - setErrorPassword(t('profile.errors.password_nomatch')); - return; - } - } - setIsEditingPassword(false); - }; - - if (isEditingPassword) - return ( - <EditPassword - oldPassword={oldPassword} - newPassword={newPassword} - setOldPassword={setOldPassword} - setNewPassword={setNewPassword} - error={errorPassword} - save={savePassword} - cancel={() => { - setIsEditingPassword(false); - setNewPassword(''); - setOldPassword(''); - }} - /> - ); - - return ( - <form - onSubmit={evt => { - if (evt.preventDefault) evt.preventDefault(); - if (isEditing) { - try { - updateProfile({firstName, lastName, email}); - } catch (err) { - console.log(err); - return; - } - addToast(t('profile.updated')); - } - setIsEditing(!isEditing); - }} - > - <Card> - <CardHeader - action={ - <IconButton - color="inherit" - edge="end" - id="EditProfileAction" - type="submit" - title={ - isEditing - ? t('profile.actions.save') - : t('profile.actions.edit') - } - > - <Icon>{isEditing ? 'done' : 'edit'}</Icon> - </IconButton> - } - title={t('profile.title')} - /> - <CardContent> - <ProfileField - name="firstName" - value={firstName} - label={t('profile.firstName')} - defaultValue={t('profile.not_defined', { - field: '$t(profile.firstName)', - })} - onChange={setFirstName} - isEditing={isEditing} - /> - <ProfileField - name="lastName" - value={lastName} - label={t('profile.lastName')} - defaultValue={t('profile.not_defined', { - field: '$t(profile.lastName)', - })} - onChange={setLastName} - isEditing={isEditing} - /> - <ProfileField - name="email" - value={email} - label={t('profile.email')} - defaultValue={t('profile.not_defined', { - field: '$t(profile.email)', - })} - onChange={setEmail} - isEditing={isEditing} - /> - </CardContent> - <CardActions className={classes.actions}> - {isEditing && ( - <Button - type="button" - color="primary" - size="small" - onClick={evt => { - if (evt.preventDefault) evt.preventDefault(); - setIsEditingPassword(true); - }} - > - {t('profile.actions.change_password')} - </Button> - )} - {!isEditing && ( - <Button - type="button" - color="secondary" - size="small" - onClick={() => logout()} - > - {t('profile.actions.logout')} - </Button> - )} - </CardActions> - </Card> - </form> - ); -}; - -const useStyles = makeStyles(theme => ({ - actions: { - marginTop: theme.spacing(2), - }, -})); -export default Profile;
M app/src/containers/Profile/index.jsapp/src/containers/Profile/index.js

@@ -1,3 +1,163 @@

-import Profile from './Profile'; +import React, {useState} from 'react'; +import Card from '@material-ui/core/Card'; +import CardHeader from '@material-ui/core/CardHeader'; +import CardContent from '@material-ui/core/CardContent'; +import IconButton from '@material-ui/core/IconButton'; +import Icon from '@material-ui/core/Icon'; +import CardActions from '@material-ui/core/CardActions'; +import Button from '@material-ui/core/Button'; +import {useTranslation} from 'react-i18next'; +import EditPassword from './EditPassword'; +import {useToast} from '../../contexts/Toast'; +import ProfileField from './ProfileField'; +import {makeStyles} from '@material-ui/core'; + +const Profile = ({profile, updateProfile, logout}) => { + const {t} = useTranslation(); + const {addToast} = useToast(); + const classes = useStyles(); + const [isEditing, setIsEditing] = useState(false); + const [isEditingPassword, setIsEditingPassword] = useState(false); + const [firstName, setFirstName] = useState(profile.firstName); + const [lastName, setLastName] = useState(profile.lastName); + const [email, setEmail] = useState(profile.email); + const [oldPassword, setOldPassword] = useState(''); + const [newPassword, setNewPassword] = useState(''); + const [errorPassword, setErrorPassword] = useState(''); + + const savePassword = async () => { + try { + await updateProfile({ + old_password: oldPassword, + password: newPassword, + }); + addToast(t('profile.password_changed')); + } catch (err) { + if (err.kind === 'bad_data') { + setErrorPassword(t('profile.errors.password_nomatch')); + return; + } + } + setIsEditingPassword(false); + }; + if (isEditingPassword) + return ( + <EditPassword + oldPassword={oldPassword} + newPassword={newPassword} + setOldPassword={setOldPassword} + setNewPassword={setNewPassword} + error={errorPassword} + save={savePassword} + cancel={() => { + setIsEditingPassword(false); + setNewPassword(''); + setOldPassword(''); + }} + /> + ); + + return ( + <form + onSubmit={evt => { + if (evt.preventDefault) evt.preventDefault(); + if (isEditing) { + try { + updateProfile({firstName, lastName, email}); + } catch (err) { + console.log(err); + return; + } + addToast(t('profile.updated')); + } + setIsEditing(!isEditing); + }} + > + <Card> + <CardHeader + action={ + <IconButton + color="inherit" + edge="end" + id="EditProfileAction" + type="submit" + title={ + isEditing + ? t('profile.actions.save') + : t('profile.actions.edit') + } + > + <Icon>{isEditing ? 'done' : 'edit'}</Icon> + </IconButton> + } + title={t('profile.title')} + /> + <CardContent> + <ProfileField + name="firstName" + value={firstName} + label={t('profile.firstName')} + defaultValue={t('profile.not_defined', { + field: '$t(profile.firstName)', + })} + onChange={setFirstName} + isEditing={isEditing} + /> + <ProfileField + name="lastName" + value={lastName} + label={t('profile.lastName')} + defaultValue={t('profile.not_defined', { + field: '$t(profile.lastName)', + })} + onChange={setLastName} + isEditing={isEditing} + /> + <ProfileField + name="email" + value={email} + label={t('profile.email')} + defaultValue={t('profile.not_defined', { + field: '$t(profile.email)', + })} + onChange={setEmail} + isEditing={isEditing} + /> + </CardContent> + <CardActions className={classes.actions}> + {isEditing && ( + <Button + type="button" + color="primary" + size="small" + onClick={evt => { + if (evt.preventDefault) evt.preventDefault(); + setIsEditingPassword(true); + }} + > + {t('profile.actions.change_password')} + </Button> + )} + {!isEditing && ( + <Button + type="button" + color="secondary" + size="small" + onClick={() => logout()} + > + {t('profile.actions.logout')} + </Button> + )} + </CardActions> + </Card> + </form> + ); +}; + +const useStyles = makeStyles(theme => ({ + actions: { + marginTop: theme.spacing(2), + }, +})); export default Profile;
A app/src/containers/ResetPassword/index.js

@@ -0,0 +1,81 @@

+import React from 'react'; +import Card from '@material-ui/core/Card'; +import CardHeader from '@material-ui/core/CardHeader'; +import CardContent from '@material-ui/core/CardContent'; +import CardActions from '@material-ui/core/CardActions'; +import Button from '@material-ui/core/Button'; +import {useTranslation} from 'react-i18next'; +import TextField from '@material-ui/core/TextField'; +import IconButton from '@material-ui/core/IconButton'; +import Icon from '@material-ui/core/Icon'; + +const ResetPassword = ({ + password, + setPassword, + passwordConfirmation, + setPasswordConfirmation, + error, + isLoading, +}) => { + const {t} = useTranslation(); + return ( + <Card> + <CardHeader + title={t('profile.actions.change_password')} + action={ + <IconButton + color="inherit" + edge="end" + id="ChangePasswordAction" + type="submit" + title={t('profile.actions.save')} + > + <Icon>done</Icon> + </IconButton> + } + /> + <CardContent> + <TextField + label={t('lost_password.password')} + type="password" + fullWidth + autoFocus + margin="dense" + value={password} + onChange={({target: {value = ''}}) => setPassword(value)} + id="ResetPasswordNewPassword" + name="new_password" + error={!!error} + helperText={error} + /> + <TextField + type="password" + label={t('lost_password.password_confirmation')} + fullWidth + margin="dense" + value={passwordConfirmation} + onChange={({target: {value = ''}}) => setPasswordConfirmation(value)} + id="ResetPasswordNewPasswordConfirmation" + name="new_password_confirmation" + /> + </CardContent> + <CardActions> + <Button + type="submit" + color="primary" + size="small" + variant="contained" + disabled={ + isLoading || + password.length < 4 || + password !== passwordConfirmation + } + > + {t('lost_password.actions.save_new_password')} + </Button> + </CardActions> + </Card> + ); +}; + +export default ResetPassword;
M app/src/containers/SignInForm/index.jsapp/src/containers/SignInForm/index.js

@@ -1,9 +1,11 @@

import React, {useCallback, useState, useMemo} from 'react'; -import {Redirect} from 'react-router-dom'; +import {Redirect, Link as RouterLink} from 'react-router-dom'; import {useTranslation} from 'react-i18next'; import {useAuth} from 'strapi-react-context'; import TextField from '@material-ui/core/TextField'; import Button from '@material-ui/core/Button'; +import Link from '@material-ui/core/Link'; + import CardContent from '@material-ui/core/CardContent'; import {CircularProgress} from '@material-ui/core'; import CardActions from '@material-ui/core/CardActions';

@@ -16,6 +18,7 @@ const classes = useStyles();

const {login, token, authState} = useAuth(); const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(''); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const {addToast} = useToast();

@@ -38,7 +41,8 @@ // TODO add to my event if saved in local storage

// TODO remove from local storage. } catch (error) { console.log('ERROR', {error}); - if (error) { + if (error.kind === 'bad_data') { + setError(t('signin.errors')); addToast(t('signin.errors')); } }

@@ -68,6 +72,8 @@ onChange={({target: {value = ''}}) => setEmail(value)}

id="SignInEmail" name="email" type="email" + error={!!error} + helperText={error} /> <TextField label={t('signin.password')}

@@ -79,6 +85,14 @@ onChange={({target: {value = ''}}) => setPassword(value)}

id="SignInEmail" name="password" type="password" + error={!!error} + helperText={ + error && ( + <RouterLink to="/lost-password" component={Link}> + {t('lost_password.message')} + </RouterLink> + ) + } /> </CardContent> <CardActions>
M app/src/locales/fr.jsonapp/src/locales/fr.json

@@ -189,5 +189,24 @@ "password": "Mot de passe",

"login": "Se connecter", "register": "Pas encore de compte ? S'inscrire", "errors": "Vérifier votre email et mot de passe" + }, + "lost_password": { + "title": "Récupération de mot de passe", + "reset_title": "Définition d'un nouveau mot de passe", + "message": "Perdu son mot de passe ?", + "email": "Votre email", + "password": "Nouveau mot de passe", + "password_confirmation": "Confirmation du nouveau mot de passe", + "sent": "Un email a été envoyé à {{email}}, avec un lien pour récupérer votre mot de passe", + "error": "Cet email n'existe pas", + "change_success": "Votre mot de passe a été modifié", + "actions": { + "send": "Envoyer un email de récupération", + "cancel": "Annuler", + "login": "Retour à l'écran de connexion", + "resend": "Envoyer à nouveau", + "register": "Créer un compte ?", + "save_new_password": "Mettre à jour" + } } }
A app/src/pages/LostPassword.js

@@ -0,0 +1,22 @@

+import React from 'react'; +import LostPasswordContainer from '../containers/LostPassword'; +import Layout from '../layouts/Centered'; +import {useTranslation} from 'react-i18next'; +import {useAuth} from 'strapi-react-context'; +import {Redirect} from 'react-router-dom'; +const LostPassword = () => { + const {t} = useTranslation(); + const {token} = useAuth(); + + if (token) { + return <Redirect to="/dashboard" />; + } + + return ( + <Layout menuTitle={t('lost_password.title')}> + <LostPasswordContainer /> + </Layout> + ); +}; + +export default LostPassword;
A app/src/pages/ResetPassword.js

@@ -0,0 +1,63 @@

+import React, {useState} from 'react'; +import {useLocation, useHistory, Redirect} from 'react-router-dom'; +import {useAuth} from 'strapi-react-context'; +import NotFound from './NotFound'; +import ResetPasswordContainer from '../containers/ResetPassword'; +import Layout from '../layouts/Centered'; +import {useTranslation} from 'react-i18next'; +import {useToast} from '../contexts/Toast'; + +const ResetPassword = () => { + const {resetPassword, token} = useAuth(); + const {search} = useLocation(); + const history = useHistory(); + const [isLoading, setIsLoading] = useState(false); + const {addToast} = useToast(); + const {t} = useTranslation(); + + const [password, setPassword] = useState(''); + const [passwordConfirmation, setPasswordConfirmation] = useState(''); + const [passwordError, setPasswordError] = useState(''); + const code = new URLSearchParams(search).get('code'); + + const onReset = async evt => { + if (evt.preventDefault) evt.preventDefault(); + setIsLoading(true); + try { + await resetPassword(code, password, passwordConfirmation); + setPasswordError(''); + addToast(t('forgot_password.change_success')); + history.push('/login'); + } catch (err) { + if (err.kind === 'bad_data') { + setPasswordError(t('generic.errors.unknown')); + } + } + setIsLoading(false); + }; + + if (token) { + return <Redirect to="/dashboard" />; + } + + if (!code) { + return <NotFound />; + } + + return ( + <Layout menuTitle={t('lost_password.reset_title')}> + <form onSubmit={onReset}> + <ResetPasswordContainer + isLoading={isLoading} + password={password} + setPassword={setPassword} + passwordConfirmation={passwordConfirmation} + setPasswordConfirmation={setPasswordConfirmation} + error={passwordError} + /> + </form> + </Layout> + ); +}; + +export default ResetPassword;
M config/permissions.jsonconfig/permissions.json

@@ -21,6 +21,15 @@ "name": "settings",

"actions": ["find"] } ] + }, + { + "type": "users-permissions", + "controllers": [ + { + "name": "user", + "actions": ["resetpassword"] + } + ] } ], "authenticated": [