From 1e608fcff5d98e9c1bdb2556788dac2df8afa701 Mon Sep 17 00:00:00 2001 From: longyan Date: Mon, 13 Jul 2026 21:25:34 +0800 Subject: [PATCH 1/3] feat(oauth2): Add DingTalk OAuth2 provider backend services and mapper registration Add DINGTALK mapper type to MapperType enum, register DingTalkClientMapper in OAuth2ClientMapperProvider, and implement DingTalk-specific OAuth2 components: auth code filter, user service, token response client, client mapper, and configuration XML template. fix(auth): Fix NPE in Oauth2AuthenticationFailureHandler when error message is null --- .../dingtalk_config.json | 28 ++++ .../auth/oauth2/DingTalkAuthCodeFilter.java | 85 +++++++++++ .../oauth2/DingTalkOAuth2ClientMapper.java | 50 +++++++ .../oauth2/DingTalkOAuth2UserService.java | 107 +++++++++++++ .../oauth2/DingTalkTokenResponseClient.java | 141 ++++++++++++++++++ .../oauth2/OAuth2ClientMapperProvider.java | 6 + .../Oauth2AuthenticationFailureHandler.java | 6 +- .../server/common/data/oauth2/MapperType.java | 2 +- .../dao/model/sql/OAuth2ClientEntity.java | 2 +- 9 files changed, 424 insertions(+), 3 deletions(-) create mode 100644 application/src/main/data/json/system/oauth2_config_templates/dingtalk_config.json create mode 100644 application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/DingTalkAuthCodeFilter.java create mode 100644 application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/DingTalkOAuth2ClientMapper.java create mode 100644 application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/DingTalkOAuth2UserService.java create mode 100644 application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/DingTalkTokenResponseClient.java diff --git a/application/src/main/data/json/system/oauth2_config_templates/dingtalk_config.json b/application/src/main/data/json/system/oauth2_config_templates/dingtalk_config.json new file mode 100644 index 0000000000..adc43fd3b1 --- /dev/null +++ b/application/src/main/data/json/system/oauth2_config_templates/dingtalk_config.json @@ -0,0 +1,28 @@ +{ + "providerId": "DingTalk", + "additionalInfo": { + "providerName": "DingTalk" + }, + "accessTokenUri": "https://api.dingtalk.com/v1.0/oauth2/userAccessToken", + "authorizationUri": "https://login.dingtalk.com/oauth2/auth", + "scope": [ + "corpid" + ], + "jwkSetUri": null, + "userInfoUri": "https://api.dingtalk.com/v1.0/contact/users/me", + "clientAuthenticationMethod": "POST", + "userNameAttributeName": "userid", + "mapperConfig": { + "type": "DINGTALK", + "basic": { + "emailAttributeKey": "email", + "firstNameAttributeKey": "nick", + "lastNameAttributeKey": null, + "tenantNameStrategy": "DOMAIN" + } + }, + "comment": null, + "loginButtonIcon": "dingtalk-logo", + "loginButtonLabel": "DingTalk", + "helpLink": "https://open.dingtalk.com/document/isvapp/obtain-user-token" +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/DingTalkAuthCodeFilter.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/DingTalkAuthCodeFilter.java new file mode 100644 index 0000000000..1d36044fe7 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/DingTalkAuthCodeFilter.java @@ -0,0 +1,85 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.security.auth.oauth2; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizationCodeGrantFilter; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; +import org.thingsboard.server.queue.util.TbCoreComponent; + +import java.io.IOException; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; + +@Component +@TbCoreComponent +public class DingTalkAuthCodeFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + + String authCode = request.getParameter("authCode"); + if (authCode != null && !authCode.isEmpty() && request.getParameter(OAuth2ParameterNames.CODE) == null) { + HttpServletRequest wrappedRequest = new DingTalkAuthCodeRequestWrapper(request, authCode); + filterChain.doFilter(wrappedRequest, response); + return; + } + filterChain.doFilter(request, response); + } + + private static class DingTalkAuthCodeRequestWrapper extends HttpServletRequestWrapper { + + private final Map modifiedParams; + + DingTalkAuthCodeRequestWrapper(HttpServletRequest request, String authCode) { + super(request); + this.modifiedParams = new HashMap<>(request.getParameterMap()); + this.modifiedParams.put(OAuth2ParameterNames.CODE, new String[]{authCode}); + } + + @Override + public String getParameter(String name) { + if (OAuth2ParameterNames.CODE.equals(name)) { + return modifiedParams.get(OAuth2ParameterNames.CODE)[0]; + } + return super.getParameter(name); + } + + @Override + public Map getParameterMap() { + return Collections.unmodifiableMap(modifiedParams); + } + + @Override + public Enumeration getParameterNames() { + return Collections.enumeration(modifiedParams.keySet()); + } + + @Override + public String[] getParameterValues(String name) { + return modifiedParams.get(name); + } + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/DingTalkOAuth2ClientMapper.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/DingTalkOAuth2ClientMapper.java new file mode 100644 index 0000000000..61c0df193a --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/DingTalkOAuth2ClientMapper.java @@ -0,0 +1,50 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.security.auth.oauth2; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.oauth2.OAuth2Client; +import org.thingsboard.server.dao.oauth2.OAuth2User; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.security.model.SecurityUser; + +import java.util.Map; + +@Service(value = "dingTalkOAuth2ClientMapper") +@Slf4j +@TbCoreComponent +public class DingTalkOAuth2ClientMapper extends AbstractOAuth2ClientMapper implements OAuth2ClientMapper { + + @Override + public SecurityUser getOrCreateUserByClientPrincipal(HttpServletRequest request, OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Client oAuth2Client) { + Map attributes = token.getPrincipal().getAttributes(); + String email = attributes.containsKey("email") ? String.valueOf(attributes.get("email")) : null; + String nick = attributes.containsKey("nick") ? String.valueOf(attributes.get("nick")) : null; + + if (email == null || email.isEmpty()) { + throw new UsernameNotFoundException("Please try again after configuring personal email account in DingTalk."); + } + + OAuth2User oauth2User = BasicMapperUtils.getOAuth2User(email, attributes, oAuth2Client.getMapperConfig()); + oauth2User.setFirstName(nick); + + return getOrCreateSecurityUserFromOAuth2User(oauth2User, oAuth2Client); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/DingTalkOAuth2UserService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/DingTalkOAuth2UserService.java new file mode 100644 index 0000000000..cb7ea14e19 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/DingTalkOAuth2UserService.java @@ -0,0 +1,107 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.security.auth.oauth2; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.user.DefaultOAuth2User; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.stereotype.Component; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.HttpServerErrorException; +import org.springframework.web.client.RestTemplate; +import org.thingsboard.server.queue.util.TbCoreComponent; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +@Component +@Slf4j +@TbCoreComponent +public class DingTalkOAuth2UserService extends DefaultOAuth2UserService { + + private static final String DINGTALK_USER_INFO_URI = "https://api.dingtalk.com/v1.0/contact/users/me"; + + private final RestTemplate restTemplate; + + public DingTalkOAuth2UserService() { + this.restTemplate = new RestTemplate(); + } + + @Override + public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { + ClientRegistration.ProviderDetails providerDetails = userRequest.getClientRegistration().getProviderDetails(); + String userInfoUri = providerDetails.getUserInfoEndpoint().getUri(); + + if (!DINGTALK_USER_INFO_URI.equals(userInfoUri)) { + return super.loadUser(userRequest); + } + + String accessToken = userRequest.getAccessToken().getTokenValue(); + + HttpHeaders headers = new HttpHeaders(); + headers.set("x-acs-dingtalk-access-token", accessToken); + headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); + + HttpEntity entity = new HttpEntity<>(headers); + + Map responseBody; + try { + responseBody = restTemplate.exchange(userInfoUri, HttpMethod.GET, entity, Map.class).getBody(); + } catch (HttpClientErrorException | HttpServerErrorException e) { + log.error("Failed to get user info from DingTalk: status={}, body={}", e.getStatusCode(), e.getResponseBodyAsString()); + throw new OAuth2AuthenticationException("Failed to get user info from DingTalk: HTTP " + e.getStatusCode() + " - " + e.getResponseBodyAsString()); + } catch (Exception e) { + log.error("Failed to get user info from DingTalk: {}", e.getMessage(), e); + throw new OAuth2AuthenticationException("Failed to get user info from DingTalk: " + e.getMessage()); + } + + if (responseBody == null) { + throw new OAuth2AuthenticationException("Empty response from DingTalk user info endpoint"); + } + + Map userAttributes = new HashMap<>(responseBody); + + String userNameAttributeName = providerDetails.getUserInfoEndpoint().getUserNameAttributeName(); + + if (userAttributes.get(userNameAttributeName) == null) { + if (userAttributes.get("unionId") != null) { + userAttributes.put(userNameAttributeName, userAttributes.get("unionId")); + log.warn("DingTalk response missing '{}', using 'unionId' as fallback", userNameAttributeName); + } else if (userAttributes.get("openContactId") != null) { + userAttributes.put(userNameAttributeName, userAttributes.get("openContactId")); + log.warn("DingTalk response missing '{}', using 'openContactId' as fallback", userNameAttributeName); + } else { + log.error("DingTalk response missing '{}' and no fallback fields available. Response: {}", userNameAttributeName, responseBody); + throw new OAuth2AuthenticationException("DingTalk response does not contain '" + userNameAttributeName + "' or any fallback identifier (unionId, openContactId)"); + } + } + + return new DefaultOAuth2User( + Collections.emptyList(), + userAttributes, + userNameAttributeName + ); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/DingTalkTokenResponseClient.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/DingTalkTokenResponseClient.java new file mode 100644 index 0000000000..46b8271777 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/DingTalkTokenResponseClient.java @@ -0,0 +1,141 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.security.auth.oauth2; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.security.oauth2.client.endpoint.DefaultAuthorizationCodeTokenResponseClient; +import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; +import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; +import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; +import org.springframework.web.client.RestTemplate; +import org.thingsboard.server.queue.util.TbCoreComponent; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +@Component +@Slf4j +@TbCoreComponent +public class DingTalkTokenResponseClient implements OAuth2AccessTokenResponseClient { + + private static final String DINGTALK_TOKEN_URI = "https://api.dingtalk.com/v1.0/oauth2/userAccessToken"; + private static final long DEFAULT_EXPIRE_SECONDS = 7200; + + private final RestTemplate restTemplate; + private final OAuth2AccessTokenResponseClient defaultClient; + + public DingTalkTokenResponseClient() { + this.restTemplate = new RestTemplate(); + this.defaultClient = new DefaultAuthorizationCodeTokenResponseClient(); + } + + @Override + public OAuth2AccessTokenResponse getTokenResponse(OAuth2AuthorizationCodeGrantRequest grantRequest) { + if (supports(grantRequest.getClientRegistration())) { + return getTokenResponseInternal(grantRequest); + } + return defaultClient.getTokenResponse(grantRequest); + } + + private OAuth2AccessTokenResponse getTokenResponseInternal(OAuth2AuthorizationCodeGrantRequest grantRequest) { + ClientRegistration clientRegistration = grantRequest.getClientRegistration(); + String tokenUri = clientRegistration.getProviderDetails().getTokenUri(); + + Map body = new HashMap<>(); + body.put("clientId", clientRegistration.getClientId()); + body.put("clientSecret", clientRegistration.getClientSecret()); + body.put("code", grantRequest.getAuthorizationExchange().getAuthorizationResponse().getCode()); + body.put("grantType", "authorization_code"); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); + + HttpEntity> entity = new HttpEntity<>(body, headers); + + Map responseBody; + try { + responseBody = restTemplate.exchange(tokenUri, HttpMethod.POST, entity, Map.class).getBody(); + } catch (Exception e) { + log.error("Failed to get access token from DingTalk: {}", e.getMessage(), e); + throw new OAuth2AuthenticationException("Failed to get access token from DingTalk: " + e.getMessage()); + } + + if (responseBody == null) { + throw new OAuth2AuthenticationException("Empty response from DingTalk token endpoint"); + } + + if (responseBody.containsKey("code")) { + String responseCode = String.valueOf(responseBody.get("code")); + if (!"0".equals(responseCode)) { + String message = responseBody.containsKey("message") ? String.valueOf(responseBody.get("message")) : "Unknown error"; + throw new OAuth2AuthenticationException("DingTalk error [" + responseCode + "]: " + message); + } + } + + Object accessToken = responseBody.get("accessToken"); + if (accessToken == null) { + throw new OAuth2AuthenticationException("DingTalk token response missing 'accessToken'"); + } + + String refreshToken = responseBody.containsKey("refreshToken") ? String.valueOf(responseBody.get("refreshToken")) : null; + long expireIn = DEFAULT_EXPIRE_SECONDS; + if (responseBody.containsKey("expireIn")) { + try { + expireIn = Long.parseLong(String.valueOf(responseBody.get("expireIn"))); + } catch (NumberFormatException e) { + log.warn("Failed to parse expireIn from DingTalk response, using default: {}", DEFAULT_EXPIRE_SECONDS); + } + } + + Map additionalParameters = new HashMap<>(); + if (responseBody.containsKey("corpId")) { + additionalParameters.put("corpId", responseBody.get("corpId")); + } + + if (!CollectionUtils.isEmpty(additionalParameters)) { + return OAuth2AccessTokenResponse.withToken(String.valueOf(accessToken)) + .tokenType(OAuth2AccessToken.TokenType.BEARER) + .expiresIn(expireIn) + .refreshToken(refreshToken) + .scopes(grantRequest.getAuthorizationExchange().getAuthorizationRequest().getScopes()) + .additionalParameters(additionalParameters) + .build(); + } + + return OAuth2AccessTokenResponse.withToken(String.valueOf(accessToken)) + .tokenType(OAuth2AccessToken.TokenType.BEARER) + .expiresIn(expireIn) + .refreshToken(refreshToken) + .scopes(grantRequest.getAuthorizationExchange().getAuthorizationRequest().getScopes()) + .build(); + } + + public boolean supports(ClientRegistration clientRegistration) { + String tokenUri = clientRegistration.getProviderDetails().getTokenUri(); + return DINGTALK_TOKEN_URI.equals(tokenUri); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapperProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapperProvider.java index 8604461390..967e21d25e 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapperProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapperProvider.java @@ -43,6 +43,10 @@ public class OAuth2ClientMapperProvider { @Qualifier("appleOAuth2ClientMapper") private OAuth2ClientMapper appleOAuth2ClientMapper; + @Autowired + @Qualifier("dingTalkOAuth2ClientMapper") + private OAuth2ClientMapper dingTalkOAuth2ClientMapper; + public OAuth2ClientMapper getOAuth2ClientMapperByType(MapperType oauth2MapperType) { switch (oauth2MapperType) { case CUSTOM: @@ -53,6 +57,8 @@ public class OAuth2ClientMapperProvider { return githubOAuth2ClientMapper; case APPLE: return appleOAuth2ClientMapper; + case DINGTALK: + return dingTalkOAuth2ClientMapper; default: throw new RuntimeException("OAuth2ClientRegistrationMapper with type " + oauth2MapperType + " is not supported!"); } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationFailureHandler.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationFailureHandler.java index 3b9d325c38..9690def78f 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationFailureHandler.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationFailureHandler.java @@ -67,7 +67,11 @@ public class Oauth2AuthenticationFailureHandler extends SimpleUrlAuthenticationF errorPrefix = "/login?loginError="; } httpCookieOAuth2AuthorizationRequestRepository.removeAuthorizationRequestCookies(request, response); + String errorMessage = exception.getMessage(); + if (errorMessage == null) { + errorMessage = "Unknown error"; + } getRedirectStrategy().sendRedirect(request, response, baseUrl + errorPrefix + - URLEncoder.encode(exception.getMessage(), StandardCharsets.UTF_8.toString())); + URLEncoder.encode(errorMessage, StandardCharsets.UTF_8)); } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/MapperType.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/MapperType.java index fe8971a618..5c11045880 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/MapperType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/MapperType.java @@ -16,5 +16,5 @@ package org.thingsboard.server.common.data.oauth2; public enum MapperType { - BASIC, CUSTOM, GITHUB, APPLE; + BASIC, CUSTOM, GITHUB, APPLE, DINGTALK; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2ClientEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2ClientEntity.java index 3db2cbc39a..6cbc828109 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2ClientEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2ClientEntity.java @@ -177,7 +177,7 @@ public class OAuth2ClientEntity extends BaseSqlEntity { .activateUser(activateUser) .type(type) .basic( - (type == MapperType.BASIC || type == MapperType.GITHUB || type == MapperType.APPLE) ? + (type == MapperType.BASIC || type == MapperType.GITHUB || type == MapperType.APPLE || type == MapperType.DINGTALK) ? OAuth2BasicMapperConfig.builder() .emailAttributeKey(emailAttributeKey) .firstNameAttributeKey(firstNameAttributeKey) From edfb0c3c72ba132562006359174893571fa765e6 Mon Sep 17 00:00:00 2001 From: longyan Date: Mon, 13 Jul 2026 21:33:20 +0800 Subject: [PATCH 2/3] feat(oauth2): Wire DingTalk OAuth2 filter and services into Spring Security Register DingTalkAuthCodeFilter, DingTalkOAuth2UserService, and DingTalkTokenResponseClient beans in the oauth2Login security chain. Add prompt=consent parameter for DingTalk authorization requests. --- ...stomOAuth2AuthorizationRequestResolver.java | 4 ++++ .../ThingsboardSecurityConfiguration.java | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/application/src/main/java/org/thingsboard/server/config/CustomOAuth2AuthorizationRequestResolver.java b/application/src/main/java/org/thingsboard/server/config/CustomOAuth2AuthorizationRequestResolver.java index 0d959361f6..e8cf7e6c9b 100644 --- a/application/src/main/java/org/thingsboard/server/config/CustomOAuth2AuthorizationRequestResolver.java +++ b/application/src/main/java/org/thingsboard/server/config/CustomOAuth2AuthorizationRequestResolver.java @@ -170,6 +170,10 @@ public class CustomOAuth2AuthorizationRequestResolver implements OAuth2Authoriza if (ClientAuthenticationMethod.NONE.equals(clientRegistration.getClientAuthenticationMethod())) { addPkceParameters(attributes, additionalParameters); } + String authorizationUri = clientRegistration.getProviderDetails().getAuthorizationUri(); + if (authorizationUri != null && authorizationUri.contains("login.dingtalk.com")) { + additionalParameters.put("prompt", "consent"); + } builder.additionalParameters(additionalParameters); } else { throw new IllegalArgumentException("Invalid Authorization Grant Type (" + diff --git a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java index 65bbe459c1..b4e6de3a9f 100644 --- a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java +++ b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java @@ -33,6 +33,7 @@ import org.springframework.security.config.annotation.web.configuration.EnableWe import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.annotation.web.configurers.RequestCacheConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizationCodeGrantFilter; import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.AuthenticationFailureHandler; @@ -53,6 +54,9 @@ import org.thingsboard.server.service.security.auth.jwt.JwtTokenAuthenticationPr import org.thingsboard.server.service.security.auth.jwt.RefreshTokenAuthenticationProvider; import org.thingsboard.server.service.security.auth.jwt.RefreshTokenProcessingFilter; import org.thingsboard.server.service.security.auth.jwt.SkipPathRequestMatcher; +import org.thingsboard.server.service.security.auth.oauth2.DingTalkAuthCodeFilter; +import org.thingsboard.server.service.security.auth.oauth2.DingTalkOAuth2UserService; +import org.thingsboard.server.service.security.auth.oauth2.DingTalkTokenResponseClient; import org.thingsboard.server.service.security.auth.oauth2.HttpCookieOAuth2AuthorizationRequestRepository; import org.thingsboard.server.service.security.auth.pat.ApiKeyAuthenticationProvider; import org.thingsboard.server.service.security.auth.pat.ApiKeyTokenAuthenticationProcessingFilter; @@ -126,6 +130,15 @@ public class ThingsboardSecurityConfiguration { @Autowired(required = false) OAuth2Configuration oauth2Configuration; + @Autowired(required = false) + private DingTalkTokenResponseClient dingTalkTokenResponseClient; + + @Autowired(required = false) + private DingTalkOAuth2UserService dingTalkOAuth2UserService; + + @Autowired(required = false) + private DingTalkAuthCodeFilter dingTalkAuthCodeFilter; + @Autowired @Qualifier("jwtHeaderTokenExtractor") private TokenExtractor jwtHeaderTokenExtractor; @@ -279,12 +292,17 @@ public class ThingsboardSecurityConfiguration { .addFilterAfter(rateLimitProcessingFilter, UsernamePasswordAuthenticationFilter.class) .addFilterBefore(authExceptionHandler, buildRestLoginProcessingFilter().getClass()); if (oauth2Configuration != null) { + http.addFilterBefore(dingTalkAuthCodeFilter, OAuth2AuthorizationCodeGrantFilter.class); http.oauth2Login(login -> login .authorizationEndpoint(config -> config .authorizationRequestRepository(httpCookieOAuth2AuthorizationRequestRepository) .authorizationRequestResolver(oAuth2AuthorizationRequestResolver)) .loginPage("/oauth2Login") .loginProcessingUrl(oauth2Configuration.getLoginProcessingUrl()) + .tokenEndpoint(config -> config.accessTokenResponseClient(dingTalkTokenResponseClient)) + .userInfoEndpoint(config -> { + config.userService(dingTalkOAuth2UserService); + }) .successHandler(oauth2AuthenticationSuccessHandler) .failureHandler(oauth2AuthenticationFailureHandler)); } From 85b853f73941caeeca6a92ce6bb0b9dd34797281 Mon Sep 17 00:00:00 2001 From: longyan Date: Mon, 13 Jul 2026 21:46:42 +0800 Subject: [PATCH 3/3] feat(dingtalk): Add DingTalk provider frontend models, icon resources and install support Add DINGTALK to MapperType and Provider enums on frontend, include dingtalk-logo SVG icon, help link, and load OAuth2 templates during system install/upgrade. --- .../install/ThingsboardInstallService.java | 1 + ui-ngx/src/app/shared/models/constants.ts | 237 +++++++++--------- ui-ngx/src/app/shared/models/icon.models.ts | 8 + ui-ngx/src/app/shared/models/oauth2.models.ts | 7 +- 4 files changed, 133 insertions(+), 120 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java index 66695c396f..f09536e53a 100644 --- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java +++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java @@ -124,6 +124,7 @@ public class ThingsboardInstallService { systemDataLoaderService.loadSystemWidgets(); installScripts.loadSystemLwm2mResources(); installScripts.loadSystemImagesAndResources(); + systemDataLoaderService.createOAuth2Templates(); databaseSchemaVersionService.updateSchemaVersion(); } log.info("Upgrade finished successfully!"); diff --git a/ui-ngx/src/app/shared/models/constants.ts b/ui-ngx/src/app/shared/models/constants.ts index 38e3670017..ad3d6e4383 100644 --- a/ui-ngx/src/app/shared/models/constants.ts +++ b/ui-ngx/src/app/shared/models/constants.ts @@ -97,135 +97,136 @@ export const docPlatformPrefix = ''; /* eslint-disable max-len */ export const HelpLinks = { linksMap: { - outgoingMailSettings: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/mail-settings/`, - smsProviderSettings: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/sms-provider-settings/`, - slackSettings: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/slack-settings/`, - securitySettings: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/security/`, - oauth2Settings: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/security/oauth-2-support/`, + outgoingMailSettings: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/mail-settings`, + smsProviderSettings: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/sms-provider-settings`, + slackSettings: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/slack-settings`, + securitySettings: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/security-settings`, + oauth2Settings: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/oauth-2-support/`, oauth2Apple: 'https://developer.apple.com/sign-in-with-apple/get-started/', oauth2Facebook: 'https://developers.facebook.com/docs/facebook-login/web#logindialog', oauth2Github: 'https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app', oauth2Google: 'https://developers.google.com/identity/protocols/oauth2', - ruleEngine: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine/`, - ruleNodeCheckRelation: `${helpBaseUrl}/docs/reference/rule-engine/nodes/filter/check-relation-presence/`, - ruleNodeCheckExistenceFields: `${helpBaseUrl}/docs/reference/rule-engine/nodes/filter/check-fields-presence/`, - ruleNodeGpsGeofencingFilter: `${helpBaseUrl}/docs/reference/rule-engine/nodes/filter/gps-geofencing-filter/`, - ruleNodeJsFilter: `${helpBaseUrl}/docs/reference/rule-engine/nodes/filter/script/`, - ruleNodeJsSwitch: `${helpBaseUrl}/docs/reference/rule-engine/nodes/filter/switch/`, - ruleNodeAssetProfileSwitch: `${helpBaseUrl}/docs/reference/rule-engine/nodes/filter/asset-profile-switch/`, - ruleNodeDeviceProfileSwitch: `${helpBaseUrl}/docs/reference/rule-engine/nodes/filter/device-profile-switch/`, - ruleNodeCheckAlarmStatus: `${helpBaseUrl}/docs/reference/rule-engine/nodes/filter/alarm-status-filter/`, - ruleNodeMessageTypeFilter: `${helpBaseUrl}/docs/reference/rule-engine/nodes/filter/message-type-filter/`, - ruleNodeMessageTypeSwitch: `${helpBaseUrl}/docs/reference/rule-engine/nodes/filter/message-type-switch/`, - ruleNodeOriginatorTypeFilter: `${helpBaseUrl}/docs/reference/rule-engine/nodes/filter/entity-type-filter/`, - ruleNodeOriginatorTypeSwitch: `${helpBaseUrl}/docs/reference/rule-engine/nodes/filter/entity-type-switch/`, - ruleNodeOriginatorAttributes: `${helpBaseUrl}/docs/reference/rule-engine/nodes/enrichment/originator-attributes/`, - ruleNodeOriginatorFields: `${helpBaseUrl}/docs/reference/rule-engine/nodes/enrichment/originator-fields/`, - ruleNodeOriginatorTelemetry: `${helpBaseUrl}/docs/reference/rule-engine/nodes/enrichment/originator-telemetry/`, - ruleNodeCustomerAttributes: `${helpBaseUrl}/docs/reference/rule-engine/nodes/enrichment/customer-attributes/`, - ruleNodeCustomerDetails: `${helpBaseUrl}/docs/reference/rule-engine/nodes/enrichment/customer-details/`, - ruleNodeFetchDeviceCredentials: `${helpBaseUrl}/docs/reference/rule-engine/nodes/enrichment/fetch-device-credentials/`, - ruleNodeDeviceAttributes: `${helpBaseUrl}/docs/reference/rule-engine/nodes/enrichment/related-device-attributes/`, - ruleNodeRelatedAttributes: `${helpBaseUrl}/docs/reference/rule-engine/nodes/enrichment/related-entity-data/`, - ruleNodeTenantAttributes: `${helpBaseUrl}/docs/reference/rule-engine/nodes/enrichment/tenant-attributes/`, - ruleNodeTenantDetails: `${helpBaseUrl}/docs/reference/rule-engine/nodes/enrichment/tenant-details/`, - ruleNodeChangeOriginator: `${helpBaseUrl}/docs/reference/rule-engine/nodes/transformation/change-originator/`, - ruleNodeCopyKeyValuePairs: `${helpBaseUrl}/docs/reference/rule-engine/nodes/transformation/copy-key-value-pairs/`, - ruleNodeDeduplication: `${helpBaseUrl}/docs/reference/rule-engine/nodes/transformation/deduplication/`, - ruleNodeDeleteKeyValuePairs: `${helpBaseUrl}/docs/reference/rule-engine/nodes/transformation/delete-key-value-pairs/`, - ruleNodeJsonPath: `${helpBaseUrl}/docs/reference/rule-engine/nodes/transformation/json-path/`, - ruleNodeRenameKeys: `${helpBaseUrl}/docs/reference/rule-engine/nodes/transformation/rename-keys/`, - ruleNodeTransformMsg: `${helpBaseUrl}/docs/reference/rule-engine/nodes/transformation/script/`, - ruleNodeSplitArrayMsg: `${helpBaseUrl}/docs/reference/rule-engine/nodes/transformation/split-array-msg/`, - ruleNodeMsgToEmail: `${helpBaseUrl}/docs/reference/rule-engine/nodes/transformation/to-email/`, - ruleNodeAssignToCustomer: `${helpBaseUrl}/docs/reference/rule-engine/nodes/action/assign-to-customer/`, - ruleNodeUnassignFromCustomer: `${helpBaseUrl}/docs/reference/rule-engine/nodes/action/unassign-from-customer/`, - ruleNodeCalculatedFields: `${helpBaseUrl}/docs/reference/rule-engine/nodes/action/calculated-fields/`, - ruleNodeClearAlarm: `${helpBaseUrl}/docs/reference/rule-engine/nodes/action/clear-alarm/`, - ruleNodeCreateAlarm: `${helpBaseUrl}/docs/reference/rule-engine/nodes/action/create-alarm/`, - ruleNodeCopyToView: `${helpBaseUrl}/docs/reference/rule-engine/nodes/action/copy-to-view/`, - ruleNodeCreateRelation: `${helpBaseUrl}/docs/reference/rule-engine/nodes/action/create-relation/`, - ruleNodeDeleteRelation: `${helpBaseUrl}/docs/reference/rule-engine/nodes/action/delete-relation/`, - ruleNodeDeviceState: `${helpBaseUrl}/docs/reference/rule-engine/nodes/action/device-state/`, - ruleNodeMessageCount: `${helpBaseUrl}/docs/reference/rule-engine/nodes/action/message-count/`, - ruleNodeMsgDelay: `${helpBaseUrl}/docs/reference/rule-engine/nodes/action/delay/`, - ruleNodeMsgGenerator: `${helpBaseUrl}/docs/reference/rule-engine/nodes/action/generator/`, - ruleNodeGpsGeofencingEvents: `${helpBaseUrl}/docs/reference/rule-engine/nodes/action/gps-geofencing-events/`, - ruleNodeLog: `${helpBaseUrl}/docs/reference/rule-engine/nodes/action/log/`, - ruleNodeRpcCallReply: `${helpBaseUrl}/docs/reference/rule-engine/nodes/action/rpc-call-reply/`, - ruleNodeRpcCallRequest: `${helpBaseUrl}/docs/reference/rule-engine/nodes/action/rpc-call-request/`, - ruleNodeSaveAttributes: `${helpBaseUrl}/docs/reference/rule-engine/nodes/action/save-attributes/`, - ruleNodeDeleteAttributes: `${helpBaseUrl}/docs/reference/rule-engine/nodes/action/delete-attributes/`, - ruleNodeSaveTimeseries: `${helpBaseUrl}/docs/reference/rule-engine/nodes/action/save-timeseries/`, - ruleNodeSaveToCustomTable: `${helpBaseUrl}/docs/reference/rule-engine/nodes/action/save-to-custom-table/`, - ruleNodeRuleChain: `${helpBaseUrl}/docs/reference/rule-engine/nodes/flow/rule-chain/`, - ruleNodeOutputNode: `${helpBaseUrl}/docs/reference/rule-engine/nodes/flow/output/`, - ruleNodeAiRequest: `${helpBaseUrl}/docs/reference/rule-engine/nodes/external/ai-request/`, - ruleNodeAwsLambda: `${helpBaseUrl}/docs/reference/rule-engine/nodes/external/aws-lambda/`, - ruleNodeAwsSns: `${helpBaseUrl}/docs/reference/rule-engine/nodes/external/aws-sns/`, - ruleNodeAwsSqs: `${helpBaseUrl}/docs/reference/rule-engine/nodes/external/aws-sqs/`, - ruleNodeKafka: `${helpBaseUrl}/docs/reference/rule-engine/nodes/external/kafka/`, - ruleNodeMqtt: `${helpBaseUrl}/docs/reference/rule-engine/nodes/external/mqtt/`, - ruleNodeAzureIotHub: `${helpBaseUrl}/docs/reference/rule-engine/nodes/external/azure-iot-hub/`, - ruleNodeGcpPubSub: `${helpBaseUrl}/docs/reference/rule-engine/nodes/external/gcp-pubsub/`, - ruleNodeRabbitMq: `${helpBaseUrl}/docs/reference/rule-engine/nodes/external/rabbitmq/`, - ruleNodeRestApiCall: `${helpBaseUrl}/docs/reference/rule-engine/nodes/external/rest-api-call/`, - ruleNodeSendEmail: `${helpBaseUrl}/docs/reference/rule-engine/nodes/external/send-email/`, - ruleNodeSendSms: `${helpBaseUrl}/docs/reference/rule-engine/nodes/external/send-sms/`, - ruleNodeMath: `${helpBaseUrl}/docs/reference/rule-engine/nodes/action/math-function/`, - ruleNodeCalculateDelta: `${helpBaseUrl}/docs/reference/rule-engine/nodes/enrichment/calculate-delta/`, - ruleNodeRestCallReply: `${helpBaseUrl}/docs/reference/rule-engine/nodes/action/rest-call-reply/`, - ruleNodePushToCloud: `${helpBaseUrl}/docs/reference/rule-engine/nodes/action/push-to-cloud/`, - ruleNodePushToEdge: `${helpBaseUrl}/docs/reference/rule-engine/nodes/action/push-to-edge/`, - ruleNodeDeviceProfile: `${helpBaseUrl}/docs/reference/rule-engine/nodes/action/device-profile/`, - ruleNodeAcknowledge: `${helpBaseUrl}/docs/reference/rule-engine/nodes/flow/acknowledge/`, - ruleNodeCheckpoint: `${helpBaseUrl}/docs/reference/rule-engine/nodes/flow/checkpoint/`, - ruleNodeSendNotification: `${helpBaseUrl}/docs/reference/rule-engine/nodes/external/send-notification/`, - ruleNodeSendSlack: `${helpBaseUrl}/docs/reference/rule-engine/nodes/external/send-to-slack/`, - tenants: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/multi-tenancy/`, - tenantProfiles: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/tenant-profiles/`, - customers: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/customers/`, - users: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/users/`, - devices: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/devices/`, - deviceProfiles: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/device-profiles/`, - assetProfiles: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/asset-profiles/`, - edges: `${helpBaseUrl}/docs/edge/why-thingsboard-edge/`, - assets: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/assets/`, - entityViews: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/entity-views/`, - entitiesImport: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/provisioning/#bulk-provisioning`, - rulechains: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine/`, - lwm2mResourceLibrary: `${helpBaseUrl}/docs${docPlatformPrefix}/reference/lwm2m-api/getting-started/`, - jsExtension: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/contribution/widgets-development/advanced/`, - dashboards: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/dashboards/`, - otaUpdates: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ota-updates/`, - widgetTypes: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/contribution/widgets-development/#widget-types`, - widgetsBundles: `${helpBaseUrl}/docs${docPlatformPrefix}/reference/widgets/widget-library/`, - widgetsConfig: `${helpBaseUrl}/docs${docPlatformPrefix}/reference/widgets/widget-library/`, - widgetsConfigTimeseries: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/contribution/widgets-development/time-series/`, - widgetsConfigLatest: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/contribution/widgets-development/latest-values/`, - widgetsConfigRpc: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/contribution/widgets-development/rpc-control/`, - widgetsConfigAlarm: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/contribution/widgets-development/alarm-widget/`, - widgetsConfigStatic: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/contribution/widgets-development/static-widget/`, - queue: `${helpBaseUrl}/docs${docPlatformPrefix}/reference/architecture/queue/`, + oauth2DingTalk: 'https://open.dingtalk.com/document/development/obtain-the-access-token-of-an-internal-app', + ruleEngine: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/overview/`, + ruleNodeCheckRelation: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/filter/check-relation-presence/`, + ruleNodeCheckExistenceFields: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/filter/check-fields-presence/`, + ruleNodeGpsGeofencingFilter: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/filter/gps-geofencing-filter/`, + ruleNodeJsFilter: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/filter/script/`, + ruleNodeJsSwitch: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/filter/switch/`, + ruleNodeAssetProfileSwitch: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/filter/asset-profile-switch/`, + ruleNodeDeviceProfileSwitch: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/filter/device-profile-switch/`, + ruleNodeCheckAlarmStatus: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/filter/alarm-status-filter/`, + ruleNodeMessageTypeFilter: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/filter/message-type-filter/`, + ruleNodeMessageTypeSwitch: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/filter/message-type-switch/`, + ruleNodeOriginatorTypeFilter: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/filter/entity-type-filter/`, + ruleNodeOriginatorTypeSwitch: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/filter/entity-type-switch/`, + ruleNodeOriginatorAttributes: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/enrichment/originator-attributes/`, + ruleNodeOriginatorFields: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/enrichment/originator-fields/`, + ruleNodeOriginatorTelemetry: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/enrichment/originator-telemetry/`, + ruleNodeCustomerAttributes: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/enrichment/customer-attributes/`, + ruleNodeCustomerDetails: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/enrichment/customer-details/`, + ruleNodeFetchDeviceCredentials: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/enrichment/fetch-device-credentials/`, + ruleNodeDeviceAttributes: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/enrichment/related-device-attributes/`, + ruleNodeRelatedAttributes: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/enrichment/related-entity-data/`, + ruleNodeTenantAttributes: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/enrichment/tenant-attributes/`, + ruleNodeTenantDetails: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/enrichment/tenant-details/`, + ruleNodeChangeOriginator: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/transformation/change-originator/`, + ruleNodeCopyKeyValuePairs: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/transformation/copy-key-value-pairs/`, + ruleNodeDeduplication: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/transformation/deduplication/`, + ruleNodeDeleteKeyValuePairs: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/transformation/delete-key-value-pairs/`, + ruleNodeJsonPath: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/transformation/json-path/`, + ruleNodeRenameKeys: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/transformation/rename-keys/`, + ruleNodeTransformMsg: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/transformation/script/`, + ruleNodeSplitArrayMsg: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/transformation/split-array-msg/`, + ruleNodeMsgToEmail: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/transformation/to-email/`, + ruleNodeAssignToCustomer: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/assign-to-customer/`, + ruleNodeUnassignFromCustomer: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/unassign-from-customer/`, + ruleNodeCalculatedFields: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/calculated-fields/`, + ruleNodeClearAlarm: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/clear-alarm/`, + ruleNodeCreateAlarm: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/create-alarm/`, + ruleNodeCopyToView: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/copy-to-view/`, + ruleNodeCreateRelation: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/create-relation/`, + ruleNodeDeleteRelation: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/delete-relation/`, + ruleNodeDeviceState: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/device-state/`, + ruleNodeMessageCount: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/message-count/`, + ruleNodeMsgDelay: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/delay/`, + ruleNodeMsgGenerator: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/generator/`, + ruleNodeGpsGeofencingEvents: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/gps-geofencing-events/`, + ruleNodeLog: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/log/`, + ruleNodeRpcCallReply: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/rpc-call-reply/`, + ruleNodeRpcCallRequest: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/rpc-call-request/`, + ruleNodeSaveAttributes: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/save-attributes/`, + ruleNodeDeleteAttributes: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/delete-attributes/`, + ruleNodeSaveTimeseries: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/save-timeseries/`, + ruleNodeSaveToCustomTable: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/save-to-custom-table/`, + ruleNodeRuleChain: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/flow/rule-chain/`, + ruleNodeOutputNode: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/flow/output/`, + ruleNodeAiRequest: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/external/ai-request/`, + ruleNodeAwsLambda: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/external/aws-lambda/`, + ruleNodeAwsSns: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/external/aws-sns/`, + ruleNodeAwsSqs: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/external/aws-sqs/`, + ruleNodeKafka: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/external/kafka/`, + ruleNodeMqtt: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/external/mqtt/`, + ruleNodeAzureIotHub: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/external/azure-iot-hub/`, + ruleNodeGcpPubSub: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/external/gcp-pubsub/`, + ruleNodeRabbitMq: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/external/rabbitmq/`, + ruleNodeRestApiCall: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/external/rest-api-call/`, + ruleNodeSendEmail: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/external/send-email/`, + ruleNodeSendSms: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/external/send-sms/`, + ruleNodeMath: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/math-function/`, + ruleNodeCalculateDelta: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/enrichment/calculate-delta/`, + ruleNodeRestCallReply: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/rest-call-reply/`, + ruleNodePushToCloud: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/push-to-cloud/`, + ruleNodePushToEdge: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/push-to-edge/`, + ruleNodeDeviceProfile: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/device-profile/`, + ruleNodeAcknowledge: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/flow/acknowledge/`, + ruleNodeCheckpoint: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/flow/checkpoint/`, + ruleNodeSendNotification: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/external/send-notification/`, + ruleNodeSendSlack: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/external/send-to-slack/`, + tenants: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/tenants`, + tenantProfiles: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/tenant-profiles`, + customers: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/customers`, + users: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/users`, + devices: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/devices`, + deviceProfiles: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/device-profiles`, + assetProfiles: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/asset-profiles`, + edges: `${helpBaseUrl}/docs/edge/getting-started-guides/what-is-edge`, + assets: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/assets`, + entityViews: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/entity-views`, + entitiesImport: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/bulk-provisioning`, + rulechains: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/rule-chains`, + lwm2mResourceLibrary: `${helpBaseUrl}/docs${docPlatformPrefix}/reference/lwm2m-api`, + jsExtension: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/contribution/ui/advanced-development`, + dashboards: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/dashboards`, + otaUpdates: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ota-updates`, + widgetTypes: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/widget-library/#widget-types`, + widgetsBundles: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/widget-library/#widgets-library-bundles`, + widgetsConfig: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/widget-library`, + widgetsConfigTimeseries: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/widget-library#time-series`, + widgetsConfigLatest: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/widget-library#latest-values`, + widgetsConfigRpc: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/widget-library#control-widget`, + widgetsConfigAlarm: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/widget-library#alarm-widget`, + widgetsConfigStatic: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/widget-library#static`, + queue: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/queue`, repositorySettings: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/version-control/#git-settings-configuration`, autoCommitSettings: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/version-control/#auto-commit`, - twoFactorAuthentication: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/security/two-factor-authentication/`, + twoFactorAuthentication: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/two-factor-authentication`, sentNotification: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/notifications/#send-notification`, templateNotifications: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/notifications/#templates`, recipientNotifications: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/notifications/#recipients`, ruleNotifications: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/notifications/#rules`, - jwtSecuritySettings: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/security/#jwt-security-settings`, - gatewayInstall: `${helpBaseUrl}/docs/iot-gateway/installation/docker-installation/`, - scada: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/scada/`, - scadaSymbolDev: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/scada-symbol-dev/`, - scadaSymbolDevAnimation: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/scada-symbol-dev/#scadasymbolanimation`, - scadaSymbolDevConnectorAnimation: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/scada-symbol-dev/#connectorscadasymbolanimation`, - domains: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/security/domains/`, - mobileApplication: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/mobile-app-center/applications/`, - mobileBundle: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/mobile-app-center/`, - mobileQrCode: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/mobile-app-center/qr-code-widget/`, + jwtSecuritySettings: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/jwt-security-settings`, + gatewayInstall: `${helpBaseUrl}/docs/iot-gateway/install/docker-installation`, + scada: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/scada`, + scadaSymbolDev: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/scada/scada-symbols-dev-guide/`, + scadaSymbolDevAnimation: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/scada/scada-symbols-dev-guide/#scadasymbolanimation`, + scadaSymbolDevConnectorAnimation: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/scada/scada-symbols-dev-guide/#connectorscadasymbolanimation`, + domains: `${helpBaseUrl}/docs${docPlatformPrefix}/domains`, + mobileApplication: `${helpBaseUrl}/docs${docPlatformPrefix}/mobile-center/applications/`, + mobileBundle: `${helpBaseUrl}/docs${docPlatformPrefix}/mobile-center/mobile-center/`, + mobileQrCode: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/mobile-qr-code/`, calculatedField: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/calculated-fields/`, - aiModels: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ai-models/`, + aiModels: `${helpBaseUrl}/docs${docPlatformPrefix}/samples/analytics/ai-models/`, apiKeys: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/security/api-keys/`, timewindowSettings: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/dashboards/#time-window`, trendzSettings: `${helpBaseUrl}/docs/trendz/`, diff --git a/ui-ngx/src/app/shared/models/icon.models.ts b/ui-ngx/src/app/shared/models/icon.models.ts index e08abb75c5..1fd9d3e7bb 100644 --- a/ui-ngx/src/app/shared/models/icon.models.ts +++ b/ui-ngx/src/app/shared/models/icon.models.ts @@ -49,6 +49,14 @@ export const svgIcons: {[key: string]: string} = { 'C185.218331,36.9088319 192.607958,18.4081019 190.591988,0 C174.766312,0.636050225 155.629514,10.5457909 ' + '144.278109,23.8283506 C134.10507,35.5906758 125.195775,54.4170275 127.599657,72.4607932 C145.239231,73.8255433 ' + '163.259413,63.4970262 174.239142,50.1987249" fill="#000000">', + 'dingtalk-logo': '', 'queues-list': '' + '' + '' + diff --git a/ui-ngx/src/app/shared/models/oauth2.models.ts b/ui-ngx/src/app/shared/models/oauth2.models.ts index ab8e9a544a..8f605e4936 100644 --- a/ui-ngx/src/app/shared/models/oauth2.models.ts +++ b/ui-ngx/src/app/shared/models/oauth2.models.ts @@ -115,7 +115,8 @@ export enum MapperType { BASIC = 'BASIC', CUSTOM = 'CUSTOM', GITHUB = 'GITHUB', - APPLE = 'APPLE' + APPLE = 'APPLE', + DINGTALK = 'DINGTALK' } export interface OAuth2BasicMapperConfig { @@ -174,7 +175,8 @@ export enum Provider { FACEBOOK = 'Facebook', GOOGLE = 'Google', GITHUB = 'Github', - APPLE = 'Apple' + APPLE = 'Apple', + DINGTALK = 'DingTalk' } const providerHelpLinkMap = new Map( @@ -184,5 +186,6 @@ const providerHelpLinkMap = new Map( [Provider.FACEBOOK, 'oauth2Facebook'], [Provider.GITHUB, 'oauth2Github'], [Provider.GOOGLE, 'oauth2Google'], + [Provider.DINGTALK, 'oauth2DingTalk'], ] )