Browse Source

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

pull/214/head
Igor Kulikov 9 years ago
parent
commit
86515896dd
  1. 7
      application/src/main/java/org/thingsboard/server/controller/AuthController.java
  2. 32
      application/src/main/java/org/thingsboard/server/controller/UserController.java
  3. 17
      ui/src/app/api/user.service.js
  4. 11
      ui/src/app/app.run.js
  5. 8
      ui/src/app/components/grid.directive.js
  6. 10
      ui/src/app/locale/locale.constant.js
  7. 35
      ui/src/app/user/activation-link.controller.js
  8. 55
      ui/src/app/user/activation-link.dialog.tpl.html
  9. 112
      ui/src/app/user/add-user.controller.js
  10. 13
      ui/src/app/user/add-user.tpl.html
  11. 4
      ui/src/app/user/index.js
  12. 10
      ui/src/app/user/user-fieldset.tpl.html
  13. 28
      ui/src/app/user/user.controller.js
  14. 1
      ui/src/app/user/user.directive.js
  15. 1
      ui/src/app/user/users.tpl.html

7
application/src/main/java/org/thingsboard/server/controller/AuthController.java

@ -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);

32
application/src/main/java/org/thingsboard/server/controller/UserController.java

@ -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)

17
ui/src/app/api/user.service.js

@ -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') {

11
ui/src/app/app.run.js

@ -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;
});
}
})

8
ui/src/app/components/grid.directive.js

@ -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),

10
ui/src/app/locale/locale.constant.js

@ -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",

35
ui/src/app/user/activation-link.controller.js

@ -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();
}
}

55
ui/src/app/user/activation-link.dialog.tpl.html

@ -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>

112
ui/src/app/user/add-user.controller.js

@ -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;
}
}

13
ui/src/app/user/add-user.tpl.html

@ -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">

4
ui/src/app/user/index.js

@ -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;

10
ui/src/app/user/user-fieldset.tpl.html

@ -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()"

28
ui/src/app/user/user.controller.js

@ -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'));

1
ui/src/app/user/user.directive.js

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

1
ui/src/app/user/users.tpl.html

@ -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>

Loading…
Cancel
Save