Commit 22210e6833db3599fa599daf95b44fe716515ce3

Authored by Adsumus
2 parents e74f6b40 cd88afea

Merge branch 'map/3.0' of https://github.com/ArtemHalushko/thingsboard into map/3.0

Showing 56 changed files with 1192 additions and 96 deletions
@@ -133,6 +133,18 @@ @@ -133,6 +133,18 @@
133 <artifactId>spring-boot-starter-websocket</artifactId> 133 <artifactId>spring-boot-starter-websocket</artifactId>
134 </dependency> 134 </dependency>
135 <dependency> 135 <dependency>
  136 + <groupId>org.springframework.cloud</groupId>
  137 + <artifactId>spring-cloud-starter-oauth2</artifactId>
  138 + </dependency>
  139 + <dependency>
  140 + <groupId>org.springframework.security</groupId>
  141 + <artifactId>spring-security-oauth2-client</artifactId>
  142 + </dependency>
  143 + <dependency>
  144 + <groupId>org.springframework.security</groupId>
  145 + <artifactId>spring-security-oauth2-jose</artifactId>
  146 + </dependency>
  147 + <dependency>
136 <groupId>io.jsonwebtoken</groupId> 148 <groupId>io.jsonwebtoken</groupId>
137 <artifactId>jjwt</artifactId> 149 <artifactId>jjwt</artifactId>
138 </dependency> 150 </dependency>
@@ -39,6 +39,7 @@ import org.springframework.security.web.util.matcher.AntPathRequestMatcher; @@ -39,6 +39,7 @@ import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
39 import org.springframework.web.cors.UrlBasedCorsConfigurationSource; 39 import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
40 import org.springframework.web.filter.CorsFilter; 40 import org.springframework.web.filter.CorsFilter;
41 import org.thingsboard.server.dao.audit.AuditLogLevelFilter; 41 import org.thingsboard.server.dao.audit.AuditLogLevelFilter;
  42 +import org.thingsboard.server.dao.oauth2.OAuth2Configuration;
42 import org.thingsboard.server.exception.ThingsboardErrorResponseHandler; 43 import org.thingsboard.server.exception.ThingsboardErrorResponseHandler;
43 import org.thingsboard.server.service.security.auth.jwt.JwtAuthenticationProvider; 44 import org.thingsboard.server.service.security.auth.jwt.JwtAuthenticationProvider;
44 import org.thingsboard.server.service.security.auth.jwt.JwtTokenAuthenticationProcessingFilter; 45 import org.thingsboard.server.service.security.auth.jwt.JwtTokenAuthenticationProcessingFilter;
@@ -73,12 +74,22 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt @@ -73,12 +74,22 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt
73 public static final String WS_TOKEN_BASED_AUTH_ENTRY_POINT = "/api/ws/**"; 74 public static final String WS_TOKEN_BASED_AUTH_ENTRY_POINT = "/api/ws/**";
74 75
75 @Autowired private ThingsboardErrorResponseHandler restAccessDeniedHandler; 76 @Autowired private ThingsboardErrorResponseHandler restAccessDeniedHandler;
76 - @Autowired private AuthenticationSuccessHandler successHandler; 77 +
  78 + @Autowired(required = false)
  79 + @Qualifier("oauth2AuthenticationSuccessHandler")
  80 + private AuthenticationSuccessHandler oauth2AuthenticationSuccessHandler;
  81 +
  82 + @Autowired
  83 + @Qualifier("defaultAuthenticationSuccessHandler")
  84 + private AuthenticationSuccessHandler successHandler;
  85 +
77 @Autowired private AuthenticationFailureHandler failureHandler; 86 @Autowired private AuthenticationFailureHandler failureHandler;
78 @Autowired private RestAuthenticationProvider restAuthenticationProvider; 87 @Autowired private RestAuthenticationProvider restAuthenticationProvider;
79 @Autowired private JwtAuthenticationProvider jwtAuthenticationProvider; 88 @Autowired private JwtAuthenticationProvider jwtAuthenticationProvider;
80 @Autowired private RefreshTokenAuthenticationProvider refreshTokenAuthenticationProvider; 89 @Autowired private RefreshTokenAuthenticationProvider refreshTokenAuthenticationProvider;
81 90
  91 + @Autowired(required = false) OAuth2Configuration oauth2Configuration;
  92 +
82 @Autowired 93 @Autowired
83 @Qualifier("jwtHeaderTokenExtractor") 94 @Qualifier("jwtHeaderTokenExtractor")
84 private TokenExtractor jwtHeaderTokenExtractor; 95 private TokenExtractor jwtHeaderTokenExtractor;
@@ -189,6 +200,12 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt @@ -189,6 +200,12 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt
189 .addFilterBefore(buildRefreshTokenProcessingFilter(), UsernamePasswordAuthenticationFilter.class) 200 .addFilterBefore(buildRefreshTokenProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
190 .addFilterBefore(buildWsJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class) 201 .addFilterBefore(buildWsJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
191 .addFilterAfter(rateLimitProcessingFilter, UsernamePasswordAuthenticationFilter.class); 202 .addFilterAfter(rateLimitProcessingFilter, UsernamePasswordAuthenticationFilter.class);
  203 + if (oauth2Configuration != null && oauth2Configuration.isEnabled()) {
  204 + http.oauth2Login()
  205 + .loginPage("/oauth2Login")
  206 + .loginProcessingUrl(oauth2Configuration.getLoginProcessingUrl())
  207 + .successHandler(oauth2AuthenticationSuccessHandler);
  208 + }
192 } 209 }
193 210
194 211
@@ -38,8 +38,10 @@ import org.thingsboard.server.common.data.audit.ActionType; @@ -38,8 +38,10 @@ import org.thingsboard.server.common.data.audit.ActionType;
38 import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; 38 import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
39 import org.thingsboard.server.common.data.exception.ThingsboardException; 39 import org.thingsboard.server.common.data.exception.ThingsboardException;
40 import org.thingsboard.server.common.data.id.TenantId; 40 import org.thingsboard.server.common.data.id.TenantId;
  41 +import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo;
41 import org.thingsboard.server.common.data.security.UserCredentials; 42 import org.thingsboard.server.common.data.security.UserCredentials;
42 import org.thingsboard.server.dao.audit.AuditLogService; 43 import org.thingsboard.server.dao.audit.AuditLogService;
  44 +import org.thingsboard.server.dao.oauth2.OAuth2Service;
43 import org.thingsboard.server.queue.util.TbCoreComponent; 45 import org.thingsboard.server.queue.util.TbCoreComponent;
44 import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository; 46 import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository;
45 import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails; 47 import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails;
@@ -55,6 +57,7 @@ import ua_parser.Client; @@ -55,6 +57,7 @@ import ua_parser.Client;
55 import javax.servlet.http.HttpServletRequest; 57 import javax.servlet.http.HttpServletRequest;
56 import java.net.URI; 58 import java.net.URI;
57 import java.net.URISyntaxException; 59 import java.net.URISyntaxException;
  60 +import java.util.List;
58 61
59 @RestController 62 @RestController
60 @TbCoreComponent 63 @TbCoreComponent
@@ -80,6 +83,9 @@ public class AuthController extends BaseController { @@ -80,6 +83,9 @@ public class AuthController extends BaseController {
80 @Autowired 83 @Autowired
81 private AuditLogService auditLogService; 84 private AuditLogService auditLogService;
82 85
  86 + @Autowired
  87 + private OAuth2Service oauth2Service;
  88 +
83 @PreAuthorize("isAuthenticated()") 89 @PreAuthorize("isAuthenticated()")
84 @RequestMapping(value = "/auth/user", method = RequestMethod.GET) 90 @RequestMapping(value = "/auth/user", method = RequestMethod.GET)
85 public @ResponseBody User getUser() throws ThingsboardException { 91 public @ResponseBody User getUser() throws ThingsboardException {
@@ -330,4 +336,13 @@ public class AuthController extends BaseController { @@ -330,4 +336,13 @@ public class AuthController extends BaseController {
330 } 336 }
331 } 337 }
332 338
  339 + @RequestMapping(value = "/noauth/oauth2Clients", method = RequestMethod.POST)
  340 + @ResponseBody
  341 + public List<OAuth2ClientInfo> getOath2Clients() throws ThingsboardException {
  342 + try {
  343 + return oauth2Service.getOAuth2Clients();
  344 + } catch (Exception e) {
  345 + throw handleException(e);
  346 + }
  347 + }
333 } 348 }
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.service.security.auth.oauth2;
  17 +
  18 +import lombok.extern.slf4j.Slf4j;
  19 +import org.springframework.beans.factory.annotation.Autowired;
  20 +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
  21 +import org.springframework.security.core.userdetails.UsernameNotFoundException;
  22 +import org.springframework.util.StringUtils;
  23 +import org.thingsboard.server.common.data.Customer;
  24 +import org.thingsboard.server.common.data.Tenant;
  25 +import org.thingsboard.server.common.data.User;
  26 +import org.thingsboard.server.common.data.id.CustomerId;
  27 +import org.thingsboard.server.common.data.id.TenantId;
  28 +import org.thingsboard.server.common.data.page.PageLink;
  29 +import org.thingsboard.server.common.data.security.Authority;
  30 +import org.thingsboard.server.dao.customer.CustomerService;
  31 +import org.thingsboard.server.dao.oauth2.OAuth2User;
  32 +import org.thingsboard.server.dao.tenant.TenantService;
  33 +import org.thingsboard.server.dao.user.UserService;
  34 +import org.thingsboard.server.service.security.model.SecurityUser;
  35 +import org.thingsboard.server.service.security.model.UserPrincipal;
  36 +
  37 +import java.util.List;
  38 +import java.util.Optional;
  39 +import java.util.concurrent.locks.Lock;
  40 +import java.util.concurrent.locks.ReentrantLock;
  41 +
  42 +@Slf4j
  43 +public abstract class AbstractOAuth2ClientMapper {
  44 +
  45 + @Autowired
  46 + private UserService userService;
  47 +
  48 + @Autowired
  49 + private TenantService tenantService;
  50 +
  51 + @Autowired
  52 + private CustomerService customerService;
  53 +
  54 + private final Lock userCreationLock = new ReentrantLock();
  55 +
  56 + protected SecurityUser getOrCreateSecurityUserFromOAuth2User(OAuth2User oauth2User, boolean allowUserCreation) {
  57 + UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, oauth2User.getEmail());
  58 +
  59 + User user = userService.findUserByEmail(TenantId.SYS_TENANT_ID, oauth2User.getEmail());
  60 +
  61 + if (user == null && !allowUserCreation) {
  62 + throw new UsernameNotFoundException("User not found: " + oauth2User.getEmail());
  63 + }
  64 +
  65 + if (user == null) {
  66 + userCreationLock.lock();
  67 + try {
  68 + user = userService.findUserByEmail(TenantId.SYS_TENANT_ID, oauth2User.getEmail());
  69 + if (user == null) {
  70 + user = new User();
  71 + if (oauth2User.getCustomerId() == null && StringUtils.isEmpty(oauth2User.getCustomerName())) {
  72 + user.setAuthority(Authority.TENANT_ADMIN);
  73 + } else {
  74 + user.setAuthority(Authority.CUSTOMER_USER);
  75 + }
  76 + TenantId tenantId = oauth2User.getTenantId() != null ?
  77 + oauth2User.getTenantId() : getTenantId(oauth2User.getTenantName());
  78 + user.setTenantId(tenantId);
  79 + CustomerId customerId = oauth2User.getCustomerId() != null ?
  80 + oauth2User.getCustomerId() : getCustomerId(user.getTenantId(), oauth2User.getCustomerName());
  81 + user.setCustomerId(customerId);
  82 + user.setEmail(oauth2User.getEmail());
  83 + user.setFirstName(oauth2User.getFirstName());
  84 + user.setLastName(oauth2User.getLastName());
  85 + user = userService.saveUser(user);
  86 + }
  87 + } finally {
  88 + userCreationLock.unlock();
  89 + }
  90 + }
  91 +
  92 + try {
  93 + SecurityUser securityUser = new SecurityUser(user, true, principal);
  94 + return (SecurityUser) new UsernamePasswordAuthenticationToken(securityUser, null, securityUser.getAuthorities()).getPrincipal();
  95 + } catch (Exception e) {
  96 + log.error("Can't get or create security user from oauth2 user", e);
  97 + throw new RuntimeException("Can't get or create security user from oauth2 user", e);
  98 + }
  99 + }
  100 +
  101 + private TenantId getTenantId(String tenantName) {
  102 + List<Tenant> tenants = tenantService.findTenants(new PageLink(1, 0, tenantName)).getData();
  103 + Tenant tenant;
  104 + if (tenants == null || tenants.isEmpty()) {
  105 + tenant = new Tenant();
  106 + tenant.setTitle(tenantName);
  107 + tenant = tenantService.saveTenant(tenant);
  108 + } else {
  109 + tenant = tenants.get(0);
  110 + }
  111 + return tenant.getTenantId();
  112 + }
  113 +
  114 + private CustomerId getCustomerId(TenantId tenantId, String customerName) {
  115 + if (StringUtils.isEmpty(customerName)) {
  116 + return null;
  117 + }
  118 + Optional<Customer> customerOpt = customerService.findCustomerByTenantIdAndTitle(tenantId, customerName);
  119 + if (customerOpt.isPresent()) {
  120 + return customerOpt.get().getId();
  121 + } else {
  122 + Customer customer = new Customer();
  123 + customer.setTenantId(tenantId);
  124 + customer.setTitle(customerName);
  125 + return customerService.saveCustomer(customer).getId();
  126 + }
  127 + }
  128 +}
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.service.security.auth.oauth2;
  17 +
  18 +import lombok.extern.slf4j.Slf4j;
  19 +import org.apache.commons.lang3.text.StrSubstitutor;
  20 +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
  21 +import org.springframework.stereotype.Service;
  22 +import org.springframework.util.StringUtils;
  23 +import org.thingsboard.server.dao.oauth2.OAuth2ClientMapperConfig;
  24 +import org.thingsboard.server.dao.oauth2.OAuth2User;
  25 +import org.thingsboard.server.service.security.model.SecurityUser;
  26 +
  27 +import java.util.Map;
  28 +
  29 +@Service(value = "basicOAuth2ClientMapper")
  30 +@Slf4j
  31 +public class BasicOAuth2ClientMapper extends AbstractOAuth2ClientMapper implements OAuth2ClientMapper {
  32 +
  33 + private static final String START_PLACEHOLDER_PREFIX = "%{";
  34 + private static final String END_PLACEHOLDER_PREFIX = "}";
  35 + private static final String EMAIL_TENANT_STRATEGY = "email";
  36 + private static final String DOMAIN_TENANT_STRATEGY = "domain";
  37 + private static final String CUSTOM_TENANT_STRATEGY = "custom";
  38 +
  39 + @Override
  40 + public SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, OAuth2ClientMapperConfig config) {
  41 + OAuth2User oauth2User = new OAuth2User();
  42 + Map<String, Object> attributes = token.getPrincipal().getAttributes();
  43 + String email = getStringAttributeByKey(attributes, config.getBasic().getEmailAttributeKey());
  44 + oauth2User.setEmail(email);
  45 + oauth2User.setTenantName(getTenantName(attributes, config));
  46 + if (!StringUtils.isEmpty(config.getBasic().getLastNameAttributeKey())) {
  47 + String lastName = getStringAttributeByKey(attributes, config.getBasic().getLastNameAttributeKey());
  48 + oauth2User.setLastName(lastName);
  49 + }
  50 + if (!StringUtils.isEmpty(config.getBasic().getFirstNameAttributeKey())) {
  51 + String firstName = getStringAttributeByKey(attributes, config.getBasic().getFirstNameAttributeKey());
  52 + oauth2User.setFirstName(firstName);
  53 + }
  54 + if (!StringUtils.isEmpty(config.getBasic().getCustomerNameStrategyPattern())) {
  55 + StrSubstitutor sub = new StrSubstitutor(attributes, START_PLACEHOLDER_PREFIX, END_PLACEHOLDER_PREFIX);
  56 + String customerName = sub.replace(config.getBasic().getCustomerNameStrategyPattern());
  57 + oauth2User.setCustomerName(customerName);
  58 + }
  59 + return getOrCreateSecurityUserFromOAuth2User(oauth2User, config.getBasic().isAllowUserCreation());
  60 + }
  61 +
  62 + private String getTenantName(Map<String, Object> attributes, OAuth2ClientMapperConfig config) {
  63 + switch (config.getBasic().getTenantNameStrategy()) {
  64 + case EMAIL_TENANT_STRATEGY:
  65 + return getStringAttributeByKey(attributes, config.getBasic().getEmailAttributeKey());
  66 + case DOMAIN_TENANT_STRATEGY:
  67 + String email = getStringAttributeByKey(attributes, config.getBasic().getEmailAttributeKey());
  68 + return email.substring(email .indexOf("@") + 1);
  69 + case CUSTOM_TENANT_STRATEGY:
  70 + StrSubstitutor sub = new StrSubstitutor(attributes, START_PLACEHOLDER_PREFIX, END_PLACEHOLDER_PREFIX);
  71 + return sub.replace(config.getBasic().getTenantNameStrategyPattern());
  72 + default:
  73 + throw new RuntimeException("Tenant Name Strategy with type " + config.getBasic().getTenantNameStrategy() + " is not supported!");
  74 + }
  75 + }
  76 +
  77 + private String getStringAttributeByKey(Map<String, Object> attributes, String key) {
  78 + String result = null;
  79 + try {
  80 + result = (String) attributes.get(key);
  81 +
  82 + } catch (Exception e) {
  83 + log.warn("Can't convert attribute to String by key " + key);
  84 + }
  85 + return result;
  86 + }
  87 +}
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.service.security.auth.oauth2;
  17 +
  18 +import com.fasterxml.jackson.core.JsonProcessingException;
  19 +import com.fasterxml.jackson.databind.ObjectMapper;
  20 +import lombok.extern.slf4j.Slf4j;
  21 +import org.springframework.boot.web.client.RestTemplateBuilder;
  22 +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
  23 +import org.springframework.stereotype.Service;
  24 +import org.springframework.util.StringUtils;
  25 +import org.springframework.web.client.RestTemplate;
  26 +import org.thingsboard.server.dao.oauth2.OAuth2ClientMapperConfig;
  27 +import org.thingsboard.server.dao.oauth2.OAuth2User;
  28 +import org.thingsboard.server.service.security.model.SecurityUser;
  29 +
  30 +@Service(value = "customOAuth2ClientMapper")
  31 +@Slf4j
  32 +public class CustomOAuth2ClientMapper extends AbstractOAuth2ClientMapper implements OAuth2ClientMapper {
  33 +
  34 + private static final ObjectMapper json = new ObjectMapper();
  35 +
  36 + private RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder();
  37 +
  38 + @Override
  39 + public SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, OAuth2ClientMapperConfig config) {
  40 + OAuth2User oauth2User = getOAuth2User(token, config.getCustom());
  41 + return getOrCreateSecurityUserFromOAuth2User(oauth2User, config.getBasic().isAllowUserCreation());
  42 + }
  43 +
  44 + public OAuth2User getOAuth2User(OAuth2AuthenticationToken token, OAuth2ClientMapperConfig.CustomOAuth2ClientMapperConfig custom) {
  45 + if (!StringUtils.isEmpty(custom.getUsername()) && !StringUtils.isEmpty(custom.getPassword())) {
  46 + restTemplateBuilder = restTemplateBuilder.basicAuthentication(custom.getUsername(), custom.getPassword());
  47 + }
  48 + RestTemplate restTemplate = restTemplateBuilder.build();
  49 + String request;
  50 + try {
  51 + request = json.writeValueAsString(token.getPrincipal());
  52 + } catch (JsonProcessingException e) {
  53 + log.error("Can't convert principal to JSON string", e);
  54 + throw new RuntimeException("Can't convert principal to JSON string", e);
  55 + }
  56 + try {
  57 + return restTemplate.postForEntity(custom.getUrl(), request, OAuth2User.class).getBody();
  58 + } catch (Exception e) {
  59 + log.error("Can't connect to custom mapper endpoint", e);
  60 + throw new RuntimeException("Can't connect to custom mapper endpoint", e);
  61 + }
  62 + }
  63 +}
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.service.security.auth.oauth2;
  17 +
  18 +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
  19 +import org.thingsboard.server.dao.oauth2.OAuth2ClientMapperConfig;
  20 +import org.thingsboard.server.service.security.model.SecurityUser;
  21 +
  22 +public interface OAuth2ClientMapper {
  23 + SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, OAuth2ClientMapperConfig config);
  24 +}
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.service.security.auth.oauth2;
  17 +
  18 +import lombok.extern.slf4j.Slf4j;
  19 +import org.springframework.beans.factory.annotation.Autowired;
  20 +import org.springframework.beans.factory.annotation.Qualifier;
  21 +import org.springframework.stereotype.Component;
  22 +
  23 +@Component
  24 +@Slf4j
  25 +public class OAuth2ClientMapperProvider {
  26 +
  27 + @Autowired
  28 + @Qualifier("basicOAuth2ClientMapper")
  29 + private OAuth2ClientMapper basicOAuth2ClientMapper;
  30 +
  31 + @Autowired
  32 + @Qualifier("customOAuth2ClientMapper")
  33 + private OAuth2ClientMapper customOAuth2ClientMapper;
  34 +
  35 + public OAuth2ClientMapper getOAuth2ClientMapperByType(String oauth2ClientType) {
  36 + switch (oauth2ClientType) {
  37 + case "custom":
  38 + return customOAuth2ClientMapper;
  39 + case "basic":
  40 + return basicOAuth2ClientMapper;
  41 + default:
  42 + throw new RuntimeException("OAuth2ClientMapper with type " + oauth2ClientType + " is not supported!");
  43 + }
  44 + }
  45 +}
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.service.security.auth.oauth2;
  17 +
  18 +import org.springframework.beans.factory.annotation.Autowired;
  19 +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
  20 +import org.springframework.security.core.Authentication;
  21 +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
  22 +import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
  23 +import org.springframework.stereotype.Component;
  24 +import org.thingsboard.server.dao.oauth2.OAuth2Client;
  25 +import org.thingsboard.server.dao.oauth2.OAuth2Configuration;
  26 +import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository;
  27 +import org.thingsboard.server.service.security.model.SecurityUser;
  28 +import org.thingsboard.server.service.security.model.token.JwtToken;
  29 +import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
  30 +
  31 +import javax.servlet.http.HttpServletRequest;
  32 +import javax.servlet.http.HttpServletResponse;
  33 +import java.io.IOException;
  34 +
  35 +@Component(value = "oauth2AuthenticationSuccessHandler")
  36 +@ConditionalOnProperty(prefix = "security.oauth2", value = "enabled", havingValue = "true")
  37 +public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
  38 +
  39 + private final JwtTokenFactory tokenFactory;
  40 + private final RefreshTokenRepository refreshTokenRepository;
  41 + private final OAuth2ClientMapperProvider oauth2ClientMapperProvider;
  42 + private final OAuth2Configuration oauth2Configuration;
  43 +
  44 + @Autowired
  45 + public Oauth2AuthenticationSuccessHandler(final JwtTokenFactory tokenFactory,
  46 + final RefreshTokenRepository refreshTokenRepository,
  47 + final OAuth2ClientMapperProvider oauth2ClientMapperProvider,
  48 + final OAuth2Configuration oauth2Configuration) {
  49 + this.tokenFactory = tokenFactory;
  50 + this.refreshTokenRepository = refreshTokenRepository;
  51 + this.oauth2ClientMapperProvider = oauth2ClientMapperProvider;
  52 + this.oauth2Configuration = oauth2Configuration;
  53 + }
  54 +
  55 + @Override
  56 + public void onAuthenticationSuccess(HttpServletRequest request,
  57 + HttpServletResponse response,
  58 + Authentication authentication) throws IOException {
  59 + OAuth2AuthenticationToken token = (OAuth2AuthenticationToken) authentication;
  60 +
  61 + OAuth2Client oauth2Client = oauth2Configuration.getClientByRegistrationId(token.getAuthorizedClientRegistrationId());
  62 + OAuth2ClientMapper mapper = oauth2ClientMapperProvider.getOAuth2ClientMapperByType(oauth2Client.getMapperConfig().getType());
  63 + SecurityUser securityUser = mapper.getOrCreateUserByClientPrincipal(token, oauth2Client.getMapperConfig());
  64 +
  65 + JwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser);
  66 + JwtToken refreshToken = refreshTokenRepository.requestRefreshToken(securityUser);
  67 +
  68 + getRedirectStrategy().sendRedirect(request, response, "/?accessToken=" + accessToken.getToken() + "&refreshToken=" + refreshToken.getToken());
  69 + }
  70 +}
@@ -36,7 +36,7 @@ import java.io.IOException; @@ -36,7 +36,7 @@ import java.io.IOException;
36 import java.util.HashMap; 36 import java.util.HashMap;
37 import java.util.Map; 37 import java.util.Map;
38 38
39 -@Component 39 +@Component(value = "defaultAuthenticationSuccessHandler")
40 public class RestAwareAuthenticationSuccessHandler implements AuthenticationSuccessHandler { 40 public class RestAwareAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
41 private final ObjectMapper mapper; 41 private final ObjectMapper mapper;
42 private final JwtTokenFactory tokenFactory; 42 private final JwtTokenFactory tokenFactory;
@@ -97,6 +97,41 @@ security: @@ -97,6 +97,41 @@ security:
97 allowClaimingByDefault: "${SECURITY_CLAIM_ALLOW_CLAIMING_BY_DEFAULT:true}" 97 allowClaimingByDefault: "${SECURITY_CLAIM_ALLOW_CLAIMING_BY_DEFAULT:true}"
98 # Time allowed to claim the device in milliseconds 98 # Time allowed to claim the device in milliseconds
99 duration: "${SECURITY_CLAIM_DURATION:60000}" # 1 minute, note this value must equal claimDevices.timeToLiveInMinutes value 99 duration: "${SECURITY_CLAIM_DURATION:60000}" # 1 minute, note this value must equal claimDevices.timeToLiveInMinutes value
  100 + basic:
  101 + enabled: "${SECURITY_BASIC_ENABLED:false}"
  102 + oauth2:
  103 + enabled: "${SECURITY_OAUTH2_ENABLED:false}"
  104 + loginProcessingUrl: "${SECURITY_OAUTH2_LOGIN_PROCESSING_URL:/login/oauth2/code/}"
  105 + clients:
  106 + default:
  107 + loginButtonLabel: "${SECURITY_OAUTH2_DEFAULT_LOGIN_BUTTON_LABEL:Default}" # Label that going to be show on login screen
  108 + loginButtonIcon: "${SECURITY_OAUTH2_DEFAULT_LOGIN_BUTTON_ICON:}" # Icon that going to be show on login screen. Material design icon ID (https://material.angularjs.org/latest/api/directive/mdIcon)
  109 + clientName: "${SECURITY_OAUTH2_DEFAULT_CLIENT_NAME:ClientName}"
  110 + clientId: "${SECURITY_OAUTH2_DEFAULT_CLIENT_ID:}"
  111 + clientSecret: "${SECURITY_OAUTH2_DEFAULT_CLIENT_SECRET:}"
  112 + accessTokenUri: "${SECURITY_OAUTH2_DEFAULT_ACCESS_TOKEN_URI:}"
  113 + authorizationUri: "${SECURITY_OAUTH2_DEFAULT_AUTHORIZATION_URI:}"
  114 + scope: "${SECURITY_OAUTH2_DEFAULT_SCOPE:}"
  115 + redirectUriTemplate: "${SECURITY_OAUTH2_DEFAULT_REDIRECT_URI_TEMPLATE:http://localhost:8080/login/oauth2/code/}" # Must be in sync with security.oauth2.loginProcessingUrl
  116 + jwkSetUri: "${SECURITY_OAUTH2_DEFAULT_JWK_SET_URI:}"
  117 + authorizationGrantType: "${SECURITY_OAUTH2_DEFAULT_AUTHORIZATION_GRANT_TYPE:authorization_code}" # authorization_code, implicit, refresh_token or client_credentials
  118 + clientAuthenticationMethod: "${SECURITY_OAUTH2_DEFAULT_CLIENT_AUTHENTICATION_METHOD:post}" # basic or post
  119 + userInfoUri: "${SECURITY_OAUTH2_DEFAULT_USER_INFO_URI:}"
  120 + userNameAttributeName: "${SECURITY_OAUTH2_DEFAULT_USER_NAME_ATTRIBUTE_NAME:email}"
  121 + mapperConfig:
  122 + type: "${SECURITY_OAUTH2_DEFAULT_MAPPER_TYPE:basic}" # basic or custom
  123 + basic:
  124 + allowUserCreation: "${SECURITY_OAUTH2_DEFAULT_MAPPER_BASIC_ALLOW_USER_CREATION:true}" # Allows to create user if it not exists
  125 + emailAttributeKey: "${SECURITY_OAUTH2_DEFAULT_MAPPER_BASIC_EMAIL_ATTRIBUTE_KEY:email}" # Attribute key to use as email for the user
  126 + firstNameAttributeKey: "${SECURITY_OAUTH2_DEFAULT_MAPPER_BASIC_FIRST_NAME_ATTRIBUTE_KEY:}"
  127 + lastNameAttributeKey: "${SECURITY_OAUTH2_DEFAULT_MAPPER_BASIC_LAST_NAME_ATTRIBUTE_KEY:}"
  128 + tenantNameStrategy: "${SECURITY_OAUTH2_DEFAULT_MAPPER_BASIC_TENANT_NAME_STRATEGY:domain}" # domain, email or custom
  129 + tenantNameStrategyPattern: "${SECURITY_OAUTH2_DEFAULT_MAPPER_BASIC_TENANT_NAME_STRATEGY_PATTERN:}"
  130 + customerNameStrategyPattern: "${SECURITY_OAUTH2_DEFAULT_MAPPER_BASIC_CUSTOMER_NAME_STRATEGY_PATTERN:}"
  131 + custom:
  132 + url: "${SECURITY_OAUTH2_DEFAULT_MAPPER_CUSTOM_URL:}"
  133 + username: "${SECURITY_OAUTH2_DEFAULT_MAPPER_CUSTOM_USERNAME:}"
  134 + password: "${SECURITY_OAUTH2_DEFAULT_MAPPER_CUSTOM_PASSWORD:}"
100 135
101 # Dashboard parameters 136 # Dashboard parameters
102 dashboard: 137 dashboard:
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.dao.oauth2;
  17 +
  18 +import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo;
  19 +
  20 +import java.util.List;
  21 +
  22 +public interface OAuth2Service {
  23 +
  24 + List<OAuth2ClientInfo> getOAuth2Clients();
  25 +}
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.dao.oauth2;
  17 +
  18 +import lombok.Data;
  19 +import org.thingsboard.server.common.data.id.CustomerId;
  20 +import org.thingsboard.server.common.data.id.TenantId;
  21 +
  22 +@Data
  23 +public class OAuth2User {
  24 + private String tenantName;
  25 + private TenantId tenantId;
  26 + private String customerName;
  27 + private CustomerId customerId;
  28 + private String email;
  29 + private String firstName;
  30 + private String lastName;
  31 +}
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.common.data.id;
  17 +
  18 +import com.fasterxml.jackson.annotation.JsonCreator;
  19 +import com.fasterxml.jackson.annotation.JsonProperty;
  20 +
  21 +import java.util.UUID;
  22 +
  23 +public class OAuth2IntegrationId extends UUIDBased {
  24 +
  25 + private static final long serialVersionUID = 1L;
  26 +
  27 + @JsonCreator
  28 + public OAuth2IntegrationId(@JsonProperty("id") UUID id) {
  29 + super(id);
  30 + }
  31 +
  32 + public static OAuth2IntegrationId fromString(String oauth2IntegrationId) {
  33 + return new OAuth2IntegrationId(UUID.fromString(oauth2IntegrationId));
  34 + }
  35 +}
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.common.data.oauth2;
  17 +
  18 +import lombok.Data;
  19 +import lombok.EqualsAndHashCode;
  20 +import org.thingsboard.server.common.data.BaseData;
  21 +import org.thingsboard.server.common.data.id.OAuth2IntegrationId;
  22 +
  23 +@EqualsAndHashCode(callSuper = true)
  24 +@Data
  25 +public class OAuth2ClientInfo extends BaseData<OAuth2IntegrationId> {
  26 +
  27 + private String name;
  28 + private String icon;
  29 + private String url;
  30 +
  31 + public OAuth2ClientInfo() {
  32 + super();
  33 + }
  34 +
  35 + public OAuth2ClientInfo(OAuth2IntegrationId id) {
  36 + super(id);
  37 + }
  38 +
  39 + public OAuth2ClientInfo(OAuth2ClientInfo oauth2ClientInfo) {
  40 + super(oauth2ClientInfo);
  41 + this.name = oauth2ClientInfo.getName();
  42 + this.icon = oauth2ClientInfo.getIcon();
  43 + this.url = oauth2ClientInfo.getUrl();
  44 + }
  45 +}
@@ -116,6 +116,10 @@ @@ -116,6 +116,10 @@
116 <artifactId>spring-web</artifactId> 116 <artifactId>spring-web</artifactId>
117 <scope>provided</scope> 117 <scope>provided</scope>
118 </dependency> 118 </dependency>
  119 + <dependency>
  120 + <groupId>org.springframework.security</groupId>
  121 + <artifactId>spring-security-oauth2-client</artifactId>
  122 + </dependency>
119 <dependency> 123 <dependency>
120 <groupId>com.datastax.cassandra</groupId> 124 <groupId>com.datastax.cassandra</groupId>
121 <artifactId>cassandra-driver-core</artifactId> 125 <artifactId>cassandra-driver-core</artifactId>
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.dao.oauth2;
  17 +
  18 +import lombok.Data;
  19 +
  20 +@Data
  21 +public class OAuth2Client {
  22 +
  23 + private String loginButtonLabel;
  24 + private String loginButtonIcon;
  25 + private String clientName;
  26 + private String clientId;
  27 + private String clientSecret;
  28 + private String accessTokenUri;
  29 + private String authorizationUri;
  30 + private String scope;
  31 + private String redirectUriTemplate;
  32 + private String jwkSetUri;
  33 + private String authorizationGrantType;
  34 + private String clientAuthenticationMethod;
  35 + private String userInfoUri;
  36 + private String userNameAttributeName;
  37 + private OAuth2ClientMapperConfig mapperConfig;
  38 +
  39 +}
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.dao.oauth2;
  17 +
  18 +import lombok.Data;
  19 +
  20 +@Data
  21 +public class OAuth2ClientMapperConfig {
  22 +
  23 + private String type;
  24 + private BasicOAuth2ClientMapperConfig basic;
  25 + private CustomOAuth2ClientMapperConfig custom;
  26 +
  27 + @Data
  28 + public static class BasicOAuth2ClientMapperConfig {
  29 + private boolean allowUserCreation;
  30 + private String emailAttributeKey;
  31 + private String firstNameAttributeKey;
  32 + private String lastNameAttributeKey;
  33 + private String tenantNameStrategy;
  34 + private String tenantNameStrategyPattern;
  35 + private String customerNameStrategyPattern;
  36 + }
  37 +
  38 + @Data
  39 + public static class CustomOAuth2ClientMapperConfig {
  40 + private String url;
  41 + private String username;
  42 + private String password;
  43 + }
  44 +}
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.dao.oauth2;
  17 +
  18 +import lombok.Data;
  19 +import lombok.extern.slf4j.Slf4j;
  20 +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
  21 +import org.springframework.boot.context.properties.ConfigurationProperties;
  22 +import org.springframework.context.annotation.Bean;
  23 +import org.springframework.context.annotation.Configuration;
  24 +import org.springframework.security.oauth2.client.registration.ClientRegistration;
  25 +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
  26 +import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
  27 +import org.springframework.security.oauth2.core.AuthorizationGrantType;
  28 +import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
  29 +
  30 +import java.util.ArrayList;
  31 +import java.util.HashMap;
  32 +import java.util.List;
  33 +import java.util.Map;
  34 +
  35 +@Configuration
  36 +@ConditionalOnProperty(prefix = "security.oauth2", value = "enabled", havingValue = "true")
  37 +@ConfigurationProperties(prefix = "security.oauth2")
  38 +@Data
  39 +@Slf4j
  40 +public class OAuth2Configuration {
  41 +
  42 + private boolean enabled;
  43 + private String loginProcessingUrl;
  44 + private Map<String, OAuth2Client> clients = new HashMap<>();
  45 +
  46 + @Bean
  47 + public ClientRegistrationRepository clientRegistrationRepository() {
  48 + List<ClientRegistration> result = new ArrayList<>();
  49 + for (Map.Entry<String, OAuth2Client> entry : clients.entrySet()) {
  50 + OAuth2Client client = entry.getValue();
  51 + ClientRegistration registration = ClientRegistration.withRegistrationId(entry.getKey())
  52 + .clientId(client.getClientId())
  53 + .authorizationUri(client.getAuthorizationUri())
  54 + .clientSecret(client.getClientSecret())
  55 + .tokenUri(client.getAccessTokenUri())
  56 + .redirectUriTemplate(client.getRedirectUriTemplate())
  57 + .scope(client.getScope().split(","))
  58 + .clientName(client.getClientName())
  59 + .authorizationGrantType(new AuthorizationGrantType(client.getAuthorizationGrantType()))
  60 + .userInfoUri(client.getUserInfoUri())
  61 + .userNameAttributeName(client.getUserNameAttributeName())
  62 + .jwkSetUri(client.getJwkSetUri())
  63 + .clientAuthenticationMethod(new ClientAuthenticationMethod(client.getClientAuthenticationMethod()))
  64 + .build();
  65 + result.add(registration);
  66 + }
  67 + return new InMemoryClientRegistrationRepository(result);
  68 + }
  69 +
  70 + public OAuth2Client getClientByRegistrationId(String registrationId) {
  71 + OAuth2Client result = null;
  72 + if (clients != null && !clients.isEmpty()) {
  73 + for (String key : clients.keySet()) {
  74 + if (key.equals(registrationId)) {
  75 + result = clients.get(key);
  76 + break;
  77 + }
  78 + }
  79 + }
  80 + return result;
  81 + }
  82 +}
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.dao.oauth2;
  17 +
  18 +import lombok.extern.slf4j.Slf4j;
  19 +import org.springframework.beans.factory.annotation.Autowired;
  20 +import org.springframework.stereotype.Service;
  21 +import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo;
  22 +
  23 +import java.util.ArrayList;
  24 +import java.util.Collections;
  25 +import java.util.List;
  26 +import java.util.Map;
  27 +
  28 +@Slf4j
  29 +@Service
  30 +public class OAuth2ServiceImpl implements OAuth2Service {
  31 +
  32 + @Autowired(required = false)
  33 + OAuth2Configuration oauth2Configuration;
  34 +
  35 + @Override
  36 + public List<OAuth2ClientInfo> getOAuth2Clients() {
  37 + if (oauth2Configuration == null || !oauth2Configuration.isEnabled()) {
  38 + return Collections.emptyList();
  39 + }
  40 + List<OAuth2ClientInfo> result = new ArrayList<>();
  41 + for (Map.Entry<String, OAuth2Client> entry : oauth2Configuration.getClients().entrySet()) {
  42 + OAuth2ClientInfo client = new OAuth2ClientInfo();
  43 + client.setName(entry.getValue().getLoginButtonLabel());
  44 + client.setUrl(String.format("/oauth2/authorization/%s", entry.getKey()));
  45 + client.setIcon(entry.getValue().getLoginButtonIcon());
  46 + result.add(client);
  47 + }
  48 + return result;
  49 + }
  50 +}
@@ -31,6 +31,7 @@ @@ -31,6 +31,7 @@
31 <main.dir>${basedir}</main.dir> 31 <main.dir>${basedir}</main.dir>
32 <pkg.user>thingsboard</pkg.user> 32 <pkg.user>thingsboard</pkg.user>
33 <spring-boot.version>2.2.4.RELEASE</spring-boot.version> 33 <spring-boot.version>2.2.4.RELEASE</spring-boot.version>
  34 + <spring-oauth2.version>2.1.2.RELEASE</spring-oauth2.version>
34 <spring.version>5.2.2.RELEASE</spring.version> 35 <spring.version>5.2.2.RELEASE</spring.version>
35 <spring-security.version>5.2.2.RELEASE</spring-security.version> 36 <spring-security.version>5.2.2.RELEASE</spring-security.version>
36 <spring-data-redis.version>2.2.4.RELEASE</spring-data-redis.version> 37 <spring-data-redis.version>2.2.4.RELEASE</spring-data-redis.version>
@@ -463,6 +464,21 @@ @@ -463,6 +464,21 @@
463 <version>${spring-boot.version}</version> 464 <version>${spring-boot.version}</version>
464 </dependency> 465 </dependency>
465 <dependency> 466 <dependency>
  467 + <groupId>org.springframework.cloud</groupId>
  468 + <artifactId>spring-cloud-starter-oauth2</artifactId>
  469 + <version>${spring-oauth2.version}</version>
  470 + </dependency>
  471 + <dependency>
  472 + <groupId>org.springframework.security</groupId>
  473 + <artifactId>spring-security-oauth2-client</artifactId>
  474 + <version>${spring.version}</version>
  475 + </dependency>
  476 + <dependency>
  477 + <groupId>org.springframework.security</groupId>
  478 + <artifactId>spring-security-oauth2-jose</artifactId>
  479 + <version>${spring.version}</version>
  480 + </dependency>
  481 + <dependency>
466 <groupId>org.springframework.boot</groupId> 482 <groupId>org.springframework.boot</groupId>
467 <artifactId>spring-boot-starter-web</artifactId> 483 <artifactId>spring-boot-starter-web</artifactId>
468 <version>${spring-boot.version}</version> 484 <version>${spring-boot.version}</version>
@@ -21,7 +21,7 @@ import { HttpClient } from '@angular/common/http'; @@ -21,7 +21,7 @@ import { HttpClient } from '@angular/common/http';
21 import { forkJoin, Observable, of, throwError } from 'rxjs'; 21 import { forkJoin, Observable, of, throwError } from 'rxjs';
22 import { catchError, map, mergeMap, tap } from 'rxjs/operators'; 22 import { catchError, map, mergeMap, tap } from 'rxjs/operators';
23 23
24 -import { LoginRequest, LoginResponse, PublicLoginRequest } from '@shared/models/login.models'; 24 +import { LoginRequest, LoginResponse, OAuth2Client, PublicLoginRequest } from '@shared/models/login.models';
25 import { ActivatedRoute, Router, UrlTree } from '@angular/router'; 25 import { ActivatedRoute, Router, UrlTree } from '@angular/router';
26 import { defaultHttpOptions } from '../http/http-utils'; 26 import { defaultHttpOptions } from '../http/http-utils';
27 import { ReplaySubject } from 'rxjs/internal/ReplaySubject'; 27 import { ReplaySubject } from 'rxjs/internal/ReplaySubject';
@@ -65,6 +65,7 @@ export class AuthService { @@ -65,6 +65,7 @@ export class AuthService {
65 } 65 }
66 66
67 redirectUrl: string; 67 redirectUrl: string;
  68 + oauth2Clients: Array<OAuth2Client> = null;
68 69
69 private refreshTokenSubject: ReplaySubject<LoginResponse> = null; 70 private refreshTokenSubject: ReplaySubject<LoginResponse> = null;
70 private jwtHelper = new JwtHelperService(); 71 private jwtHelper = new JwtHelperService();
@@ -193,6 +194,15 @@ export class AuthService { @@ -193,6 +194,15 @@ export class AuthService {
193 }); 194 });
194 } 195 }
195 196
  197 + public loadOAuth2Clients(): Observable<Array<OAuth2Client>> {
  198 + return this.http.post<Array<OAuth2Client>>(`/api/noauth/oauth2Clients`,
  199 + null, defaultHttpOptions()).pipe(
  200 + tap((OAuth2Clients) => {
  201 + this.oauth2Clients = OAuth2Clients;
  202 + })
  203 + );
  204 + }
  205 +
196 private forceDefaultPlace(authState?: AuthState, path?: string, params?: any): boolean { 206 private forceDefaultPlace(authState?: AuthState, path?: string, params?: any): boolean {
197 if (authState && authState.authUser) { 207 if (authState && authState.authUser) {
198 if (authState.authUser.authority === Authority.TENANT_ADMIN || authState.authUser.authority === Authority.CUSTOMER_USER) { 208 if (authState.authUser.authority === Authority.TENANT_ADMIN || authState.authUser.authority === Authority.CUSTOMER_USER) {
@@ -20,9 +20,9 @@ import { AuthService } from '../auth/auth.service'; @@ -20,9 +20,9 @@ import { AuthService } from '../auth/auth.service';
20 import { select, Store } from '@ngrx/store'; 20 import { select, Store } from '@ngrx/store';
21 import { AppState } from '../core.state'; 21 import { AppState } from '../core.state';
22 import { selectAuth } from '../auth/auth.selectors'; 22 import { selectAuth } from '../auth/auth.selectors';
23 -import { catchError, map, skipWhile, take } from 'rxjs/operators'; 23 +import { catchError, map, mergeMap, skipWhile, take } from 'rxjs/operators';
24 import { AuthState } from '../auth/auth.models'; 24 import { AuthState } from '../auth/auth.models';
25 -import { Observable, of } from 'rxjs'; 25 +import { forkJoin, Observable, of } from 'rxjs';
26 import { enterZone } from '@core/operator/enterZone'; 26 import { enterZone } from '@core/operator/enterZone';
27 import { Authority } from '@shared/models/authority.enum'; 27 import { Authority } from '@shared/models/authority.enum';
28 import { DialogService } from '@core/services/dialog.service'; 28 import { DialogService } from '@core/services/dialog.service';
@@ -54,7 +54,7 @@ export class AuthGuard implements CanActivate, CanActivateChild { @@ -54,7 +54,7 @@ export class AuthGuard implements CanActivate, CanActivateChild {
54 state: RouterStateSnapshot) { 54 state: RouterStateSnapshot) {
55 55
56 return this.getAuthState().pipe( 56 return this.getAuthState().pipe(
57 - map((authState) => { 57 + mergeMap((authState) => {
58 const url: string = state.url; 58 const url: string = state.url;
59 59
60 let lastChild = state.root; 60 let lastChild = state.root;
@@ -78,13 +78,21 @@ export class AuthGuard implements CanActivate, CanActivateChild { @@ -78,13 +78,21 @@ export class AuthGuard implements CanActivate, CanActivateChild {
78 if (publicId && publicId.length > 0) { 78 if (publicId && publicId.length > 0) {
79 this.authService.setUserFromJwtToken(null, null, false); 79 this.authService.setUserFromJwtToken(null, null, false);
80 this.authService.reloadUser(); 80 this.authService.reloadUser();
81 - return false; 81 + return of(false);
82 } else if (!isPublic) { 82 } else if (!isPublic) {
83 this.authService.redirectUrl = url; 83 this.authService.redirectUrl = url;
84 // this.authService.gotoDefaultPlace(false); 84 // this.authService.gotoDefaultPlace(false);
85 - return this.authService.defaultUrl(false); 85 + return of(this.authService.defaultUrl(false));
86 } else { 86 } else {
87 - return true; 87 + const tasks: Observable<any>[] = [];
  88 + if (path === 'login') {
  89 + tasks.push(this.authService.loadOAuth2Clients());
  90 + }
  91 + return forkJoin(tasks).pipe(
  92 + map(() => {
  93 + return true;
  94 + })
  95 + );
88 } 96 }
89 } else { 97 } else {
90 if (authState.authUser.isPublic) { 98 if (authState.authUser.isPublic) {
@@ -95,20 +103,20 @@ export class AuthGuard implements CanActivate, CanActivateChild { @@ -95,20 +103,20 @@ export class AuthGuard implements CanActivate, CanActivateChild {
95 } else { 103 } else {
96 this.authService.logout(); 104 this.authService.logout();
97 } 105 }
98 - return false; 106 + return of(false);
99 } 107 }
100 } 108 }
101 const defaultUrl = this.authService.defaultUrl(true, authState, path, params); 109 const defaultUrl = this.authService.defaultUrl(true, authState, path, params);
102 if (defaultUrl) { 110 if (defaultUrl) {
103 // this.authService.gotoDefaultPlace(true); 111 // this.authService.gotoDefaultPlace(true);
104 - return defaultUrl; 112 + return of(defaultUrl);
105 } else { 113 } else {
106 const authority = Authority[authState.authUser.authority]; 114 const authority = Authority[authState.authUser.authority];
107 if (data.auth && data.auth.indexOf(authority) === -1) { 115 if (data.auth && data.auth.indexOf(authority) === -1) {
108 this.dialogService.forbidden(); 116 this.dialogService.forbidden();
109 - return false; 117 + return of(false);
110 } else { 118 } else {
111 - return true; 119 + return of(true);
112 } 120 }
113 } 121 }
114 } 122 }
@@ -15,10 +15,11 @@ @@ -15,10 +15,11 @@
15 /// 15 ///
16 16
17 import _ from 'lodash'; 17 import _ from 'lodash';
18 -import { Observable, Subject, fromEvent, of } from 'rxjs';  
19 -import { finalize, share, map } from 'rxjs/operators'; 18 +import { Observable, Observer, of, Subject } from 'rxjs';
  19 +import { finalize, map, share } from 'rxjs/operators';
20 import base64js from 'base64-js'; 20 import base64js from 'base64-js';
21 import { Datasource } from '@app/shared/models/widget.models'; 21 import { Datasource } from '@app/shared/models/widget.models';
  22 +import { FormattedData } from '@app/modules/home/components/widget/lib/maps/map-models';
22 23
23 export function onParentScrollOrWindowResize(el: Node): Observable<Event> { 24 export function onParentScrollOrWindowResize(el: Node): Observable<Event> {
24 const scrollSubject = new Subject<Event>(); 25 const scrollSubject = new Subject<Event>();
@@ -224,11 +225,14 @@ function scrollParents(node: Node): Node[] { @@ -224,11 +225,14 @@ function scrollParents(node: Node): Node[] {
224 225
225 function hashCode(str) { 226 function hashCode(str) {
226 let hash = 0; 227 let hash = 0;
227 - let i, char; 228 + let i;
  229 + let char;
228 if (str.length === 0) return hash; 230 if (str.length === 0) return hash;
229 for (i = 0; i < str.length; i++) { 231 for (i = 0; i < str.length; i++) {
230 char = str.charCodeAt(i); 232 char = str.charCodeAt(i);
  233 + // tslint:disable-next-line:no-bitwise
231 hash = ((hash << 5) - hash) + char; 234 hash = ((hash << 5) - hash) + char;
  235 + // tslint:disable-next-line:no-bitwise
232 hash = hash & hash; // Convert to 32bit integer 236 hash = hash & hash; // Convert to 32bit integer
233 } 237 }
234 return hash; 238 return hash;
@@ -430,10 +434,24 @@ export function getDescendantProp(obj: any, path: string): any { @@ -430,10 +434,24 @@ export function getDescendantProp(obj: any, path: string): any {
430 } 434 }
431 435
432 export function imageLoader(imageUrl: string): Observable<HTMLImageElement> { 436 export function imageLoader(imageUrl: string): Observable<HTMLImageElement> {
433 - const image = new Image();  
434 - const imageLoad$ = fromEvent(image, 'load').pipe(map(() => image));  
435 - image.src = imageUrl;  
436 - return imageLoad$; 437 + return new Observable((observer: Observer<HTMLImageElement>) => {
  438 + const image = new Image();
  439 + image.style.position = 'absolute';
  440 + image.style.left = '-99999px';
  441 + image.style.top = '-99999px';
  442 + image.onload = () => {
  443 + observer.next(image);
  444 + document.body.removeChild(image);
  445 + observer.complete();
  446 + };
  447 + image.onerror = err => {
  448 + observer.error(err);
  449 + document.body.removeChild(image);
  450 + observer.complete();
  451 + };
  452 + document.body.appendChild(image)
  453 + image.src = imageUrl;
  454 + });
437 } 455 }
438 456
439 export function createLabelFromDatasource(datasource: Datasource, pattern: string) { 457 export function createLabelFromDatasource(datasource: Datasource, pattern: string) {
@@ -504,12 +522,12 @@ export function parseArray(input: any[]): any[] { @@ -504,12 +522,12 @@ export function parseArray(input: any[]): any[] {
504 ); 522 );
505 } 523 }
506 524
507 -export function parseData(input: any[]): any[] { 525 +export function parseData(input: any[]): FormattedData[] {
508 return _(input).groupBy(el => el?.datasource?.entityName) 526 return _(input).groupBy(el => el?.datasource?.entityName)
509 .values().value().map((entityArray, i) => { 527 .values().value().map((entityArray, i) => {
510 const obj = { 528 const obj = {
511 entityName: entityArray[0]?.datasource?.entityName, 529 entityName: entityArray[0]?.datasource?.entityName,
512 - $datasource: entityArray[0]?.datasource, 530 + $datasource: entityArray[0]?.datasource as Datasource,
513 dsIndex: i, 531 dsIndex: i,
514 deviceType: null 532 deviceType: null
515 }; 533 };
@@ -568,7 +586,7 @@ export function parseTemplate(template: string, data: { $datasource?: Datasource @@ -568,7 +586,7 @@ export function parseTemplate(template: string, data: { $datasource?: Datasource
568 formatted.forEach(value => { 586 formatted.forEach(value => {
569 const [variable, digits] = value.replace('${', '').replace('}', '').split(':'); 587 const [variable, digits] = value.replace('${', '').replace('}', '').split(':');
570 data[variable] = padValue(data[variable], +digits); 588 data[variable] = padValue(data[variable], +digits);
571 - if (isNaN(data[variable])) data[value] = ''; 589 + if (data[variable] === 'NaN') data[variable] = '';
572 template = template.replace(value, '${' + variable + '}'); 590 template = template.replace(value, '${' + variable + '}');
573 }); 591 });
574 const variables = template.match(/\$\{.*?\}/g); 592 const variables = template.match(/\$\{.*?\}/g);
@@ -198,9 +198,15 @@ export default abstract class LeafletMap { @@ -198,9 +198,15 @@ export default abstract class LeafletMap {
198 198
199 fitBounds(bounds: LatLngBounds, useDefaultZoom = false, padding?: LatLngTuple) { 199 fitBounds(bounds: LatLngBounds, useDefaultZoom = false, padding?: LatLngTuple) {
200 if (bounds.isValid()) { 200 if (bounds.isValid()) {
201 - if ((!this.options.fitMapBounds || this.options.useDefaultCenterPosition) && this.options.defaultZoomLevel) { 201 + this.bounds = this.bounds.extend(bounds);
  202 + if (!this.options.fitMapBounds && this.options.defaultZoomLevel) {
202 this.map.setZoom(this.options.defaultZoomLevel, { animate: false }); 203 this.map.setZoom(this.options.defaultZoomLevel, { animate: false });
203 - this.map.panTo(this.options.defaultCenterPosition, { animate: false }); 204 + if (this.options.useDefaultCenterPosition) {
  205 + this.map.panTo(this.options.defaultCenterPosition, { animate: false });
  206 + }
  207 + else {
  208 + this.map.panTo(this.bounds.getCenter());
  209 + }
204 } else { 210 } else {
205 this.map.once('zoomend', () => { 211 this.map.once('zoomend', () => {
206 if (!this.options.defaultZoomLevel && this.map.getZoom() > this.options.minZoomLevel) { 212 if (!this.options.defaultZoomLevel && this.map.getZoom() > this.options.minZoomLevel) {
@@ -212,7 +218,6 @@ export default abstract class LeafletMap { @@ -212,7 +218,6 @@ export default abstract class LeafletMap {
212 } 218 }
213 this.map.fitBounds(bounds, { padding: padding || [50, 50], animate: false }); 219 this.map.fitBounds(bounds, { padding: padding || [50, 50], animate: false });
214 } 220 }
215 - this.bounds = bounds;  
216 } 221 }
217 } 222 }
218 223
@@ -15,6 +15,7 @@ @@ -15,6 +15,7 @@
15 /// 15 ///
16 16
17 import { LatLngTuple, LeafletMouseEvent } from 'leaflet'; 17 import { LatLngTuple, LeafletMouseEvent } from 'leaflet';
  18 +import { Datasource } from '@app/shared/models/widget.models';
18 19
19 export type GenericFunction = (data: FormattedData, dsData: FormattedData[], dsIndex: number) => string; 20 export type GenericFunction = (data: FormattedData, dsData: FormattedData[], dsIndex: number) => string;
20 export type MarkerImageFunction = (data: FormattedData, dsData: FormattedData[], dsIndex: number) => string; 21 export type MarkerImageFunction = (data: FormattedData, dsData: FormattedData[], dsIndex: number) => string;
@@ -96,11 +97,11 @@ export type MarkerSettings = { @@ -96,11 +97,11 @@ export type MarkerSettings = {
96 } 97 }
97 98
98 export interface FormattedData { 99 export interface FormattedData {
99 - aliasName: string; 100 + $datasource: Datasource;
100 entityName: string; 101 entityName: string;
101 - $datasource: string;  
102 dsIndex: number; 102 dsIndex: number;
103 - deviceType: string 103 + deviceType: string;
  104 + [key: string]: any
104 } 105 }
105 106
106 export type PolygonSettings = { 107 export type PolygonSettings = {
@@ -151,6 +152,6 @@ export interface HistorySelectSettings { @@ -151,6 +152,6 @@ export interface HistorySelectSettings {
151 buttonColor: string; 152 buttonColor: string;
152 } 153 }
153 154
154 -export type actionsHandler = ($event: Event | LeafletMouseEvent) => void; 155 +export type actionsHandler = ($event: Event | LeafletMouseEvent, datasource: Datasource) => void;
155 156
156 export type UnitedMapSettings = MapSettings & PolygonSettings & MarkerSettings & PolylineSettings; 157 export type UnitedMapSettings = MapSettings & PolygonSettings & MarkerSettings & PolylineSettings;
@@ -36,7 +36,7 @@ import { initSchema, addToSchema, mergeSchemes, addCondition, addGroupInfo } fro @@ -36,7 +36,7 @@ import { initSchema, addToSchema, mergeSchemes, addCondition, addGroupInfo } fro
36 import { of, Subject } from 'rxjs'; 36 import { of, Subject } from 'rxjs';
37 import { WidgetContext } from '@app/modules/home/models/widget-component.models'; 37 import { WidgetContext } from '@app/modules/home/models/widget-component.models';
38 import { getDefCenterPosition } from './maps-utils'; 38 import { getDefCenterPosition } from './maps-utils';
39 -import { JsonSettingsSchema, WidgetActionDescriptor, DatasourceType, widgetType } from '@shared/models/widget.models'; 39 +import { JsonSettingsSchema, WidgetActionDescriptor, DatasourceType, widgetType, Datasource } from '@shared/models/widget.models';
40 import { EntityId } from '@shared/models/id/entity-id'; 40 import { EntityId } from '@shared/models/id/entity-id';
41 import { AttributeScope, DataKeyType, LatestTelemetry } from '@shared/models/telemetry/telemetry.models'; 41 import { AttributeScope, DataKeyType, LatestTelemetry } from '@shared/models/telemetry/telemetry.models';
42 import { AttributeService } from '@core/http/attribute.service'; 42 import { AttributeService } from '@core/http/attribute.service';
@@ -142,7 +142,7 @@ export class MapWidgetController implements MapWidgetInterface { @@ -142,7 +142,7 @@ export class MapWidgetController implements MapWidgetInterface {
142 const descriptors = this.ctx.actionsApi.getActionDescriptors(name); 142 const descriptors = this.ctx.actionsApi.getActionDescriptors(name);
143 const actions = {}; 143 const actions = {};
144 descriptors.forEach(descriptor => { 144 descriptors.forEach(descriptor => {
145 - actions[descriptor.name] = ($event: Event) => this.onCustomAction(descriptor, $event); 145 + actions[descriptor.name] = ($event: Event, datasource: Datasource) => this.onCustomAction(descriptor, $event, datasource);
146 }, actions); 146 }, actions);
147 return actions; 147 return actions;
148 } 148 }
@@ -150,16 +150,15 @@ export class MapWidgetController implements MapWidgetInterface { @@ -150,16 +150,15 @@ export class MapWidgetController implements MapWidgetInterface {
150 onInit() { 150 onInit() {
151 } 151 }
152 152
153 - private onCustomAction(descriptor: WidgetActionDescriptor, $event: any) { 153 + private onCustomAction(descriptor: WidgetActionDescriptor, $event: any, entityInfo: Datasource) {
154 if ($event && $event.stopPropagation) { 154 if ($event && $event.stopPropagation) {
155 $event?.stopPropagation(); 155 $event?.stopPropagation();
156 } 156 }
157 - // safeExecute(parseFunction(descriptor.customFunction, ['$event', 'widgetContext']), [$event, this.ctx])  
158 - const entityInfo = this.ctx.actionsApi.getActiveEntityInfo();  
159 - const entityId = entityInfo ? entityInfo.entityId : null;  
160 - const entityName = entityInfo ? entityInfo.entityName : null;  
161 - const entityLabel = entityInfo ? entityInfo.entityLabel : null;  
162 - this.ctx.actionsApi.handleWidgetAction($event, descriptor, entityId, entityName, null, entityLabel); 157 + const { entityId, entityName, entityLabel, entityType } = entityInfo;
  158 + this.ctx.actionsApi.handleWidgetAction($event, descriptor, {
  159 + entityType,
  160 + id: entityId
  161 + }, entityName, null, entityLabel);
163 } 162 }
164 163
165 setMarkerLocation = (e) => { 164 setMarkerLocation = (e) => {
@@ -16,19 +16,22 @@ @@ -16,19 +16,22 @@
16 16
17 import L from 'leaflet'; 17 import L from 'leaflet';
18 import { MarkerSettings, PolygonSettings, PolylineSettings } from './map-models'; 18 import { MarkerSettings, PolygonSettings, PolylineSettings } from './map-models';
  19 +import { Datasource } from '@app/shared/models/widget.models';
19 20
20 export function createTooltip(target: L.Layer, 21 export function createTooltip(target: L.Layer,
21 settings: MarkerSettings | PolylineSettings | PolygonSettings, 22 settings: MarkerSettings | PolylineSettings | PolygonSettings,
22 - content?: string | HTMLElement): L.Popup { 23 + datasource: Datasource,
  24 + content?: string | HTMLElement
  25 +): L.Popup {
23 const popup = L.popup(); 26 const popup = L.popup();
24 popup.setContent(content); 27 popup.setContent(content);
25 target.bindPopup(popup, { autoClose: settings.autocloseTooltip, closeOnClick: false }); 28 target.bindPopup(popup, { autoClose: settings.autocloseTooltip, closeOnClick: false });
26 if (settings.showTooltipAction === 'hover') { 29 if (settings.showTooltipAction === 'hover') {
27 target.off('click'); 30 target.off('click');
28 - target.on('mouseover', function () { 31 + target.on('mouseover', () => {
29 target.openPopup(); 32 target.openPopup();
30 }); 33 });
31 - target.on('mouseout', function () { 34 + target.on('mouseout', () => {
32 target.closePopup(); 35 target.closePopup();
33 }); 36 });
34 } 37 }
@@ -37,7 +40,7 @@ export function createTooltip(target: L.Layer, @@ -37,7 +40,7 @@ export function createTooltip(target: L.Layer,
37 Array.from(actions).forEach( 40 Array.from(actions).forEach(
38 (element: HTMLElement) => { 41 (element: HTMLElement) => {
39 if (element && settings.tooltipAction[element.id]) { 42 if (element && settings.tooltipAction[element.id]) {
40 - element.addEventListener('click', settings.tooltipAction[element.id]) 43 + element.addEventListener('click', ($event) => settings.tooltipAction[element.id]($event, datasource));
41 } 44 }
42 }); 45 });
43 }); 46 });
@@ -42,7 +42,7 @@ export class Marker { @@ -42,7 +42,7 @@ export class Marker {
42 }); 42 });
43 43
44 if (settings.showTooltip) { 44 if (settings.showTooltip) {
45 - this.tooltip = createTooltip(this.leafletMarker, settings); 45 + this.tooltip = createTooltip(this.leafletMarker, settings, data.$datasource);
46 this.updateMarkerTooltip(data); 46 this.updateMarkerTooltip(data);
47 } 47 }
48 48
@@ -50,7 +50,7 @@ export class Marker { @@ -50,7 +50,7 @@ export class Marker {
50 this.leafletMarker.on('click', (event: LeafletMouseEvent) => { 50 this.leafletMarker.on('click', (event: LeafletMouseEvent) => {
51 for (const action in this.settings.markerClick) { 51 for (const action in this.settings.markerClick) {
52 if (typeof (this.settings.markerClick[action]) === 'function') { 52 if (typeof (this.settings.markerClick[action]) === 'function') {
53 - this.settings.markerClick[action](event); 53 + this.settings.markerClick[action](event, this.data.$datasource);
54 } 54 }
55 } 55 }
56 }); 56 });
@@ -14,7 +14,7 @@ @@ -14,7 +14,7 @@
14 /// limitations under the License. 14 /// limitations under the License.
15 /// 15 ///
16 16
17 -import L, { LatLngExpression, LatLngTuple } from 'leaflet'; 17 +import L, { LatLngExpression, LatLngTuple, LeafletMouseEvent } from 'leaflet';
18 import { createTooltip } from './maps-utils'; 18 import { createTooltip } from './maps-utils';
19 import { PolygonSettings, FormattedData } from './map-models'; 19 import { PolygonSettings, FormattedData } from './map-models';
20 import { DatasourceData } from '@app/shared/models/widget.models'; 20 import { DatasourceData } from '@app/shared/models/widget.models';
@@ -27,7 +27,7 @@ export class Polygon { @@ -27,7 +27,7 @@ export class Polygon {
27 data; 27 data;
28 dataSources; 28 dataSources;
29 29
30 - constructor(public map, polyData: DatasourceData, dataSources, private settings: PolygonSettings, onClickListener?) { 30 + constructor(public map, polyData: DatasourceData, dataSources, private settings: PolygonSettings) {
31 this.leafletPoly = L.polygon(polyData.data, { 31 this.leafletPoly = L.polygon(polyData.data, {
32 fill: true, 32 fill: true,
33 fillColor: settings.polygonColor, 33 fillColor: settings.polygonColor,
@@ -39,11 +39,17 @@ export class Polygon { @@ -39,11 +39,17 @@ export class Polygon {
39 this.dataSources = dataSources; 39 this.dataSources = dataSources;
40 this.data = polyData; 40 this.data = polyData;
41 if (settings.showPolygonTooltip) { 41 if (settings.showPolygonTooltip) {
42 - this.tooltip = createTooltip(this.leafletPoly, settings); 42 + this.tooltip = createTooltip(this.leafletPoly, settings, polyData.datasource);
43 this.updateTooltip(polyData); 43 this.updateTooltip(polyData);
44 } 44 }
45 - if (onClickListener) {  
46 - this.leafletPoly.on('click', onClickListener); 45 + if (settings.polygonClick) {
  46 + this.leafletPoly.on('click', (event: LeafletMouseEvent) => {
  47 + for (const action in this.settings.polygonClick) {
  48 + if (typeof (this.settings.polygonClick[action]) === 'function') {
  49 + this.settings.polygonClick[action](event, polyData.datasource);
  50 + }
  51 + }
  52 + });
47 } 53 }
48 } 54 }
49 55
@@ -69,8 +75,8 @@ export class Polygon { @@ -69,8 +75,8 @@ export class Polygon {
69 updatePolygonColor(settings) { 75 updatePolygonColor(settings) {
70 const style: L.PathOptions = { 76 const style: L.PathOptions = {
71 fill: true, 77 fill: true,
72 - fillColor: settings.color,  
73 - color: settings.color, 78 + fillColor: settings.polygonColor,
  79 + color: settings.polygonStrokeColor,
74 weight: settings.polygonStrokeWeight, 80 weight: settings.polygonStrokeWeight,
75 fillOpacity: settings.polygonOpacity, 81 fillOpacity: settings.polygonOpacity,
76 opacity: settings.polygonStrokeOpacity 82 opacity: settings.polygonStrokeOpacity
@@ -86,4 +92,4 @@ export class Polygon { @@ -86,4 +92,4 @@ export class Polygon {
86 this.leafletPoly.setLatLngs(latLngs); 92 this.leafletPoly.setLatLngs(latLngs);
87 this.leafletPoly.redraw(); 93 this.leafletPoly.redraw();
88 } 94 }
89 -}  
  95 +}
@@ -14,7 +14,7 @@ @@ -14,7 +14,7 @@
14 /// limitations under the License. 14 /// limitations under the License.
15 /// 15 ///
16 16
17 -import L, { LatLngLiteral } from 'leaflet'; 17 +import L, { LatLngLiteral, LatLngBounds, LatLngTuple } from 'leaflet';
18 import LeafletMap from '../leaflet-map'; 18 import LeafletMap from '../leaflet-map';
19 import { UnitedMapSettings } from '../map-models'; 19 import { UnitedMapSettings } from '../map-models';
20 import { aspectCache, parseFunction } from '@app/core/utils'; 20 import { aspectCache, parseFunction } from '@app/core/utils';
@@ -108,11 +108,12 @@ export class ImageMap extends LeafletMap { @@ -108,11 +108,12 @@ export class ImageMap extends LeafletMap {
108 this.updateBounds(updateImage, lastCenterPos); 108 this.updateBounds(updateImage, lastCenterPos);
109 this.map.invalidateSize(true); 109 this.map.invalidateSize(true);
110 } 110 }
111 -  
112 } 111 }
113 } 112 }
114 } 113 }
115 114
  115 + fitBounds(bounds: LatLngBounds, useDefaultZoom = false, padding?: LatLngTuple) { }
  116 +
116 initMap(updateImage?) { 117 initMap(updateImage?) {
117 if (!this.map && this.aspect > 0) { 118 if (!this.map && this.aspect > 0) {
118 const center = this.pointToLatLng(this.width / 2, this.height / 2); 119 const center = this.pointToLatLng(this.width / 2, this.height / 2);
@@ -16,7 +16,7 @@ @@ -16,7 +16,7 @@
16 16
17 --> 17 -->
18 <div class="tb-login-content mat-app-background tb-dark" fxFlex fxLayoutAlign="center center"> 18 <div class="tb-login-content mat-app-background tb-dark" fxFlex fxLayoutAlign="center center">
19 - <mat-card style="height: 100%; max-height: 525px; overflow-y: auto;"> 19 + <mat-card style="max-height: 80vh; overflow-y: auto;">
20 <mat-card-content> 20 <mat-card-content>
21 <form class="tb-login-form" [formGroup]="loginFormGroup" (ngSubmit)="login()"> 21 <form class="tb-login-form" [formGroup]="loginFormGroup" (ngSubmit)="login()">
22 <fieldset [disabled]="isLoading$ | async" fxLayout="column"> 22 <fieldset [disabled]="isLoading$ | async" fxLayout="column">
@@ -49,6 +49,19 @@ @@ -49,6 +49,19 @@
49 <button mat-raised-button color="accent" [disabled]="(isLoading$ | async)" 49 <button mat-raised-button color="accent" [disabled]="(isLoading$ | async)"
50 type="submit">{{ 'login.login' | translate }}</button> 50 type="submit">{{ 'login.login' | translate }}</button>
51 </div> 51 </div>
  52 + <div class="oauth-container" fxLayout="column" fxLayoutGap="16px" *ngIf="oauth2Clients?.length">
  53 + <div class="container-divider">
  54 + <div class="line"><mat-divider></mat-divider></div>
  55 + <div class="text mat-typography">{{ "login.or" | translate | uppercase }}</div>
  56 + <div class="line"><mat-divider></mat-divider></div>
  57 + </div>
  58 + <ng-container *ngFor="let oauth2Client of oauth2Clients">
  59 + <button mat-raised-button color="primary" class="centered" routerLink="{{ oauth2Client.url }}">
  60 + <mat-icon class="material-icons md-18" svgIcon="{{ oauth2Client.icon }}"></mat-icon>
  61 + {{ 'login.login-with' | translate: {name: oauth2Client.name} }}
  62 + </button>
  63 + </ng-container>
  64 + </div>
52 </div> 65 </div>
53 </fieldset> 66 </fieldset>
54 </form> 67 </form>
@@ -32,8 +32,44 @@ @@ -32,8 +32,44 @@
32 } 32 }
33 33
34 .tb-action-button{ 34 .tb-action-button{
35 - padding-top: 20px;  
36 - padding-bottom: 20px; 35 + padding: 20px 0 16px;
  36 + }
  37 + }
  38 +
  39 + .oauth-container{
  40 + padding: 0;
  41 +
  42 + .container-divider {
  43 + display: flex;
  44 + flex-direction: row;
  45 + align-items: center;
  46 + justify-content: center;
  47 + width: 100%;
  48 +
  49 + .line {
  50 + flex: 1;
  51 + }
  52 +
  53 + .mat-divider-horizontal{
  54 + position: relative;
  55 + }
  56 +
  57 + .text {
  58 + padding-right: 10px;
  59 + padding-left: 10px;
  60 + }
  61 + }
  62 +
  63 + .material-icons{
  64 + width: 20px;
  65 + min-width: 20px;
  66 + }
  67 +
  68 +
  69 + .centered ::ng-deep .mat-button-wrapper {
  70 + display: flex;
  71 + justify-content: center;
  72 + align-items: center;
37 } 73 }
38 } 74 }
39 } 75 }
@@ -23,6 +23,7 @@ import { FormBuilder } from '@angular/forms'; @@ -23,6 +23,7 @@ import { FormBuilder } from '@angular/forms';
23 import { HttpErrorResponse } from '@angular/common/http'; 23 import { HttpErrorResponse } from '@angular/common/http';
24 import { Constants } from '@shared/models/constants'; 24 import { Constants } from '@shared/models/constants';
25 import { Router } from '@angular/router'; 25 import { Router } from '@angular/router';
  26 +import { OAuth2Client } from '@shared/models/login.models';
26 27
27 @Component({ 28 @Component({
28 selector: 'tb-login', 29 selector: 'tb-login',
@@ -35,6 +36,7 @@ export class LoginComponent extends PageComponent implements OnInit { @@ -35,6 +36,7 @@ export class LoginComponent extends PageComponent implements OnInit {
35 username: '', 36 username: '',
36 password: '' 37 password: ''
37 }); 38 });
  39 + oauth2Clients: Array<OAuth2Client> = null;
38 40
39 constructor(protected store: Store<AppState>, 41 constructor(protected store: Store<AppState>,
40 private authService: AuthService, 42 private authService: AuthService,
@@ -44,6 +46,7 @@ export class LoginComponent extends PageComponent implements OnInit { @@ -44,6 +46,7 @@ export class LoginComponent extends PageComponent implements OnInit {
44 } 46 }
45 47
46 ngOnInit() { 48 ngOnInit() {
  49 + this.oauth2Clients = this.authService.oauth2Clients;
47 } 50 }
48 51
49 login(): void { 52 login(): void {
@@ -27,3 +27,9 @@ export interface LoginResponse { @@ -27,3 +27,9 @@ export interface LoginResponse {
27 token: string; 27 token: string;
28 refreshToken: string; 28 refreshToken: string;
29 } 29 }
  30 +
  31 +export interface OAuth2Client {
  32 + name: string;
  33 + icon?: string;
  34 + url: string;
  35 +}
@@ -1137,7 +1137,7 @@ @@ -1137,7 +1137,7 @@
1137 "total": "celkem" 1137 "total": "celkem"
1138 }, 1138 },
1139 "login": { 1139 "login": {
1140 - "login": "Přihlásit", 1140 + "login": "Přihlásit se",
1141 "request-password-reset": "Vyžádat reset hesla", 1141 "request-password-reset": "Vyžádat reset hesla",
1142 "reset-password": "Reset hesla", 1142 "reset-password": "Reset hesla",
1143 "create-password": "Vytvořit heslo", 1143 "create-password": "Vytvořit heslo",
@@ -1151,7 +1151,9 @@ @@ -1151,7 +1151,9 @@
1151 "new-password": "Nové heslo", 1151 "new-password": "Nové heslo",
1152 "new-password-again": "Nové heslo znovu", 1152 "new-password-again": "Nové heslo znovu",
1153 "password-link-sent-message": "Odkaz pro reset hesla byl úspěšně odeslán!", 1153 "password-link-sent-message": "Odkaz pro reset hesla byl úspěšně odeslán!",
1154 - "email": "Email" 1154 + "email": "Email",
  1155 + "login-with": "Přihlásit se přes {{name}}",
  1156 + "or": "nebo"
1155 }, 1157 },
1156 "position": { 1158 "position": {
1157 "top": "Nahoře", 1159 "top": "Nahoře",
@@ -1150,7 +1150,7 @@ @@ -1150,7 +1150,7 @@
1150 "total": "Gesamt" 1150 "total": "Gesamt"
1151 }, 1151 },
1152 "login": { 1152 "login": {
1153 - "login": "Login", 1153 + "login": "Anmelden",
1154 "request-password-reset": "Passwortzurücksetzung anfordern", 1154 "request-password-reset": "Passwortzurücksetzung anfordern",
1155 "reset-password": "Passwort zurücksetzen", 1155 "reset-password": "Passwort zurücksetzen",
1156 "create-password": "Passwort erstellen", 1156 "create-password": "Passwort erstellen",
@@ -1164,7 +1164,9 @@ @@ -1164,7 +1164,9 @@
1164 "new-password": "Neues Passwort", 1164 "new-password": "Neues Passwort",
1165 "new-password-again": "Neues Passwort wiederholen", 1165 "new-password-again": "Neues Passwort wiederholen",
1166 "password-link-sent-message": "Der Link zum Zurücksetzen des Passworts wurde erfolgreich versendet!", 1166 "password-link-sent-message": "Der Link zum Zurücksetzen des Passworts wurde erfolgreich versendet!",
1167 - "email": "E-Mail" 1167 + "email": "E-Mail",
  1168 + "login-with": "Mit {{name}} anmelden",
  1169 + "or": "oder"
1168 }, 1170 },
1169 "position": { 1171 "position": {
1170 "top": "Oben", 1172 "top": "Oben",
@@ -1706,7 +1706,9 @@ @@ -1706,7 +1706,9 @@
1706 "password-link-sent-message": "Ο σύνδεσμος επαναφοράς κωδικού πρόσβασης στάλθηκε με επιτυχία!", 1706 "password-link-sent-message": "Ο σύνδεσμος επαναφοράς κωδικού πρόσβασης στάλθηκε με επιτυχία!",
1707 "email": "Email", 1707 "email": "Email",
1708 "no-account": "Δεν έχετε λογαριασμό;", 1708 "no-account": "Δεν έχετε λογαριασμό;",
1709 - "create-account": "Δημιουργία λογαριασμού" 1709 + "create-account": "Δημιουργία λογαριασμού",
  1710 + "login-with": "Σύνδεση μέσω {{name}}",
  1711 + "or": "ή"
1710 }, 1712 },
1711 "signup": { 1713 "signup": {
1712 "firstname": "Όνομα", 1714 "firstname": "Όνομα",
@@ -1353,7 +1353,9 @@ @@ -1353,7 +1353,9 @@
1353 "new-password": "New password", 1353 "new-password": "New password",
1354 "new-password-again": "New password again", 1354 "new-password-again": "New password again",
1355 "password-link-sent-message": "Password reset link was successfully sent!", 1355 "password-link-sent-message": "Password reset link was successfully sent!",
1356 - "email": "Email" 1356 + "email": "Email",
  1357 + "login-with": "Login with {{name}}",
  1358 + "or": "or"
1357 }, 1359 },
1358 "position": { 1360 "position": {
1359 "top": "Top", 1361 "top": "Top",
@@ -1230,7 +1230,9 @@ @@ -1230,7 +1230,9 @@
1230 "new-password": "Nueva contraseña", 1230 "new-password": "Nueva contraseña",
1231 "new-password-again": "Repita la nueva contraseña", 1231 "new-password-again": "Repita la nueva contraseña",
1232 "password-link-sent-message": "¡El enlace para el restablecer la contraseña fue enviado correctamente!", 1232 "password-link-sent-message": "¡El enlace para el restablecer la contraseña fue enviado correctamente!",
1233 - "email": "Correo electrónico" 1233 + "email": "Correo electrónico",
  1234 + "login-with": "Iniciar sesión con {{name}}",
  1235 + "or": "o"
1234 }, 1236 },
1235 "position": { 1237 "position": {
1236 "top": "Superior", 1238 "top": "Superior",
@@ -1198,7 +1198,7 @@ @@ -1198,7 +1198,7 @@
1198 "create-password": "Créer un mot de passe", 1198 "create-password": "Créer un mot de passe",
1199 "email": "Email", 1199 "email": "Email",
1200 "forgot-password": "Mot de passe oublié?", 1200 "forgot-password": "Mot de passe oublié?",
1201 - "login": "Login", 1201 + "login": "Connexion",
1202 "new-password": "Nouveau mot de passe", 1202 "new-password": "Nouveau mot de passe",
1203 "new-password-again": "nouveau mot de passe", 1203 "new-password-again": "nouveau mot de passe",
1204 "password-again": "Mot de passe à nouveau", 1204 "password-again": "Mot de passe à nouveau",
@@ -1209,7 +1209,9 @@ @@ -1209,7 +1209,9 @@
1209 "request-password-reset": "Demander la réinitialisation du mot de passe", 1209 "request-password-reset": "Demander la réinitialisation du mot de passe",
1210 "reset-password": "Réinitialiser le mot de passe", 1210 "reset-password": "Réinitialiser le mot de passe",
1211 "sign-in": "Veuillez vous connecter", 1211 "sign-in": "Veuillez vous connecter",
1212 - "username": "Nom d'utilisateur (courriel)" 1212 + "username": "Nom d'utilisateur (courriel)",
  1213 + "login-with": "Se connecter avec {{name}}",
  1214 + "or": "ou"
1213 }, 1215 },
1214 "position": { 1216 "position": {
1215 "bottom": "Bas", 1217 "bottom": "Bas",
@@ -1161,7 +1161,7 @@ @@ -1161,7 +1161,7 @@
1161 "total": "totale" 1161 "total": "totale"
1162 }, 1162 },
1163 "login": { 1163 "login": {
1164 - "login": "Login", 1164 + "login": "Accedi",
1165 "request-password-reset": "Richiesta reset password", 1165 "request-password-reset": "Richiesta reset password",
1166 "reset-password": "Reset Password", 1166 "reset-password": "Reset Password",
1167 "create-password": "Crea Password", 1167 "create-password": "Crea Password",
@@ -1175,7 +1175,9 @@ @@ -1175,7 +1175,9 @@
1175 "new-password": "Nuova password", 1175 "new-password": "Nuova password",
1176 "new-password-again": "Ripeti nuova password", 1176 "new-password-again": "Ripeti nuova password",
1177 "password-link-sent-message": "Link reset password inviato con successo!", 1177 "password-link-sent-message": "Link reset password inviato con successo!",
1178 - "email": "Email" 1178 + "email": "Email",
  1179 + "login-with": "Accedi con {{name}}",
  1180 + "or": "o"
1179 }, 1181 },
1180 "position": { 1182 "position": {
1181 "top": "Alto", 1183 "top": "Alto",
@@ -1025,7 +1025,9 @@ @@ -1025,7 +1025,9 @@
1025 "new-password": "新しいパスワード", 1025 "new-password": "新しいパスワード",
1026 "new-password-again": "新しいパスワードを再入力", 1026 "new-password-again": "新しいパスワードを再入力",
1027 "password-link-sent-message": "パスワードリセットリンクが正常に送信されました!", 1027 "password-link-sent-message": "パスワードリセットリンクが正常に送信されました!",
1028 - "email": "Eメール" 1028 + "email": "Eメール",
  1029 + "login-with": "{{name}}でログイン",
  1030 + "or": "または"
1029 }, 1031 },
1030 "position": { 1032 "position": {
1031 "top": "上", 1033 "top": "上",
@@ -934,7 +934,9 @@ @@ -934,7 +934,9 @@
934 "new-password": "새 비밀번호", 934 "new-password": "새 비밀번호",
935 "new-password-again": "새 비밀번호 확인", 935 "new-password-again": "새 비밀번호 확인",
936 "password-link-sent-message": "비밀번호 재설정 링크가 성공적으로 전송되었습니다!", 936 "password-link-sent-message": "비밀번호 재설정 링크가 성공적으로 전송되었습니다!",
937 - "email": "이메일" 937 + "email": "이메일",
  938 + "login-with": "{{name}}으로 로그인",
  939 + "or": "또는"
938 }, 940 },
939 "position": { 941 "position": {
940 "top": "상단", 942 "top": "상단",
@@ -1231,7 +1231,7 @@ @@ -1231,7 +1231,7 @@
1231 } 1231 }
1232 }, 1232 },
1233 "login": { 1233 "login": {
1234 - "login": "Intră în Cont", 1234 + "login": "Conectare",
1235 "request-password-reset": "Solicită Resetarea Parolei", 1235 "request-password-reset": "Solicită Resetarea Parolei",
1236 "reset-password": "Resetează Parolă", 1236 "reset-password": "Resetează Parolă",
1237 "create-password": "Creează Parolă", 1237 "create-password": "Creează Parolă",
@@ -1246,7 +1246,9 @@ @@ -1246,7 +1246,9 @@
1246 "new-password": "Parolă nouă", 1246 "new-password": "Parolă nouă",
1247 "new-password-again": "Verificare parolă nouă", 1247 "new-password-again": "Verificare parolă nouă",
1248 "password-link-sent-message": "Ți-am trimis pe eMail un link pentru resetarea parolei", 1248 "password-link-sent-message": "Ți-am trimis pe eMail un link pentru resetarea parolei",
1249 - "email": "eMail" 1249 + "email": "eMail",
  1250 + "login-with": "Conectare cu {{name}}",
  1251 + "or": "sau"
1250 }, 1252 },
1251 "position": { 1253 "position": {
1252 "top": "Sus", 1254 "top": "Sus",
@@ -1247,7 +1247,9 @@ @@ -1247,7 +1247,9 @@
1247 "new-password": "Новый пароль", 1247 "new-password": "Новый пароль",
1248 "new-password-again": "Повторите новый пароль", 1248 "new-password-again": "Повторите новый пароль",
1249 "password-link-sent-message": "Ссылка для сброса пароля была успешно отправлена!", 1249 "password-link-sent-message": "Ссылка для сброса пароля была успешно отправлена!",
1250 - "email": "Эл. адрес" 1250 + "email": "Эл. адрес",
  1251 + "login-with": "Войти через {{name}}",
  1252 + "or": "или"
1251 }, 1253 },
1252 "position": { 1254 "position": {
1253 "top": "Верх", 1255 "top": "Верх",
@@ -1091,7 +1091,7 @@ @@ -1091,7 +1091,7 @@
1091 "total": "toplam" 1091 "total": "toplam"
1092 }, 1092 },
1093 "login": { 1093 "login": {
1094 - "login": "Oturum aç", 1094 + "login": "Giriş Yap",
1095 "request-password-reset": "Parola Sıfırlama İsteği Gönder", 1095 "request-password-reset": "Parola Sıfırlama İsteği Gönder",
1096 "reset-password": "Parola Sıfırla", 1096 "reset-password": "Parola Sıfırla",
1097 "create-password": "Parola Oluştur", 1097 "create-password": "Parola Oluştur",
@@ -1105,7 +1105,9 @@ @@ -1105,7 +1105,9 @@
1105 "new-password": "Yeni parola", 1105 "new-password": "Yeni parola",
1106 "new-password-again": "Yeni parola tekrarı", 1106 "new-password-again": "Yeni parola tekrarı",
1107 "password-link-sent-message": "Parola sıfırlama e-postası başarıyla gönderildi!", 1107 "password-link-sent-message": "Parola sıfırlama e-postası başarıyla gönderildi!",
1108 - "email": "E-posta" 1108 + "email": "E-posta",
  1109 + "login-with": "{{name}} ile Giriş Yap",
  1110 + "or": "ya da"
1109 }, 1111 },
1110 "position": { 1112 "position": {
1111 "top": "Üst", 1113 "top": "Üst",
@@ -1647,7 +1647,7 @@ @@ -1647,7 +1647,7 @@
1647 } 1647 }
1648 }, 1648 },
1649 "login": { 1649 "login": {
1650 - "login": "Вхід", 1650 + "login": "Увійти",
1651 "request-password-reset": "Запит скидання пароля", 1651 "request-password-reset": "Запит скидання пароля",
1652 "reset-password": "Скинути пароль", 1652 "reset-password": "Скинути пароль",
1653 "create-password": "Створити пароль", 1653 "create-password": "Створити пароль",
@@ -1662,7 +1662,9 @@ @@ -1662,7 +1662,9 @@
1662 "new-password": "Новий пароль", 1662 "new-password": "Новий пароль",
1663 "new-password-again": "Повторіть новий пароль", 1663 "new-password-again": "Повторіть новий пароль",
1664 "password-link-sent-message": "Посилання для скидання пароля було успішно надіслано!", 1664 "password-link-sent-message": "Посилання для скидання пароля було успішно надіслано!",
1665 - "email": "Електронна пошта" 1665 + "email": "Електронна пошта",
  1666 + "login-with": "Увійти через {{name}}",
  1667 + "or": "або"
1666 }, 1668 },
1667 "position": { 1669 "position": {
1668 "top": "Угорі", 1670 "top": "Угорі",
@@ -18,7 +18,7 @@ export default angular.module('thingsboard.api.login', []) @@ -18,7 +18,7 @@ export default angular.module('thingsboard.api.login', [])
18 .name; 18 .name;
19 19
20 /*@ngInject*/ 20 /*@ngInject*/
21 -function LoginService($http, $q) { 21 +function LoginService($http, $q, $rootScope) {
22 22
23 var service = { 23 var service = {
24 activate: activate, 24 activate: activate,
@@ -28,6 +28,7 @@ function LoginService($http, $q) { @@ -28,6 +28,7 @@ function LoginService($http, $q) {
28 publicLogin: publicLogin, 28 publicLogin: publicLogin,
29 resetPassword: resetPassword, 29 resetPassword: resetPassword,
30 sendResetPasswordLink: sendResetPasswordLink, 30 sendResetPasswordLink: sendResetPasswordLink,
  31 + loadOAuth2Clients: loadOAuth2Clients
31 } 32 }
32 33
33 return service; 34 return service;
@@ -109,4 +110,16 @@ function LoginService($http, $q) { @@ -109,4 +110,16 @@ function LoginService($http, $q) {
109 }); 110 });
110 return deferred.promise; 111 return deferred.promise;
111 } 112 }
  113 +
  114 + function loadOAuth2Clients(){
  115 + var deferred = $q.defer();
  116 + var url = '/api/noauth/oauth2Clients';
  117 + $http.post(url).then(function success(response) {
  118 + $rootScope.oauth2Clients = response.data;
  119 + deferred.resolve();
  120 + }, function fail() {
  121 + deferred.reject();
  122 + });
  123 + return deferred.promise;
  124 + }
112 } 125 }
@@ -17,7 +17,7 @@ import Flow from '@flowjs/ng-flow/dist/ng-flow-standalone.min'; @@ -17,7 +17,7 @@ import Flow from '@flowjs/ng-flow/dist/ng-flow-standalone.min';
17 import UrlHandler from './url.handler'; 17 import UrlHandler from './url.handler';
18 18
19 /*@ngInject*/ 19 /*@ngInject*/
20 -export default function AppRun($rootScope, $window, $injector, $location, $log, $state, $mdDialog, $filter, loginService, userService, $translate) { 20 +export default function AppRun($rootScope, $window, $injector, $location, $log, $state, $mdDialog, $filter, $q, loginService, userService, $translate) {
21 21
22 $window.Flow = Flow; 22 $window.Flow = Flow;
23 var frame = null; 23 var frame = null;
@@ -41,11 +41,13 @@ export default function AppRun($rootScope, $window, $injector, $location, $log, @@ -41,11 +41,13 @@ export default function AppRun($rootScope, $window, $injector, $location, $log,
41 } 41 }
42 42
43 initWatchers(); 43 initWatchers();
44 - 44 +
  45 + var skipStateChange = false;
  46 +
45 function initWatchers() { 47 function initWatchers() {
46 $rootScope.unauthenticatedHandle = $rootScope.$on('unauthenticated', function (event, doLogout) { 48 $rootScope.unauthenticatedHandle = $rootScope.$on('unauthenticated', function (event, doLogout) {
47 if (doLogout) { 49 if (doLogout) {
48 - $state.go('login'); 50 + gotoPublicModule('login');
49 } else { 51 } else {
50 UrlHandler($injector, $location); 52 UrlHandler($injector, $location);
51 } 53 }
@@ -61,6 +63,11 @@ export default function AppRun($rootScope, $window, $injector, $location, $log, @@ -61,6 +63,11 @@ export default function AppRun($rootScope, $window, $injector, $location, $log,
61 63
62 $rootScope.stateChangeStartHandle = $rootScope.$on('$stateChangeStart', function (evt, to, params) { 64 $rootScope.stateChangeStartHandle = $rootScope.$on('$stateChangeStart', function (evt, to, params) {
63 65
  66 + if (skipStateChange) {
  67 + skipStateChange = false;
  68 + return;
  69 + }
  70 +
64 function waitForUserLoaded() { 71 function waitForUserLoaded() {
65 if ($rootScope.userLoadedHandle) { 72 if ($rootScope.userLoadedHandle) {
66 $rootScope.userLoadedHandle(); 73 $rootScope.userLoadedHandle();
@@ -128,7 +135,10 @@ export default function AppRun($rootScope, $window, $injector, $location, $log, @@ -128,7 +135,10 @@ export default function AppRun($rootScope, $window, $injector, $location, $log,
128 redirectParams.toName = to.name; 135 redirectParams.toName = to.name;
129 redirectParams.params = params; 136 redirectParams.params = params;
130 userService.setRedirectParams(redirectParams); 137 userService.setRedirectParams(redirectParams);
131 - $state.go('login', params); 138 + gotoPublicModule('login', params);
  139 + } else {
  140 + evt.preventDefault();
  141 + gotoPublicModule(to.name, params);
132 } 142 }
133 } 143 }
134 } else { 144 } else {
@@ -158,6 +168,23 @@ export default function AppRun($rootScope, $window, $injector, $location, $log, @@ -158,6 +168,23 @@ export default function AppRun($rootScope, $window, $injector, $location, $log,
158 userService.gotoDefaultPlace(params); 168 userService.gotoDefaultPlace(params);
159 } 169 }
160 170
  171 + function gotoPublicModule(name, params) {
  172 + let tasks = [];
  173 + if (name === "login") {
  174 + tasks.push(loginService.loadOAuth2Clients());
  175 + }
  176 + $q.all(tasks).then(
  177 + () => {
  178 + skipStateChange = true;
  179 + $state.go(name, params);
  180 + },
  181 + () => {
  182 + skipStateChange = true;
  183 + $state.go(name, params);
  184 + }
  185 + );
  186 + }
  187 +
161 function showForbiddenDialog() { 188 function showForbiddenDialog() {
162 if (forbiddenDialog === null) { 189 if (forbiddenDialog === null) {
163 $translate(['access.access-forbidden', 190 $translate(['access.access-forbidden',
@@ -1136,7 +1136,7 @@ @@ -1136,7 +1136,7 @@
1136 "total": "celkem" 1136 "total": "celkem"
1137 }, 1137 },
1138 "login": { 1138 "login": {
1139 - "login": "Přihlásit", 1139 + "login": "Přihlásit se",
1140 "request-password-reset": "Vyžádat reset hesla", 1140 "request-password-reset": "Vyžádat reset hesla",
1141 "reset-password": "Reset hesla", 1141 "reset-password": "Reset hesla",
1142 "create-password": "Vytvořit heslo", 1142 "create-password": "Vytvořit heslo",
@@ -1150,7 +1150,9 @@ @@ -1150,7 +1150,9 @@
1150 "new-password": "Nové heslo", 1150 "new-password": "Nové heslo",
1151 "new-password-again": "Nové heslo znovu", 1151 "new-password-again": "Nové heslo znovu",
1152 "password-link-sent-message": "Odkaz pro reset hesla byl úspěšně odeslán!", 1152 "password-link-sent-message": "Odkaz pro reset hesla byl úspěšně odeslán!",
1153 - "email": "Email" 1153 + "email": "Email",
  1154 + "login-with": "Přihlásit se přes {{name}}",
  1155 + "or": "nebo"
1154 }, 1156 },
1155 "position": { 1157 "position": {
1156 "top": "Nahoře", 1158 "top": "Nahoře",
@@ -1317,7 +1317,7 @@ @@ -1317,7 +1317,7 @@
1317 } 1317 }
1318 }, 1318 },
1319 "login": { 1319 "login": {
1320 - "login": "Login", 1320 + "login": "Log in",
1321 "request-password-reset": "Request Password Reset", 1321 "request-password-reset": "Request Password Reset",
1322 "reset-password": "Reset Password", 1322 "reset-password": "Reset Password",
1323 "create-password": "Create Password", 1323 "create-password": "Create Password",
@@ -1332,7 +1332,9 @@ @@ -1332,7 +1332,9 @@
1332 "new-password": "New password", 1332 "new-password": "New password",
1333 "new-password-again": "New password again", 1333 "new-password-again": "New password again",
1334 "password-link-sent-message": "Password reset link was successfully sent!", 1334 "password-link-sent-message": "Password reset link was successfully sent!",
1335 - "email": "Email" 1335 + "email": "Email",
  1336 + "login-with": "Login with {{name}}",
  1337 + "or": "or"
1336 }, 1338 },
1337 "position": { 1339 "position": {
1338 "top": "Top", 1340 "top": "Top",
@@ -1246,7 +1246,9 @@ @@ -1246,7 +1246,9 @@
1246 "new-password": "Новый пароль", 1246 "new-password": "Новый пароль",
1247 "new-password-again": "Повторите новый пароль", 1247 "new-password-again": "Повторите новый пароль",
1248 "password-link-sent-message": "Ссылка для сброса пароля была успешно отправлена!", 1248 "password-link-sent-message": "Ссылка для сброса пароля была успешно отправлена!",
1249 - "email": "Эл. адрес" 1249 + "email": "Эл. адрес",
  1250 + "login-with": "Войти через {{name}}",
  1251 + "or": "или"
1250 }, 1252 },
1251 "position": { 1253 "position": {
1252 "top": "Верх", 1254 "top": "Верх",
@@ -1646,7 +1646,7 @@ @@ -1646,7 +1646,7 @@
1646 } 1646 }
1647 }, 1647 },
1648 "login": { 1648 "login": {
1649 - "login": "Вхід", 1649 + "login": "Увійти",
1650 "request-password-reset": "Запит скидання пароля", 1650 "request-password-reset": "Запит скидання пароля",
1651 "reset-password": "Скинути пароль", 1651 "reset-password": "Скинути пароль",
1652 "create-password": "Створити пароль", 1652 "create-password": "Створити пароль",
@@ -1661,7 +1661,9 @@ @@ -1661,7 +1661,9 @@
1661 "new-password": "Новий пароль", 1661 "new-password": "Новий пароль",
1662 "new-password-again": "Повторіть новий пароль", 1662 "new-password-again": "Повторіть новий пароль",
1663 "password-link-sent-message": "Посилання для скидання пароля було успішно надіслано!", 1663 "password-link-sent-message": "Посилання для скидання пароля було успішно надіслано!",
1664 - "email": "Електронна пошта" 1664 + "email": "Електронна пошта",
  1665 + "login-with": "Увійти через {{name}}",
  1666 + "or": "або"
1665 }, 1667 },
1666 "position": { 1668 "position": {
1667 "top": "Угорі", 1669 "top": "Угорі",
@@ -22,6 +22,10 @@ md-card.tb-login-card { @@ -22,6 +22,10 @@ md-card.tb-login-card {
22 width: 450px !important; 22 width: 450px !important;
23 } 23 }
24 24
  25 + .tb-padding {
  26 + padding: 8px;
  27 + }
  28 +
25 md-card-title { 29 md-card-title {
26 img.tb-login-logo { 30 img.tb-login-logo {
27 height: 50px; 31 height: 50px;
@@ -31,4 +35,36 @@ md-card.tb-login-card { @@ -31,4 +35,36 @@ md-card.tb-login-card {
31 md-card-content { 35 md-card-content {
32 margin-top: -50px; 36 margin-top: -50px;
33 } 37 }
  38 +
  39 + md-input-container .md-errors-spacer {
  40 + display: none;
  41 + }
  42 +
  43 + .oauth-container{
  44 + .container-divider {
  45 + display: flex;
  46 + flex-direction: row;
  47 + align-items: center;
  48 + justify-content: center;
  49 + width: 100%;
  50 + margin: 10px 0;
  51 +
  52 + .line {
  53 + flex: 1;
  54 + }
  55 +
  56 + .text {
  57 + padding-right: 10px;
  58 + padding-left: 10px;
  59 + }
  60 + }
  61 +
  62 + .material-icons{
  63 + width: 20px;
  64 + min-width: 20px;
  65 + height: 20px;
  66 + min-height: 20px;
  67 + margin: 0 4px;
  68 + }
  69 + }
34 } 70 }
@@ -24,7 +24,7 @@ @@ -24,7 +24,7 @@
24 md-mode="indeterminate" ng-disabled="!$root.loading" ng-show="$root.loading"></md-progress-linear> 24 md-mode="indeterminate" ng-disabled="!$root.loading" ng-show="$root.loading"></md-progress-linear>
25 <md-card-content> 25 <md-card-content>
26 <form class="login-form" ng-submit="vm.login()"> 26 <form class="login-form" ng-submit="vm.login()">
27 - <div layout="column" layout-padding="" id="toast-parent"> 27 + <div layout="column" class="tb-padding" id="toast-parent">
28 <span style="height: 50px;"></span> 28 <span style="height: 50px;"></span>
29 <md-input-container class="md-block"> 29 <md-input-container class="md-block">
30 <label translate>login.username</label> 30 <label translate>login.username</label>
@@ -40,13 +40,23 @@ @@ -40,13 +40,23 @@
40 </md-icon> 40 </md-icon>
41 <input id="password-input" type="password" ng-model="vm.user.password"/> 41 <input id="password-input" type="password" ng-model="vm.user.password"/>
42 </md-input-container> 42 </md-input-container>
43 - <div layout-gt-sm="column" layout-align="space-between stretch">  
44 - <div layout-gt-sm="column" layout-align="space-between end">  
45 - <md-button ui-sref="login.resetPasswordRequest">{{ 'login.forgot-password' | translate }}  
46 - </md-button>  
47 - </div> 43 + <div layout-gt-sm="column" layout-align="center end" class="tb-padding">
  44 + <md-button ui-sref="login.resetPasswordRequest">{{ 'login.forgot-password' | translate }}
  45 + </md-button>
48 </div> 46 </div>
49 <md-button class="md-raised" type="submit">{{ 'login.login' | translate }}</md-button> 47 <md-button class="md-raised" type="submit">{{ 'login.login' | translate }}</md-button>
  48 + <div class="oauth-container" layout="column" ng-if="oauth2Clients.length">
  49 + <div class="container-divider">
  50 + <div class="line"><md-divider></md-divider></div>
  51 + <div class="text mat-typography">{{ "login.or" | translate | uppercase }}</div>
  52 + <div class="line"><md-divider></md-divider></div>
  53 + </div>
  54 + <md-button ng-repeat="oauth2Client in oauth2Clients" class="md-raised"
  55 + layout="row" layout-align="center center" ng-href="{{ oauth2Client.url }}" target="_self">
  56 + <md-icon class="material-icons md-18" md-svg-icon="{{ oauth2Client.icon }}"></md-icon>
  57 + {{ 'login.login-with' | translate: {name: oauth2Client.name} }}
  58 + </md-button>
  59 + </div>
50 </div> 60 </div>
51 </form> 61 </form>
52 </md-card-content> 62 </md-card-content>