18F/e-QIP-prototype

View on GitHub
src/views/Login/Login.jsx

Summary

Maintainability
A
0 mins
Test Coverage
import React from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import { withRouter } from 'react-router'
import { connect } from 'react-redux'

import i18n from 'util/i18n'
import { env } from 'config'
import { api, getQueryValue } from 'services'
import { login } from 'actions/AuthActions'
import * as errorCodes from 'constants/errorCodes'
import { Consent } from 'components/Form'

export class Login extends React.Component {
  constructor(props) {
    super(props)

    this.state = {
      username: '',
      password: '',
      showPassword: false,
      samlData: {},
      samlError: null,
    }
  }

  componentWillMount() {
    this.redirect()

    if (env.SamlEnabled()) {
      api.saml().then((response) => {
        const { data } = response
        this.setState({ samlData: data })
      }).catch((e) => {
        console.warn('SAML initialization failed', e)
        // TODO - what are potential errors here?
        this.setState({
          samlData: {},
          samlError: i18n.t('login.error.saml'),
        })
      })
    }
  }

  onUsernameChange = (e) => {
    this.setState({ username: e.target.value })
  }

  onPasswordChange = (e) => {
    this.setState({ password: e.target.value })
  }

  togglePassword = () => {
    this.setState(prev => ({
      showPassword: !prev.showPassword,
    }))
  }

  login = (event) => {
    const { dispatch } = this.props
    const { username, password } = this.state

    event.preventDefault()
    dispatch(login(username, password))
  }

  redirect() {
    const { history } = this.props

    const err = getQueryValue(window.location.search, 'error')
    if (err) {
      switch (err) {
        case 'token':
          history.push('/token')
          return
        case 'access_denied':
          history.push('/accessdenied')
          return
        case 'saml_logout_failed':
          history.push('/error')
          break
        default:
          history.push('/error')
          break
      }
    }
  }

  errorMessage() {
    const { errors } = this.props

    if (!errors || errors.length < 0) {
      return null
    }

    // Only show first error for now? UI TBD if multiple
    const [error] = errors
    const { message, code } = error

    let errorMessage
    switch (code) {
      case errorCodes.USERNAME_MISSING:
      case errorCodes.PASSWORD_MISSING:
        errorMessage = i18n.t('login.error.generic')
        break
      case errorCodes.NETWORK_ERROR:
        errorMessage = i18n.t('login.error.network')
        break
      case errorCodes.UNKNOWN_ERROR:
        errorMessage = i18n.t('login.error.network', { code })
        break
      default:
        errorMessage = message // default to message sent by API
    }

    return this.renderLoginError(errorMessage)
  }

  renderLoginError = error => (
    <div className="field no-margin-bottom">
      <div className="table">
        <div className="usa-alert usa-alert-error" role="alert">
          <div className="usa-alert-body">
            <h5 className="usa-alert-heading">{i18n.t('login.error.title')}</h5>
            <p>{error}</p>
          </div>
        </div>
      </div>
    </div>
  )

  renderSAMLAuth() {
    const { samlData, samlError } = this.state
    const { URL = '', Base64XML } = samlData

    return (
      <div id="saml" className="auth saml">
        <form method="post" action={URL}>
          {Base64XML && (
            <input
              type="hidden"
              name="SAMLRequest"
              value={Base64XML}
            />
          )}

          {samlError && this.renderLoginError(samlError)}

          <button
            type="submit"
            className="usa-button-big"
            disabled={!URL || !Base64XML}
          >
            <span>{i18n.t('login.saml.button')}</span>
          </button>
        </form>
      </div>
    )
  }

  renderBasicAuth() {
    const { errors } = this.props
    const { username, password, showPassword } = this.state
    const pwClass = classnames(
      'password',
      'help',
      { 'usa-input-error': errors && errors.length }
    )

    const showPasswordButtonTitle = showPassword
      ? i18n.t('login.basic.hide.title')
      : i18n.t('login.basic.show.title')

    const showPasswordButtonText = showPassword
      ? i18n.t('login.basic.hide.text')
      : i18n.t('login.basic.show.text')

    return (
      <div id="basic" className="auth basic">
        <form onSubmit={this.login}>
          <div>
            <label htmlFor="user">{i18n.t('login.basic.username.label')}</label>
            <input
              id="user"
              name="user"
              type="text"
              value={username}
              onChange={this.onUsernameChange}
            />
          </div>
          <div className={pwClass}>
            <label htmlFor="password">
              {i18n.t('login.basic.password.label')}
            </label>
            <input
              id="password"
              name="password"
              type={showPassword ? 'text' : 'password'}
              value={password}
              onChange={this.onPasswordChange}
            />
            <div className="peek">
              <button
                type="button"
                id="show-password"
                onClick={this.togglePassword}
                title={showPasswordButtonTitle}
              >
                {showPasswordButtonText}
              </button>
            </div>
            {this.errorMessage()}
          </div>
          <div>
            <button type="submit" className="usa-button-big">
              {i18n.t('login.basic.button')}
            </button>
            <a
              id="forgot-password"
              href="#TODO"
              title={i18n.t('login.basic.forgot.title')}
            >
              {i18n.t('login.basic.forgot.text')}
            </a>
          </div>
        </form>
      </div>
    )
  }

  renderLogin() {
    const samlEnabled = env.SamlEnabled()
    const basicEnabled = env.BasicAuthenticationEnabled()

    const loginClasses = classnames('table', {
      two: samlEnabled && basicEnabled,
      one: (samlEnabled && !basicEnabled) || (!samlEnabled && basicEnabled),
      zero: !samlEnabled && !basicEnabled,
    })

    return (
      <div className={loginClasses}>
        {samlEnabled && this.renderSAMLAuth()}
        {basicEnabled && this.renderBasicAuth()}
      </div>
    )
  }

  render() {
    const { dispatch, authenticated } = this.props
    const modalOpen = document.body.classList.contains('modal-open')

    return (
      <div className="login eapp-core" id="login">
        <Consent dispatch={dispatch} />
        <div
          id="seal-header"
          className="seal-header text-center"
          aria-hidden={modalOpen}
          aria-disabled={modalOpen}
        >
          <div className="content">
            <img
              src="/img/nbis-seal.png"
              alt="National Background Investigation Services"
            />
            <h2>{i18n.t('login.title')}</h2>
          </div>
        </div>
        <div
          className="content"
          aria-hidden={modalOpen}
          aria-disabled={modalOpen}
        >
          {!authenticated && this.renderLogin()}
        </div>
      </div>
    )
  }
}

Login.propTypes = {
  authenticated: PropTypes.bool,
  history: PropTypes.object,
  dispatch: PropTypes.func,
  errors: PropTypes.array,
}

Login.defaultProps = {
  authenticated: false,
  history: {},
  dispatch: () => {},
  errors: null,
}

/**
 * Maps the relevant subtree state from the applications state tree.
 * In this case, we pull the authentication sub-state. This is mapped
 * to the authentication reducer. When actions are dispatched, this
 * method is executed which causes a re-render.
 */
function mapStateToProps(state) {
  const { authentication } = state

  return {
    authenticated: authentication.authenticated,
    errors: authentication.error,
  }
}

// Wraps the the App component with connect() which adds the dispatch()
// function to the props property for this component
export default withRouter(connect(mapStateToProps)(Login))