Login Component

From Expertiza_Wiki
Jump to navigation Jump to search

Introduction

In the original Expertiza RoR application, user login is done by storing unique user ids in session store. All the authentication information is stored on the server side, in the session hash. Here, our server is stateful, keeping track of whether or not a user has logged in, and who that user is. When using Rails API, we'll need to tell the client, i.e. our React app, to store some kind of unique identifier and send that unique identifier to Rails API on every request. Rails can then use the unique identifier to identify the user making the request. We have used JWT authentication for authenticating the user. JWT encrypts user's unique ID into a compact and secure JSON web token. This token is generated by Rails and sent to the client. The client sends it to the back-end in HTTP Authentication header in all subsequent requests.

When a user logs in using the React application, a redux action is dispatched, which posts the user's email id and password to the Rails back-end. Upon successful authentication, the Rails API sends back a JWT token, which is then stored in the Redux store, and can be used to make subsequent requests.

Rails

The HTTP request made is passed onto session_controller.rb found in app/controllers/api/v1/sessions_controller.rb.

def create
           user = User.find_by(name: auth_params[:name])
           if user && user.valid_password?(auth_params[:password])
           jwt = JWT.encode( {user: user.id , exp: (Time.now + 1.week).to_i },
                               Rails.application.secrets.secret_key_base,
                               'HS256')
           render json: {jwt: jwt}
           else
               head(:unauthorized)
           end
       end

This encodes the user id into a jwt token, which is then rendered as a json object and returned to the front-end.

Redux

Redux actions for this component can be found in client/src/redux/actions/Auth.js. These actions are mapped to the React component using mapStateToDispatch, and are dispatched when suitable events take place on the React Application. We use axios here to make suitable HTTP requests to the controllers in the back-end.

The first action we will consider here is auth

export const auth = (name, password) => {

   return dispatch => {
       if( !localStorage.getItem('jwt') || (localStorage.getItem('jwt') && 
               localStorage.getItem('jwt_exp') <= (Date.now()/60) ) ) {
           axios({
               method: 'post',
               url: 'sessions',
               headers: { "Content-Type": "application/json"},
               data: {auth: { name: name, password: password }}
           })
           .then(response => {
               localStorage.setItem('jwt', response.data.jwt)
               localStorage.setItem('jwt_exp',(Date.now()/60) + 60*60*24*7)
               dispatch(authSuccess(response.data.jwt))
               dispatch(actions.fetchProfile())
               dispatch(actions.fetchInstitutions())
               dispatch(actions.fetchStudentsTeamedWith())
               dispatch(actions.fetchStudentTasks())
               dispatch(actions.fetchTeamCourse())
               dispatch(actions.fetchTasks())
               dispatch(actions.fetchRevisions())
               dispatch(actions.containsTopics())
               dispatch(actions.containsBadges())
           })
           .catch(error => {
                           console.log(error)
                           alert('Invalid username or password')
                           dispatch(actions.authFailure(error))
                           } )
       }else {
           console.log('jwt exists allready')
           dispatch(authSuccess(localStorage.getItem('jwt')))
           dispatch(actions.fetchProfile())
           dispatch(actions.fetchInstitutions())
           dispatch(actions.fetchStudentsTeamedWith())
           dispatch(actions.fetchStudentTasks())
           dispatch(actions.fetchTeamCourse())
           dispatch(actions.fetchTasks())
           dispatch(actions.fetchRevisions())
           dispatch(actions.containsTopics())
           dispatch(actions.containsBadges())
       }
   }

}


This code checks whether or not there is already a valid JWT token in the local store. If there is a valid token, all the subsequent actions are called with the same JWT token. If not, an HTTP post request is made to sessions controller in Rails API by sending username and password as data. If the request is successful, the resulting JWT token that is sent by the back-end is stored in the local store along with the timestamp.

React

The component that renders the login can be found in client/src/components/login/Login.js. Here, different React life-cycle methods are used to take care of various cases. The first thing to do is to initialize the state of the component. As for login, we are only concerned with two parameters, viz. username and password.

state = {
       username: ,
       password: 
   }


The connect function connects React component to Redux store. The mapStateToProps and mapDispatchToProps deals with the Redux store’s state and dispatch, respectively.If a mapStateToProps function is specified, the new wrapper component will subscribe to Redux store updates. This means that any time the store is updated, mapStateToProps will be called. The results of mapStateToProps must be a plain object, which will be merged into the wrapped component’s props.

const mapStatetoProps = state => {

   return {
       loggedin: state.auth.loggedIn
   }

}

const mapDispatchToProps = dispatch => {

   return {
       onSubmit: (name, password) => {dispatch(actions.auth(name, password))},
       onUsernameForget : () => {dispatch(actions.forgetUsername())},
       checkForAutoLogin : () => { dispatch(actions.checkForAutoLogIn())}
   }

} export default connect(mapStatetoProps, mapDispatchToProps)(Login);