mirror of https://github.com/portainer/portainer
feat(users): add the ability to rename a user (#3884)
* feat(users): update username in server * feat(users): add username text field * fix(users): rename label and change buttons size * feat(users): change update message * feat(users): disable submit when not changed * feat(users): confirm updating username * feat(users): minor UI update Co-authored-by: Anthony Lapenna <lapenna.anthony@gmail.com>pull/2776/merge
parent
7325407f5f
commit
25ca036070
|
@ -3,6 +3,7 @@ package users
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/asaskevich/govalidator"
|
||||||
httperror "github.com/portainer/libhttp/error"
|
httperror "github.com/portainer/libhttp/error"
|
||||||
"github.com/portainer/libhttp/request"
|
"github.com/portainer/libhttp/request"
|
||||||
"github.com/portainer/libhttp/response"
|
"github.com/portainer/libhttp/response"
|
||||||
|
@ -11,11 +12,16 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type userUpdatePayload struct {
|
type userUpdatePayload struct {
|
||||||
|
Username string
|
||||||
Password string
|
Password string
|
||||||
Role int
|
Role int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (payload *userUpdatePayload) Validate(r *http.Request) error {
|
func (payload *userUpdatePayload) Validate(r *http.Request) error {
|
||||||
|
if govalidator.Contains(payload.Username, " ") {
|
||||||
|
return portainer.Error("Invalid username. Must not contain any whitespace")
|
||||||
|
}
|
||||||
|
|
||||||
if payload.Role != 0 && payload.Role != 1 && payload.Role != 2 {
|
if payload.Role != 0 && payload.Role != 1 && payload.Role != 2 {
|
||||||
return portainer.Error("Invalid role value. Value must be one of: 1 (administrator) or 2 (regular user)")
|
return portainer.Error("Invalid role value. Value must be one of: 1 (administrator) or 2 (regular user)")
|
||||||
}
|
}
|
||||||
|
@ -55,6 +61,18 @@ func (handler *Handler) userUpdate(w http.ResponseWriter, r *http.Request) *http
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a user with the specified identifier inside the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a user with the specified identifier inside the database", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if payload.Username != "" && payload.Username != user.Username {
|
||||||
|
sameNameUser, err := handler.DataStore.User().UserByUsername(payload.Username)
|
||||||
|
if err != nil && err != portainer.ErrObjectNotFound {
|
||||||
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve users from the database", err}
|
||||||
|
}
|
||||||
|
if sameNameUser != nil && sameNameUser.ID != portainer.UserID(userID) {
|
||||||
|
return &httperror.HandlerError{http.StatusConflict, "Another user with the same username already exists", portainer.ErrUserAlreadyExists}
|
||||||
|
}
|
||||||
|
|
||||||
|
user.Username = payload.Username
|
||||||
|
}
|
||||||
|
|
||||||
if payload.Password != "" {
|
if payload.Password != "" {
|
||||||
user.Password, err = handler.CryptoService.Hash(payload.Password)
|
user.Password, err = handler.CryptoService.Hash(payload.Password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -78,12 +78,8 @@ angular.module('portainer.app').factory('UserService', [
|
||||||
return Users.remove({ id: id }).$promise;
|
return Users.remove({ id: id }).$promise;
|
||||||
};
|
};
|
||||||
|
|
||||||
service.updateUser = function (id, password, role) {
|
service.updateUser = function (id, { password, role, username }) {
|
||||||
var query = {
|
return Users.update({ id }, { password, role, username }).$promise;
|
||||||
password: password,
|
|
||||||
role: role,
|
|
||||||
};
|
|
||||||
return Users.update({ id: id }, query).$promise;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
service.updateUserPassword = function (id, currentPassword, newPassword) {
|
service.updateUserPassword = function (id, currentPassword, newPassword) {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<rd-header>
|
<rd-header>
|
||||||
<rd-header-title title-text="User details"></rd-header-title>
|
<rd-header-title title-text="User details"></rd-header-title>
|
||||||
<rd-header-content>
|
<rd-header-content>
|
||||||
<a ui-sref="portainer.users">Users</a> > <a ui-sref="portainer.users.user({id: user.Id})">{{ user.Username }}</a>
|
<a ui-sref="portainer.users">Users</a> > <a ui-sref="portainer.users.user({id: user.Id})">{{ formValues.username }}</a>
|
||||||
</rd-header-content>
|
</rd-header-content>
|
||||||
</rd-header>
|
</rd-header>
|
||||||
|
|
||||||
|
@ -9,30 +9,35 @@
|
||||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||||
<rd-widget>
|
<rd-widget>
|
||||||
<rd-widget-header icon="fa-user" title-text="User details"></rd-widget-header>
|
<rd-widget-header icon="fa-user" title-text="User details"></rd-widget-header>
|
||||||
<rd-widget-body classes="no-padding">
|
<rd-widget-body>
|
||||||
<table class="table">
|
<form class="form-horizontal" style="margin-top: 15px;">
|
||||||
<tbody>
|
<div class="form-group">
|
||||||
<tr>
|
<label for="username_field" class="col-sm-2 control-label text-left">Username</label>
|
||||||
<td><label>Name</label></td>
|
<div class="col-sm-8">
|
||||||
<td>
|
<input class="form-control" ng-model="formValues.username" id="username_field" />
|
||||||
{{ user.Username }}
|
</div>
|
||||||
<button class="btn btn-xs btn-danger" ng-click="deleteUser()"><i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Delete this user</button>
|
</div>
|
||||||
</td>
|
|
||||||
</tr>
|
<div class="form-group" ng-if="isAdmin">
|
||||||
<tr ng-if="isAdmin">
|
<div class="col-sm-4">
|
||||||
<td colspan="2">
|
<label for="permissions" class="control-label text-left">
|
||||||
<label for="permissions" class="control-label text-left">
|
Administrator
|
||||||
Administrator
|
<portainer-tooltip
|
||||||
<portainer-tooltip
|
position="bottom"
|
||||||
position="bottom"
|
message="Administrators have access to Portainer settings management as well as full control over all defined endpoints and their resources."
|
||||||
message="Administrators have access to Portainer settings management as well as full control over all defined endpoints and their resources."
|
></portainer-tooltip>
|
||||||
></portainer-tooltip>
|
</label>
|
||||||
</label>
|
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="formValues.Administrator" /><i></i> </label>
|
||||||
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="formValues.Administrator" ng-change="updatePermissions()" /><i></i> </label>
|
</div>
|
||||||
</td>
|
</div>
|
||||||
</tr>
|
|
||||||
</tbody>
|
<div class="form-group">
|
||||||
</table>
|
<div class="col-sm-4">
|
||||||
|
<button class="btn btn-primary btn-sm" ng-disabled="!isSubmitEnabled()" ng-click="updateUser()">Save</button>
|
||||||
|
<button class="btn btn-danger btn-sm" ng-click="deleteUser()"><i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Delete this user</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</rd-widget-body>
|
</rd-widget-body>
|
||||||
</rd-widget>
|
</rd-widget>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -14,6 +14,7 @@ angular.module('portainer.app').controller('UserController', [
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.formValues = {
|
$scope.formValues = {
|
||||||
|
username: '',
|
||||||
newPassword: '',
|
newPassword: '',
|
||||||
confirmPassword: '',
|
confirmPassword: '',
|
||||||
Administrator: false,
|
Administrator: false,
|
||||||
|
@ -28,12 +29,33 @@ angular.module('portainer.app').controller('UserController', [
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.updatePermissions = function () {
|
$scope.updateUser = async function () {
|
||||||
var role = $scope.formValues.Administrator ? 1 : 2;
|
const role = $scope.formValues.Administrator ? 1 : 2;
|
||||||
UserService.updateUser($scope.user.Id, undefined, role, 0)
|
const oldUsername = $scope.user.Username;
|
||||||
|
const username = $scope.formValues.username;
|
||||||
|
let promise = Promise.resolve(true);
|
||||||
|
if (username != oldUsername) {
|
||||||
|
promise = new Promise((resolve) =>
|
||||||
|
ModalService.confirm({
|
||||||
|
title: 'Are you sure?',
|
||||||
|
message: `Are you sure you want to rename the user ${oldUsername} to ${username}?`,
|
||||||
|
buttons: {
|
||||||
|
confirm: {
|
||||||
|
label: 'Update',
|
||||||
|
className: 'btn-primary',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
callback: resolve,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const confirmed = await promise;
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
UserService.updateUser($scope.user.Id, { role, username })
|
||||||
.then(function success() {
|
.then(function success() {
|
||||||
var newRole = role === 1 ? 'administrator' : 'user';
|
Notifications.success('User successfully updated');
|
||||||
Notifications.success('Permissions successfully updated', $scope.user.Username + ' is now ' + newRole);
|
|
||||||
$state.reload();
|
$state.reload();
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
|
@ -42,7 +64,7 @@ angular.module('portainer.app').controller('UserController', [
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.updatePassword = function () {
|
$scope.updatePassword = function () {
|
||||||
UserService.updateUser($scope.user.Id, $scope.formValues.newPassword, undefined, -1)
|
UserService.updateUser($scope.user.Id, { password: $scope.formValues.newPassword })
|
||||||
.then(function success() {
|
.then(function success() {
|
||||||
Notifications.success('Password successfully updated');
|
Notifications.success('Password successfully updated');
|
||||||
$state.reload();
|
$state.reload();
|
||||||
|
@ -63,6 +85,12 @@ angular.module('portainer.app').controller('UserController', [
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$scope.isSubmitEnabled = isSubmitEnabled;
|
||||||
|
function isSubmitEnabled() {
|
||||||
|
const { user, formValues } = $scope;
|
||||||
|
return user && (user.Username !== formValues.username || (formValues.Administrator && user.Role !== 1) || (!formValues.Administrator && user.Role === 1));
|
||||||
|
}
|
||||||
|
|
||||||
function initView() {
|
function initView() {
|
||||||
$scope.isAdmin = Authentication.isAdmin();
|
$scope.isAdmin = Authentication.isAdmin();
|
||||||
|
|
||||||
|
@ -74,6 +102,7 @@ angular.module('portainer.app').controller('UserController', [
|
||||||
var user = data.user;
|
var user = data.user;
|
||||||
$scope.user = user;
|
$scope.user = user;
|
||||||
$scope.formValues.Administrator = user.Role === 1;
|
$scope.formValues.Administrator = user.Role === 1;
|
||||||
|
$scope.formValues.username = user.Username;
|
||||||
$scope.AuthenticationMethod = data.settings.AuthenticationMethod;
|
$scope.AuthenticationMethod = data.settings.AuthenticationMethod;
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
|
|
Loading…
Reference in New Issue