TB-71: Ability to activate users without sending email.

This commit is contained in:
Igor Kulikov 2017-07-21 17:31:59 +03:00
parent 3019bf7570
commit 86515896dd
15 changed files with 331 additions and 13 deletions

View File

@ -173,7 +173,12 @@ public class AuthController extends BaseController {
String baseUrl = constructBaseUrl(request);
String loginUrl = String.format("%s/login", baseUrl);
String email = user.getEmail();
mailService.sendAccountActivatedEmail(loginUrl, email);
try {
mailService.sendAccountActivatedEmail(loginUrl, email);
} catch (Exception e) {
log.info("Unable to send account activation email [{}]", e.getMessage());
}
JwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser);
JwtToken refreshToken = refreshTokenRepository.requestRefreshToken(securityUser);

View File

@ -63,6 +63,7 @@ public class UserController extends BaseController {
@RequestMapping(value = "/user", method = RequestMethod.POST)
@ResponseBody
public User saveUser(@RequestBody User user,
@RequestParam(required = false, defaultValue = "true") boolean sendActivationMail,
HttpServletRequest request) throws ThingsboardException {
try {
SecurityUser authUser = getCurrentUser();
@ -70,7 +71,7 @@ public class UserController extends BaseController {
throw new ThingsboardException("You don't have permission to perform this operation!",
ThingsboardErrorCode.PERMISSION_DENIED);
}
boolean sendEmail = user.getId() == null;
boolean sendEmail = user.getId() == null && sendActivationMail;
if (getCurrentUser().getAuthority() == Authority.TENANT_ADMIN) {
user.setTenantId(getCurrentUser().getTenantId());
}
@ -116,6 +117,35 @@ public class UserController extends BaseController {
}
}
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/user/{userId}/activationLink", method = RequestMethod.GET, produces = "text/plain")
@ResponseBody
public String getActivationLink(
@PathVariable("userId") String strUserId,
HttpServletRequest request) throws ThingsboardException {
checkParameter("userId", strUserId);
try {
UserId userId = new UserId(toUUID(strUserId));
SecurityUser authUser = getCurrentUser();
if (authUser.getAuthority() == Authority.CUSTOMER_USER && !authUser.getId().equals(userId)) {
throw new ThingsboardException("You don't have permission to perform this operation!",
ThingsboardErrorCode.PERMISSION_DENIED);
}
User user = checkUserId(userId);
UserCredentials userCredentials = userService.findUserCredentialsByUserId(user.getId());
if (!userCredentials.isEnabled()) {
String baseUrl = constructBaseUrl(request);
String activateUrl = String.format("%s/api/noauth/activate?activateToken=%s", baseUrl,
userCredentials.getActivateToken());
return activateUrl;
} else {
throw new ThingsboardException("User is already active!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
}
} catch (Exception e) {
throw handleException(e);
}
}
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/user/{userId}", method = RequestMethod.DELETE)
@ResponseStatus(value = HttpStatus.OK)

View File

@ -45,6 +45,7 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi
isUserLoaded: isUserLoaded,
saveUser: saveUser,
sendActivationEmail: sendActivationEmail,
getActivationLink: getActivationLink,
setUserFromJwtToken: setUserFromJwtToken,
getJwtToken: getJwtToken,
clearJwtToken: clearJwtToken,
@ -397,9 +398,12 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi
return deferred.promise;
}
function saveUser(user) {
function saveUser(user, sendActivationMail) {
var deferred = $q.defer();
var url = '/api/user';
if (angular.isDefined(sendActivationMail)) {
url += '?sendActivationMail=' + sendActivationMail;
}
$http.post(url, user).then(function success(response) {
deferred.resolve(response.data);
}, function fail(response) {
@ -441,6 +445,17 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi
return deferred.promise;
}
function getActivationLink(userId) {
var deferred = $q.defer();
var url = `/api/user/${userId}/activationLink`
$http.get(url).then(function success(response) {
deferred.resolve(response.data);
}, function fail() {
deferred.reject();
});
return deferred.promise;
}
function forceDefaultPlace(to, params) {
if (currentUser && isAuthenticated()) {
if (currentUser.authority === 'TENANT_ADMIN' || currentUser.authority === 'CUSTOMER_USER') {

View File

@ -74,6 +74,11 @@ export default function AppRun($rootScope, $window, $injector, $location, $log,
var locationSearch = $location.search();
var publicId = locationSearch.publicId;
var activateToken = locationSearch.activateToken;
if (to.url === '/createPassword?activateToken' && activateToken && activateToken.length) {
userService.setUserFromJwtToken(null, null, false);
}
if (userService.isUserLoaded() === true) {
if (userService.isAuthenticated()) {
@ -124,7 +129,7 @@ export default function AppRun($rootScope, $window, $injector, $location, $log,
}
})
$rootScope.pageTitle = 'Thingsboard';
$rootScope.pageTitle = 'ThingsBoard';
$rootScope.stateChangeSuccessHandle = $rootScope.$on('$stateChangeSuccess', function (evt, to, params) {
if (userService.isPublic() && to.name === 'home.dashboards.dashboard') {
@ -133,9 +138,9 @@ export default function AppRun($rootScope, $window, $injector, $location, $log,
}
if (angular.isDefined(to.data.pageTitle)) {
$translate(to.data.pageTitle).then(function (translation) {
$rootScope.pageTitle = 'Thingsboard | ' + translation;
$rootScope.pageTitle = 'ThingsBoard | ' + translation;
}, function (translationId) {
$rootScope.pageTitle = 'Thingsboard | ' + translationId;
$rootScope.pageTitle = 'ThingsBoard | ' + translationId;
});
}
})

View File

@ -26,6 +26,7 @@ import gridTemplate from './grid.tpl.html';
export default angular.module('thingsboard.directives.grid', [thingsboardScopeElement, thingsboardDetailsSidenav])
.directive('tbGrid', Grid)
.controller('AddItemController', AddItemController)
.controller('ItemCardController', ItemCardController)
.directive('tbGridCardContent', GridCardContent)
.filter('range', RangeFilter)
@ -342,6 +343,11 @@ function GridController($scope, $state, $mdDialog, $document, $q, $timeout, $tra
} else {
vm.itemCardController = 'ItemCardController';
}
if (vm.config.addItemController) {
vm.addItemController = vm.config.addItemController;
} else {
vm.addItemController = 'AddItemController';
}
vm.parentCtl = vm.config.parentCtl || vm;
@ -468,7 +474,7 @@ function GridController($scope, $state, $mdDialog, $document, $q, $timeout, $tra
function addItem($event) {
$mdDialog.show({
controller: AddItemController,
controller: vm.addItemController,
controllerAs: 'vm',
templateUrl: vm.addItemTemplateUrl,
parent: angular.element($document[0].body),

View File

@ -1037,6 +1037,7 @@ export default angular.module('thingsboard.locale', [])
"resend-activation": "Resend activation",
"email": "Email",
"email-required": "Email is required.",
"invalid-email-format": "Invalid email format.",
"first-name": "First Name",
"last-name": "Last Name",
"description": "Description",
@ -1044,7 +1045,14 @@ export default angular.module('thingsboard.locale', [])
"always-fullscreen": "Always fullscreen",
"select-user": "Select user",
"no-users-matching": "No users matching '{{entity}}' were found.",
"user-required": "User is required"
"user-required": "User is required",
"activation-method": "Activation method",
"display-activation-link": "Display activation link",
"send-activation-mail": "Send activation mail",
"activation-link": "User activation link",
"activation-link-text": "In order to activate user use the following <a href='{{activationLink}}' target='_blank'>activation link</a> :",
"copy-activation-link": "Copy activation link",
"activation-link-copied-message": "User activation link has been copied to clipboard"
},
"value": {
"type": "Value type",

View File

@ -0,0 +1,35 @@
/*
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed 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.
*/
/*@ngInject*/
export default function ActivationLinkDialogController($mdDialog, $translate, toast, activationLink) {
var vm = this;
vm.activationLink = activationLink;
vm.onActivationLinkCopied = onActivationLinkCopied;
vm.close = close;
function onActivationLinkCopied(){
toast.showSuccess($translate.instant('user.activation-link-copied-message'), 750, angular.element('#activation-link-dialog-content'), 'bottom left');
}
function close() {
$mdDialog.hide();
}
}

View File

@ -0,0 +1,55 @@
<!--
Copyright © 2016-2017 The Thingsboard Authors
Licensed 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.
-->
<md-dialog aria-label="{{ 'user.activation-link' | translate }}" style="min-width: 400px;">
<form>
<md-toolbar>
<div class="md-toolbar-tools">
<h2 translate="user.activation-link"></h2>
<span flex></span>
<md-button class="md-icon-button" ng-click="vm.close()">
<ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
</md-button>
</div>
</md-toolbar>
<md-dialog-content>
<div id="activation-link-dialog-content" class="md-dialog-content">
<md-content class="md-padding" layout="column">
<span translate="user.activation-link-text" translate-values="{activationLink: vm.activationLink}"></span>
<div layout="row" layout-align="start center">
<pre class="tb-highlight" flex><code>{{ vm.activationLink }}</code></pre>
<md-button class="md-icon-button"
ngclipboard
data-clipboard-text="{{ vm.activationLink }}"
ngclipboard-success="vm.onActivationLinkCopied(e)">
<md-icon md-svg-icon="mdi:clipboard-arrow-left"></md-icon>
<md-tooltip md-direction="top">
{{ 'user.copy-activation-link' | translate }}
</md-tooltip>
</md-button>
</div>
</md-content>
</div>
</md-dialog-content>
<md-dialog-actions layout="row">
<span flex></span>
<md-button ng-click="vm.close()">{{ 'action.ok' |
translate }}
</md-button>
</md-dialog-actions>
</form>
</md-dialog>

View File

@ -0,0 +1,112 @@
/*
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed 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.
*/
/* eslint-disable import/no-unresolved, import/default */
import activationLinkDialogTemplate from './activation-link.dialog.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/
export default function AddUserController($scope, $mdDialog, $state, $stateParams, $document, $q, types, userService, saveItemFunction, helpLinks) {
var vm = this;
var tenantId = $stateParams.tenantId;
var customerId = $stateParams.customerId;
var usersType = $state.$current.data.usersType;
vm.helpLinks = helpLinks;
vm.item = {};
vm.activationMethods = [
{
value: 'displayActivationLink',
name: 'user.display-activation-link'
},
{
value: 'sendActivationMail',
name: 'user.send-activation-mail'
}
];
vm.userActivationMethod = 'displayActivationLink';
vm.add = add;
vm.cancel = cancel;
function cancel() {
$mdDialog.cancel();
}
function add($event) {
var sendActivationMail = false;
if (vm.userActivationMethod == 'sendActivationMail') {
sendActivationMail = true;
}
if (usersType === 'tenant') {
vm.item.authority = "TENANT_ADMIN";
vm.item.tenantId = {
entityType: types.entityType.tenant,
id: tenantId
};
} else if (usersType === 'customer') {
vm.item.authority = "CUSTOMER_USER";
vm.item.customerId = {
entityType: types.entityType.customer,
id: customerId
};
}
userService.saveUser(vm.item, sendActivationMail).then(function success(item) {
vm.item = item;
$scope.theForm.$setPristine();
if (vm.userActivationMethod == 'displayActivationLink') {
userService.getActivationLink(vm.item.id.id).then(
function success(activationLink) {
displayActivationLink($event, activationLink).then(
function() {
$mdDialog.hide();
}
);
}
);
} else {
$mdDialog.hide();
}
});
}
function displayActivationLink($event, activationLink) {
var deferred = $q.defer();
$mdDialog.show({
controller: 'ActivationLinkDialogController',
controllerAs: 'vm',
templateUrl: activationLinkDialogTemplate,
locals: {
activationLink: activationLink
},
parent: angular.element($document[0].body),
fullscreen: true,
skipHide: true,
targetEvent: $event
}).then(function () {
deferred.resolve();
});
return deferred.promise;
}
}

View File

@ -15,8 +15,8 @@
limitations under the License.
-->
<md-dialog aria-label="{{ 'user.add' | translate }}" tb-help="'users'" help-container-id="help-container">
<form name="theForm" ng-submit="vm.add()">
<md-dialog style="width: 600px;" aria-label="{{ 'user.add' | translate }}" tb-help="'users'" help-container-id="help-container">
<form name="theForm" ng-submit="vm.add($event)">
<md-toolbar>
<div class="md-toolbar-tools">
<h2 translate>user.add</h2>
@ -32,6 +32,15 @@
<md-dialog-content>
<div class="md-dialog-content">
<tb-user user="vm.item" is-edit="true" the-form="theForm"></tb-user>
<md-input-container class="md-block">
<label translate>user.activation-method</label>
<md-select aria-label="{{ 'user.activation-method' | translate }}"
ng-model="vm.userActivationMethod">
<md-option ng-repeat="activationMethod in vm.activationMethods" ng-value="activationMethod.value">
{{activationMethod.name | translate}}
</md-option>
</md-select>
</md-input-container>
</div>
</md-dialog-content>
<md-dialog-actions layout="row">

View File

@ -20,6 +20,8 @@ import thingsboardToast from '../services/toast';
import UserRoutes from './user.routes';
import UserController from './user.controller';
import AddUserController from './add-user.controller';
import ActivationLinkDialogController from './activation-link.controller';
import UserDirective from './user.directive';
export default angular.module('thingsboard.user', [
@ -30,5 +32,7 @@ export default angular.module('thingsboard.user', [
])
.config(UserRoutes)
.controller('UserController', UserController)
.controller('AddUserController', AddUserController)
.controller('ActivationLinkDialogController', ActivationLinkDialogController)
.directive('tbUser', UserDirective)
.name;

View File

@ -15,6 +15,9 @@
limitations under the License.
-->
<md-button ng-click="onDisplayActivationLink({event: $event})" ng-show="!isEdit" class="md-raised md-primary">{{
'user.display-activation-link' | translate }}
</md-button>
<md-button ng-click="onResendActivation({event: $event})" ng-show="!isEdit" class="md-raised md-primary">{{
'user.resend-activation' | translate }}
</md-button>
@ -26,9 +29,12 @@
<fieldset ng-disabled="loading || !isEdit">
<md-input-container class="md-block">
<label translate>user.email</label>
<input required name="email" type="email" ng-model="user.email">
<input required name="email"
ng-pattern="/^[_a-z0-9]+(\.[_a-z0-9]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,4})$/"
ng-model="user.email">
<div ng-messages="theForm.email.$error">
<div translate ng-message="required">user.email-required</div>
<div translate ng-message="pattern">user.invalid-email-format</div>
</div>
</md-input-container>
<md-input-container class="md-block">
@ -43,7 +49,7 @@
<label translate>user.description</label>
<textarea ng-model="user.additionalInfo.description" rows="2"></textarea>
</md-input-container>
<section class="tb-default-dashboard" flex layout="column">
<section class="tb-default-dashboard" flex layout="column" ng-if="user.id">
<span class="tb-default-dashboard-label" ng-class="{'tb-disabled-label': loading || !isEdit}" translate>user.default-dashboard</span>
<section flex layout="column" layout-gt-sm="row">
<tb-dashboard-autocomplete ng-if="isTenantAdmin()"

View File

@ -17,12 +17,13 @@
import addUserTemplate from './add-user.tpl.html';
import userCard from './user-card.tpl.html';
import activationLinkDialogTemplate from './activation-link.dialog.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/
export default function UserController(userService, toast, $scope, $controller, $state, $stateParams, $translate, types) {
export default function UserController(userService, toast, $scope, $mdDialog, $document, $controller, $state, $stateParams, $translate, types) {
var tenantId = $stateParams.tenantId;
var customerId = $stateParams.customerId;
@ -58,6 +59,7 @@ export default function UserController(userService, toast, $scope, $controller,
onGridInited: gridInited,
addItemTemplateUrl: addUserTemplate,
addItemController: 'AddUserController',
addItemText: function() { return $translate.instant('user.add-user-text') },
noItemsText: function() { return $translate.instant('user.no-users-text') },
@ -72,6 +74,7 @@ export default function UserController(userService, toast, $scope, $controller,
vm.userGridConfig.topIndex = $stateParams.topIndex;
}
vm.displayActivationLink = displayActivationLink;
vm.resendActivation = resendActivation;
initController();
@ -151,6 +154,29 @@ export default function UserController(userService, toast, $scope, $controller,
return userService.deleteUser(userId);
}
function displayActivationLink(event, user) {
userService.getActivationLink(user.id.id).then(
function success(activationLink) {
openActivationLinkDialog(event, activationLink);
}
);
}
function openActivationLinkDialog(event, activationLink) {
$mdDialog.show({
controller: 'ActivationLinkDialogController',
controllerAs: 'vm',
templateUrl: activationLinkDialogTemplate,
locals: {
activationLink: activationLink
},
parent: angular.element($document[0].body),
fullscreen: true,
skipHide: true,
targetEvent: event
});
}
function resendActivation(user) {
userService.sendActivationEmail(user.email).then(function success() {
toast.showSuccess($translate.instant('user.activation-email-sent-message'));

View File

@ -45,6 +45,7 @@ export default function UserDirective($compile, $templateCache/*, dashboardServi
user: '=',
isEdit: '=',
theForm: '=',
onDisplayActivationLink: '&',
onResendActivation: '&',
onDeleteUser: '&'
}

View File

@ -22,6 +22,7 @@
<tb-user user="vm.grid.operatingItem()"
is-edit="vm.grid.detailsConfig.isDetailsEditMode"
the-form="vm.grid.detailsForm"
on-display-activation-link="vm.displayActivationLink(event, vm.grid.detailsConfig.currentItem)"
on-resend-activation="vm.resendActivation(vm.grid.detailsConfig.currentItem)"
on-delete-user="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-user>
</tb-grid>