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:
And this is the relevant code that deals with the “Register” button being clicked:
_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:
- Mongo itself which you can just grab from here: https://www.mongodb.com/download-center
- 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”.
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, _) =>
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 {
_.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:
And this is what the relevant code on the client looks like to send across the JSON payload.
_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
.
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:
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.
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.