React Native Gestionnaire de mots de passe : Gestion des mots de passe

07 novembre 2017

Thibault MOCELLIN

Bonjour à tous dans cet article nous allons ajouter la gestion des mots de passe à notre application.

Affichage des mots de passe

Pour commencer cet article nous allons connecter l'écran Password.js à redux et récupérer la liste des mots de passe qui est dans le reducers data.

Comme à chaque fois nous allons ajouter les imports de connect, bindActionCreators et reduxState :

import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import type { ReduxState } from '../reducers/types'

Ensuite nous ajoutons la fonction mapStateToProps et nous connectons l'écran :

function mapStateToProps(state: ReduxState) {
  const passwords = _.values(state.data.passwords.byId)
  return {
    passwords,
  }
}
export default connect(mapStateToProps, dispatch => ({
  actions: bindActionCreators({}, dispatch),
}))(PassworsScreen)

Ici nous utilisons la fonction values de Lodash pour transformer notre objet byId qui contient les mots de passe sous la forme suivante en un tableau d'objet :

byId: {
  idPassword1: {
    /*attributs du mots de passe */
  }
  idPassword2: {
    /*attributs du mots de passe 2*/
  }
}

Et nous ajoutons un objet vide dans le bindActionCreators puisque nous n'avons pas d'actions pour le moment.

Il faut maintenant modifier le type State de l'écran et ajouter le type Props :

type State = {
  searchResults: Array<Password>,
  searchValue: string,
}

type Props = {
  passwords: Array<Password>,
  navigation: Object,
}

A présent nous avons retirer l'attributs passwords du state il faut donc remplacer toutes les occurrences de this.state.passwords par this.props.passwords et il faut aussi modifier le state initial du composant :

state = {
  searchValue: '',
  searchResults: this.props.passwords,
}

On définit que la liste des résultats de recherche que nous utilisons comme source de données pour notre composant PasswordList aura pour valeurs this.props.passwords.

Actuellement il reste un problème étant donné qu'on utilise this.state.searchResults comme source de données et qu'on défini ces valeurs seulement à l'initialisation du composant lorsque la liste des mots de passe de this.props.passwords va être altérée (ajout/modification/suppression) le state ne sera pas mis à jour et donc les modifications ne seront pas répercutées à l'écran.

Pour palier à ce problème ajoutez le code suivant :

componentWillReceiveProps(nextProps: Props) {
  this.setState({
    searchResults: nextProps.passwords,
  });
}

La fonction componenetWillReceiveProps est exécutée automatiquement des lors que les propriétés du composant sont modifiées.

De cette manière nous pallions au problème de la mise à jour du state puisque dès que la liste sera modifiée nous mettrons à jour le state automatiquement.

Action / ActionType et Reducers

Nous allons commencer par ajouter nos types d'actions pour l'ajout, la modification et la suppression d'un mot de passe ainsi que la suppression du tous les mots de passe.

Rajoutez les types suivants dans le fichier types.js du dossier actions :

export type AddPasswordAction = {
  type: 'ADD_PASSWORD',
  password: Password,
};
export type UpdatePasswordAction = {
  type: 'UPDATE_PASSWORD',
  password: Password,
};
export type DeletePasswordAction = {
  type: 'DELETE_PASSWORD',
  passwordKey: string,
};
export type DeleteAllPasswordsAction = {
  type: 'DELETE_ALL_PASSWORDS',
};

export type Action =
  ...
  | AddPasswordAction
  | UpdatePasswordAction
  | DeletePasswordAction
  | DeleteAllPasswordsAction;

Ensuite avant de créer les actions nous allons ajouter un fichier reduxUtils.js dans le dossier common ce fichier contiendra deux fonction permettant de supprimer un enregistrement d'un tableaux et l'autre de supprimer un attribut d'un objet nous allons avoir besoin des ces deux fonctions dans les actions et dans le reducer data pour pouvoir mettre à jour la liste des mots de passe.

Ajoutez reduxUtils.js dans le dossier common :

/*
* @flow
*/
import _ from 'lodash'

export const removeInObject = (object: Object, keyToRemove: string) =>
  _.pickBy(object, obj => obj.key !== keyToRemove)
export const removeInArray = (array: Array<Object>, keyToRemove: string) =>
  array.filter(key => key !== keyToRemove)

Maintenant passons au actions, créez un fichier nommé password.js toujours dans le dossier actions :

/*
* @flow
*/

import CryptoJS from 'crypto-js'
import type {
  ThunkAction,
  Dispatch,
  AddPasswordAction,
  UpdatePasswordAction,
  DeletePasswordAction,
  DeleteAllPasswordsAction,
  UpdateCryptedPasswordsAction,
} from './types'
import type { Password } from '../types/Password'
import type { NormalizedState } from '../types/NormalizedState'
import { Encrypt } from '../common/CryptoHelper'
import { removeInObject, removeInArray } from '../common/ReduxUtils'
/*
*** Actions ***
*/

export const EditPassword = (
  password: Password,
  edition: boolean,
  key: CryptoJS.WordArray,
  iv: string,
  passwordsState: NormalizedState,
  back: () => void
): ThunkAction => (dispatch: Dispatch) => {
  let updatedPasswordsState = {}

  if (edition) {
    updatedPasswordsState = {
      ...passwordsState,
      byId: { ...passwordsState.byId, [password.key]: password },
    }
    dispatch(updatePassword(password))
  } else {
    updatedPasswordsState = {
      ...passwordsState,
      byId: { ...passwordsState.byId, [password.key]: password },
      allIds: [...passwordsState.allIds, password.key],
    }
    dispatch(addPassword(password))
  }

  const cryptedPasswords = Encrypt(
    JSON.stringify(updatedPasswordsState),
    key,
    iv
  )
  dispatch(updateCryptedPasswords(cryptedPasswords))

  back()
}
export const DeletePassword = (
  passwordKey: string,
  key: CryptoJS.WordArray,
  iv: string,
  passwordsState: NormalizedState,
  back: () => void
): ThunkAction => (dispatch: Dispatch) => {
  let updatedPasswordsState = {}

  updatedPasswordsState = {
    ...passwordsState,
    byId: removeInObject(passwordsState.byId, passwordKey),
    allIds: removeInArray(passwordsState.allIds, passwordKey),
  }
  dispatch(removePassword(passwordKey))

  const cryptedPasswords = Encrypt(
    JSON.stringify(updatedPasswordsState),
    key,
    iv
  )
  dispatch(updateCryptedPasswords(cryptedPasswords))

  back()
}

export const DeleteAllPasswords = (
  key: CryptoJS.WordArray,
  iv: string
): ThunkAction => (dispatch: Dispatch) => {
  const emptyPassword = JSON.stringify({ allIds: [], byId: {} })
  dispatch(removeAllPasswords())
  const cryptedPasswords = Encrypt(emptyPassword, key, iv)
  dispatch(updateCryptedPasswords(cryptedPasswords))
}

/*
*** Actions Creator ***
*/

const addPassword = (password: Password): AddPasswordAction => ({
  type: 'ADD_PASSWORD',
  password,
})

const updatePassword = (password: Password): UpdatePasswordAction => ({
  type: 'UPDATE_PASSWORD',
  password,
})
const removePassword = (passwordKey: string): DeletePasswordAction => ({
  type: 'DELETE_PASSWORD',
  passwordKey,
})

const removeAllPasswords = (): DeleteAllPasswordsAction => ({
  type: 'DELETE_ALL_PASSWORDS',
})
const updateCryptedPasswords = (
  cryptedPassword: string
): UpdateCryptedPasswordsAction => ({
  type: 'UPDATE_CRYPTED_PASSWORDS',
  cryptedPassword,
})

Pour chacune des actions on vient mettre à jour la liste de mots de passe cryptés.

Passons au reducer, chacune des actions que nous venons de créer vont modifier le state data. Modifiez le fichier data.js dans le dossiers reducers :

/*
* @flow
*/

import type { Action } from '../actions/types'
import type { DataState } from './types'
import { removeInObject, removeInArray } from '../common/ReduxUtils'

const initialState: DataState = {
  passwords: { allIds: [], byId: {} },
  key: '',
  error: '',
}

const dataState = (
  state: DataState = initialState,
  action: Action
): DataState => {
  switch (action.type) {
    case 'UNLOCK_APP':
      return {
        ...state,
        passwords: action.passwords,
        key: action.key,
        error: '',
      }
    case 'UNLOCK_APP_FAIL':
      return {
        ...state,
        error: action.error,
      }
    case 'ADD_PASSWORD':
      return {
        ...state,
        passwords: {
          ...state.passwords,
          byId: {
            ...state.passwords.byId,
            [action.password.key]: action.password,
          },
          allIds: [...state.passwords.allIds, action.password.key],
        },
      }
    case 'UPDATE_PASSWORD':
      return {
        ...state,
        passwords: {
          ...state.passwords,
          byId: {
            ...state.passwords.byId,
            [action.password.key]: action.password,
          },
        },
      }
    case 'DELETE_PASSWORD':
      return {
        ...state,
        passwords: {
          ...state.passwords,
          byId: removeInObject(state.passwords.byId, action.passwordKey),
          allIds: removeInArray(state.passwords.allIds, action.passwordKey),
        },
      }
    case 'DELETE_ALL_PASSWORDS':
      return { ...state, passwords: { allIds: [], byId: {} } }
    default:
      return state
  }
}

export default dataState

Ajout d'un mot de passe

Maintenant que nous avons créé nos actions et modifié le reducer data nous allons pouvoir connecter l'écran d'édition à redux et commencer par mettre en place la création d'un mot de passe.

Tout d'abord nous allons ajouter la fonction qui permet de générer les mots de passe dans le PasswordHelper :

export const GeneratePassword = (length: number): string => {
  const plength = length
  const keylistalpha = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
  const keylistint = '123456789'
  const keylistspec = '!@#_+/?$%'
  let temp = ''
  let len = plength / 2
  len -= 1
  for (let i = 0; i < len; i += 1) {
    temp += keylistalpha.charAt(Math.floor(Math.random() * keylistalpha.length))
  }
  for (let i = 0; i < len; i += 1) {
    temp += keylistspec.charAt(Math.floor(Math.random() * keylistspec.length))
  }
  for (let i = 0; i < len; i += 1) {
    temp += keylistint.charAt(Math.floor(Math.random() * keylistint.length))
  }
  temp = temp
    .split('')
    .sort(() => 0.5 - Math.random())
    .join('')

  return temp
}

Nous allons aussi ajouter la librairie uuid qui va nous permettre de générer un uuid dès que l'on créé un mot de passe cet uuid sera utilisé pour l'attribut key de notre mot de passe.

yarn add uuid

L'écran d'édition est utilisé pour la création mais aussi pour la modification d'un mot de passe. Il faut donc que nous ajoutions un moyen de définir lorsqu'on arrive sur cet écran si l'on en création et dans ce cas initialiser le state avec des valeurs par défaut ou alors si l'on est en modification et dans ce cas récupérer les valeurs du mot de passe que l'on a sélectionné.

Pour cela nous allons passez l'attribut key dans les paramètres de navigation lorsqu'on sélectionne un mot de passe et lorsqu'on est en création nous passerons la valeur 0.

Nous allons modifié l'écran Passwords pour ajouter le paramètre de navigation lorsqu'on ajoute un mot de passe il faut donc modifié la fonction addNewItem de cette manière :

addNewItem() {
  this.props.navigation.navigate('Edit', { passwordKey: 0 });
}

Nous pouvons maintenant connecter l'écran Edit.js à redux, on commence par rajouter les imports nécéssaires :

import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import uuidV4 from 'uuid/v4'
import CryptoJS from 'crypto-js'
import type { ReduxState } from '../reducers/types'
import { GeneratePassword } from '../common/PasswordHelper'
import type { Password } from '../types/Password'
import type { NormalizedState } from '../types/NormalizedState'
import * as PasswordActions from '../actions/password'

Ensuite on ajoute mapStateToProps et bindActionCreator :

function mapStateToProps(state: ReduxState, ownProps: Object) {
  const passwordKey = ownProps.navigation.state.params.passwordKey
  const edition = passwordKey !== 0
  const password = state.data.passwords.byId[passwordKey]
  return {
    edition,
    password,
    passwordLength: state.settings.passwordLength,
    autoGeneration: state.settings.autoGeneration,
    cryptoKey: state.data.key,
    iv: state.user.iv,
    passwords: state.data.passwords,
  }
}
export default connect(mapStateToProps, dispatch => ({
  actions: bindActionCreators(PasswordActions, dispatch),
}))(ReadOnlyScreen)

Ici vous pouvez remarquer qu'on a rajouté un paramètre nommée ownProps à la fonction mapStateToProps. Ce paramètre nous permet de récupérer tous les propriétés qui sont passées à l'écran mais que ne proviennent pas de redux.

Nous récupérons la valeur de passwordKey à partir de ownProps puis nous déterminons si on est en modification et on termine par récupérer le mot de passe à partir de sa key dans la liste des mots de passe.

On récupère aussi les réglages concernant l'application ainsi que la clé, l'iv et l'objet passwords depuis le state de l'application.

Ensuite il faut rajouter le type Props et modifier la déclaration de la classe :

type Props = {
  edition: boolean,
  password: Password,
  passwordLength: number,
  autoGeneration: boolean,
  navigation: Object,
  actions: Object,
  cryptoKey: CryptoJS.WordArray,
  iv: string,
  passwords: NormalizedState,
};

class ReadOnlyScreen extends Component<void, Props, State>{
  ...

Puis nous allons modifier la déclaration du state initiale et rajouter un constructeur:

state = {
    key: uuidV4(),
    name: '',
    color: PRIMARY,
    password: this.props.autoGeneration ? GeneratePassword(this.props.passwordLength) : '',
    icon: 'cubes',
    login: '',
    url: '',
    modalIsOpen: false,
  };

  constructor(props: Props) {
    super(props);
    if (this.props.edition) {
      const { key, name, color, password, icon, login, url } = this.props.password;
      this.state = {
        key,
        name,
        color,
        password,
        icon,
        login,
        url,
        modalIsOpen: false,
      };
    }
  }

Dans le code ci-dessus on définit le state avec les valeurs par défaut et ensuite dans le constructeur de la classe on teste si l'on est en édition, si c'est le cas alors on redéfinit le state avec les valeurs du mot de passe passer en propriété.

Enfin il ne nous reste plus qu'a modifier les fonctions generatePassword et save :

save() {
  const { cryptoKey, iv, passwords, edition,navigation } = this.props;
  const passwordToEdit: Password = {
    key: this.state.key,
    name: this.state.name,
    color: this.state.color,
    password: this.state.password,
    icon: this.state.icon,
    login: this.state.login,
    url: this.state.url,
  };
  this.props.actions.EditPassword(passwordToEdit, edition, cryptoKey, iv, passwords, () =>
    navigation.goBack(),
  );
}

generatePassword() {
  this.setState({
    password: GeneratePassword(this.props.passwordLength),
  });
}

Modification et suppression d'un mot de passe

La première chose que nous allons faire est de modifier la navigation lorsqu'on sélectionne un mot de passe dans la liste pour y ajouter le paramètre passwordKey comme nous l'avons fait pour la fonction addNewItem.

Modifiez la fonction showPassword comme ceci :

showPassword(password: Password) {
  this.props.navigation.navigate('ReadOnly', {
    siteName: password.name,
    passwordKey: password.key,
  });
}

Ensuite nous allons connecter l'écran ReadOnly à redux comme précédemment on commence par ajouter les imports :

import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import CryptoJS from 'crypto-js';
import type { ReduxState } from '../reducers/types';
import type { Password } from '../types/Password';
import type { NormalizedState } from '../types/NormalizedState';
import { deletePassword } from '../actions/passwords';
Ensuite on ajoute mapStateToProps et bindActionCreator :

function mapStateToProps(state: ReduxState, ownProps: Object) {
  const passwordKey = ownProps.navigation.state.params.passwordKey;
  const password = state.data.passwords.byId[passwordKey];
  return {
    password,
    cryptoKey: state.data.key,
    iv: state.user.iv,
    passwords: state.data.passwords,
  };
}
export default connect(mapStateToProps, dispatch => ({
  actions: bindActionCreators({ DeletePassword }, dispatch),
}))(ReadOnlyScreen);

Puis nous allons supprimer le type State nous l'avions ajoutez lors de la création de l'écran pour afficher les données mais maintenant toutes les informations dont nous avons besoin sont contenues dans les propriétés.

Il faut cependant ajouté le type Props :

type Props = {
  password: Password,
  cryptoKey: CryptoJS.WordArray,
  iv: string,
  passwords: NormalizedState,
}

Enfin modifiez l'écran comme ceci:

class ReadOnlyScreen extends Component<void, Props, void> {
  editPassword() {
    this.props.navigation.navigate('Edit', {
      passwordKey: this.props.password.key,
    })
  }

  copyPassword() {
    console.log('copy password')
  }

  deletePassword() {
    const { cryptoKey, iv, passwords, navigation } = this.props
    this.props.actions.DeletePassword(
      this.props.password.key,
      cryptoKey,
      iv,
      passwords,
      () => navigation.goBack()
    )
  }

  render() {
    if (!this.props.password) {
      return <View />
    }
    const { icon, color, name, password, login, url } = this.props.password
    return (
      <ScrollView style={styles.scrollContent}>
        <View style={styles.container}>
          <View style={styles.iconCtnr}>
            <View style={styles.icon}>
              <Icon name={icon} size={35} color={color} />
            </View>
          </View>
          <ReadOnlyRow label={strings.siteName} value={name} />
          <ReadOnlyRow label={strings.siteUrl} value={url} />
          <ReadOnlyRow label={strings.userName} value={login} />
          <ReadOnlyRow label={strings.password} value={password} />

          <View style={styles.actionContainer}>
            <TouchableOpacity
              style={styles.action}
              onPress={() => this.copyPassword()}
            >
              <Text style={[styles.actionLabel, { color: '#647CF6' }]}>
                {strings.copy}
              </Text>
            </TouchableOpacity>
            <TouchableOpacity
              style={styles.action}
              onPress={() => this.editPassword()}
            >
              <Text style={[styles.actionLabel, { color: PRIMARY }]}>
                {strings.edit}
              </Text>
            </TouchableOpacity>
            <TouchableOpacity
              style={styles.action}
              onPress={() => this.deletePassword()}
            >
              <Text style={[styles.actionLabel, { color: DELETE_COLOR }]}>
                {strings.delete}
              </Text>
            </TouchableOpacity>
          </View>
        </View>
      </ScrollView>
    )
  }
}

Dans la méthode render on teste si le mot de passe venant des propriétés est null et si c'est le cas nous retournons une simple vue. Nous faisons ça car étant données que le mot de passe est récupéré depuis le state redux lorsque nous allons supprimer le mot de passe les propriétés vont être modifiées et donc la méthode render sera exécuter à nouveaux. Puisque le password n'existera plus alors la récupération des données via

const { icon, color, name, password, login, url } = this.props.password

retournera une erreur. Donc le fait de retourné la vue va nous permettre de contourner ce problème le temps que la navigation vers l'écran de la liste mot de passe soit effective.

Suppression de tous les mots de passe

La suppression de tous les mots de passe est accessible dans les réglages de l'application, cet écran est déjà connecté à redux nous allons devoir juste rajouter l'action deleteAllPassword dans le bindActionCreator et ensuite executer cette action lors du clique sur la ligne.

Dans l'écran Settings.js rajoutez l'import de l'action DeleteAllPasswords et CryptoJS:

import CryptoJS from 'crypto-js'
import { DeleteAllPasswords } from '../actions/passwords'

Ensuite il faut modifié mapStateToProps pour ajouter les propriétés key et iv et ensuite modifier le bindActionCreator pour rajouter l'action DeleteAllPassord :

function mapStateToProps(state: ReduxState) {
  return {
    passwordLength: state.settings.passwordLength,
    autoGeneration: state.settings.autoGeneration,
    cryptoKey: state.data.key,
    iv: state.user.iv,
  }
}
export default connect(mapStateToProps, dispatch => ({
  actions: bindActionCreators(
    { ...{}, ...{ DeleteAllPasswords }, ...SettingsActions },
    dispatch
  ),
}))(SettingsScreen)

Puis on rajoute iv et cryproKey dans le type Props :

type Props = {
  passwordLength: number,
  autoGeneration: boolean,
  navigation: Object,
  actions: Object,
  iv: string,
  cryptoKey: CryptoJS.WordArray,
}

Et enfin on modifie la fonction deleteAllPassword :

deleteAllPasswords() {
  const { iv, cryptoKey } = this.props;
  Alert.alert(strings.clear, strings.clearConfirmation, [
    { text: strings.cancel, style: 'cancel' },
    {
      text: strings.delete,
      onPress: () => this.props.actions.DeleteAllPasswords(cryptoKey, iv),
    },
  ]);
}

Ajout du mot de passe au clipboard

Pour terminer cet article nous allons rajouter la dernière action qui manque à l'écran ReadOnly qui est la copie du mot de passe dans le clipboard.

React Native permet de copier des éléments dans le clipboard de manière très simple il suffit d'importer Clipboard depuis React Native est d'utiliser la fonction suivante :

Clipboard.setString('hello world')

Dans l'écran ReadOnly ajoutez Clipboard et Alert dans les imports de React Native puis modifiez la fonction copyPassword comme ceci :

copyPassword() {
 Clipboard.setString(this.props.password.password);
 Alert.alert(strings.succes, strings.copyMessage);
}

Enfin ajoutez les ressources manquantes :

fr.js

...
succes: 'Succès',
copyMessage: 'Le mot de passe a bien été ajouté au presse-papier

en.js

...
succes: 'Success',
copyMessage: 'The password has been added to the clipboard',

strings.js

...
succes: I18n.t('succes'),
copyMessage: I18n.t('copyMessage'),

Nous en avons maintenant terminé avec la gestion des mots de passe vous pouvez retrouver le code source ici.

Vous souhaitez suivre les derniers conseils, astuces et articles ?

Abonnez-vous à notre newsletter!

Partager cet article

2 mois d'hébergement offert

Dans cette catégorie

DWA Studio sur FacebookDWA Studio sur TwitterDWA Studio sur GithubDWA Studio sur Google Plus

© 2018, DWA Studio. Tous droits réservés

Credits freepik - hero - 404