mirror of https://github.com/portainer/portainer
feat(init-admin): allow to specify a username for the initial admin account (#1160)
parent
13b2fcffd2
commit
e65d132b3d
|
@ -13,8 +13,10 @@ const (
|
||||||
const (
|
const (
|
||||||
ErrUserNotFound = Error("User not found")
|
ErrUserNotFound = Error("User not found")
|
||||||
ErrUserAlreadyExists = Error("User already exists")
|
ErrUserAlreadyExists = Error("User already exists")
|
||||||
ErrInvalidUsername = Error("Invalid username. White spaces are not allowed.")
|
ErrInvalidUsername = Error("Invalid username. White spaces are not allowed")
|
||||||
ErrAdminAlreadyInitialized = Error("Admin user already initialized")
|
ErrAdminAlreadyInitialized = Error("An administrator user already exists")
|
||||||
|
ErrCannotRemoveAdmin = Error("Cannot remove the default administrator account")
|
||||||
|
ErrAdminCannotRemoveSelf = Error("Cannot remove your own user account. Contact another administrator")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Team errors.
|
// Team errors.
|
||||||
|
|
|
@ -82,6 +82,7 @@ type (
|
||||||
}
|
}
|
||||||
|
|
||||||
postAdminInitRequest struct {
|
postAdminInitRequest struct {
|
||||||
|
Username string `valid:"required"`
|
||||||
Password string `valid:"required"`
|
Password string `valid:"required"`
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -358,10 +359,14 @@ func (handler *UserHandler) handlePostAdminInit(w http.ResponseWriter, r *http.R
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := handler.UserService.UserByUsername("admin")
|
users, err := handler.UserService.UsersByRole(portainer.AdministratorRole)
|
||||||
if err == portainer.ErrUserNotFound {
|
if err != nil {
|
||||||
|
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(users) == 0 {
|
||||||
user := &portainer.User{
|
user := &portainer.User{
|
||||||
Username: "admin",
|
Username: req.Username,
|
||||||
Role: portainer.AdministratorRole,
|
Role: portainer.AdministratorRole,
|
||||||
}
|
}
|
||||||
user.Password, err = handler.CryptoService.Hash(req.Password)
|
user.Password, err = handler.CryptoService.Hash(req.Password)
|
||||||
|
@ -375,11 +380,7 @@ func (handler *UserHandler) handlePostAdminInit(w http.ResponseWriter, r *http.R
|
||||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else if err != nil {
|
} else {
|
||||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if user != nil {
|
|
||||||
httperror.WriteErrorResponse(w, portainer.ErrAdminAlreadyInitialized, http.StatusConflict, handler.Logger)
|
httperror.WriteErrorResponse(w, portainer.ErrAdminAlreadyInitialized, http.StatusConflict, handler.Logger)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -396,6 +397,22 @@ func (handler *UserHandler) handleDeleteUser(w http.ResponseWriter, r *http.Requ
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if userID == 1 {
|
||||||
|
httperror.WriteErrorResponse(w, portainer.ErrCannotRemoveAdmin, http.StatusForbidden, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenData, err := security.RetrieveTokenData(r)
|
||||||
|
if err != nil {
|
||||||
|
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if tokenData.ID == portainer.UserID(userID) {
|
||||||
|
httperror.WriteErrorResponse(w, portainer.ErrAdminCannotRemoveSelf, http.StatusForbidden, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
_, err = handler.UserService.User(portainer.UserID(userID))
|
_, err = handler.UserService.User(portainer.UserID(userID))
|
||||||
|
|
||||||
if err == portainer.ErrUserNotFound {
|
if err == portainer.ErrUserNotFound {
|
||||||
|
|
39
app/app.js
39
app/app.js
|
@ -34,11 +34,12 @@ angular.module('portainer', [
|
||||||
'docker',
|
'docker',
|
||||||
'endpoint',
|
'endpoint',
|
||||||
'endpointAccess',
|
'endpointAccess',
|
||||||
'endpointInit',
|
|
||||||
'endpoints',
|
'endpoints',
|
||||||
'events',
|
'events',
|
||||||
'image',
|
'image',
|
||||||
'images',
|
'images',
|
||||||
|
'initAdmin',
|
||||||
|
'initEndpoint',
|
||||||
'main',
|
'main',
|
||||||
'network',
|
'network',
|
||||||
'networks',
|
'networks',
|
||||||
|
@ -321,6 +322,33 @@ angular.module('portainer', [
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.state('init', {
|
||||||
|
abstract: true,
|
||||||
|
url: '/init',
|
||||||
|
views: {
|
||||||
|
'content@': {
|
||||||
|
template: '<div ui-view="content@"></div>'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.state('init.endpoint', {
|
||||||
|
url: '/endpoint',
|
||||||
|
views: {
|
||||||
|
'content@': {
|
||||||
|
templateUrl: 'app/components/initEndpoint/initEndpoint.html',
|
||||||
|
controller: 'InitEndpointController'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.state('init.admin', {
|
||||||
|
url: '/admin',
|
||||||
|
views: {
|
||||||
|
'content@': {
|
||||||
|
templateUrl: 'app/components/initAdmin/initAdmin.html',
|
||||||
|
controller: 'InitAdminController'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
.state('docker', {
|
.state('docker', {
|
||||||
url: '/docker/',
|
url: '/docker/',
|
||||||
views: {
|
views: {
|
||||||
|
@ -373,15 +401,6 @@ angular.module('portainer', [
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.state('endpointInit', {
|
|
||||||
url: '/init/endpoint',
|
|
||||||
views: {
|
|
||||||
'content@': {
|
|
||||||
templateUrl: 'app/components/endpointInit/endpointInit.html',
|
|
||||||
controller: 'EndpointInitController'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.state('events', {
|
.state('events', {
|
||||||
url: '/events/',
|
url: '/events/',
|
||||||
views: {
|
views: {
|
||||||
|
|
|
@ -1,92 +1,38 @@
|
||||||
<div class="page-wrapper">
|
<div class="page-wrapper">
|
||||||
<!-- login box -->
|
<!-- login box -->
|
||||||
<div class="container simple-box">
|
<div class="container simple-box">
|
||||||
<div class="col-md-6 col-md-offset-3 col-sm-6 col-sm-offset-3">
|
<div class="col-sm-6 col-sm-offset-3">
|
||||||
<!-- login box logo -->
|
<!-- login box logo -->
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<img ng-if="logo" ng-src="{{ logo }}" class="simple-box-logo">
|
|
||||||
<img ng-if="!logo" src="images/logo_alt.png" class="simple-box-logo" alt="Portainer">
|
<img ng-if="!logo" src="images/logo_alt.png" class="simple-box-logo" alt="Portainer">
|
||||||
|
<img ng-if="logo" ng-src="{{ logo }}" class="simple-box-logo">
|
||||||
</div>
|
</div>
|
||||||
<!-- !login box logo -->
|
<!-- !login box logo -->
|
||||||
<!-- init password panel -->
|
|
||||||
<div class="panel panel-default" ng-if="initPassword">
|
|
||||||
<div class="panel-body">
|
|
||||||
<!-- init password form -->
|
|
||||||
<form class="login-form form-horizontal" enctype="multipart/form-data" method="POST">
|
|
||||||
<!-- comment -->
|
|
||||||
<div class="input-group">
|
|
||||||
<p style="margin: 5px;">
|
|
||||||
Please specify a password for the <b>admin</b> user account.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<!-- !comment input -->
|
|
||||||
<!-- comment -->
|
|
||||||
<div class="input-group">
|
|
||||||
<p style="margin: 5px;">
|
|
||||||
<i ng-class="{true: 'fa fa-check green-icon', false: 'fa fa-times red-icon'}[initPasswordData.password.length >= 8]" aria-hidden="true"></i>
|
|
||||||
Your password must be at least 8 characters long
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<!-- !comment input -->
|
|
||||||
<!-- password input -->
|
|
||||||
<div class="input-group">
|
|
||||||
<span class="input-group-addon"><i class="fa fa-lock" aria-hidden="true"></i></span>
|
|
||||||
<input id="admin_password" type="password" class="form-control" name="password" ng-model="initPasswordData.password" autofocus>
|
|
||||||
</div>
|
|
||||||
<!-- !password input -->
|
|
||||||
<!-- comment -->
|
|
||||||
<div class="input-group">
|
|
||||||
<p style="margin: 5px;">
|
|
||||||
<i ng-class="{true: 'fa fa-check green-icon', false: 'fa fa-times red-icon'}[initPasswordData.password !== '' && initPasswordData.password === initPasswordData.password_confirmation]" aria-hidden="true"></i>
|
|
||||||
Confirm your password
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<!-- !comment input -->
|
|
||||||
<!-- password confirmation input -->
|
|
||||||
<div class="input-group">
|
|
||||||
<span class="input-group-addon"><i class="fa fa-lock" aria-hidden="true"></i></span>
|
|
||||||
<input id="password_confirmation" type="password" class="form-control" name="password" ng-model="initPasswordData.password_confirmation">
|
|
||||||
</div>
|
|
||||||
<!-- !password confirmation input -->
|
|
||||||
<!-- validate button -->
|
|
||||||
<div class="form-group">
|
|
||||||
<div class="col-sm-12 controls">
|
|
||||||
<p class="pull-left text-danger" ng-if="initPasswordData.error" style="margin: 5px;">
|
|
||||||
<i class="fa fa-exclamation-circle" aria-hidden="true"></i> Unable to create default user
|
|
||||||
</p>
|
|
||||||
<button type="submit" class="btn btn-primary pull-right" ng-disabled="initPasswordData.password.length < 8 || initPasswordData.password !== initPasswordData.password_confirmation" ng-click="createAdminUser()"><i class="fa fa-key" aria-hidden="true"></i> Validate</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- !validate button -->
|
|
||||||
</form>
|
|
||||||
<!-- !init password form -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- !init password panel -->
|
|
||||||
<!-- login panel -->
|
<!-- login panel -->
|
||||||
<div class="panel panel-default" ng-if="!initPassword">
|
<div class="panel panel-default">
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<!-- login form -->
|
<!-- login form -->
|
||||||
<form class="login-form form-horizontal" enctype="multipart/form-data" method="POST">
|
<form class="simple-box-form form-horizontal">
|
||||||
<!-- username input -->
|
<!-- username input -->
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<span class="input-group-addon"><i class="fa fa-user" aria-hidden="true"></i></span>
|
<span class="input-group-addon"><i class="fa fa-user" aria-hidden="true"></i></span>
|
||||||
<input id="username" type="text" class="form-control" name="username" ng-model="authData.username" placeholder="Username">
|
<input id="username" type="text" class="form-control" name="username" ng-model="formValues.Username" placeholder="admin" autofocus>
|
||||||
</div>
|
</div>
|
||||||
<!-- !username input -->
|
<!-- !username input -->
|
||||||
<!-- password input -->
|
<!-- password input -->
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<span class="input-group-addon"><i class="fa fa-lock" aria-hidden="true"></i></span>
|
<span class="input-group-addon"><i class="fa fa-lock" aria-hidden="true"></i></span>
|
||||||
<input id="password" type="password" class="form-control" name="password" ng-model="authData.password" autofocus>
|
<input id="password" type="password" class="form-control" name="password" ng-model="formValues.Password">
|
||||||
</div>
|
</div>
|
||||||
<!-- !password input -->
|
<!-- !password input -->
|
||||||
<!-- login button -->
|
<!-- login button -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12 controls">
|
<div class="col-sm-12">
|
||||||
<p class="pull-left text-danger" ng-if="authData.error" style="margin: 5px;">
|
<button type="submit" class="btn btn-primary btn-sm pull-right" ng-click="authenticateUser()"><i class="fa fa-sign-in" aria-hidden="true"></i> Login</button>
|
||||||
<i class="fa fa-exclamation-circle" aria-hidden="true"></i> {{ authData.error }}
|
<span class="pull-left" style="margin: 5px;" ng-if="state.AuthenticationError">
|
||||||
</p>
|
<i class="fa fa-exclamation-triangle red-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||||
<button type="submit" class="btn btn-primary pull-right" ng-click="authenticateUser()"><i class="fa fa-sign-in" aria-hidden="true"></i> Login</button>
|
<span class="small text-danger">{{ state.AuthenticationError }}</span>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- !login button -->
|
<!-- !login button -->
|
||||||
|
|
|
@ -1,123 +1,110 @@
|
||||||
angular.module('auth', [])
|
angular.module('auth', [])
|
||||||
.controller('AuthenticationController', ['$scope', '$state', '$stateParams', '$window', '$timeout', '$sanitize', 'Authentication', 'Users', 'EndpointService', 'StateManager', 'EndpointProvider', 'Notifications', 'SettingsService',
|
.controller('AuthenticationController', ['$scope', '$state', '$stateParams', '$window', '$timeout', '$sanitize', 'Authentication', 'Users', 'UserService', 'EndpointService', 'StateManager', 'EndpointProvider', 'Notifications', 'SettingsService',
|
||||||
function ($scope, $state, $stateParams, $window, $timeout, $sanitize, Authentication, Users, EndpointService, StateManager, EndpointProvider, Notifications, SettingsService) {
|
function ($scope, $state, $stateParams, $window, $timeout, $sanitize, Authentication, Users, UserService, EndpointService, StateManager, EndpointProvider, Notifications, SettingsService) {
|
||||||
|
|
||||||
$scope.authData = {
|
|
||||||
username: 'admin',
|
|
||||||
password: '',
|
|
||||||
error: ''
|
|
||||||
};
|
|
||||||
$scope.initPasswordData = {
|
|
||||||
password: '',
|
|
||||||
password_confirmation: '',
|
|
||||||
error: false
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.logo = StateManager.getState().application.logo;
|
$scope.logo = StateManager.getState().application.logo;
|
||||||
|
|
||||||
if (!$scope.applicationState.application.authentication) {
|
$scope.formValues = {
|
||||||
EndpointService.endpoints()
|
Username: '',
|
||||||
.then(function success(data) {
|
Password: ''
|
||||||
if (data.length > 0) {
|
|
||||||
endpointID = EndpointProvider.endpointID();
|
|
||||||
if (!endpointID) {
|
|
||||||
endpointID = data[0].Id;
|
|
||||||
EndpointProvider.setEndpointID(endpointID);
|
|
||||||
}
|
|
||||||
StateManager.updateEndpointState(true)
|
|
||||||
.then(function success() {
|
|
||||||
$state.go('dashboard');
|
|
||||||
}, function error(err) {
|
|
||||||
Notifications.error('Failure', err, 'Unable to connect to the Docker endpoint');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$state.go('endpointInit');
|
|
||||||
}
|
|
||||||
}, function error(err) {
|
|
||||||
Notifications.error('Failure', err, 'Unable to retrieve endpoints');
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
Users.checkAdminUser({}, function () {},
|
|
||||||
function (e) {
|
|
||||||
if (e.status === 404) {
|
|
||||||
$scope.initPassword = true;
|
|
||||||
} else {
|
|
||||||
Notifications.error('Failure', e, 'Unable to verify administrator account existence');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($stateParams.logout) {
|
|
||||||
Authentication.logout();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($stateParams.error) {
|
|
||||||
$scope.authData.error = $stateParams.error;
|
|
||||||
Authentication.logout();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Authentication.isAuthenticated()) {
|
|
||||||
$state.go('dashboard');
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.createAdminUser = function() {
|
|
||||||
var password = $sanitize($scope.initPasswordData.password);
|
|
||||||
Users.initAdminUser({password: password}, function (d) {
|
|
||||||
$scope.initPassword = false;
|
|
||||||
$timeout(function() {
|
|
||||||
var element = $window.document.getElementById('password');
|
|
||||||
if(element) {
|
|
||||||
element.focus();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, function (e) {
|
|
||||||
$scope.initPassword.error = true;
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.state = {
|
||||||
|
AuthenticationError: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
function setActiveEndpointAndRedirectToDashboard(endpoint) {
|
||||||
|
var endpointID = EndpointProvider.endpointID();
|
||||||
|
if (!endpointID) {
|
||||||
|
EndpointProvider.setEndpointID(endpoint.Id);
|
||||||
|
}
|
||||||
|
StateManager.updateEndpointState(true)
|
||||||
|
.then(function success(data) {
|
||||||
|
$state.go('dashboard');
|
||||||
|
})
|
||||||
|
.catch(function error(err) {
|
||||||
|
Notifications.error('Failure', err, 'Unable to connect to the Docker endpoint');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function unauthenticatedFlow() {
|
||||||
|
EndpointService.endpoints()
|
||||||
|
.then(function success(data) {
|
||||||
|
var endpoints = data;
|
||||||
|
if (endpoints.length > 0) {
|
||||||
|
setActiveEndpointAndRedirectToDashboard(endpoints[0]);
|
||||||
|
} else {
|
||||||
|
$state.go('init.endpoint');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function error(err) {
|
||||||
|
Notifications.error('Failure', err, 'Unable to retrieve endpoints');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function authenticatedFlow() {
|
||||||
|
UserService.administratorExists()
|
||||||
|
.then(function success(exists) {
|
||||||
|
if (!exists) {
|
||||||
|
$state.go('init.admin');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function error(err) {
|
||||||
|
Notifications.error('Failure', err, 'Unable to verify administrator account existence');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
$scope.authenticateUser = function() {
|
$scope.authenticateUser = function() {
|
||||||
$scope.authenticationError = false;
|
var username = $scope.formValues.Username;
|
||||||
|
var password = $scope.formValues.Password;
|
||||||
|
|
||||||
SettingsService.publicSettings()
|
SettingsService.publicSettings()
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
var settings = data;
|
var settings = data;
|
||||||
var username = $scope.authData.username;
|
|
||||||
var password = $scope.authData.password;
|
|
||||||
if (settings.AuthenticationMethod === 1) {
|
if (settings.AuthenticationMethod === 1) {
|
||||||
username = $sanitize($scope.authData.username);
|
username = $sanitize(username);
|
||||||
password = $sanitize($scope.authData.password);
|
password = $sanitize(password);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Authentication.login(username, password);
|
return Authentication.login(username, password);
|
||||||
})
|
})
|
||||||
.then(function success(data) {
|
.then(function success() {
|
||||||
return EndpointService.endpoints();
|
return EndpointService.endpoints();
|
||||||
})
|
})
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
|
var endpoints = data;
|
||||||
var userDetails = Authentication.getUserDetails();
|
var userDetails = Authentication.getUserDetails();
|
||||||
if (data.length > 0) {
|
if (endpoints.length > 0) {
|
||||||
endpointID = EndpointProvider.endpointID();
|
setActiveEndpointAndRedirectToDashboard(endpoints[0]);
|
||||||
if (!endpointID) {
|
} else if (endpoints.length === 0 && userDetails.role === 1) {
|
||||||
endpointID = data[0].Id;
|
$state.go('init.endpoint');
|
||||||
EndpointProvider.setEndpointID(endpointID);
|
} else if (endpoints.length === 0 && userDetails.role === 2) {
|
||||||
}
|
|
||||||
StateManager.updateEndpointState(true)
|
|
||||||
.then(function success() {
|
|
||||||
$state.go('dashboard');
|
|
||||||
}, function error(err) {
|
|
||||||
Notifications.error('Failure', err, 'Unable to connect to the Docker endpoint');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else if (data.length === 0 && userDetails.role === 1) {
|
|
||||||
$state.go('endpointInit');
|
|
||||||
} else if (data.length === 0 && userDetails.role === 2) {
|
|
||||||
Authentication.logout();
|
Authentication.logout();
|
||||||
$scope.authData.error = 'User not allowed. Please contact your administrator.';
|
$scope.state.AuthenticationError = 'User not allowed. Please contact your administrator.';
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error() {
|
||||||
$scope.authData.error = 'Authentication error';
|
$scope.state.AuthenticationError = 'Invalid credentials';
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function initView() {
|
||||||
|
if ($stateParams.logout || $stateParams.error) {
|
||||||
|
Authentication.logout();
|
||||||
|
$scope.state.AuthenticationError = $stateParams.error;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Authentication.isAuthenticated()) {
|
||||||
|
$state.go('dashboard');
|
||||||
|
}
|
||||||
|
|
||||||
|
var authenticationEnabled = $scope.applicationState.application.authentication;
|
||||||
|
if (!authenticationEnabled) {
|
||||||
|
unauthenticatedFlow();
|
||||||
|
} else {
|
||||||
|
authenticatedFlow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initView();
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
<div class="page-wrapper">
|
||||||
|
<!-- simple box -->
|
||||||
|
<div class="container simple-box">
|
||||||
|
<div class="col-md-8 col-md-offset-2 col-sm-10 col-sm-offset-1">
|
||||||
|
<!-- simple box logo -->
|
||||||
|
<div class="row">
|
||||||
|
<img ng-if="logo" ng-src="{{ logo }}" class="simple-box-logo">
|
||||||
|
<img ng-if="!logo" src="images/logo_alt.png" class="simple-box-logo" alt="Portainer">
|
||||||
|
</div>
|
||||||
|
<!-- !simple box logo -->
|
||||||
|
<!-- init password panel -->
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-body">
|
||||||
|
<!-- init password form -->
|
||||||
|
<form class="simple-box-form form-horizontal">
|
||||||
|
<!-- note -->
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<span class="small text-muted">
|
||||||
|
Please create the initial administrator user.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !note -->
|
||||||
|
<!-- username-input -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="username" class="col-sm-4 control-label text-left">
|
||||||
|
Username
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-8">
|
||||||
|
<input type="text" class="form-control" id="username" ng-model="formValues.Username" placeholder="e.g. admin">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !username-input -->
|
||||||
|
<!-- new-password-input -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password" class="col-sm-4 control-label text-left">Password</label>
|
||||||
|
<div class="col-sm-8">
|
||||||
|
<input type="password" class="form-control" ng-model="formValues.Password" id="password">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !new-password-input -->
|
||||||
|
<!-- confirm-password-input -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="confirm_password" class="col-sm-4 control-label text-left">Confirm password</label>
|
||||||
|
<div class="col-sm-8">
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="password" class="form-control" ng-model="formValues.ConfirmPassword" id="confirm_password">
|
||||||
|
<span class="input-group-addon"><i ng-class="{true: 'fa fa-check green-icon', false: 'fa fa-times red-icon'}[formValues.Password !== '' && formValues.Password === formValues.ConfirmPassword]" aria-hidden="true"></i></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !confirm-password-input -->
|
||||||
|
<!-- note -->
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<span class="small text-muted">
|
||||||
|
<i ng-class="{true: 'fa fa-check green-icon', false: 'fa fa-times red-icon'}[formValues.Password.length >= 8]" aria-hidden="true"></i>
|
||||||
|
The password must be at least 8 characters long
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !note -->
|
||||||
|
<!-- actions -->
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<button type="submit" class="btn btn-primary btn-sm" ng-disabled="formValues.Password.length < 8 || formValues.Password !== formValues.ConfirmPassword" ng-click="createAdminUser()"><i class="fa fa-user-plus" aria-hidden="true"></i> Create user</button>
|
||||||
|
<i id="createResourceSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !actions -->
|
||||||
|
</form>
|
||||||
|
<!-- !init password form -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !init password panel -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !simple box -->
|
||||||
|
</div>
|
|
@ -0,0 +1,33 @@
|
||||||
|
angular.module('initAdmin', [])
|
||||||
|
.controller('InitAdminController', ['$scope', '$state', '$sanitize', 'Notifications', 'Authentication', 'StateManager', 'UserService',
|
||||||
|
function ($scope, $state, $sanitize, Notifications, Authentication, StateManager, UserService) {
|
||||||
|
|
||||||
|
$scope.logo = StateManager.getState().application.logo;
|
||||||
|
|
||||||
|
$scope.formValues = {
|
||||||
|
Username: 'admin',
|
||||||
|
Password: '',
|
||||||
|
ConfirmPassword: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.createAdminUser = function() {
|
||||||
|
$('#createResourceSpinner').show();
|
||||||
|
var username = $sanitize($scope.formValues.Username);
|
||||||
|
var password = $sanitize($scope.formValues.Password);
|
||||||
|
|
||||||
|
UserService.initAdministrator(username, password)
|
||||||
|
.then(function success() {
|
||||||
|
return Authentication.login(username, password);
|
||||||
|
})
|
||||||
|
.then(function success() {
|
||||||
|
$state.go('init.endpoint');
|
||||||
|
})
|
||||||
|
.catch(function error(err) {
|
||||||
|
Notifications.error('Failure', err, 'Unable to create administrator user');
|
||||||
|
})
|
||||||
|
.finally(function final() {
|
||||||
|
$('#createResourceSpinner').hide();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
}]);
|
|
@ -12,51 +12,74 @@
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<!-- init-endpoint form -->
|
<!-- init-endpoint form -->
|
||||||
<form class="form-horizontal" style="margin: 20px;" enctype="multipart/form-data" method="POST">
|
<form class="simple-box-form form-horizontal">
|
||||||
<!-- comment -->
|
<!-- note -->
|
||||||
<div class="form-group" style="text-align: center;">
|
|
||||||
<h4>Connect Portainer to a Docker engine or Swarm cluster endpoint</h4>
|
|
||||||
</div>
|
|
||||||
<!-- !comment input -->
|
|
||||||
<!-- endpoin-type radio -->
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="radio">
|
<div class="col-sm-12">
|
||||||
<label><input type="radio" name="endpointType" value="local" ng-model="formValues.endpointType" ng-click="resetErrorMessage()">Manage the Docker instance where Portainer is running</label>
|
<span class="small text-muted">
|
||||||
</div>
|
Connect Portainer to the Docker environment you want to manage.
|
||||||
<div class="radio">
|
</span>
|
||||||
<label><input type="radio" name="endpointType" value="remote" ng-model="formValues.endpointType" ng-click="resetErrorMessage()">Manage a remote Docker instance</label>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- endpoint-type radio -->
|
<!-- !note -->
|
||||||
<!-- local-endpoint -->
|
<!-- endpoint-type -->
|
||||||
<div ng-if="formValues.endpointType === 'local'" style="margin-top: 25px;">
|
<div class="form-group" style="margin-bottom: 0">
|
||||||
<div class="form-group">
|
<div class="boxselector_wrapper">
|
||||||
<i class="fa fa-exclamation-triangle" aria-hidden="true" style="margin-right: 5px;"></i>
|
<div>
|
||||||
<span class="small text-primary">This feature is not yet available for native Docker Windows containers.</span>
|
<input type="radio" id="local_endpoint" ng-model="formValues.EndpointType" value="local">
|
||||||
<div class="small text-primary">On Linux and when using Docker for Mac or Docker for Windows or Docker Toolbox, ensure that you have started Portainer container with the following Docker flag <code>-v "/var/run/docker.sock:/var/run/docker.sock"</code></div>
|
<label for="local_endpoint">
|
||||||
|
<div class="boxselector_header">
|
||||||
|
<i class="fa fa-bolt" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||||
|
Local
|
||||||
|
</div>
|
||||||
|
<p>Manage the Docker environment where Portainer is running</p>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input type="radio" id="remote_endpoint" ng-model="formValues.EndpointType" value="remote">
|
||||||
|
<label for="remote_endpoint">
|
||||||
|
<div class="boxselector_header">
|
||||||
|
<i class="fa fa-plug" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||||
|
Remote
|
||||||
|
</div>
|
||||||
|
<p>Manage a remote Docker environment</p>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- connect button -->
|
</div>
|
||||||
<div class="form-group" style="margin-top: 10px;">
|
<!-- !endpoint-type -->
|
||||||
<div class="col-sm-12 controls">
|
<!-- local-endpoint -->
|
||||||
<p class="pull-left text-danger" ng-if="state.error" style="margin: 5px;">
|
<div ng-if="formValues.EndpointType === 'local'">
|
||||||
<i class="fa fa-exclamation-circle" aria-hidden="true"></i> {{ state.error }}
|
<div class="form-group">
|
||||||
</p>
|
<div class="col-sm-12">
|
||||||
<span class="pull-right">
|
<span class="small">
|
||||||
<i id="initEndpointSpinner" class="fa fa-cog fa-spin" style="margin-right: 5px; display: none;"></i>
|
<p class="text-muted">
|
||||||
<button type="submit" class="btn btn-primary" ng-click="createLocalEndpoint()"><i class="fa fa-plug" aria-hidden="true"></i> Connect</button>
|
<i class="fa fa-exclamation-triangle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||||
|
This feature is not yet available for <u>native Docker Windows containers</u>.
|
||||||
|
</p>
|
||||||
|
<p class="text-primary">
|
||||||
|
Please ensure that you have started the Portainer container with the following Docker flag <code>-v "/var/run/docker.sock:/var/run/docker.sock"</code> in order to connect to the local Docker environment.
|
||||||
|
</p>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- !connect button -->
|
<!-- actions -->
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<button type="submit" class="btn btn-primary btn-sm" ng-click="createLocalEndpoint()"><i class="fa fa-bolt" aria-hidden="true"></i> Connect</button>
|
||||||
|
<i id="createResourceSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !actions -->
|
||||||
</div>
|
</div>
|
||||||
<!-- !local-endpoint -->
|
<!-- !local-endpoint -->
|
||||||
<!-- remote-endpoint -->
|
<!-- remote-endpoint -->
|
||||||
<div ng-if="formValues.endpointType === 'remote'" style="margin-top: 25px;">
|
<div ng-if="formValues.EndpointType === 'remote'">
|
||||||
<!-- name-input -->
|
<!-- name-input -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="container_name" class="col-sm-4 col-lg-3 control-label text-left">Name</label>
|
<label for="endpoint_name" class="col-sm-4 col-lg-3 control-label text-left">Name</label>
|
||||||
<div class="col-sm-8 col-lg-9">
|
<div class="col-sm-8 col-lg-9">
|
||||||
<input type="text" class="form-control" id="container_name" ng-model="formValues.Name" placeholder="e.g. docker-prod01">
|
<input type="text" class="form-control" id="endpoint_name" ng-model="formValues.Name" placeholder="e.g. docker-prod01">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- !name-input -->
|
<!-- !name-input -->
|
||||||
|
@ -127,19 +150,14 @@
|
||||||
<!-- !key-input -->
|
<!-- !key-input -->
|
||||||
</div>
|
</div>
|
||||||
<!-- !tls-certs -->
|
<!-- !tls-certs -->
|
||||||
<!-- connect button -->
|
<!-- actions -->
|
||||||
<div class="form-group" style="margin-top: 10px;">
|
<div class="form-group">
|
||||||
<div class="col-sm-12 controls">
|
<div class="col-sm-12">
|
||||||
<p class="pull-left text-danger" ng-if="state.error" style="margin: 5px;">
|
<button type="submit" class="btn btn-primary btn-sm" ng-disabled="!formValues.Name || !formValues.URL || (formValues.TLS && (!formValues.TLSCACert || !formValues.TLSCert || !formValues.TLSKey))" ng-click="createRemoteEndpoint()"><i class="fa fa-plug" aria-hidden="true"></i> Connect</button>
|
||||||
<i class="fa fa-exclamation-circle" aria-hidden="true"></i> {{ state.error }}
|
<i id="createResourceSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
|
||||||
</p>
|
|
||||||
<span class="pull-right">
|
|
||||||
<i id="initEndpointSpinner" class="fa fa-cog fa-spin" style="margin-right: 5px; display: none;"></i>
|
|
||||||
<button type="submit" class="btn btn-primary" ng-disabled="!formValues.Name || !formValues.URL || (formValues.TLS && (!formValues.TLSCACert || !formValues.TLSCert || !formValues.TLSKey))" ng-click="createRemoteEndpoint()"><i class="fa fa-plug" aria-hidden="true"></i> Connect</button>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- !connect button -->
|
<!-- !actions -->
|
||||||
</div>
|
</div>
|
||||||
<!-- !remote-endpoint -->
|
<!-- !remote-endpoint -->
|
||||||
</form>
|
</form>
|
|
@ -1,13 +1,15 @@
|
||||||
angular.module('endpointInit', [])
|
angular.module('initEndpoint', [])
|
||||||
.controller('EndpointInitController', ['$scope', '$state', 'EndpointService', 'StateManager', 'EndpointProvider', 'Notifications',
|
.controller('InitEndpointController', ['$scope', '$state', 'EndpointService', 'StateManager', 'EndpointProvider', 'Notifications',
|
||||||
function ($scope, $state, EndpointService, StateManager, EndpointProvider, Notifications) {
|
function ($scope, $state, EndpointService, StateManager, EndpointProvider, Notifications) {
|
||||||
|
|
||||||
|
$scope.logo = StateManager.getState().application.logo;
|
||||||
|
|
||||||
$scope.state = {
|
$scope.state = {
|
||||||
error: '',
|
|
||||||
uploadInProgress: false
|
uploadInProgress: false
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.formValues = {
|
$scope.formValues = {
|
||||||
endpointType: 'remote',
|
EndpointType: 'remote',
|
||||||
Name: '',
|
Name: '',
|
||||||
URL: '',
|
URL: '',
|
||||||
TLS: false,
|
TLS: false,
|
||||||
|
@ -20,51 +22,31 @@ function ($scope, $state, EndpointService, StateManager, EndpointProvider, Notif
|
||||||
$state.go('dashboard');
|
$state.go('dashboard');
|
||||||
}
|
}
|
||||||
|
|
||||||
$scope.resetErrorMessage = function() {
|
|
||||||
$scope.state.error = '';
|
|
||||||
};
|
|
||||||
|
|
||||||
function showErrorMessage(message) {
|
$scope.createLocalEndpoint = function() {
|
||||||
$scope.state.uploadInProgress = false;
|
$('#createResourceSpinner').show();
|
||||||
$scope.state.error = message;
|
var name = 'local';
|
||||||
}
|
var URL = 'unix:///var/run/docker.sock';
|
||||||
|
|
||||||
function updateEndpointState(endpointID) {
|
EndpointService.createLocalEndpoint(name, URL, false, true)
|
||||||
EndpointProvider.setEndpointID(endpointID);
|
.then(function success(data) {
|
||||||
StateManager.updateEndpointState(false)
|
var endpointID = data.Id;
|
||||||
|
EndpointProvider.setEndpointID(endpointID);
|
||||||
|
return StateManager.updateEndpointState(false);
|
||||||
|
})
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
$state.go('dashboard');
|
$state.go('dashboard');
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
EndpointService.deleteEndpoint(endpointID)
|
Notifications.error('Failure', err, 'Unable to connect to the Docker endpoint');
|
||||||
.then(function success() {
|
|
||||||
showErrorMessage('Unable to connect to the Docker endpoint');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.createLocalEndpoint = function() {
|
|
||||||
$('#initEndpointSpinner').show();
|
|
||||||
$scope.state.error = '';
|
|
||||||
var name = 'local';
|
|
||||||
var URL = 'unix:///var/run/docker.sock';
|
|
||||||
var TLS = false;
|
|
||||||
|
|
||||||
EndpointService.createLocalEndpoint(name, URL, TLS, true)
|
|
||||||
.then(function success(data) {
|
|
||||||
var endpointID = data.Id;
|
|
||||||
updateEndpointState(data.Id);
|
|
||||||
}, function error() {
|
|
||||||
$scope.state.error = 'Unable to create endpoint';
|
|
||||||
})
|
})
|
||||||
.finally(function final() {
|
.finally(function final() {
|
||||||
$('#initEndpointSpinner').hide();
|
$('#createResourceSpinner').hide();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.createRemoteEndpoint = function() {
|
$scope.createRemoteEndpoint = function() {
|
||||||
$('#initEndpointSpinner').show();
|
$('#createResourceSpinner').show();
|
||||||
$scope.state.error = '';
|
|
||||||
var name = $scope.formValues.Name;
|
var name = $scope.formValues.Name;
|
||||||
var URL = $scope.formValues.URL;
|
var URL = $scope.formValues.URL;
|
||||||
var PublicURL = URL.split(':')[0];
|
var PublicURL = URL.split(':')[0];
|
||||||
|
@ -76,16 +58,17 @@ function ($scope, $state, EndpointService, StateManager, EndpointProvider, Notif
|
||||||
EndpointService.createRemoteEndpoint(name, URL, PublicURL, TLS, TLSCAFile, TLSCertFile, TLSKeyFile)
|
EndpointService.createRemoteEndpoint(name, URL, PublicURL, TLS, TLSCAFile, TLSCertFile, TLSKeyFile)
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
var endpointID = data.Id;
|
var endpointID = data.Id;
|
||||||
updateEndpointState(endpointID);
|
EndpointProvider.setEndpointID(endpointID);
|
||||||
}, function error(err) {
|
return StateManager.updateEndpointState(false);
|
||||||
showErrorMessage(err.msg);
|
})
|
||||||
}, function update(evt) {
|
.then(function success(data) {
|
||||||
if (evt.upload) {
|
$state.go('dashboard');
|
||||||
$scope.state.uploadInProgress = evt.upload;
|
})
|
||||||
}
|
.catch(function error(err) {
|
||||||
|
Notifications.error('Failure', err, 'Unable to connect to the Docker endpoint');
|
||||||
})
|
})
|
||||||
.finally(function final() {
|
.finally(function final() {
|
||||||
$('#initEndpointSpinner').hide();
|
$('#createResourceSpinner').hide();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}]);
|
}]);
|
|
@ -134,5 +134,26 @@ angular.module('portainer.services')
|
||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
service.initAdministrator = function(username, password) {
|
||||||
|
return Users.initAdminUser({ Username: username, Password: password }).$promise;
|
||||||
|
};
|
||||||
|
|
||||||
|
service.administratorExists = function() {
|
||||||
|
var deferred = $q.defer();
|
||||||
|
|
||||||
|
Users.checkAdminUser({}).$promise
|
||||||
|
.then(function success(data) {
|
||||||
|
deferred.resolve(true);
|
||||||
|
})
|
||||||
|
.catch(function error(err) {
|
||||||
|
if (err.status === 404) {
|
||||||
|
deferred.resolve(false);
|
||||||
|
}
|
||||||
|
deferred.reject({ msg: 'Unable to verify administrator account existence', err: err });
|
||||||
|
});
|
||||||
|
|
||||||
|
return deferred.promise;
|
||||||
|
};
|
||||||
|
|
||||||
return service;
|
return service;
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -254,11 +254,11 @@ a[ng-click]{
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-form > div {
|
.simple-box-form > div {
|
||||||
margin-bottom: 25px;
|
margin-bottom: 25px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-form > div:last-child {
|
.simple-box-form > div:last-child {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,8 +29,11 @@
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body ng-controller="MainController">
|
<body ng-controller="MainController">
|
||||||
<div id="page-wrapper" ng-class="{open: toggle && $state.current.name !== 'auth' && $state.current.name !== 'endpointInit' && $state.current.name !== 'init', nopadding: $state.current.name === 'auth' || $state.current.name === 'endpointInit' || $state.current.name === 'init' || applicationState.loading }" ng-cloak>
|
<div id="page-wrapper" ng-class="{
|
||||||
|
open: toggle && ['auth', 'init', 'init.endpoint', 'init.admin'].indexOf($state.current.name) === -1,
|
||||||
|
nopadding: ['auth', 'init', 'init.endpoint', 'init.admin'].indexOf($state.current.name) > -1 || applicationState.loading
|
||||||
|
}"
|
||||||
|
ng-cloak>
|
||||||
<div id="sideview" ui-view="sidebar" ng-if="!applicationState.loading"></div>
|
<div id="sideview" ui-view="sidebar" ng-if="!applicationState.loading"></div>
|
||||||
|
|
||||||
<div id="content-wrapper">
|
<div id="content-wrapper">
|
||||||
|
|
Loading…
Reference in New Issue