diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/auth.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/auth.resource.js index 710ee58e52..604ba4b582 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/auth.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/auth.resource.js @@ -34,16 +34,28 @@ function authResource($q, $http, umbRequestHelper, angularHelper) { "PostLogout"))); }, - /** Sends a request to the server to check if the current cookie value is valid for the user */ - isAuthenticated: function () { + /** Sends a request to the server to get the current user details, will return a 401 if the user is not logged in */ + getCurrentUser: function () { return umbRequestHelper.resourcePromise( $http.get( umbRequestHelper.getApiUrl( "authenticationApiBaseUrl", "GetCurrentUser")), - 'Server call failed for checking authorization'); - } + 'Server call failed for getting current user'); + }, + + /** Checks if the user is logged in or not - does not return 401 or 403 */ + isAuthenticated: function () { + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "authenticationApiBaseUrl", + "IsAuthenticated")), + 'Server call failed for checking authentication'); + }, + }; } diff --git a/src/Umbraco.Web.UI.Client/src/common/services/user.service.js b/src/Umbraco.Web.UI.Client/src/common/services/user.service.js index 626b11b98f..811ed55c18 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/user.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/user.service.js @@ -53,30 +53,20 @@ angular.module('umbraco.services') return { + /** Internal method to display the login dialog */ + _showLoginDialog: function () { + openLoginDialog(); + }, + /** Returns a promise, sends a request to the server to check if the current cookie is authorized */ - isAuthenticated: function (args) { - - return authResource.isAuthenticated() - .then(function(data) { - - //note, this can return null if they are not authenticated - if (!data) { - throw "Not authenticated"; - } - else { - - var result = { user: data, authenticated: true, lastUserId: lastUserId }; - - if (args.broadcastEvent) { - //broadcast a global event, will inform listening controllers to load in the user specific data - $rootScope.$broadcast("authenticated", result); - } - - currentUser = data; - currentUser.avatar = 'http://www.gravatar.com/avatar/' + data.emailHash + '?s=40&d=404'; - return result; - } - }); + isAuthenticated: function () { + //if we've got a current user then just return true + if (currentUser) { + var deferred = $q.defer(); + deferred.resolve(true); + return deferred.promise; + } + return authResource.isAuthenticated(); }, /** Returns a promise, sends a request to the server to validate the credentials */ @@ -97,6 +87,7 @@ angular.module('umbraco.services') }); }, + /** Logs the user out and redirects to the login page */ logout: function () { return authResource.performLogout() .then(function (data) { @@ -107,14 +98,38 @@ angular.module('umbraco.services') //broadcast a global event $rootScope.$broadcast("notAuthenticated"); - openLoginDialog(); + $location.path("/login").search({check: false}); + return null; }); }, - /** Returns the current user object, if null then calls to authenticated or authenticate must be called */ - getCurrentUser: function () { - return currentUser; + /** Returns the current user object in a promise */ + getCurrentUser: function (args) { + var deferred = $q.defer(); + + if (!currentUser) { + authResource.getCurrentUser() + .then(function(data) { + + var result = { user: data, authenticated: true, lastUserId: lastUserId }; + + if (args.broadcastEvent) { + //broadcast a global event, will inform listening controllers to load in the user specific data + $rootScope.$broadcast("authenticated", result); + } + + currentUser = data; + currentUser.avatar = 'http://www.gravatar.com/avatar/' + data.emailHash + '?s=40&d=404'; + deferred.resolve(currentUser); + }); + + } + else { + deferred.resolve(currentUser); + } + + return deferred.promise; } }; diff --git a/src/Umbraco.Web.UI.Client/src/routes.js b/src/Umbraco.Web.UI.Client/src/routes.js index b3bd6791cd..bbda26b835 100644 --- a/src/Umbraco.Web.UI.Client/src/routes.js +++ b/src/Umbraco.Web.UI.Client/src/routes.js @@ -1,15 +1,62 @@ app.config(function ($routeProvider) { + + /** This checks if the user is authenticated for a route and what the isRequired is set to. + Depending on whether isRequired = true, it first check if the user is authenticated and will resolve successfully + otherwise the route will fail and the $routeChangeError event will execute, in that handler we will redirect to the rejected + path that is resolved from this method and prevent default (prevent the route from executing) */ + var checkAuth = function(isRequired) { + return { + isAuthenticated: function ($q, userService, $route) { + var deferred = $q.defer(); + + //don't need to check if we've redirected to login and we've already checked auth + if (!$route.current.params.section && $route.current.params.check === false) { + deferred.resolve(true); + return deferred.promise; + } + + userService.isAuthenticated() + .then(function () { + if (isRequired) { + //this will resolve successfully so the route will continue + deferred.resolve(true); + } + else { + deferred.reject({ path: "/" }); + } + }, function () { + if (isRequired) { + //the check=false is checked above so that we don't have to make another http call to check + //if they are logged in since we already know they are not. + deferred.reject({ path: "/login", search: { check: false } }); + } + else { + //this will resolve successfully so the route will continue + deferred.resolve(true); + } + }); + return deferred.promise; + } + }; + }; + $routeProvider + .when('/login', { + templateUrl: 'views/common/login.html', + //ensure auth is *not* required so it will redirect to /content otherwise + resolve: checkAuth(false) + }) .when('/:section', { templateUrl: function (rp) { - if (rp.section === "default" || rp.section === "") + if (rp.section.toLowerCase() === "default" || rp.section.toLowerCase() === "umbraco" || rp.section === "") { rp.section = "content"; } rp.url = "dashboard.aspx?app=" + rp.section; return 'views/common/dashboard.html'; - } + }, + resolve: checkAuth(true) }) .when('/framed/:url', { //This occurs when we need to launch some content in an iframe @@ -18,7 +65,8 @@ app.config(function ($routeProvider) { throw "A framed resource must have a url route parameter"; return 'views/common/legacy.html'; - } + }, + resolve: checkAuth(true) }) .when('/:section/:method', { templateUrl: function(rp) { @@ -32,7 +80,8 @@ app.config(function ($routeProvider) { // dashboards (as tabs if we wanted) and each tab could actually be a route link to one of these views? return 'views/' + rp.section + '/' + rp.method + '.html'; - } + }, + resolve: checkAuth(true) }) .when('/:section/:tree/:method/:id', { templateUrl: function (rp) { @@ -43,9 +92,10 @@ app.config(function ($routeProvider) { //we don't need to put views into section folders since theoretically trees // could be moved among sections, we only need folders for specific trees. return 'views/' + rp.tree + '/' + rp.method + '.html'; - } + }, + resolve: checkAuth(true) }) - .otherwise({ redirectTo: '/content' }); + .otherwise({ redirectTo: '/login' }); }).config(function ($locationProvider) { //$locationProvider.html5Mode(false).hashPrefix('!'); //turn html5 mode off @@ -53,10 +103,28 @@ app.config(function ($routeProvider) { }); -app.run(['userService', '$log', '$rootScope', function (userService, $log, $rootScope) { +app.run(['userService', '$log', '$rootScope', '$location', function (userService, $log, $rootScope, $location) { - // Get the current user when the application starts - // (in case they are still logged in from a previous session) + var firstRun = true; + + /** when we have a successful first route that is not the login page - meaning the user is authenticated + we'll get the current user from the user service and ensure it broadcasts it's events. If the route + is successful from after a login then this will not actually do anything since the authenticated event would + have alraedy fired, but if the user is loading the angularjs app for the first time and they are already authenticated + then this is when the authenticated event will be fired. + */ + $rootScope.$on('$routeChangeSuccess', function (event, current, previous) { + if (firstRun && !$location.url().toLowerCase().startsWith("/login")) { + firstRun = false; + userService.getCurrentUser({ broadcastEvent: true }); + } + }); + + /** When the route change is rejected - based on checkAuth - we'll prevent the rejected route from executing including + wiring up it's controller, etc... and then redirect to the rejected URL. */ + $rootScope.$on('$routeChangeError', function (event, current, previous, rejection) { + event.preventDefault(); + $location.path(rejection.path).search(rejection.search); + }); - userService.isAuthenticated({broadcastEvent: true}); }]); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/user.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/user.controller.js index e683c831d1..a7ecbada40 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/user.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/user.controller.js @@ -5,9 +5,9 @@ angular.module("umbraco") $scope.history = historyService.current; $scope.logout = function () { - userService.logout(); - $scope.hide(); - $location.path("/"); + userService.logout().then(function() { + $scope.hide(); + }); }; $scope.gotoHistory = function (link) { diff --git a/src/Umbraco.Web.UI.Client/src/views/common/login.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/login.controller.js new file mode 100644 index 0000000000..8b94debf39 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/login.controller.js @@ -0,0 +1,11 @@ +/** This controller is simply here to launch the login dialog when the route is explicitly changed to /login */ +angular.module('umbraco').controller("Umbraco.LoginController", function ($scope, userService, $location) { + + userService._showLoginDialog(); + + //when a user is authorized redirect - this will only be handled here when we are actually on the /login route + $scope.$on("authenticated", function(evt, data) { + $location.path("/").search(""); + }); + +}); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/common/login.html b/src/Umbraco.Web.UI.Client/src/views/common/login.html new file mode 100644 index 0000000000..d490d395eb --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/login.html @@ -0,0 +1,3 @@ +
+ +
\ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/common/main.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/main.controller.js index 66ef09e5ab..5a0347254d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/main.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/main.controller.js @@ -80,7 +80,7 @@ function MainController($scope, $location, $routeParams, $rootScope, $timeout, $ //if the user has changed we need to redirect to the root so they don't try to continue editing the //last item in the URL - if (data.lastUserId !== data.user.id) { + if (data.lastUserId && data.lastUserId !== data.user.id) { $location.path("/").search(""); } diff --git a/src/Umbraco.Web/Editors/AuthenticationController.cs b/src/Umbraco.Web/Editors/AuthenticationController.cs index f2a8996203..793ca4be62 100644 --- a/src/Umbraco.Web/Editors/AuthenticationController.cs +++ b/src/Umbraco.Web/Editors/AuthenticationController.cs @@ -33,7 +33,26 @@ namespace Umbraco.Web.Editors } /// - /// Simply checks if the current user's cookie is valid and if so returns the user object associated + /// Checks if the current user's cookie is valid and if so returns OK or a 400 (BadRequest) + /// + /// + [HttpGet] + public HttpResponseMessage IsAuthenticated() + { + var attempt = UmbracoContext.Security.AuthorizeRequest(); + if (attempt == ValidateRequestAttempt.Success) + { + return Request.CreateResponse(HttpStatusCode.OK); + } + //return Forbidden (403), we don't want to return a 401 because that get's intercepted + // by our angular helper because it thinks that we need to re-perform the request once we are + // authorized. + return Request.CreateResponse(HttpStatusCode.Forbidden); + } + + + /// + /// Checks if the current user's cookie is valid and if so returns the user object associated /// /// public UserDetail GetCurrentUser()