committed by
GitHub
122 changed files with 4055 additions and 259 deletions
@ -0,0 +1,154 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2025 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.controller; |
||||
|
|
||||
|
import io.swagger.v3.oas.annotations.Parameter; |
||||
|
import io.swagger.v3.oas.annotations.media.Schema; |
||||
|
import jakarta.validation.Valid; |
||||
|
import lombok.RequiredArgsConstructor; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.springframework.security.access.prepost.PreAuthorize; |
||||
|
import org.springframework.web.bind.annotation.DeleteMapping; |
||||
|
import org.springframework.web.bind.annotation.GetMapping; |
||||
|
import org.springframework.web.bind.annotation.PathVariable; |
||||
|
import org.springframework.web.bind.annotation.PostMapping; |
||||
|
import org.springframework.web.bind.annotation.PutMapping; |
||||
|
import org.springframework.web.bind.annotation.RequestBody; |
||||
|
import org.springframework.web.bind.annotation.RequestMapping; |
||||
|
import org.springframework.web.bind.annotation.RequestParam; |
||||
|
import org.springframework.web.bind.annotation.RestController; |
||||
|
import org.thingsboard.server.common.data.User; |
||||
|
import org.thingsboard.server.common.data.exception.ThingsboardException; |
||||
|
import org.thingsboard.server.common.data.id.ApiKeyId; |
||||
|
import org.thingsboard.server.common.data.id.UserId; |
||||
|
import org.thingsboard.server.common.data.page.PageData; |
||||
|
import org.thingsboard.server.common.data.page.PageLink; |
||||
|
import org.thingsboard.server.common.data.pat.ApiKey; |
||||
|
import org.thingsboard.server.common.data.pat.ApiKeyInfo; |
||||
|
import org.thingsboard.server.config.annotations.ApiOperation; |
||||
|
import org.thingsboard.server.dao.pat.ApiKeyService; |
||||
|
import org.thingsboard.server.queue.util.TbCoreComponent; |
||||
|
import org.thingsboard.server.service.security.model.SecurityUser; |
||||
|
import org.thingsboard.server.service.security.permission.Operation; |
||||
|
import org.thingsboard.server.service.security.permission.Resource; |
||||
|
|
||||
|
import java.util.Optional; |
||||
|
import java.util.UUID; |
||||
|
|
||||
|
import static org.thingsboard.server.controller.ControllerConstants.API_KEY_ID_PARAM_DESCRIPTION; |
||||
|
import static org.thingsboard.server.controller.ControllerConstants.API_KEY_TEXT_SEARCH_DESCRIPTION; |
||||
|
import static org.thingsboard.server.controller.ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER; |
||||
|
import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS; |
||||
|
import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; |
||||
|
import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION; |
||||
|
import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION; |
||||
|
import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION; |
||||
|
import static org.thingsboard.server.controller.ControllerConstants.USER_ID_PARAM_DESCRIPTION; |
||||
|
|
||||
|
@RestController |
||||
|
@TbCoreComponent |
||||
|
@Slf4j |
||||
|
@RequestMapping("/api") |
||||
|
@RequiredArgsConstructor |
||||
|
public class ApiKeyController extends BaseController { |
||||
|
|
||||
|
private final ApiKeyService apiKeyService; |
||||
|
|
||||
|
@ApiOperation(value = "Save API key for user (saveApiKey)", |
||||
|
notes = "Creates an API key for the given user and returns the token ONCE as 'ApiKey <value>'." + AVAILABLE_FOR_ANY_AUTHORIZED_USER) |
||||
|
@PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN', 'CUSTOMER_USER')") |
||||
|
@PostMapping(value = "/apiKey") |
||||
|
public ApiKey saveApiKey( |
||||
|
@Parameter(description = "A JSON value representing the Api Key token.") |
||||
|
@RequestBody @Valid ApiKeyInfo apiKeyInfo) throws ThingsboardException { |
||||
|
User user = checkUserId(apiKeyInfo.getUserId(), Operation.WRITE); |
||||
|
apiKeyInfo.setTenantId(user.getTenantId()); |
||||
|
checkEntity(apiKeyInfo.getId(), apiKeyInfo, Resource.API_KEY); |
||||
|
return checkNotNull(apiKeyService.saveApiKey(apiKeyInfo.getTenantId(), apiKeyInfo)); |
||||
|
} |
||||
|
|
||||
|
@ApiOperation(value = "Get User Api Keys (getUserApiKeys)", |
||||
|
notes = "Returns a page of api keys owned by user. " + |
||||
|
PAGE_DATA_PARAMETERS + AVAILABLE_FOR_ANY_AUTHORIZED_USER) |
||||
|
@PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN', 'CUSTOMER_USER')") |
||||
|
@GetMapping(value = "/apiKeys/{userId}") |
||||
|
public PageData<ApiKeyInfo> getUserApiKeys( |
||||
|
@Parameter(description = USER_ID_PARAM_DESCRIPTION) |
||||
|
@PathVariable("userId") String userIdStr, |
||||
|
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) |
||||
|
@RequestParam int pageSize, |
||||
|
@Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) |
||||
|
@RequestParam int page, |
||||
|
@Parameter(description = API_KEY_TEXT_SEARCH_DESCRIPTION) |
||||
|
@RequestParam(required = false) String textSearch, |
||||
|
@Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "expirationTime", "description", "enabled"})) |
||||
|
@RequestParam(required = false) String sortProperty, |
||||
|
@Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"})) |
||||
|
@RequestParam(required = false) String sortOrder) throws ThingsboardException { |
||||
|
SecurityUser securityUser = getCurrentUser(); |
||||
|
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); |
||||
|
UserId userId = new UserId(toUUID(userIdStr)); |
||||
|
accessControlService.checkPermission(securityUser, Resource.API_KEY, Operation.READ); |
||||
|
User user = checkUserId(userId, Operation.READ); |
||||
|
return apiKeyService.findApiKeysByUserId(user.getTenantId(), userId, pageLink); |
||||
|
} |
||||
|
|
||||
|
@ApiOperation(value = "Update API key Description", |
||||
|
notes = "Updates the description of the existing API key by apiKeyId. " + |
||||
|
"Only the description can be updated. " + |
||||
|
"Referencing a non-existing ApiKey Id will cause a 'Not Found' error." + AVAILABLE_FOR_ANY_AUTHORIZED_USER) |
||||
|
@PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN', 'CUSTOMER_USER')") |
||||
|
@PutMapping("/apiKey/{id}/description") |
||||
|
public ApiKeyInfo updateApiKeyDescription( |
||||
|
@Parameter(description = API_KEY_ID_PARAM_DESCRIPTION, required = true) |
||||
|
@PathVariable UUID id, |
||||
|
@Parameter(description = "New description for the API key", example = "Description") |
||||
|
@RequestBody Optional<String> description) throws Exception { |
||||
|
ApiKeyId apiKeyId = new ApiKeyId(id); |
||||
|
ApiKey apiKey = checkApiKeyId(apiKeyId, Operation.WRITE); |
||||
|
checkUserId(apiKey.getUserId(), Operation.WRITE); |
||||
|
apiKey.setDescription(description.orElse(null)); |
||||
|
return apiKeyService.saveApiKey(apiKey.getTenantId(), apiKey); |
||||
|
} |
||||
|
|
||||
|
@ApiOperation(value = "Enable or disable API key (enableApiKey)", |
||||
|
notes = "Updates api key with enabled = true/false. " + AVAILABLE_FOR_ANY_AUTHORIZED_USER) |
||||
|
@PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN', 'CUSTOMER_USER')") |
||||
|
@PutMapping(value = "/apiKey/{id}/enabled/{enabledValue}") |
||||
|
public ApiKeyInfo enableApiKey( |
||||
|
@Parameter(description = "Unique identifier of the API key to enable/disable", required = true) |
||||
|
@PathVariable UUID id, |
||||
|
@Parameter(description = "Enabled or disabled api key", required = true) |
||||
|
@PathVariable(value = "enabledValue") Boolean enabledValue) throws ThingsboardException { |
||||
|
ApiKeyId apiKeyId = new ApiKeyId(id); |
||||
|
ApiKey apiKey = checkApiKeyId(apiKeyId, Operation.WRITE); |
||||
|
checkUserId(apiKey.getUserId(), Operation.WRITE); |
||||
|
apiKey.setEnabled(enabledValue); |
||||
|
return apiKeyService.saveApiKey(apiKey.getTenantId(), apiKey); |
||||
|
} |
||||
|
|
||||
|
@ApiOperation(value = "Delete API key by ID (deleteApiKey)", |
||||
|
notes = "Deletes the API key. Referencing non-existing ApiKey Id will cause an error." + AVAILABLE_FOR_ANY_AUTHORIZED_USER) |
||||
|
@PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN', 'CUSTOMER_USER')") |
||||
|
@DeleteMapping(value = "/apiKey/{id}") |
||||
|
public void deleteApiKey(@PathVariable UUID id) throws ThingsboardException { |
||||
|
ApiKeyId apiKeyId = new ApiKeyId(id); |
||||
|
ApiKey apiKey = checkApiKeyId(apiKeyId, Operation.DELETE); |
||||
|
checkUserId(apiKey.getUserId(), Operation.WRITE); |
||||
|
apiKeyService.deleteApiKey(apiKey.getTenantId(), apiKey, false); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,29 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2025 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.extractor; |
||||
|
|
||||
|
import org.springframework.stereotype.Component; |
||||
|
|
||||
|
import static org.thingsboard.server.config.ThingsboardSecurityConfiguration.API_KEY_HEADER_PREFIX; |
||||
|
|
||||
|
@Component(value = "apiKeyHeaderTokenExtractor") |
||||
|
public class ApiKeyHeaderTokenExtractor extends AbstractHeaderTokenExtractor { |
||||
|
|
||||
|
public ApiKeyHeaderTokenExtractor() { |
||||
|
super(API_KEY_HEADER_PREFIX); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,29 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2025 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.extractor; |
||||
|
|
||||
|
import org.springframework.stereotype.Component; |
||||
|
|
||||
|
import static org.thingsboard.server.config.ThingsboardSecurityConfiguration.BEARER_HEADER_PREFIX; |
||||
|
|
||||
|
@Component(value = "jwtHeaderTokenExtractor") |
||||
|
public class JwtHeaderTokenExtractor extends AbstractHeaderTokenExtractor { |
||||
|
|
||||
|
public JwtHeaderTokenExtractor() { |
||||
|
super(BEARER_HEADER_PREFIX); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,86 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2025 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.pat; |
||||
|
|
||||
|
import lombok.RequiredArgsConstructor; |
||||
|
import org.springframework.security.authentication.BadCredentialsException; |
||||
|
import org.springframework.security.authentication.CredentialsExpiredException; |
||||
|
import org.springframework.security.authentication.DisabledException; |
||||
|
import org.springframework.security.authentication.InsufficientAuthenticationException; |
||||
|
import org.springframework.security.core.Authentication; |
||||
|
import org.springframework.security.core.AuthenticationException; |
||||
|
import org.springframework.security.core.userdetails.UsernameNotFoundException; |
||||
|
import org.springframework.stereotype.Component; |
||||
|
import org.thingsboard.server.common.data.StringUtils; |
||||
|
import org.thingsboard.server.common.data.User; |
||||
|
import org.thingsboard.server.common.data.UserAuthDetails; |
||||
|
import org.thingsboard.server.common.data.pat.ApiKey; |
||||
|
import org.thingsboard.server.dao.pat.ApiKeyService; |
||||
|
import org.thingsboard.server.service.security.model.SecurityUser; |
||||
|
import org.thingsboard.server.service.security.model.UserPrincipal; |
||||
|
import org.thingsboard.server.service.security.model.token.RawApiKey; |
||||
|
import org.thingsboard.server.service.user.cache.UserAuthDetailsCache; |
||||
|
|
||||
|
@Component |
||||
|
@RequiredArgsConstructor |
||||
|
public class ApiKeyAuthenticationProvider implements org.springframework.security.authentication.AuthenticationProvider { |
||||
|
|
||||
|
private final ApiKeyService apiKeyService; |
||||
|
private final UserAuthDetailsCache userAuthDetailsCache; |
||||
|
|
||||
|
@Override |
||||
|
public Authentication authenticate(Authentication authentication) throws AuthenticationException { |
||||
|
RawApiKey rawApiKey = (RawApiKey) authentication.getCredentials(); |
||||
|
SecurityUser securityUser = authenticate(rawApiKey.apiKey()); |
||||
|
return new ApiKeyAuthenticationToken(securityUser); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public boolean supports(Class<?> authentication) { |
||||
|
return ApiKeyAuthenticationToken.class.isAssignableFrom(authentication); |
||||
|
} |
||||
|
|
||||
|
private SecurityUser authenticate(String key) { |
||||
|
if (StringUtils.isEmpty(key)) { |
||||
|
throw new BadCredentialsException("Empty API key"); |
||||
|
} |
||||
|
ApiKey apiKey = apiKeyService.findApiKeyByValue(key); |
||||
|
if (apiKey == null) { |
||||
|
throw new BadCredentialsException("User not found for the provided API key"); |
||||
|
} |
||||
|
if (!apiKey.isEnabled()) { |
||||
|
throw new DisabledException("API key auth is not active"); |
||||
|
} |
||||
|
if (apiKey.getExpirationTime() != 0 && apiKey.getExpirationTime() < System.currentTimeMillis()) { |
||||
|
throw new CredentialsExpiredException("API key is expired"); |
||||
|
} |
||||
|
UserAuthDetails userAuthDetails = userAuthDetailsCache.getUserAuthDetails(apiKey.getTenantId(), apiKey.getUserId()); |
||||
|
if (userAuthDetails == null) { |
||||
|
throw new UsernameNotFoundException("User with credentials not found"); |
||||
|
} |
||||
|
if (!userAuthDetails.credentialsEnabled()) { |
||||
|
throw new DisabledException("User is not active"); |
||||
|
} |
||||
|
|
||||
|
User user = userAuthDetails.user(); |
||||
|
if (user.getAuthority() == null) { |
||||
|
throw new InsufficientAuthenticationException("User has no authority assigned"); |
||||
|
} |
||||
|
UserPrincipal userPrincipal = new UserPrincipal(UserPrincipal.Type.USER_NAME, user.getEmail()); |
||||
|
return new SecurityUser(user, true, userPrincipal); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,61 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2025 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.pat; |
||||
|
|
||||
|
import org.springframework.security.authentication.AbstractAuthenticationToken; |
||||
|
import org.thingsboard.server.service.security.model.SecurityUser; |
||||
|
import org.thingsboard.server.service.security.model.token.RawApiKey; |
||||
|
|
||||
|
import java.io.Serial; |
||||
|
|
||||
|
public class ApiKeyAuthenticationToken extends AbstractAuthenticationToken { |
||||
|
|
||||
|
@Serial |
||||
|
private static final long serialVersionUID = 2978710889397403536L; |
||||
|
|
||||
|
private RawApiKey rawApiKey; |
||||
|
private SecurityUser securityUser; |
||||
|
|
||||
|
public ApiKeyAuthenticationToken(RawApiKey rawApiKey) { |
||||
|
super(null); |
||||
|
this.rawApiKey = rawApiKey; |
||||
|
setAuthenticated(false); |
||||
|
} |
||||
|
|
||||
|
public ApiKeyAuthenticationToken(SecurityUser securityUser) { |
||||
|
super(securityUser.getAuthorities()); |
||||
|
this.eraseCredentials(); |
||||
|
this.securityUser = securityUser; |
||||
|
super.setAuthenticated(true); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public Object getCredentials() { |
||||
|
return rawApiKey; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public Object getPrincipal() { |
||||
|
return this.securityUser; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void eraseCredentials() { |
||||
|
super.eraseCredentials(); |
||||
|
this.rawApiKey = null; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,87 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2025 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.pat; |
||||
|
|
||||
|
import jakarta.servlet.FilterChain; |
||||
|
import jakarta.servlet.ServletException; |
||||
|
import jakarta.servlet.http.HttpServletRequest; |
||||
|
import jakarta.servlet.http.HttpServletResponse; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.beans.factory.annotation.Qualifier; |
||||
|
import org.springframework.security.core.Authentication; |
||||
|
import org.springframework.security.core.AuthenticationException; |
||||
|
import org.springframework.security.core.context.SecurityContext; |
||||
|
import org.springframework.security.core.context.SecurityContextHolder; |
||||
|
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; |
||||
|
import org.springframework.security.web.authentication.AuthenticationFailureHandler; |
||||
|
import org.springframework.security.web.util.matcher.RequestMatcher; |
||||
|
import org.thingsboard.server.service.security.auth.extractor.TokenExtractor; |
||||
|
import org.thingsboard.server.service.security.model.token.RawApiKey; |
||||
|
|
||||
|
import java.io.IOException; |
||||
|
|
||||
|
import static org.thingsboard.server.config.ThingsboardSecurityConfiguration.API_KEY_HEADER_PREFIX; |
||||
|
import static org.thingsboard.server.config.ThingsboardSecurityConfiguration.AUTHORIZATION_HEADER; |
||||
|
import static org.thingsboard.server.config.ThingsboardSecurityConfiguration.AUTHORIZATION_HEADER_V2; |
||||
|
|
||||
|
public class ApiKeyTokenAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter { |
||||
|
|
||||
|
private final AuthenticationFailureHandler failureHandler; |
||||
|
private final TokenExtractor tokenExtractor; |
||||
|
|
||||
|
@Autowired |
||||
|
public ApiKeyTokenAuthenticationProcessingFilter(AuthenticationFailureHandler failureHandler, |
||||
|
@Qualifier("apiKeyHeaderTokenExtractor") TokenExtractor tokenExtractor, RequestMatcher matcher) { |
||||
|
super(matcher); |
||||
|
this.failureHandler = failureHandler; |
||||
|
this.tokenExtractor = tokenExtractor; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { |
||||
|
RawApiKey rawApiKey = new RawApiKey(tokenExtractor.extract(request)); |
||||
|
return getAuthenticationManager().authenticate(new ApiKeyAuthenticationToken(rawApiKey)); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, |
||||
|
Authentication authResult) throws IOException, ServletException { |
||||
|
SecurityContext context = SecurityContextHolder.createEmptyContext(); |
||||
|
context.setAuthentication(authResult); |
||||
|
SecurityContextHolder.setContext(context); |
||||
|
chain.doFilter(request, response); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) { |
||||
|
if (!super.requiresAuthentication(request, response)) { |
||||
|
return false; |
||||
|
} |
||||
|
String header = request.getHeader(AUTHORIZATION_HEADER); |
||||
|
if (header == null) { |
||||
|
header = request.getHeader(AUTHORIZATION_HEADER_V2); |
||||
|
} |
||||
|
return header != null && header.startsWith(API_KEY_HEADER_PREFIX); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, |
||||
|
AuthenticationException failed) throws IOException, ServletException { |
||||
|
SecurityContextHolder.clearContext(); |
||||
|
failureHandler.onAuthenticationFailure(request, response, failed); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,18 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2025 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.model.token; |
||||
|
|
||||
|
public record RawApiKey(String apiKey) {} |
||||
@ -0,0 +1,56 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2025 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.ttl; |
||||
|
|
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; |
||||
|
import org.springframework.scheduling.annotation.Scheduled; |
||||
|
import org.springframework.stereotype.Service; |
||||
|
import org.thingsboard.server.dao.pat.ApiKeyDao; |
||||
|
import org.thingsboard.server.queue.discovery.PartitionService; |
||||
|
import org.thingsboard.server.queue.util.TbCoreComponent; |
||||
|
|
||||
|
@Slf4j |
||||
|
@Service |
||||
|
@TbCoreComponent |
||||
|
@ConditionalOnExpression("${sql.ttl.api_keys.enabled:true} && ${sql.ttl.api_keys.ttl:0} > 0") |
||||
|
public class ApiKeysCleanUpService extends AbstractCleanUpService { |
||||
|
|
||||
|
public static final String RANDOM_DELAY_INTERVAL_MS_EXPRESSION = |
||||
|
"#{T(org.apache.commons.lang3.RandomUtils).nextLong(0, ${sql.ttl.api_keys.checking_interval_ms})}"; |
||||
|
|
||||
|
private final ApiKeyDao apiKeyDao; |
||||
|
|
||||
|
public ApiKeysCleanUpService(PartitionService partitionService, ApiKeyDao apiKeyDao) { |
||||
|
super(partitionService); |
||||
|
this.apiKeyDao = apiKeyDao; |
||||
|
} |
||||
|
|
||||
|
@Scheduled( |
||||
|
initialDelayString = RANDOM_DELAY_INTERVAL_MS_EXPRESSION, |
||||
|
fixedDelayString = "${sql.ttl.api_keys.checking_interval_ms:86400000}" |
||||
|
) |
||||
|
public void cleanUp() { |
||||
|
long threshold = System.currentTimeMillis(); |
||||
|
if (isSystemTenantPartitionMine()) { |
||||
|
int deleted = apiKeyDao.deleteAllByExpirationTimeBefore(threshold); |
||||
|
if (deleted > 0) { |
||||
|
log.info("API key cleanup removed {} keys (thresholdTs={})", deleted, threshold); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,78 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2025 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.user.cache; |
||||
|
|
||||
|
import com.github.benmanes.caffeine.cache.Cache; |
||||
|
import com.github.benmanes.caffeine.cache.Caffeine; |
||||
|
import jakarta.annotation.PostConstruct; |
||||
|
import lombok.RequiredArgsConstructor; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.springframework.beans.factory.annotation.Value; |
||||
|
import org.springframework.context.event.EventListener; |
||||
|
import org.springframework.stereotype.Service; |
||||
|
import org.thingsboard.server.common.data.EntityType; |
||||
|
import org.thingsboard.server.common.data.UserAuthDetails; |
||||
|
import org.thingsboard.server.common.data.id.TenantId; |
||||
|
import org.thingsboard.server.common.data.id.UserId; |
||||
|
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; |
||||
|
import org.thingsboard.server.dao.user.UserService; |
||||
|
import org.thingsboard.server.queue.util.TbCoreComponent; |
||||
|
|
||||
|
import java.util.concurrent.TimeUnit; |
||||
|
|
||||
|
@Slf4j |
||||
|
@Service |
||||
|
@TbCoreComponent |
||||
|
@RequiredArgsConstructor |
||||
|
public class DefaultUserAuthDetailsCache implements UserAuthDetailsCache { |
||||
|
|
||||
|
private final UserService userService; |
||||
|
|
||||
|
@Value("${cache.userAuthDetails.maxSize:1000}") |
||||
|
private int cacheMaxSize; |
||||
|
@Value("${cache.userAuthDetails.timeToLiveInMinutes:30}") |
||||
|
private int cacheValueTtl; |
||||
|
private Cache<UserId, UserAuthDetails> cache; |
||||
|
|
||||
|
@PostConstruct |
||||
|
private void init() { |
||||
|
cache = Caffeine.newBuilder() |
||||
|
.maximumSize(cacheMaxSize) |
||||
|
.expireAfterAccess(cacheValueTtl, TimeUnit.MINUTES) |
||||
|
.build(); |
||||
|
} |
||||
|
|
||||
|
@EventListener(ComponentLifecycleMsg.class) |
||||
|
public void onComponentLifecycleEvent(ComponentLifecycleMsg event) { |
||||
|
if (event.getEntityId() != null) { |
||||
|
if (event.getEntityId().getEntityType() == EntityType.USER) { |
||||
|
evict(new UserId(event.getEntityId().getId())); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public UserAuthDetails getUserAuthDetails(TenantId tenantId, UserId userId) { |
||||
|
log.trace("Retrieving user with enabled credentials status for id {} for tenant {} from cache", userId, tenantId); |
||||
|
return cache.get(userId, id -> userService.findUserAuthDetailsByUserId(tenantId, id)); |
||||
|
} |
||||
|
|
||||
|
public void evict(UserId userId) { |
||||
|
cache.invalidate(userId); |
||||
|
log.trace("Evicted record for user {} from cache", userId); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,26 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2025 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.user.cache; |
||||
|
|
||||
|
import org.thingsboard.server.common.data.UserAuthDetails; |
||||
|
import org.thingsboard.server.common.data.id.TenantId; |
||||
|
import org.thingsboard.server.common.data.id.UserId; |
||||
|
|
||||
|
public interface UserAuthDetailsCache { |
||||
|
|
||||
|
UserAuthDetails getUserAuthDetails(TenantId tenantId, UserId userId); |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,144 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2025 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.controller; |
||||
|
|
||||
|
import com.fasterxml.jackson.core.type.TypeReference; |
||||
|
import org.junit.Assert; |
||||
|
import org.junit.Before; |
||||
|
import org.junit.Test; |
||||
|
import org.thingsboard.server.common.data.page.PageData; |
||||
|
import org.thingsboard.server.common.data.page.PageLink; |
||||
|
import org.thingsboard.server.common.data.pat.ApiKey; |
||||
|
import org.thingsboard.server.common.data.pat.ApiKeyInfo; |
||||
|
import org.thingsboard.server.dao.service.DaoSqlTest; |
||||
|
|
||||
|
import java.util.UUID; |
||||
|
|
||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; |
||||
|
|
||||
|
@DaoSqlTest |
||||
|
public class ApiKeyControllerTest extends AbstractControllerTest { |
||||
|
|
||||
|
@Before |
||||
|
public void setUp() throws Exception { |
||||
|
loginTenantAdmin(); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void testSaveApiKey() throws Exception { |
||||
|
ApiKeyInfo apiKeyInfo = constructApiKeyInfo("New API key description", true); |
||||
|
|
||||
|
doPost("/api/apiKey", apiKeyInfo, ApiKey.class); |
||||
|
|
||||
|
PageData<ApiKeyInfo> pageData = doGetTypedWithPageLink("/api/apiKeys/" + tenantAdminUserId + "?", new TypeReference<>() {}, new PageLink(10, 0)); |
||||
|
Assert.assertEquals(1, pageData.getData().size()); |
||||
|
|
||||
|
ApiKeyInfo savedApiKey = pageData.getData().get(0); |
||||
|
Assert.assertNotNull(savedApiKey); |
||||
|
Assert.assertEquals(apiKeyInfo.getDescription(), savedApiKey.getDescription()); |
||||
|
Assert.assertEquals(apiKeyInfo.isEnabled(), savedApiKey.isEnabled()); |
||||
|
Assert.assertEquals(tenantId, savedApiKey.getTenantId()); |
||||
|
Assert.assertEquals(tenantAdminUser.getId(), savedApiKey.getUserId()); |
||||
|
|
||||
|
doDelete("/api/apiKey/" + savedApiKey.getId()).andExpect(status().isOk()); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void tesFindUserApiKeys() throws Exception { |
||||
|
PageData<ApiKeyInfo> pageData = doGetTypedWithPageLink("/api/apiKeys/" + tenantAdminUserId + "?", new TypeReference<>() {}, new PageLink(10, 0)); |
||||
|
Assert.assertTrue(pageData.getData().isEmpty()); |
||||
|
|
||||
|
ApiKeyInfo apiKeyInfo = constructApiKeyInfo("Test API key description", true); |
||||
|
int expectedSize = 10; |
||||
|
for (int i = 0; i < expectedSize; i++) { |
||||
|
doPost("/api/apiKey", apiKeyInfo, ApiKey.class); |
||||
|
} |
||||
|
|
||||
|
PageData<ApiKeyInfo> pageData2 = doGetTypedWithPageLink("/api/apiKeys/" + tenantAdminUserId + "?", new TypeReference<>() {}, new PageLink(10, 0)); |
||||
|
Assert.assertEquals(expectedSize, pageData2.getData().size()); |
||||
|
|
||||
|
pageData2.getData().forEach(apiKey -> { |
||||
|
try { |
||||
|
doDelete("/api/apiKey/" + apiKey.getId()).andExpect(status().isOk()); |
||||
|
} catch (Exception e) { |
||||
|
throw new RuntimeException(e); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void testUpdateApiKeyDescription() throws Exception { |
||||
|
ApiKeyInfo apiKeyInfo = constructApiKeyInfo("Test API key description", true); |
||||
|
doPost("/api/apiKey", apiKeyInfo, ApiKey.class); |
||||
|
|
||||
|
PageData<ApiKeyInfo> pageData = doGetTypedWithPageLink("/api/apiKeys/" + tenantAdminUserId + "?", new TypeReference<>() {}, new PageLink(10, 0)); |
||||
|
Assert.assertEquals(1, pageData.getData().size()); |
||||
|
|
||||
|
ApiKeyInfo savedApiKey = pageData.getData().get(0); |
||||
|
|
||||
|
String newDescription = "Updated API Key Description"; |
||||
|
|
||||
|
ApiKeyInfo updatedApiKeyInfo = doPut("/api/apiKey/" + savedApiKey.getId().getId() + "/description", newDescription, ApiKeyInfo.class); |
||||
|
Assert.assertNotNull(updatedApiKeyInfo); |
||||
|
Assert.assertEquals(newDescription, updatedApiKeyInfo.getDescription()); |
||||
|
|
||||
|
doDelete("/api/apiKey/" + savedApiKey.getId()).andExpect(status().isOk()); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void testEnableApiKey() throws Exception { |
||||
|
ApiKeyInfo apiKeyInfo = constructApiKeyInfo("Test API key description", true); |
||||
|
doPost("/api/apiKey", apiKeyInfo, ApiKey.class); |
||||
|
|
||||
|
PageData<ApiKeyInfo> pageData = doGetTypedWithPageLink("/api/apiKeys/" + tenantAdminUserId + "?", new TypeReference<>() {}, new PageLink(10, 0)); |
||||
|
Assert.assertEquals(1, pageData.getData().size()); |
||||
|
|
||||
|
ApiKeyInfo savedApiKey = pageData.getData().get(0); |
||||
|
|
||||
|
ApiKeyInfo disabledApiKeyInfo = doPut("/api/apiKey/" + savedApiKey.getId().getId() + "/enabled/false", Boolean.FALSE, ApiKeyInfo.class); |
||||
|
Assert.assertNotNull(disabledApiKeyInfo); |
||||
|
Assert.assertFalse(disabledApiKeyInfo.isEnabled()); |
||||
|
|
||||
|
ApiKeyInfo enabledApiKeyInfo = doPut("/api/apiKey/" + savedApiKey.getId().getId() + "/enabled/true", Boolean.TRUE, ApiKeyInfo.class); |
||||
|
Assert.assertNotNull(enabledApiKeyInfo); |
||||
|
Assert.assertTrue(enabledApiKeyInfo.isEnabled()); |
||||
|
|
||||
|
doDelete("/api/apiKey/" + savedApiKey.getId()).andExpect(status().isOk()); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void testDeleteApiKey() throws Exception { |
||||
|
doDelete("/api/apiKey/" + UUID.randomUUID()).andExpect(status().isNotFound()); |
||||
|
|
||||
|
ApiKeyInfo apiKeyInfo = constructApiKeyInfo("Test API key description", false); |
||||
|
doPost("/api/apiKey", apiKeyInfo, ApiKey.class); |
||||
|
|
||||
|
PageData<ApiKeyInfo> pageData = doGetTypedWithPageLink("/api/apiKeys/" + tenantAdminUserId + "?", new TypeReference<>() {}, new PageLink(10, 0)); |
||||
|
Assert.assertEquals(1, pageData.getData().size()); |
||||
|
ApiKeyInfo savedApiKey = pageData.getData().get(0); |
||||
|
|
||||
|
doDelete("/api/apiKey/" + savedApiKey.getId().getId()).andExpect(status().isOk()); |
||||
|
} |
||||
|
|
||||
|
private ApiKeyInfo constructApiKeyInfo(String description, boolean enabled) { |
||||
|
ApiKeyInfo apiKeyInfo = new ApiKeyInfo(); |
||||
|
apiKeyInfo.setDescription(description); |
||||
|
apiKeyInfo.setEnabled(enabled); |
||||
|
apiKeyInfo.setUserId(tenantAdminUserId); |
||||
|
return apiKeyInfo; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,112 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2025 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.pat; |
||||
|
|
||||
|
import org.junit.After; |
||||
|
import org.junit.Assert; |
||||
|
import org.junit.Before; |
||||
|
import org.junit.Test; |
||||
|
import org.mockito.Mockito; |
||||
|
import org.thingsboard.server.common.data.audit.ActionType; |
||||
|
import org.thingsboard.server.common.data.edge.Edge; |
||||
|
import org.thingsboard.server.common.data.pat.ApiKey; |
||||
|
import org.thingsboard.server.common.data.pat.ApiKeyInfo; |
||||
|
import org.thingsboard.server.controller.AbstractControllerTest; |
||||
|
import org.thingsboard.server.dao.service.DaoSqlTest; |
||||
|
|
||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; |
||||
|
import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID; |
||||
|
|
||||
|
@DaoSqlTest |
||||
|
public class ApiKeyAuthenticationProviderTest extends AbstractControllerTest { |
||||
|
|
||||
|
ApiKey savedApiKey; |
||||
|
|
||||
|
@Before |
||||
|
public void setUp() throws Exception { |
||||
|
loginTenantAdmin(); |
||||
|
|
||||
|
ApiKeyInfo apiKeyInfo = constructApiKeyInfo(); |
||||
|
savedApiKey = doPost("/api/apiKey", apiKeyInfo, ApiKey.class); |
||||
|
setApiKey(savedApiKey.getValue()); |
||||
|
} |
||||
|
|
||||
|
@After |
||||
|
public void cleanUp() throws Exception { |
||||
|
resetApiKey(); |
||||
|
doDelete("/api/apiKey/" + savedApiKey.getId()).andExpect(status().isOk()); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void testSaveEdgeWithApiKey() throws Exception { |
||||
|
Edge edge = constructEdge("My edge", "default"); |
||||
|
|
||||
|
Mockito.reset(tbClusterService, auditLogService); |
||||
|
|
||||
|
Edge savedEdge = doPostWithApiKey("/api/edge", edge, Edge.class); |
||||
|
|
||||
|
Assert.assertNotNull(savedEdge); |
||||
|
Assert.assertNotNull(savedEdge.getId()); |
||||
|
Assert.assertTrue(savedEdge.getCreatedTime() > 0); |
||||
|
Assert.assertEquals(tenantId, savedEdge.getTenantId()); |
||||
|
Assert.assertNotNull(savedEdge.getCustomerId()); |
||||
|
Assert.assertEquals(NULL_UUID, savedEdge.getCustomerId().getId()); |
||||
|
Assert.assertEquals(edge.getName(), savedEdge.getName()); |
||||
|
|
||||
|
testNotifyEdgeStateChangeEventManyTimeMsgToEdgeServiceNever(savedEdge, savedEdge.getId(), savedEdge.getId(), |
||||
|
tenantId, tenantAdminUser.getCustomerId(), tenantAdminUser.getId(), tenantAdminUser.getEmail(), |
||||
|
ActionType.ADDED, 2); |
||||
|
|
||||
|
savedEdge.setName("My new edge"); |
||||
|
doPostWithApiKey("/api/edge", savedEdge, Edge.class); |
||||
|
|
||||
|
Edge foundEdge = doGetWithApiKey("/api/edge/" + savedEdge.getId().getId().toString(), Edge.class); |
||||
|
Assert.assertEquals(foundEdge.getName(), savedEdge.getName()); |
||||
|
|
||||
|
testNotifyEdgeStateChangeEventManyTimeMsgToEdgeServiceNever(foundEdge, foundEdge.getId(), foundEdge.getId(), |
||||
|
tenantId, tenantAdminUser.getCustomerId(), tenantAdminUser.getId(), tenantAdminUser.getEmail(), |
||||
|
ActionType.UPDATED, 1); |
||||
|
|
||||
|
doDeleteWithApiKey("/api/edge/" + savedEdge.getId().getId().toString()) |
||||
|
.andExpect(status().isOk()); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void testUnauthorizedWhenKeyDisabled() throws Exception { |
||||
|
ApiKeyInfo disabledApiKeyInfo = doPut("/api/apiKey/" + savedApiKey.getId().getId() + "/enabled/false", Boolean.FALSE, ApiKeyInfo.class); |
||||
|
Assert.assertFalse(disabledApiKeyInfo.isEnabled()); |
||||
|
doGetWithApiKey("/api/admin/featuresInfo").andExpect(status().isUnauthorized()); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void testUnauthorizedWhenKeyExpired() throws Exception { |
||||
|
ApiKeyInfo apiKeyInfo = constructApiKeyInfo(); |
||||
|
apiKeyInfo.setExpirationTime(System.currentTimeMillis() - 1000); |
||||
|
ApiKey savedApiKeyWithBad = doPost("/api/apiKey", apiKeyInfo, ApiKey.class); |
||||
|
setApiKey(savedApiKeyWithBad.getValue()); |
||||
|
doPost("/api/apiKey", savedApiKey, ApiKeyInfo.class); |
||||
|
doGetWithApiKey("/api/admin/featuresInfo").andExpect(status().isUnauthorized()); |
||||
|
} |
||||
|
|
||||
|
private ApiKeyInfo constructApiKeyInfo() { |
||||
|
ApiKeyInfo apiKeyInfo = new ApiKeyInfo(); |
||||
|
apiKeyInfo.setDescription("New API key description"); |
||||
|
apiKeyInfo.setEnabled(true); |
||||
|
apiKeyInfo.setUserId(tenantAdminUserId); |
||||
|
return apiKeyInfo; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,41 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2025 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.dao.pat; |
||||
|
|
||||
|
import org.thingsboard.server.common.data.id.ApiKeyId; |
||||
|
import org.thingsboard.server.common.data.id.TenantId; |
||||
|
import org.thingsboard.server.common.data.id.UserId; |
||||
|
import org.thingsboard.server.common.data.page.PageData; |
||||
|
import org.thingsboard.server.common.data.page.PageLink; |
||||
|
import org.thingsboard.server.common.data.pat.ApiKey; |
||||
|
import org.thingsboard.server.common.data.pat.ApiKeyInfo; |
||||
|
import org.thingsboard.server.dao.entity.EntityDaoService; |
||||
|
|
||||
|
public interface ApiKeyService extends EntityDaoService { |
||||
|
|
||||
|
ApiKey saveApiKey(TenantId tenantId, ApiKeyInfo apiKey); |
||||
|
|
||||
|
void deleteApiKey(TenantId tenantId, ApiKey apiKey, boolean force); |
||||
|
|
||||
|
void deleteByUserId(TenantId tenantId, UserId userId); |
||||
|
|
||||
|
ApiKey findApiKeyByValue(String value); |
||||
|
|
||||
|
ApiKey findApiKeyById(TenantId tenantId, ApiKeyId apiKeyId); |
||||
|
|
||||
|
PageData<ApiKeyInfo> findApiKeysByUserId(TenantId tenantId, UserId userId, PageLink pageLink); |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,18 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2025 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.common.data; |
||||
|
|
||||
|
public record UserAuthDetails(User user, boolean credentialsEnabled) {} |
||||
@ -0,0 +1,46 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2025 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.common.data.id; |
||||
|
|
||||
|
import com.fasterxml.jackson.annotation.JsonCreator; |
||||
|
import com.fasterxml.jackson.annotation.JsonProperty; |
||||
|
import io.swagger.v3.oas.annotations.media.Schema; |
||||
|
import org.thingsboard.server.common.data.EntityType; |
||||
|
|
||||
|
import java.io.Serial; |
||||
|
import java.util.UUID; |
||||
|
|
||||
|
public class ApiKeyId extends UUIDBased implements EntityId { |
||||
|
|
||||
|
@Serial |
||||
|
private static final long serialVersionUID = -273913539653684641L; |
||||
|
|
||||
|
@JsonCreator |
||||
|
public ApiKeyId(@JsonProperty("id") UUID id) { |
||||
|
super(id); |
||||
|
} |
||||
|
|
||||
|
public static ApiKeyId fromString(String secretId) { |
||||
|
return new ApiKeyId(UUID.fromString(secretId)); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "API_KEY", allowableValues = "API_KEY") |
||||
|
public EntityType getEntityType() { |
||||
|
return EntityType.API_KEY; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,61 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2025 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.common.data.pat; |
||||
|
|
||||
|
import io.swagger.v3.oas.annotations.media.Schema; |
||||
|
import lombok.Data; |
||||
|
import lombok.EqualsAndHashCode; |
||||
|
import org.thingsboard.server.common.data.id.ApiKeyId; |
||||
|
import org.thingsboard.server.common.data.validation.NoXss; |
||||
|
|
||||
|
import java.io.Serial; |
||||
|
|
||||
|
@Schema |
||||
|
@Data |
||||
|
@EqualsAndHashCode(callSuper = true) |
||||
|
public class ApiKey extends ApiKeyInfo { |
||||
|
|
||||
|
@Serial |
||||
|
private static final long serialVersionUID = -2313196723950490263L; |
||||
|
|
||||
|
@NoXss |
||||
|
@Schema(description = "Api key value", requiredMode = Schema.RequiredMode.REQUIRED) |
||||
|
private String value; |
||||
|
|
||||
|
public ApiKey() { |
||||
|
super(); |
||||
|
} |
||||
|
|
||||
|
public ApiKey(ApiKeyId id) { |
||||
|
super(id); |
||||
|
} |
||||
|
|
||||
|
public ApiKey(ApiKey apiKey) { |
||||
|
super(apiKey); |
||||
|
this.value = apiKey.getValue(); |
||||
|
} |
||||
|
|
||||
|
public ApiKey(ApiKeyInfo apiKeyInfo) { |
||||
|
super(apiKeyInfo); |
||||
|
this.value = null; |
||||
|
} |
||||
|
|
||||
|
public ApiKey(ApiKeyInfo apiKeyInfo, String value) { |
||||
|
super(apiKeyInfo); |
||||
|
this.value = value; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,96 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2025 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.common.data.pat; |
||||
|
|
||||
|
import com.fasterxml.jackson.annotation.JsonProperty; |
||||
|
import io.swagger.v3.oas.annotations.media.Schema; |
||||
|
import jakarta.validation.constraints.NotBlank; |
||||
|
import lombok.Data; |
||||
|
import lombok.EqualsAndHashCode; |
||||
|
import org.thingsboard.server.common.data.BaseData; |
||||
|
import org.thingsboard.server.common.data.HasTenantId; |
||||
|
import org.thingsboard.server.common.data.id.ApiKeyId; |
||||
|
import org.thingsboard.server.common.data.id.TenantId; |
||||
|
import org.thingsboard.server.common.data.id.UserId; |
||||
|
import org.thingsboard.server.common.data.validation.Length; |
||||
|
import org.thingsboard.server.common.data.validation.NoXss; |
||||
|
|
||||
|
import java.io.Serial; |
||||
|
|
||||
|
@Schema |
||||
|
@Data |
||||
|
@EqualsAndHashCode(callSuper = true) |
||||
|
public class ApiKeyInfo extends BaseData<ApiKeyId> implements HasTenantId { |
||||
|
|
||||
|
@Serial |
||||
|
private static final long serialVersionUID = -2313196723950490263L; |
||||
|
|
||||
|
@Schema(description = "JSON object with Tenant Id. Tenant Id of the api key cannot be changed.", accessMode = Schema.AccessMode.READ_ONLY) |
||||
|
private TenantId tenantId; |
||||
|
|
||||
|
@Schema(description = "JSON object with User Id. User Id of the api key cannot be changed.") |
||||
|
private UserId userId; |
||||
|
|
||||
|
@Schema(description = "Expiration time of the api key.") |
||||
|
private long expirationTime; |
||||
|
|
||||
|
@NoXss |
||||
|
@NotBlank |
||||
|
@Length(fieldName = "description") |
||||
|
@Schema(description = "Api Key description.", example = "Api Key description") |
||||
|
private String description; |
||||
|
|
||||
|
@Schema(description = "Enabled/disabled api key.", example = "true") |
||||
|
private boolean enabled; |
||||
|
|
||||
|
@JsonProperty(access = JsonProperty.Access.READ_ONLY) |
||||
|
@Schema(description = "Indicates if the api key is expired based on current time. Returns false if expirationTime is 0 (no expiry).", |
||||
|
example = "false", |
||||
|
accessMode = Schema.AccessMode.READ_ONLY) |
||||
|
public boolean isExpired() { |
||||
|
if (expirationTime == 0) { |
||||
|
return false; |
||||
|
} |
||||
|
return System.currentTimeMillis() > expirationTime; |
||||
|
} |
||||
|
|
||||
|
@Schema(description = "JSON object with the Api Key Id. " + |
||||
|
"Specify this field to update the Api Key. " + |
||||
|
"Referencing non-existing Api Key Id will cause error. " + |
||||
|
"Omit this field to create new Api Key.") |
||||
|
@Override |
||||
|
public ApiKeyId getId() { |
||||
|
return super.getId(); |
||||
|
} |
||||
|
|
||||
|
public ApiKeyInfo() { |
||||
|
super(); |
||||
|
} |
||||
|
|
||||
|
public ApiKeyInfo(ApiKeyId id) { |
||||
|
super(id); |
||||
|
} |
||||
|
|
||||
|
public ApiKeyInfo(ApiKeyInfo apiKeyInfo) { |
||||
|
super(apiKeyInfo); |
||||
|
this.tenantId = apiKeyInfo.getTenantId(); |
||||
|
this.userId = apiKeyInfo.getUserId(); |
||||
|
this.expirationTime = apiKeyInfo.getExpirationTime(); |
||||
|
this.enabled = apiKeyInfo.isEnabled(); |
||||
|
this.description = apiKeyInfo.getDescription(); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,81 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2025 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.dao.model.sql; |
||||
|
|
||||
|
import jakarta.persistence.Column; |
||||
|
import jakarta.persistence.MappedSuperclass; |
||||
|
import lombok.Data; |
||||
|
import lombok.EqualsAndHashCode; |
||||
|
import org.thingsboard.server.common.data.id.ApiKeyId; |
||||
|
import org.thingsboard.server.common.data.id.TenantId; |
||||
|
import org.thingsboard.server.common.data.id.UserId; |
||||
|
import org.thingsboard.server.common.data.pat.ApiKeyInfo; |
||||
|
import org.thingsboard.server.dao.model.BaseEntity; |
||||
|
import org.thingsboard.server.dao.model.BaseSqlEntity; |
||||
|
|
||||
|
import java.util.UUID; |
||||
|
|
||||
|
import static org.thingsboard.server.dao.model.ModelConstants.API_KEY_DESCRIPTION_COLUMN_NAME; |
||||
|
import static org.thingsboard.server.dao.model.ModelConstants.API_KEY_ENABLED_COLUMN_NAME; |
||||
|
import static org.thingsboard.server.dao.model.ModelConstants.API_KEY_EXPIRATION_TIME_COLUMN_NAME; |
||||
|
import static org.thingsboard.server.dao.model.ModelConstants.API_KEY_TENANT_ID_COLUMN_NAME; |
||||
|
import static org.thingsboard.server.dao.model.ModelConstants.API_KEY_USER_ID_COLUMN_NAME; |
||||
|
|
||||
|
@Data |
||||
|
@EqualsAndHashCode(callSuper = true) |
||||
|
@MappedSuperclass |
||||
|
public abstract class AbstractApiKeyInfoEntity<T extends ApiKeyInfo> extends BaseSqlEntity<T> implements BaseEntity<T> { |
||||
|
|
||||
|
@Column(name = API_KEY_TENANT_ID_COLUMN_NAME) |
||||
|
private UUID tenantId; |
||||
|
|
||||
|
@Column(name = API_KEY_USER_ID_COLUMN_NAME) |
||||
|
private UUID userId; |
||||
|
|
||||
|
@Column(name = API_KEY_EXPIRATION_TIME_COLUMN_NAME) |
||||
|
private long expirationTime; |
||||
|
|
||||
|
@Column(name = API_KEY_ENABLED_COLUMN_NAME) |
||||
|
private boolean enabled; |
||||
|
|
||||
|
@Column(name = API_KEY_DESCRIPTION_COLUMN_NAME) |
||||
|
private String description; |
||||
|
|
||||
|
public AbstractApiKeyInfoEntity() { |
||||
|
super(); |
||||
|
} |
||||
|
|
||||
|
public AbstractApiKeyInfoEntity(ApiKeyInfo apiKeyInfo) { |
||||
|
super(apiKeyInfo); |
||||
|
this.tenantId = apiKeyInfo.getTenantId().getId(); |
||||
|
this.userId = apiKeyInfo.getUserId().getId(); |
||||
|
this.expirationTime = apiKeyInfo.getExpirationTime(); |
||||
|
this.description = apiKeyInfo.getDescription(); |
||||
|
this.enabled = apiKeyInfo.isEnabled(); |
||||
|
} |
||||
|
|
||||
|
protected ApiKeyInfo toApiKeyInfo() { |
||||
|
ApiKeyInfo apiKeyInfo = new ApiKeyInfo(new ApiKeyId(getUuid())); |
||||
|
apiKeyInfo.setCreatedTime(createdTime); |
||||
|
apiKeyInfo.setTenantId(TenantId.fromUUID(tenantId)); |
||||
|
apiKeyInfo.setUserId(new UserId(userId)); |
||||
|
apiKeyInfo.setEnabled(enabled); |
||||
|
apiKeyInfo.setExpirationTime(expirationTime); |
||||
|
apiKeyInfo.setDescription(description); |
||||
|
return apiKeyInfo; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,51 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2025 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.dao.model.sql; |
||||
|
|
||||
|
import jakarta.persistence.Column; |
||||
|
import jakarta.persistence.Entity; |
||||
|
import jakarta.persistence.Table; |
||||
|
import lombok.Data; |
||||
|
import lombok.EqualsAndHashCode; |
||||
|
import org.thingsboard.server.common.data.pat.ApiKey; |
||||
|
|
||||
|
import static org.thingsboard.server.dao.model.ModelConstants.API_KEY_TABLE_NAME; |
||||
|
import static org.thingsboard.server.dao.model.ModelConstants.API_KEY_VALUE_COLUMN_NAME; |
||||
|
|
||||
|
@Data |
||||
|
@EqualsAndHashCode(callSuper = true) |
||||
|
@Entity |
||||
|
@Table(name = API_KEY_TABLE_NAME) |
||||
|
public class ApiKeyEntity extends AbstractApiKeyInfoEntity<ApiKey> { |
||||
|
|
||||
|
@Column(name = API_KEY_VALUE_COLUMN_NAME) |
||||
|
private String value; |
||||
|
|
||||
|
public ApiKeyEntity() { |
||||
|
super(); |
||||
|
} |
||||
|
|
||||
|
public ApiKeyEntity(ApiKey apiKey) { |
||||
|
super(apiKey); |
||||
|
this.value = apiKey.getValue(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public ApiKey toData() { |
||||
|
return new ApiKey(super.toApiKeyInfo(), value); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,46 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2025 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.dao.model.sql; |
||||
|
|
||||
|
import jakarta.persistence.Entity; |
||||
|
import jakarta.persistence.Table; |
||||
|
import lombok.Data; |
||||
|
import lombok.EqualsAndHashCode; |
||||
|
import org.thingsboard.server.common.data.pat.ApiKey; |
||||
|
import org.thingsboard.server.common.data.pat.ApiKeyInfo; |
||||
|
|
||||
|
import static org.thingsboard.server.dao.model.ModelConstants.API_KEY_TABLE_NAME; |
||||
|
|
||||
|
@Data |
||||
|
@EqualsAndHashCode(callSuper = true) |
||||
|
@Entity |
||||
|
@Table(name = API_KEY_TABLE_NAME) |
||||
|
public class ApiKeyInfoEntity extends AbstractApiKeyInfoEntity<ApiKeyInfo> { |
||||
|
|
||||
|
public ApiKeyInfoEntity() { |
||||
|
super(); |
||||
|
} |
||||
|
|
||||
|
public ApiKeyInfoEntity(ApiKey apiKey) { |
||||
|
super(apiKey); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public ApiKeyInfo toData() { |
||||
|
return super.toApiKeyInfo(); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,40 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2025 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.dao.pat; |
||||
|
|
||||
|
import org.checkerframework.checker.nullness.qual.NonNull; |
||||
|
|
||||
|
import java.io.Serializable; |
||||
|
|
||||
|
import static java.util.Objects.requireNonNull; |
||||
|
|
||||
|
record ApiKeyCacheKey(String value) implements Serializable { |
||||
|
|
||||
|
ApiKeyCacheKey { |
||||
|
requireNonNull(value); |
||||
|
} |
||||
|
|
||||
|
static ApiKeyCacheKey of(String value) { |
||||
|
return new ApiKeyCacheKey(value); |
||||
|
} |
||||
|
|
||||
|
@NonNull |
||||
|
@Override |
||||
|
public String toString() { |
||||
|
return /* cache name */ "_" + value; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,33 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2025 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.dao.pat; |
||||
|
|
||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
||||
|
import org.springframework.cache.CacheManager; |
||||
|
import org.springframework.stereotype.Service; |
||||
|
import org.thingsboard.server.cache.CaffeineTbTransactionalCache; |
||||
|
import org.thingsboard.server.common.data.CacheConstants; |
||||
|
import org.thingsboard.server.common.data.pat.ApiKey; |
||||
|
|
||||
|
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) |
||||
|
@Service("ApiKeyCache") |
||||
|
public class ApiKeyCaffeineCache extends CaffeineTbTransactionalCache<ApiKeyCacheKey, ApiKey> { |
||||
|
|
||||
|
public ApiKeyCaffeineCache(CacheManager cacheManager) { |
||||
|
super(cacheManager, CacheConstants.API_KEYS_CACHE); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,35 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2025 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.dao.pat; |
||||
|
|
||||
|
import org.thingsboard.server.common.data.id.TenantId; |
||||
|
import org.thingsboard.server.common.data.id.UserId; |
||||
|
import org.thingsboard.server.common.data.pat.ApiKey; |
||||
|
import org.thingsboard.server.dao.Dao; |
||||
|
|
||||
|
import java.util.Set; |
||||
|
|
||||
|
public interface ApiKeyDao extends Dao<ApiKey> { |
||||
|
|
||||
|
ApiKey findByValue(String value); |
||||
|
|
||||
|
Set<String> deleteByTenantId(TenantId tenantId); |
||||
|
|
||||
|
Set<String> deleteByUserId(TenantId tenantId, UserId userId); |
||||
|
|
||||
|
int deleteAllByExpirationTimeBefore(long ts); |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,18 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2025 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.dao.pat; |
||||
|
|
||||
|
public record ApiKeyEvictEvent(String value) {} |
||||
@ -0,0 +1,29 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2025 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.dao.pat; |
||||
|
|
||||
|
import org.thingsboard.server.common.data.id.TenantId; |
||||
|
import org.thingsboard.server.common.data.id.UserId; |
||||
|
import org.thingsboard.server.common.data.page.PageData; |
||||
|
import org.thingsboard.server.common.data.page.PageLink; |
||||
|
import org.thingsboard.server.common.data.pat.ApiKeyInfo; |
||||
|
import org.thingsboard.server.dao.Dao; |
||||
|
|
||||
|
public interface ApiKeyInfoDao extends Dao<ApiKeyInfo> { |
||||
|
|
||||
|
PageData<ApiKeyInfo> findByUserId(TenantId tenantId, UserId userId, PageLink pageLink); |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,36 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2025 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.dao.pat; |
||||
|
|
||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
||||
|
import org.springframework.data.redis.connection.RedisConnectionFactory; |
||||
|
import org.springframework.stereotype.Service; |
||||
|
import org.thingsboard.server.cache.CacheSpecsMap; |
||||
|
import org.thingsboard.server.cache.RedisTbTransactionalCache; |
||||
|
import org.thingsboard.server.cache.TBRedisCacheConfiguration; |
||||
|
import org.thingsboard.server.cache.TbJsonRedisSerializer; |
||||
|
import org.thingsboard.server.common.data.CacheConstants; |
||||
|
import org.thingsboard.server.common.data.pat.ApiKey; |
||||
|
|
||||
|
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") |
||||
|
@Service("ApiKeyCache") |
||||
|
public class ApiKeyRedisCache extends RedisTbTransactionalCache<ApiKeyCacheKey, ApiKey> { |
||||
|
|
||||
|
public ApiKeyRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { |
||||
|
super(CacheConstants.API_KEYS_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbJsonRedisSerializer<>(ApiKey.class)); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,163 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2025 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.dao.pat; |
||||
|
|
||||
|
import com.google.common.util.concurrent.FluentFuture; |
||||
|
import lombok.RequiredArgsConstructor; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.springframework.beans.factory.annotation.Value; |
||||
|
import org.springframework.context.annotation.Lazy; |
||||
|
import org.springframework.stereotype.Service; |
||||
|
import org.springframework.transaction.event.TransactionalEventListener; |
||||
|
import org.thingsboard.server.common.data.EntityType; |
||||
|
import org.thingsboard.server.common.data.StringUtils; |
||||
|
import org.thingsboard.server.common.data.id.ApiKeyId; |
||||
|
import org.thingsboard.server.common.data.id.EntityId; |
||||
|
import org.thingsboard.server.common.data.id.HasId; |
||||
|
import org.thingsboard.server.common.data.id.TenantId; |
||||
|
import org.thingsboard.server.common.data.id.UserId; |
||||
|
import org.thingsboard.server.common.data.page.PageData; |
||||
|
import org.thingsboard.server.common.data.page.PageLink; |
||||
|
import org.thingsboard.server.common.data.pat.ApiKey; |
||||
|
import org.thingsboard.server.common.data.pat.ApiKeyInfo; |
||||
|
import org.thingsboard.server.dao.entity.AbstractCachedEntityService; |
||||
|
import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; |
||||
|
import org.thingsboard.server.dao.service.validator.ApiKeyDataValidator; |
||||
|
|
||||
|
import java.util.Optional; |
||||
|
import java.util.Set; |
||||
|
import java.util.UUID; |
||||
|
|
||||
|
import static com.google.common.util.concurrent.MoreExecutors.directExecutor; |
||||
|
import static org.thingsboard.server.dao.service.Validator.validateId; |
||||
|
import static org.thingsboard.server.dao.user.UserServiceImpl.INCORRECT_TENANT_ID; |
||||
|
import static org.thingsboard.server.dao.user.UserServiceImpl.INCORRECT_USER_ID; |
||||
|
|
||||
|
@Slf4j |
||||
|
@Service |
||||
|
@RequiredArgsConstructor |
||||
|
public class ApiKeyServiceImpl extends AbstractCachedEntityService<ApiKeyCacheKey, ApiKey, ApiKeyEvictEvent> implements ApiKeyService { |
||||
|
|
||||
|
private static final String INCORRECT_API_KEY_ID = "Incorrect ApiKeyId "; |
||||
|
private static final int MAX_API_KEY_VALUE_LENGTH = 255; |
||||
|
|
||||
|
private final ApiKeyDao apiKeyDao; |
||||
|
private final ApiKeyInfoDao apiKeyInfoDao; |
||||
|
@Lazy |
||||
|
private final ApiKeyDataValidator apiKeyValidator; |
||||
|
|
||||
|
@Value("${security.api_key.value_prefix:}") |
||||
|
private String prefix; |
||||
|
|
||||
|
@Value("${security.api_key.value_bytes_size:64}") |
||||
|
private int valueBytesSize; |
||||
|
|
||||
|
@Override |
||||
|
@TransactionalEventListener |
||||
|
public void handleEvictEvent(ApiKeyEvictEvent event) { |
||||
|
cache.evict(ApiKeyCacheKey.of(event.value())); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public ApiKey saveApiKey(TenantId tenantId, ApiKeyInfo apiKeyInfo) { |
||||
|
log.trace("Executing saveApiKey [{}]", apiKeyInfo); |
||||
|
try { |
||||
|
var apiKey = new ApiKey(apiKeyInfo); |
||||
|
var old = apiKeyValidator.validate(apiKey, ApiKeyInfo::getTenantId); |
||||
|
if (old == null) { |
||||
|
String value = generateApiKeySecret(); |
||||
|
apiKey.setValue(value); |
||||
|
} else { |
||||
|
apiKey.setValue(old.getValue()); |
||||
|
} |
||||
|
var savedApiKey = apiKeyDao.save(tenantId, apiKey); |
||||
|
eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId).entityId(savedApiKey.getId()).entity(savedApiKey).created(apiKey.getId() == null).build()); |
||||
|
if (old != null && old.isEnabled() != apiKey.isEnabled()) { |
||||
|
publishEvictEvent(new ApiKeyEvictEvent(apiKey.getValue())); |
||||
|
} |
||||
|
return savedApiKey; |
||||
|
} catch (Exception e) { |
||||
|
checkConstraintViolation(e, "api_key_value_unq_key", "API Key with such value already exists!"); |
||||
|
throw e; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public ApiKey findApiKeyById(TenantId tenantId, ApiKeyId apiKeyId) { |
||||
|
log.trace("Executing findApiKeyById [{}] [{}]", tenantId, apiKeyId); |
||||
|
validateId(apiKeyId, id -> INCORRECT_API_KEY_ID + id); |
||||
|
return apiKeyDao.findById(tenantId, apiKeyId.getId()); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public PageData<ApiKeyInfo> findApiKeysByUserId(TenantId tenantId, UserId userId, PageLink pageLink) { |
||||
|
log.trace("Executing findApiKeysByUserId [{}][{}]", tenantId, userId); |
||||
|
validateId(userId, id -> INCORRECT_USER_ID + id); |
||||
|
return apiKeyInfoDao.findByUserId(tenantId, userId, pageLink); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public Optional<HasId<?>> findEntity(TenantId tenantId, EntityId entityId) { |
||||
|
return Optional.ofNullable(findApiKeyById(tenantId, new ApiKeyId(entityId.getId()))); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public FluentFuture<Optional<HasId<?>>> findEntityAsync(TenantId tenantId, EntityId entityId) { |
||||
|
return FluentFuture.from(apiKeyDao.findByIdAsync(tenantId, entityId.getId())) |
||||
|
.transform(Optional::ofNullable, directExecutor()); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void deleteApiKey(TenantId tenantId, ApiKey apiKey, boolean force) { |
||||
|
UUID apiKeyId = apiKey.getUuidId(); |
||||
|
validateId(apiKeyId, id -> INCORRECT_API_KEY_ID + id); |
||||
|
apiKeyDao.removeById(tenantId, apiKeyId); |
||||
|
publishEvictEvent(new ApiKeyEvictEvent(apiKey.getValue())); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void deleteByTenantId(TenantId tenantId) { |
||||
|
log.trace("Executing deleteApiKeysByTenantId, tenantId [{}]", tenantId); |
||||
|
validateId(tenantId, id -> INCORRECT_TENANT_ID + id); |
||||
|
Set<String> values = apiKeyDao.deleteByTenantId(tenantId); |
||||
|
values.forEach(value -> publishEvictEvent(new ApiKeyEvictEvent(value))); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void deleteByUserId(TenantId tenantId, UserId userId) { |
||||
|
log.trace("Executing deleteApiKeysByUserId, tenantId [{}]", tenantId); |
||||
|
validateId(userId, id -> INCORRECT_USER_ID + id); |
||||
|
Set<String> values = apiKeyDao.deleteByUserId(tenantId, userId); |
||||
|
values.forEach(value -> publishEvictEvent(new ApiKeyEvictEvent(value))); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public ApiKey findApiKeyByValue(String value) { |
||||
|
log.trace("Executing findApiKeyByValue [{}]", value); |
||||
|
var cacheKey = ApiKeyCacheKey.of(value); |
||||
|
return cache.getAndPutInTransaction(cacheKey, () -> apiKeyDao.findByValue(value), true); |
||||
|
} |
||||
|
|
||||
|
private String generateApiKeySecret() { |
||||
|
return prefix + StringUtils.generateSafeToken(Math.min(valueBytesSize, MAX_API_KEY_VALUE_LENGTH)); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public EntityType getEntityType() { |
||||
|
return EntityType.API_KEY; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,77 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2025 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.dao.service.validator; |
||||
|
|
||||
|
import lombok.RequiredArgsConstructor; |
||||
|
import org.springframework.stereotype.Component; |
||||
|
import org.thingsboard.server.common.data.id.TenantId; |
||||
|
import org.thingsboard.server.common.data.pat.ApiKey; |
||||
|
import org.thingsboard.server.dao.exception.DataValidationException; |
||||
|
import org.thingsboard.server.dao.pat.ApiKeyDao; |
||||
|
import org.thingsboard.server.dao.service.DataValidator; |
||||
|
import org.thingsboard.server.dao.tenant.TenantService; |
||||
|
import org.thingsboard.server.dao.user.UserService; |
||||
|
|
||||
|
@Component |
||||
|
@RequiredArgsConstructor |
||||
|
public class ApiKeyDataValidator extends DataValidator<ApiKey> { |
||||
|
|
||||
|
private final ApiKeyDao apiKeyDao; |
||||
|
private final TenantService tenantService; |
||||
|
private final UserService userService; |
||||
|
|
||||
|
@Override |
||||
|
protected void validateDataImpl(TenantId tenantId, ApiKey apiKey) { |
||||
|
if (apiKey.getId() != null) { |
||||
|
if (apiKey.getUuidId() == null) { |
||||
|
throw new DataValidationException("API Key UUID should be specified!"); |
||||
|
} |
||||
|
if (apiKey.getId().isNullUid()) { |
||||
|
throw new DataValidationException("API key UUID must not be the reserved null value!"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (apiKey.getTenantId() == null || apiKey.getTenantId().getId() == null) { |
||||
|
throw new DataValidationException("API key should be assigned to tenant!"); |
||||
|
} |
||||
|
if (!TenantId.SYS_TENANT_ID.equals(apiKey.getTenantId()) && !tenantService.tenantExists(apiKey.getTenantId())) { |
||||
|
throw new DataValidationException("API key reference a non-existent tenant!"); |
||||
|
} |
||||
|
|
||||
|
if (apiKey.getUserId() == null || apiKey.getUserId().getId() == null) { |
||||
|
throw new DataValidationException("API key should be assigned to user!"); |
||||
|
} |
||||
|
if (userService.findUserById(apiKey.getTenantId(), apiKey.getUserId()) == null) { |
||||
|
throw new DataValidationException("API key reference a non-existent user!"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
protected ApiKey validateUpdate(TenantId tenantId, ApiKey apiKey) { |
||||
|
ApiKey old = apiKeyDao.findById(tenantId, apiKey.getUuidId()); |
||||
|
if (old == null) { |
||||
|
throw new DataValidationException("Cannot update non-existent API key!"); |
||||
|
} |
||||
|
if (!old.getUserId().equals(apiKey.getUserId())) { |
||||
|
throw new DataValidationException("Cannot update API key user id!"); |
||||
|
} |
||||
|
if (old.getExpirationTime() != apiKey.getExpirationTime()) { |
||||
|
throw new DataValidationException("Cannot update API key expiration time!"); |
||||
|
} |
||||
|
return old; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,36 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2025 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.dao.sql.pat; |
||||
|
|
||||
|
import org.springframework.data.domain.Page; |
||||
|
import org.springframework.data.domain.Pageable; |
||||
|
import org.springframework.data.jpa.repository.JpaRepository; |
||||
|
import org.springframework.data.jpa.repository.Query; |
||||
|
import org.springframework.data.repository.query.Param; |
||||
|
import org.thingsboard.server.dao.model.sql.ApiKeyInfoEntity; |
||||
|
|
||||
|
import java.util.UUID; |
||||
|
|
||||
|
public interface ApiKeyInfoRepository extends JpaRepository<ApiKeyInfoEntity, UUID> { |
||||
|
|
||||
|
@Query("SELECT ak FROM ApiKeyInfoEntity ak WHERE ak.tenantId = :tenantId AND ak.userId = :userId AND " + |
||||
|
"(:searchText is NULL OR ilike(ak.description, concat('%', :searchText, '%')) = true)") |
||||
|
Page<ApiKeyInfoEntity> findByUserId(@Param("tenantId") UUID tenantId, |
||||
|
@Param("userId") UUID userId, |
||||
|
@Param("searchText") String searchText, |
||||
|
Pageable pageable); |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,58 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2025 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.dao.sql.pat; |
||||
|
|
||||
|
import org.springframework.data.jpa.repository.JpaRepository; |
||||
|
import org.springframework.data.jpa.repository.Modifying; |
||||
|
import org.springframework.data.jpa.repository.Query; |
||||
|
import org.springframework.data.repository.query.Param; |
||||
|
import org.springframework.transaction.annotation.Transactional; |
||||
|
import org.thingsboard.server.dao.model.sql.ApiKeyEntity; |
||||
|
|
||||
|
import java.util.Set; |
||||
|
import java.util.UUID; |
||||
|
|
||||
|
public interface ApiKeyRepository extends JpaRepository<ApiKeyEntity, UUID> { |
||||
|
|
||||
|
ApiKeyEntity findByValue(String value); |
||||
|
|
||||
|
@Transactional |
||||
|
@Modifying |
||||
|
@Query(value = """ |
||||
|
DELETE FROM api_key |
||||
|
WHERE tenant_id = :tenantId |
||||
|
RETURNING value |
||||
|
""", nativeQuery = true |
||||
|
) |
||||
|
Set<String> deleteByTenantId(@Param("tenantId") UUID tenantId); |
||||
|
|
||||
|
@Transactional |
||||
|
@Modifying |
||||
|
@Query(value = """ |
||||
|
DELETE FROM api_key |
||||
|
WHERE tenant_id = :tenantId AND user_id = :userId |
||||
|
RETURNING value |
||||
|
""", nativeQuery = true |
||||
|
) |
||||
|
Set<String> deleteByUserId(@Param("tenantId") UUID tenantId, |
||||
|
@Param("userId") UUID userId); |
||||
|
|
||||
|
@Transactional |
||||
|
@Modifying |
||||
|
@Query("DELETE FROM ApiKeyEntity ak WHERE ak.expirationTime > 0 AND ak.expirationTime < :ts") |
||||
|
int deleteAllByExpirationTimeBefore(@Param("ts") long ts); |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,78 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2025 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.dao.sql.pat; |
||||
|
|
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.data.jpa.repository.JpaRepository; |
||||
|
import org.springframework.stereotype.Component; |
||||
|
import org.thingsboard.server.common.data.EntityType; |
||||
|
import org.thingsboard.server.common.data.id.TenantId; |
||||
|
import org.thingsboard.server.common.data.id.UserId; |
||||
|
import org.thingsboard.server.common.data.pat.ApiKey; |
||||
|
import org.thingsboard.server.dao.DaoUtil; |
||||
|
import org.thingsboard.server.dao.model.sql.ApiKeyEntity; |
||||
|
import org.thingsboard.server.dao.pat.ApiKeyDao; |
||||
|
import org.thingsboard.server.dao.sql.JpaAbstractDao; |
||||
|
import org.thingsboard.server.dao.util.SqlDao; |
||||
|
|
||||
|
import java.util.Set; |
||||
|
import java.util.UUID; |
||||
|
|
||||
|
@Slf4j |
||||
|
@SqlDao |
||||
|
@Component |
||||
|
public class JpaApiKeyDao extends JpaAbstractDao<ApiKeyEntity, ApiKey> implements ApiKeyDao { |
||||
|
|
||||
|
@Autowired |
||||
|
private ApiKeyRepository apiKeyRepository; |
||||
|
|
||||
|
@Override |
||||
|
public ApiKey findByValue(String value) { |
||||
|
return DaoUtil.getData(apiKeyRepository.findByValue(value)); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public Set<String> deleteByTenantId(TenantId tenantId) { |
||||
|
return apiKeyRepository.deleteByTenantId(tenantId.getId()); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public Set<String> deleteByUserId(TenantId tenantId, UserId userId) { |
||||
|
return apiKeyRepository.deleteByUserId(tenantId.getId(), userId.getId()); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public int deleteAllByExpirationTimeBefore(long ts) { |
||||
|
return apiKeyRepository.deleteAllByExpirationTimeBefore(ts); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
protected Class<ApiKeyEntity> getEntityClass() { |
||||
|
return ApiKeyEntity.class; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
protected JpaRepository<ApiKeyEntity, UUID> getRepository() { |
||||
|
return apiKeyRepository; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public EntityType getEntityType() { |
||||
|
return EntityType.API_KEY; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,58 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2025 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.dao.sql.pat; |
||||
|
|
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.data.jpa.repository.JpaRepository; |
||||
|
import org.springframework.stereotype.Component; |
||||
|
import org.thingsboard.server.common.data.id.TenantId; |
||||
|
import org.thingsboard.server.common.data.id.UserId; |
||||
|
import org.thingsboard.server.common.data.page.PageData; |
||||
|
import org.thingsboard.server.common.data.page.PageLink; |
||||
|
import org.thingsboard.server.common.data.pat.ApiKeyInfo; |
||||
|
import org.thingsboard.server.dao.DaoUtil; |
||||
|
import org.thingsboard.server.dao.model.sql.ApiKeyInfoEntity; |
||||
|
import org.thingsboard.server.dao.pat.ApiKeyInfoDao; |
||||
|
import org.thingsboard.server.dao.sql.JpaAbstractDao; |
||||
|
import org.thingsboard.server.dao.util.SqlDao; |
||||
|
|
||||
|
import java.util.UUID; |
||||
|
|
||||
|
@Slf4j |
||||
|
@SqlDao |
||||
|
@Component |
||||
|
public class JpaApiKeyInfoDao extends JpaAbstractDao<ApiKeyInfoEntity, ApiKeyInfo> implements ApiKeyInfoDao { |
||||
|
|
||||
|
@Autowired |
||||
|
private ApiKeyInfoRepository apiKeyInfoRepository; |
||||
|
|
||||
|
@Override |
||||
|
public PageData<ApiKeyInfo> findByUserId(TenantId tenantId, UserId userId, PageLink pageLink) { |
||||
|
return DaoUtil.toPageData(apiKeyInfoRepository.findByUserId(tenantId.getId(), userId.getId(), pageLink.getTextSearch(), DaoUtil.toPageable(pageLink))); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
protected Class<ApiKeyInfoEntity> getEntityClass() { |
||||
|
return ApiKeyInfoEntity.class; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
protected JpaRepository<ApiKeyInfoEntity, UUID> getRepository() { |
||||
|
return apiKeyInfoRepository; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,219 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2025 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.dao.service; |
||||
|
|
||||
|
import org.junit.After; |
||||
|
import org.junit.Assert; |
||||
|
import org.junit.Before; |
||||
|
import org.junit.Test; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.thingsboard.server.common.data.StringUtils; |
||||
|
import org.thingsboard.server.common.data.User; |
||||
|
import org.thingsboard.server.common.data.id.TenantId; |
||||
|
import org.thingsboard.server.common.data.id.UserId; |
||||
|
import org.thingsboard.server.common.data.page.PageData; |
||||
|
import org.thingsboard.server.common.data.page.PageLink; |
||||
|
import org.thingsboard.server.common.data.pat.ApiKey; |
||||
|
import org.thingsboard.server.common.data.pat.ApiKeyInfo; |
||||
|
import org.thingsboard.server.common.data.security.Authority; |
||||
|
import org.thingsboard.server.dao.exception.DataValidationException; |
||||
|
import org.thingsboard.server.dao.pat.ApiKeyService; |
||||
|
import org.thingsboard.server.dao.user.UserService; |
||||
|
|
||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy; |
||||
|
|
||||
|
@DaoSqlTest |
||||
|
public class ApiKeyServiceTest extends AbstractServiceTest { |
||||
|
|
||||
|
private static final String TEST_API_KEY_DESCRIPTION = "Test API Key Description"; |
||||
|
|
||||
|
@Autowired |
||||
|
ApiKeyService apiKeyService; |
||||
|
@Autowired |
||||
|
UserService userService; |
||||
|
|
||||
|
private UserId userId; |
||||
|
|
||||
|
@Before |
||||
|
public void before() { |
||||
|
User tenantAdmin = new User(); |
||||
|
tenantAdmin.setAuthority(Authority.TENANT_ADMIN); |
||||
|
tenantAdmin.setTenantId(tenantId); |
||||
|
tenantAdmin.setEmail("tenant@thingsboard.org"); |
||||
|
User user = userService.saveUser(TenantId.SYS_TENANT_ID, tenantAdmin); |
||||
|
userId = user.getId(); |
||||
|
} |
||||
|
|
||||
|
@After |
||||
|
public void after() { |
||||
|
apiKeyService.deleteByTenantId(tenantId); |
||||
|
User user = userService.findUserById(tenantId, userId); |
||||
|
userService.deleteUser(tenantId, user); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void testSaveApiKey() { |
||||
|
ApiKeyInfo apiKeyInfo = createApiKeyInfo(TEST_API_KEY_DESCRIPTION); |
||||
|
ApiKey savedApiKey = apiKeyService.saveApiKey(tenantId, apiKeyInfo); |
||||
|
|
||||
|
Assert.assertNotNull(savedApiKey); |
||||
|
Assert.assertNotNull(savedApiKey.getId()); |
||||
|
Assert.assertEquals(tenantId, savedApiKey.getTenantId()); |
||||
|
Assert.assertEquals(TEST_API_KEY_DESCRIPTION, savedApiKey.getDescription()); |
||||
|
Assert.assertTrue(savedApiKey.isEnabled()); |
||||
|
Assert.assertNotNull(savedApiKey.getValue()); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void testSaveApiKeyWithTooLongDescription() { |
||||
|
ApiKeyInfo apiKeyInfo = createApiKeyInfo(StringUtils.randomAlphabetic(300)); |
||||
|
|
||||
|
assertThatThrownBy(() -> apiKeyService.saveApiKey(tenantId, apiKeyInfo)) |
||||
|
.isInstanceOf(DataValidationException.class) |
||||
|
.hasMessageContaining("description length must be equal or less than 255"); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void testUpdateDescriptionApiKey() { |
||||
|
ApiKeyInfo apiKeyInfo = createApiKeyInfo(TEST_API_KEY_DESCRIPTION); |
||||
|
ApiKey savedApiKey = apiKeyService.saveApiKey(tenantId, apiKeyInfo); |
||||
|
|
||||
|
String newDescription = "Updated API Key Description"; |
||||
|
savedApiKey.setDescription(newDescription); |
||||
|
ApiKey updatedApiKey = apiKeyService.saveApiKey(tenantId, savedApiKey); |
||||
|
|
||||
|
Assert.assertNotNull(updatedApiKey); |
||||
|
Assert.assertEquals(savedApiKey.getId(), updatedApiKey.getId()); |
||||
|
Assert.assertEquals(newDescription, updatedApiKey.getDescription()); |
||||
|
Assert.assertEquals(savedApiKey.getValue(), updatedApiKey.getValue()); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void testDisableApiKey() { |
||||
|
ApiKeyInfo apiKeyInfo = createApiKeyInfo(TEST_API_KEY_DESCRIPTION); |
||||
|
ApiKey savedApiKey = apiKeyService.saveApiKey(tenantId, apiKeyInfo); |
||||
|
|
||||
|
savedApiKey.setEnabled(false); |
||||
|
ApiKey disabledApiKey = apiKeyService.saveApiKey(tenantId, savedApiKey); |
||||
|
|
||||
|
Assert.assertNotNull(disabledApiKey); |
||||
|
Assert.assertEquals(savedApiKey.getId(), disabledApiKey.getId()); |
||||
|
Assert.assertFalse(disabledApiKey.isEnabled()); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void testFindApiKeyById() { |
||||
|
ApiKeyInfo apiKeyInfo = createApiKeyInfo(TEST_API_KEY_DESCRIPTION); |
||||
|
ApiKey savedApiKey = apiKeyService.saveApiKey(tenantId, apiKeyInfo); |
||||
|
|
||||
|
ApiKey foundApiKey = apiKeyService.findApiKeyById(tenantId, savedApiKey.getId()); |
||||
|
|
||||
|
Assert.assertNotNull(foundApiKey); |
||||
|
Assert.assertEquals(savedApiKey.getId(), foundApiKey.getId()); |
||||
|
Assert.assertEquals(savedApiKey.getDescription(), foundApiKey.getDescription()); |
||||
|
Assert.assertEquals(savedApiKey.isEnabled(), foundApiKey.isEnabled()); |
||||
|
Assert.assertEquals(savedApiKey.getValue(), foundApiKey.getValue()); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void testFindApiKeyByHash() { |
||||
|
ApiKeyInfo apiKeyInfo = createApiKeyInfo(TEST_API_KEY_DESCRIPTION); |
||||
|
ApiKey savedApiKey = apiKeyService.saveApiKey(tenantId, apiKeyInfo); |
||||
|
|
||||
|
ApiKey foundApiKey = apiKeyService.findApiKeyByValue(savedApiKey.getValue()); |
||||
|
|
||||
|
Assert.assertNotNull(foundApiKey); |
||||
|
Assert.assertEquals(savedApiKey.getId(), foundApiKey.getId()); |
||||
|
Assert.assertEquals(savedApiKey.getDescription(), foundApiKey.getDescription()); |
||||
|
Assert.assertEquals(savedApiKey.isEnabled(), foundApiKey.isEnabled()); |
||||
|
Assert.assertEquals(savedApiKey.getValue(), foundApiKey.getValue()); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void testFindApiKeysByUserId() { |
||||
|
int size = 3; |
||||
|
for (int i = 0; i < size; i++) { |
||||
|
ApiKeyInfo apiKeyInfo = createApiKeyInfo("API Key " + i); |
||||
|
apiKeyService.saveApiKey(tenantId, apiKeyInfo); |
||||
|
} |
||||
|
|
||||
|
PageLink pageLink = new PageLink(10); |
||||
|
PageData<ApiKeyInfo> pageData = apiKeyService.findApiKeysByUserId(tenantId, userId, pageLink); |
||||
|
|
||||
|
Assert.assertNotNull(pageData); |
||||
|
Assert.assertEquals(size, pageData.getData().size()); |
||||
|
Assert.assertEquals(size, pageData.getTotalElements()); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void testDeleteApiKey() { |
||||
|
ApiKeyInfo apiKeyInfo = createApiKeyInfo(TEST_API_KEY_DESCRIPTION); |
||||
|
ApiKey savedApiKey = apiKeyService.saveApiKey(tenantId, apiKeyInfo); |
||||
|
|
||||
|
apiKeyService.deleteApiKey(tenantId, savedApiKey, false); |
||||
|
|
||||
|
ApiKey foundApiKey = apiKeyService.findApiKeyById(tenantId, savedApiKey.getId()); |
||||
|
Assert.assertNull(foundApiKey); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void testDeleteByTenantId() { |
||||
|
for (int i = 0; i < 3; i++) { |
||||
|
ApiKeyInfo apiKeyInfo = createApiKeyInfo("API Key " + i); |
||||
|
apiKeyService.saveApiKey(tenantId, apiKeyInfo); |
||||
|
} |
||||
|
|
||||
|
apiKeyService.deleteByTenantId(tenantId); |
||||
|
|
||||
|
PageLink pageLink = new PageLink(10); |
||||
|
PageData<ApiKeyInfo> pageData = apiKeyService.findApiKeysByUserId(tenantId, userId, pageLink); |
||||
|
|
||||
|
Assert.assertNotNull(pageData); |
||||
|
Assert.assertEquals(0, pageData.getData().size()); |
||||
|
Assert.assertEquals(0, pageData.getTotalElements()); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void testDeleteByUserId() { |
||||
|
int size = 3; |
||||
|
for (int i = 0; i < size; i++) { |
||||
|
ApiKeyInfo apiKeyInfo = createApiKeyInfo("API Key " + i); |
||||
|
apiKeyService.saveApiKey(tenantId, apiKeyInfo); |
||||
|
} |
||||
|
|
||||
|
PageData<ApiKeyInfo> pageData = apiKeyService.findApiKeysByUserId(tenantId, userId, new PageLink(10)); |
||||
|
Assert.assertNotNull(pageData); |
||||
|
Assert.assertEquals(size, pageData.getData().size()); |
||||
|
Assert.assertEquals(size, pageData.getTotalElements()); |
||||
|
|
||||
|
apiKeyService.deleteByUserId(tenantId, userId); |
||||
|
|
||||
|
pageData = apiKeyService.findApiKeysByUserId(tenantId, userId, new PageLink(10)); |
||||
|
Assert.assertNotNull(pageData); |
||||
|
Assert.assertEquals(0, pageData.getData().size()); |
||||
|
Assert.assertEquals(0, pageData.getTotalElements()); |
||||
|
} |
||||
|
|
||||
|
private ApiKeyInfo createApiKeyInfo(String description) { |
||||
|
ApiKeyInfo apiKeyInfo = new ApiKeyInfo(); |
||||
|
apiKeyInfo.setTenantId(tenantId); |
||||
|
apiKeyInfo.setUserId(userId); |
||||
|
apiKeyInfo.setDescription(description); |
||||
|
apiKeyInfo.setEnabled(true); |
||||
|
return apiKeyInfo; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,54 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2025 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.
|
||||
|
///
|
||||
|
|
||||
|
import { Injectable } from '@angular/core'; |
||||
|
import { HttpClient } from '@angular/common/http'; |
||||
|
import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils'; |
||||
|
import { Observable } from 'rxjs'; |
||||
|
import { PageLink } from '@shared/models/page/page-link'; |
||||
|
import { PageData } from '@shared/models/page/page-data'; |
||||
|
import { ApiKeyInfo, ApiKey } from '@shared/models/api-key.models'; |
||||
|
|
||||
|
@Injectable({ |
||||
|
providedIn: 'root' |
||||
|
}) |
||||
|
export class ApiKeyService { |
||||
|
|
||||
|
constructor( |
||||
|
private http: HttpClient |
||||
|
) { |
||||
|
} |
||||
|
|
||||
|
public saveApiKey(apiKey: ApiKeyInfo, config?: RequestConfig): Observable<ApiKey> { |
||||
|
return this.http.post<ApiKey>('/api/apiKey', apiKey, defaultHttpOptionsFromConfig(config)); |
||||
|
} |
||||
|
|
||||
|
public deleteApiKey(id: string, config?: RequestConfig): Observable<void> { |
||||
|
return this.http.delete<void>(`/api/apiKey/${id}`, defaultHttpOptionsFromConfig(config)); |
||||
|
} |
||||
|
|
||||
|
public updateApiKeyDescription(id: string, description: string, config?: RequestConfig): Observable<ApiKeyInfo> { |
||||
|
return this.http.put<ApiKeyInfo>(`/api/apiKey/${id}/description`, description, defaultHttpOptionsFromConfig(config)); |
||||
|
} |
||||
|
|
||||
|
public enableApiKey(id: string, enabledValue: boolean, config?: RequestConfig): Observable<ApiKeyInfo> { |
||||
|
return this.http.put<ApiKeyInfo>(`/api/apiKey/${id}/enabled/${enabledValue}`, defaultHttpOptionsFromConfig(config)); |
||||
|
} |
||||
|
|
||||
|
public getUserApiKeys(userId: string, pageLink: PageLink, config?: RequestConfig): Observable<PageData<ApiKeyInfo>> { |
||||
|
return this.http.get<PageData<ApiKeyInfo>>(`/api/apiKeys/${userId}${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,83 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2025 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. |
||||
|
|
||||
|
--> |
||||
|
<mat-toolbar class="min-w-0" color="primary"> |
||||
|
<h2>{{ 'api-key.generate-title' | translate }}</h2> |
||||
|
<span class="flex-1"></span> |
||||
|
<div tb-help="apiKeys"></div> |
||||
|
<button mat-icon-button |
||||
|
(click)="close()" |
||||
|
type="button"> |
||||
|
<mat-icon class="material-icons">close</mat-icon> |
||||
|
</button> |
||||
|
</mat-toolbar> |
||||
|
@if (isLoading$ | async) { |
||||
|
<mat-progress-bar color="warn" mode="indeterminate"></mat-progress-bar> |
||||
|
} |
||||
|
<div mat-dialog-content> |
||||
|
<section class="tb-form-panel no-border no-padding" [formGroup]="apiKeyForm"> |
||||
|
<div class="api-key-text"> |
||||
|
<span translate>api-key.generate-text</span> |
||||
|
</div> |
||||
|
<mat-form-field class="mat-block" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<mat-label translate>api-key.description</mat-label> |
||||
|
<textarea #input cdkTextareaAutosize matInput formControlName="description" rows="2" maxLength="255"></textarea> |
||||
|
</mat-form-field> |
||||
|
<mat-slide-toggle class="mat-slide" formControlName="enabled"> |
||||
|
{{ 'api-key.enable' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
<section class="flex gap-3"> |
||||
|
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="flex-1"> |
||||
|
<mat-select formControlName="expirationTime" aria-label="Expiration date selector" (selectionChange)="onExpirationDateChange()"> |
||||
|
@for (value of expirationTimeOptions; track value) { |
||||
|
<mat-option [value]="value"> |
||||
|
{{ value | dateExpiration }} |
||||
|
</mat-option> |
||||
|
} |
||||
|
<mat-option value="never">{{'api-key.expiration-time-never' | translate}}</mat-option> |
||||
|
<mat-option value="custom">{{'api-key.expiration-time-custom' | translate}}</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
@if (isCustomExpirationTime()) { |
||||
|
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<mat-label translate>api-key.date</mat-label> |
||||
|
<mat-datetimepicker-toggle [for]="datePicker" matSuffix></mat-datetimepicker-toggle> |
||||
|
<mat-datetimepicker #datePicker type="datetime" openOnFocus="true"></mat-datetimepicker> |
||||
|
<input matInput required formControlName="customExpirationTime" |
||||
|
[matDatetimepicker]="datePicker" |
||||
|
[min]="startDate"/> |
||||
|
</mat-form-field> |
||||
|
} |
||||
|
</section> |
||||
|
</section> |
||||
|
</div> |
||||
|
<div mat-dialog-actions class="flex items-center justify-end"> |
||||
|
<button mat-button color="primary" |
||||
|
type="button" |
||||
|
cdkFocusInitial |
||||
|
[disabled]="(isLoading$ | async)" |
||||
|
(click)="close()"> |
||||
|
{{ 'action.cancel' | translate }} |
||||
|
</button> |
||||
|
<button mat-raised-button color="primary" |
||||
|
type="submit" |
||||
|
(click)="add()" |
||||
|
[disabled]="(isLoading$ | async) || apiKeyForm?.invalid"> |
||||
|
{{ 'api-key.generate' | translate }} |
||||
|
</button> |
||||
|
</div> |
||||
|
|
||||
@ -0,0 +1,49 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2025 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. |
||||
|
*/ |
||||
|
@import '../../src/scss/constants'; |
||||
|
|
||||
|
:host { |
||||
|
display: grid; |
||||
|
width: 700px; |
||||
|
height: 100%; |
||||
|
max-width: 100%; |
||||
|
max-height: 100vh; |
||||
|
grid-template-rows: min-content 4px minmax(auto, 1fr) min-content; |
||||
|
|
||||
|
.mat-mdc-dialog-content { |
||||
|
grid-row: 3; |
||||
|
} |
||||
|
.mat-mdc-dialog-actions { |
||||
|
grid-row: 4; |
||||
|
} |
||||
|
.api-key-text { |
||||
|
position: relative; |
||||
|
padding: 8px 16px 8px 16px; |
||||
|
&::before { |
||||
|
content: ''; |
||||
|
position: absolute; |
||||
|
inset: 0; |
||||
|
background-color: $tb-primary-color; |
||||
|
border-radius: 6px; |
||||
|
opacity: 0.04; |
||||
|
} |
||||
|
|
||||
|
span { |
||||
|
font-size: 12px; |
||||
|
color: rgba(0, 0, 0, 0.54); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,103 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2025 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.
|
||||
|
///
|
||||
|
|
||||
|
|
||||
|
import { Component, Inject } from '@angular/core'; |
||||
|
import { DialogComponent } from '@shared/components/dialog.component'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { Router } from '@angular/router'; |
||||
|
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; |
||||
|
import { FormBuilder, Validators } from '@angular/forms'; |
||||
|
import { deepTrim } from '@core/utils'; |
||||
|
import { ApiKeyService } from '@core/http/api-key.service'; |
||||
|
import { ApiKeyInfo } from '@shared/models/api-key.models'; |
||||
|
import { ApiKeysTableDialogData } from '@home/components/api-key/api-keys-table-dialog.component'; |
||||
|
import { DAY } from '@shared/models/time/time.models'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-add-api-key-dialog', |
||||
|
templateUrl: './add-api-key-dialog.component.html', |
||||
|
styleUrls: ['./add-api-key-dialog.component.scss'] |
||||
|
}) |
||||
|
export class AddApiKeyDialogComponent extends DialogComponent<AddApiKeyDialogComponent, ApiKeyInfo | string> { |
||||
|
|
||||
|
readonly startDate = new Date(); |
||||
|
readonly expirationTimeOptions: Array<number> = [7, 30, 60, 90].map(days => days * DAY); |
||||
|
readonly apiKeyForm = this.fb.group({ |
||||
|
description: [{value: null, disabled: false}, [Validators.required]], |
||||
|
enabled: [{value: true, disabled: false}, []], |
||||
|
expirationTime: [{value: this.expirationTimeOptions[1] as string | number, disabled: false}, [Validators.required]], |
||||
|
customExpirationTime: [{value: null, disabled: true}, []], |
||||
|
}); |
||||
|
|
||||
|
constructor( |
||||
|
protected store: Store<AppState>, |
||||
|
protected router: Router, |
||||
|
public dialogRef: MatDialogRef<AddApiKeyDialogComponent, ApiKeyInfo | string>, |
||||
|
private fb: FormBuilder, |
||||
|
private apiKeyService: ApiKeyService, |
||||
|
@Inject(MAT_DIALOG_DATA) public data: ApiKeysTableDialogData, |
||||
|
) { |
||||
|
super(store, router, dialogRef); |
||||
|
} |
||||
|
|
||||
|
close(): void { |
||||
|
this.dialogRef.close(null); |
||||
|
} |
||||
|
|
||||
|
add(): void { |
||||
|
const formValue = this.apiKeyForm.value; |
||||
|
const userId = this.data.userId; |
||||
|
const expirationTime = this.calcExpirationTime(); |
||||
|
const apiKey = { |
||||
|
...deepTrim(formValue), |
||||
|
expirationTime, |
||||
|
userId, |
||||
|
} as ApiKeyInfo; |
||||
|
this.apiKeyService.saveApiKey(apiKey).subscribe( |
||||
|
(res) => { |
||||
|
this.dialogRef.close(res); |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
isCustomExpirationTime() { |
||||
|
return this.apiKeyForm.value?.expirationTime === 'custom'; |
||||
|
} |
||||
|
|
||||
|
onExpirationDateChange() { |
||||
|
const customExpirationTimeControl = this.apiKeyForm.get('customExpirationTime'); |
||||
|
if (this.isCustomExpirationTime()) { |
||||
|
customExpirationTimeControl.enable({emitEvent: false}); |
||||
|
} else { |
||||
|
customExpirationTimeControl.disable({emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private calcExpirationTime(): number { |
||||
|
const expirationTimeValue = this.apiKeyForm.get('expirationTime').value; |
||||
|
let value: number; |
||||
|
if (this.isCustomExpirationTime()) { |
||||
|
value = this.apiKeyForm.get('customExpirationTime').value.getTime(); |
||||
|
} else if (expirationTimeValue === 'never') { |
||||
|
value = 0; |
||||
|
} else { |
||||
|
value = expirationTimeValue as number + Date.now(); |
||||
|
} |
||||
|
return value; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,101 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2025 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. |
||||
|
|
||||
|
--> |
||||
|
<mat-toolbar class="min-w-0" color="primary"> |
||||
|
<h2>{{ 'api-key.generated-api-key-title' | translate }}</h2> |
||||
|
<span class="flex-1"></span> |
||||
|
<button mat-icon-button |
||||
|
(click)="close()" |
||||
|
type="button"> |
||||
|
<mat-icon class="material-icons">close</mat-icon> |
||||
|
</button> |
||||
|
</mat-toolbar> |
||||
|
<div mat-dialog-content> |
||||
|
<div class="tb-form-panel no-padding no-border"> |
||||
|
<div class="tb-no-data-text font-normal" translate>api-key.generated-api-key-copy</div> |
||||
|
<div class="tb-form-panel no-padding no-border"> |
||||
|
<tb-markdown usePlainMarkdown containerClass="tb-command-code" |
||||
|
[data]='createMarkDownCommand(data.apiKey.value)'></tb-markdown> |
||||
|
</div> |
||||
|
<div class="tb-form-panel no-padding no-border tb-tab-body"> |
||||
|
<div class="tb-no-data-text font-normal" translate>api-key.generated-api-key-command</div> |
||||
|
<mat-tab-group [selectedIndex]="selectedTab"> |
||||
|
<mat-tab> |
||||
|
<ng-template mat-tab-label> |
||||
|
<mat-icon class="tabs-icon" svgIcon="windows"></mat-icon> |
||||
|
Windows |
||||
|
</ng-template> |
||||
|
<ng-template matTabContent> |
||||
|
<div class="tb-form-panel no-padding no-border tb-tab-body"> |
||||
|
<div class="tb-form-panel stroked"> |
||||
|
<div class="tb-form-panel-title" translate>device.connectivity.install-necessary-client-tools</div> |
||||
|
<div class="tb-install-instruction-text" translate>device.connectivity.install-curl-windows</div> |
||||
|
</div> |
||||
|
<ng-container *ngTemplateOutlet="executeCommand"></ng-container> |
||||
|
</div> |
||||
|
</ng-template> |
||||
|
</mat-tab> |
||||
|
<mat-tab> |
||||
|
<ng-template mat-tab-label> |
||||
|
<mat-icon class="tabs-icon" svgIcon="macos"></mat-icon> |
||||
|
MacOS |
||||
|
</ng-template> |
||||
|
<ng-template matTabContent> |
||||
|
<div class="tb-form-panel no-padding no-border tb-tab-body"> |
||||
|
<div class="tb-form-panel stroked"> |
||||
|
<div class="tb-form-panel-title" translate>device.connectivity.install-necessary-client-tools</div> |
||||
|
<div class="tb-install-instruction-text" translate>device.connectivity.install-curl-macos</div> |
||||
|
</div> |
||||
|
<ng-container *ngTemplateOutlet="executeCommand"></ng-container> |
||||
|
</div> |
||||
|
</ng-template> |
||||
|
</mat-tab> |
||||
|
<mat-tab> |
||||
|
<ng-template mat-tab-label> |
||||
|
<mat-icon class="tabs-icon" svgIcon="linux"></mat-icon> |
||||
|
Linux |
||||
|
</ng-template> |
||||
|
<ng-template matTabContent> |
||||
|
<div class="tb-form-panel no-padding no-border tb-tab-body"> |
||||
|
<div class="tb-form-panel stroked"> |
||||
|
<div class="tb-form-panel-title" translate>device.connectivity.install-necessary-client-tools</div> |
||||
|
<tb-markdown usePlainMarkdown containerClass="tb-command-code" |
||||
|
[data]='createMarkDownCommand("sudo apt-get install curl")'></tb-markdown> |
||||
|
</div> |
||||
|
<ng-container *ngTemplateOutlet="executeCommand"></ng-container> |
||||
|
</div> |
||||
|
</ng-template> |
||||
|
</mat-tab> |
||||
|
</mat-tab-group> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div mat-dialog-actions class="justify-end"> |
||||
|
<button mat-raised-button color="primary" |
||||
|
type="button" |
||||
|
[disabled]="(isLoading$ | async)" |
||||
|
(click)="close()"> |
||||
|
{{ 'action.close' | translate }} |
||||
|
</button> |
||||
|
</div> |
||||
|
|
||||
|
<ng-template #executeCommand> |
||||
|
<div class="tb-form-panel stroked"> |
||||
|
<div class="tb-form-panel-title" translate>device.connectivity.execute-following-command</div> |
||||
|
<tb-markdown usePlainMarkdown containerClass="tb-command-code" [data]='createMarkDownCommand(apiKeyCommand)'></tb-markdown> |
||||
|
</div> |
||||
|
</ng-template> |
||||
@ -0,0 +1,95 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2025 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. |
||||
|
*/ |
||||
|
@import '../../src/scss/constants'; |
||||
|
|
||||
|
:host{ |
||||
|
display: grid; |
||||
|
width: 500px; |
||||
|
height: 100%; |
||||
|
max-width: 100%; |
||||
|
max-height: 100vh; |
||||
|
grid-template-rows: min-content minmax(auto, 1fr) min-content; |
||||
|
|
||||
|
.tb-install-instruction-text { |
||||
|
min-height: 42px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
:host ::ng-deep { |
||||
|
.tb-markdown-view { |
||||
|
.tb-command-code { |
||||
|
.code-wrapper { |
||||
|
padding: 0; |
||||
|
pre[class*=language-] { |
||||
|
margin: 0; |
||||
|
background: #F3F6FA; |
||||
|
border-color: $tb-primary-color; |
||||
|
padding-right: 38px; |
||||
|
overflow: hidden; |
||||
|
overflow-x: auto; |
||||
|
padding-bottom: 4px; |
||||
|
min-height: 42px; |
||||
|
scrollbar-width: thin; |
||||
|
|
||||
|
&::-webkit-scrollbar { |
||||
|
width: 4px; |
||||
|
height: 4px; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
button.clipboard-btn { |
||||
|
right: -2px; |
||||
|
p { |
||||
|
color: $tb-primary-color; |
||||
|
} |
||||
|
p, div { |
||||
|
background-color: #F3F6FA; |
||||
|
} |
||||
|
div { |
||||
|
img { |
||||
|
display: none; |
||||
|
} |
||||
|
&:after { |
||||
|
content: ""; |
||||
|
position: initial; |
||||
|
display: block; |
||||
|
width: 18px; |
||||
|
height: 18px; |
||||
|
background: $tb-primary-color; |
||||
|
mask-image: url(/assets/copy-code-icon.svg); |
||||
|
-webkit-mask-image: url(/assets/copy-code-icon.svg); |
||||
|
mask-repeat: no-repeat; |
||||
|
-webkit-mask-repeat: no-repeat; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
.mdc-button__label > span { |
||||
|
.mat-icon { |
||||
|
vertical-align: text-bottom; |
||||
|
box-sizing: initial; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.tabs-icon { |
||||
|
margin-right: 8px; |
||||
|
} |
||||
|
|
||||
|
.tb-form-panel.tb-tab-body { |
||||
|
padding: 16px 0 0; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,77 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2025 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.
|
||||
|
///
|
||||
|
|
||||
|
import { Component, Inject } from '@angular/core'; |
||||
|
import { DialogComponent } from '@shared/components/dialog.component'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { Router } from '@angular/router'; |
||||
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; |
||||
|
import { userInfoCommand, ApiKey } from '@shared/models/api-key.models'; |
||||
|
import { getOS } from '@core/utils'; |
||||
|
|
||||
|
export interface ApiKeyGeneratedDialogData { |
||||
|
apiKey: ApiKey; |
||||
|
} |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-api-key-generated-dialog', |
||||
|
templateUrl: './api-key-generated-dialog.component.html', |
||||
|
styleUrls: ['api-key-generated-dialog.component.scss'] |
||||
|
}) |
||||
|
export class ApiKeyGeneratedDialogComponent extends DialogComponent<ApiKeyGeneratedDialogComponent, void> { |
||||
|
|
||||
|
apiKeyCommand = userInfoCommand(this.data.apiKey.value); |
||||
|
selectedTab: number; |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
protected router: Router, |
||||
|
protected dialogRef: MatDialogRef<ApiKeyGeneratedDialogComponent, void>, |
||||
|
@Inject(MAT_DIALOG_DATA) public data: ApiKeyGeneratedDialogData) { |
||||
|
super(store, router, dialogRef); |
||||
|
this.selectTabIndexForUserOS(); |
||||
|
} |
||||
|
|
||||
|
close(): void { |
||||
|
this.dialogRef.close(null); |
||||
|
} |
||||
|
|
||||
|
createMarkDownCommand(command: string): string { |
||||
|
return '```bash\n' + |
||||
|
command + |
||||
|
'{:copy-code}\n' + |
||||
|
'```'; |
||||
|
} |
||||
|
|
||||
|
private selectTabIndexForUserOS() { |
||||
|
const currentOS = getOS(); |
||||
|
switch (currentOS) { |
||||
|
case 'linux': |
||||
|
case 'android': |
||||
|
this.selectedTab = 2; |
||||
|
break; |
||||
|
case 'macos': |
||||
|
case 'ios': |
||||
|
this.selectedTab = 1; |
||||
|
break; |
||||
|
case 'windows': |
||||
|
this.selectedTab = 0; |
||||
|
break; |
||||
|
default: |
||||
|
this.selectedTab = 2; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue