Click here to Skip to main content
15,899,549 members
Articles / Programming Languages / Scala

Madcap Idea Part 7 : Registration/login Backend

Rate me:
Please Sign up or sign in to vote.
5.00/5 (3 votes)
1 Aug 2017CPOL7 min read 6.1K   1
Registration/login backend

Last Time

So let me start with an apology that has taken me so long to get this new post up since last time. I have snuck in a holiday and been on a training course which really only left me 1 week of train rides to do this in, lame excuse but there you have it.

Anyway last time we looked at doing all the react markup for the screens, this time we will be doing at the backend registration and login functions which will make use of reactive mongo for the play side of things.

PreAmble

Just as a reminder, this is part of my ongoing set of posts which I talk about here, where we will be building up to a point where we have a full app using lots of different stuff, such as these:

  • WebPack
  • React.js
  • React Router
  • TypeScript
  • Babel.js
  • Akka
  • Scala
  • Play (Scala Http Stack)
  • MySql
  • SBT
  • Kafka
  • Kafka Streams

So I think the best way to cover this post is just dive in to the 2 main topics covered.

Registration

Passenger Registration

As a reminder, this is what the Passenger Registration screen looks like:

image

And this is the relevant code that deals with the “Register” button being clicked:

JavaScript
_handleValidSubmit = (values) => {
    var driver = values;
    var self = this;

    $.ajax({
        type: 'POST',
        url: 'registration/save/driver',
        data: JSON.stringify(driver),
        contentType: "application/json; charset=utf-8",
        dataType: 'json'
    })
    .done(function (jdata, textStatus, jqXHR) {
        var redactedDriver = driver;
        redactedDriver.password = "";
        console.log("redacted ${redactedDriver}");
        console.log(redactedDriver);
        console.log("Auth Service");
        console.log(self.props.authService);
        self.props.authService.storeUser(redactedDriver);
        self.setState(
            {
                okDialogHeaderText: 'Registration Successful',
                okDialogBodyText: 'You are now registered',
                okDialogOpen: true,
                okDialogKey: Math.random()
            });
    })
    .fail(function (jqXHR, textStatus, errorThrown) {
        self.setState(
            {
                okDialogHeaderText: 'Error',
                okDialogBodyText: jqXHR.responseText,
                okDialogOpen: true,
                okDialogKey: Math.random()
            });
    });
}

_okDialogCallBack = () => {
    this.setState(
        {
            okDialogOpen: false
        });
    if (this.state.wasSuccessful) {
        hashHistory.push('/');
    }
}

There are a couple of things of note there:

  • That we use a standard POST that will post the registration data as JSON to the Play API backend code
  • That we also show a standard Boostrap OkDialog which we looked at last time, which when the Ok button is clicked will use the React Router to navigate to the route page

Let’s now turn our attention to the Play API backend code that goes with the “Passenger Registration”.

JSON Read/Write support

The first thing we need to do is to set up support for reading JSON into Scala objects from a JSON string, and also allowing Scala objects to be turned back into a JSON string. For a “Passenger Registration” the Play API Json support comes via 3 Traits namely:

  • Reads: Which allows reading a JSON string into a Scala object
  • Writes: Which allows a Scala object to be turned into a JSON string
  • Format: is just a mix of the Reads and Writes traits and can be used for implicit conversion in place of its components

The recommendation for both of these is that they are exposed as implicit vals. You can read more about it here.

For the “Passenger Registration”, it looks like this:

package Entities

import play.api.libs.json._
import play.api.libs.functional.syntax._

case class PassengerRegistration(
  fullName: String,
  email: String,
  password: String)

object PassengerRegistration {
  implicit val formatter = Json.format[PassengerRegistration]
}

object PassengerRegistrationJsonFormatters {

  implicit val passengerRegistrationWrites = new Writes[PassengerRegistration] {
    def writes(passengerRegistration: PassengerRegistration) = Json.obj(
      "fullname" -> passengerRegistration.fullName,
      "email" -> passengerRegistration.email,
      "password" -> passengerRegistration.password
    )
  }

  implicit val passengerRegistrationReads: Reads[PassengerRegistration] = (
    (JsPath \ "fullname").read[String] and
      (JsPath \ "email").read[String] and
      (JsPath \ "password").read[String]
    )(PassengerRegistration.apply _)
}

Once we have that in place, we need to turn our attention to the actual endpoint to support the POST of a PassengerRegistration object. We first need to set up the route in the conf/routes file as follows:

# Registration page

POST /registration/save/passenger controllers.RegistrationController.savePassengerRegistration()

Reactive Mongo

Now that the route is in place, it's just a standard Play controller that we need. However, I have chosen to use Reactive Mongo as my storage mechanism for registration/login data. This means we have a few things that we need to install:

  1. Mongo itself which you can just grab from here: https://www.mongodb.com/download-center
  2. We then need to provision the actual Reactive Mongo Scala library, which we can do using the standard build.sbt file, where we specify the following dependency:
libraryDependencies ++= Seq(
  "org.reactivemongo" %% "play2-reactivemongo" % "0.11.12"
)

Ensure Mongod.exe Is Running

In order to run the app, we must ensure that the Mongo server is actually running which for my installation of Mongo means starting “Mongod.exe” from “C:\Program Files\MongoDB\Server\3.5\bin\mongod.exe” before I run the app.

The Registration Controller

So now that we have talked about the JSON Reads/Writes and we know that we need Mongo downloaded and running, let's see what the actual controller looks like shall we. Here is the FULL code for the “Passenger Registration”.

Java
package controllers

import javax.inject.Inject
import play.api.mvc.{Action, Controller, Result}
import Entities._
import Entities.DriverRegistrationJsonFormatters._
import Entities.PassengerRegistrationJsonFormatters._
import scala.concurrent.{ExecutionContext, Future}
import play.modules.reactivemongo._
import play.api.Logger
import utils.Errors
import play.api.libs.json._
import reactivemongo.api.ReadPreference
import reactivemongo.play.json._
import collection._

class RegistrationController @Inject()(val reactiveMongoApi: ReactiveMongoApi)
  (implicit ec: ExecutionContext)
  extends Controller with MongoController with ReactiveMongoComponents {

  def passRegistrationFuture: Future[JSONCollection] = 
	database.map(_.collection[JSONCollection]("passenger-registrations"))

  def savePassengerRegistration = Action.async(parse.json) { request =>
    Json.fromJson[PassengerRegistration](request.body) match {
      case JsSuccess(newPassRegistration, _) =>

        //https://github.com/ReactiveMongo/ReactiveMongo-Extensions/blob/0.10.x/guide/dsl.md
        val query = Json.obj("email" -> 
        Json.obj("$eq" -> newPassRegistration.email))

        dealWithRegistration[PassengerRegistration](
          newPassRegistration,
          passRegistrationFuture,
          query,
          PassengerRegistration.formatter)
      case JsError(errors) =>
        Future.successful(BadRequest(
			"Could not build a PassengerRegistration from the json provided. " +
			Errors.show(errors)))
    }
  }

  private def dealWithRegistration[T](
          incomingRegistration: T,
          jsonCollectionFuture: Future[JSONCollection],
          query: JsObject,
          formatter: OFormat[T])
          (implicit ec: ExecutionContext): Future[Result] = {

    def hasExistingRegistrationFuture = jsonCollectionFuture.flatMap {
        //http://reactivemongo.org/releases/0.11/documentation/advanced-topics/collection-api.html
        _.find(query)
        .cursor[JsObject](ReadPreference.primary)
        .collect[List]()
      }.map(_.length match {
          case 0 => false
          case _ => true
      }
    )

    hasExistingRegistrationFuture.flatMap {
      case false => {
        for {
          registrations <- jsonCollectionFuture
          writeResult <- registrations.insert(incomingRegistration)(formatter,ec)
        } yield {
          Logger.debug(s"Successfully inserted with LastError: $writeResult")
          Ok(Json.obj())
        }
      }
      case true => Future(BadRequest("Registration already exists"))
    }
  }
}

Let's break this down into chunks:

  • The controller constructor
    • This takes a ReactiveMongoApi (this is mandatory to satisfy the base trait MongoController requirements)
    • Inherits from MongoController which provides a lot of use functionality
    • It also inherits from ReactiveMongoComponents in order to allow the cake pattern/self typing requirements of the base MongoController which expects a ReactiveMongoComponents
  • The use of JSONCollection
    • There is a Future[JSONCollection] that represents the passenger collection in Mongo. This is a collection that stores JSON. When using reactive Mongo, you have a choice about whether to use the standard BSON collections of JSON. I opted for JSON.
  • The Guts Of The Logic
    • So now, we have discussed the controller constructor and the Mongo collections. We just need to talk about the actual work that happens on registration. In a nutshell, it works like this.
    • The incoming JSON string is turned into a PassengerRegistration object via Play.
    • We then create a new JSON query object to query the Mongo JSONCollection to see if a registration already exists
    • If a registration already exists, we exit with a BadRequest output.
    • If a registration does NOT already exist, we insert the new registration details into the Mongo JSONCollection, and then we return an Ok output.

And that is how the “Passenger Registration” works.

Driver Registration

The driver registration works in much the same way as described above, its just slightly different JSON, but it does share the same core logic/controller as the “Passenger Registration”.

Login

Although the Login share some of the same ideas as Registration, it has to do slightly different work, we will talk about what is different along the way, but this is what the Login screen looks like:

image

And this is what the relevant code on the client looks like to send across the JSON payload.

JavaScript
_handleValidSubmit = (values) => {
    var logindetails = values;
    var self = this;

    $.ajax({
        type: 'POST',
        url: 'login/validate',
        data: JSON.stringify(logindetails),
        contentType: "application/json; charset=utf-8",
        dataType: 'json'
    })
    .done(function (jdata, textStatus, jqXHR) {

        console.log("result of login");
        console.log(jqXHR.responseText);
        let currentUser = jqXHR.responseText;
        self._authService.storeUser(currentUser);

        self.setState(
            {
                okDialogHeaderText: 'Login Successful',
                okDialogBodyText: 'You are now logged in',
                okDialogOpen: true,
                okDialogKey: Math.random()
            });
    })
    .fail(function (jqXHR, textStatus, errorThrown) {
        self.setState(
            {
                okDialogHeaderText: 'Error',
                okDialogBodyText: jqXHR.responseText,
                okDialogOpen: true,
                okDialogKey: Math.random()
            });
    });
}

What is different this time is that when the Login is successful, we make use of an injected AuthService which we push out a true or false to indicate login status on a Rx subject, which all the other screens/components listen to, where each component can changes its own state/rendering depending on the login status. Here is the full code for the AuthService.

jscipt
import { injectable, inject } from "inversify";
import { TYPES } from "../types";
import Rx from 'rx';  

@injectable()
export class AuthService {

    private _isAuthenticated: boolean;
    private _authenticatedSubject = new Rx.Subject<boolean>();

    constructor() {

    }

    clearUser = () => {
        this._isAuthenticated = false;
        sessionStorage.removeItem('currentUserProfile');
        this._authenticatedSubject.onNext(false);
    }

    storeUser = (currentUser) => {
        this._isAuthenticated = true;
        sessionStorage.setItem('currentUserProfile', currentUser);
        this._authenticatedSubject.onNext(true);
    }

    userName = () => {
        var user = JSON.parse(sessionStorage.getItem('currentUserProfile'));
        return user.fullName;
    }

    isAuthenticated = () => {
        return this._isAuthenticated;
    }

    getAuthenticationStream = () => {
        return this._authenticatedSubject.asObservable();
    }
}

And here is an example of how a React component may listen to this Rx subject to affect its own rendering:

JavaScript
import * as React from "react";
import * as ReactDOM from "react-dom";

....
....

let authService = ContainerOperations.getInstance().container.get<AuthService>(TYPES.AuthService);

export interface MainNavProps {
    authService: AuthService;
}

export interface MainNavState {
    isLoggedIn: boolean;
}

class MainNav extends React.Component<MainNavProps, MainNavState> {

    private _subscription: any;


    constructor(props: any) {
        super(props);
        console.log(props);
        this.state = {
            isLoggedIn: false
        };
    }

    componentWillMount() {
        this._subscription = 
                this.props.authService.getAuthenticationStream().subscribe(isAuthenticated => {
            this.state = {
                isLoggedIn: isAuthenticated
            };
            if (this.state.isLoggedIn) {
                hashHistory.push('/createjob');
            }
            else {
                hashHistory.push('/');
            }
        });
    }

    componentWillUnmount() {
        this._subscription.dispose();
    }

    render() {
       ....
       ....
    }
}

Now moving on to the server side Play API code, as before, we need a Play backend route for the Login.

# Login page

POST  /login/validate                          controllers.LoginController.validateLogin()

And as before, we also have a Reactive Mongo enabled Play controller. I won’t go over the common stuff again, but here is the guts of the LoginController, and shown below is a bullet point list of what it does.

Java
package controllers

import javax.inject.Inject

import Entities.DriverRegistrationJsonFormatters._
import Entities.PassengerRegistrationJsonFormatters._
import Entities._
import play.api.Logger
import play.api.libs.json._
import play.api.mvc.{Action, Controller, Result}
import play.modules.reactivemongo._
import reactivemongo.api.ReadPreference
import reactivemongo.play.json._
import reactivemongo.play.json.collection._
import utils.Errors

import scala.concurrent.{ExecutionContext, Future}

class LoginController @Inject()(val reactiveMongoApi: ReactiveMongoApi)
                               (implicit ec: ExecutionContext)
  extends Controller with MongoController with ReactiveMongoComponents {

  def passRegistrationFuture: Future[JSONCollection] = 
  database.map(_.collection[JSONCollection]("passenger-registrations"))
  def driverRegistrationFuture: Future[JSONCollection] = 
  database.map(_.collection[JSONCollection]("driver-registrations"))


  def validateLogin = Action.async(parse.json) { request =>
    Json.fromJson[Login](request.body) match {
      case JsSuccess(newLoginDetails, _) =>
        newLoginDetails.isDriver match {
          case false => {
            val maybePassengerReg = extractExistingRegistration(
              passRegistrationFuture.flatMap {
                _.find(Json.obj("email" -> 
                Json.obj("$eq" -> newLoginDetails.email))).
                  cursor[JsObject](ReadPreference.primary).
                  collect[List]()
              })
            returnRedactedRegistration[PassengerRegistration](
              maybePassengerReg,
              (reg: PassengerRegistration) => Ok(Json.toJson(reg.copy(password = "")))
            )

          }
          case true => {
            val maybeDriverReg = extractExistingRegistration(
              driverRegistrationFuture.flatMap {
                _.find(Json.obj("email" -> 
                Json.obj("$eq" -> newLoginDetails.email))).
                  cursor[JsObject](ReadPreference.primary).
                  collect[List]()
              })
            returnRedactedRegistration[DriverRegistration](
              maybeDriverReg,
              (reg: DriverRegistration) => Ok(Json.toJson(reg.copy(password = "")))
            )
          }
        }
      case JsError(errors) =>
        Future.successful(BadRequest
        ("Could not build a Login from the json provided. " +
          Errors.show(errors)))
    }
  }

  private def returnRedactedRegistration[T]
  (
    maybeDriverRegFuture: Future[Option[JsObject]],
    redactor : T => Result
  )(implicit reads: Reads[T]): Future[Result] = {
    maybeDriverRegFuture.map {
      case Some(json) => {
        val reg = Json.fromJson[T](json)
        reg match {
          case JsSuccess(reg, _) => {
            redactor(reg)
          }
          case _ => BadRequest("Registration already exists")
        }
      }
      case None => BadRequest("Could not find registration")
    }
  }

  private def extractExistingRegistration[T]
  (incomingRegistrations: Future[List[T]])
  (implicit writes: Writes[T], ec: ExecutionContext): Future[Option[T]] = {
    incomingRegistrations.map(matchedRegistrations =>
      matchedRegistrations.length match {
        case 0 => None
        case _ => Some(matchedRegistrations(0))
      }
    )
  }
}

Essentially, the code above does the following:

  • Takes the deserialized JSON Login information, and decides whether it’s a Driver or a Passenger that is trying to login based on the IsDriver boolean.
  • Once we know if it’s a Driver or a Passenger that we are dealing with, we continue to check if there is a registration for someone that has the login email.
  • If we find a registered Driver or Passenger, we obtain the registration that was previously stored and redact the password, and then convert it to JSON and send it back over the wire to the React code where it is stored in Local Storage and exposes out to the JS code using the AuthService.ts code above, and will notify any Rx subscribers of the login status, such that any listening React components can adjust their state/render information.

Conclusion

I hope you have enjoyed this post. I know I have had fun with this one. I like the Reactive Mongo stuff, and I like how its all Future based which makes for a nice non-blocking workflow. We are now past the 1st phase of this project, the next phase will be to start with the Kafka messaging, where I may end up doing some more experimental/self contained code, rather than trying to shoe horn things into the main app in one go. I will still get it into the main app, but I am just thinking the Kafka posts may be better as stand alone things. I’ll see how it goes, at any rate the Kafka Streams stuff is phase 2, and we will be looking at that next.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
United Kingdom United Kingdom
I currently hold the following qualifications (amongst others, I also studied Music Technology and Electronics, for my sins)

- MSc (Passed with distinctions), in Information Technology for E-Commerce
- BSc Hons (1st class) in Computer Science & Artificial Intelligence

Both of these at Sussex University UK.

Award(s)

I am lucky enough to have won a few awards for Zany Crazy code articles over the years

  • Microsoft C# MVP 2016
  • Codeproject MVP 2016
  • Microsoft C# MVP 2015
  • Codeproject MVP 2015
  • Microsoft C# MVP 2014
  • Codeproject MVP 2014
  • Microsoft C# MVP 2013
  • Codeproject MVP 2013
  • Microsoft C# MVP 2012
  • Codeproject MVP 2012
  • Microsoft C# MVP 2011
  • Codeproject MVP 2011
  • Microsoft C# MVP 2010
  • Codeproject MVP 2010
  • Microsoft C# MVP 2009
  • Codeproject MVP 2009
  • Microsoft C# MVP 2008
  • Codeproject MVP 2008
  • And numerous codeproject awards which you can see over at my blog

Comments and Discussions

 
GeneralMy vote of 5 Pin
Member 123643902-Aug-17 4:33
Member 123643902-Aug-17 4:33 
good thorough job, over my head mostly. I thought you were going to use MySQL for DB not mongo ?

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.