Correct the registration screen.
authorymh <ymh.work@gmail.com>
Tue, 18 Dec 2018 02:27:22 +0100
changeset 199 c78d579f4b55
parent 198 f0f83f5530a6
child 200 1b9b9401ba7c
Correct the registration screen. First version of a password reset screen.
client/src/actions/authActions.js
client/src/components/Login.js
client/src/components/Register.js
client/src/components/Reset.js
client/src/constants/actionTypes.js
client/src/index.js
client/src/locales/en/translation.json
client/src/locales/fr/translation.json
client/src/reducers/authReducer.js
client/src/sagas/authSaga.js
src/.env.tmpl
src/irinotes/settings.py
--- a/client/src/actions/authActions.js	Thu Dec 06 01:35:30 2018 +0100
+++ b/client/src/actions/authActions.js	Tue Dec 18 02:27:22 2018 +0100
@@ -35,7 +35,14 @@
   };
 }
 
-
 export const resetAll = () => {
   return { type: types.SYNC_RESET_ALL }
 }
+
+export const resetSubmit = (email) => {
+  return {
+    type: types.AUTH_RESET_SUBMIT,
+    email,
+  };
+}
+
--- a/client/src/components/Login.js	Thu Dec 06 01:35:30 2018 +0100
+++ b/client/src/components/Login.js	Tue Dec 18 02:27:22 2018 +0100
@@ -41,6 +41,11 @@
     this.props.history.push('/register');
   }
 
+  onClickReset = (e) => {
+    e.preventDefault();
+    this.props.history.push('/reset');
+  }
+
   renderErrorMessage(errorMessages, fieldname) {
     if (errorMessages && errorMessages.has(fieldname)) {
       return errorMessages.get(fieldname).map((message, key) =>
@@ -108,7 +113,9 @@
                 </div>
               </div>
               <p className="text-center">
-                <a className="text-muted" href="/register" onClick={ this.onClickRegister }><Trans i18nKey='login.registration_message'>Pas encore inscrit ? Créer un compte.</Trans></a>
+                <a className="text-muted" href="/register" onClick={ this.onClickRegister }><Trans i18nKey='login.registration_message'>Pas encore inscrit ? Créer un compte</Trans></a>
+                {/* &nbsp;/&nbsp;
+                <a className="text-muted" href="/reset" onClick={ this.onClickReset }><Trans i18nKey='login.reset_message'>Mot de passe oublié</Trans></a> */}
               </p>
             </div>
           </div>
--- a/client/src/components/Register.js	Thu Dec 06 01:35:30 2018 +0100
+++ b/client/src/components/Register.js	Tue Dec 18 02:27:22 2018 +0100
@@ -2,18 +2,40 @@
 import { connect } from 'react-redux';
 import { bindActionCreators } from 'redux';
 import * as authActions from '../actions/authActions';
+import { Trans } from 'react-i18next';
+import * as R from 'ramda';
 
 class Register extends Component {
 
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      username: '',
+      email: '',
+      password1: '',
+      password2: ''
+    }
+
+
+  }
+
   register = () => {
-    const username = this.username.value;
-    const email = this.email.value;
-    const password1 = this.password1.value;
-    const password2 = this.password2.value;
+    const {
+      username,
+      email,
+      password1,
+      password2 } = this.state;
 
     this.props.authActions.registerSubmit(username, email, password1, password2);
   }
 
+  handleChange = (event) => {
+    const newState = {};
+    newState[event.target.name] = event.target.value;
+    this.setState(newState);
+  }
+
   submit = (e) => {
     e.preventDefault();
 
@@ -26,16 +48,44 @@
   }
 
   renderErrorMessage(errorMessages, fieldname) {
-    if (errorMessages && errorMessages.has(fieldname)) {
-      return errorMessages.get(fieldname).map((message, key) =>
-        <p className="form-text" key={ key }>{ message }</p>
+    if (errorMessages && fieldname in errorMessages) {
+      return errorMessages[fieldname].map((message, key) =>
+        <p key={ key } className="form-text alert alert-danger mt-4" role="alert" >{ message }</p>
       );
     }
   }
 
+  renderNonFieldErrors(errorMessages) {
+
+    if (errorMessages && errorMessages.error) {
+      return (
+        <div className="alert alert-danger mt-4" role="alert">
+          <Trans i18nKey="login.login_error">Unable to log in.</Trans>
+        </div>
+      )
+    }
+
+    const errors = R.reduce(
+      (acc, p) => R.concat(acc, R.ifElse(Array.isArray, R.identity, v => [v,])(R.pathOr([], ['data', p], errorMessages))),
+      [],
+      ['non_field_errors', 'detail']
+    );
+    if (errors && errors.length > 0 ) {
+      return (
+        <div className="alert alert-danger mt-4" role="alert">
+        { errors.map((message, key) =>
+          <p key={ key }><Trans i18nKey="register.register_error">{ message }</Trans></p>
+        ) }
+        </div>
+      )
+    }
+
+  }
+
+
   render() {
 
-    // const errorMessages = this.props.register.errorMessages;
+    const errorMessages = this.props.register.error ? this.props.register.errorMessages : false ;
 
     return (
       <div>
@@ -47,34 +97,35 @@
                 <div className="card-header bg-secondary border-0 mt-5 pt-5">
                   <h4 className="text-center card-title font-weight-bold text-lg" onClick={this.onClickHome}>IRI Notes</h4>
                   <form className="pt-3 ml-5 pl-5" onSubmit={this.submit}>
-                    <div className="form-group mb-2 ml-3 w-75" /*validationState={ errorMessages && errorMessages.has('username') ? 'error' : null }*/>
-                      <label className="col-form-label text-primary font-weight-bold mt-2">Nom d'utilisateur</label>
-                      <input className="form-control bg-irinotes-form border-0 text-muted" type="text" /*inputRef={ref => { this.username = ref; }}*/ />
-                      {/* { this.renderErrorMessage(errorMessages, 'username') } */}
+                    <div className="form-group mb-2 ml-3 w-75">
+                      <label className="col-form-label text-primary font-weight-bold mt-2" htmlFor="username"><Trans i18nKey="common.username">Nom d'utilisateur</Trans></label>
+                      <input className="form-control bg-irinotes-form border-0 text-muted" type="text" onChange={this.handleChange} value={this.state.username} name="username" />
+                      { errorMessages && this.renderErrorMessage(errorMessages.data, 'username') }
                     </div>
-                    <div className="form-group mb-2 ml-3 w-75" /*validationState={ errorMessages && errorMessages.has('email') ? 'error' : null }*/>
-                      <label className="col-form-label text-primary font-weight-bold mt-2">Email</label>
-                      <input className="form-control bg-irinotes-form border-0 text-muted" type="email" /*inputRef={ref => { this.email = ref; }}*/ />
-                      {/* { this.renderErrorMessage(errorMessages, 'email') } */}
+                    <div className="form-group mb-2 ml-3 w-75">
+                      <label className="col-form-label text-primary font-weight-bold mt-2" htmlFor="email"><Trans i18nKey="common.email">Email</Trans></label>
+                      <input className="form-control bg-irinotes-form border-0 text-muted" type="email" onChange={this.handleChange} value={this.state.email} name="email" />
+                      { errorMessages && this.renderErrorMessage(errorMessages.data, 'email') }
                     </div>
-                    <div className="form-group mb-2 ml-3 w-75" /*validationState={ errorMessages && errorMessages.has('password1') ? 'error' : null }*/>
-                      <label className="col-form-label text-primary font-weight-bold mt-2">Mot de passe</label>
-                      <input className="form-control bg-irinotes-form border-0 text-muted" type="password" /*inputRef={ref => { this.password1 = ref; }}*/ />
-                      {/* { this.renderErrorMessage(errorMessages, 'password1') } */}
+                    <div className="form-group mb-2 ml-3 w-75">
+                      <label className="col-form-label text-primary font-weight-bold mt-2" htmlFor="password1"><Trans i18nKey="common.password">Mot de passe</Trans></label>
+                      <input className="form-control bg-irinotes-form border-0 text-muted" type="password" onChange={this.handleChange} value={this.state.password1} name="password1" />
+                      { errorMessages && this.renderErrorMessage(errorMessages.data, 'password1') }
                     </div>
-                    <div className="form-group mb-2 ml-3 w-75" /*validationState={ errorMessages && errorMessages.has('password2') ? 'error' : null }*/>
-                      <label className="col-form-label text-primary font-weight-bold mt-2">Confirmer le mot de passe</label>
-                      <input className="form-control bg-irinotes-form border-0 text-muted" type="password" /*inputRef={ref => { this.password2 = ref; }}*/ />
-                      {/* { this.renderErrorMessage(errorMessages, 'password2') } */}
+                    <div className="form-group mb-2 ml-3 w-75">
+                      <label className="col-form-label text-primary font-weight-bold mt-2" htmlFor="password2"><Trans i18nKey="register.password_confirmation">Confirmer le mot de passe</Trans></label>
+                      <input className="form-control bg-irinotes-form border-0 text-muted" type="password" onChange={this.handleChange} value={this.state.password2} name="password2" />
+                      { errorMessages && this.renderErrorMessage(errorMessages.data, 'password2') }
                     </div>
+                    { this.renderNonFieldErrors(errorMessages) }
                     <div className="text-center mr-5 pr-5">
-                    <button type="submit" onClick={this.submit} className="btn btn-primary btn-lg text-secondary font-weight-bold mt-3">S'inscrire</button>
+                    <button type="submit" onClick={this.submit} className="btn btn-primary btn-lg text-secondary font-weight-bold mt-3"><Trans i18nKey="register.register">S'inscrire</Trans></button>
                     </div>
                   </form>
                 </div>
               </div>
               <p className="text-center">
-                <a className="text-muted" href="/login" onClick={ this.onClickLogin }>Déjà inscrit ? Se connecter.</a>
+                <a className="text-muted" href="/login" onClick={ this.onClickLogin }><Trans i18nKey="register.already_registered">Déjà inscrit ? Se connecter</Trans>.</a>
               </p>
             </div>
           </div>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/Reset.js	Tue Dec 18 02:27:22 2018 +0100
@@ -0,0 +1,128 @@
+import React, { Component } from 'react';
+import { connect } from 'react-redux';
+import { bindActionCreators } from 'redux';
+import * as authActions from '../actions/authActions';
+import { Trans } from 'react-i18next';
+import * as R from 'ramda';
+
+class Reset extends Component {
+
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      email: ''
+    }
+
+
+  }
+
+  reset = () => {
+    const { email } = this.state;
+
+    this.props.authActions.resetSubmit(email);
+  }
+
+  handleChange = (event) => {
+    const newState = {};
+    newState[event.target.name] = event.target.value;
+    this.setState(newState);
+  }
+
+  submit = (e) => {
+    e.preventDefault();
+
+    this.reset();
+  }
+
+  onClickLogin = (e) => {
+    e.preventDefault();
+    this.props.history.push('/login');
+  }
+
+  renderErrorMessage(errorMessages, fieldname) {
+    if (errorMessages && fieldname in errorMessages) {
+      return errorMessages[fieldname].map((message, key) =>
+        <p key={ key } className="form-text alert alert-danger mt-4" role="alert" >{ message }</p>
+      );
+    }
+  }
+
+  renderNonFieldErrors(errorMessages) {
+
+    if (errorMessages && errorMessages.error) {
+      return (
+        <div className="alert alert-danger mt-4" role="alert">
+          <Trans i18nKey="login.login_error">Unable to log in.</Trans>
+        </div>
+      )
+    }
+
+    const errors = R.reduce(
+      (acc, p) => R.concat(acc, R.ifElse(Array.isArray, R.identity, v => [v,])(R.pathOr([], ['data', p], errorMessages))),
+      [],
+      ['non_field_errors', 'detail']
+    );
+    if (errors && errors.length > 0 ) {
+      return (
+        <div className="alert alert-danger mt-4" role="alert">
+        { errors.map((message, key) =>
+          <p key={ key }><Trans i18nKey="reset.reset_error">{ message }</Trans></p>
+        ) }
+        </div>
+      )
+    }
+
+  }
+
+
+  render() {
+
+    const errorMessages = this.props.register.error ? this.props.register.errorMessages : false ;
+
+    return (
+      <div>
+        {/* <Navbar history={this.props.history} /> */}
+        <div className="container-fluid">
+          <div className="row">
+            <div className="col-lg-6 offset-md-3">
+              <div className="panel-login panel panel-default">
+                <div className="card-header bg-secondary border-0 mt-5 pt-5">
+                  <h4 className="text-center card-title font-weight-bold text-lg" onClick={this.onClickHome}>IRI Notes</h4>
+                  <form className="pt-3 ml-5 pl-5" onSubmit={this.submit}>
+                    <div className="form-group mb-2 ml-3 w-75">
+                      <label className="col-form-label text-primary font-weight-bold mt-2" htmlFor="email"><Trans i18nKey="common.email">Email</Trans></label>
+                      <input className="form-control bg-irinotes-form border-0 text-muted" type="email" onChange={this.handleChange} value={this.state.email} name="email" />
+                      { errorMessages && this.renderErrorMessage(errorMessages.data, 'email') }
+                    </div>
+                    { this.renderNonFieldErrors(errorMessages) }
+                    <div className="text-center mr-5 pr-5">
+                    <button type="submit" onClick={this.submit} className="btn btn-primary btn-lg text-secondary font-weight-bold mt-3"><Trans i18nKey="reset.reset">Reset passord</Trans></button>
+                    </div>
+                  </form>
+                </div>
+              </div>
+              <p className="text-center">
+                <a className="text-muted" href="/login" onClick={ this.onClickLogin }><Trans i18nKey="register.already_registered">Déjà inscrit ? Se connecter</Trans>.</a>
+              </p>
+            </div>
+          </div>
+        </div>
+      </div>
+    );
+  }
+}
+
+function mapStateToProps(state, props) {
+  return {
+    register: state.register
+  };
+}
+
+function mapDispatchToProps(dispatch) {
+  return {
+    authActions: bindActionCreators(authActions, dispatch)
+  }
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(Reset);
--- a/client/src/constants/actionTypes.js	Thu Dec 06 01:35:30 2018 +0100
+++ b/client/src/constants/actionTypes.js	Tue Dec 18 02:27:22 2018 +0100
@@ -25,6 +25,10 @@
 export const AUTH_REGISTER_REQUEST = 'AUTH_REGISTER_REQUEST';
 export const AUTH_REGISTER_ERROR = 'AUTH_REGISTER_ERROR';
 
+export const AUTH_RESET_SUBMIT = 'AUTH_RESET_SUBMIT';
+export const AUTH_RESET_REQUEST = 'AUTH_RESET_REQUEST';
+export const AUTH_RESET_ERROR = 'AUTH_RESET_ERROR';
+
 // Used both by login & register
 export const AUTH_LOGIN_SUCCESS = 'AUTH_LOGIN_SUCCESS';
 
--- a/client/src/index.js	Thu Dec 06 01:35:30 2018 +0100
+++ b/client/src/index.js	Tue Dec 18 02:27:22 2018 +0100
@@ -13,6 +13,7 @@
 import CreateGroup from './components/CreateGroup';
 import ReadOnlySession from './components/ReadOnlySession';
 import Register from './components/Register';
+import Reset from './components/Reset';
 import Settings from './components/Settings';
 import './index.css';
 import registerServiceWorker from './registerServiceWorker';
@@ -35,6 +36,7 @@
           <Route exact path="/sessions/:id" component={Session} />
           <Route exact path="/sessions" component={SessionList} />
           <Route exact path="/register" component={Register} />
+          <Route exact path="/reset" component={Reset} />
           <Route exact path="/login" component={Login} />
           <Route exact path="/create-group" component={CreateGroup} />
           <Route exact path="/read-only/:id" component={ReadOnlySession} />
--- a/client/src/locales/en/translation.json	Thu Dec 06 01:35:30 2018 +0100
+++ b/client/src/locales/en/translation.json	Tue Dec 18 02:27:22 2018 +0100
@@ -11,7 +11,8 @@
         "logout_modal": "Some sessions were not saved.<1></1>If you continue, they will be lost."
     },
     "login": {
-        "registration_message": "No account yet? Please register.",
+        "registration_message": "No account yet? Please register",
+        "reset_message": "Reset password.",
         "credentials_error": "Unable to log in with provided credentials.",
         "login_error": "Unable to log in."
     },
@@ -34,6 +35,15 @@
         "press_enter_msg": "Press <1>Enter</1> to add a note",
         "placeholder": "Type your note here..."
     },
+    "register": {
+        "register_error": "Error creating the account.",
+        "password_confirmation": "password confirmation",
+        "register": "Register",
+        "already_registered": "Already registered ? Connect"
+    },
+    "reset": {
+        "reset": "Reset password"
+    },
     "common": {
         "title": "title",
         "description": "description",
@@ -46,6 +56,7 @@
         "parameters": "parameters",
         "username": "username",
         "password": "password",
+        "email": "email",
         "delete": "delete",
         "name": "name",
         "create": "create",
--- a/client/src/locales/fr/translation.json	Thu Dec 06 01:35:30 2018 +0100
+++ b/client/src/locales/fr/translation.json	Tue Dec 18 02:27:22 2018 +0100
@@ -11,7 +11,8 @@
         "logout_modal": "Certaines sessions n'ont pas encore été sauvegardées.<1></1>Si vous continuez, elles seront perdues."
     },
     "login": {
-        "registration_message": "Pas encore inscrit ? Créer un compte.",
+        "registration_message": "Pas encore inscrit ? Créer un compte",
+        "reset_message": "Mot de passe oublié.",
         "credentials_error": "Impossible de se connecter avec les identifiants fournis.",
         "login_error": "Impossible de se connecter."
     },
@@ -34,6 +35,15 @@
         "press_enter_msg": "Appuyer sur <1>Entrée</1> pour ajouter une note",
         "placeholder": "Votre espace de prise de note..."
     },
+    "register": {
+        "register_error": "Erreur lors de la création du compte.",
+        "password_confirmation": "Confirmer le mot de passe",
+        "register": "S'inscrire",
+        "already_registered": "Déjà inscrit ? Se connecter"
+    },
+    "reset": {
+        "reset": "Soumettre"
+    },
     "common": {
         "title": "titre",
         "description": "description",
@@ -46,6 +56,7 @@
         "parameters": "paramètres",
         "username": "nom d'utilisateur",
         "password": "mot de passe",
+        "email": "e-mail",
         "delete": "supprimer",
         "name": "nom",
         "create": "créer",
--- a/client/src/reducers/authReducer.js	Thu Dec 06 01:35:30 2018 +0100
+++ b/client/src/reducers/authReducer.js	Tue Dec 18 02:27:22 2018 +0100
@@ -114,7 +114,7 @@
     case types.AUTH_REGISTER_ERROR:
       return {
         loading: false,
-        success: action.type === types.AUTH_LOGIN_SUCCESS,
+        success: (action.type === types.AUTH_LOGIN_SUCCESS || action.type === types.AUTH_LOGIN_SUCCESS),
         error: action.type === types.AUTH_REGISTER_ERROR,
         errorMessages: action.type === types.AUTH_REGISTER_ERROR ? action.error : {}
       }
--- a/client/src/sagas/authSaga.js	Thu Dec 06 01:35:30 2018 +0100
+++ b/client/src/sagas/authSaga.js	Tue Dec 18 02:27:22 2018 +0100
@@ -100,6 +100,33 @@
   }
 }
 
+export function* watchResetSubmit() {
+  while (true) {
+    const { email } = yield take(types.AUTH_RESET_SUBMIT);
+    yield put({ type: types.AUTH_RESET_REQUEST, email });
+  }
+}
+
+
+function* watchResetRequest(context) {
+  while (true) {
+    try {
+
+        const { email } = yield take(types.AUTH_RESET_REQUEST);
+
+        const client = context.client;
+        yield client.post('/api/auth/password/reset/', { email });
+
+        context.history.push('/sessions');
+
+    } catch(e) {
+      yield put({ type: types.AUTH_RESET_ERROR, error: e });
+    }
+  }
+}
+
+
+
 // ---
 
 export default function* rootSaga(context) {
@@ -110,5 +137,7 @@
     watchRegisterRequest(context),
     watchStoreToken(),
     watchUpdateSettings(context),
+    watchResetSubmit(),
+    watchResetRequest(context),
   ])
 }
--- a/src/.env.tmpl	Thu Dec 06 01:35:30 2018 +0100
+++ b/src/.env.tmpl	Tue Dec 18 02:27:22 2018 +0100
@@ -42,8 +42,33 @@
 
 # expiration delta for JWT tokens (in seconds)
 # default: 3600
-# JWT_EXPIRATION_DELTA = 3600
+# JWT_EXPIRATION_DELTA=3600
 
 # expiration refresh delta for JWT tokens (in seconds)
 # default: 3600*24*7 = 604800
-# JWT_REFRESH_EXPIRATION_DELTA = 604800
+# JWT_REFRESH_EXPIRATION_DELTA=604800
+
+# email backend c.f. https://docs.djangoproject.com/en/2.1/topics/email/#obtaining-an-instance-of-an-email-backend
+# default: 'django.core.mail.backends.console.EmailBackend'
+# EMAIL_BACKEND=django.core.mail.backends.console.EmailBackend
+
+# email host
+# default: localhost
+# EMAIL_HOST=localhost
+
+# email port (integer)
+# default: 25
+# EMAIL_PORT=25
+
+# email host user
+# default: None
+# EMAIL_HOST_USER=
+
+# email host password
+# default: None
+# EMAIL_HOST_PASSWORD=
+
+# email use TLS
+# default: False
+# EMAIL_USE_TLS=False
+
--- a/src/irinotes/settings.py	Thu Dec 06 01:35:30 2018 +0100
+++ b/src/irinotes/settings.py	Tue Dec 18 02:27:22 2018 +0100
@@ -284,3 +284,12 @@
 )
 
 CORS_URLS_REGEX = r'^/api/.*$'
+
+ACCOUNT_EMAIL_VERIFICATION = 'none'
+
+EMAIL_BACKEND = config('EMAIL_BACKEND', 'django.core.mail.backends.console.EmailBackend')
+EMAIL_HOST = config('EMAIL_HOST', 'localhost')
+EMAIL_PORT = config('EMAIL_PORT', 25, cast=int)
+EMAIL_HOST_USER = config('EMAIL_HOST_USER', None)
+EMAIL_HOST_PASSWORD = config('EMAIL_HSOT_PASSWORD', None)
+EMAIL_USE_TLS = config('EMAIL_USE_TLS', False, cast=bool)