To edit, add and remove users, we will need an admin page. We will not be adding the functionality to edit users just yet, but this is also a good place to display the list of all items by all users. This is basically the functionality that ListOfItems used to provided, before it changed to only return the items of
this user.
Three endpoints will be implemented in
AdminController.java: First,
/resource/admin/list/: list all items, regardless of user. Second,
/resource/userlist/: returns a list of all usernames (strings) on the system. Third,
/resource/user/{username}/: shows full details of the user, including password. Finally, we need to see who is currently logged in and we do this via the
/who/ endpoint, which shows name of the currently logged in user.
For full details, please consult the AdminController.java file. Below is the code for one endpoint, the others are very similar:
@RequestMapping(value = "/resource/admin/userlist/", method = RequestMethod.GET)
List<String> userList(Principal principal) {
User user = userRepository.findByUsername(principal.getName());
if (user != null && user.isAdmin()) {
Iterable <User> iterable = userRepository.findAll();
List<String> result = new ArrayList<>();
iterable.iterator().forEachRemaining((User u) -> {result.add(u.getUsername()); });
return result;
} else {
System.err.println("Unauthorized request to /resource/admin/userlist/ by " + principal.getName());
return new ArrayList<String>();
}
}
We connect this function to an endpoint with
RequestMapping as usual. This function will return a List of Strings (usernames), and it takes a
Principal as an argument. When Spring Security is active, any function can have Principal as an argument and the value, a Spring object describing the currently logged in user, will be filled in automatically.
To work with principal, we usually just do a
getName(). In this code, we proceed to look up that user in our own
UserRepository, using an automatically created JPA function
findUserByUsername (the interface for this function is defined in the UserRepository; the implementation is provided under the hood). Only admin users should be able to see the full list of users, so we check whether our user lookup was successful (non-null answer) and whether that user has
isAdmin set.
In this first pass, we have put some code in
SpringdoApplication.java to create two users, Navya and Dirk, both with password "secret". Navya is an admin user, and Dirk is not.
If the authorization is successful, we can retrieve all users on the system and return a list of their names. This is done via a
findAll on the UserRepository.
findAll is a built-in JPA function which does what it says on the can. FindAll returns an iterable, from which we derive an iterator. A lambda function is applied to each item with
forEachRemaining, using a simple Java 8 lambda which takes a user
u as an input and runs the
add method on our result array. This is a little convoluted, partially because of Java design choices regarding iterators and iterables, and partially because we can only return a list of real items to the front end so we have to populate an arraylist with string values instead of passing the iterator or iterable.
If the authorization is not successful, we print an error on the console and return an empty list. Ideally, the front end would check for this with an
ng-if and display some placeholder text ("No users found, are you admin?") in case the list is empty. This has not been implemented in the current version, but it should be less than 10 minutes work for you.
Lesson Learned If you look at the AdminController.java file, you see that each method is wrapped in an if, checking whether the user is authorized to call this routine. This is getting tedious and we will explore a Spring method to do this more efficiently later.
Back on the admin page, we show the list of users on the system. Each name has a details button next to it, and clicking that will show the user details after retrieving them from the backend. This separation between a user list and user details is good style: If the user list is long, only returning a minimal amount of information speeds up the page load. Secondly, sensitive information is only sent over the wire when explicitly requested.
The bottom half of the admin page shows all the todo items, their item IDs and the user who owns them. I have not implemented the ability to click on an item here to expand it inline, but you should be able to create this swiftly using my code for the user list as an example.
The admin html page uses two
ng-repeat statements to loop over users and items. It also uses two partials to render the users and items inside the loops, consistent with the pattern we used on the main page. On the javascript side,
admin.js does all the expected contacting of the backend and storing the information in variables on the scope.
The only interesting code is the
userQuery array shown below: This is initialized to
{} and filled with user details when requested via the button press. The buttons are wired to
userDetails(username) calls (with the username filled in) and the routine will contact the backend and populate the
userQuery map. On the html side (admin.html), an
ng-if"userQuery[user]"= hides details that have not been retrieved yet: If there are no data yet,
userQuery[user] will return undefined, which is falsy and will surpress the html fragment.
angular.module('admin', []).controller('ad1', function($scope, $http) {
// ..
$scope.userQuery = {};
$scope.userDetails = function (username) {
$http.get('/resource/user/' + username + '/').then(
function (success) {
$scope.userQuery[success.data['username']] = success.data;
}
);
};
We have not defined a 'failure' branch in the javascript above. This is consistent with my stance on this: I do not know how to substantially help the user if the request failed. Clearly, the username must exist on the system because our usernames have been retrieved from the server earlier. A failing call to
/resource/user/<id> means that the server is down, the connection is very slow, or the user was since removed by another admin. So there are three reasons, which I cannot distinguish between and none of them can be easily addressed by my user.
What does the User Experience look like in case of failure? If no result is returned, the user details will stay hidden (thanks to the
ng-if in the admin.html). The user will probably click on the username again, thinking that somehow their first click did not register. If the request is successful the second time, they will not think much of it; if it isn't, they will think the button is somehow broken. In either case, I don't think the user experience will be enhanced by flashing a message like "Sorry, but I cannot connect to the server right now, or maybe the user has been deleted? If you are on a mobile device, move to an area with better reception. If you are on a desktop, check your wifi connection or reinsert your IP cable. " This is adding unnecessary information to an already confusing situation, not a better User Experience. It reminds me of this (infamous) Windows 8 screen: