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