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
Chaim Lev-Ari 2020-06-09 05:42:40 +03:00 committed by GitHub
parent 7325407f5f
commit 25ca036070
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 85 additions and 37 deletions

View File

@ -3,6 +3,7 @@ package users
import (
"net/http"
"github.com/asaskevich/govalidator"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
@ -11,11 +12,16 @@ import (
)
type userUpdatePayload struct {
Username string
Password string
Role int
}
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 {
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}
}
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 != "" {
user.Password, err = handler.CryptoService.Hash(payload.Password)
if err != nil {

View File

@ -78,12 +78,8 @@ angular.module('portainer.app').factory('UserService', [
return Users.remove({ id: id }).$promise;
};
service.updateUser = function (id, password, role) {
var query = {
password: password,
role: role,
};
return Users.update({ id: id }, query).$promise;
service.updateUser = function (id, { password, role, username }) {
return Users.update({ id }, { password, role, username }).$promise;
};
service.updateUserPassword = function (id, currentPassword, newPassword) {

View File

@ -1,7 +1,7 @@
<rd-header>
<rd-header-title title-text="User details"></rd-header-title>
<rd-header-content>
<a ui-sref="portainer.users">Users</a> &gt; <a ui-sref="portainer.users.user({id: user.Id})">{{ user.Username }}</a>
<a ui-sref="portainer.users">Users</a> &gt; <a ui-sref="portainer.users.user({id: user.Id})">{{ formValues.username }}</a>
</rd-header-content>
</rd-header>
@ -9,30 +9,35 @@
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-user" title-text="User details"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<tbody>
<tr>
<td><label>Name</label></td>
<td>
{{ user.Username }}
<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>
</td>
</tr>
<tr ng-if="isAdmin">
<td colspan="2">
<label for="permissions" class="control-label text-left">
Administrator
<portainer-tooltip
position="bottom"
message="Administrators have access to Portainer settings management as well as full control over all defined endpoints and their resources."
></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="formValues.Administrator" ng-change="updatePermissions()" /><i></i> </label>
</td>
</tr>
</tbody>
</table>
<rd-widget-body>
<form class="form-horizontal" style="margin-top: 15px;">
<div class="form-group">
<label for="username_field" class="col-sm-2 control-label text-left">Username</label>
<div class="col-sm-8">
<input class="form-control" ng-model="formValues.username" id="username_field" />
</div>
</div>
<div class="form-group" ng-if="isAdmin">
<div class="col-sm-4">
<label for="permissions" class="control-label text-left">
Administrator
<portainer-tooltip
position="bottom"
message="Administrators have access to Portainer settings management as well as full control over all defined endpoints and their resources."
></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="formValues.Administrator" /><i></i> </label>
</div>
</div>
<div class="form-group">
<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>
</div>

View File

@ -14,6 +14,7 @@ angular.module('portainer.app').controller('UserController', [
};
$scope.formValues = {
username: '',
newPassword: '',
confirmPassword: '',
Administrator: false,
@ -28,12 +29,33 @@ angular.module('portainer.app').controller('UserController', [
});
};
$scope.updatePermissions = function () {
var role = $scope.formValues.Administrator ? 1 : 2;
UserService.updateUser($scope.user.Id, undefined, role, 0)
$scope.updateUser = async function () {
const role = $scope.formValues.Administrator ? 1 : 2;
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() {
var newRole = role === 1 ? 'administrator' : 'user';
Notifications.success('Permissions successfully updated', $scope.user.Username + ' is now ' + newRole);
Notifications.success('User successfully updated');
$state.reload();
})
.catch(function error(err) {
@ -42,7 +64,7 @@ angular.module('portainer.app').controller('UserController', [
};
$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() {
Notifications.success('Password successfully updated');
$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() {
$scope.isAdmin = Authentication.isAdmin();
@ -74,6 +102,7 @@ angular.module('portainer.app').controller('UserController', [
var user = data.user;
$scope.user = user;
$scope.formValues.Administrator = user.Role === 1;
$scope.formValues.username = user.Username;
$scope.AuthenticationMethod = data.settings.AuthenticationMethod;
})
.catch(function error(err) {