From b7ddd7a95b0e4a6d8328a467b7d8f5d39b11fa74 Mon Sep 17 00:00:00 2001
From: Will Billingsley <wbilling@une.edu.au>
Date: Sun, 31 Jul 2016 16:40:00 +1000
Subject: [PATCH] Added a solution, not polished

---
 app/controllers/Application.java              |  20 ++-
 app/controllers/UserController.java           | 117 ++++++++++++++++++
 app/model/Session.java                        |  39 ++++++
 app/model/SessionService.java                 |  65 ++++++++++
 app/model/User.java                           |  32 +++++
 app/model/UserService.java                    |  53 ++++++++
 .../application/fragments/header.scala.html   |  14 +++
 .../fragments/includeBootstrap.scala.html     |  10 ++
 app/views/application/index.scala.html        |  30 ++---
 app/views/application/login.scala.html        |  37 ++++++
 app/views/application/mysessions.scala.html   |  24 ++++
 app/views/application/register.scala.html     |  33 +++++
 conf/routes                                   |   7 ++
 13 files changed, 453 insertions(+), 28 deletions(-)
 create mode 100644 app/controllers/UserController.java
 create mode 100644 app/model/Session.java
 create mode 100644 app/model/SessionService.java
 create mode 100644 app/model/User.java
 create mode 100644 app/model/UserService.java
 create mode 100644 app/views/application/fragments/header.scala.html
 create mode 100644 app/views/application/fragments/includeBootstrap.scala.html
 create mode 100644 app/views/application/login.scala.html
 create mode 100644 app/views/application/mysessions.scala.html
 create mode 100644 app/views/application/register.scala.html

diff --git a/app/controllers/Application.java b/app/controllers/Application.java
index 7fa68bf..e9374bd 100644
--- a/app/controllers/Application.java
+++ b/app/controllers/Application.java
@@ -3,6 +3,7 @@ package controllers;
 import actors.Setup;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
+import model.User;
 import play.mvc.Controller;
 import play.mvc.Result;
 import scala.compat.java8.FutureConverters;
@@ -18,26 +19,23 @@ public class Application extends Controller {
 
     WSClient wsClient;
 
+    UserController userController;
+
     @Inject
-    public Application(Setup actorSetup, WSClient wsClient) {
+    public Application(Setup actorSetup, WSClient wsClient, UserController userController) {
         this.actorSetup = actorSetup;
         this.wsClient = wsClient;
+        this.userController = userController;
     }
 
     /**
      * Play framework suppors asynchronous controller methods -- that is, methods that instead of returning a Result
      * return a CompletionStage, which will produce a Result some time in the future
      */
-    public CompletionStage<Result> index() {
-        /*
-         * This code might look a little complex.
-         *
-         * ask sends a message to an ActorRef, and then returns a Future that will eventually contain the response.
-         * But Future is a Scala class, so FutureConverters.toJava converts it into a CompletionStage, which is Java's equivalent.
-         * thenApply is a method on CompletionStage that means "when you get the result, do this with it, and return a new CompletionStage"
-         */
-        return FutureConverters.toJava(ask(actorSetup.marshallActor, "Report!", 1000))
-                .thenApply(response -> ok(response.toString()));
+    public Result index() {
+        User u = userController.getLoggedInUser();
+
+        return ok(views.html.application.index.render(u));
     }
 
     /**
diff --git a/app/controllers/UserController.java b/app/controllers/UserController.java
new file mode 100644
index 0000000..031c94a
--- /dev/null
+++ b/app/controllers/UserController.java
@@ -0,0 +1,117 @@
+package controllers;
+
+import com.google.inject.Singleton;
+import model.Session;
+import model.SessionService;
+import model.User;
+import model.UserService;
+import org.mindrot.jbcrypt.BCrypt;
+import play.mvc.Controller;
+import play.mvc.Result;
+
+import java.util.UUID;
+
+/**
+ * Controller for logging in, registering, logging out, etc.
+ */
+@Singleton
+public class UserController extends Controller {
+
+    public final static String sessionVar = "MY_SESSION";
+
+    protected UserService getUserService() {
+        return UserService.instance;
+    }
+
+    protected SessionService getSessionService() {
+        return SessionService.instance;
+    }
+
+    protected String getSessionId() {
+        String id = session(sessionVar);
+
+        /*
+         * Because we're not persisting the sessions in a database, it's possible the
+         * browser already has a session ID that no longer corresponds to one we remember.
+         *
+         * So we also check for whether the session exists in the "database"
+         */
+        if (id == null || getSessionService().get(id) == null) {
+            Session s = new Session(request().remoteAddress());
+            SessionService.instance.put(s);
+
+            id = s.getId();
+
+            session(sessionVar, id);
+        }
+        return id;
+    }
+
+    public User getLoggedInUser() {
+        return getSessionService().getUserFromSession(getSessionId());
+    }
+
+    public Result loginForm() {
+        return ok(views.html.application.login.render(getLoggedInUser(), null));
+    }
+
+    public Result registerForm() {
+        return ok(views.html.application.register.render(
+                getLoggedInUser(),
+                null
+        ));
+    }
+
+    public Result doLogin() {
+        String email = request().body().asFormUrlEncoded().get("email")[0];
+        String password = request().body().asFormUrlEncoded().get("password")[0];
+
+        String sessionId = getSessionId();
+        User u = getUserService().getUser(email, password);
+
+        if (u != null) {
+            SessionService.instance.setUserIdForSession(sessionId, u.getId());
+            return redirect("/mysessions");
+        } else {
+            return ok(views.html.application.login.render(
+                    getLoggedInUser(),
+                    "Email address or password was incorrect")
+            );
+        }
+    }
+
+    public Result doRegister() {
+        String email = request().body().asFormUrlEncoded().get("email")[0];
+        String password = request().body().asFormUrlEncoded().get("password")[0];
+
+        String id = UUID.randomUUID().toString();
+        User u = new User(id , email, BCrypt.hashpw(password, BCrypt.gensalt()));
+
+        try {
+            getUserService().registerUser(u);
+        } catch (IllegalStateException ex) {
+            return ok(views.html.application.register.render(
+                    getLoggedInUser(),
+                    ex.getMessage())
+            );
+        }
+
+        return redirect("/login");
+    }
+
+    public Result mySessions() {
+        String sessionId = getSessionId();
+        User loggedInUser = SessionService.instance.getUserFromSession(sessionId);
+        Session[] sessions = SessionService.instance.getSessionsForUser(loggedInUser);
+
+        return ok(views.html.application.mysessions.render(getLoggedInUser(), sessions));
+    }
+
+    public Result logoutSession() {
+        String sessionId = request().body().asFormUrlEncoded().get("sessionId")[0];
+        getSessionService().logout(sessionId);
+
+        return redirect("/mysessions");
+    }
+
+}
diff --git a/app/model/Session.java b/app/model/Session.java
new file mode 100644
index 0000000..844689d
--- /dev/null
+++ b/app/model/Session.java
@@ -0,0 +1,39 @@
+package model;
+
+import java.util.Date;
+
+/**
+ * Reprepresents a user's logged in session in our application
+ */
+public class Session {
+
+    String id;
+
+    String ipAddress;
+
+    long since;
+
+    public Session(String ipAddress) {
+        this.id = java.util.UUID.randomUUID().toString();
+        this.since = System.currentTimeMillis();
+        this.ipAddress = ipAddress;
+    }
+
+    public String getIpAddress() {
+        return this.ipAddress;
+    }
+
+    public String getId() {
+        return id;
+    }
+
+    public long getSince() {
+        return this.since;
+    }
+
+    public String getSinceAsString() {
+        Date d = new Date(since);
+        return d.toString();
+    }
+
+}
diff --git a/app/model/SessionService.java b/app/model/SessionService.java
new file mode 100644
index 0000000..191e964
--- /dev/null
+++ b/app/model/SessionService.java
@@ -0,0 +1,65 @@
+package model;
+
+import java.util.ArrayList;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Because we don't have a database we store the sessions in memory
+ */
+public class SessionService {
+
+    public static final SessionService instance = new SessionService();
+
+    protected ConcurrentHashMap<String, Session> sessions = new ConcurrentHashMap<>();
+
+    protected ConcurrentHashMap<String, String> sessionToUser = new ConcurrentHashMap<>();
+
+    public Session get(String id) {
+        return sessions.get(id);
+    }
+
+    public void put(Session s) {
+        sessions.put(s.getId(), s);
+    }
+
+    public String getUserIdForSession(String sessionId) {
+        return sessionToUser.get(sessionId);
+    }
+
+    /** Associates the session with a particular user */
+    public void setUserIdForSession(String sessionId, String userId) {
+        Session s = sessions.get(sessionId);
+        if (s == null) {
+            throw new IllegalStateException("The user had a session ID that was not in the database");
+        }
+
+        // Associate the session
+        sessionToUser.put(sessionId, userId);
+    }
+
+    public void logout(String sessionId) {
+        sessionToUser.remove(sessionId);
+    }
+
+    public User getUserFromSession(String sessionId) {
+        return UserService.instance.getUser(getUserIdForSession(sessionId));
+    }
+
+    public Session[] getSessionsForUser(User u) {
+        if (u == null) {
+            return new Session[0];
+        } else {
+            ArrayList<Session> foundSessions = new ArrayList<>();
+            for (Map.Entry<String, String> entry : sessionToUser.entrySet()) {
+                if (entry.getValue().equals(u.getId())) {
+                    foundSessions.add(sessions.get(entry.getKey()));
+                }
+            }
+            return foundSessions.toArray(new Session[0]);
+        }
+    }
+
+
+
+}
diff --git a/app/model/User.java b/app/model/User.java
new file mode 100644
index 0000000..1d94e6b
--- /dev/null
+++ b/app/model/User.java
@@ -0,0 +1,32 @@
+package model;
+
+/**
+ * A simple model for a User
+ */
+public class User {
+
+    String id;
+
+    String email;
+
+    String hash;
+
+    public User(String id, String email, String hash) {
+        this.id = id;
+        this.email = email;
+        this.hash = hash;
+    }
+
+    public String getId() {
+        return this.id;
+    }
+
+    public String getEmail() {
+        return this.email;
+    }
+
+    public String getHash() {
+        return this.hash;
+    }
+
+}
\ No newline at end of file
diff --git a/app/model/UserService.java b/app/model/UserService.java
new file mode 100644
index 0000000..99fba57
--- /dev/null
+++ b/app/model/UserService.java
@@ -0,0 +1,53 @@
+package model;
+
+import org.mindrot.jbcrypt.BCrypt;
+
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Stores all our users because we are not using a database in this tutorial
+ */
+public class UserService {
+
+    public static final UserService instance = new UserService();
+
+    /** Stand-in database */
+    ConcurrentHashMap<String, User> users = new ConcurrentHashMap<>();
+
+    /**
+     * Registers a new user
+     */
+    public User registerUser(User u) {
+        users.values().forEach((user) -> {
+            if (user.getEmail().equals(u.getEmail())) {
+                throw new IllegalStateException("User already exists");
+            }
+        });
+
+        users.put(u.getId(), u);
+        return u;
+    }
+
+    /**
+     * Gets a user from their User ID.
+     */
+    public User getUser(String id) {
+        if (id != null) {
+            return users.get(id);
+        } else return null;
+    }
+
+    /**
+     * Gets a user from the database if their email address and password matches
+     */
+    public User getUser(String email, String password) {
+        for (User u : users.values()) {
+            if (u.getEmail().equals(email) && BCrypt.checkpw(password, u.getHash())) {
+                return u;
+            }
+        }
+        return null;
+    }
+
+
+}
\ No newline at end of file
diff --git a/app/views/application/fragments/header.scala.html b/app/views/application/fragments/header.scala.html
new file mode 100644
index 0000000..92c6151
--- /dev/null
+++ b/app/views/application/fragments/header.scala.html
@@ -0,0 +1,14 @@
+@(user:model.User)
+
+<div class="container">
+    <a href="/">Example app</a>
+
+    <span class="pull-right">
+        @if(user != null){
+          @{user.getEmail()}
+        } else {
+          <a href="/login">log in</a>
+        }
+        <a href="/mysessions">My Sessions</a>
+    </span>
+</div>
\ No newline at end of file
diff --git a/app/views/application/fragments/includeBootstrap.scala.html b/app/views/application/fragments/includeBootstrap.scala.html
new file mode 100644
index 0000000..e3642ab
--- /dev/null
+++ b/app/views/application/fragments/includeBootstrap.scala.html
@@ -0,0 +1,10 @@
+@()
+
+<!-- Latest compiled and minified CSS -->
+<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
+
+<!-- Optional theme -->
+<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous">
+
+<!-- Latest compiled and minified JavaScript -->
+<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
diff --git a/app/views/application/index.scala.html b/app/views/application/index.scala.html
index 0031591..382e47b 100644
--- a/app/views/application/index.scala.html
+++ b/app/views/application/index.scala.html
@@ -1,7 +1,10 @@
-@(indexes:Array[Int])
+@(user:model.User)
 
 <!DOCTYPE html>
 
+@{fragments.header(user)}
+@{fragments.includeBootstrap()}
+
 <html>
 <head lang="en">
     <meta name="referrer" content="no-referrer">
@@ -10,22 +13,15 @@
 </head>
 <body>
 
-<h1>Tick all the beagles:</h1>
-
-<form action="matches" method="POST">
-    @for(idx <- indexes) {
-      <div>
-        <input name="sent" value="@idx" type="hidden" />
-        <input name="beagle" value="@idx" type="checkbox" />
-        <img src="@model.Captcha.getPhoto(idx)" style="max-width: 400px; max-height: 200px;" />
-      </div>
-    }
-
-    <!--
-      TODO: For each index in the array, show a checkbox with the value of the index, and the relevant img
-    -->
-    <button type="submit">Submit</button>
-</form>
+<div class="container">
+    <h2>Tutorial 5 example</h2>
 
+    Pages:
+    <ul>
+        <li><a href="/login">log in</a></li>
+        <li><a href="/register">register</a></li>
+        <li><a href="/mysessions">list your sessions (and log them out)</a></li>
+    </ul>
+</div>
 </body>
 </html>
\ No newline at end of file
diff --git a/app/views/application/login.scala.html b/app/views/application/login.scala.html
new file mode 100644
index 0000000..f80ddac
--- /dev/null
+++ b/app/views/application/login.scala.html
@@ -0,0 +1,37 @@
+@(user:model.User, error:String)
+
+<!DOCTYPE html>
+<html>
+
+@{fragments.header(user)}
+@{fragments.includeBootstrap()}
+
+<div class="container">
+
+    <h2>Log in</h2>
+    <form action="/login" method="POST">
+
+        <div class="form-group">
+            <label>Email address</label>
+            <input class="form-control" type="email" name="email" />
+        </div>
+        <div class="form-group">
+            <label>Password</label>
+            <input  class="form-control" type="password" name="password" />
+        </div>
+
+        @if(error != null) {
+        <div class="alert alert-danger">@error</div>
+        }
+
+        <button class="btn btn-primary" type="submit">Log In</button>
+
+    </form>
+
+    <div>
+        or <a href="/register">register a new account</a>.
+    </div>
+
+</div>
+
+</html>
diff --git a/app/views/application/mysessions.scala.html b/app/views/application/mysessions.scala.html
new file mode 100644
index 0000000..cb73ce3
--- /dev/null
+++ b/app/views/application/mysessions.scala.html
@@ -0,0 +1,24 @@
+@(user:model.User, sessions:Array[model.Session])
+
+@{fragments.header(user)}
+@{fragments.includeBootstrap()}
+
+<!DOCTYPE html>
+<html>
+
+<div class="container">
+    <h2>My sessions</h2>
+
+    <ul>
+        @for(session <- sessions){
+          <li>
+              @session.getIpAddress() since @session.getSinceAsString()
+              <form action="/logoutSession" method="POST">
+                  <input type="hidden" name="sessionId" value="@{session.getId()}" />
+                  <button class="btn btn-link" type="submit">logout</button>
+              </form>
+          </li>
+        }
+    </ul>
+</div>
+</html>
diff --git a/app/views/application/register.scala.html b/app/views/application/register.scala.html
new file mode 100644
index 0000000..c9cbfcb
--- /dev/null
+++ b/app/views/application/register.scala.html
@@ -0,0 +1,33 @@
+@(user:model.User, error:String)
+
+<!DOCTYPE html>
+<html>
+
+@{fragments.header(user)}
+@{fragments.includeBootstrap()}
+
+<div class="container">
+
+    <h2>Register</h2>
+    <form action="/register" method="POST">
+
+        <div class="form-group">
+            <label>Email address</label>
+            <input class="form-control" type="email" name="email" />
+        </div>
+        <div class="form-group">
+            <label>Password</label>
+            <input  class="form-control" type="password" name="password" />
+        </div>
+
+        @if(error != null) {
+        <div class="alert alert-danger">@error</div>
+        }
+
+        <button class="btn btn-primary" type="submit">Register</button>
+
+    </form>
+
+</div>
+
+</html>
diff --git a/conf/routes b/conf/routes
index 8fd2761..4517f62 100644
--- a/conf/routes
+++ b/conf/routes
@@ -5,5 +5,12 @@
 GET     /                           controllers.Application.index()
 GET     /ws                         controllers.Application.whatDidGitLabSay()
 
+GET     /login                      controllers.UserController.loginForm()
+GET     /register                   controllers.UserController.registerForm()
+GET     /mysessions                 controllers.UserController.mySessions()
+POST    /login                      controllers.UserController.doLogin()
+POST    /register                   controllers.UserController.doRegister()
+POST    /logoutSession              controllers.UserController.logoutSession()
+
 # Map static resources from the /public folder to the /assets URL path
 GET     /assets/*file               controllers.Assets.versioned(path="/public", file: Asset)
\ No newline at end of file
-- 
GitLab