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