You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by ja...@apache.org on 2018/10/09 09:09:17 UTC
[13/27] lucene-solr:solr7896-login-page: Testing new approach
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/5bb585c3/solr/webapp/web/index.html
----------------------------------------------------------------------
diff --git a/solr/webapp/web/index.html b/solr/webapp/web/index.html
index a98a30da..f36dc13 100644
--- a/solr/webapp/web/index.html
+++ b/solr/webapp/web/index.html
@@ -23,6 +23,7 @@ limitations under the License.
<link rel="icon" type="image/x-icon" href="img/favicon.ico?_=${version}">
<link rel="shortcut icon" type="image/x-icon" href="img/favicon.ico?_=${version}">
+ <link rel="stylesheet" type="text/css" href="css/bootstrap-3.3/bootstrap.min.css?_=${version}">
<link rel="stylesheet" type="text/css" href="css/angular/common.css?_=${version}">
<link rel="stylesheet" type="text/css" href="css/angular/analysis.css?_=${version}">
<link rel="stylesheet" type="text/css" href="css/angular/cloud.css?_=${version}">
@@ -67,6 +68,7 @@ limitations under the License.
<script src="js/angular/http-auth-interceptor.js"></script>
<script src="js/angular/login-controllers.js"></script>
<script src="js/angular/controllers/index.js"></script>
+ <script src="js/angular/controllers/login.js"></script>
<script src="js/angular/controllers/logging.js"></script>
<script src="js/angular/controllers/cloud.js"></script>
<script src="js/angular/controllers/collections.js"></script>
@@ -142,7 +144,8 @@ limitations under the License.
<div>
<ul id="menu">
-
+ <li id="login" class="global" ng-class="{active:page=='login'}"><p><a href="#/login">Logout</a></p></li>
+
<li id="index" class="global" ng-class="{active:page=='index'}"><p><a href="#/">Dashboard</a></p></li>
<li id="logging" class="global" ng-class="{active:page=='logging'}"><p><a href="#/~logging">Logging</a></p>
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/5bb585c3/solr/webapp/web/js/angular/app.js
----------------------------------------------------------------------
diff --git a/solr/webapp/web/js/angular/app.js b/solr/webapp/web/js/angular/app.js
index fe686fe..a0bc4ab 100644
--- a/solr/webapp/web/js/angular/app.js
+++ b/solr/webapp/web/js/angular/app.js
@@ -31,6 +31,10 @@ solrAdminApp.config([
templateUrl: 'partials/index.html',
controller: 'IndexController'
}).
+ when('/login', {
+ templateUrl: 'partials/login.html',
+ controller: 'LoginController'
+ }).
when('/~logging', {
templateUrl: 'partials/logging.html',
controller: 'LoggingController'
@@ -369,56 +373,73 @@ solrAdminApp.config([
return {request: started, response: ended, responseError: failed};
})
-.factory('authInterceptor', function($q, $rootScope, $timeout, $injector) {
- var started = function(config) {
- var ah = "Basic c29scjpTb2xyUm9ja3M="; // solr / SolrRocks
- config.headers['Authorization'] = ah;
- console.log("Added authorization header " + ah);
- return config || $q.when(config);
- };
-
- var ended = function(response) {
- console.log("Response headers: " + JSON.stringify(response.headers, undefined, 2));
- if (response.headers['WWW-Authenticate'] != null) {
- console.log("Got WWW-Authenticate header: " + response.headers['WWW-Authenticate']);
- }
- return response || $q.when(response);
- };
-
- var failed = function(rejection) {
- console.log("Failed with rejection " + JSON.stringify(rejection, undefined, 2));
- if (rejection.status === 401) {
- console.log("Status code is 401");
- } else {
- console.log("Rejection status is " + rejection.status)
- }
- $rootScope.$broadcast('loadingStatusInactive');
- return $q.reject(rejection);
- };
-
- return {request: started, response: ended, responseError: failed};
-})
+// NOCOMMIT First iteration
+// .factory('authInterceptor', function($q, $rootScope, $timeout, $injector) {
+// var started = function(config) {
+// var ah = "Basic c29scjpyb2Nrcw=="; // solr / SolrRocks
+// config.headers['Authorization'] = ah;
+// console.log("Added authorization header " + ah);
+// return config || $q.when(config);
+// };
+//
+// var ended = function(response) {
+// console.log("Response headers: " + JSON.stringify(response.headers, undefined, 2));
+// if (response.headers['WWW-Authenticate'] != null) {
+// console.log("Got WWW-Authenticate header: " + response.headers['WWW-Authenticate']);
+// }
+// return response || $q.when(response);
+// };
+//
+// var failed = function(rejection) {
+// console.log("Failed with rejection " + JSON.stringify(rejection, undefined, 2));
+// if (rejection.status === 401) {
+// console.log("Status code is 401");
+// } else {
+// console.log("Rejection status is " + rejection.status)
+// }
+// $rootScope.$broadcast('loadingStatusInactive');
+// return $q.reject(rejection);
+// };
+//
+// return {request: started, response: ended, responseError: failed};
+// })
.config(function($httpProvider) {
$httpProvider.interceptors.push("httpInterceptor");
- $httpProvider.interceptors.push("authInterceptor");
+ // NOCOMMIT $httpProvider.interceptors.push("authInterceptor");
// Tell the BasicAuth plugin that we are Admin UI so it can serve us a 'Authorization: xBasic xxxx' header
// so that the browser will not interfer with the login dialogue
$httpProvider.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
})
+// NOCOMMIT: just for testing
+.run(['$rootScope', '$location', '$cookieStore', '$http',
+ function ($rootScope, $location, $cookieStore, $http) {
+ // keep user logged in after page refresh
+ $rootScope.globals = $cookieStore.get('globals') || {};
+ if ($rootScope.globals.currentUser) {
+ $http.defaults.headers.common['Authorization'] = 'Basic ' + $rootScope.globals.currentUser.authdata; // jshint ignore:line
+ }
+
+ $rootScope.$on('$locationChangeStart', function (event, next, current) {
+ // redirect to login page if not logged in
+ if ($location.path() !== '/login' && !$rootScope.globals.currentUser) {
+ $location.path('/login');
+ }
+ });
+ }])
.directive('fileModel', function ($parse) {
- return {
- restrict: 'A',
- link: function(scope, element, attrs) {
- var model = $parse(attrs.fileModel);
- var modelSetter = model.assign;
+ return {
+ restrict: 'A',
+ link: function(scope, element, attrs) {
+ var model = $parse(attrs.fileModel);
+ var modelSetter = model.assign;
- element.bind('change', function(){
- scope.$apply(function(){
- modelSetter(scope, element[0].files[0]);
- });
- });
- }
- };
+ element.bind('change', function(){
+ scope.$apply(function(){
+ modelSetter(scope, element[0].files[0]);
+ });
+ });
+ }
+ };
});
solrAdminApp.controller('MainController', function($scope, $route, $rootScope, $location, Cores, Collections, System, Ping, Constants) {
@@ -432,7 +453,7 @@ solrAdminApp.controller('MainController', function($scope, $route, $rootScope, $
$scope.refresh = function() {
$scope.cores = [];
$scope.collections = [];
- }
+ };
$scope.refresh();
$scope.resetMenu = function(page, pageType) {
@@ -462,7 +483,7 @@ solrAdminApp.controller('MainController', function($scope, $route, $rootScope, $
for (key in data.collections) {
var collection = {name: data.collections[key]};
$scope.collections.push(collection);
- if (pageType == Constants.IS_COLLECTION_PAGE && collection.name == currentCollectionName) {
+ if (pageType === Constants.IS_COLLECTION_PAGE && collection.name === currentCollectionName) {
$scope.currentCollection = collection;
}
}
@@ -486,15 +507,15 @@ solrAdminApp.controller('MainController', function($scope, $route, $rootScope, $
$scope.dumpCloud = function() {
$scope.$broadcast("cloud-dump");
- }
+ };
$scope.showCore = function(core) {
$location.url("/" + core.name);
- }
+ };
$scope.showCollection = function(collection) {
$location.url("/" + collection.name + "/collection-overview")
- }
+ };
$scope.$on('$routeChangeStart', function() {
$rootScope.exceptions = {};
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/5bb585c3/solr/webapp/web/js/angular/controllers/login.js
----------------------------------------------------------------------
diff --git a/solr/webapp/web/js/angular/controllers/login.js b/solr/webapp/web/js/angular/controllers/login.js
new file mode 100644
index 0000000..b0b7096
--- /dev/null
+++ b/solr/webapp/web/js/angular/controllers/login.js
@@ -0,0 +1,36 @@
+/*
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+solrAdminApp.controller('LoginController',
+ ['$scope', '$rootScope', '$location', 'AuthenticationService',
+ function ($scope, $rootScope, $location, AuthenticationService) {
+ // reset login status
+ AuthenticationService.ClearCredentials();
+
+ $scope.login = function () {
+ $scope.dataLoading = true;
+ AuthenticationService.Login($scope.username, $scope.password, function (response) {
+ if (response.success) {
+ AuthenticationService.SetCredentials($scope.username, $scope.password);
+ $location.path('/');
+ } else {
+ $scope.error = response.message;
+ $scope.dataLoading = false;
+ }
+ });
+ };
+ }]);
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/5bb585c3/solr/webapp/web/js/angular/directives.js
----------------------------------------------------------------------
diff --git a/solr/webapp/web/js/angular/directives.js b/solr/webapp/web/js/angular/directives.js
deleted file mode 100644
index ba6e701..0000000
--- a/solr/webapp/web/js/angular/directives.js
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-'use strict';
-
-angular.module('loginDirectives', []).
-directive('loginDialog', function($timeout) {
- return {
- templateUrl: '/templates/loginDialog.html',
- restrict: 'E',
- replace: true,
- controller: CredentialsController,
- link: function(scope, element, attributes, controller) {
- var isShowing = false;
-
- element.on('shown.bs.modal', function(e) {
- element.find('#userName').focus();
- });
-
- scope.$on('event:auth-loginRequired', function() {
- if (isShowing) {
- return;
- }
-
- // If we're in the process of hiding the modal, we need to wait for
- // all CSS animations to complete before showing the modal again.
- // Otherwise, we might end up with an invisible modal, making the whole
- // view rather unusable. I've been unable to control the transitions
- // between "showing", "shown", "hiding", and "hidden" tightly using
- // JQuery notifications without collecting more and more modal backdrops
- // in the DOM, so the dirty solution here is to simply wait a second
- // before showing the log-in dialog.
- isShowing = true;
- $timeout(function() {
- element.modal('show');
- isShowing = false;
- }, 1000);
- });
-
- scope.$on('event:auth-loginConfirmed', function() {
- element.modal('hide');
- scope.credentials.password = '';
- });
- }
- }
-}).
-directive('logoutLink', function() {
- return {
- templateUrl: '/templates/logoutLink.html',
- restrict: 'E',
- replace: true,
- controller: LoginController
- }
-});
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/5bb585c3/solr/webapp/web/js/angular/http-auth-interceptor.js
----------------------------------------------------------------------
diff --git a/solr/webapp/web/js/angular/http-auth-interceptor.js b/solr/webapp/web/js/angular/http-auth-interceptor.js
deleted file mode 100644
index 2f34861..0000000
--- a/solr/webapp/web/js/angular/http-auth-interceptor.js
+++ /dev/null
@@ -1,122 +0,0 @@
-/*global angular:true, browser:true */
-
-/**
- * @license HTTP Auth Interceptor Module for AngularJS
- * (c) 2012 Witold Szczerba
- * License: MIT
- */
-(function () {
- 'use strict';
-
- angular.module('http-auth-interceptor', ['http-auth-interceptor-buffer'])
-
- .factory('authService', ['$rootScope','httpBuffer', function($rootScope, httpBuffer) {
- return {
- /**
- * Call this function to indicate that authentication was successfull and trigger a
- * retry of all deferred requests.
- * @param data an optional argument to pass on to $broadcast which may be useful for
- * example if you need to pass through details of the user that was logged in
- */
- loginConfirmed: function(data, configUpdater) {
- var updater = configUpdater || function(config) {return config;};
- $rootScope.$broadcast('event:auth-loginConfirmed', data);
- httpBuffer.retryAll(updater);
- },
-
- /**
- * Call this function to indicate that authentication should not proceed.
- * All deferred requests will be abandoned or rejected (if reason is provided).
- * @param data an optional argument to pass on to $broadcast.
- * @param reason if provided, the requests are rejected; abandoned otherwise.
- */
- loginCancelled: function(data, reason) {
- httpBuffer.rejectAll(reason);
- $rootScope.$broadcast('event:auth-loginCancelled', data);
- }
- };
- }])
-
- /**
- * $http interceptor.
- * On 401 response (without 'ignoreAuthModule' option) stores the request
- * and broadcasts 'event:angular-auth-loginRequired'.
- */
- .config(['$httpProvider', function($httpProvider) {
- $httpProvider.interceptors.push(['$rootScope', '$q', 'httpBuffer', function($rootScope, $q, httpBuffer) {
- return {
- responseError: function(rejection) {
- if (rejection.status === 401 && !rejection.config.ignoreAuthModule) {
- var deferred = $q.defer();
- httpBuffer.append(rejection.config, deferred);
- $rootScope.$broadcast('event:auth-loginRequired', rejection);
- return deferred.promise;
- }
- // otherwise, default behaviour
- return $q.reject(rejection);
- }
- };
- }]);
- }]);
-
- /**
- * Private module, a utility, required internally by 'http-auth-interceptor'.
- */
- angular.module('http-auth-interceptor-buffer', [])
-
- .factory('httpBuffer', ['$injector', function($injector) {
- /** Holds all the requests, so they can be re-requested in future. */
- var buffer = [];
-
- /** Service initialized later because of circular dependency problem. */
- var $http;
-
- function retryHttpRequest(config, deferred) {
- // Make room for new 'Authenticate' header value
- delete config.headers['Authorization'];
-
- function successCallback(response) {
- deferred.resolve(response);
- }
- function errorCallback(response) {
- deferred.reject(response);
- }
- $http = $http || $injector.get('$http');
- $http(config).then(successCallback, errorCallback);
- }
-
- return {
- /**
- * Appends HTTP request configuration object with deferred response attached to buffer.
- */
- append: function(config, deferred) {
- buffer.push({
- config: config,
- deferred: deferred
- });
- },
-
- /**
- * Abandon or reject (if reason provided) all the buffered requests.
- */
- rejectAll: function(reason) {
- if (reason) {
- for (var i = 0; i < buffer.length; ++i) {
- buffer[i].deferred.reject(reason);
- }
- }
- buffer = [];
- },
-
- /**
- * Retries all the buffered requests clears the buffer.
- */
- retryAll: function(updater) {
- for (var i = 0; i < buffer.length; ++i) {
- retryHttpRequest(updater(buffer[i].config), buffer[i].deferred);
- }
- buffer = [];
- }
- };
- }]);
-})();
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/5bb585c3/solr/webapp/web/js/angular/login-controllers.js
----------------------------------------------------------------------
diff --git a/solr/webapp/web/js/angular/login-controllers.js b/solr/webapp/web/js/angular/login-controllers.js
deleted file mode 100644
index 819ccb3..0000000
--- a/solr/webapp/web/js/angular/login-controllers.js
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-'use strict';
-
-function CredentialsController($scope, loginService) {
- $scope.credentials = { userName: '', password: '' };
-
- $scope.submit = function() {
- loginService.setUserNameAndPassword($scope.credentials.userName, $scope.credentials.password);
- }
-}
-
-function LoginController($scope, $location, loginService) {
- $scope.isLoggedIn = function() {
- return loginService.isLoggedIn();
- }
-
- $scope.logOut = function() {
- loginService.logOut();
- $location.path('/about');
- }
-}
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/5bb585c3/solr/webapp/web/js/angular/services.js
----------------------------------------------------------------------
diff --git a/solr/webapp/web/js/angular/services.js b/solr/webapp/web/js/angular/services.js
index e3dcd3f..ffd7135 100644
--- a/solr/webapp/web/js/angular/services.js
+++ b/solr/webapp/web/js/angular/services.js
@@ -262,4 +262,138 @@ solrAdminServices.factory('System',
return $resource(':core/config', {wt: 'json', core: '@core', _:Date.now()}, {
get: {method: "GET"}
})
-}]);
+}])
+.factory('AuthenticationService',
+ ['Base64', '$http', '$cookieStore', '$rootScope', '$timeout',
+ function (Base64, $http, $cookieStore, $rootScope, $timeout) {
+ var service = {};
+
+ service.Login = function (username, password, callback) {
+
+ /* NOCOMMIT Dummy authentication for testing, uses $timeout to simulate api call
+ ----------------------------------------------*/
+ $timeout(function () {
+ var response = { success: username === 'test' && password === 'test' };
+ if (!response.success) {
+ response.message = 'Username or password is incorrect';
+ }
+ callback(response);
+ }, 1000);
+
+
+ /* Use this for real authentication
+ ----------------------------------------------*/
+ //$http.post('/api/authenticate', { username: username, password: password })
+ // .success(function (response) {
+ // callback(response);
+ // });
+
+ };
+
+ service.SetCredentials = function (username, password) {
+ var authdata = Base64.encode(username + ':' + password);
+
+ $rootScope.globals = {
+ currentUser: {
+ username: username,
+ authdata: authdata
+ }
+ };
+
+ $http.defaults.headers.common['Authorization'] = 'Basic ' + authdata; // jshint ignore:line
+ $cookieStore.put('globals', $rootScope.globals);
+ };
+
+ service.ClearCredentials = function () {
+ $rootScope.globals = {};
+ $cookieStore.remove('globals');
+ $http.defaults.headers.common.Authorization = 'Basic ';
+ };
+
+ return service;
+ }])
+.factory('Base64', function () {
+ /* jshint ignore:start */
+
+ var keyStr = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
+
+ return {
+ encode: function (input) {
+ var output = "";
+ var chr1, chr2, chr3 = "";
+ var enc1, enc2, enc3, enc4 = "";
+ var i = 0;
+
+ do {
+ chr1 = input.charCodeAt(i++);
+ chr2 = input.charCodeAt(i++);
+ chr3 = input.charCodeAt(i++);
+
+ enc1 = chr1 >> 2;
+ enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
+ enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
+ enc4 = chr3 & 63;
+
+ if (isNaN(chr2)) {
+ enc3 = enc4 = 64;
+ } else if (isNaN(chr3)) {
+ enc4 = 64;
+ }
+
+ output = output +
+ keyStr.charAt(enc1) +
+ keyStr.charAt(enc2) +
+ keyStr.charAt(enc3) +
+ keyStr.charAt(enc4);
+ chr1 = chr2 = chr3 = "";
+ enc1 = enc2 = enc3 = enc4 = "";
+ } while (i < input.length);
+
+ return output;
+ },
+
+ decode: function (input) {
+ var output = "";
+ var chr1, chr2, chr3 = "";
+ var enc1, enc2, enc3, enc4 = "";
+ var i = 0;
+
+ // remove all characters that are not A-Z, a-z, 0-9, +, /, or =
+ var base64test = /[^A-Za-z0-9\+\/\=]/g;
+ if (base64test.exec(input)) {
+ window.alert("There were invalid base64 characters in the input text.\n" +
+ "Valid base64 characters are A-Z, a-z, 0-9, '+', '/',and '='\n" +
+ "Expect errors in decoding.");
+ }
+ input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
+
+ do {
+ enc1 = keyStr.indexOf(input.charAt(i++));
+ enc2 = keyStr.indexOf(input.charAt(i++));
+ enc3 = keyStr.indexOf(input.charAt(i++));
+ enc4 = keyStr.indexOf(input.charAt(i++));
+
+ chr1 = (enc1 << 2) | (enc2 >> 4);
+ chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
+ chr3 = ((enc3 & 3) << 6) | enc4;
+
+ output = output + String.fromCharCode(chr1);
+
+ if (enc3 != 64) {
+ output = output + String.fromCharCode(chr2);
+ }
+ if (enc4 != 64) {
+ output = output + String.fromCharCode(chr3);
+ }
+
+ chr1 = chr2 = chr3 = "";
+ enc1 = enc2 = enc3 = enc4 = "";
+
+ } while (i < input.length);
+
+ return output;
+ }
+ };
+
+ /* jshint ignore:end */
+});
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/5bb585c3/solr/webapp/web/partials/login.html
----------------------------------------------------------------------
diff --git a/solr/webapp/web/partials/login.html b/solr/webapp/web/partials/login.html
new file mode 100644
index 0000000..cae1cc8
--- /dev/null
+++ b/solr/webapp/web/partials/login.html
@@ -0,0 +1,44 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<div id="login" class="jumbotron">
+
+ <div class="col-xs-offset-2 col-xs-8">
+ <div class="alert alert-info">
+ Username: test<br />
+ Password: test
+ </div>
+ <div ng-show="error" class="alert alert-danger">{{error}}</div>
+ <form name="form" ng-submit="login()" role="form">
+ <div class="form-group">
+ <label for="username">Username</label>
+ <i class="fa fa-key"></i>
+ <input type="text" name="username" id="username" class="form-control" ng-model="username" required />
+ <span ng-show="form.username.$dirty && form.username.$error.required" class="help-block">Username is required</span>
+ </div>
+ <div class="form-group">
+ <label for="password">Password</label>
+ <i class="fa fa-lock"></i>
+ <input type="password" name="password" id="password" class="form-control" ng-model="password" required />
+ <span ng-show="form.password.$dirty && form.password.$error.required" class="help-block">Password is required</span>
+ </div>
+ <div class="form-actions">
+ <button type="submit" ng-disabled="form.$invalid || dataLoading" class="btn btn-danger">Login</button>
+ <img ng-if="dataLoading" src="data:image/gif;base64,R0lGODlhEAAQAPIAAP///wAAAMLCwkJCQgAAAGJiYoKCgpKSkiH/C05FVFNDQVBFMi4wAwEAAAAh/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQJCgAAACwAAAAAEAAQAAADMwi63P4wyklrE2MIOggZnAdOmGYJRbExwroUmcG2LmDEwnHQLVsYOd2mBzkYDAdKa+dIAAAh+QQJCgAAACwAAAAAEAAQAAADNAi63P5OjCEgG4QMu7DmikRxQlFUYDEZIGBMRVsaqHwctXXf7WEYB4Ag1xjihkMZsiUkKhIAIfkECQoAAAAsAAAAABAAEAAAAzYIujIjK8pByJDMlFYvBoVjHA70GU7xSUJhmKtwHPAKzLO9HMaoKwJZ7Rf8AYPDDzKpZBqfvwQAIfkECQoAAAAsAAAAABAAEAAAAzMIumIlK8oyhpHsnFZfhYumCYUhDAQxRIdhHBGqRoKw0R8DYlJd8z0fMDgsGo/IpHI5TAAAIfkECQoAAAAsAAAAABAAEAAAAzIIunInK0rnZBTwGPNMgQwmdsNgXGJUlIWEuR5oWUIpz8pAEAMe6TwfwyYsGo/IpFKSAAAh+QQJCgAAACwAAAAAEAAQAAADMwi6IMKQORfjdOe82p4wGccc4CEuQradylesojEMBgsUc2G7sDX3lQGBMLAJibufbSlKAAAh+QQJCgAAACwAAAAAEAAQAAADMgi63P7wCRHZnFVdmgHu2nFwlWCI3WGc3TSWhUFGxTAUkGCbtgENBMJAEJsxgMLWzpEAACH5BAkKAAAALAAAAAAQABAAAAMyCLrc/jDKSatlQtScKdceCAjDII7HcQ4EMTCpyrCuUBjCYRgHVtqlAiB1YhiCnlsRkAAAOwAAAAAAAAAAAA==" />
+ </div>
+ </form>
+ </div>
+</div>