#{extends '../main.html'/}# #{set title:'Webpieces QuickStart'/}# #{set tab:'management'/}# #{renderTagArgs 'docHome.html'/}# #{renderTagArgs 'quickStartList.html'/}#
First, let's discuss some requirements that frequently get missed when dealing with login that are super annoying to customers. If you are going to use a new webserver or framework, you should definitely walk through these use cases to make sure they all work for a great customer experience
Use case 1 is when a user hits a secure page but is not logged in yet. Ideally, the first step is to redirect the user to a login page. Unfortunately, many java script apps pop-up a window saying you have to login first with a link. How dumb? Instead, just put them on the login page with a red message you must login first..duh. Why make the user do 1 extra click? Some javascript apps are even worse and just error out.
Next, in this same use case, the user then types his login info. Many webapps then direct you to some 'logged in home' webpage. Uh, that is not the webpage I was looking for. I was trying to get to the one I originally typed in you piece of crap app!!!! ie. After login, we should be redirecting users back to the original page they wanted. We do this making your life easy and your customers very happy
In some cases, you may have timed out a user's session. He may have left a form open and he comes back and starts to edit that form. Then, he clicks save and 'should' be redirected to the login page. After that, he logs in, and in many apps, all the data he typed in just got blown away. For normal html CRUD, this is not true as we flash the data he typed in and after he logs in, all that data populated the form so he can then just click save again. Now, AJAX CRUD typically is VERY hard to keep that data as well, but if you follow the pattern/example we give you, it also works!!! When you login, any changes you made on add/edit are still there and we actually list the users AND we popup the modal dialogue with the previous changes the user had made. Pretty slick, eh? I hope you like it because IT WAS a HUGE PAIN IN THE @$$ to figure out but it sure felt good to figure out. There are a few tradeoffs that I decided to sacrifice though. One of the trade-offs though has better validation on controller methods outputting the correct variables so we can error out and now tell you which variable you forgot to output from your method that the template requires.
On some websites, you login and then decide to open a new tab. The new tab then makes you login again. OMG, I hate that with a passion. I am already logged in and with webpieces, that just works!
Nothing is more annoying than you roll a whole cluster and all your customers are logged out of the application. For this reason, login should survive a reboot and webpieces does just that because it is truely stateless.
This is nearly the same as above BUT let's say the user types in the wrong password the first time. In this case, we will re-render login page with errors and he will then hopefully type in the correct login. At this point, we still want to redirect him to the original page he requested. Of course, webpieces works here too while other platforms fall flat
In this use case, but I still run into webapps that let me hit the login page while I am logged in which is stupid as I am already logged in you stupid web app. Most everyone else works though but we want to be complete. For this use case, we naturally want a logged-in home web page that we redirect to after logging in perhaps even saying *['Welcome ${user.name}$']* just once.
Again, most people work here but ironically some websites don't and want you to login again which is very annoying. When I am logged in and hit a webpage, I expect to go directly to that webpage. Usually it's really old forum software that doesn't work in this case.
Our initial login is actually a component that customers could have written themselves consisting of a filter and an abstract controller that you subclass. Thankfully, webpieces template comes with a Login installed out of the box. We will be moving our AJAX crud to sit behind that login. First, let's discuss how it is installed currently. First, like most things, we created a LoginRouteId.java file for you
*[public enum LoginRouteId implements RouteId{
LOGIN, POST_LOGIN, LOGOUT, LOGGED_IN_HOME
}]*
Then, we created a LoginRoutes.java file for you as well:
*[package org.webpieces.helloworld.base.crud.login;
import org.webpieces.ctx.api.HttpMethod;
import org.webpieces.router.api.routing.RouteId;
import org.webpieces.router.api.routing.Router;
import org.webpieces.webserver.api.login.AbstractLoginRoutes;
/**
* Move this to the client applications instead since it is specific to one of the app's login methods
* @author dhiller
*
*/
public class LoginRoutes extends AbstractLoginRoutes {
/**
* @param controller
* @param basePath The 'unsecure' path that has a login page so you can get to the secure path
* @param securePath The path for the secure filter that ensures everyone under that path is secure
* @param sessionToken
*/
public LoginRoutes(String controller, String securePath) {
super(controller, null, securePath);
}
@Override
protected RouteId getPostLoginRoute() {
return LoginRouteId.POST_LOGIN;
}
@Override
protected RouteId getRenderLoginRoute() {
return LoginRouteId.LOGIN;
}
@Override
protected RouteId getRenderLogoutRoute() {
return LoginRouteId.LOGOUT;
}
@Override
protected String getSessionToken() {
return AppLoginController.TOKEN;
}
@Override
protected void addLoggedInHome(Router httpsRouter) {
Router scopedRouter = httpsRouter.getScopedRouter("/secure", true);
scopedRouter.addRoute(HttpMethod.GET , "/loggedinhome", "AppLoginController.home", LoginRouteId.LOGGED_IN_HOME);
}
}
]*
In this case however, we extend a class in webpieces that is re-usable and does some work for you. Feel free to check out that class as well to see the routes it installs.
Then, open up MyHelloWorldMeta.java and you will see the line where we install the routes into your web app:
*[new LoginRoutes("/org/webpieces/helloworld/base/crud/login/AppLoginController", "/secure/.*"),]*
This is saying use AppLoginController.java as my login controller and put any routes with the url /secure/.* so that you must login first to see those pages.
Now, even I make mistakes and with Login, in the future, we can fix a mistake I made. Here is the issues I think about when thinking about login and where I went slightly wrong:
I realized later, I should have made each module such that the whole module can be tied to being logged in or not. Of course, I have already found some use cases have to split having some public and some secure pages as well so it's not a trivial issue to solve. The easiest thing for now is to match on a url pattern. The login stuff is actually just a component that users could have written themselves. ie. this logged in stuff is separate from any core webpieces code).
The next piece you can open is AppLoginController.java. In here, you should really modify the method isValidLogin to wire into a database of users of some sort:
*[package org.webpieces.helloworld.base.crud.login;
import javax.inject.Singleton;
import org.webpieces.ctx.api.Current;
import org.webpieces.router.api.actions.Action;
import org.webpieces.router.api.actions.Actions;
import org.webpieces.router.api.actions.Render;
import org.webpieces.router.api.routing.RouteId;
import org.webpieces.webserver.api.login.AbstractLoginController;
@Singleton
public class AppLoginController extends AbstractLoginController {
public static final String TOKEN = "userId";
@Override
protected boolean isValidLogin(String username, String password) {
if(!"dean".equals(username)) {
Current.flash().setError("No Soup for you!");
Current.validation().addError("username", "I lied, Username must be 'dean'");
return false;
}
return true;
}
@Override
protected Action fetchGetLoginPageAction() {
return Actions.renderView("/org/webpieces/helloworld/base/crud/login/login.html");
}
public Render home() {
return Actions.renderThis();
}
public Render tags() {
return Actions.renderThis();
}
public Render index() {
return Actions.renderThis();
}
@Override
protected String getLoginSessionKey() {
return TOKEN;
}
@Override
protected RouteId getRenderLoginRoute() {
return LoginRouteId.LOGIN;
}
@Override
protected RouteId getRenderAfterLoginHome() {
return LoginRouteId.LOGGED_IN_HOME;
}
}
]*
Lastly, we implement all your *.html pages that correspond to each controller method and you can modify those like login.html or home.html. You should probably delete the method tag() with the tag.html file as you will not need that(sorry about that legacy stuff):
So, on with the show and actually changing stuff. Modify MyMainRoutes.java to REMOVE the CRUD route like so:
*[package org.webpieces.helloworld.myapp;
import org.webpieces.ctx.api.HttpMethod;
import org.webpieces.router.api.routing.AbstractRoutes;
public class MyMainRoutes extends AbstractRoutes {
@Override
protected void configure() {
addRoute(HttpMethod.GET , "/helloworld", "MyMainController.helloWorld", MyMainRouteId.HELLO_WORLD);
addRoute(HttpMethod.GET , "/helloworld/{name}/{id}", "MyMainController.dynamicHelloWorld", MyMainRouteId.DYNAMIC_HELLO_WORLD);
//route to render the page of creating a new user
addRoute(HttpMethod.GET , "/car/new", "MyMainController.carAddEdit", MyMainRouteId.GET_CREATE_CAR_PAGE);
//route to render the page of editing an existing user
addRoute(HttpMethod.GET , "/car/edit/{name}", "MyMainController.carAddEdit", MyMainRouteId.GET_EDIT_CAR_PAGE);
//route to save a new or existing user
addRoute(HttpMethod.POST, "/car/post", "MyMainController.postSaveCar", MyMainRouteId.POST_CAR);
}
}]*
Next, add a new AccountRoutes but extend ScopedRoutes this time like so and add the CRUD routes into that:
*[package org.webpieces.helloworld.myapp;
import org.webpieces.router.api.routing.CrudRouteIds;
import org.webpieces.router.api.routing.ScopedRoutes;
public class AccountRoutes extends ScopedRoutes {
@Override
protected void configure() {
CrudRouteIds routeIds = new CrudRouteIds(
AccountRouteId.LIST_ACCOUNTS, AccountRouteId.GET_ADD_ACCOUNT_FORM,
AccountRouteId.GET_EDIT_ACCOUNT_FORM, AccountRouteId.POST_ACCOUNT_FORM,
AccountRouteId.CONFIRM_DELETE_ACCOUNT, AccountRouteId.POST_DELETE_ACCOUNT);
addCrud("account", "CrudAccountController", routeIds);
}
@Override
protected String getScope() {
return "/secure";
}
@Override
protected boolean isHttpsOnlyRoutes() {
return true;
}
}
]*
Lastly, add this new AccountRoutes to MyHelloWorldMeta.java list of route files like so:
*[@Override
public List getRouteModules() {
return Lists.newArrayList(
new AppRoutes(),
new LoginRoutes("/org/webpieces/helloworld/base/crud/login/AppLoginController", "/secure/.*"),
new CrudRoutes(),
new AjaxCrudRoutes(),
new JsonRoutes(),
new MyMainRoutes(),
new AccountRoutes()
);
}]*
Well, that's it. Now, you can only access those routes over https and only if you are logged in! Go to https://localhost:8443/secure/account/list to see how it works (I am not sure if you are logged in or not so you may need to click logout)
Then play with logging in and rebooting the server and notice how you are not logged out unless you click logout. Pretty slick, right?
Next Writing a Test