From 99cfc07b14a690b977eb78a174482033ba37d164 Mon Sep 17 00:00:00 2001 From: AlexSanchez-bit Date: Thu, 16 Oct 2025 09:54:23 -0400 Subject: [PATCH 001/128] fix[frontend](app_settings): added GMT+12 and daylight saving options on date settings --- frontend/src/app/shared/constants/date-timezone-date.const.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/shared/constants/date-timezone-date.const.ts b/frontend/src/app/shared/constants/date-timezone-date.const.ts index ac2068658..ebb65546d 100644 --- a/frontend/src/app/shared/constants/date-timezone-date.const.ts +++ b/frontend/src/app/shared/constants/date-timezone-date.const.ts @@ -25,6 +25,8 @@ export const TIMEZONES: Array<{ label: string; timezone: string, zone: string }> {label: 'Sydney (AEST)', timezone: 'Australia/Sydney', zone: 'Australia'}, {label: 'Melbourne (AEST)', timezone: 'Australia/Melbourne', zone: 'Australia'}, {label: 'Perth (AWST)', timezone: 'Australia/Perth', zone: 'Australia'}, + {label: 'New Zealand (NZST)', timezone: 'Pacific/Auckland', zone: 'Pacific'}, + {label: 'Fiji (FJT)', timezone: 'Pacific/Fiji', zone: 'Pacific'}, {label: 'Beijing (CST)', timezone: 'Asia/Shanghai', zone: 'Asia'}, {label: 'Tokyo (JST)', timezone: 'Asia/Tokyo', zone: 'Asia'}, {label: 'Seoul (KST)', timezone: 'Asia/Seoul', zone: 'Asia'}, @@ -37,7 +39,6 @@ export const TIMEZONES: Array<{ label: string; timezone: string, zone: string }> {label: 'Buenos Aires (ART)', timezone: 'America/Argentina/Buenos_Aires', zone: 'America'}, {label: 'São Paulo (BRT)', timezone: 'America/Sao_Paulo', zone: 'America'}, ]; - export const DATE_FORMATS: Array<{ label: string; format: string; equivalentTo: string }> = [ {label: 'Short', format: 'short', equivalentTo: 'M/d/yy, h:mm a'}, {label: 'Medium', format: 'medium', equivalentTo: 'MMM d, y, h:mm:ss a'}, From afd0b4f36d69a0d6b93873d7c99fff16997a3bc7 Mon Sep 17 00:00:00 2001 From: AlexSanchez-bit Date: Thu, 16 Oct 2025 10:56:40 -0400 Subject: [PATCH 002/128] fix[frontend](web_console): sanitized password parameter to admit all utf8 characters even url structure ones --- frontend/src/app/core/auth/account.service.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/core/auth/account.service.ts b/frontend/src/app/core/auth/account.service.ts index 1631572ca..02ec14cb5 100644 --- a/frontend/src/app/core/auth/account.service.ts +++ b/frontend/src/app/core/auth/account.service.ts @@ -37,7 +37,8 @@ export class AccountService { } checkPassword(password: string, uuid: string): Observable> { - return this.http.get(SERVER_API_URL + `api/check-credentials?password=${password}&checkUUID=${uuid}`, { + const sanitized_password = encodeURIComponent(password) + return this.http.get(SERVER_API_URL + `api/check-credentials?password=${sanitized_password}&checkUUID=${uuid}`, { observe: 'response', responseType: 'text' }); From 932e802115579b0a9021fc0aedc4ecb34b9dc396 Mon Sep 17 00:00:00 2001 From: AlexSanchez-bit Date: Fri, 17 Oct 2025 10:08:23 -0400 Subject: [PATCH 003/128] feat[backend](api-keys): added api keys dto, controllers and entities --- backend/.nvim/.env | 18 ++ backend/mvnw | 0 backend/mvnw.cmd | 0 .../domain/api_keys/UtmApiKeyModel.java | 50 +++++ .../dto/api_key/UtmApiKeyResponseDto.java | 36 ++++ .../dto/api_key/UtmApiKeyUpsertDto.java | 29 +++ .../rest/api-keys/UtmApiKeyController.java | 178 ++++++++++++++++++ .../changelog/20250917001_adding_api_keys.xml | 43 +++++ .../config/liquibase/scripts/tables.sql | 21 +++ 9 files changed, 375 insertions(+) create mode 100644 backend/.nvim/.env mode change 100644 => 100755 backend/mvnw mode change 100644 => 100755 backend/mvnw.cmd create mode 100644 backend/src/main/java/com/park/utmstack/domain/api_keys/UtmApiKeyModel.java create mode 100644 backend/src/main/java/com/park/utmstack/service/dto/api_key/UtmApiKeyResponseDto.java create mode 100644 backend/src/main/java/com/park/utmstack/service/dto/api_key/UtmApiKeyUpsertDto.java create mode 100644 backend/src/main/java/com/park/utmstack/web/rest/api-keys/UtmApiKeyController.java create mode 100644 backend/src/main/resources/config/liquibase/changelog/20250917001_adding_api_keys.xml diff --git a/backend/.nvim/.env b/backend/.nvim/.env new file mode 100644 index 000000000..628f21921 --- /dev/null +++ b/backend/.nvim/.env @@ -0,0 +1,18 @@ +revision=4 +AD_AUDIT_SERVICE=http://localhost:8081/api +DB_HOST=192.168.1.18 +DB_NAME=utmstack +DB_PASS=DF7JOMKyU6oNwmy4 +DB_PORT=5432 +DB_USER=postgres +ELASTICSEARCH_HOST=192.168.1.18 +ELASTICSEARCH_PORT=9200 +ENCRYPTION_KEY=nWkGxX1vRTDyZc1Jm59YUkR3KsUmbYrJ +EVENT_PROCESSOR_HOST=192.168.1.18 +EVENT_PROCESSOR_PORT=9002 +GRPC_AGENT_MANAGER_HOST=192.168.1.18 +GRPC_AGENT_MANAGER_PORT=9000 +INTERNAL_KEY=qNANPjjNm7eantt7sgld0iSWFFeGKz5i +LOGSTASH_URL=http://localhost:9600 +SERVER_NAME=UTM +SOC_AI_BASE_URL=http://localhost:8081/process diff --git a/backend/mvnw b/backend/mvnw old mode 100644 new mode 100755 diff --git a/backend/mvnw.cmd b/backend/mvnw.cmd old mode 100644 new mode 100755 diff --git a/backend/src/main/java/com/park/utmstack/domain/api_keys/UtmApiKeyModel.java b/backend/src/main/java/com/park/utmstack/domain/api_keys/UtmApiKeyModel.java new file mode 100644 index 000000000..8f532da04 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/domain/api_keys/UtmApiKeyModel.java @@ -0,0 +1,50 @@ +package com.park.utmstack.domain.api_keys; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.time.Instant; +import java.util.UUID; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "api_keys") +public class ApiKey implements Serializable { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + @Column(columnDefinition = "UUID") + private UUID id; + + @Column(nullable = false) + private UUID accountId; + + @Column(nullable = false) + private String name; + + @Column(nullable = false) + private String apiKey; + + @Column + private String allowedIp; + + @Column(nullable = false) + private Instant createdAt; + + private Instant generatedAt; + + @Column + private Instant expiresAt; +} diff --git a/backend/src/main/java/com/park/utmstack/service/dto/api_key/UtmApiKeyResponseDto.java b/backend/src/main/java/com/park/utmstack/service/dto/api_key/UtmApiKeyResponseDto.java new file mode 100644 index 000000000..232e77f56 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/service/dto/api_key/UtmApiKeyResponseDto.java @@ -0,0 +1,36 @@ +package com.utmstack.api.service.dto.apikey; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.Instant; +import java.util.List; +import java.util.UUID; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ApiKeyResponseDTO { + + @Schema(description = "Unique identifier of the API key") + private UUID id; + + @Schema(description = "User-friendly API key name") + private String name; + + @Schema(description = "Allowed IP address or IP range in CIDR notation (e.g., '192.168.1.100' or '192.168.1.0/24')") + private List allowedIp; + + @Schema(description = "API key creation timestamp") + private Instant createdAt; + + @Schema(description = "API key expiration timestamp (if applicable)") + private Instant expiresAt; + + @Schema(description = "Generated At") + private Instant generatedAt; +} diff --git a/backend/src/main/java/com/park/utmstack/service/dto/api_key/UtmApiKeyUpsertDto.java b/backend/src/main/java/com/park/utmstack/service/dto/api_key/UtmApiKeyUpsertDto.java new file mode 100644 index 000000000..518aa6e35 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/service/dto/api_key/UtmApiKeyUpsertDto.java @@ -0,0 +1,29 @@ +package com.utmstack.api.service.dto.apikey; + + +import com.utmstack.api.annotation.ValidIPOrCIDR; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.Instant; +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ApiKeyUpsertDTO { + @NotNull + @Schema(description = "API Key name", requiredMode = Schema.RequiredMode.REQUIRED) + private String name; + + @Schema(description = "Allowed IP address or IP range in CIDR notation (e.g., '192.168.1.100' or '192.168.1.0/24'). If null, no IP restrictions are applied.") + private List<@ValidIPOrCIDR String> allowedIp; + + @Schema(description = "Expiration timestamp of the API key") + private Instant expiresAt; +} diff --git a/backend/src/main/java/com/park/utmstack/web/rest/api-keys/UtmApiKeyController.java b/backend/src/main/java/com/park/utmstack/web/rest/api-keys/UtmApiKeyController.java new file mode 100644 index 000000000..27c48af2f --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/web/rest/api-keys/UtmApiKeyController.java @@ -0,0 +1,178 @@ +@RestController +@RequestMapping("/api/api-keys") +@PreAuthorize("hasAuthority(\"" + AuthoritiesConstants.USER + "\")") +@AllArgsConstructor +@Hidden +public class ApiKeyResource { + + private static final String CLASSNAME = "ApiKeyResource"; + private final Logger log = LoggerFactory.getLogger(ApiKeyResource.class); + + private final ApiKeyService apiKeyService; + private final UserService userService; + + private UUID getCurrentAccountId() throws CurrentUserLoginNotFoundException { + User user = userService.getUserWithAuthoritiesByLogin(SecurityUtils.currentUserLogin()); + return UUID.fromString(user.getAccountId()); + } + + @Operation(summary = "Create API key", + description = "Creates a new API key record using the provided settings. The plain text key is not generated at creation.") + @ApiResponses({ + @ApiResponse(responseCode = "201", description = "API key created successfully", + content = @Content(schema = @Schema(implementation = ApiKeyResponseDTO.class))), + @ApiResponse(responseCode = "409", description = "API key already exists", content = @Content), + @ApiResponse(responseCode = "500", description = "Internal server error", + content = @Content, headers = { + @Header(name = "X-App-Error", description = "Technical error details") + }) + }) + @PostMapping + public ResponseEntity createApiKey(@RequestBody ApiKeyUpsertDTO dto) { + final String ctx = CLASSNAME + ".createApiKey"; + try { + UUID accountId = getCurrentAccountId(); + ApiKeyResponseDTO responseDTO = apiKeyService.createApiKey(accountId, dto); + return ResponseEntity.status(HttpStatus.CREATED).body(responseDTO); + } catch (ApiKeyExistException e) { + return ResponseUtil.buildConflictResponse(e.getMessage()); + } catch (Exception e) { + String msg = ctx + ": " + e.getMessage(); + log.error(msg, e); + return ResponseUtil.buildInternalServerErrorResponse(msg); + } + } + + @Operation(summary = "Generate a new API key", + description = "Generates (or renews) a new random API key for the specified API key record. The plain text key is returned only once.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "API key generated successfully", + content = @Content(schema = @Schema(type = "string"))), + @ApiResponse(responseCode = "404", description = "API key not found", content = @Content), + @ApiResponse(responseCode = "500", description = "Internal server error", + content = @Content, headers = { + @Header(name = "X-App-Error", description = "Technical error details") + }) + }) + @PostMapping("/{id}/generate") + public ResponseEntity generateApiKey(@PathVariable("id") UUID apiKeyId) { + final String ctx = CLASSNAME + ".generateApiKey"; + try { + UUID accountId = getCurrentAccountId(); + String plainKey = apiKeyService.generateApiKey(accountId, apiKeyId); + return ResponseEntity.ok(plainKey); + } catch (ApiKeyNotFoundException e) { + return ResponseUtil.buildNotFoundResponse(e.getMessage()); + } catch (Exception e) { + String msg = ctx + ": " + e.getMessage(); + log.error(msg, e); + return ResponseUtil.buildInternalServerErrorResponse(msg); + } + } + + @Operation(summary = "Retrieve API key", + description = "Retrieves the API key details for the specified API key record.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "API key retrieved successfully", + content = @Content(schema = @Schema(implementation = ApiKeyResponseDTO.class))), + @ApiResponse(responseCode = "404", description = "API key not found", content = @Content), + @ApiResponse(responseCode = "500", description = "Internal server error", + content = @Content, headers = { + @Header(name = "X-App-Error", description = "Technical error details") + }) + }) + @GetMapping("/{id}") + public ResponseEntity getApiKey(@PathVariable("id") UUID apiKeyId) { + final String ctx = CLASSNAME + ".getApiKey"; + try { + UUID accountId = getCurrentAccountId(); + ApiKeyResponseDTO responseDTO = apiKeyService.getApiKey(accountId, apiKeyId); + return ResponseEntity.ok(responseDTO); + } catch (ApiKeyNotFoundException e) { + return ResponseUtil.buildNotFoundResponse(e.getMessage()); + } catch (Exception e) { + String msg = ctx + ": " + e.getMessage(); + log.error(msg, e); + return ResponseUtil.buildInternalServerErrorResponse(msg); + } + } + + @Operation(summary = "List API keys", + description = "Retrieves the API key list.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "API key retrieved successfully", + content = @Content(schema = @Schema(implementation = ApiKeyResponseDTO.class))), + @ApiResponse(responseCode = "404", description = "API key not found", content = @Content), + @ApiResponse(responseCode = "500", description = "Internal server error", + content = @Content, headers = { + @Header(name = "X-App-Error", description = "Technical error details") + }) + }) + @GetMapping("") + public ResponseEntity> listApiKeys(@ParameterObject Pageable pageable) { + final String ctx = CLASSNAME + ".listApiKeys"; + try { + UUID accountId = getCurrentAccountId(); + Page page = apiKeyService.listApiKeys(accountId, pageable); + HttpHeaders headers = PaginationUtil.generatePaginationHttpHeaders(ServletUriComponentsBuilder.fromCurrentRequest(), page); + return ResponseEntity.ok().headers(headers).body(page.getContent()); + } catch (Exception e) { + String msg = ctx + ": " + e.getMessage(); + log.error(msg, e); + return ResponseUtil.buildInternalServerErrorResponse(msg); + } + } + + @Operation(summary = "Update API key", + description = "Updates mutable fields (name, allowed IPs, expiration) for the specified API key record.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "API key updated successfully", + content = @Content(schema = @Schema(implementation = ApiKeyResponseDTO.class))), + @ApiResponse(responseCode = "404", description = "API key not found", content = @Content), + @ApiResponse(responseCode = "500", description = "Internal server error", + content = @Content, headers = { + @Header(name = "X-App-Error", description = "Technical error details") + }) + }) + @PutMapping("/{id}") + public ResponseEntity updateApiKey(@PathVariable("id") UUID apiKeyId, + @RequestBody ApiKeyUpsertDTO dto) { + final String ctx = CLASSNAME + ".updateApiKey"; + try { + UUID accountId = getCurrentAccountId(); + ApiKeyResponseDTO responseDTO = apiKeyService.updateApiKey(accountId, apiKeyId, dto); + return ResponseEntity.ok(responseDTO); + } catch (ApiKeyNotFoundException e) { + return ResponseUtil.buildNotFoundResponse(e.getMessage()); + } catch (Exception e) { + String msg = ctx + ": " + e.getMessage(); + log.error(msg, e); + return ResponseUtil.buildInternalServerErrorResponse(msg); + } + } + + @Operation(summary = "Delete API key", + description = "Deletes the specified API key record for the authenticated user.") + @ApiResponses({ + @ApiResponse(responseCode = "204", description = "API key deleted successfully", content = @Content), + @ApiResponse(responseCode = "404", description = "API key not found", content = @Content), + @ApiResponse(responseCode = "500", description = "Internal server error", + content = @Content, headers = { + @Header(name = "X-App-Error", description = "Technical error details") + }) + }) + @DeleteMapping("/{id}") + public ResponseEntity deleteApiKey(@PathVariable("id") UUID apiKeyId) { + final String ctx = CLASSNAME + ".deleteApiKey"; + try { + UUID accountId = getCurrentAccountId(); + apiKeyService.deleteApiKey(accountId, apiKeyId); + return ResponseEntity.noContent().build(); + } catch (ApiKeyNotFoundException e) { + return ResponseUtil.buildNotFoundResponse(e.getMessage()); + } catch (Exception e) { + String msg = ctx + ": " + e.getMessage(); + log.error(msg, e); + return ResponseUtil.buildInternalServerErrorResponse(msg); + } + } diff --git a/backend/src/main/resources/config/liquibase/changelog/20250917001_adding_api_keys.xml b/backend/src/main/resources/config/liquibase/changelog/20250917001_adding_api_keys.xml new file mode 100644 index 000000000..cb1c2cd75 --- /dev/null +++ b/backend/src/main/resources/config/liquibase/changelog/20250917001_adding_api_keys.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO api_keys (account_id, name, api_key, created_at) + SELECT account_id::uuid, 'DefaultApiKey', api_key, CURRENT_TIMESTAMP + FROM jhi_user + WHERE api_key IS NOT NULL; + + + + + + + diff --git a/backend/src/main/resources/config/liquibase/scripts/tables.sql b/backend/src/main/resources/config/liquibase/scripts/tables.sql index f50a695d1..629971010 100644 --- a/backend/src/main/resources/config/liquibase/scripts/tables.sql +++ b/backend/src/main/resources/config/liquibase/scripts/tables.sql @@ -12,6 +12,7 @@ DROP TABLE IF EXISTS public.utm_gvm_scan_result; DROP TABLE IF EXISTS public.utm_gvm_task; DROP TABLE IF EXISTS public.utm_module_modal; DROP TABLE IF EXISTS public.utm_system_restart; +DROP TABLE IF EXISTS public.utm_api_keys; CREATE TABLE IF NOT EXISTS public.jhi_authority ( @@ -830,3 +831,23 @@ CREATE TABLE IF NOT EXISTS public.utm_space_notification_control next_notification timestamp without time zone NOT NULL, CONSTRAINT utm_space_notification_control_pkey PRIMARY KEY (id) ); + +CREATE TABLE IF NOT EXISTS public.utm_api_keys +( + id uuid default uuid_generate_v4() not null + primary key, + account_id uuid not null, + name varchar(255) not null, + api_key varchar(255) not null, + allowed_ip text, + created_at timestamp not null, + expires_at timestamp, + generated_at timestamp +); + +alter table utm_api_keys + owner to postgres; + +create unique index uk_api_keys_api_key + on utm_api_keys (api_key); + From ec75a02aa5b73f305a25bf4032c613f36afed4f1 Mon Sep 17 00:00:00 2001 From: AlexSanchez-bit Date: Fri, 17 Oct 2025 11:48:55 -0400 Subject: [PATCH 004/128] feat[backend](api_keys): added api keys --- .../{UtmApiKeyModel.java => ApiKey.java} | 0 .../service/api_key/ApiKeyService.java | 212 ++++++++++++++++++ ...esponseDto.java => ApiKeyResponseDTO.java} | 0 ...KeyUpsertDto.java => ApiKeyUpsertDTO.java} | 0 .../rest/api-keys/UtmApiKeyController.java | 1 + 5 files changed, 213 insertions(+) rename backend/src/main/java/com/park/utmstack/domain/api_keys/{UtmApiKeyModel.java => ApiKey.java} (100%) create mode 100644 backend/src/main/java/com/park/utmstack/service/api_key/ApiKeyService.java rename backend/src/main/java/com/park/utmstack/service/dto/api_key/{UtmApiKeyResponseDto.java => ApiKeyResponseDTO.java} (100%) rename backend/src/main/java/com/park/utmstack/service/dto/api_key/{UtmApiKeyUpsertDto.java => ApiKeyUpsertDTO.java} (100%) diff --git a/backend/src/main/java/com/park/utmstack/domain/api_keys/UtmApiKeyModel.java b/backend/src/main/java/com/park/utmstack/domain/api_keys/ApiKey.java similarity index 100% rename from backend/src/main/java/com/park/utmstack/domain/api_keys/UtmApiKeyModel.java rename to backend/src/main/java/com/park/utmstack/domain/api_keys/ApiKey.java diff --git a/backend/src/main/java/com/park/utmstack/service/api_key/ApiKeyService.java b/backend/src/main/java/com/park/utmstack/service/api_key/ApiKeyService.java new file mode 100644 index 000000000..775d37f7d --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/service/api_key/ApiKeyService.java @@ -0,0 +1,212 @@ +package com.utmstack.api.service; + +import com.utmstack.api.domain.User; +import com.utmstack.api.domain.api_key.ApiKey; +import com.utmstack.api.domain.api_key.ApiKeyUsageLog_; +import com.utmstack.api.domain.api_key.index.ApiKeyUsageLogIndexDocument; +import com.utmstack.api.domain.enumeration.NotificationMessageKeyEnum; +import com.utmstack.api.repository.elasticsearch.ApiKeyUsageLogRepository; +import com.utmstack.api.repository.jpa.ApiKeyRepository; +import com.utmstack.api.repository.jpa.UserRepository; +import com.utmstack.api.service.criteria.api_key.ApiKeyUsageLogCriteria; +import com.utmstack.api.service.dto.SearchHitsResponseDTO; +import com.utmstack.api.service.dto.api_key.ApiKeyResponseDTO; +import com.utmstack.api.service.dto.api_key.ApiKeyUpsertDTO; +import com.utmstack.api.service.exceptions.ApiKeyExistException; +import com.utmstack.api.service.exceptions.ApiKeyNotFoundException; +import com.utmstack.api.service.mapper.ApiKeyMapper; +import com.utmstack.api.service.user.UserNotificationService; +import lombok.AllArgsConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.elasticsearch.core.ElasticsearchOperations; +import org.springframework.data.elasticsearch.core.SearchHit; +import org.springframework.data.elasticsearch.core.SearchHits; +import org.springframework.data.elasticsearch.core.query.Criteria; +import org.springframework.data.elasticsearch.core.query.CriteriaQuery; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import java.security.SecureRandom; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Base64; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Collectors; + +@Service +@AllArgsConstructor +public class ApiKeyService { + private static final String CLASSNAME = "ApiKeyService"; + private final Logger log = LoggerFactory.getLogger(ApiKeyService.class); + private final ApiKeyRepository apiKeyRepository; + private final ApiKeyMapper apiKeyMapper; + private final ApiKeyUsageLogRepository apiUsageLogRepository; + private final ElasticsearchOperations elasticsearchOperations; + private final UserRepository userRepository; + private final UserNotificationService userNotificationService; + private final MailService mailService; + + + public ApiKeyResponseDTO createApiKey(UUID accountId, ApiKeyUpsertDTO dto) { + final String ctx = CLASSNAME + ".createApiKey"; + try { + apiKeyRepository.findByNameAndAccountId(dto.getName(), accountId) + .ifPresent(apiKey -> { + throw new ApiKeyExistException("Api key already exists"); + }); + var apiKey = api_key.builder() + .accountId(accountId) + .name(dto.getName()) + .expiresAt(dto.getExpiresAt()) + .allowedIp(String.join(",", dto.getAllowedIp())) + .createdAt(Instant.now()) + .apiKey(generateRandomKey()) + .build(); + return apiKeyMapper.toDto(apiKeyRepository.save(apiKey)); + } catch (Exception e) { + throw new RuntimeException(ctx + ": " + e.getMessage()); + } + } + + public String generateApiKey(UUID accountId, UUID apiKeyId) { + final String ctx = CLASSNAME + ".generateApiKey"; + try { + ApiKey apiKey = apiKeyRepository.findByIdAndAccountId(apiKeyId, accountId) + .orElseThrow(() -> new ApiKeyNotFoundException("API key not found")); + String plainKey = generateRandomKey(); + api_key.setApiKey(plainKey); + api_key.setGeneratedAt(Instant.now()); + apiKeyRepository.save(apiKey); + return plainKey; + } catch (Exception e) { + throw new RuntimeException(ctx + ": " + e.getMessage()); + } + } + + public ApiKeyResponseDTO updateApiKey(UUID accountId, UUID apiKeyId, ApiKeyUpsertDTO dto) { + final String ctx = CLASSNAME + ".updateApiKey"; + try { + ApiKey apiKey = apiKeyRepository.findByIdAndAccountId(apiKeyId, accountId) + .orElseThrow(() -> new ApiKeyNotFoundException("API key not found")); + api_key.setName(dto.getName()); + if (dto.getAllowedIp() != null) { + api_key.setAllowedIp(String.join(",", dto.getAllowedIp())); + } else { + api_key.setAllowedIp(null); + } + api_key.setExpiresAt(dto.getExpiresAt()); + ApiKey updated = apiKeyRepository.save(apiKey); + return apiKeyMapper.toDto(updated); + } catch (Exception e) { + throw new RuntimeException(ctx + ": " + e.getMessage()); + } + } + + public ApiKeyResponseDTO getApiKey(UUID accountId, UUID apiKeyId) { + final String ctx = CLASSNAME + ".getApiKey"; + try { + ApiKey apiKey = apiKeyRepository.findByIdAndAccountId(apiKeyId, accountId) + .orElseThrow(() -> new ApiKeyNotFoundException("API key not found")); + return apiKeyMapper.toDto(apiKey); + } catch (Exception e) { + throw new RuntimeException(ctx + ": " + e.getMessage()); + } + } + + public Page listApiKeys(UUID accountId, Pageable pageable) { + final String ctx = CLASSNAME + ".listApiKeys"; + try { + return apiKeyRepository.findByAccountId(accountId, pageable).map(apiKeyMapper::toDto); + } catch (Exception e) { + throw new RuntimeException(ctx + ": " + e.getMessage()); + } + } + + + public void deleteApiKey(UUID accountId, UUID apiKeyId) { + final String ctx = CLASSNAME + ".deleteApiKey"; + try { + ApiKey apiKey = apiKeyRepository.findByIdAndAccountId(apiKeyId, accountId) + .orElseThrow(() -> new ApiKeyNotFoundException("API key not found")); + apiKeyRepository.delete(apiKey); + } catch (Exception e) { + throw new RuntimeException(ctx + ": " + e.getMessage()); + } + } + + private String generateRandomKey() { + final String ctx = CLASSNAME + ".generateRandomKey"; + try { + SecureRandom random = new SecureRandom(); + byte[] keyBytes = new byte[32]; + random.nextBytes(keyBytes); + return Base64.getUrlEncoder().withoutPadding().encodeToString(keyBytes); + } catch (Exception e) { + throw new RuntimeException(ctx + ": " + e.getMessage()); + } + } + + @Async + public void logUsage(ApiKeyUsageLogIndexDocument apiKeyUsageLog) { + final String ctx = CLASSNAME + ".logUsage"; + try { + apiUsageLogRepository.save(apiKeyUsageLog); + } catch (Exception e) { + log.error(ctx + ": {}", e.getMessage()); + } + } + + public Optional findOneByApiKey(String apiKey) { + return apiKeyRepository.findOneByApiKey(apiKey); + } + + public SearchHitsResponseDTO getApiKeyUsageLogs(User user, + ApiKeyUsageLogCriteria criteria, + Pageable pageable) { + final String ctx = CLASSNAME + ".getApiKeyUsageLogs"; + try { + CriteriaQuery query = new CriteriaQuery(criteria != null ? criteria.toCriteriaQuery() : new Criteria(), pageable); + query.addCriteria(new Criteria(ApiKeyUsageLog_.accountId).is(user.getAccountId())); + SearchHits result = elasticsearchOperations.search(query, ApiKeyUsageLogIndexDocument.class); + return SearchHitsResponseDTO.builder() + .totalHits(result.getTotalHits()) + .items(result.stream() + .map(SearchHit::getContent) + .collect(Collectors.toList())) + .build(); + } catch (Exception e) { + throw new RuntimeException(ctx + ": " + e.getMessage()); + } + } + + @Scheduled(cron = "0 0 9 * * ?") + public void checkExpiringApiKeys() { + Instant fiveDaysFromNow = Instant.now().plus(5, ChronoUnit.DAYS); + Instant now = Instant.now(); + List expiringKeys = apiKeyRepository.findAllByExpiresAtAfterAndExpiresAtLessThanEqual(now, fiveDaysFromNow); + + if (!expiringKeys.isEmpty()) { + Map> expiringKeysByAccount = expiringKeys.stream() + .collect(Collectors.groupingBy(ApiKey::getAccountId)); + + expiringKeysByAccount.forEach((accountId, apiKeys) -> { + var principal = userRepository.findByAccountIdAndAccountOwnerIsTrue(accountId.toString()).orElse(null); + if (principal == null) { + return; + } + mailService.sendKeyExpirationEmail(principal, apiKeys); + + userNotificationService.createAndSendNotification(principal.getUuid(), + NotificationMessageKeyEnum.API_KEY_EXPIRATION, + Map.of("names", apiKeys.stream().map(ApiKey::getName).collect(Collectors.joining(",")))); + }); + } + } +} diff --git a/backend/src/main/java/com/park/utmstack/service/dto/api_key/UtmApiKeyResponseDto.java b/backend/src/main/java/com/park/utmstack/service/dto/api_key/ApiKeyResponseDTO.java similarity index 100% rename from backend/src/main/java/com/park/utmstack/service/dto/api_key/UtmApiKeyResponseDto.java rename to backend/src/main/java/com/park/utmstack/service/dto/api_key/ApiKeyResponseDTO.java diff --git a/backend/src/main/java/com/park/utmstack/service/dto/api_key/UtmApiKeyUpsertDto.java b/backend/src/main/java/com/park/utmstack/service/dto/api_key/ApiKeyUpsertDTO.java similarity index 100% rename from backend/src/main/java/com/park/utmstack/service/dto/api_key/UtmApiKeyUpsertDto.java rename to backend/src/main/java/com/park/utmstack/service/dto/api_key/ApiKeyUpsertDTO.java diff --git a/backend/src/main/java/com/park/utmstack/web/rest/api-keys/UtmApiKeyController.java b/backend/src/main/java/com/park/utmstack/web/rest/api-keys/UtmApiKeyController.java index 27c48af2f..253fabc76 100644 --- a/backend/src/main/java/com/park/utmstack/web/rest/api-keys/UtmApiKeyController.java +++ b/backend/src/main/java/com/park/utmstack/web/rest/api-keys/UtmApiKeyController.java @@ -176,3 +176,4 @@ public ResponseEntity deleteApiKey(@PathVariable("id") UUID apiKeyId) { return ResponseUtil.buildInternalServerErrorResponse(msg); } } +} From 545ac098a237af7cca7d4b99188d5f80ecf673a3 Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Fri, 17 Oct 2025 11:02:50 -0500 Subject: [PATCH 005/128] feat(api_keys): create api_keys table with user_id and add foreign key constraint --- .../park/utmstack/domain/api_keys/ApiKey.java | 9 ++----- .../changelog/20250917001_adding_api_keys.xml | 25 ++++++++----------- .../resources/config/liquibase/master.xml | 2 ++ 3 files changed, 15 insertions(+), 21 deletions(-) diff --git a/backend/src/main/java/com/park/utmstack/domain/api_keys/ApiKey.java b/backend/src/main/java/com/park/utmstack/domain/api_keys/ApiKey.java index 8f532da04..8d0585a3d 100644 --- a/backend/src/main/java/com/park/utmstack/domain/api_keys/ApiKey.java +++ b/backend/src/main/java/com/park/utmstack/domain/api_keys/ApiKey.java @@ -1,16 +1,11 @@ package com.park.utmstack.domain.api_keys; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.Table; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import javax.persistence.*; import java.io.Serializable; import java.time.Instant; import java.util.UUID; @@ -29,7 +24,7 @@ public class ApiKey implements Serializable { private UUID id; @Column(nullable = false) - private UUID accountId; + private Long userId; @Column(nullable = false) private String name; diff --git a/backend/src/main/resources/config/liquibase/changelog/20250917001_adding_api_keys.xml b/backend/src/main/resources/config/liquibase/changelog/20250917001_adding_api_keys.xml index cb1c2cd75..2b996f83c 100644 --- a/backend/src/main/resources/config/liquibase/changelog/20250917001_adding_api_keys.xml +++ b/backend/src/main/resources/config/liquibase/changelog/20250917001_adding_api_keys.xml @@ -4,12 +4,12 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd"> - + - + @@ -24,20 +24,17 @@ + - - - - INSERT INTO api_keys (account_id, name, api_key, created_at) - SELECT account_id::uuid, 'DefaultApiKey', api_key, CURRENT_TIMESTAMP - FROM jhi_user - WHERE api_key IS NOT NULL; - - - - - + + diff --git a/backend/src/main/resources/config/liquibase/master.xml b/backend/src/main/resources/config/liquibase/master.xml index 5201db735..b7ab36816 100644 --- a/backend/src/main/resources/config/liquibase/master.xml +++ b/backend/src/main/resources/config/liquibase/master.xml @@ -253,5 +253,7 @@ + + From c4fef1ed83253d66351d61b1cff31a6dd2f2a4b0 Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Fri, 17 Oct 2025 12:13:17 -0500 Subject: [PATCH 006/128] feat(api_keys): implement API key management with CRUD operations and validation --- .../com/park/utmstack/config/Constants.java | 1 + .../domain/api_keys/ApiKeyUsageLog.java | 40 ++++++ .../repository/api_key/ApiKeyRepository.java | 29 ++++ .../service/api_key/ApiKeyService.java | 127 +++++++----------- .../dto/api_key/ApiKeyResponseDTO.java | 2 +- .../service/dto/api_key/ApiKeyUpsertDTO.java | 7 +- .../utmstack/service/mapper/ApiKeyMapper.java | 31 +++++ .../util/exceptions/ApiKeyExistException.java | 7 + .../ApiKeyInvalidAccessException.java | 9 ++ .../exceptions/ApiKeyNotFoundException.java | 7 + .../validation/api_key/ValidIPOrCIDR.java | 23 ++++ .../api_key/ValidIPOrCIDRValidator.java | 37 +++++ .../ApiKeyResource.java} | 72 ++++++++++ ... => 20251017001_create_api_keys_table.xml} | 0 .../resources/config/liquibase/master.xml | 2 +- 15 files changed, 308 insertions(+), 86 deletions(-) create mode 100644 backend/src/main/java/com/park/utmstack/domain/api_keys/ApiKeyUsageLog.java create mode 100644 backend/src/main/java/com/park/utmstack/repository/api_key/ApiKeyRepository.java create mode 100644 backend/src/main/java/com/park/utmstack/service/mapper/ApiKeyMapper.java create mode 100644 backend/src/main/java/com/park/utmstack/util/exceptions/ApiKeyExistException.java create mode 100644 backend/src/main/java/com/park/utmstack/util/exceptions/ApiKeyInvalidAccessException.java create mode 100644 backend/src/main/java/com/park/utmstack/util/exceptions/ApiKeyNotFoundException.java create mode 100644 backend/src/main/java/com/park/utmstack/validation/api_key/ValidIPOrCIDR.java create mode 100644 backend/src/main/java/com/park/utmstack/validation/api_key/ValidIPOrCIDRValidator.java rename backend/src/main/java/com/park/utmstack/web/rest/{api-keys/UtmApiKeyController.java => api_key/ApiKeyResource.java} (70%) rename backend/src/main/resources/config/liquibase/changelog/{20250917001_adding_api_keys.xml => 20251017001_create_api_keys_table.xml} (100%) diff --git a/backend/src/main/java/com/park/utmstack/config/Constants.java b/backend/src/main/java/com/park/utmstack/config/Constants.java index b1f771850..5c85a810f 100644 --- a/backend/src/main/java/com/park/utmstack/config/Constants.java +++ b/backend/src/main/java/com/park/utmstack/config/Constants.java @@ -137,6 +137,7 @@ public final class Constants { // Defines the index pattern for querying Elasticsearch statistics indexes. // ---------------------------------------------------------------------------------- public static final String STATISTICS_INDEX_PATTERN = "v11-statistics-*"; + public static final String V11_API_ACCESS_LOGS = "v11-api-access-logs-*"; // Logging public static final String TRACE_ID_KEY = "traceId"; diff --git a/backend/src/main/java/com/park/utmstack/domain/api_keys/ApiKeyUsageLog.java b/backend/src/main/java/com/park/utmstack/domain/api_keys/ApiKeyUsageLog.java new file mode 100644 index 000000000..d097aa4fb --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/domain/api_keys/ApiKeyUsageLog.java @@ -0,0 +1,40 @@ +package com.park.utmstack.domain.api_keys; + + +import lombok.*; +import java.time.Instant; +import java.util.UUID; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class ApiKeyUsageLog { + + private UUID id; + + private UUID apiKeyId; + + private String apiKeyName; + + private Long userId; + + private Instant timestamp; + + private String endpoint; + + private String address; + + private String errorMessage; + + private String queryParams; + + private String payload; + + private String userAgent; + + private String httpMethod; + + private Integer statusCode; +} diff --git a/backend/src/main/java/com/park/utmstack/repository/api_key/ApiKeyRepository.java b/backend/src/main/java/com/park/utmstack/repository/api_key/ApiKeyRepository.java new file mode 100644 index 000000000..e76001064 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/repository/api_key/ApiKeyRepository.java @@ -0,0 +1,29 @@ +package com.park.utmstack.repository.api_key; + +import com.park.utmstack.domain.api_keys.ApiKey; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import javax.validation.constraints.NotNull; +import java.time.Instant; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +@Repository +public interface ApiKeyRepository extends JpaRepository { + + Optional findByIdAndUserId(UUID id, Long userId); + + Page findByUserId(Long userId, Pageable pageable); + + @Cacheable(cacheNames = "apikey", key = "#root.args[0]") + Optional findOneByApiKey(@NotNull String apiKey); + + Optional findByNameAndUserId(@NotNull String name, Long userId); + + List findAllByExpiresAtAfterAndExpiresAtLessThanEqual(Instant now, Instant fiveDaysFromNow); +} diff --git a/backend/src/main/java/com/park/utmstack/service/api_key/ApiKeyService.java b/backend/src/main/java/com/park/utmstack/service/api_key/ApiKeyService.java index 775d37f7d..cb402e356 100644 --- a/backend/src/main/java/com/park/utmstack/service/api_key/ApiKeyService.java +++ b/backend/src/main/java/com/park/utmstack/service/api_key/ApiKeyService.java @@ -1,88 +1,73 @@ -package com.utmstack.api.service; - -import com.utmstack.api.domain.User; -import com.utmstack.api.domain.api_key.ApiKey; -import com.utmstack.api.domain.api_key.ApiKeyUsageLog_; -import com.utmstack.api.domain.api_key.index.ApiKeyUsageLogIndexDocument; -import com.utmstack.api.domain.enumeration.NotificationMessageKeyEnum; -import com.utmstack.api.repository.elasticsearch.ApiKeyUsageLogRepository; -import com.utmstack.api.repository.jpa.ApiKeyRepository; -import com.utmstack.api.repository.jpa.UserRepository; -import com.utmstack.api.service.criteria.api_key.ApiKeyUsageLogCriteria; -import com.utmstack.api.service.dto.SearchHitsResponseDTO; -import com.utmstack.api.service.dto.api_key.ApiKeyResponseDTO; -import com.utmstack.api.service.dto.api_key.ApiKeyUpsertDTO; -import com.utmstack.api.service.exceptions.ApiKeyExistException; -import com.utmstack.api.service.exceptions.ApiKeyNotFoundException; -import com.utmstack.api.service.mapper.ApiKeyMapper; -import com.utmstack.api.service.user.UserNotificationService; +package com.park.utmstack.service.api_key; + +import com.park.utmstack.domain.api_keys.ApiKey; +import com.park.utmstack.domain.api_keys.ApiKeyUsageLog; +import com.park.utmstack.repository.api_key.ApiKeyRepository; +import com.park.utmstack.service.dto.api_key.ApiKeyResponseDTO; +import com.park.utmstack.service.dto.api_key.ApiKeyUpsertDTO; +import com.park.utmstack.service.elasticsearch.OpensearchClientBuilder; +import com.park.utmstack.service.mapper.ApiKeyMapper; +import com.park.utmstack.util.exceptions.ApiKeyExistException; +import com.park.utmstack.util.exceptions.ApiKeyNotFoundException; import lombok.AllArgsConstructor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; -import org.springframework.data.elasticsearch.core.ElasticsearchOperations; -import org.springframework.data.elasticsearch.core.SearchHit; -import org.springframework.data.elasticsearch.core.SearchHits; -import org.springframework.data.elasticsearch.core.query.Criteria; -import org.springframework.data.elasticsearch.core.query.CriteriaQuery; + import org.springframework.scheduling.annotation.Async; -import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import java.security.SecureRandom; import java.time.Instant; -import java.time.temporal.ChronoUnit; import java.util.Base64; -import java.util.List; -import java.util.Map; import java.util.Optional; import java.util.UUID; -import java.util.stream.Collectors; + +import static com.park.utmstack.config.Constants.V11_API_ACCESS_LOGS; @Service @AllArgsConstructor public class ApiKeyService { + private static final String CLASSNAME = "ApiKeyService"; private final Logger log = LoggerFactory.getLogger(ApiKeyService.class); private final ApiKeyRepository apiKeyRepository; private final ApiKeyMapper apiKeyMapper; - private final ApiKeyUsageLogRepository apiUsageLogRepository; - private final ElasticsearchOperations elasticsearchOperations; - private final UserRepository userRepository; - private final UserNotificationService userNotificationService; - private final MailService mailService; + private final OpensearchClientBuilder client; - public ApiKeyResponseDTO createApiKey(UUID accountId, ApiKeyUpsertDTO dto) { + public ApiKeyResponseDTO createApiKey(Long userId, ApiKeyUpsertDTO dto) { final String ctx = CLASSNAME + ".createApiKey"; try { - apiKeyRepository.findByNameAndAccountId(dto.getName(), accountId) + apiKeyRepository.findByNameAndUserId(dto.getName(), userId) .ifPresent(apiKey -> { throw new ApiKeyExistException("Api key already exists"); }); - var apiKey = api_key.builder() - .accountId(accountId) + + var apiKey = ApiKey.builder() + .userId(userId) .name(dto.getName()) .expiresAt(dto.getExpiresAt()) .allowedIp(String.join(",", dto.getAllowedIp())) .createdAt(Instant.now()) .apiKey(generateRandomKey()) .build(); + return apiKeyMapper.toDto(apiKeyRepository.save(apiKey)); } catch (Exception e) { throw new RuntimeException(ctx + ": " + e.getMessage()); } } - public String generateApiKey(UUID accountId, UUID apiKeyId) { + public String generateApiKey(Long userId, UUID apiKeyId) { final String ctx = CLASSNAME + ".generateApiKey"; try { - ApiKey apiKey = apiKeyRepository.findByIdAndAccountId(apiKeyId, accountId) + ApiKey apiKey = apiKeyRepository.findByIdAndUserId(apiKeyId, userId) .orElseThrow(() -> new ApiKeyNotFoundException("API key not found")); String plainKey = generateRandomKey(); - api_key.setApiKey(plainKey); - api_key.setGeneratedAt(Instant.now()); + apiKey.setApiKey(plainKey); + apiKey.setGeneratedAt(Instant.now()); apiKeyRepository.save(apiKey); return plainKey; } catch (Exception e) { @@ -90,18 +75,18 @@ public String generateApiKey(UUID accountId, UUID apiKeyId) { } } - public ApiKeyResponseDTO updateApiKey(UUID accountId, UUID apiKeyId, ApiKeyUpsertDTO dto) { + public ApiKeyResponseDTO updateApiKey(Long userId, UUID apiKeyId, ApiKeyUpsertDTO dto) { final String ctx = CLASSNAME + ".updateApiKey"; try { - ApiKey apiKey = apiKeyRepository.findByIdAndAccountId(apiKeyId, accountId) + ApiKey apiKey = apiKeyRepository.findByIdAndUserId(apiKeyId, userId) .orElseThrow(() -> new ApiKeyNotFoundException("API key not found")); - api_key.setName(dto.getName()); + apiKey.setName(dto.getName()); if (dto.getAllowedIp() != null) { - api_key.setAllowedIp(String.join(",", dto.getAllowedIp())); + apiKey.setAllowedIp(String.join(",", dto.getAllowedIp())); } else { - api_key.setAllowedIp(null); + apiKey.setAllowedIp(null); } - api_key.setExpiresAt(dto.getExpiresAt()); + apiKey.setExpiresAt(dto.getExpiresAt()); ApiKey updated = apiKeyRepository.save(apiKey); return apiKeyMapper.toDto(updated); } catch (Exception e) { @@ -109,10 +94,10 @@ public ApiKeyResponseDTO updateApiKey(UUID accountId, UUID apiKeyId, ApiKeyUpser } } - public ApiKeyResponseDTO getApiKey(UUID accountId, UUID apiKeyId) { + public ApiKeyResponseDTO getApiKey(Long userId, UUID apiKeyId) { final String ctx = CLASSNAME + ".getApiKey"; try { - ApiKey apiKey = apiKeyRepository.findByIdAndAccountId(apiKeyId, accountId) + ApiKey apiKey = apiKeyRepository.findByIdAndUserId(apiKeyId, userId) .orElseThrow(() -> new ApiKeyNotFoundException("API key not found")); return apiKeyMapper.toDto(apiKey); } catch (Exception e) { @@ -120,20 +105,20 @@ public ApiKeyResponseDTO getApiKey(UUID accountId, UUID apiKeyId) { } } - public Page listApiKeys(UUID accountId, Pageable pageable) { + public Page listApiKeys(Long userId, Pageable pageable) { final String ctx = CLASSNAME + ".listApiKeys"; try { - return apiKeyRepository.findByAccountId(accountId, pageable).map(apiKeyMapper::toDto); + return apiKeyRepository.findByUserId(userId, pageable).map(apiKeyMapper::toDto); } catch (Exception e) { throw new RuntimeException(ctx + ": " + e.getMessage()); } } - public void deleteApiKey(UUID accountId, UUID apiKeyId) { + public void deleteApiKey(Long userId, UUID apiKeyId) { final String ctx = CLASSNAME + ".deleteApiKey"; try { - ApiKey apiKey = apiKeyRepository.findByIdAndAccountId(apiKeyId, accountId) + ApiKey apiKey = apiKeyRepository.findByIdAndUserId(apiKeyId, userId) .orElseThrow(() -> new ApiKeyNotFoundException("API key not found")); apiKeyRepository.delete(apiKey); } catch (Exception e) { @@ -154,10 +139,10 @@ private String generateRandomKey() { } @Async - public void logUsage(ApiKeyUsageLogIndexDocument apiKeyUsageLog) { + public void logUsage(ApiKeyUsageLog apiKeyUsageLog) { final String ctx = CLASSNAME + ".logUsage"; try { - apiUsageLogRepository.save(apiKeyUsageLog); + client.getClient().index(V11_API_ACCESS_LOGS, apiKeyUsageLog); } catch (Exception e) { log.error(ctx + ": {}", e.getMessage()); } @@ -167,46 +152,28 @@ public Optional findOneByApiKey(String apiKey) { return apiKeyRepository.findOneByApiKey(apiKey); } - public SearchHitsResponseDTO getApiKeyUsageLogs(User user, - ApiKeyUsageLogCriteria criteria, - Pageable pageable) { - final String ctx = CLASSNAME + ".getApiKeyUsageLogs"; - try { - CriteriaQuery query = new CriteriaQuery(criteria != null ? criteria.toCriteriaQuery() : new Criteria(), pageable); - query.addCriteria(new Criteria(ApiKeyUsageLog_.accountId).is(user.getAccountId())); - SearchHits result = elasticsearchOperations.search(query, ApiKeyUsageLogIndexDocument.class); - return SearchHitsResponseDTO.builder() - .totalHits(result.getTotalHits()) - .items(result.stream() - .map(SearchHit::getContent) - .collect(Collectors.toList())) - .build(); - } catch (Exception e) { - throw new RuntimeException(ctx + ": " + e.getMessage()); - } - } - @Scheduled(cron = "0 0 9 * * ?") + /*@Scheduled(cron = "0 0 9 * * ?") public void checkExpiringApiKeys() { Instant fiveDaysFromNow = Instant.now().plus(5, ChronoUnit.DAYS); Instant now = Instant.now(); List expiringKeys = apiKeyRepository.findAllByExpiresAtAfterAndExpiresAtLessThanEqual(now, fiveDaysFromNow); if (!expiringKeys.isEmpty()) { - Map> expiringKeysByAccount = expiringKeys.stream() - .collect(Collectors.groupingBy(ApiKey::getAccountId)); + Map> expiringKeysByAccount = expiringKeys.stream() + .collect(Collectors.groupingBy(ApiKey::getUserId)); - expiringKeysByAccount.forEach((accountId, apiKeys) -> { - var principal = userRepository.findByAccountIdAndAccountOwnerIsTrue(accountId.toString()).orElse(null); + expiringKeysByAccount.forEach((userId, apiKeys) -> { + var principal = userRepository.findByuserIdAndAccountOwnerIsTrue(userId.toString()).orElse(null); if (principal == null) { return; } mailService.sendKeyExpirationEmail(principal, apiKeys); userNotificationService.createAndSendNotification(principal.getUuid(), - NotificationMessageKeyEnum.API_KEY_EXPIRATION, + NotificationMessageKeyEnum.apiKey_EXPIRATION, Map.of("names", apiKeys.stream().map(ApiKey::getName).collect(Collectors.joining(",")))); }); } - } + }*/ } diff --git a/backend/src/main/java/com/park/utmstack/service/dto/api_key/ApiKeyResponseDTO.java b/backend/src/main/java/com/park/utmstack/service/dto/api_key/ApiKeyResponseDTO.java index 232e77f56..024f7282c 100644 --- a/backend/src/main/java/com/park/utmstack/service/dto/api_key/ApiKeyResponseDTO.java +++ b/backend/src/main/java/com/park/utmstack/service/dto/api_key/ApiKeyResponseDTO.java @@ -1,4 +1,4 @@ -package com.utmstack.api.service.dto.apikey; +package com.park.utmstack.service.dto.api_key; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; diff --git a/backend/src/main/java/com/park/utmstack/service/dto/api_key/ApiKeyUpsertDTO.java b/backend/src/main/java/com/park/utmstack/service/dto/api_key/ApiKeyUpsertDTO.java index 518aa6e35..6345f2032 100644 --- a/backend/src/main/java/com/park/utmstack/service/dto/api_key/ApiKeyUpsertDTO.java +++ b/backend/src/main/java/com/park/utmstack/service/dto/api_key/ApiKeyUpsertDTO.java @@ -1,14 +1,13 @@ -package com.utmstack.api.service.dto.apikey; +package com.park.utmstack.service.dto.api_key; - -import com.utmstack.api.annotation.ValidIPOrCIDR; +import com.park.utmstack.validation.api_key.ValidIPOrCIDR; import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotNull; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import javax.validation.constraints.NotNull; import java.time.Instant; import java.util.List; diff --git a/backend/src/main/java/com/park/utmstack/service/mapper/ApiKeyMapper.java b/backend/src/main/java/com/park/utmstack/service/mapper/ApiKeyMapper.java new file mode 100644 index 000000000..0439c563b --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/service/mapper/ApiKeyMapper.java @@ -0,0 +1,31 @@ +package com.park.utmstack.service.mapper; + +import com.park.utmstack.domain.api_keys.ApiKey; +import com.park.utmstack.service.dto.api_key.ApiKeyResponseDTO; +import org.mapstruct.Mapper; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Optional; +import java.util.stream.Collectors; + +@Mapper(componentModel = "spring") +public class ApiKeyMapper { + + public ApiKeyResponseDTO toDto(ApiKey apiKey){ + return ApiKeyResponseDTO.builder() + .id(apiKey.getId()) + .name(apiKey.getName()) + .createdAt(apiKey.getCreatedAt()) + .expiresAt(apiKey.getExpiresAt()) + .allowedIp( + Optional.ofNullable(apiKey.getAllowedIp()) + .map(s -> Arrays.stream(s.split(",")) + .map(String::trim) + .filter(str -> !str.isEmpty()) + .collect(Collectors.toList())) + .orElse(Collections.emptyList()) + ) + .build(); + } +} diff --git a/backend/src/main/java/com/park/utmstack/util/exceptions/ApiKeyExistException.java b/backend/src/main/java/com/park/utmstack/util/exceptions/ApiKeyExistException.java new file mode 100644 index 000000000..20577f508 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/util/exceptions/ApiKeyExistException.java @@ -0,0 +1,7 @@ +package com.park.utmstack.util.exceptions; + +public class ApiKeyExistException extends RuntimeException { + public ApiKeyExistException(String message) { + super(message); + } +} diff --git a/backend/src/main/java/com/park/utmstack/util/exceptions/ApiKeyInvalidAccessException.java b/backend/src/main/java/com/park/utmstack/util/exceptions/ApiKeyInvalidAccessException.java new file mode 100644 index 000000000..c5a13ead4 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/util/exceptions/ApiKeyInvalidAccessException.java @@ -0,0 +1,9 @@ +package com.park.utmstack.util.exceptions; + +import org.springframework.security.core.AuthenticationException; + +public class ApiKeyInvalidAccessException extends AuthenticationException { + public ApiKeyInvalidAccessException(String message) { + super(message); + } +} diff --git a/backend/src/main/java/com/park/utmstack/util/exceptions/ApiKeyNotFoundException.java b/backend/src/main/java/com/park/utmstack/util/exceptions/ApiKeyNotFoundException.java new file mode 100644 index 000000000..173d8f442 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/util/exceptions/ApiKeyNotFoundException.java @@ -0,0 +1,7 @@ +package com.park.utmstack.util.exceptions; + +public class ApiKeyNotFoundException extends RuntimeException { + public ApiKeyNotFoundException(String message) { + super(message); + } +} diff --git a/backend/src/main/java/com/park/utmstack/validation/api_key/ValidIPOrCIDR.java b/backend/src/main/java/com/park/utmstack/validation/api_key/ValidIPOrCIDR.java new file mode 100644 index 000000000..55dfe9593 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/validation/api_key/ValidIPOrCIDR.java @@ -0,0 +1,23 @@ +package com.park.utmstack.validation.api_key; + + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Documented +@Constraint(validatedBy = ValidIPOrCIDRValidator.class) +@Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE, TYPE_USE}) +@Retention(RUNTIME) +public @interface ValidIPOrCIDR { + String message() default "Invalid IP address or CIDR notation"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/backend/src/main/java/com/park/utmstack/validation/api_key/ValidIPOrCIDRValidator.java b/backend/src/main/java/com/park/utmstack/validation/api_key/ValidIPOrCIDRValidator.java new file mode 100644 index 000000000..8324094f8 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/validation/api_key/ValidIPOrCIDRValidator.java @@ -0,0 +1,37 @@ +package com.park.utmstack.validation.api_key; + + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import java.util.regex.Pattern; + +public class ValidIPOrCIDRValidator implements ConstraintValidator { + + private static final Pattern IPV4_PATTERN = Pattern.compile( + "^(?:(?:25[0-5]|2[0-4]\\d|[01]?\\d?\\d)\\.){3}(?:25[0-5]|2[0-4]\\d|[01]?\\d?\\d)$" + ); + + private static final Pattern IPV4_CIDR_PATTERN = Pattern.compile( + "^(?:(?:25[0-5]|2[0-4]\\d|[01]?\\d?\\d)\\.){3}(?:25[0-5]|2[0-4]\\d|[01]?\\d?\\d)/(\\d|[1-2]\\d|3[0-2])$" + ); + private static final Pattern IPV6_PATTERN = Pattern.compile( + "^(?:[\\da-fA-F]{1,4}:){7}[\\da-fA-F]{1,4}$" + ); + + private static final Pattern IPV6_CIDR_PATTERN = Pattern.compile( + "^(?:[\\da-fA-F]{1,4}:){7}[\\da-fA-F]{1,4}/(\\d|[1-9]\\d|1[01]\\d|12[0-8])$" + ); + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + // Allow null or empty values; use @NotNull/@NotEmpty to enforce non-null if needed. + if (value == null || value.trim().isEmpty()) { + return true; + } + String trimmed = value.trim(); + if (IPV4_PATTERN.matcher(trimmed).matches() || IPV4_CIDR_PATTERN.matcher(trimmed).matches()) { + return true; + } + return IPV6_PATTERN.matcher(trimmed).matches() || IPV6_CIDR_PATTERN.matcher(trimmed).matches(); + } +} diff --git a/backend/src/main/java/com/park/utmstack/web/rest/api-keys/UtmApiKeyController.java b/backend/src/main/java/com/park/utmstack/web/rest/api_key/ApiKeyResource.java similarity index 70% rename from backend/src/main/java/com/park/utmstack/web/rest/api-keys/UtmApiKeyController.java rename to backend/src/main/java/com/park/utmstack/web/rest/api_key/ApiKeyResource.java index 253fabc76..c5f17588d 100644 --- a/backend/src/main/java/com/park/utmstack/web/rest/api-keys/UtmApiKeyController.java +++ b/backend/src/main/java/com/park/utmstack/web/rest/api_key/ApiKeyResource.java @@ -1,3 +1,49 @@ +package com.park.utmstack.web.rest.api_key; + +import com.insecureweb.api.domain.User; +import com.insecureweb.api.domain.apikey.index.ApiKeyUsageLogIndexDocument; +import com.insecureweb.api.security.AuthoritiesConstants; +import com.insecureweb.api.security.SecurityUtils; +import com.insecureweb.api.service.ApiKeyService; +import com.insecureweb.api.service.criteria.apikey.ApiKeyUsageLogCriteria; +import com.insecureweb.api.service.dto.SearchHitsResponseDTO; +import com.insecureweb.api.service.dto.apikey.ApiKeyResponseDTO; +import com.insecureweb.api.service.dto.apikey.ApiKeyUpsertDTO; +import com.insecureweb.api.service.exceptions.*; +import com.insecureweb.api.service.user.UserService; +import com.insecureweb.api.util.ResponseUtil; +import com.insecureweb.api.web.rest.restutil.ResponseSearchHitsUtil; +import com.park.utmstack.domain.application_events.enums.ApplicationEventType; +import com.park.utmstack.domain.chart_builder.types.query.FilterType; +import com.park.utmstack.domain.chart_builder.types.query.OperatorType; +import com.park.utmstack.util.ResponseUtil; +import com.park.utmstack.util.UtilPagination; +import io.swagger.v3.oas.annotations.Hidden; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.headers.Header; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import lombok.AllArgsConstructor; +import org.opensearch.client.opensearch.core.SearchResponse; +import org.opensearch.client.opensearch.core.search.Hit; +import org.opensearch.client.opensearch.core.search.HitsMetadata; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springdoc.core.annotations.ParameterObject; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; +import tech.jhipster.web.util.PaginationUtil; + +import java.util.*; + @RestController @RequestMapping("/api/api-keys") @PreAuthorize("hasAuthority(\"" + AuthoritiesConstants.USER + "\")") @@ -176,4 +222,30 @@ public ResponseEntity deleteApiKey(@PathVariable("id") UUID apiKeyId) { return ResponseUtil.buildInternalServerErrorResponse(msg); } } + + @GetMapping("/usage") + public ResponseEntity> search(@RequestBody(required = false) List filters, + @RequestParam Integer top, @RequestParam String indexPattern, + @RequestParam(required = false, defaultValue = "false") boolean includeChildren, + Pageable pageable) { + final String ctx = CLASSNAME + ".search"; + try { + SearchResponse searchResponse = elasticsearchService.search(filters, top, indexPattern, + pageable, Map.class); + + if (Objects.isNull(searchResponse) || Objects.isNull(searchResponse.hits()) || searchResponse.hits().total().value() == 0) + return ResponseEntity.ok(Collections.emptyList()); + + HitsMetadata hits = searchResponse.hits(); + HttpHeaders headers = UtilPagination.generatePaginationHttpHeaders(Math.min(hits.total().value(), top), + pageable.getPageNumber(), pageable.getPageSize(), "/api/elasticsearch/search"); + + return ResponseEntity.ok().headers(headers).body(results); + } catch (Exception e) { + String msg = ctx + ": " + e.getMessage(); + log.error(msg); + applicationEventService.createEvent(msg, ApplicationEventType.ERROR); + return com.park.utmstack.util.ResponseUtil.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); + } + } } diff --git a/backend/src/main/resources/config/liquibase/changelog/20250917001_adding_api_keys.xml b/backend/src/main/resources/config/liquibase/changelog/20251017001_create_api_keys_table.xml similarity index 100% rename from backend/src/main/resources/config/liquibase/changelog/20250917001_adding_api_keys.xml rename to backend/src/main/resources/config/liquibase/changelog/20251017001_create_api_keys_table.xml diff --git a/backend/src/main/resources/config/liquibase/master.xml b/backend/src/main/resources/config/liquibase/master.xml index b7ab36816..8735ef517 100644 --- a/backend/src/main/resources/config/liquibase/master.xml +++ b/backend/src/main/resources/config/liquibase/master.xml @@ -253,7 +253,7 @@ - + From a30c27ec3bddffd01d82108c751df5f607aaf142 Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Fri, 17 Oct 2025 13:31:26 -0500 Subject: [PATCH 007/128] refactor(api_keys): simplify API key management by removing user ID dependency in service methods --- .../advice/GlobalExceptionHandler.java | 15 +- .../park/utmstack/service/UserService.java | 3 +- .../service/api_key/ApiKeyService.java | 20 ++- .../web/rest/api_key/ApiKeyResource.java | 159 +++++------------- 4 files changed, 65 insertions(+), 132 deletions(-) diff --git a/backend/src/main/java/com/park/utmstack/advice/GlobalExceptionHandler.java b/backend/src/main/java/com/park/utmstack/advice/GlobalExceptionHandler.java index 937bdde86..b84c2ea14 100644 --- a/backend/src/main/java/com/park/utmstack/advice/GlobalExceptionHandler.java +++ b/backend/src/main/java/com/park/utmstack/advice/GlobalExceptionHandler.java @@ -4,10 +4,7 @@ import com.park.utmstack.security.TooMuchLoginAttemptsException; import com.park.utmstack.service.application_events.ApplicationEventService; import com.park.utmstack.util.ResponseUtil; -import com.park.utmstack.util.exceptions.IncidentAlertConflictException; -import com.park.utmstack.util.exceptions.NoAlertsProvidedException; -import com.park.utmstack.util.exceptions.TfaVerificationException; -import com.park.utmstack.util.exceptions.TooManyRequestsException; +import com.park.utmstack.util.exceptions.*; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; @@ -41,8 +38,9 @@ public ResponseEntity handleTooManyLoginAttempts(TooMuchLoginAttemptsExceptio return ResponseUtil.buildLockedResponse(e.getMessage()); } - @ExceptionHandler(NoSuchElementException.class) - public ResponseEntity handleNotFound(NoSuchElementException e, HttpServletRequest request) { + @ExceptionHandler({NoSuchElementException.class, + ApiKeyNotFoundException.class}) + public ResponseEntity handleNotFound(Exception e, HttpServletRequest request) { return ResponseUtil.buildNotFoundResponse(e.getMessage()); } @@ -56,8 +54,9 @@ public ResponseEntity handleNoAlertsProvided(Exception e, HttpServletRequest return ResponseUtil.buildErrorResponse(HttpStatus.BAD_REQUEST, e.getMessage()); } - @ExceptionHandler(IncidentAlertConflictException.class) - public ResponseEntity handleConflict(IncidentAlertConflictException e, HttpServletRequest request) { + @ExceptionHandler({IncidentAlertConflictException.class, + ApiKeyExistException.class}) + public ResponseEntity handleConflict(Exception e, HttpServletRequest request) { return ResponseUtil.buildErrorResponse(HttpStatus.CONFLICT, e.getMessage()); } diff --git a/backend/src/main/java/com/park/utmstack/service/UserService.java b/backend/src/main/java/com/park/utmstack/service/UserService.java index ed1a6379b..5dacde192 100644 --- a/backend/src/main/java/com/park/utmstack/service/UserService.java +++ b/backend/src/main/java/com/park/utmstack/service/UserService.java @@ -301,6 +301,7 @@ public List getAuthorities() { public User getCurrentUserLogin() { String userLogin = SecurityUtils.getCurrentUserLogin().orElseThrow(() -> new CurrentUserLoginNotFoundException("No current user login was found")); - return userRepository.findOneWithAuthoritiesByLogin(userLogin).orElseThrow(() -> new CurrentUserLoginNotFoundException(String.format("No user with login %1$s was found", userLogin))); + return userRepository.findOneWithAuthoritiesByLogin(userLogin) + .orElseThrow(() -> new CurrentUserLoginNotFoundException(String.format("No user with login %1$s was found", userLogin))); } } diff --git a/backend/src/main/java/com/park/utmstack/service/api_key/ApiKeyService.java b/backend/src/main/java/com/park/utmstack/service/api_key/ApiKeyService.java index cb402e356..71310d26d 100644 --- a/backend/src/main/java/com/park/utmstack/service/api_key/ApiKeyService.java +++ b/backend/src/main/java/com/park/utmstack/service/api_key/ApiKeyService.java @@ -3,6 +3,7 @@ import com.park.utmstack.domain.api_keys.ApiKey; import com.park.utmstack.domain.api_keys.ApiKeyUsageLog; import com.park.utmstack.repository.api_key.ApiKeyRepository; +import com.park.utmstack.service.UserService; import com.park.utmstack.service.dto.api_key.ApiKeyResponseDTO; import com.park.utmstack.service.dto.api_key.ApiKeyUpsertDTO; import com.park.utmstack.service.elasticsearch.OpensearchClientBuilder; @@ -35,11 +36,13 @@ public class ApiKeyService { private final ApiKeyRepository apiKeyRepository; private final ApiKeyMapper apiKeyMapper; private final OpensearchClientBuilder client; + private final UserService userService; - public ApiKeyResponseDTO createApiKey(Long userId, ApiKeyUpsertDTO dto) { + public ApiKeyResponseDTO createApiKey(ApiKeyUpsertDTO dto) { final String ctx = CLASSNAME + ".createApiKey"; try { + Long userId = userService.getCurrentUserLogin().getId(); apiKeyRepository.findByNameAndUserId(dto.getName(), userId) .ifPresent(apiKey -> { throw new ApiKeyExistException("Api key already exists"); @@ -60,9 +63,10 @@ public ApiKeyResponseDTO createApiKey(Long userId, ApiKeyUpsertDTO dto) { } } - public String generateApiKey(Long userId, UUID apiKeyId) { + public String generateApiKey(UUID apiKeyId) { final String ctx = CLASSNAME + ".generateApiKey"; try { + Long userId = userService.getCurrentUserLogin().getId(); ApiKey apiKey = apiKeyRepository.findByIdAndUserId(apiKeyId, userId) .orElseThrow(() -> new ApiKeyNotFoundException("API key not found")); String plainKey = generateRandomKey(); @@ -75,9 +79,10 @@ public String generateApiKey(Long userId, UUID apiKeyId) { } } - public ApiKeyResponseDTO updateApiKey(Long userId, UUID apiKeyId, ApiKeyUpsertDTO dto) { + public ApiKeyResponseDTO updateApiKey(UUID apiKeyId, ApiKeyUpsertDTO dto) { final String ctx = CLASSNAME + ".updateApiKey"; try { + Long userId = userService.getCurrentUserLogin().getId(); ApiKey apiKey = apiKeyRepository.findByIdAndUserId(apiKeyId, userId) .orElseThrow(() -> new ApiKeyNotFoundException("API key not found")); apiKey.setName(dto.getName()); @@ -94,9 +99,10 @@ public ApiKeyResponseDTO updateApiKey(Long userId, UUID apiKeyId, ApiKeyUpsertDT } } - public ApiKeyResponseDTO getApiKey(Long userId, UUID apiKeyId) { + public ApiKeyResponseDTO getApiKey(UUID apiKeyId) { final String ctx = CLASSNAME + ".getApiKey"; try { + Long userId = userService.getCurrentUserLogin().getId(); ApiKey apiKey = apiKeyRepository.findByIdAndUserId(apiKeyId, userId) .orElseThrow(() -> new ApiKeyNotFoundException("API key not found")); return apiKeyMapper.toDto(apiKey); @@ -105,9 +111,10 @@ public ApiKeyResponseDTO getApiKey(Long userId, UUID apiKeyId) { } } - public Page listApiKeys(Long userId, Pageable pageable) { + public Page listApiKeys(Pageable pageable) { final String ctx = CLASSNAME + ".listApiKeys"; try { + Long userId = userService.getCurrentUserLogin().getId(); return apiKeyRepository.findByUserId(userId, pageable).map(apiKeyMapper::toDto); } catch (Exception e) { throw new RuntimeException(ctx + ": " + e.getMessage()); @@ -115,9 +122,10 @@ public Page listApiKeys(Long userId, Pageable pageable) { } - public void deleteApiKey(Long userId, UUID apiKeyId) { + public void deleteApiKey(UUID apiKeyId) { final String ctx = CLASSNAME + ".deleteApiKey"; try { + Long userId = userService.getCurrentUserLogin().getId(); ApiKey apiKey = apiKeyRepository.findByIdAndUserId(apiKeyId, userId) .orElseThrow(() -> new ApiKeyNotFoundException("API key not found")); apiKeyRepository.delete(apiKey); diff --git a/backend/src/main/java/com/park/utmstack/web/rest/api_key/ApiKeyResource.java b/backend/src/main/java/com/park/utmstack/web/rest/api_key/ApiKeyResource.java index c5f17588d..9f012f765 100644 --- a/backend/src/main/java/com/park/utmstack/web/rest/api_key/ApiKeyResource.java +++ b/backend/src/main/java/com/park/utmstack/web/rest/api_key/ApiKeyResource.java @@ -1,23 +1,14 @@ package com.park.utmstack.web.rest.api_key; -import com.insecureweb.api.domain.User; -import com.insecureweb.api.domain.apikey.index.ApiKeyUsageLogIndexDocument; -import com.insecureweb.api.security.AuthoritiesConstants; -import com.insecureweb.api.security.SecurityUtils; -import com.insecureweb.api.service.ApiKeyService; -import com.insecureweb.api.service.criteria.apikey.ApiKeyUsageLogCriteria; -import com.insecureweb.api.service.dto.SearchHitsResponseDTO; -import com.insecureweb.api.service.dto.apikey.ApiKeyResponseDTO; -import com.insecureweb.api.service.dto.apikey.ApiKeyUpsertDTO; -import com.insecureweb.api.service.exceptions.*; -import com.insecureweb.api.service.user.UserService; -import com.insecureweb.api.util.ResponseUtil; -import com.insecureweb.api.web.rest.restutil.ResponseSearchHitsUtil; -import com.park.utmstack.domain.application_events.enums.ApplicationEventType; + import com.park.utmstack.domain.chart_builder.types.query.FilterType; -import com.park.utmstack.domain.chart_builder.types.query.OperatorType; -import com.park.utmstack.util.ResponseUtil; +import com.park.utmstack.security.AuthoritiesConstants; +import com.park.utmstack.service.api_key.ApiKeyService; +import com.park.utmstack.service.dto.api_key.ApiKeyResponseDTO; +import com.park.utmstack.service.dto.api_key.ApiKeyUpsertDTO; +import com.park.utmstack.service.elasticsearch.ElasticsearchService; import com.park.utmstack.util.UtilPagination; +import com.park.utmstack.web.rest.elasticsearch.ElasticsearchResource; import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.headers.Header; @@ -26,12 +17,11 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.opensearch.client.opensearch.core.SearchResponse; import org.opensearch.client.opensearch.core.search.Hit; import org.opensearch.client.opensearch.core.search.HitsMetadata; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springdoc.core.annotations.ParameterObject; +import org.springdoc.api.annotations.ParameterObject; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.http.HttpHeaders; @@ -43,24 +33,18 @@ import tech.jhipster.web.util.PaginationUtil; import java.util.*; +import java.util.stream.Collectors; @RestController @RequestMapping("/api/api-keys") @PreAuthorize("hasAuthority(\"" + AuthoritiesConstants.USER + "\")") +@Slf4j @AllArgsConstructor @Hidden public class ApiKeyResource { - private static final String CLASSNAME = "ApiKeyResource"; - private final Logger log = LoggerFactory.getLogger(ApiKeyResource.class); - private final ApiKeyService apiKeyService; - private final UserService userService; - - private UUID getCurrentAccountId() throws CurrentUserLoginNotFoundException { - User user = userService.getUserWithAuthoritiesByLogin(SecurityUtils.currentUserLogin()); - return UUID.fromString(user.getAccountId()); - } + private final ElasticsearchService elasticsearchService; @Operation(summary = "Create API key", description = "Creates a new API key record using the provided settings. The plain text key is not generated at creation.") @@ -75,18 +59,8 @@ private UUID getCurrentAccountId() throws CurrentUserLoginNotFoundException { }) @PostMapping public ResponseEntity createApiKey(@RequestBody ApiKeyUpsertDTO dto) { - final String ctx = CLASSNAME + ".createApiKey"; - try { - UUID accountId = getCurrentAccountId(); - ApiKeyResponseDTO responseDTO = apiKeyService.createApiKey(accountId, dto); + ApiKeyResponseDTO responseDTO = apiKeyService.createApiKey(dto); return ResponseEntity.status(HttpStatus.CREATED).body(responseDTO); - } catch (ApiKeyExistException e) { - return ResponseUtil.buildConflictResponse(e.getMessage()); - } catch (Exception e) { - String msg = ctx + ": " + e.getMessage(); - log.error(msg, e); - return ResponseUtil.buildInternalServerErrorResponse(msg); - } } @Operation(summary = "Generate a new API key", @@ -102,18 +76,8 @@ public ResponseEntity createApiKey(@RequestBody ApiKeyUpsertD }) @PostMapping("/{id}/generate") public ResponseEntity generateApiKey(@PathVariable("id") UUID apiKeyId) { - final String ctx = CLASSNAME + ".generateApiKey"; - try { - UUID accountId = getCurrentAccountId(); - String plainKey = apiKeyService.generateApiKey(accountId, apiKeyId); - return ResponseEntity.ok(plainKey); - } catch (ApiKeyNotFoundException e) { - return ResponseUtil.buildNotFoundResponse(e.getMessage()); - } catch (Exception e) { - String msg = ctx + ": " + e.getMessage(); - log.error(msg, e); - return ResponseUtil.buildInternalServerErrorResponse(msg); - } + String plainKey = apiKeyService.generateApiKey(apiKeyId); + return ResponseEntity.ok(plainKey); } @Operation(summary = "Retrieve API key", @@ -129,18 +93,8 @@ public ResponseEntity generateApiKey(@PathVariable("id") UUID apiKeyId) }) @GetMapping("/{id}") public ResponseEntity getApiKey(@PathVariable("id") UUID apiKeyId) { - final String ctx = CLASSNAME + ".getApiKey"; - try { - UUID accountId = getCurrentAccountId(); - ApiKeyResponseDTO responseDTO = apiKeyService.getApiKey(accountId, apiKeyId); - return ResponseEntity.ok(responseDTO); - } catch (ApiKeyNotFoundException e) { - return ResponseUtil.buildNotFoundResponse(e.getMessage()); - } catch (Exception e) { - String msg = ctx + ": " + e.getMessage(); - log.error(msg, e); - return ResponseUtil.buildInternalServerErrorResponse(msg); - } + ApiKeyResponseDTO responseDTO = apiKeyService.getApiKey(apiKeyId); + return ResponseEntity.ok(responseDTO); } @Operation(summary = "List API keys", @@ -156,17 +110,10 @@ public ResponseEntity getApiKey(@PathVariable("id") UUID apiK }) @GetMapping("") public ResponseEntity> listApiKeys(@ParameterObject Pageable pageable) { - final String ctx = CLASSNAME + ".listApiKeys"; - try { - UUID accountId = getCurrentAccountId(); - Page page = apiKeyService.listApiKeys(accountId, pageable); - HttpHeaders headers = PaginationUtil.generatePaginationHttpHeaders(ServletUriComponentsBuilder.fromCurrentRequest(), page); - return ResponseEntity.ok().headers(headers).body(page.getContent()); - } catch (Exception e) { - String msg = ctx + ": " + e.getMessage(); - log.error(msg, e); - return ResponseUtil.buildInternalServerErrorResponse(msg); - } + Page page = apiKeyService.listApiKeys(pageable); + HttpHeaders headers = PaginationUtil.generatePaginationHttpHeaders(ServletUriComponentsBuilder.fromCurrentRequest(), page); + + return ResponseEntity.ok().headers(headers).body(page.getContent()); } @Operation(summary = "Update API key", @@ -183,18 +130,10 @@ public ResponseEntity> listApiKeys(@ParameterObject Page @PutMapping("/{id}") public ResponseEntity updateApiKey(@PathVariable("id") UUID apiKeyId, @RequestBody ApiKeyUpsertDTO dto) { - final String ctx = CLASSNAME + ".updateApiKey"; - try { - UUID accountId = getCurrentAccountId(); - ApiKeyResponseDTO responseDTO = apiKeyService.updateApiKey(accountId, apiKeyId, dto); - return ResponseEntity.ok(responseDTO); - } catch (ApiKeyNotFoundException e) { - return ResponseUtil.buildNotFoundResponse(e.getMessage()); - } catch (Exception e) { - String msg = ctx + ": " + e.getMessage(); - log.error(msg, e); - return ResponseUtil.buildInternalServerErrorResponse(msg); - } + + ApiKeyResponseDTO responseDTO = apiKeyService.updateApiKey(apiKeyId, dto); + return ResponseEntity.ok(responseDTO); + } @Operation(summary = "Delete API key", @@ -209,18 +148,9 @@ public ResponseEntity updateApiKey(@PathVariable("id") UUID a }) @DeleteMapping("/{id}") public ResponseEntity deleteApiKey(@PathVariable("id") UUID apiKeyId) { - final String ctx = CLASSNAME + ".deleteApiKey"; - try { - UUID accountId = getCurrentAccountId(); - apiKeyService.deleteApiKey(accountId, apiKeyId); - return ResponseEntity.noContent().build(); - } catch (ApiKeyNotFoundException e) { - return ResponseUtil.buildNotFoundResponse(e.getMessage()); - } catch (Exception e) { - String msg = ctx + ": " + e.getMessage(); - log.error(msg, e); - return ResponseUtil.buildInternalServerErrorResponse(msg); - } + + apiKeyService.deleteApiKey(apiKeyId); + return ResponseEntity.noContent().build(); } @GetMapping("/usage") @@ -228,24 +158,19 @@ public ResponseEntity> search(@RequestBody(required = false) List searchResponse = elasticsearchService.search(filters, top, indexPattern, - pageable, Map.class); - - if (Objects.isNull(searchResponse) || Objects.isNull(searchResponse.hits()) || searchResponse.hits().total().value() == 0) - return ResponseEntity.ok(Collections.emptyList()); - - HitsMetadata hits = searchResponse.hits(); - HttpHeaders headers = UtilPagination.generatePaginationHttpHeaders(Math.min(hits.total().value(), top), - pageable.getPageNumber(), pageable.getPageSize(), "/api/elasticsearch/search"); - - return ResponseEntity.ok().headers(headers).body(results); - } catch (Exception e) { - String msg = ctx + ": " + e.getMessage(); - log.error(msg); - applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return com.park.utmstack.util.ResponseUtil.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); - } + + SearchResponse searchResponse = elasticsearchService.search(filters, top, indexPattern, + pageable, Map.class); + + if (Objects.isNull(searchResponse) || Objects.isNull(searchResponse.hits()) || searchResponse.hits().total().value() == 0) + return ResponseEntity.ok(Collections.emptyList()); + + HitsMetadata hits = searchResponse.hits(); + HttpHeaders headers = UtilPagination.generatePaginationHttpHeaders(Math.min(hits.total().value(), top), + pageable.getPageNumber(), pageable.getPageSize(), "/api/elasticsearch/search"); + + return ResponseEntity.ok().headers(headers).body(hits.hits().stream() + .map(Hit::source).collect(Collectors.toList())); + } } From ef0398e0c7d5ec99ff92a40f0d558c79b5ef0167 Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Sun, 19 Oct 2025 10:07:52 -0500 Subject: [PATCH 008/128] feat(api_keys): implement API key filtering and usage logging for enhanced security --- backend/pom.xml | 6 + .../com/park/utmstack/config/Constants.java | 5 + .../domain/api_keys/ApiKeyUsageLog.java | 53 +++--- .../enums/ApplicationEventType.java | 6 +- .../api_key/ApiKeyUsageLoggingService.java | 138 ++++++++++++++ .../security/api_key/ApiKeyFilter.java | 176 ++++++++++++++++++ .../ApplicationEventService.java | 2 +- 7 files changed, 362 insertions(+), 24 deletions(-) create mode 100644 backend/src/main/java/com/park/utmstack/loggin/api_key/ApiKeyUsageLoggingService.java create mode 100644 backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java diff --git a/backend/pom.xml b/backend/pom.xml index 325396715..2109f1376 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -376,6 +376,12 @@ 3.0.5 + + commons-net + commons-net + 3.9.0 + + diff --git a/backend/src/main/java/com/park/utmstack/config/Constants.java b/backend/src/main/java/com/park/utmstack/config/Constants.java index 5c85a810f..d8e7a86a1 100644 --- a/backend/src/main/java/com/park/utmstack/config/Constants.java +++ b/backend/src/main/java/com/park/utmstack/config/Constants.java @@ -2,7 +2,9 @@ import com.park.utmstack.domain.index_pattern.enums.SystemIndexPattern; +import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; public final class Constants { @@ -152,6 +154,9 @@ public final class Constants { public static final String ENV_TFA_ENABLE = "APP_TFA_ENABLED"; public static final String TFA_EXEMPTION_HEADER = "X-Bypass-TFA"; + public static final String API_KEY_HEADER = "api-key"; + public static final List API_ENDPOINT_IGNORE = Collections.emptyList(); + private Constants() { } } diff --git a/backend/src/main/java/com/park/utmstack/domain/api_keys/ApiKeyUsageLog.java b/backend/src/main/java/com/park/utmstack/domain/api_keys/ApiKeyUsageLog.java index d097aa4fb..54e0c8d15 100644 --- a/backend/src/main/java/com/park/utmstack/domain/api_keys/ApiKeyUsageLog.java +++ b/backend/src/main/java/com/park/utmstack/domain/api_keys/ApiKeyUsageLog.java @@ -1,40 +1,49 @@ package com.park.utmstack.domain.api_keys; - +import com.park.utmstack.service.dto.auditable.AuditableDTO; import lombok.*; -import java.time.Instant; -import java.util.UUID; +import java.util.HashMap; +import java.util.Map; + +@Builder @Getter @Setter @NoArgsConstructor @AllArgsConstructor -@Builder -public class ApiKeyUsageLog { - - private UUID id; - - private UUID apiKeyId; +public class ApiKeyUsageLog implements AuditableDTO { + private String id; + private String apiKeyId; private String apiKeyName; - - private Long userId; - - private Instant timestamp; - + private String userId; + private String timestamp; private String endpoint; - private String address; - private String errorMessage; - private String queryParams; - private String payload; - private String userAgent; - private String httpMethod; - - private Integer statusCode; + private String statusCode; + + @Override + public Map toAuditMap() { + Map map = new HashMap<>(); + + map.put("id", id); + map.put("api_key_id", apiKeyId); + map.put("api_key_name", apiKeyName); + map.put("user_id", userId); + map.put("timestamp", timestamp != null ? timestamp : null); + map.put("endpoint", endpoint); + map.put("address", address); + map.put("error_message", errorMessage); + map.put("query_params", queryParams); + map.put("user_agent", userAgent); + map.put("http_method", httpMethod); + map.put("status_code", statusCode); + + return map; + } } diff --git a/backend/src/main/java/com/park/utmstack/domain/application_events/enums/ApplicationEventType.java b/backend/src/main/java/com/park/utmstack/domain/application_events/enums/ApplicationEventType.java index 9df92c191..eca45ec19 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_events/enums/ApplicationEventType.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_events/enums/ApplicationEventType.java @@ -42,5 +42,9 @@ public enum ApplicationEventType { ERROR, WARNING, INFO, - MODULE_ACTIVATION_ATTEMPT, MODULE_ACTIVATION_SUCCESS, UNDEFINED + MODULE_ACTIVATION_ATTEMPT, + MODULE_ACTIVATION_SUCCESS, + API_KEY_ACCESS_SUCCESS, + API_KEY_ACCESS_FAILURE, + UNDEFINED } diff --git a/backend/src/main/java/com/park/utmstack/loggin/api_key/ApiKeyUsageLoggingService.java b/backend/src/main/java/com/park/utmstack/loggin/api_key/ApiKeyUsageLoggingService.java new file mode 100644 index 000000000..e0969e2c0 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/loggin/api_key/ApiKeyUsageLoggingService.java @@ -0,0 +1,138 @@ +package com.park.utmstack.loggin.api_key; + +import com.park.utmstack.domain.api_keys.ApiKey; +import com.park.utmstack.domain.api_keys.ApiKeyUsageLog; +import com.park.utmstack.domain.application_events.enums.ApplicationEventType; +import com.park.utmstack.service.api_key.ApiKeyService; +import com.park.utmstack.service.application_events.ApplicationEventService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.web.util.ContentCachingRequestWrapper; +import org.springframework.web.util.ContentCachingResponseWrapper; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.UUID; + +@Service +@Slf4j +@RequiredArgsConstructor +public class ApiKeyUsageLoggingService { + + private final ApiKeyService apiKeyService; + private final ApplicationEventService applicationEventService; + private static final String LOG_USAGE_FLAG = "LOG_USAGE_DONE"; + + public void logUsage(HttpServletRequest request, + HttpServletResponse response, + ApiKey apiKey, + String ipAddress, + String message) { + + if (Boolean.TRUE.equals(request.getAttribute(LOG_USAGE_FLAG))) { + return; + } + + try { + String payload = extractPayload(request); + String errorText = extractErrorText(response); + int status = safeStatus(response); + + ApiKeyUsageLog usage = buildUsageLog(apiKey, ipAddress, request, status, errorText, payload, message); + + apiKeyService.logUsage(usage); + + ApplicationEventType eventType = (status >= 400) + ? ApplicationEventType.API_KEY_ACCESS_FAILURE + : ApplicationEventType.API_KEY_ACCESS_SUCCESS; + + String eventMessage = (status >= 400) + ? "API key access failure" + : "API key access"; + + applicationEventService.createEvent(eventMessage, eventType, usage.toAuditMap()); + + } catch (Exception e) { + log.error("Error while logging API key usage: {}", e.getMessage(), e); + } finally { + request.setAttribute(LOG_USAGE_FLAG, Boolean.TRUE); + } + } + + private int safeStatus(HttpServletResponse response) { + try { + return response.getStatus(); + } catch (Exception e) { + return 0; + } + } + + private String extractPayload(HttpServletRequest request) { + if (request instanceof ContentCachingRequestWrapper wrapper) { + byte[] buf = wrapper.getContentAsByteArray(); + if (buf.length > 0) { + return extractBody(buf); + } + } + return null; + } + + private String extractErrorText(HttpServletResponse response) { + if (response instanceof ContentCachingResponseWrapper wrapper) { + byte[] buf = wrapper.getContentAsByteArray(); + if (buf.length > 0) { + return extractBody(buf); + } + } + return null; + } + + private String extractBody(byte[] buf) { + return buf.length > 0 ? new String(buf, StandardCharsets.UTF_8) : null; + } + + private ApiKeyUsageLog buildUsageLog(ApiKey apiKey, + String ipAddress, + HttpServletRequest request, + int status, + String errorText, + String payload, + String message) { + + String id = UUID.randomUUID().toString(); + String apiKeyId = apiKey != null && apiKey.getId() != null ? apiKey.getId().toString() : null; + String apiKeyName = apiKey != null ? apiKey.getName() : null; + String userId = apiKey != null && apiKey.getUserId() != null ? apiKey.getUserId().toString() : null; + String timestamp = Instant.now().toString(); + String endpoint = request != null ? request.getRequestURI() : null; + String queryParams = request != null ? request.getQueryString() : null; + String userAgent = request != null ? request.getHeader("User-Agent") : null; + String httpMethod = request != null ? request.getMethod() : null; + String statusCode = String.valueOf(status); + + String safePayload = null; + if (payload != null) { + int PAYLOAD_MAX_LENGTH = 2000; + safePayload = payload.length() > PAYLOAD_MAX_LENGTH ? payload.substring(0, PAYLOAD_MAX_LENGTH) : payload; + } + + return ApiKeyUsageLog.builder() + .id(id) + .apiKeyId(apiKeyId) + .apiKeyName(apiKeyName) + .userId(userId) + .timestamp(timestamp) + .endpoint(endpoint) + .address(ipAddress) + .errorMessage(errorText) + .queryParams(queryParams) + .payload(safePayload) + .userAgent(userAgent) + .httpMethod(httpMethod) + .statusCode(statusCode) + .build(); + } +} diff --git a/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java b/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java new file mode 100644 index 000000000..f629da7fc --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java @@ -0,0 +1,176 @@ +package com.park.utmstack.security.api_key; + + +import com.park.utmstack.config.Constants; +import com.park.utmstack.domain.api_keys.ApiKey; +import com.park.utmstack.loggin.api_key.ApiKeyUsageLoggingService; +import com.park.utmstack.repository.UserRepository; +import com.park.utmstack.service.api_key.ApiKeyService; +import com.park.utmstack.service.application_events.ApplicationEventService; +import com.park.utmstack.util.exceptions.ApiKeyInvalidAccessException; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.net.util.SubnetUtils; +import org.springframework.core.annotation.Order; +import org.springframework.lang.NonNull; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.util.ContentCachingRequestWrapper; +import org.springframework.web.util.ContentCachingResponseWrapper; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.time.Instant; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.regex.Pattern; + +import static com.park.utmstack.config.Constants.API_ENDPOINT_IGNORE; + +@Order(1) +@AllArgsConstructor +@Slf4j +@Component +public class ApiKeyFilter extends OncePerRequestFilter { + + private static final String LOG_USAGE_FLAG = "LOG_USAGE_DONE"; + private static final Pattern CIDR_PATTERN = Pattern.compile( + "^((25[0-5]|2[0-4]\\d|[01]?\\d\\d?)(\\.(?!$)|$)){4}/(\\d|[1-2]\\d|3[0-2])$" + ); + + private final UserRepository userRepository; + private final ApiKeyService apiKeyService; + private final ConcurrentMap invalidApiKeyBlackList = new ConcurrentHashMap<>(); + private final ConcurrentMap cidrCache = new ConcurrentHashMap<>(); + private final ApplicationEventService applicationEventService; + private final ApiKeyUsageLoggingService apiKeyUsageLoggingService; + + @Override + protected void doFilterInternal(@NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain) throws ServletException, IOException { + + if (API_ENDPOINT_IGNORE.contains(request.getRequestURI())) { + filterChain.doFilter(request, response); + return; + } + + if (request.getAttribute(LOG_USAGE_FLAG) != null) { + filterChain.doFilter(request, response); + return; + } + + String apiKey = request.getHeader(Constants.API_KEY_HEADER); + + if (!StringUtils.hasText(apiKey)) { + filterChain.doFilter(request, response); + return; + } + + String ipAddress = request.getRemoteAddr(); + var key = getApiKey(apiKey); + + var wrappedRequest = new ContentCachingRequestWrapper(request); + var wrappedResponse = new ContentCachingResponseWrapper(response); + + UsernamePasswordAuthenticationToken authentication; + + try { + authentication = getAuthentication(key, ipAddress); + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(wrappedRequest)); + SecurityContextHolder.getContext().setAuthentication(authentication); + + } catch (ApiKeyInvalidAccessException e) { + apiKeyUsageLoggingService.logUsage(wrappedRequest, response, key, ipAddress, e.getMessage()); + throw e; + } + + filterChain.doFilter(wrappedRequest, wrappedResponse); + wrappedResponse.copyBodyToResponse(); + + apiKeyUsageLoggingService.logUsage(wrappedRequest, response, key, ipAddress, null); + } + + private ApiKey getApiKey(String apiKey) { + if (invalidApiKeyBlackList.containsKey(apiKey)) { + throw new ApiKeyInvalidAccessException("Invalid API key"); + } + return apiKeyService.findOneByApiKey(apiKey) + .orElseThrow(() -> { + invalidApiKeyBlackList.put(apiKey, Boolean.TRUE); + return new ApiKeyInvalidAccessException("Invalid API key"); + }); + } + + public UsernamePasswordAuthenticationToken getAuthentication(ApiKey apiKey, String remoteIpAddress) { + try { + + if (!allowAccessToRemoteIp(apiKey.getAllowedIp(), remoteIpAddress)) { + throw new ApiKeyInvalidAccessException("Invalid IP address, if you recognize this IP address, add to allowed ip list"); + } + + if (apiKey.getExpiresAt() != null && !apiKey.getExpiresAt().isAfter(Instant.now())) { + throw new ApiKeyInvalidAccessException("API key expired at " + apiKey.getExpiresAt()); + } + + var userEntity = userRepository + .findById(apiKey.getUserId()) + .orElseThrow(() -> new ApiKeyInvalidAccessException("User not found for api key")); + + if (!userEntity.getActivated()) { + throw new ApiKeyInvalidAccessException("User not activated"); + } + + List authorities = userEntity.getAuthorities().stream() + .map(auth -> new SimpleGrantedAuthority(auth.getName())) + .toList(); + + User principal = new User(userEntity.getLogin(), "", authorities); + + return new UsernamePasswordAuthenticationToken(principal, apiKey.getApiKey(), authorities); + + } catch (Exception e) { + throw new ApiKeyInvalidAccessException(e.getMessage()); + } + } + + public boolean allowAccessToRemoteIp(String allowedIpList, String remoteIp) { + if (allowedIpList == null || allowedIpList.trim().isEmpty()) { + return true; + } + String[] whitelistIps = allowedIpList.split(","); + for (String ip : whitelistIps) { + String allowed = ip.trim(); + if (allowed.isEmpty()) { + continue; + } + if (CIDR_PATTERN.matcher(allowed).matches()) { + try { + SubnetUtils subnetUtils = cidrCache.computeIfAbsent(allowed, key -> { + SubnetUtils su = new SubnetUtils(key); + su.setInclusiveHostCount(true); + return su; + }); + if (subnetUtils.getInfo().isInRange(remoteIp)) { + return true; + } + } catch (IllegalArgumentException e) { + log.error("Invalid CIDR notation: {}", allowed); + } + } else if (allowed.equals(remoteIp)) { + return true; + } + } + return false; + } +} diff --git a/backend/src/main/java/com/park/utmstack/service/application_events/ApplicationEventService.java b/backend/src/main/java/com/park/utmstack/service/application_events/ApplicationEventService.java index 149e387c3..450f60d37 100644 --- a/backend/src/main/java/com/park/utmstack/service/application_events/ApplicationEventService.java +++ b/backend/src/main/java/com/park/utmstack/service/application_events/ApplicationEventService.java @@ -40,7 +40,7 @@ public void createEvent(String message, ApplicationEventType type) { .message(message).timestamp(Instant.now().toString()) .source(ApplicationEventSource.PANEL.name()).type(type.name()) .build(); - /*client.getClient().index(".utmstack-logs", applicationEvent);*/ + client.getClient().index(".utmstack-logs", applicationEvent); } catch (Exception e) { log.error(ctx + ": {}", e.getMessage()); } From cf133e4f47c93cec902c9736c421e893c5716acd Mon Sep 17 00:00:00 2001 From: AlexSanchez-bit Date: Fri, 17 Oct 2025 17:04:44 -0400 Subject: [PATCH 009/128] feat[frontend](api_key): added api key list/creation components --- .../api-keys/api-keys.component.html | 53 +++++++++ .../api-keys/api-keys.component.scss | 0 .../api-keys/api-keys.component.ts | 53 +++++++++ .../api-key-modal.component.html | 109 ++++++++++++++++++ .../api-key-modal.component.scss | 1 + .../api-key-modal/api-key-modal.component.ts | 68 +++++++++++ .../api-keys/shared/models/ApiKeyResponse.ts | 8 ++ .../api-keys/shared/models/ApiKeyUpsert.ts | 5 + .../shared/service/api-keys.service.ts | 108 +++++++++++++++++ .../app-management/app-management.module.ts | 5 + .../connection-key.component.html | 20 +++- 11 files changed, 429 insertions(+), 1 deletion(-) create mode 100644 frontend/src/app/app-management/api-keys/api-keys.component.html create mode 100644 frontend/src/app/app-management/api-keys/api-keys.component.scss create mode 100644 frontend/src/app/app-management/api-keys/api-keys.component.ts create mode 100644 frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.html create mode 100644 frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.scss create mode 100644 frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts create mode 100644 frontend/src/app/app-management/api-keys/shared/models/ApiKeyResponse.ts create mode 100644 frontend/src/app/app-management/api-keys/shared/models/ApiKeyUpsert.ts create mode 100644 frontend/src/app/app-management/api-keys/shared/service/api-keys.service.ts diff --git a/frontend/src/app/app-management/api-keys/api-keys.component.html b/frontend/src/app/app-management/api-keys/api-keys.component.html new file mode 100644 index 000000000..677d5cde5 --- /dev/null +++ b/frontend/src/app/app-management/api-keys/api-keys.component.html @@ -0,0 +1,53 @@ +
+
+
+
+

API Keys

+

+ The API key is a simple encrypted string that identifies you in the application. +

+
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
NAMEALLOWED IPsCREATED ATEXPIRES ATACTIONS
{{ key.name }}{{ key.allowedIp?.join(', ') || '—' }}{{ key.createdAt | date:'M/d/yy, h:mm a' }}{{ key.expiresAt ? (key.expiresAt | date:'M/d/yy, h:mm a') : '—' }} + + + +
+
+ + + Loading... + No API Keys found. + +
+
diff --git a/frontend/src/app/app-management/api-keys/api-keys.component.scss b/frontend/src/app/app-management/api-keys/api-keys.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/app/app-management/api-keys/api-keys.component.ts b/frontend/src/app/app-management/api-keys/api-keys.component.ts new file mode 100644 index 000000000..e55e0600e --- /dev/null +++ b/frontend/src/app/app-management/api-keys/api-keys.component.ts @@ -0,0 +1,53 @@ +import { Component, OnInit } from '@angular/core'; +import { ApiKeysService } from './shared/service/api-keys.service'; +import { ApiKeyResponse } from './shared/models/ApiKeyResponse'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { ApiKeyModalComponent } from './shared/components/api-key-modal/api-key-modal.component'; + +@Component({ + selector: 'app-api-keys', + templateUrl: './api-keys.component.html', + styleUrls: ['./api-keys.component.scss'] +}) +export class ApiKeysComponent implements OnInit { + apiKeys: ApiKeyResponse[] = []; + loading = false; + + constructor( + private apiKeyService: ApiKeysService, + private modalService: NgbModal + ) {} + + ngOnInit(): void { + this.loadKeys(); + } + + loadKeys(): void { + this.loading = true; + this.apiKeyService.list().subscribe({ + next: (res) => { + this.apiKeys = res.body || []; + this.loading = false; + }, + error: () => (this.loading = false) + }); + } + + openCreateModal(): void { + const modalRef = this.modalService.open(ApiKeyModalComponent, { centered: true, size: 'lg' }); + modalRef.result.then((result) => { + if (result === 'created') this.loadKeys(); + }).catch(() => {}); + } + + deleteKey(id: string): void { + if (!confirm('Are you sure you want to delete this API key?')) return; + this.apiKeyService.delete(id).subscribe(() => this.loadKeys()); + } + + regenerateKey(id: string): void { + this.apiKeyService.generate(id).subscribe((res) => { + alert('New API Key: ' + res.body); + }); + } +} diff --git a/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.html b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.html new file mode 100644 index 000000000..28ae3a6a6 --- /dev/null +++ b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.html @@ -0,0 +1,109 @@ + + + + + + diff --git a/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.scss b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.scss new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.scss @@ -0,0 +1 @@ + diff --git a/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts new file mode 100644 index 000000000..c2944f8d1 --- /dev/null +++ b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts @@ -0,0 +1,68 @@ +import { Component, OnInit } from '@angular/core'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { ApiKeysService } from '../../service/api-keys.service'; +import { FormBuilder, FormGroup, Validators, FormArray } from '@angular/forms'; + +@Component({ + selector: 'app-api-key-modal', + templateUrl: './api-key-modal.component.html', + styleUrls: ['./api-key-modal.component.scss'] +}) +export class ApiKeyModalComponent implements OnInit { + apiKeyForm: FormGroup; + ipInput = ''; + loading = false; + errorMsg = ''; + + constructor( + public activeModal: NgbActiveModal, + private apiKeyService: ApiKeysService, + private fb: FormBuilder + ) { + this.apiKeyForm = this.fb.group({ + name: ['', Validators.required], + allowedIp: this.fb.array([]), + expiresAt: [null] + }); + } + + ngOnInit(): void {} + + get allowedIp(): FormArray { + return this.apiKeyForm.get('allowedIp') as FormArray; + } + + addIp(): void { + const ip = this.ipInput.trim(); + if (ip) { + this.allowedIp.push(this.fb.control(ip)); + this.ipInput = ''; + } + } + + removeIp(index: number): void { + this.allowedIp.removeAt(index); + } + + create(): void { + this.errorMsg = ''; + + if (this.apiKeyForm.invalid) { + this.errorMsg = 'Name is required.'; + return; + } + + this.loading = true; + this.apiKeyService.create(this.apiKeyForm.value).subscribe({ + next: () => { + this.loading = false; + this.activeModal.close('created'); + }, + error: (err) => { + this.loading = false; + this.errorMsg = err.error.message || 'An error occurred while creating the API key.'; + } + }); + } +} + diff --git a/frontend/src/app/app-management/api-keys/shared/models/ApiKeyResponse.ts b/frontend/src/app/app-management/api-keys/shared/models/ApiKeyResponse.ts new file mode 100644 index 000000000..8026a23ab --- /dev/null +++ b/frontend/src/app/app-management/api-keys/shared/models/ApiKeyResponse.ts @@ -0,0 +1,8 @@ +export interface ApiKeyResponse { + id: string; + name: string; + allowedIp: string[]; + createdAt: Date; + expiresAt?: Date; + generatedAt?: Date; +} diff --git a/frontend/src/app/app-management/api-keys/shared/models/ApiKeyUpsert.ts b/frontend/src/app/app-management/api-keys/shared/models/ApiKeyUpsert.ts new file mode 100644 index 000000000..f706ba1dc --- /dev/null +++ b/frontend/src/app/app-management/api-keys/shared/models/ApiKeyUpsert.ts @@ -0,0 +1,5 @@ +export interface ApiKeyUpsert { + name: string; + allowedIp?: string[]; + expiresAt?: Date; +} diff --git a/frontend/src/app/app-management/api-keys/shared/service/api-keys.service.ts b/frontend/src/app/app-management/api-keys/shared/service/api-keys.service.ts new file mode 100644 index 000000000..210755994 --- /dev/null +++ b/frontend/src/app/app-management/api-keys/shared/service/api-keys.service.ts @@ -0,0 +1,108 @@ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpResponse } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { SERVER_API_URL } from '../../../../app.constants'; +import { ApiKeyResponse } from '../models/ApiKeyResponse'; +import { ApiKeyUpsert } from '../models/ApiKeyUpsert'; + +/** + * Service for managing API keys + */ +@Injectable({ + providedIn: 'root' +}) +export class ApiKeysService { + public resourceUrl = SERVER_API_URL + 'api/api-keys'; + + constructor(private http: HttpClient) {} + + /** + * Create a new API key + */ + create(dto: ApiKeyUpsert): Observable> { + return this.http.post( + this.resourceUrl, + dto, + { observe: 'response' } + ); + } + + /** + * Generate (or renew) a plain API key for the given id + * Returns the plain text key (only once) + */ + generate(id: string): Observable> { + return this.http.post( + `${this.resourceUrl}/${id}/generate`, + {}, + { observe: 'response', responseType: 'text' } + ); + } + + /** + * Get API key by id + */ + get(id: string): Observable> { + return this.http.get( + `${this.resourceUrl}/${id}`, + { observe: 'response' } + ); + } + + /** + * List all API keys (with optional pagination) + */ + list(params?: any): Observable> { + return this.http.get( + this.resourceUrl, + { observe: 'response', params } + ); + } + + /** + * Update an existing API key + */ + update(id: string, dto: ApiKeyUpsert): Observable> { + return this.http.put( + `${this.resourceUrl}/${id}`, + dto, + { observe: 'response' } + ); + } + + /** + * Delete API key + */ + delete(id: string): Observable> { + return this.http.delete( + `${this.resourceUrl}/${id}`, + { observe: 'response' } + ); + } + + /** + * Search API key usage in Elasticsearch + */ + usage(params: { + filters?: any[]; + top: number; + indexPattern: string; + includeChildren?: boolean; + page?: number; + size?: number; + }): Observable { + return this.http.get( + `${this.resourceUrl}/usage`, + { + params: { + top: params.top.toString(), + indexPattern: params.indexPattern, + includeChildren: params.includeChildren.toString() || 'false', + page: params.page.toString() || '0', + size: params.size.toString() || '10' + } + } + ); + } +} + diff --git a/frontend/src/app/app-management/app-management.module.ts b/frontend/src/app/app-management/app-management.module.ts index 32484f1c9..5841a0f91 100644 --- a/frontend/src/app/app-management/app-management.module.ts +++ b/frontend/src/app/app-management/app-management.module.ts @@ -45,9 +45,13 @@ import {UtmApiDocComponent} from './utm-api-doc/utm-api-doc.component'; import { UtmNotificationViewComponent } from "./utm-notification/components/notifications-view/utm-notification-view.component"; +import { ApiKeysComponent } from './api-keys/api-keys.component'; +import { ApiKeyModalComponent } from './api-keys/shared/components/api-key-modal/api-key-modal.component'; @NgModule({ declarations: [ + ApiKeysComponent, + ApiKeyModalComponent, AppManagementComponent, AppManagementSidebarComponent, IndexPatternHelpComponent, @@ -84,6 +88,7 @@ import { HealthDetailComponent, MenuDeleteDialogComponent, TokenActivateComponent, + ApiKeyModalComponent, IndexDeleteComponent], imports: [ CommonModule, diff --git a/frontend/src/app/app-management/connection-key/connection-key.component.html b/frontend/src/app/app-management/connection-key/connection-key.component.html index aa95f0ae7..441546517 100644 --- a/frontend/src/app/app-management/connection-key/connection-key.component.html +++ b/frontend/src/app/app-management/connection-key/connection-key.component.html @@ -1,4 +1,6 @@ -
+
+ +
Connection key @@ -71,5 +73,21 @@
Connection Key
+
+ + + +
+
+
+ For developers +
+
+ +
+ +
+
+
From 94951f87779375fd2f0b8d611c078b99271e3211 Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Mon, 20 Oct 2025 08:33:33 -0500 Subject: [PATCH 010/128] refactor(api_keys): remove unused ApplicationEventService from ApiKeyFilter --- .../java/com/park/utmstack/security/api_key/ApiKeyFilter.java | 1 - backend/src/main/resources/config/liquibase/scripts/tables.sql | 1 - 2 files changed, 2 deletions(-) diff --git a/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java b/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java index f629da7fc..05950f08b 100644 --- a/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java +++ b/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java @@ -52,7 +52,6 @@ public class ApiKeyFilter extends OncePerRequestFilter { private final ApiKeyService apiKeyService; private final ConcurrentMap invalidApiKeyBlackList = new ConcurrentHashMap<>(); private final ConcurrentMap cidrCache = new ConcurrentHashMap<>(); - private final ApplicationEventService applicationEventService; private final ApiKeyUsageLoggingService apiKeyUsageLoggingService; @Override diff --git a/backend/src/main/resources/config/liquibase/scripts/tables.sql b/backend/src/main/resources/config/liquibase/scripts/tables.sql index 629971010..7500ca2cf 100644 --- a/backend/src/main/resources/config/liquibase/scripts/tables.sql +++ b/backend/src/main/resources/config/liquibase/scripts/tables.sql @@ -12,7 +12,6 @@ DROP TABLE IF EXISTS public.utm_gvm_scan_result; DROP TABLE IF EXISTS public.utm_gvm_task; DROP TABLE IF EXISTS public.utm_module_modal; DROP TABLE IF EXISTS public.utm_system_restart; -DROP TABLE IF EXISTS public.utm_api_keys; CREATE TABLE IF NOT EXISTS public.jhi_authority ( From af0a34945a976731a2fe9f2f58bc56a630f7444d Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Mon, 20 Oct 2025 08:46:16 -0500 Subject: [PATCH 011/128] refactor(api_keys): update API key table schema and change ID type to BIGINT --- .../park/utmstack/domain/api_keys/ApiKey.java | 5 ++--- .../dto/api_key/ApiKeyResponseDTO.java | 2 +- .../20251017001_create_api_keys_table.xml | 4 ++-- .../config/liquibase/scripts/tables.sql | 20 ------------------- 4 files changed, 5 insertions(+), 26 deletions(-) diff --git a/backend/src/main/java/com/park/utmstack/domain/api_keys/ApiKey.java b/backend/src/main/java/com/park/utmstack/domain/api_keys/ApiKey.java index 8d0585a3d..4986a7b58 100644 --- a/backend/src/main/java/com/park/utmstack/domain/api_keys/ApiKey.java +++ b/backend/src/main/java/com/park/utmstack/domain/api_keys/ApiKey.java @@ -19,9 +19,8 @@ public class ApiKey implements Serializable { @Id - @GeneratedValue(strategy = GenerationType.AUTO) - @Column(columnDefinition = "UUID") - private UUID id; + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; @Column(nullable = false) private Long userId; diff --git a/backend/src/main/java/com/park/utmstack/service/dto/api_key/ApiKeyResponseDTO.java b/backend/src/main/java/com/park/utmstack/service/dto/api_key/ApiKeyResponseDTO.java index 024f7282c..abfa4d02d 100644 --- a/backend/src/main/java/com/park/utmstack/service/dto/api_key/ApiKeyResponseDTO.java +++ b/backend/src/main/java/com/park/utmstack/service/dto/api_key/ApiKeyResponseDTO.java @@ -17,7 +17,7 @@ public class ApiKeyResponseDTO { @Schema(description = "Unique identifier of the API key") - private UUID id; + private Long id; @Schema(description = "User-friendly API key name") private String name; diff --git a/backend/src/main/resources/config/liquibase/changelog/20251017001_create_api_keys_table.xml b/backend/src/main/resources/config/liquibase/changelog/20251017001_create_api_keys_table.xml index 2b996f83c..ad2fbaf47 100644 --- a/backend/src/main/resources/config/liquibase/changelog/20251017001_create_api_keys_table.xml +++ b/backend/src/main/resources/config/liquibase/changelog/20251017001_create_api_keys_table.xml @@ -4,9 +4,9 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd"> - + - + diff --git a/backend/src/main/resources/config/liquibase/scripts/tables.sql b/backend/src/main/resources/config/liquibase/scripts/tables.sql index 7500ca2cf..f50a695d1 100644 --- a/backend/src/main/resources/config/liquibase/scripts/tables.sql +++ b/backend/src/main/resources/config/liquibase/scripts/tables.sql @@ -830,23 +830,3 @@ CREATE TABLE IF NOT EXISTS public.utm_space_notification_control next_notification timestamp without time zone NOT NULL, CONSTRAINT utm_space_notification_control_pkey PRIMARY KEY (id) ); - -CREATE TABLE IF NOT EXISTS public.utm_api_keys -( - id uuid default uuid_generate_v4() not null - primary key, - account_id uuid not null, - name varchar(255) not null, - api_key varchar(255) not null, - allowed_ip text, - created_at timestamp not null, - expires_at timestamp, - generated_at timestamp -); - -alter table utm_api_keys - owner to postgres; - -create unique index uk_api_keys_api_key - on utm_api_keys (api_key); - From dcde7f3619fa87fa279e280e0546195b3935514e Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Thu, 23 Oct 2025 14:26:15 -0500 Subject: [PATCH 012/128] feat(api_keys): enhance API key management UI Signed-off-by: Manuel Abascal --- .../api-keys/api-keys.component.html | 78 ++++++++++++------- .../api-keys/api-keys.component.scss | 7 ++ .../api-keys/api-keys.component.ts | 5 ++ .../api-key-modal/api-key-modal.component.ts | 6 +- .../app-management-routing.module.ts | 12 ++- .../connection-key.component.html | 20 +---- .../app-management-sidebar.component.html | 10 +++ 7 files changed, 89 insertions(+), 49 deletions(-) diff --git a/frontend/src/app/app-management/api-keys/api-keys.component.html b/frontend/src/app/app-management/api-keys/api-keys.component.html index 677d5cde5..418bbd87d 100644 --- a/frontend/src/app/app-management/api-keys/api-keys.component.html +++ b/frontend/src/app/app-management/api-keys/api-keys.component.html @@ -1,25 +1,47 @@ -
-
-
-
-

API Keys

-

- The API key is a simple encrypted string that identifies you in the application. -

-
- +
+
+
+
+ API Keys +
+ + The API key is a simple encrypted string that identifies you in the application. With this key, you can access the REST API. +
- -
- - + + +
+
+
+ - - - - + + + + @@ -27,17 +49,17 @@

API Keys

- - + + diff --git a/frontend/src/app/app-management/api-keys/api-keys.component.scss b/frontend/src/app/app-management/api-keys/api-keys.component.scss index e69de29bb..b3751c83a 100644 --- a/frontend/src/app/app-management/api-keys/api-keys.component.scss +++ b/frontend/src/app/app-management/api-keys/api-keys.component.scss @@ -0,0 +1,7 @@ +:host{ + display: flex; + flex-direction: column; + flex: 1 1 auto; + min-height: 0; + height: 100%; +} diff --git a/frontend/src/app/app-management/api-keys/api-keys.component.ts b/frontend/src/app/app-management/api-keys/api-keys.component.ts index e55e0600e..cea8476b4 100644 --- a/frontend/src/app/app-management/api-keys/api-keys.component.ts +++ b/frontend/src/app/app-management/api-keys/api-keys.component.ts @@ -3,6 +3,7 @@ import { ApiKeysService } from './shared/service/api-keys.service'; import { ApiKeyResponse } from './shared/models/ApiKeyResponse'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { ApiKeyModalComponent } from './shared/components/api-key-modal/api-key-modal.component'; +import {SortEvent} from "../../shared/directives/sortable/type/sort-event"; @Component({ selector: 'app-api-keys', @@ -50,4 +51,8 @@ export class ApiKeysComponent implements OnInit { alert('New API Key: ' + res.body); }); } + + onSortBy($event: SortEvent | string) { + + } } diff --git a/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts index c2944f8d1..d9b97f091 100644 --- a/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts +++ b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts @@ -53,7 +53,11 @@ export class ApiKeyModalComponent implements OnInit { } this.loading = true; - this.apiKeyService.create(this.apiKeyForm.value).subscribe({ + const payload = { + ...this.apiKeyForm.value, + expiresAt: this.apiKeyForm.value.expiresAt + ':00.000Z', + }; + this.apiKeyService.create(payload).subscribe({ next: () => { this.loading = false; this.activeModal.close('created'); diff --git a/frontend/src/app/app-management/app-management-routing.module.ts b/frontend/src/app/app-management/app-management-routing.module.ts index 616d6fa01..d498dae62 100644 --- a/frontend/src/app/app-management/app-management-routing.module.ts +++ b/frontend/src/app/app-management/app-management-routing.module.ts @@ -16,6 +16,7 @@ import {MenuComponent} from './menu/menu.component'; import {RolloverConfigComponent} from './rollover-config/rollover-config.component'; import {UtmApiDocComponent} from './utm-api-doc/utm-api-doc.component'; import {UtmNotificationViewComponent} from './utm-notification/components/notifications-view/utm-notification-view.component'; +import {ApiKeysComponent} from "./api-keys/api-keys.component"; const routes: Routes = [ {path: '', redirectTo: 'settings', pathMatch: 'full'}, @@ -123,7 +124,16 @@ const routes: Routes = [ data: { authorities: [ADMIN_ROLE] }, - }], + }, + { + path: 'api-keys', + component: ApiKeysComponent, + canActivate: [UserRouteAccessService], + data: { + authorities: [ADMIN_ROLE] + }, + } + ], }, ]; diff --git a/frontend/src/app/app-management/connection-key/connection-key.component.html b/frontend/src/app/app-management/connection-key/connection-key.component.html index 441546517..aa95f0ae7 100644 --- a/frontend/src/app/app-management/connection-key/connection-key.component.html +++ b/frontend/src/app/app-management/connection-key/connection-key.component.html @@ -1,6 +1,4 @@ -
- -
+
Connection key @@ -73,21 +71,5 @@
Connection Key
-
- - - -
-
-
- For developers -
-
- -
- -
-
-
diff --git a/frontend/src/app/app-management/layout/app-management-sidebar/app-management-sidebar.component.html b/frontend/src/app/app-management/layout/app-management-sidebar/app-management-sidebar.component.html index 66efbcbfb..a67af0739 100644 --- a/frontend/src/app/app-management/layout/app-management-sidebar/app-management-sidebar.component.html +++ b/frontend/src/app/app-management/layout/app-management-sidebar/app-management-sidebar.component.html @@ -125,6 +125,16 @@ + + +   + API Keys + + + Date: Thu, 23 Oct 2025 14:52:05 -0500 Subject: [PATCH 013/128] feat(api_keys): enhance API key management UI Signed-off-by: Manuel Abascal --- .../api-keys/api-keys.component.ts | 7 ++- .../api-key-modal.component.html | 59 +++++++------------ .../api-key-modal/api-key-modal.component.ts | 7 ++- .../api-keys/shared/models/ApiKeyUpsert.ts | 1 + 4 files changed, 33 insertions(+), 41 deletions(-) diff --git a/frontend/src/app/app-management/api-keys/api-keys.component.ts b/frontend/src/app/app-management/api-keys/api-keys.component.ts index cea8476b4..929808937 100644 --- a/frontend/src/app/app-management/api-keys/api-keys.component.ts +++ b/frontend/src/app/app-management/api-keys/api-keys.component.ts @@ -30,12 +30,15 @@ export class ApiKeysComponent implements OnInit { this.apiKeys = res.body || []; this.loading = false; }, - error: () => (this.loading = false) + error: () => { + this.loading = false; + this.apiKeys = []; + } }); } openCreateModal(): void { - const modalRef = this.modalService.open(ApiKeyModalComponent, { centered: true, size: 'lg' }); + const modalRef = this.modalService.open(ApiKeyModalComponent, { centered: true }); modalRef.result.then((result) => { if (result === 'created') this.loadKeys(); }).catch(() => {}); diff --git a/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.html b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.html index 28ae3a6a6..6047a4749 100644 --- a/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.html +++ b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.html @@ -1,16 +1,6 @@ - + -
- +
-
    -
  • - {{ ip.value }} - -
  • -
- - + + +
+ + +
+ +
+ {{ ipInputError }} +
+ +
    +
  • + +
    +
    + + {{ ip.value }} + {{ getIpType(ip.value) }} +
    +
    + +
    +
    +
  • +
+ +
diff --git a/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.scss b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.scss index 8b1378917..e31427b61 100644 --- a/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.scss +++ b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.scss @@ -1 +1,12 @@ +.disabled-rounded-start { + border-top-left-radius: 0 !important; + border-bottom-left-radius: 0 !important; +} +.disabled-rounded-end { + border-top-right-radius: 0 !important; + border-bottom-right-radius: 0 !important; +} +.mt-4 { + margin-top: 9rem !important; +} diff --git a/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts index dd4435cec..b72d4017a 100644 --- a/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts +++ b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts @@ -3,6 +3,7 @@ import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import { ApiKeysService } from '../../service/api-keys.service'; import { FormBuilder, FormGroup, Validators, FormArray } from '@angular/forms'; import {ApiKeyUpsert} from '../../models/ApiKeyUpsert'; +import {IpFormsValidators} from "../../../../../rule-management/app-rule/validators/ip.forms.validators"; @Component({ selector: 'app-api-key-modal', @@ -18,17 +19,19 @@ export class ApiKeyModalComponent implements OnInit { loading = false; errorMsg = ''; isSaving: string | string[] | Set | { [p: string]: any }; + ipInputError: string = ''; constructor( public activeModal: NgbActiveModal, private apiKeyService: ApiKeysService, - private fb: FormBuilder - ) { + private fb: FormBuilder) { + this.apiKeyForm = this.fb.group({ name: ['', Validators.required], allowedIp: this.fb.array([]), - expiresAt: [null] + expiresAt: ['', Validators.required], }); + } ngOnInit(): void {} @@ -38,11 +41,36 @@ export class ApiKeyModalComponent implements OnInit { } addIp(): void { - const ip = this.ipInput.trim(); - if (ip) { - this.allowedIp.push(this.fb.control(ip)); - this.ipInput = ''; + const trimmedIp = this.ipInput.trim(); + + if (!trimmedIp) { + this.ipInputError = 'Please enter an IP address or CIDR'; // Se asigna el error + return; + } + + const tempControl = this.fb.control(trimmedIp, [IpFormsValidators.ipOrCidr()]); + + if (tempControl.invalid) { + if (tempControl.hasError('invalidIp')) { + this.ipInputError = 'Invalid IP address format'; + } else if (tempControl.hasError('invalidCidr')) { + this.ipInputError = 'Invalid CIDR format'; + } + return; } + + const isDuplicate = this.allowedIp.controls.some( + control => control.value === trimmedIp + ); + + if (isDuplicate) { + this.ipInputError = 'This IP is already added'; + return; + } + + this.allowedIp.push(this.fb.control(trimmedIp, [IpFormsValidators.ipOrCidr()])); + this.ipInput = ''; + this.ipInputError = ''; } removeIp(index: number): void { @@ -73,5 +101,13 @@ export class ApiKeyModalComponent implements OnInit { } }); } + + getIpType(value: string): string { + if (!value) { return ''; } + if (value.includes('/')) { + return value.includes(':') ? 'IPv6 CIDR' : 'IPv4 CIDR'; + } + return value.includes(':') ? 'IPv6' : 'IPv4'; + } } diff --git a/frontend/src/app/rule-management/app-rule/validators/ip.forms.validators.ts b/frontend/src/app/rule-management/app-rule/validators/ip.forms.validators.ts new file mode 100644 index 000000000..b82b574d6 --- /dev/null +++ b/frontend/src/app/rule-management/app-rule/validators/ip.forms.validators.ts @@ -0,0 +1,86 @@ +import {AbstractControl, ValidationErrors, ValidatorFn} from '@angular/forms'; + +export class IpFormsValidators { + + static ipOrCidr(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + if (!control.value) { + return null; + } + + const value = control.value.trim(); + + if (value.includes('/')) { + return IpFormsValidators.validateCIDR(value) ? null : { invalidCidr: true }; + } + + const isValidIPv4 = IpFormsValidators.validateIPv4(value); + const isValidIPv6 = IpFormsValidators.validateIPv6(value); + + return (isValidIPv4 || isValidIPv6) ? null : { invalidIp: true }; + }; + } + + private static validateIPv4(ip: string): boolean { + const ipv4Regex = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/; + const match = ip.match(ipv4Regex); + + if (!match) { + return false; + } + + for (let i = 1; i <= 4; i++) { + const octet = parseInt(match[i], 10); + if (octet < 0 || octet > 255) { + return false; + } + } + + return true; + } + + private static validateIPv6(ip: string): boolean { + // tslint:disable-next-line:max-line-length + const ipv6Regex = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/; + + return ipv6Regex.test(ip); + } + + private static validateCIDR(cidr: string): boolean { + const parts = cidr.split('/'); + + if (parts.length !== 2) { + return false; + } + + const [ip, prefix] = parts; + const prefixNum = parseInt(prefix, 10); + + const isIPv4 = ip.includes('.') && !ip.includes(':'); + const isIPv6 = ip.includes(':'); + + if (isIPv4) { + if (!IpFormsValidators.validateIPv4(ip)) { + return false; + } + + if (isNaN(prefixNum) || prefixNum < 0 || prefixNum > 32) { + return false; + } + } else if (isIPv6) { + + if (!IpFormsValidators.validateIPv6(ip)) { + return false; + } + + if (isNaN(prefixNum) || prefixNum < 0 || prefixNum > 128) { + return false; + } + } else { + return false; + } + + return true; + } + +} From 3f06c6589c6da14309debb0168d451f893e1e61f Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Fri, 24 Oct 2025 11:51:36 -0500 Subject: [PATCH 019/128] feat(api_keys): add API key generation and expiration handling with user feedback Signed-off-by: Manuel Abascal --- .../api-keys/api-keys.component.html | 43 ++++++++- .../api-keys/api-keys.component.ts | 89 ++++++++++++++++--- .../api-key-modal.component.html | 2 + .../api-key-modal/api-key-modal.component.ts | 43 ++++++--- .../api-keys/shared/models/ApiKeyResponse.ts | 6 +- .../shared/service/api-keys.service.ts | 7 ++ 6 files changed, 158 insertions(+), 32 deletions(-) diff --git a/frontend/src/app/app-management/api-keys/api-keys.component.html b/frontend/src/app/app-management/api-keys/api-keys.component.html index 418bbd87d..22b6db422 100644 --- a/frontend/src/app/app-management/api-keys/api-keys.component.html +++ b/frontend/src/app/app-management/api-keys/api-keys.component.html @@ -47,12 +47,20 @@
- + - + diff --git a/frontend/src/app/app-management/api-keys/api-keys.component.ts b/frontend/src/app/app-management/api-keys/api-keys.component.ts index 20f256fcd..c2d305776 100644 --- a/frontend/src/app/app-management/api-keys/api-keys.component.ts +++ b/frontend/src/app/app-management/api-keys/api-keys.component.ts @@ -1,6 +1,10 @@ import {Component, OnInit, TemplateRef, ViewChild} from '@angular/core'; import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; import * as moment from 'moment'; +import {UtmToastService} from "../../shared/alert/utm-toast.service"; +import { + ModalConfirmationComponent +} from "../../shared/components/utm/util/modal-confirmation/modal-confirmation.component"; import {ITEMS_PER_PAGE} from '../../shared/constants/pagination.constants'; import {SortEvent} from '../../shared/directives/sortable/type/sort-event'; import { ApiKeyModalComponent } from './shared/components/api-key-modal/api-key-modal.component'; @@ -28,9 +32,9 @@ export class ApiKeysComponent implements OnInit { generatedModalRef!: NgbModalRef; copied = false; - constructor( - private apiKeyService: ApiKeysService, - private modalService: NgbModal + constructor( private toastService: UtmToastService, + private apiKeyService: ApiKeysService, + private modalService: NgbModal ) {} ngOnInit(): void { @@ -67,9 +71,44 @@ export class ApiKeysComponent implements OnInit { }); } - deleteKey(id: string): void { - if (!confirm('Are you sure you want to delete this API key?')) { return; } - this.apiKeyService.delete(id).subscribe(() => this.loadKeys()); + editKey(key: ApiKeyResponse): void { + const modalRef = this.modalService.open(ApiKeyModalComponent, {centered: true}); + modalRef.componentInstance.apiKey = key; + + modalRef.result.then((key: ApiKeyResponse) => { + if (key) { + this.generateKey(key); + } + }); + } + + deleteKey(apiKey: ApiKeyResponse): void { + const modalRef = this.modalService.open(ModalConfirmationComponent, {centered: true}); + modalRef.componentInstance.header = `Delete API Key: ${apiKey.name}`; + modalRef.componentInstance.message = 'Are you sure you want to delete this API key?'; + modalRef.componentInstance.confirmBtnType = 'delete'; + modalRef.componentInstance.type = 'danger'; + modalRef.componentInstance.confirmBtnText = 'Delete'; + modalRef.componentInstance.confirmBtnIcon = 'icon-cross-circle'; + + modalRef.result.then(reason => { + if (reason === 'ok') { + this.delete(apiKey); + } + }); + } + + delete(apiKey: ApiKeyResponse): void { + this.apiKeyService.delete(apiKey.id).subscribe({ + next: () => { + this.toastService.showSuccess('API key deleted successfully.'); + this.loadKeys(); + }, + error: (err) => { + this.toastService.showError('Error', 'An error occurred while deleting the API key.'); + throw err; + } + }); } getDaysUntilExpire(expiresAt: string): number { diff --git a/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts index e78f53d42..87289aee6 100644 --- a/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts +++ b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts @@ -4,9 +4,8 @@ import { FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import {IpFormsValidators} from '../../../../../rule-management/app-rule/validators/ip.forms.validators'; import {UtmToastService} from '../../../../../shared/alert/utm-toast.service'; -import {ApiKeyUpsert} from '../../models/ApiKeyUpsert'; +import {ApiKeyResponse} from '../../models/ApiKeyResponse'; import { ApiKeysService } from '../../service/api-keys.service'; -import {ApiKeyResponse} from "../../models/ApiKeyResponse"; @Component({ selector: 'app-api-key-modal', @@ -15,7 +14,7 @@ import {ApiKeyResponse} from "../../models/ApiKeyResponse"; }) export class ApiKeyModalComponent implements OnInit { - @Input() apiKey: ApiKeyUpsert = null; + @Input() apiKey: ApiKeyResponse = null; apiKeyForm: FormGroup; ipInput = ''; @@ -29,17 +28,24 @@ export class ApiKeyModalComponent implements OnInit { private apiKeyService: ApiKeysService, private fb: FormBuilder, private toastService: UtmToastService) { + } + + ngOnInit(): void { + + const expiresAtDate = this.apiKey && this.apiKey.expiresAt ? new Date(this.apiKey.expiresAt) : null; + const expiresAtNgbDate = expiresAtDate ? { + year: expiresAtDate.getUTCFullYear(), + month: expiresAtDate.getUTCMonth() + 1, + day: expiresAtDate.getUTCDate() + } : null; this.apiKeyForm = this.fb.group({ - name: ['', Validators.required], - allowedIp: this.fb.array([]), - expiresAt: ['', Validators.required], + name: [ this.apiKey ? this.apiKey.name : '', Validators.required], + allowedIp: this.fb.array(this.apiKey ? this.apiKey.allowedIp : []), + expiresAt: [expiresAtNgbDate, Validators.required], }); - } - ngOnInit(): void {} - get allowedIp(): FormArray { return this.apiKeyForm.get('allowedIp') as FormArray; } @@ -102,7 +108,11 @@ export class ApiKeyModalComponent implements OnInit { ...this.apiKeyForm.value, expiresAt: formattedDate, }; - this.apiKeyService.create(payload).subscribe({ + + const save = this.apiKey ? this.apiKeyService.update(this.apiKey.id, payload) : + this.apiKeyService.create(payload); + + save.subscribe({ next: (response) => { this.loading = false; this.activeModal.close(response.body as ApiKeyResponse); @@ -114,7 +124,6 @@ export class ApiKeyModalComponent implements OnInit { } else if (err.status === 500) { this.toastService.showError('Error', 'Server error occurred while creating the API key.'); } - } }); } From d97830ac274f652c3919e913e1bdbf6f0e102847 Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Mon, 27 Oct 2025 09:09:45 -0500 Subject: [PATCH 021/128] refactor(api_keys): change API key identifier type from UUID to Long for consistency --- .../utmstack/repository/api_key/ApiKeyRepository.java | 4 ++-- .../park/utmstack/service/api_key/ApiKeyService.java | 10 +++++----- .../park/utmstack/web/rest/api_key/ApiKeyResource.java | 8 ++++---- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/backend/src/main/java/com/park/utmstack/repository/api_key/ApiKeyRepository.java b/backend/src/main/java/com/park/utmstack/repository/api_key/ApiKeyRepository.java index e76001064..ece32eba9 100644 --- a/backend/src/main/java/com/park/utmstack/repository/api_key/ApiKeyRepository.java +++ b/backend/src/main/java/com/park/utmstack/repository/api_key/ApiKeyRepository.java @@ -14,9 +14,9 @@ import java.util.UUID; @Repository -public interface ApiKeyRepository extends JpaRepository { +public interface ApiKeyRepository extends JpaRepository { - Optional findByIdAndUserId(UUID id, Long userId); + Optional findByIdAndUserId(Long id, Long userId); Page findByUserId(Long userId, Pageable pageable); diff --git a/backend/src/main/java/com/park/utmstack/service/api_key/ApiKeyService.java b/backend/src/main/java/com/park/utmstack/service/api_key/ApiKeyService.java index 5ed530e3f..56642f183 100644 --- a/backend/src/main/java/com/park/utmstack/service/api_key/ApiKeyService.java +++ b/backend/src/main/java/com/park/utmstack/service/api_key/ApiKeyService.java @@ -58,11 +58,11 @@ public ApiKeyResponseDTO createApiKey(Long userId,ApiKeyUpsertDTO dto) { return apiKeyMapper.toDto(apiKeyRepository.save(apiKey)); } catch (Exception e) { - throw new RuntimeException(ctx + ": " + e.getMessage()); + throw new ApiKeyExistException(ctx + ": " + e.getMessage()); } } - public String generateApiKey(Long userId, UUID apiKeyId) { + public String generateApiKey(Long userId, Long apiKeyId) { final String ctx = CLASSNAME + ".generateApiKey"; try { ApiKey apiKey = apiKeyRepository.findByIdAndUserId(apiKeyId, userId) @@ -77,7 +77,7 @@ public String generateApiKey(Long userId, UUID apiKeyId) { } } - public ApiKeyResponseDTO updateApiKey(Long userId, UUID apiKeyId, ApiKeyUpsertDTO dto) { + public ApiKeyResponseDTO updateApiKey(Long userId, Long apiKeyId, ApiKeyUpsertDTO dto) { final String ctx = CLASSNAME + ".updateApiKey"; try { ApiKey apiKey = apiKeyRepository.findByIdAndUserId(apiKeyId, userId) @@ -96,7 +96,7 @@ public ApiKeyResponseDTO updateApiKey(Long userId, UUID apiKeyId, ApiKeyUpsertDT } } - public ApiKeyResponseDTO getApiKey(Long userId, UUID apiKeyId) { + public ApiKeyResponseDTO getApiKey(Long userId, Long apiKeyId) { final String ctx = CLASSNAME + ".getApiKey"; try { ApiKey apiKey = apiKeyRepository.findByIdAndUserId(apiKeyId, userId) @@ -117,7 +117,7 @@ public Page listApiKeys(Long userId, Pageable pageable) { } - public void deleteApiKey(Long userId, UUID apiKeyId) { + public void deleteApiKey(Long userId, Long apiKeyId) { final String ctx = CLASSNAME + ".deleteApiKey"; try { ApiKey apiKey = apiKeyRepository.findByIdAndUserId(apiKeyId, userId) diff --git a/backend/src/main/java/com/park/utmstack/web/rest/api_key/ApiKeyResource.java b/backend/src/main/java/com/park/utmstack/web/rest/api_key/ApiKeyResource.java index 147ab5254..8453f9ab2 100644 --- a/backend/src/main/java/com/park/utmstack/web/rest/api_key/ApiKeyResource.java +++ b/backend/src/main/java/com/park/utmstack/web/rest/api_key/ApiKeyResource.java @@ -78,7 +78,7 @@ public ResponseEntity createApiKey(@RequestBody ApiKeyUpsertD }) }) @PostMapping("/{id}/generate") - public ResponseEntity generateApiKey(@PathVariable("id") UUID apiKeyId) { + public ResponseEntity generateApiKey(@PathVariable("id") Long apiKeyId) { Long userId = userService.getCurrentUserLogin().getId(); String plainKey = apiKeyService.generateApiKey(userId, apiKeyId); return ResponseEntity.ok(plainKey); @@ -96,7 +96,7 @@ public ResponseEntity generateApiKey(@PathVariable("id") UUID apiKeyId) }) }) @GetMapping("/{id}") - public ResponseEntity getApiKey(@PathVariable("id") UUID apiKeyId) { + public ResponseEntity getApiKey(@PathVariable("id") Long apiKeyId) { Long userId = userService.getCurrentUserLogin().getId(); ApiKeyResponseDTO responseDTO = apiKeyService.getApiKey(userId, apiKeyId); return ResponseEntity.ok(responseDTO); @@ -134,7 +134,7 @@ public ResponseEntity> listApiKeys(@ParameterObject Page }) }) @PutMapping("/{id}") - public ResponseEntity updateApiKey(@PathVariable("id") UUID apiKeyId, + public ResponseEntity updateApiKey(@PathVariable("id") Long apiKeyId, @RequestBody ApiKeyUpsertDTO dto) { Long userId = userService.getCurrentUserLogin().getId(); @@ -154,7 +154,7 @@ public ResponseEntity updateApiKey(@PathVariable("id") UUID a }) }) @DeleteMapping("/{id}") - public ResponseEntity deleteApiKey(@PathVariable("id") UUID apiKeyId) { + public ResponseEntity deleteApiKey(@PathVariable("id") Long apiKeyId) { Long userId = userService.getCurrentUserLogin().getId(); apiKeyService.deleteApiKey(userId, apiKeyId); From fdbea5f351c86a639586cb5e71af6979571712e8 Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Mon, 27 Oct 2025 12:12:02 -0500 Subject: [PATCH 022/128] feat(api_keys): simplify API key listing endpoint and enhance expiration handling --- .../utmstack/service/api_key/ApiKeyService.java | 14 ++++++++++++++ .../utmstack/web/rest/api_key/ApiKeyResource.java | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/backend/src/main/java/com/park/utmstack/service/api_key/ApiKeyService.java b/backend/src/main/java/com/park/utmstack/service/api_key/ApiKeyService.java index 56642f183..cbc2784a5 100644 --- a/backend/src/main/java/com/park/utmstack/service/api_key/ApiKeyService.java +++ b/backend/src/main/java/com/park/utmstack/service/api_key/ApiKeyService.java @@ -20,6 +20,7 @@ import org.springframework.stereotype.Service; import java.security.SecureRandom; +import java.time.Duration; import java.time.Instant; import java.util.Base64; import java.util.Optional; @@ -67,9 +68,22 @@ public String generateApiKey(Long userId, Long apiKeyId) { try { ApiKey apiKey = apiKeyRepository.findByIdAndUserId(apiKeyId, userId) .orElseThrow(() -> new ApiKeyNotFoundException("API key not found")); + + Instant now = Instant.now(); + Instant originalCreated = apiKey.getGeneratedAt() != null ? apiKey.getGeneratedAt() : apiKey.getCreatedAt(); + Instant originalExpires = apiKey.getExpiresAt(); + + Duration duration; + if (originalCreated != null && originalExpires != null && !originalExpires.isBefore(originalCreated)) { + duration = Duration.between(originalCreated, originalExpires); + } else { + duration = Duration.ofDays(7); + } + String plainKey = generateRandomKey(); apiKey.setApiKey(plainKey); apiKey.setGeneratedAt(Instant.now()); + apiKey.setExpiresAt(now.plus(duration)); apiKeyRepository.save(apiKey); return plainKey; } catch (Exception e) { diff --git a/backend/src/main/java/com/park/utmstack/web/rest/api_key/ApiKeyResource.java b/backend/src/main/java/com/park/utmstack/web/rest/api_key/ApiKeyResource.java index 8453f9ab2..d0bdd0d2b 100644 --- a/backend/src/main/java/com/park/utmstack/web/rest/api_key/ApiKeyResource.java +++ b/backend/src/main/java/com/park/utmstack/web/rest/api_key/ApiKeyResource.java @@ -113,7 +113,7 @@ public ResponseEntity getApiKey(@PathVariable("id") Long apiK @Header(name = "X-App-Error", description = "Technical error details") }) }) - @GetMapping("") + @GetMapping public ResponseEntity> listApiKeys(@ParameterObject Pageable pageable) { Long userId = userService.getCurrentUserLogin().getId(); Page page = apiKeyService.listApiKeys(userId,pageable); From f1a088d8f001bd609e380ce5fef7d93f0facf7c2 Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Mon, 27 Oct 2025 12:12:24 -0500 Subject: [PATCH 023/128] feat(api_keys): improve API key listing with pagination, loading states, and expiration indicators Signed-off-by: Manuel Abascal --- .../api-keys/api-keys.component.html | 57 ++++++++++++++--- .../api-keys/api-keys.component.ts | 62 ++++++++++++++----- .../shared/service/api-keys.service.ts | 4 +- 3 files changed, 96 insertions(+), 27 deletions(-) diff --git a/frontend/src/app/app-management/api-keys/api-keys.component.html b/frontend/src/app/app-management/api-keys/api-keys.component.html index d31f55a06..e2a5158ff 100644 --- a/frontend/src/app/app-management/api-keys/api-keys.component.html +++ b/frontend/src/app/app-management/api-keys/api-keys.component.html @@ -45,19 +45,26 @@
- + + + + + + + + + + + + +
NAMEALLOWED IPsCREATED ATEXPIRES AT + Name + + Allowed IPs + + Expires At + + Created At + ACTIONS
{{ key.name }} {{ key.allowedIp?.join(', ') || '—' }}{{ key.createdAt | date:'M/d/yy, h:mm a' }}{{ key.expiresAt ? (key.expiresAt | date:'M/d/yy, h:mm a') : '—' }}{{ key.expiresAt ? (key.expiresAt | date:'dd/MM/yy HH:mm' :'UTC') : '—' }}{{ key.createdAt | date:'dd/MM/yy HH:mm' :'UTC' }} - - -
{{ key.name }} {{ key.name }} {{ key.allowedIp?.join(', ') || '—' }}{{ key.expiresAt ? (key.expiresAt | date:'dd/MM/yy HH:mm' :'UTC') : '—' }} + + + + + + {{ key.expiresAt ? (key.expiresAt | date:'dd/MM/yy HH:mm' :'UTC') : '—' }} + {{ key.createdAt | date:'dd/MM/yy HH:mm' :'UTC' }} - + +
+ + {{ 'Keep this key safeKeep this key safe' }} +
+ +
+ +
+ diff --git a/frontend/src/app/app-management/api-keys/api-keys.component.ts b/frontend/src/app/app-management/api-keys/api-keys.component.ts index 929808937..20f256fcd 100644 --- a/frontend/src/app/app-management/api-keys/api-keys.component.ts +++ b/frontend/src/app/app-management/api-keys/api-keys.component.ts @@ -1,9 +1,11 @@ -import { Component, OnInit } from '@angular/core'; -import { ApiKeysService } from './shared/service/api-keys.service'; -import { ApiKeyResponse } from './shared/models/ApiKeyResponse'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import {Component, OnInit, TemplateRef, ViewChild} from '@angular/core'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import * as moment from 'moment'; +import {ITEMS_PER_PAGE} from '../../shared/constants/pagination.constants'; +import {SortEvent} from '../../shared/directives/sortable/type/sort-event'; import { ApiKeyModalComponent } from './shared/components/api-key-modal/api-key-modal.component'; -import {SortEvent} from "../../shared/directives/sortable/type/sort-event"; +import { ApiKeyResponse } from './shared/models/ApiKeyResponse'; +import { ApiKeysService } from './shared/service/api-keys.service'; @Component({ selector: 'app-api-keys', @@ -11,8 +13,20 @@ import {SortEvent} from "../../shared/directives/sortable/type/sort-event"; styleUrls: ['./api-keys.component.scss'] }) export class ApiKeysComponent implements OnInit { + + generating: string[] = []; apiKeys: ApiKeyResponse[] = []; loading = false; + generatedApiKey = ''; + @ViewChild('generatedModal') generatedModal!: TemplateRef; + request = + { + sort: 'createdAt,desc', + page: 0, + size: ITEMS_PER_PAGE + }; + generatedModalRef!: NgbModalRef; + copied = false; constructor( private apiKeyService: ApiKeysService, @@ -37,25 +51,74 @@ export class ApiKeysComponent implements OnInit { }); } + copyToClipboard(): void { + (navigator as any).clipboard.writeText(this.generatedApiKey).then(() => { + this.copied = true; + }); + } + openCreateModal(): void { const modalRef = this.modalService.open(ApiKeyModalComponent, { centered: true }); - modalRef.result.then((result) => { - if (result === 'created') this.loadKeys(); - }).catch(() => {}); + + modalRef.result.then((key: ApiKeyResponse) => { + if (key) { + this.generateKey(key); + } + }); } deleteKey(id: string): void { - if (!confirm('Are you sure you want to delete this API key?')) return; + if (!confirm('Are you sure you want to delete this API key?')) { return; } this.apiKeyService.delete(id).subscribe(() => this.loadKeys()); } - regenerateKey(id: string): void { - this.apiKeyService.generate(id).subscribe((res) => { - alert('New API Key: ' + res.body); - }); + getDaysUntilExpire(expiresAt: string): number { + if (!expiresAt) { + return 0; + } + const today = moment(); + const expireDate = moment(expiresAt); + return expireDate.diff(today, 'days'); } onSortBy($event: SortEvent | string) { } + + maskSecrets(str: string): string { + if (!str || str.length <= 10) { + return str; + } + const prefix = str.substring(0, 10); + const maskLength = str.length - 30; + const maskedPart = '*'.repeat(maskLength); + return prefix + maskedPart; + } + + generateKey(apiKey: ApiKeyResponse): void { + this.generating.push(apiKey.id); + this.apiKeyService.generateApiKey(apiKey.id).subscribe(response => { + this.generatedApiKey = response.body ? response.body : ""; + this.generatedModalRef = this.modalService.open(this.generatedModal, {centered: true}); + const index = this.generating.indexOf(apiKey.id); + if (index > -1) { + this.generating.splice(index, 1); + } + this.loadKeys(); + }); + } + + isApiKeyExpired(expiresAt?: string | null ): boolean { + if (!expiresAt) { + return false; + } + const expirationTime = new Date(expiresAt).getTime(); + return expirationTime < Date.now(); + } + + close() { + this.generatedModalRef.close(); + this.copied = false; + this.generatedApiKey = ''; + } } diff --git a/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.html b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.html index 6e7cad101..7e1399bbb 100644 --- a/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.html +++ b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.html @@ -26,6 +26,7 @@ class="form-control" id="expiresAt" name="expiresAt" + [minDate]="minDate" placeholder="yyyy-mm-dd" formControlName="expiresAt" ngbDatepicker @@ -123,3 +124,4 @@ + diff --git a/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts index b72d4017a..e78f53d42 100644 --- a/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts +++ b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts @@ -1,9 +1,12 @@ +import {HttpErrorResponse} from '@angular/common/http'; import {Component, Input, OnInit} from '@angular/core'; +import { FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ApiKeysService } from '../../service/api-keys.service'; -import { FormBuilder, FormGroup, Validators, FormArray } from '@angular/forms'; +import {IpFormsValidators} from '../../../../../rule-management/app-rule/validators/ip.forms.validators'; +import {UtmToastService} from '../../../../../shared/alert/utm-toast.service'; import {ApiKeyUpsert} from '../../models/ApiKeyUpsert'; -import {IpFormsValidators} from "../../../../../rule-management/app-rule/validators/ip.forms.validators"; +import { ApiKeysService } from '../../service/api-keys.service'; +import {ApiKeyResponse} from "../../models/ApiKeyResponse"; @Component({ selector: 'app-api-key-modal', @@ -19,12 +22,13 @@ export class ApiKeyModalComponent implements OnInit { loading = false; errorMsg = ''; isSaving: string | string[] | Set | { [p: string]: any }; - ipInputError: string = ''; + minDate = { year: new Date().getFullYear(), month: new Date().getMonth() + 1, day: new Date().getDate() }; + ipInputError = ''; - constructor( - public activeModal: NgbActiveModal, - private apiKeyService: ApiKeysService, - private fb: FormBuilder) { + constructor( public activeModal: NgbActiveModal, + private apiKeyService: ApiKeysService, + private fb: FormBuilder, + private toastService: UtmToastService) { this.apiKeyForm = this.fb.group({ name: ['', Validators.required], @@ -86,18 +90,31 @@ export class ApiKeyModalComponent implements OnInit { } this.loading = true; + + const rawDate = this.apiKeyForm.get('expiresAt').value; + let formattedDate = rawDate; + + if (rawDate && typeof rawDate === 'object') { + formattedDate = `${rawDate.year}-${String(rawDate.month).padStart(2, '0')}-${String(rawDate.day).padStart(2, '0')}T00:00:00.000Z`; + } + const payload = { ...this.apiKeyForm.value, - expiresAt: this.apiKeyForm.value.expiresAt + ':00.000Z', + expiresAt: formattedDate, }; this.apiKeyService.create(payload).subscribe({ - next: () => { + next: (response) => { this.loading = false; - this.activeModal.close('created'); + this.activeModal.close(response.body as ApiKeyResponse); }, - error: (err) => { + error: (err: HttpErrorResponse) => { this.loading = false; - this.errorMsg = err.error.message || 'An error occurred while creating the API key.'; + if (err.status === 409) { + this.toastService.showError('Error', 'An API key with this name already exists.'); + } else if (err.status === 500) { + this.toastService.showError('Error', 'Server error occurred while creating the API key.'); + } + } }); } diff --git a/frontend/src/app/app-management/api-keys/shared/models/ApiKeyResponse.ts b/frontend/src/app/app-management/api-keys/shared/models/ApiKeyResponse.ts index 8026a23ab..3f6b890d7 100644 --- a/frontend/src/app/app-management/api-keys/shared/models/ApiKeyResponse.ts +++ b/frontend/src/app/app-management/api-keys/shared/models/ApiKeyResponse.ts @@ -2,7 +2,7 @@ export interface ApiKeyResponse { id: string; name: string; allowedIp: string[]; - createdAt: Date; - expiresAt?: Date; - generatedAt?: Date; + createdAt: string; + expiresAt?: string; + generatedAt?: string; } diff --git a/frontend/src/app/app-management/api-keys/shared/service/api-keys.service.ts b/frontend/src/app/app-management/api-keys/shared/service/api-keys.service.ts index 210755994..e668367ba 100644 --- a/frontend/src/app/app-management/api-keys/shared/service/api-keys.service.ts +++ b/frontend/src/app/app-management/api-keys/shared/service/api-keys.service.ts @@ -80,6 +80,13 @@ export class ApiKeysService { ); } + generateApiKey(apiKeyId: string): Observable> { + return this.http.post(`${this.resourceUrl}/${apiKeyId}/generate`, null, { + observe: 'response', + responseType: 'text' + }); + } + /** * Search API key usage in Elasticsearch */ From 9ee4d9c4ebfd094476bd8f7fb917d1100f9ff4c3 Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Fri, 24 Oct 2025 12:20:24 -0500 Subject: [PATCH 020/128] feat(api_keys): update API key modal for editing and improved deletion confirmation Signed-off-by: Manuel Abascal --- .../api-keys/api-keys.component.html | 4 +- .../api-keys/api-keys.component.ts | 51 ++++++++++++++++--- .../api-key-modal/api-key-modal.component.ts | 31 +++++++---- 3 files changed, 67 insertions(+), 19 deletions(-) diff --git a/frontend/src/app/app-management/api-keys/api-keys.component.html b/frontend/src/app/app-management/api-keys/api-keys.component.html index 22b6db422..d31f55a06 100644 --- a/frontend/src/app/app-management/api-keys/api-keys.component.html +++ b/frontend/src/app/app-management/api-keys/api-keys.component.html @@ -63,10 +63,10 @@
- -
ACTIONS
{{ key.name }} {{ key.allowedIp?.join(', ') || '—' }} - - + + - {{ key.expiresAt ? (key.expiresAt | date:'dd/MM/yy HH:mm' :'UTC') : '—' }} + + + + + + + + {{ key.expiresAt ? (key.expiresAt | date:'dd/MM/yy HH:mm':'UTC') : '—' }} {{ key.createdAt | date:'dd/MM/yy HH:mm' :'UTC' }}
+ +
+
+ + +
+
- - Loading... - No API Keys found. - +
+
+ + + + +
+ +
diff --git a/frontend/src/app/app-management/api-keys/api-keys.component.ts b/frontend/src/app/app-management/api-keys/api-keys.component.ts index c2d305776..f3ac14e08 100644 --- a/frontend/src/app/app-management/api-keys/api-keys.component.ts +++ b/frontend/src/app/app-management/api-keys/api-keys.component.ts @@ -1,15 +1,15 @@ import {Component, OnInit, TemplateRef, ViewChild} from '@angular/core'; import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; import * as moment from 'moment'; -import {UtmToastService} from "../../shared/alert/utm-toast.service"; +import {UtmToastService} from '../../shared/alert/utm-toast.service'; import { ModalConfirmationComponent -} from "../../shared/components/utm/util/modal-confirmation/modal-confirmation.component"; +} from '../../shared/components/utm/util/modal-confirmation/modal-confirmation.component'; import {ITEMS_PER_PAGE} from '../../shared/constants/pagination.constants'; import {SortEvent} from '../../shared/directives/sortable/type/sort-event'; -import { ApiKeyModalComponent } from './shared/components/api-key-modal/api-key-modal.component'; -import { ApiKeyResponse } from './shared/models/ApiKeyResponse'; -import { ApiKeysService } from './shared/service/api-keys.service'; +import {ApiKeyModalComponent} from './shared/components/api-key-modal/api-key-modal.component'; +import {ApiKeyResponse} from './shared/models/ApiKeyResponse'; +import {ApiKeysService} from './shared/service/api-keys.service'; @Component({ selector: 'app-api-keys', @@ -19,18 +19,23 @@ import { ApiKeysService } from './shared/service/api-keys.service'; export class ApiKeysComponent implements OnInit { generating: string[] = []; + noData = false; apiKeys: ApiKeyResponse[] = []; loading = false; generatedApiKey = ''; @ViewChild('generatedModal') generatedModal!: TemplateRef; - request = - { - sort: 'createdAt,desc', - page: 0, - size: ITEMS_PER_PAGE - }; generatedModalRef!: NgbModalRef; copied = false; + readonly itemsPerPage = ITEMS_PER_PAGE; + totalItems = 0; + page = 0; + size = this.itemsPerPage; + + request = { + sort: 'createdAt,desc', + page: this.page, + size: this.size + }; constructor( private toastService: UtmToastService, private apiKeyService: ApiKeysService, @@ -43,9 +48,11 @@ export class ApiKeysComponent implements OnInit { loadKeys(): void { this.loading = true; - this.apiKeyService.list().subscribe({ + this.apiKeyService.list(this.request).subscribe({ next: (res) => { + this.totalItems = Number(res.headers.get('X-Total-Count')); this.apiKeys = res.body || []; + this.noData = this.apiKeys.length === 0; this.loading = false; }, error: () => { @@ -113,15 +120,17 @@ export class ApiKeysComponent implements OnInit { getDaysUntilExpire(expiresAt: string): number { if (!expiresAt) { - return 0; + return -1; } - const today = moment(); - const expireDate = moment(expiresAt); + + const today = moment().startOf('day'); + const expireDate = moment(expiresAt).startOf('day'); return expireDate.diff(today, 'days'); } - onSortBy($event: SortEvent | string) { - + onSortBy($event: SortEvent) { + this.request.sort = $event.column + ',' + $event.direction; + this.loadKeys(); } maskSecrets(str: string): string { @@ -160,4 +169,23 @@ export class ApiKeysComponent implements OnInit { this.copied = false; this.generatedApiKey = ''; } + + loadPage($event: number) { + this.page = $event - 1; + this.request = { + ...this.request, + page: this.page + }; + this.loadKeys(); + } + + onItemsPerPageChange($event: number) { + this.request = { + ...this.request, + size: $event, + page: 0 + }; + this.page = 0; + this.loadKeys(); + } } diff --git a/frontend/src/app/app-management/api-keys/shared/service/api-keys.service.ts b/frontend/src/app/app-management/api-keys/shared/service/api-keys.service.ts index e668367ba..e239d5e4d 100644 --- a/frontend/src/app/app-management/api-keys/shared/service/api-keys.service.ts +++ b/frontend/src/app/app-management/api-keys/shared/service/api-keys.service.ts @@ -4,6 +4,7 @@ import { Observable } from 'rxjs'; import { SERVER_API_URL } from '../../../../app.constants'; import { ApiKeyResponse } from '../models/ApiKeyResponse'; import { ApiKeyUpsert } from '../models/ApiKeyUpsert'; +import {createRequestOption} from "../../../../shared/util/request-util"; /** * Service for managing API keys @@ -53,9 +54,10 @@ export class ApiKeysService { * List all API keys (with optional pagination) */ list(params?: any): Observable> { + const httpParams = createRequestOption(params); return this.http.get( this.resourceUrl, - { observe: 'response', params } + { observe: 'response', params: httpParams }, ); } From b9b555a5bb598fb1f266396b75062697c4d9bb9a Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Tue, 28 Oct 2025 14:29:59 -0500 Subject: [PATCH 024/128] feat(api_keys): simplify API key listing endpoint and enhance expiration handling --- backend/.gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/.gitignore b/backend/.gitignore index 834dd8821..518edd788 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -62,6 +62,7 @@ local.properties *.orig classes/ out/ +.nvim/ ###################### # Visual Studio Code From b75f5e78eda2b98eb1cb6dbdefe89b910e7cb2a1 Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Tue, 28 Oct 2025 14:30:48 -0500 Subject: [PATCH 025/128] feat(api_keys): simplify API key listing endpoint and enhance expiration handling --- backend/.nvim/.env | 18 --- backend/mvnw | 286 --------------------------------------------- backend/mvnw.cmd | 161 ------------------------- 3 files changed, 465 deletions(-) delete mode 100644 backend/.nvim/.env delete mode 100755 backend/mvnw delete mode 100755 backend/mvnw.cmd diff --git a/backend/.nvim/.env b/backend/.nvim/.env deleted file mode 100644 index 628f21921..000000000 --- a/backend/.nvim/.env +++ /dev/null @@ -1,18 +0,0 @@ -revision=4 -AD_AUDIT_SERVICE=http://localhost:8081/api -DB_HOST=192.168.1.18 -DB_NAME=utmstack -DB_PASS=DF7JOMKyU6oNwmy4 -DB_PORT=5432 -DB_USER=postgres -ELASTICSEARCH_HOST=192.168.1.18 -ELASTICSEARCH_PORT=9200 -ENCRYPTION_KEY=nWkGxX1vRTDyZc1Jm59YUkR3KsUmbYrJ -EVENT_PROCESSOR_HOST=192.168.1.18 -EVENT_PROCESSOR_PORT=9002 -GRPC_AGENT_MANAGER_HOST=192.168.1.18 -GRPC_AGENT_MANAGER_PORT=9000 -INTERNAL_KEY=qNANPjjNm7eantt7sgld0iSWFFeGKz5i -LOGSTASH_URL=http://localhost:9600 -SERVER_NAME=UTM -SOC_AI_BASE_URL=http://localhost:8081/process diff --git a/backend/mvnw b/backend/mvnw deleted file mode 100755 index 5551fde8e..000000000 --- a/backend/mvnw +++ /dev/null @@ -1,286 +0,0 @@ -#!/bin/sh -# ---------------------------------------------------------------------------- -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you 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. -# ---------------------------------------------------------------------------- - -# ---------------------------------------------------------------------------- -# Maven2 Start Up Batch script -# -# Required ENV vars: -# ------------------ -# JAVA_HOME - location of a JDK home dir -# -# Optional ENV vars -# ----------------- -# M2_HOME - location of maven2's installed home dir -# MAVEN_OPTS - parameters passed to the Java VM when running Maven -# e.g. to debug Maven itself, use -# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -# MAVEN_SKIP_RC - flag to disable loading of mavenrc files -# ---------------------------------------------------------------------------- - -if [ -z "$MAVEN_SKIP_RC" ] ; then - - if [ -f /etc/mavenrc ] ; then - . /etc/mavenrc - fi - - if [ -f "$HOME/.mavenrc" ] ; then - . "$HOME/.mavenrc" - fi - -fi - -# OS specific support. $var _must_ be set to either true or false. -cygwin=false; -darwin=false; -mingw=false -case "`uname`" in - CYGWIN*) cygwin=true ;; - MINGW*) mingw=true;; - Darwin*) darwin=true - # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home - # See https://developer.apple.com/library/mac/qa/qa1170/_index.html - if [ -z "$JAVA_HOME" ]; then - if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" - else - export JAVA_HOME="/Library/Java/Home" - fi - fi - ;; -esac - -if [ -z "$JAVA_HOME" ] ; then - if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` - fi -fi - -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - PRG="$0" - - # need this for relative symlinks - while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG="`dirname "$PRG"`/$link" - fi - done - - saveddir=`pwd` - - M2_HOME=`dirname "$PRG"`/.. - - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` - - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi - -# For Cygwin, ensure paths are in UNIX format before anything is touched -if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` -fi - -# For Mingw, ensure paths are in UNIX format before anything is touched -if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" - # TODO classpath? -fi - -if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then - # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then - if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" - else - javaExecutable="`readlink -f \"$javaExecutable\"`" - fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` - JAVA_HOME="$javaHome" - export JAVA_HOME - fi - fi -fi - -if [ -z "$JAVACMD" ] ; then - if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - else - JAVACMD="`which java`" - fi -fi - -if [ ! -x "$JAVACMD" ] ; then - echo "Error: JAVA_HOME is not defined correctly." >&2 - echo " We cannot execute $JAVACMD" >&2 - exit 1 -fi - -if [ -z "$JAVA_HOME" ] ; then - echo "Warning: JAVA_HOME environment variable is not set." -fi - -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher - -# traverses directory structure from process work directory to filesystem root -# first directory with .mvn subdirectory is considered project base directory -find_maven_basedir() { - - if [ -z "$1" ] - then - echo "Path not specified to find_maven_basedir" - return 1 - fi - - basedir="$1" - wdir="$1" - while [ "$wdir" != '/' ] ; do - if [ -d "$wdir"/.mvn ] ; then - basedir=$wdir - break - fi - # workaround for JBEAP-8937 (on Solaris 10/Sparc) - if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` - fi - # end of workaround - done - echo "${basedir}" -} - -# concatenates all lines of a file -concat_lines() { - if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" - fi -} - -BASE_DIR=`find_maven_basedir "$(pwd)"` -if [ -z "$BASE_DIR" ]; then - exit 1; -fi - -########################################################################################## -# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -# This allows using the maven wrapper in projects that prohibit checking in binary data. -########################################################################################## -if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found .mvn/wrapper/maven-wrapper.jar" - fi -else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." - fi - jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" - while IFS="=" read key value; do - case "$key" in (wrapperUrl) jarUrl="$value"; break ;; - esac - done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" - if [ "$MVNW_VERBOSE" = true ]; then - echo "Downloading from: $jarUrl" - fi - wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" - - if command -v wget > /dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found wget ... using wget" - fi - wget "$jarUrl" -O "$wrapperJarPath" - elif command -v curl > /dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found curl ... using curl" - fi - curl -o "$wrapperJarPath" "$jarUrl" - else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Falling back to using Java to download" - fi - javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" - if [ -e "$javaClass" ]; then - if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Compiling MavenWrapperDownloader.java ..." - fi - # Compiling the Java class - ("$JAVA_HOME/bin/javac" "$javaClass") - fi - if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - # Running the downloader - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Running MavenWrapperDownloader.java ..." - fi - ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") - fi - fi - fi -fi -########################################################################################## -# End of extension -########################################################################################## - -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -if [ "$MVNW_VERBOSE" = true ]; then - echo $MAVEN_PROJECTBASEDIR -fi -MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" - -# For Cygwin, switch paths to Windows format before running java -if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` - [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` -fi - -WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -exec "$JAVACMD" \ - $MAVEN_OPTS \ - -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ - ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/backend/mvnw.cmd b/backend/mvnw.cmd deleted file mode 100755 index e5cfb0ae9..000000000 --- a/backend/mvnw.cmd +++ /dev/null @@ -1,161 +0,0 @@ -@REM ---------------------------------------------------------------------------- -@REM Licensed to the Apache Software Foundation (ASF) under one -@REM or more contributor license agreements. See the NOTICE file -@REM distributed with this work for additional information -@REM regarding copyright ownership. The ASF licenses this file -@REM to you under the Apache License, Version 2.0 (the -@REM "License"); you may not use this file except in compliance -@REM with the License. You may obtain a copy of the License at -@REM -@REM http://www.apache.org/licenses/LICENSE-2.0 -@REM -@REM Unless required by applicable law or agreed to in writing, -@REM software distributed under the License is distributed on an -@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -@REM KIND, either express or implied. See the License for the -@REM specific language governing permissions and limitations -@REM under the License. -@REM ---------------------------------------------------------------------------- - -@REM ---------------------------------------------------------------------------- -@REM Maven2 Start Up Batch script -@REM -@REM Required ENV vars: -@REM JAVA_HOME - location of a JDK home dir -@REM -@REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir -@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending -@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven -@REM e.g. to debug Maven itself, use -@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files -@REM ---------------------------------------------------------------------------- - -@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' -@echo off -@REM set title of command window -title %0 -@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' -@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% - -@REM set %HOME% to equivalent of $HOME -if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") - -@REM Execute a user defined script before this one -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre -@REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" -:skipRcPre - -@setlocal - -set ERROR_CODE=0 - -@REM To isolate internal variables from possible post scripts, we use another setlocal -@setlocal - -@REM ==== START VALIDATION ==== -if not "%JAVA_HOME%" == "" goto OkJHome - -echo. -echo Error: JAVA_HOME not found in your environment. >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -:OkJHome -if exist "%JAVA_HOME%\bin\java.exe" goto init - -echo. -echo Error: JAVA_HOME is set to an invalid directory. >&2 -echo JAVA_HOME = "%JAVA_HOME%" >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -@REM ==== END VALIDATION ==== - -:init - -@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". -@REM Fallback to current working directory if not found. - -set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% -IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir - -set EXEC_DIR=%CD% -set WDIR=%EXEC_DIR% -:findBaseDir -IF EXIST "%WDIR%"\.mvn goto baseDirFound -cd .. -IF "%WDIR%"=="%CD%" goto baseDirNotFound -set WDIR=%CD% -goto findBaseDir - -:baseDirFound -set MAVEN_PROJECTBASEDIR=%WDIR% -cd "%EXEC_DIR%" -goto endDetectBaseDir - -:baseDirNotFound -set MAVEN_PROJECTBASEDIR=%EXEC_DIR% -cd "%EXEC_DIR%" - -:endDetectBaseDir - -IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig - -@setlocal EnableExtensions EnableDelayedExpansion -for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a -@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% - -:endReadAdditionalConfig - -SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" -set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" -set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" -FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO ( - IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B -) - -@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -@REM This allows using the maven wrapper in projects that prohibit checking in binary data. -if exist %WRAPPER_JAR% ( - echo Found %WRAPPER_JAR% -) else ( - echo Couldn't find %WRAPPER_JAR%, downloading it ... - echo Downloading from: %DOWNLOAD_URL% - powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')" - echo Finished downloading %WRAPPER_JAR% -) -@REM End of extension - -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* -if ERRORLEVEL 1 goto error -goto end - -:error -set ERROR_CODE=1 - -:end -@endlocal & set ERROR_CODE=%ERROR_CODE% - -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost -@REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" -:skipRcPost - -@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause - -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% - -exit /B %ERROR_CODE% From fd36727998ed58870a1a74b94534318578ab5b51 Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Tue, 28 Oct 2025 14:32:44 -0500 Subject: [PATCH 026/128] feat(api_keys): simplify API key listing endpoint and enhance expiration handling --- backend/mvnw | 286 +++++++++++++++++++++++++++++++++++++++++++++++ backend/mvnw.cmd | 161 ++++++++++++++++++++++++++ 2 files changed, 447 insertions(+) create mode 100755 backend/mvnw create mode 100755 backend/mvnw.cmd diff --git a/backend/mvnw b/backend/mvnw new file mode 100755 index 000000000..5551fde8e --- /dev/null +++ b/backend/mvnw @@ -0,0 +1,286 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven2 Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" + # TODO classpath? +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + wget "$jarUrl" -O "$wrapperJarPath" + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + curl -o "$wrapperJarPath" "$jarUrl" + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/backend/mvnw.cmd b/backend/mvnw.cmd new file mode 100755 index 000000000..e5cfb0ae9 --- /dev/null +++ b/backend/mvnw.cmd @@ -0,0 +1,161 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven2 Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" +FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + echo Found %WRAPPER_JAR% +) else ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')" + echo Finished downloading %WRAPPER_JAR% +) +@REM End of extension + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% From 850c1e456baae97b23bb69c1ec4dfbbbf03bb9dd Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Tue, 28 Oct 2025 14:37:48 -0500 Subject: [PATCH 027/128] feat(api_keys): simplify API key listing endpoint and enhance expiration handling --- backend/mvnw | 286 ----------------------------------------------- backend/mvnw.cmd | 161 -------------------------- 2 files changed, 447 deletions(-) delete mode 100755 backend/mvnw delete mode 100755 backend/mvnw.cmd diff --git a/backend/mvnw b/backend/mvnw deleted file mode 100755 index 5551fde8e..000000000 --- a/backend/mvnw +++ /dev/null @@ -1,286 +0,0 @@ -#!/bin/sh -# ---------------------------------------------------------------------------- -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you 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. -# ---------------------------------------------------------------------------- - -# ---------------------------------------------------------------------------- -# Maven2 Start Up Batch script -# -# Required ENV vars: -# ------------------ -# JAVA_HOME - location of a JDK home dir -# -# Optional ENV vars -# ----------------- -# M2_HOME - location of maven2's installed home dir -# MAVEN_OPTS - parameters passed to the Java VM when running Maven -# e.g. to debug Maven itself, use -# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -# MAVEN_SKIP_RC - flag to disable loading of mavenrc files -# ---------------------------------------------------------------------------- - -if [ -z "$MAVEN_SKIP_RC" ] ; then - - if [ -f /etc/mavenrc ] ; then - . /etc/mavenrc - fi - - if [ -f "$HOME/.mavenrc" ] ; then - . "$HOME/.mavenrc" - fi - -fi - -# OS specific support. $var _must_ be set to either true or false. -cygwin=false; -darwin=false; -mingw=false -case "`uname`" in - CYGWIN*) cygwin=true ;; - MINGW*) mingw=true;; - Darwin*) darwin=true - # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home - # See https://developer.apple.com/library/mac/qa/qa1170/_index.html - if [ -z "$JAVA_HOME" ]; then - if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" - else - export JAVA_HOME="/Library/Java/Home" - fi - fi - ;; -esac - -if [ -z "$JAVA_HOME" ] ; then - if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` - fi -fi - -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - PRG="$0" - - # need this for relative symlinks - while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG="`dirname "$PRG"`/$link" - fi - done - - saveddir=`pwd` - - M2_HOME=`dirname "$PRG"`/.. - - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` - - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi - -# For Cygwin, ensure paths are in UNIX format before anything is touched -if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` -fi - -# For Mingw, ensure paths are in UNIX format before anything is touched -if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" - # TODO classpath? -fi - -if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then - # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then - if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" - else - javaExecutable="`readlink -f \"$javaExecutable\"`" - fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` - JAVA_HOME="$javaHome" - export JAVA_HOME - fi - fi -fi - -if [ -z "$JAVACMD" ] ; then - if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - else - JAVACMD="`which java`" - fi -fi - -if [ ! -x "$JAVACMD" ] ; then - echo "Error: JAVA_HOME is not defined correctly." >&2 - echo " We cannot execute $JAVACMD" >&2 - exit 1 -fi - -if [ -z "$JAVA_HOME" ] ; then - echo "Warning: JAVA_HOME environment variable is not set." -fi - -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher - -# traverses directory structure from process work directory to filesystem root -# first directory with .mvn subdirectory is considered project base directory -find_maven_basedir() { - - if [ -z "$1" ] - then - echo "Path not specified to find_maven_basedir" - return 1 - fi - - basedir="$1" - wdir="$1" - while [ "$wdir" != '/' ] ; do - if [ -d "$wdir"/.mvn ] ; then - basedir=$wdir - break - fi - # workaround for JBEAP-8937 (on Solaris 10/Sparc) - if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` - fi - # end of workaround - done - echo "${basedir}" -} - -# concatenates all lines of a file -concat_lines() { - if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" - fi -} - -BASE_DIR=`find_maven_basedir "$(pwd)"` -if [ -z "$BASE_DIR" ]; then - exit 1; -fi - -########################################################################################## -# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -# This allows using the maven wrapper in projects that prohibit checking in binary data. -########################################################################################## -if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found .mvn/wrapper/maven-wrapper.jar" - fi -else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." - fi - jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" - while IFS="=" read key value; do - case "$key" in (wrapperUrl) jarUrl="$value"; break ;; - esac - done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" - if [ "$MVNW_VERBOSE" = true ]; then - echo "Downloading from: $jarUrl" - fi - wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" - - if command -v wget > /dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found wget ... using wget" - fi - wget "$jarUrl" -O "$wrapperJarPath" - elif command -v curl > /dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found curl ... using curl" - fi - curl -o "$wrapperJarPath" "$jarUrl" - else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Falling back to using Java to download" - fi - javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" - if [ -e "$javaClass" ]; then - if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Compiling MavenWrapperDownloader.java ..." - fi - # Compiling the Java class - ("$JAVA_HOME/bin/javac" "$javaClass") - fi - if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - # Running the downloader - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Running MavenWrapperDownloader.java ..." - fi - ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") - fi - fi - fi -fi -########################################################################################## -# End of extension -########################################################################################## - -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -if [ "$MVNW_VERBOSE" = true ]; then - echo $MAVEN_PROJECTBASEDIR -fi -MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" - -# For Cygwin, switch paths to Windows format before running java -if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` - [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` -fi - -WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -exec "$JAVACMD" \ - $MAVEN_OPTS \ - -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ - ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/backend/mvnw.cmd b/backend/mvnw.cmd deleted file mode 100755 index e5cfb0ae9..000000000 --- a/backend/mvnw.cmd +++ /dev/null @@ -1,161 +0,0 @@ -@REM ---------------------------------------------------------------------------- -@REM Licensed to the Apache Software Foundation (ASF) under one -@REM or more contributor license agreements. See the NOTICE file -@REM distributed with this work for additional information -@REM regarding copyright ownership. The ASF licenses this file -@REM to you under the Apache License, Version 2.0 (the -@REM "License"); you may not use this file except in compliance -@REM with the License. You may obtain a copy of the License at -@REM -@REM http://www.apache.org/licenses/LICENSE-2.0 -@REM -@REM Unless required by applicable law or agreed to in writing, -@REM software distributed under the License is distributed on an -@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -@REM KIND, either express or implied. See the License for the -@REM specific language governing permissions and limitations -@REM under the License. -@REM ---------------------------------------------------------------------------- - -@REM ---------------------------------------------------------------------------- -@REM Maven2 Start Up Batch script -@REM -@REM Required ENV vars: -@REM JAVA_HOME - location of a JDK home dir -@REM -@REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir -@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending -@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven -@REM e.g. to debug Maven itself, use -@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files -@REM ---------------------------------------------------------------------------- - -@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' -@echo off -@REM set title of command window -title %0 -@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' -@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% - -@REM set %HOME% to equivalent of $HOME -if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") - -@REM Execute a user defined script before this one -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre -@REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" -:skipRcPre - -@setlocal - -set ERROR_CODE=0 - -@REM To isolate internal variables from possible post scripts, we use another setlocal -@setlocal - -@REM ==== START VALIDATION ==== -if not "%JAVA_HOME%" == "" goto OkJHome - -echo. -echo Error: JAVA_HOME not found in your environment. >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -:OkJHome -if exist "%JAVA_HOME%\bin\java.exe" goto init - -echo. -echo Error: JAVA_HOME is set to an invalid directory. >&2 -echo JAVA_HOME = "%JAVA_HOME%" >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -@REM ==== END VALIDATION ==== - -:init - -@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". -@REM Fallback to current working directory if not found. - -set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% -IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir - -set EXEC_DIR=%CD% -set WDIR=%EXEC_DIR% -:findBaseDir -IF EXIST "%WDIR%"\.mvn goto baseDirFound -cd .. -IF "%WDIR%"=="%CD%" goto baseDirNotFound -set WDIR=%CD% -goto findBaseDir - -:baseDirFound -set MAVEN_PROJECTBASEDIR=%WDIR% -cd "%EXEC_DIR%" -goto endDetectBaseDir - -:baseDirNotFound -set MAVEN_PROJECTBASEDIR=%EXEC_DIR% -cd "%EXEC_DIR%" - -:endDetectBaseDir - -IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig - -@setlocal EnableExtensions EnableDelayedExpansion -for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a -@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% - -:endReadAdditionalConfig - -SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" -set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" -set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" -FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO ( - IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B -) - -@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -@REM This allows using the maven wrapper in projects that prohibit checking in binary data. -if exist %WRAPPER_JAR% ( - echo Found %WRAPPER_JAR% -) else ( - echo Couldn't find %WRAPPER_JAR%, downloading it ... - echo Downloading from: %DOWNLOAD_URL% - powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')" - echo Finished downloading %WRAPPER_JAR% -) -@REM End of extension - -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* -if ERRORLEVEL 1 goto error -goto end - -:error -set ERROR_CODE=1 - -:end -@endlocal & set ERROR_CODE=%ERROR_CODE% - -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost -@REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" -:skipRcPost - -@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause - -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% - -exit /B %ERROR_CODE% From f45194a0e7db655e2cba7f45ae26e4eb2f1f43a0 Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Tue, 28 Oct 2025 14:39:25 -0500 Subject: [PATCH 028/128] feat(api_keys): simplify API key listing endpoint and enhance expiration handling --- backend/.gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/.gitignore b/backend/.gitignore index 518edd788..8de108186 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -138,6 +138,8 @@ Desktop.ini # Maven Wrapper ###################### !.mvn/wrapper/maven-wrapper.jar +mvnw +mvn.cmd ###################### # ESLint From 3cd54c4f3c6ce254162d6587aa051404fae54df3 Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Tue, 28 Oct 2025 14:40:04 -0500 Subject: [PATCH 029/128] feat(api_keys): simplify API key listing endpoint and enhance expiration handling --- backend/.gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/.gitignore b/backend/.gitignore index 8de108186..1e4c485c4 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -139,7 +139,7 @@ Desktop.ini ###################### !.mvn/wrapper/maven-wrapper.jar mvnw -mvn.cmd +mvnw.cmd ###################### # ESLint From b3f8c68ea559a47a02169b35a8c6bc92b760b9d4 Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Tue, 28 Oct 2025 14:40:51 -0500 Subject: [PATCH 030/128] feat(api_keys): simplify API key listing endpoint and enhance expiration handling --- backend/.gitignore | 2 -- 1 file changed, 2 deletions(-) diff --git a/backend/.gitignore b/backend/.gitignore index 1e4c485c4..518edd788 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -138,8 +138,6 @@ Desktop.ini # Maven Wrapper ###################### !.mvn/wrapper/maven-wrapper.jar -mvnw -mvnw.cmd ###################### # ESLint From 83118a99f56993c2cd2342ad5dbf357896d52249 Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Tue, 28 Oct 2025 14:41:55 -0500 Subject: [PATCH 031/128] feat(api_keys): simplify API key listing endpoint and enhance expiration handling --- backend/mvnw | 286 +++++++++++++++++++++++++++++++++++++++++++++++ backend/mvnw.cmd | 161 ++++++++++++++++++++++++++ 2 files changed, 447 insertions(+) create mode 100644 backend/mvnw create mode 100644 backend/mvnw.cmd diff --git a/backend/mvnw b/backend/mvnw new file mode 100644 index 000000000..5551fde8e --- /dev/null +++ b/backend/mvnw @@ -0,0 +1,286 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven2 Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" + # TODO classpath? +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + wget "$jarUrl" -O "$wrapperJarPath" + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + curl -o "$wrapperJarPath" "$jarUrl" + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/backend/mvnw.cmd b/backend/mvnw.cmd new file mode 100644 index 000000000..e5cfb0ae9 --- /dev/null +++ b/backend/mvnw.cmd @@ -0,0 +1,161 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven2 Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" +FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + echo Found %WRAPPER_JAR% +) else ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')" + echo Finished downloading %WRAPPER_JAR% +) +@REM End of extension + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% From 4ce6306028fedf8d8c350c0abb80a7285cd79a20 Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Tue, 28 Oct 2025 14:43:23 -0500 Subject: [PATCH 032/128] Update frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../shared/components/api-key-modal/api-key-modal.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts index 87289aee6..f2378c13d 100644 --- a/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts +++ b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts @@ -54,7 +54,7 @@ export class ApiKeyModalComponent implements OnInit { const trimmedIp = this.ipInput.trim(); if (!trimmedIp) { - this.ipInputError = 'Please enter an IP address or CIDR'; // Se asigna el error + this.ipInputError = 'Please enter an IP address or CIDR'; // Error is assigned return; } From c6c9b59031b09bf4210c9fd62937b24e414c952b Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Tue, 28 Oct 2025 14:45:14 -0500 Subject: [PATCH 033/128] Update backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../java/com/park/utmstack/security/api_key/ApiKeyFilter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java b/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java index 576a2556a..185db7321 100644 --- a/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java +++ b/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java @@ -49,7 +49,7 @@ public class ApiKeyFilter extends OncePerRequestFilter { private final UserRepository userRepository; private final ApiKeyService apiKeyService; - private final ConcurrentMap invalidApiKeyBlackList = new ConcurrentHashMap<>();; + private final ConcurrentMap invalidApiKeyBlackList = new ConcurrentHashMap<>(); private final ConcurrentMap cidrCache = new ConcurrentHashMap<>(); private final ApplicationEventService applicationEventService; private final ApiKeyUsageLoggingService apiKeyUsageLoggingService; From da17219c438729152e93041830c95ac90865931c Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Tue, 28 Oct 2025 14:46:12 -0500 Subject: [PATCH 034/128] feat(api_keys): simplify API key listing endpoint and enhance expiration handling --- .../java/com/park/utmstack/web/rest/api_key/ApiKeyResource.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/java/com/park/utmstack/web/rest/api_key/ApiKeyResource.java b/backend/src/main/java/com/park/utmstack/web/rest/api_key/ApiKeyResource.java index d0bdd0d2b..aa6b2052a 100644 --- a/backend/src/main/java/com/park/utmstack/web/rest/api_key/ApiKeyResource.java +++ b/backend/src/main/java/com/park/utmstack/web/rest/api_key/ApiKeyResource.java @@ -161,7 +161,7 @@ public ResponseEntity deleteApiKey(@PathVariable("id") Long apiKeyId) { return ResponseEntity.noContent().build(); } - @GetMapping("/usage") + @PostMapping("/usage") public ResponseEntity> search(@RequestBody(required = false) List filters, @RequestParam Integer top, @RequestParam String indexPattern, @RequestParam(required = false, defaultValue = "false") boolean includeChildren, From 47fea366797a4bc6c03b8f9737bb182c7cb26062 Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Wed, 29 Oct 2025 10:48:15 -0500 Subject: [PATCH 035/128] feat(api_keys): enhance clipboard functionality with fallback support and feedback Signed-off-by: Manuel Abascal --- .../api-keys/api-keys.component.ts | 47 +++++++++++++++++-- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/app-management/api-keys/api-keys.component.ts b/frontend/src/app/app-management/api-keys/api-keys.component.ts index f3ac14e08..82d9e3450 100644 --- a/frontend/src/app/app-management/api-keys/api-keys.component.ts +++ b/frontend/src/app/app-management/api-keys/api-keys.component.ts @@ -63,9 +63,50 @@ export class ApiKeysComponent implements OnInit { } copyToClipboard(): void { - (navigator as any).clipboard.writeText(this.generatedApiKey).then(() => { - this.copied = true; - }); + if (!this.generatedApiKey) { return; } + + if (navigator && (navigator as any).clipboard && (navigator as any).clipboard.writeText) { + (navigator as any).clipboard.writeText(this.generatedApiKey) + .then(() => this.copied = true) + .catch(err => { + console.error('Error al copiar con clipboard API', err); + this.fallbackCopy(this.generatedApiKey); + }); + } else { + this.fallbackCopy(this.generatedApiKey); + } + } + + private fallbackCopy(text: string): void { + try { + const textarea = document.createElement('textarea'); + textarea.value = text; + + textarea.style.position = 'fixed'; + textarea.style.top = '0'; + textarea.style.left = '0'; + textarea.style.opacity = '0'; + + document.body.appendChild(textarea); + textarea.focus(); + textarea.select(); + + const successful = document.execCommand('copy'); + document.body.removeChild(textarea); + + if (successful) { + this.showCopiedFeedback(); + } else { + console.warn('Fallback copy failed'); + } + } catch (err) { + console.error('Error en fallback copy', err); + } + } + + private showCopiedFeedback(): void { + this.copied = true; + setTimeout(() => this.copied = false, 2000); } openCreateModal(): void { From e73b24ca00f23925fa7c48d604ffe7a6c692b00d Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Wed, 29 Oct 2025 10:52:59 -0500 Subject: [PATCH 036/128] feat(api_key): enhance ApiKeyFilter with improved logging and validation checks --- .../com/park/utmstack/config/Constants.java | 5 -- .../domain/shared_types/ApplicationLayer.java | 1 + .../security/api_key/ApiKeyFilter.java | 67 +++++++++++-------- 3 files changed, 41 insertions(+), 32 deletions(-) diff --git a/backend/src/main/java/com/park/utmstack/config/Constants.java b/backend/src/main/java/com/park/utmstack/config/Constants.java index 35e6fdc4b..a36fe1733 100644 --- a/backend/src/main/java/com/park/utmstack/config/Constants.java +++ b/backend/src/main/java/com/park/utmstack/config/Constants.java @@ -162,11 +162,6 @@ public final class Constants { public static final String API_KEY_HEADER = "Utm-Api-Key"; public static final List API_ENDPOINT_IGNORE = Collections.emptyList(); - // Configuration data types for moduleGroupConfiguration - - public static final String CONF_TYPE_PASSWORD = "password"; - public static final String CONF_TYPE_FILE = "file"; - private Constants() { } } diff --git a/backend/src/main/java/com/park/utmstack/domain/shared_types/ApplicationLayer.java b/backend/src/main/java/com/park/utmstack/domain/shared_types/ApplicationLayer.java index 16bfac1cb..ed1a2af7f 100644 --- a/backend/src/main/java/com/park/utmstack/domain/shared_types/ApplicationLayer.java +++ b/backend/src/main/java/com/park/utmstack/domain/shared_types/ApplicationLayer.java @@ -7,6 +7,7 @@ @Getter public enum ApplicationLayer { SERVICE ("SERVICE"), + API ("API"), CONTROLLER ("CONTROLLER"); private final String value; diff --git a/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java b/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java index 185db7321..acefca689 100644 --- a/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java +++ b/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java @@ -3,6 +3,7 @@ import com.park.utmstack.config.Constants; import com.park.utmstack.domain.api_keys.ApiKey; +import com.park.utmstack.domain.shared_types.ApplicationLayer; import com.park.utmstack.loggin.api_key.ApiKeyUsageLoggingService; import com.park.utmstack.repository.UserRepository; import com.park.utmstack.service.api_key.ApiKeyService; @@ -31,6 +32,8 @@ import java.io.IOException; import java.time.Instant; import java.util.List; +import java.util.Map; +import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.regex.Pattern; @@ -44,9 +47,10 @@ public class ApiKeyFilter extends OncePerRequestFilter { private static final String LOG_USAGE_FLAG = "LOG_USAGE_DONE"; private static final Pattern CIDR_PATTERN = Pattern.compile( - "^((25[0-5]|2[0-4]\\d|[01]?\\d\\d?)(\\.(?!$)|$)){4}/(\\d|[1-2]\\d|3[0-2])$" + "^((25[0-5]|2[0-4]\\d|1\\d{2}|[1-9]?\\d)\\.){3}(25[0-5]|2[0-4]\\d|1\\d{2}|[1-9]?\\d)/(\\d|[1-2]\\d|3[0-2])$" ); + private final UserRepository userRepository; private final ApiKeyService apiKeyService; private final ConcurrentMap invalidApiKeyBlackList = new ConcurrentHashMap<>(); @@ -103,45 +107,54 @@ protected void doFilterInternal(@NonNull HttpServletRequest request, private ApiKey getApiKey(String apiKey) { if (invalidApiKeyBlackList.containsKey(apiKey)) { + log.info("API key invalid (cached)"); throw new ApiKeyInvalidAccessException("Invalid API key"); } + return apiKeyService.findOneByApiKey(apiKey) - .orElseThrow(() -> { - invalidApiKeyBlackList.put(apiKey, Boolean.TRUE); - return new ApiKeyInvalidAccessException("Invalid API key"); - }); + .orElseGet(() -> { + invalidApiKeyBlackList.put(apiKey, Boolean.TRUE); + log.info("API key invalid (not found)"); + throw new ApiKeyInvalidAccessException("Invalid API key"); + }); } public UsernamePasswordAuthenticationToken getAuthentication(ApiKey apiKey, String remoteIpAddress) { - try { - - if (!allowAccessToRemoteIp(apiKey.getAllowedIp(), remoteIpAddress)) { - throw new ApiKeyInvalidAccessException("Invalid IP address, if you recognize this IP address, add to allowed ip list"); - } + Objects.requireNonNull(apiKey, "API key must not be null"); + Objects.requireNonNull(remoteIpAddress, "Remote IP address must not be null"); + + if (!allowAccessToRemoteIp(apiKey.getAllowedIp(), remoteIpAddress)) { + log.warn("Access denied: IP [{}] not allowed for API key [{}]", remoteIpAddress, apiKey.getApiKey()); + throw new ApiKeyInvalidAccessException( + "Invalid IP address: " + remoteIpAddress + ". If you recognize this IP, add it to allowed IP list." + ); + } - if (apiKey.getExpiresAt() != null && !apiKey.getExpiresAt().isAfter(Instant.now())) { - throw new ApiKeyInvalidAccessException("API key expired at " + apiKey.getExpiresAt()); - } + if (apiKey.getExpiresAt() != null && !apiKey.getExpiresAt().isAfter(Instant.now())) { + log.warn("Access denied: API key [{}] expired at {}", apiKey.getApiKey(), apiKey.getExpiresAt()); + throw new ApiKeyInvalidAccessException("API key expired at " + apiKey.getExpiresAt()); + } - var userEntity = userRepository - .findById(apiKey.getUserId()) - .orElseThrow(() -> new ApiKeyInvalidAccessException("User not found for api key")); + var userEntityOpt = userRepository.findById(apiKey.getUserId()); + if (userEntityOpt.isEmpty()) { + log.warn("Access denied: User [{}] not found for API key [{}]", apiKey.getUserId(), apiKey.getApiKey()); + throw new ApiKeyInvalidAccessException("User not found for API key"); + } - if (!userEntity.getActivated()) { - throw new ApiKeyInvalidAccessException("User not activated"); - } + var userEntity = userEntityOpt.get(); - List authorities = userEntity.getAuthorities().stream() - .map(auth -> new SimpleGrantedAuthority(auth.getName())) - .toList(); + if (!userEntity.getActivated()) { + log.warn("Access denied: User [{}] not activated", userEntity.getLogin()); + throw new ApiKeyInvalidAccessException("User not activated"); + } - User principal = new User(userEntity.getLogin(), "", authorities); + List authorities = userEntity.getAuthorities().stream() + .map(auth -> new SimpleGrantedAuthority(auth.getName())) + .toList(); - return new UsernamePasswordAuthenticationToken(principal, apiKey.getApiKey(), authorities); + User principal = new User(userEntity.getLogin(), "", authorities); - } catch (Exception e) { - throw new ApiKeyInvalidAccessException(e.getMessage()); - } + return new UsernamePasswordAuthenticationToken(principal, apiKey.getApiKey(), authorities); } public boolean allowAccessToRemoteIp(String allowedIpList, String remoteIp) { From a1d08cddb907a5c213e013279cfb1bb845f28a26 Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Wed, 29 Oct 2025 11:06:00 -0500 Subject: [PATCH 037/128] feat(api_key): enhance ApiKeyFilter with improved logging and validation checks --- .../java/com/park/utmstack/security/api_key/ApiKeyFilter.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java b/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java index acefca689..8f5582528 100644 --- a/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java +++ b/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java @@ -107,14 +107,14 @@ protected void doFilterInternal(@NonNull HttpServletRequest request, private ApiKey getApiKey(String apiKey) { if (invalidApiKeyBlackList.containsKey(apiKey)) { - log.info("API key invalid (cached)"); + log.warn("Access attempt with invalid API key (cached)"); throw new ApiKeyInvalidAccessException("Invalid API key"); } return apiKeyService.findOneByApiKey(apiKey) .orElseGet(() -> { invalidApiKeyBlackList.put(apiKey, Boolean.TRUE); - log.info("API key invalid (not found)"); + log.warn("Access attempt with invalid API key (not found in DB)"); throw new ApiKeyInvalidAccessException("Invalid API key"); }); } From 201bbbd0a6f1a907e8fda07cd2515f94dc07756d Mon Sep 17 00:00:00 2001 From: AlexSanchez-bit Date: Thu, 16 Oct 2025 10:56:40 -0400 Subject: [PATCH 038/128] fix[frontend](web_console): sanitized password parameter to admit all utf8 characters even url structure ones --- frontend/src/app/core/auth/account.service.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/core/auth/account.service.ts b/frontend/src/app/core/auth/account.service.ts index 1631572ca..02ec14cb5 100644 --- a/frontend/src/app/core/auth/account.service.ts +++ b/frontend/src/app/core/auth/account.service.ts @@ -37,7 +37,8 @@ export class AccountService { } checkPassword(password: string, uuid: string): Observable> { - return this.http.get(SERVER_API_URL + `api/check-credentials?password=${password}&checkUUID=${uuid}`, { + const sanitized_password = encodeURIComponent(password) + return this.http.get(SERVER_API_URL + `api/check-credentials?password=${sanitized_password}&checkUUID=${uuid}`, { observe: 'response', responseType: 'text' }); From 38c4bf7b04e7bea9fd498795bbf809c06a49eacf Mon Sep 17 00:00:00 2001 From: AlexSanchez-bit Date: Fri, 17 Oct 2025 10:08:23 -0400 Subject: [PATCH 039/128] feat[backend](api-keys): added api keys dto, controllers and entities --- backend/.nvim/.env | 18 ++ backend/mvnw | 0 backend/mvnw.cmd | 0 .../domain/api_keys/UtmApiKeyModel.java | 50 +++++ .../dto/api_key/UtmApiKeyResponseDto.java | 36 ++++ .../dto/api_key/UtmApiKeyUpsertDto.java | 29 +++ .../rest/api-keys/UtmApiKeyController.java | 178 ++++++++++++++++++ .../changelog/20250917001_adding_api_keys.xml | 43 +++++ .../config/liquibase/scripts/tables.sql | 21 +++ 9 files changed, 375 insertions(+) create mode 100644 backend/.nvim/.env mode change 100644 => 100755 backend/mvnw mode change 100644 => 100755 backend/mvnw.cmd create mode 100644 backend/src/main/java/com/park/utmstack/domain/api_keys/UtmApiKeyModel.java create mode 100644 backend/src/main/java/com/park/utmstack/service/dto/api_key/UtmApiKeyResponseDto.java create mode 100644 backend/src/main/java/com/park/utmstack/service/dto/api_key/UtmApiKeyUpsertDto.java create mode 100644 backend/src/main/java/com/park/utmstack/web/rest/api-keys/UtmApiKeyController.java create mode 100644 backend/src/main/resources/config/liquibase/changelog/20250917001_adding_api_keys.xml diff --git a/backend/.nvim/.env b/backend/.nvim/.env new file mode 100644 index 000000000..628f21921 --- /dev/null +++ b/backend/.nvim/.env @@ -0,0 +1,18 @@ +revision=4 +AD_AUDIT_SERVICE=http://localhost:8081/api +DB_HOST=192.168.1.18 +DB_NAME=utmstack +DB_PASS=DF7JOMKyU6oNwmy4 +DB_PORT=5432 +DB_USER=postgres +ELASTICSEARCH_HOST=192.168.1.18 +ELASTICSEARCH_PORT=9200 +ENCRYPTION_KEY=nWkGxX1vRTDyZc1Jm59YUkR3KsUmbYrJ +EVENT_PROCESSOR_HOST=192.168.1.18 +EVENT_PROCESSOR_PORT=9002 +GRPC_AGENT_MANAGER_HOST=192.168.1.18 +GRPC_AGENT_MANAGER_PORT=9000 +INTERNAL_KEY=qNANPjjNm7eantt7sgld0iSWFFeGKz5i +LOGSTASH_URL=http://localhost:9600 +SERVER_NAME=UTM +SOC_AI_BASE_URL=http://localhost:8081/process diff --git a/backend/mvnw b/backend/mvnw old mode 100644 new mode 100755 diff --git a/backend/mvnw.cmd b/backend/mvnw.cmd old mode 100644 new mode 100755 diff --git a/backend/src/main/java/com/park/utmstack/domain/api_keys/UtmApiKeyModel.java b/backend/src/main/java/com/park/utmstack/domain/api_keys/UtmApiKeyModel.java new file mode 100644 index 000000000..8f532da04 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/domain/api_keys/UtmApiKeyModel.java @@ -0,0 +1,50 @@ +package com.park.utmstack.domain.api_keys; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.time.Instant; +import java.util.UUID; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "api_keys") +public class ApiKey implements Serializable { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + @Column(columnDefinition = "UUID") + private UUID id; + + @Column(nullable = false) + private UUID accountId; + + @Column(nullable = false) + private String name; + + @Column(nullable = false) + private String apiKey; + + @Column + private String allowedIp; + + @Column(nullable = false) + private Instant createdAt; + + private Instant generatedAt; + + @Column + private Instant expiresAt; +} diff --git a/backend/src/main/java/com/park/utmstack/service/dto/api_key/UtmApiKeyResponseDto.java b/backend/src/main/java/com/park/utmstack/service/dto/api_key/UtmApiKeyResponseDto.java new file mode 100644 index 000000000..232e77f56 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/service/dto/api_key/UtmApiKeyResponseDto.java @@ -0,0 +1,36 @@ +package com.utmstack.api.service.dto.apikey; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.Instant; +import java.util.List; +import java.util.UUID; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ApiKeyResponseDTO { + + @Schema(description = "Unique identifier of the API key") + private UUID id; + + @Schema(description = "User-friendly API key name") + private String name; + + @Schema(description = "Allowed IP address or IP range in CIDR notation (e.g., '192.168.1.100' or '192.168.1.0/24')") + private List allowedIp; + + @Schema(description = "API key creation timestamp") + private Instant createdAt; + + @Schema(description = "API key expiration timestamp (if applicable)") + private Instant expiresAt; + + @Schema(description = "Generated At") + private Instant generatedAt; +} diff --git a/backend/src/main/java/com/park/utmstack/service/dto/api_key/UtmApiKeyUpsertDto.java b/backend/src/main/java/com/park/utmstack/service/dto/api_key/UtmApiKeyUpsertDto.java new file mode 100644 index 000000000..518aa6e35 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/service/dto/api_key/UtmApiKeyUpsertDto.java @@ -0,0 +1,29 @@ +package com.utmstack.api.service.dto.apikey; + + +import com.utmstack.api.annotation.ValidIPOrCIDR; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.Instant; +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ApiKeyUpsertDTO { + @NotNull + @Schema(description = "API Key name", requiredMode = Schema.RequiredMode.REQUIRED) + private String name; + + @Schema(description = "Allowed IP address or IP range in CIDR notation (e.g., '192.168.1.100' or '192.168.1.0/24'). If null, no IP restrictions are applied.") + private List<@ValidIPOrCIDR String> allowedIp; + + @Schema(description = "Expiration timestamp of the API key") + private Instant expiresAt; +} diff --git a/backend/src/main/java/com/park/utmstack/web/rest/api-keys/UtmApiKeyController.java b/backend/src/main/java/com/park/utmstack/web/rest/api-keys/UtmApiKeyController.java new file mode 100644 index 000000000..27c48af2f --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/web/rest/api-keys/UtmApiKeyController.java @@ -0,0 +1,178 @@ +@RestController +@RequestMapping("/api/api-keys") +@PreAuthorize("hasAuthority(\"" + AuthoritiesConstants.USER + "\")") +@AllArgsConstructor +@Hidden +public class ApiKeyResource { + + private static final String CLASSNAME = "ApiKeyResource"; + private final Logger log = LoggerFactory.getLogger(ApiKeyResource.class); + + private final ApiKeyService apiKeyService; + private final UserService userService; + + private UUID getCurrentAccountId() throws CurrentUserLoginNotFoundException { + User user = userService.getUserWithAuthoritiesByLogin(SecurityUtils.currentUserLogin()); + return UUID.fromString(user.getAccountId()); + } + + @Operation(summary = "Create API key", + description = "Creates a new API key record using the provided settings. The plain text key is not generated at creation.") + @ApiResponses({ + @ApiResponse(responseCode = "201", description = "API key created successfully", + content = @Content(schema = @Schema(implementation = ApiKeyResponseDTO.class))), + @ApiResponse(responseCode = "409", description = "API key already exists", content = @Content), + @ApiResponse(responseCode = "500", description = "Internal server error", + content = @Content, headers = { + @Header(name = "X-App-Error", description = "Technical error details") + }) + }) + @PostMapping + public ResponseEntity createApiKey(@RequestBody ApiKeyUpsertDTO dto) { + final String ctx = CLASSNAME + ".createApiKey"; + try { + UUID accountId = getCurrentAccountId(); + ApiKeyResponseDTO responseDTO = apiKeyService.createApiKey(accountId, dto); + return ResponseEntity.status(HttpStatus.CREATED).body(responseDTO); + } catch (ApiKeyExistException e) { + return ResponseUtil.buildConflictResponse(e.getMessage()); + } catch (Exception e) { + String msg = ctx + ": " + e.getMessage(); + log.error(msg, e); + return ResponseUtil.buildInternalServerErrorResponse(msg); + } + } + + @Operation(summary = "Generate a new API key", + description = "Generates (or renews) a new random API key for the specified API key record. The plain text key is returned only once.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "API key generated successfully", + content = @Content(schema = @Schema(type = "string"))), + @ApiResponse(responseCode = "404", description = "API key not found", content = @Content), + @ApiResponse(responseCode = "500", description = "Internal server error", + content = @Content, headers = { + @Header(name = "X-App-Error", description = "Technical error details") + }) + }) + @PostMapping("/{id}/generate") + public ResponseEntity generateApiKey(@PathVariable("id") UUID apiKeyId) { + final String ctx = CLASSNAME + ".generateApiKey"; + try { + UUID accountId = getCurrentAccountId(); + String plainKey = apiKeyService.generateApiKey(accountId, apiKeyId); + return ResponseEntity.ok(plainKey); + } catch (ApiKeyNotFoundException e) { + return ResponseUtil.buildNotFoundResponse(e.getMessage()); + } catch (Exception e) { + String msg = ctx + ": " + e.getMessage(); + log.error(msg, e); + return ResponseUtil.buildInternalServerErrorResponse(msg); + } + } + + @Operation(summary = "Retrieve API key", + description = "Retrieves the API key details for the specified API key record.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "API key retrieved successfully", + content = @Content(schema = @Schema(implementation = ApiKeyResponseDTO.class))), + @ApiResponse(responseCode = "404", description = "API key not found", content = @Content), + @ApiResponse(responseCode = "500", description = "Internal server error", + content = @Content, headers = { + @Header(name = "X-App-Error", description = "Technical error details") + }) + }) + @GetMapping("/{id}") + public ResponseEntity getApiKey(@PathVariable("id") UUID apiKeyId) { + final String ctx = CLASSNAME + ".getApiKey"; + try { + UUID accountId = getCurrentAccountId(); + ApiKeyResponseDTO responseDTO = apiKeyService.getApiKey(accountId, apiKeyId); + return ResponseEntity.ok(responseDTO); + } catch (ApiKeyNotFoundException e) { + return ResponseUtil.buildNotFoundResponse(e.getMessage()); + } catch (Exception e) { + String msg = ctx + ": " + e.getMessage(); + log.error(msg, e); + return ResponseUtil.buildInternalServerErrorResponse(msg); + } + } + + @Operation(summary = "List API keys", + description = "Retrieves the API key list.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "API key retrieved successfully", + content = @Content(schema = @Schema(implementation = ApiKeyResponseDTO.class))), + @ApiResponse(responseCode = "404", description = "API key not found", content = @Content), + @ApiResponse(responseCode = "500", description = "Internal server error", + content = @Content, headers = { + @Header(name = "X-App-Error", description = "Technical error details") + }) + }) + @GetMapping("") + public ResponseEntity> listApiKeys(@ParameterObject Pageable pageable) { + final String ctx = CLASSNAME + ".listApiKeys"; + try { + UUID accountId = getCurrentAccountId(); + Page page = apiKeyService.listApiKeys(accountId, pageable); + HttpHeaders headers = PaginationUtil.generatePaginationHttpHeaders(ServletUriComponentsBuilder.fromCurrentRequest(), page); + return ResponseEntity.ok().headers(headers).body(page.getContent()); + } catch (Exception e) { + String msg = ctx + ": " + e.getMessage(); + log.error(msg, e); + return ResponseUtil.buildInternalServerErrorResponse(msg); + } + } + + @Operation(summary = "Update API key", + description = "Updates mutable fields (name, allowed IPs, expiration) for the specified API key record.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "API key updated successfully", + content = @Content(schema = @Schema(implementation = ApiKeyResponseDTO.class))), + @ApiResponse(responseCode = "404", description = "API key not found", content = @Content), + @ApiResponse(responseCode = "500", description = "Internal server error", + content = @Content, headers = { + @Header(name = "X-App-Error", description = "Technical error details") + }) + }) + @PutMapping("/{id}") + public ResponseEntity updateApiKey(@PathVariable("id") UUID apiKeyId, + @RequestBody ApiKeyUpsertDTO dto) { + final String ctx = CLASSNAME + ".updateApiKey"; + try { + UUID accountId = getCurrentAccountId(); + ApiKeyResponseDTO responseDTO = apiKeyService.updateApiKey(accountId, apiKeyId, dto); + return ResponseEntity.ok(responseDTO); + } catch (ApiKeyNotFoundException e) { + return ResponseUtil.buildNotFoundResponse(e.getMessage()); + } catch (Exception e) { + String msg = ctx + ": " + e.getMessage(); + log.error(msg, e); + return ResponseUtil.buildInternalServerErrorResponse(msg); + } + } + + @Operation(summary = "Delete API key", + description = "Deletes the specified API key record for the authenticated user.") + @ApiResponses({ + @ApiResponse(responseCode = "204", description = "API key deleted successfully", content = @Content), + @ApiResponse(responseCode = "404", description = "API key not found", content = @Content), + @ApiResponse(responseCode = "500", description = "Internal server error", + content = @Content, headers = { + @Header(name = "X-App-Error", description = "Technical error details") + }) + }) + @DeleteMapping("/{id}") + public ResponseEntity deleteApiKey(@PathVariable("id") UUID apiKeyId) { + final String ctx = CLASSNAME + ".deleteApiKey"; + try { + UUID accountId = getCurrentAccountId(); + apiKeyService.deleteApiKey(accountId, apiKeyId); + return ResponseEntity.noContent().build(); + } catch (ApiKeyNotFoundException e) { + return ResponseUtil.buildNotFoundResponse(e.getMessage()); + } catch (Exception e) { + String msg = ctx + ": " + e.getMessage(); + log.error(msg, e); + return ResponseUtil.buildInternalServerErrorResponse(msg); + } + } diff --git a/backend/src/main/resources/config/liquibase/changelog/20250917001_adding_api_keys.xml b/backend/src/main/resources/config/liquibase/changelog/20250917001_adding_api_keys.xml new file mode 100644 index 000000000..cb1c2cd75 --- /dev/null +++ b/backend/src/main/resources/config/liquibase/changelog/20250917001_adding_api_keys.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO api_keys (account_id, name, api_key, created_at) + SELECT account_id::uuid, 'DefaultApiKey', api_key, CURRENT_TIMESTAMP + FROM jhi_user + WHERE api_key IS NOT NULL; + + + + + + + diff --git a/backend/src/main/resources/config/liquibase/scripts/tables.sql b/backend/src/main/resources/config/liquibase/scripts/tables.sql index f50a695d1..629971010 100644 --- a/backend/src/main/resources/config/liquibase/scripts/tables.sql +++ b/backend/src/main/resources/config/liquibase/scripts/tables.sql @@ -12,6 +12,7 @@ DROP TABLE IF EXISTS public.utm_gvm_scan_result; DROP TABLE IF EXISTS public.utm_gvm_task; DROP TABLE IF EXISTS public.utm_module_modal; DROP TABLE IF EXISTS public.utm_system_restart; +DROP TABLE IF EXISTS public.utm_api_keys; CREATE TABLE IF NOT EXISTS public.jhi_authority ( @@ -830,3 +831,23 @@ CREATE TABLE IF NOT EXISTS public.utm_space_notification_control next_notification timestamp without time zone NOT NULL, CONSTRAINT utm_space_notification_control_pkey PRIMARY KEY (id) ); + +CREATE TABLE IF NOT EXISTS public.utm_api_keys +( + id uuid default uuid_generate_v4() not null + primary key, + account_id uuid not null, + name varchar(255) not null, + api_key varchar(255) not null, + allowed_ip text, + created_at timestamp not null, + expires_at timestamp, + generated_at timestamp +); + +alter table utm_api_keys + owner to postgres; + +create unique index uk_api_keys_api_key + on utm_api_keys (api_key); + From 5fa8b22ca7747306acc5d89422707bcdcf86ac8a Mon Sep 17 00:00:00 2001 From: AlexSanchez-bit Date: Fri, 17 Oct 2025 11:48:55 -0400 Subject: [PATCH 040/128] feat[backend](api_keys): added api keys --- .../{UtmApiKeyModel.java => ApiKey.java} | 0 .../service/api_key/ApiKeyService.java | 212 ++++++++++++++++++ ...esponseDto.java => ApiKeyResponseDTO.java} | 0 ...KeyUpsertDto.java => ApiKeyUpsertDTO.java} | 0 .../rest/api-keys/UtmApiKeyController.java | 1 + 5 files changed, 213 insertions(+) rename backend/src/main/java/com/park/utmstack/domain/api_keys/{UtmApiKeyModel.java => ApiKey.java} (100%) create mode 100644 backend/src/main/java/com/park/utmstack/service/api_key/ApiKeyService.java rename backend/src/main/java/com/park/utmstack/service/dto/api_key/{UtmApiKeyResponseDto.java => ApiKeyResponseDTO.java} (100%) rename backend/src/main/java/com/park/utmstack/service/dto/api_key/{UtmApiKeyUpsertDto.java => ApiKeyUpsertDTO.java} (100%) diff --git a/backend/src/main/java/com/park/utmstack/domain/api_keys/UtmApiKeyModel.java b/backend/src/main/java/com/park/utmstack/domain/api_keys/ApiKey.java similarity index 100% rename from backend/src/main/java/com/park/utmstack/domain/api_keys/UtmApiKeyModel.java rename to backend/src/main/java/com/park/utmstack/domain/api_keys/ApiKey.java diff --git a/backend/src/main/java/com/park/utmstack/service/api_key/ApiKeyService.java b/backend/src/main/java/com/park/utmstack/service/api_key/ApiKeyService.java new file mode 100644 index 000000000..775d37f7d --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/service/api_key/ApiKeyService.java @@ -0,0 +1,212 @@ +package com.utmstack.api.service; + +import com.utmstack.api.domain.User; +import com.utmstack.api.domain.api_key.ApiKey; +import com.utmstack.api.domain.api_key.ApiKeyUsageLog_; +import com.utmstack.api.domain.api_key.index.ApiKeyUsageLogIndexDocument; +import com.utmstack.api.domain.enumeration.NotificationMessageKeyEnum; +import com.utmstack.api.repository.elasticsearch.ApiKeyUsageLogRepository; +import com.utmstack.api.repository.jpa.ApiKeyRepository; +import com.utmstack.api.repository.jpa.UserRepository; +import com.utmstack.api.service.criteria.api_key.ApiKeyUsageLogCriteria; +import com.utmstack.api.service.dto.SearchHitsResponseDTO; +import com.utmstack.api.service.dto.api_key.ApiKeyResponseDTO; +import com.utmstack.api.service.dto.api_key.ApiKeyUpsertDTO; +import com.utmstack.api.service.exceptions.ApiKeyExistException; +import com.utmstack.api.service.exceptions.ApiKeyNotFoundException; +import com.utmstack.api.service.mapper.ApiKeyMapper; +import com.utmstack.api.service.user.UserNotificationService; +import lombok.AllArgsConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.elasticsearch.core.ElasticsearchOperations; +import org.springframework.data.elasticsearch.core.SearchHit; +import org.springframework.data.elasticsearch.core.SearchHits; +import org.springframework.data.elasticsearch.core.query.Criteria; +import org.springframework.data.elasticsearch.core.query.CriteriaQuery; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import java.security.SecureRandom; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Base64; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Collectors; + +@Service +@AllArgsConstructor +public class ApiKeyService { + private static final String CLASSNAME = "ApiKeyService"; + private final Logger log = LoggerFactory.getLogger(ApiKeyService.class); + private final ApiKeyRepository apiKeyRepository; + private final ApiKeyMapper apiKeyMapper; + private final ApiKeyUsageLogRepository apiUsageLogRepository; + private final ElasticsearchOperations elasticsearchOperations; + private final UserRepository userRepository; + private final UserNotificationService userNotificationService; + private final MailService mailService; + + + public ApiKeyResponseDTO createApiKey(UUID accountId, ApiKeyUpsertDTO dto) { + final String ctx = CLASSNAME + ".createApiKey"; + try { + apiKeyRepository.findByNameAndAccountId(dto.getName(), accountId) + .ifPresent(apiKey -> { + throw new ApiKeyExistException("Api key already exists"); + }); + var apiKey = api_key.builder() + .accountId(accountId) + .name(dto.getName()) + .expiresAt(dto.getExpiresAt()) + .allowedIp(String.join(",", dto.getAllowedIp())) + .createdAt(Instant.now()) + .apiKey(generateRandomKey()) + .build(); + return apiKeyMapper.toDto(apiKeyRepository.save(apiKey)); + } catch (Exception e) { + throw new RuntimeException(ctx + ": " + e.getMessage()); + } + } + + public String generateApiKey(UUID accountId, UUID apiKeyId) { + final String ctx = CLASSNAME + ".generateApiKey"; + try { + ApiKey apiKey = apiKeyRepository.findByIdAndAccountId(apiKeyId, accountId) + .orElseThrow(() -> new ApiKeyNotFoundException("API key not found")); + String plainKey = generateRandomKey(); + api_key.setApiKey(plainKey); + api_key.setGeneratedAt(Instant.now()); + apiKeyRepository.save(apiKey); + return plainKey; + } catch (Exception e) { + throw new RuntimeException(ctx + ": " + e.getMessage()); + } + } + + public ApiKeyResponseDTO updateApiKey(UUID accountId, UUID apiKeyId, ApiKeyUpsertDTO dto) { + final String ctx = CLASSNAME + ".updateApiKey"; + try { + ApiKey apiKey = apiKeyRepository.findByIdAndAccountId(apiKeyId, accountId) + .orElseThrow(() -> new ApiKeyNotFoundException("API key not found")); + api_key.setName(dto.getName()); + if (dto.getAllowedIp() != null) { + api_key.setAllowedIp(String.join(",", dto.getAllowedIp())); + } else { + api_key.setAllowedIp(null); + } + api_key.setExpiresAt(dto.getExpiresAt()); + ApiKey updated = apiKeyRepository.save(apiKey); + return apiKeyMapper.toDto(updated); + } catch (Exception e) { + throw new RuntimeException(ctx + ": " + e.getMessage()); + } + } + + public ApiKeyResponseDTO getApiKey(UUID accountId, UUID apiKeyId) { + final String ctx = CLASSNAME + ".getApiKey"; + try { + ApiKey apiKey = apiKeyRepository.findByIdAndAccountId(apiKeyId, accountId) + .orElseThrow(() -> new ApiKeyNotFoundException("API key not found")); + return apiKeyMapper.toDto(apiKey); + } catch (Exception e) { + throw new RuntimeException(ctx + ": " + e.getMessage()); + } + } + + public Page listApiKeys(UUID accountId, Pageable pageable) { + final String ctx = CLASSNAME + ".listApiKeys"; + try { + return apiKeyRepository.findByAccountId(accountId, pageable).map(apiKeyMapper::toDto); + } catch (Exception e) { + throw new RuntimeException(ctx + ": " + e.getMessage()); + } + } + + + public void deleteApiKey(UUID accountId, UUID apiKeyId) { + final String ctx = CLASSNAME + ".deleteApiKey"; + try { + ApiKey apiKey = apiKeyRepository.findByIdAndAccountId(apiKeyId, accountId) + .orElseThrow(() -> new ApiKeyNotFoundException("API key not found")); + apiKeyRepository.delete(apiKey); + } catch (Exception e) { + throw new RuntimeException(ctx + ": " + e.getMessage()); + } + } + + private String generateRandomKey() { + final String ctx = CLASSNAME + ".generateRandomKey"; + try { + SecureRandom random = new SecureRandom(); + byte[] keyBytes = new byte[32]; + random.nextBytes(keyBytes); + return Base64.getUrlEncoder().withoutPadding().encodeToString(keyBytes); + } catch (Exception e) { + throw new RuntimeException(ctx + ": " + e.getMessage()); + } + } + + @Async + public void logUsage(ApiKeyUsageLogIndexDocument apiKeyUsageLog) { + final String ctx = CLASSNAME + ".logUsage"; + try { + apiUsageLogRepository.save(apiKeyUsageLog); + } catch (Exception e) { + log.error(ctx + ": {}", e.getMessage()); + } + } + + public Optional findOneByApiKey(String apiKey) { + return apiKeyRepository.findOneByApiKey(apiKey); + } + + public SearchHitsResponseDTO getApiKeyUsageLogs(User user, + ApiKeyUsageLogCriteria criteria, + Pageable pageable) { + final String ctx = CLASSNAME + ".getApiKeyUsageLogs"; + try { + CriteriaQuery query = new CriteriaQuery(criteria != null ? criteria.toCriteriaQuery() : new Criteria(), pageable); + query.addCriteria(new Criteria(ApiKeyUsageLog_.accountId).is(user.getAccountId())); + SearchHits result = elasticsearchOperations.search(query, ApiKeyUsageLogIndexDocument.class); + return SearchHitsResponseDTO.builder() + .totalHits(result.getTotalHits()) + .items(result.stream() + .map(SearchHit::getContent) + .collect(Collectors.toList())) + .build(); + } catch (Exception e) { + throw new RuntimeException(ctx + ": " + e.getMessage()); + } + } + + @Scheduled(cron = "0 0 9 * * ?") + public void checkExpiringApiKeys() { + Instant fiveDaysFromNow = Instant.now().plus(5, ChronoUnit.DAYS); + Instant now = Instant.now(); + List expiringKeys = apiKeyRepository.findAllByExpiresAtAfterAndExpiresAtLessThanEqual(now, fiveDaysFromNow); + + if (!expiringKeys.isEmpty()) { + Map> expiringKeysByAccount = expiringKeys.stream() + .collect(Collectors.groupingBy(ApiKey::getAccountId)); + + expiringKeysByAccount.forEach((accountId, apiKeys) -> { + var principal = userRepository.findByAccountIdAndAccountOwnerIsTrue(accountId.toString()).orElse(null); + if (principal == null) { + return; + } + mailService.sendKeyExpirationEmail(principal, apiKeys); + + userNotificationService.createAndSendNotification(principal.getUuid(), + NotificationMessageKeyEnum.API_KEY_EXPIRATION, + Map.of("names", apiKeys.stream().map(ApiKey::getName).collect(Collectors.joining(",")))); + }); + } + } +} diff --git a/backend/src/main/java/com/park/utmstack/service/dto/api_key/UtmApiKeyResponseDto.java b/backend/src/main/java/com/park/utmstack/service/dto/api_key/ApiKeyResponseDTO.java similarity index 100% rename from backend/src/main/java/com/park/utmstack/service/dto/api_key/UtmApiKeyResponseDto.java rename to backend/src/main/java/com/park/utmstack/service/dto/api_key/ApiKeyResponseDTO.java diff --git a/backend/src/main/java/com/park/utmstack/service/dto/api_key/UtmApiKeyUpsertDto.java b/backend/src/main/java/com/park/utmstack/service/dto/api_key/ApiKeyUpsertDTO.java similarity index 100% rename from backend/src/main/java/com/park/utmstack/service/dto/api_key/UtmApiKeyUpsertDto.java rename to backend/src/main/java/com/park/utmstack/service/dto/api_key/ApiKeyUpsertDTO.java diff --git a/backend/src/main/java/com/park/utmstack/web/rest/api-keys/UtmApiKeyController.java b/backend/src/main/java/com/park/utmstack/web/rest/api-keys/UtmApiKeyController.java index 27c48af2f..253fabc76 100644 --- a/backend/src/main/java/com/park/utmstack/web/rest/api-keys/UtmApiKeyController.java +++ b/backend/src/main/java/com/park/utmstack/web/rest/api-keys/UtmApiKeyController.java @@ -176,3 +176,4 @@ public ResponseEntity deleteApiKey(@PathVariable("id") UUID apiKeyId) { return ResponseUtil.buildInternalServerErrorResponse(msg); } } +} From 86968a56cee7d8cb8de9791eb844aea13c3e583f Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Fri, 17 Oct 2025 11:02:50 -0500 Subject: [PATCH 041/128] feat(api_keys): create api_keys table with user_id and add foreign key constraint --- .../park/utmstack/domain/api_keys/ApiKey.java | 9 ++----- .../changelog/20250917001_adding_api_keys.xml | 25 ++++++++----------- .../resources/config/liquibase/master.xml | 2 ++ 3 files changed, 15 insertions(+), 21 deletions(-) diff --git a/backend/src/main/java/com/park/utmstack/domain/api_keys/ApiKey.java b/backend/src/main/java/com/park/utmstack/domain/api_keys/ApiKey.java index 8f532da04..8d0585a3d 100644 --- a/backend/src/main/java/com/park/utmstack/domain/api_keys/ApiKey.java +++ b/backend/src/main/java/com/park/utmstack/domain/api_keys/ApiKey.java @@ -1,16 +1,11 @@ package com.park.utmstack.domain.api_keys; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.Table; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import javax.persistence.*; import java.io.Serializable; import java.time.Instant; import java.util.UUID; @@ -29,7 +24,7 @@ public class ApiKey implements Serializable { private UUID id; @Column(nullable = false) - private UUID accountId; + private Long userId; @Column(nullable = false) private String name; diff --git a/backend/src/main/resources/config/liquibase/changelog/20250917001_adding_api_keys.xml b/backend/src/main/resources/config/liquibase/changelog/20250917001_adding_api_keys.xml index cb1c2cd75..2b996f83c 100644 --- a/backend/src/main/resources/config/liquibase/changelog/20250917001_adding_api_keys.xml +++ b/backend/src/main/resources/config/liquibase/changelog/20250917001_adding_api_keys.xml @@ -4,12 +4,12 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd"> - + - + @@ -24,20 +24,17 @@ + - - - - INSERT INTO api_keys (account_id, name, api_key, created_at) - SELECT account_id::uuid, 'DefaultApiKey', api_key, CURRENT_TIMESTAMP - FROM jhi_user - WHERE api_key IS NOT NULL; - - - - - + + diff --git a/backend/src/main/resources/config/liquibase/master.xml b/backend/src/main/resources/config/liquibase/master.xml index 5201db735..b7ab36816 100644 --- a/backend/src/main/resources/config/liquibase/master.xml +++ b/backend/src/main/resources/config/liquibase/master.xml @@ -253,5 +253,7 @@ + + From bb3a3ce149669b82444e408287acc7f61f9e1331 Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Fri, 17 Oct 2025 12:13:17 -0500 Subject: [PATCH 042/128] feat(api_keys): implement API key management with CRUD operations and validation --- .../com/park/utmstack/config/Constants.java | 1 + .../domain/api_keys/ApiKeyUsageLog.java | 40 ++++++ .../repository/api_key/ApiKeyRepository.java | 29 ++++ .../service/api_key/ApiKeyService.java | 127 +++++++----------- .../dto/api_key/ApiKeyResponseDTO.java | 2 +- .../service/dto/api_key/ApiKeyUpsertDTO.java | 7 +- .../utmstack/service/mapper/ApiKeyMapper.java | 31 +++++ .../util/exceptions/ApiKeyExistException.java | 7 + .../ApiKeyInvalidAccessException.java | 9 ++ .../exceptions/ApiKeyNotFoundException.java | 7 + .../validation/api_key/ValidIPOrCIDR.java | 23 ++++ .../api_key/ValidIPOrCIDRValidator.java | 37 +++++ .../ApiKeyResource.java} | 72 ++++++++++ ... => 20251017001_create_api_keys_table.xml} | 0 .../resources/config/liquibase/master.xml | 2 +- 15 files changed, 308 insertions(+), 86 deletions(-) create mode 100644 backend/src/main/java/com/park/utmstack/domain/api_keys/ApiKeyUsageLog.java create mode 100644 backend/src/main/java/com/park/utmstack/repository/api_key/ApiKeyRepository.java create mode 100644 backend/src/main/java/com/park/utmstack/service/mapper/ApiKeyMapper.java create mode 100644 backend/src/main/java/com/park/utmstack/util/exceptions/ApiKeyExistException.java create mode 100644 backend/src/main/java/com/park/utmstack/util/exceptions/ApiKeyInvalidAccessException.java create mode 100644 backend/src/main/java/com/park/utmstack/util/exceptions/ApiKeyNotFoundException.java create mode 100644 backend/src/main/java/com/park/utmstack/validation/api_key/ValidIPOrCIDR.java create mode 100644 backend/src/main/java/com/park/utmstack/validation/api_key/ValidIPOrCIDRValidator.java rename backend/src/main/java/com/park/utmstack/web/rest/{api-keys/UtmApiKeyController.java => api_key/ApiKeyResource.java} (70%) rename backend/src/main/resources/config/liquibase/changelog/{20250917001_adding_api_keys.xml => 20251017001_create_api_keys_table.xml} (100%) diff --git a/backend/src/main/java/com/park/utmstack/config/Constants.java b/backend/src/main/java/com/park/utmstack/config/Constants.java index 61d6c52a9..f69adf682 100644 --- a/backend/src/main/java/com/park/utmstack/config/Constants.java +++ b/backend/src/main/java/com/park/utmstack/config/Constants.java @@ -137,6 +137,7 @@ public final class Constants { // Defines the index pattern for querying Elasticsearch statistics indexes. // ---------------------------------------------------------------------------------- public static final String STATISTICS_INDEX_PATTERN = "v11-statistics-*"; + public static final String V11_API_ACCESS_LOGS = "v11-api-access-logs-*"; // Logging public static final String TRACE_ID_KEY = "traceId"; diff --git a/backend/src/main/java/com/park/utmstack/domain/api_keys/ApiKeyUsageLog.java b/backend/src/main/java/com/park/utmstack/domain/api_keys/ApiKeyUsageLog.java new file mode 100644 index 000000000..d097aa4fb --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/domain/api_keys/ApiKeyUsageLog.java @@ -0,0 +1,40 @@ +package com.park.utmstack.domain.api_keys; + + +import lombok.*; +import java.time.Instant; +import java.util.UUID; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class ApiKeyUsageLog { + + private UUID id; + + private UUID apiKeyId; + + private String apiKeyName; + + private Long userId; + + private Instant timestamp; + + private String endpoint; + + private String address; + + private String errorMessage; + + private String queryParams; + + private String payload; + + private String userAgent; + + private String httpMethod; + + private Integer statusCode; +} diff --git a/backend/src/main/java/com/park/utmstack/repository/api_key/ApiKeyRepository.java b/backend/src/main/java/com/park/utmstack/repository/api_key/ApiKeyRepository.java new file mode 100644 index 000000000..e76001064 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/repository/api_key/ApiKeyRepository.java @@ -0,0 +1,29 @@ +package com.park.utmstack.repository.api_key; + +import com.park.utmstack.domain.api_keys.ApiKey; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import javax.validation.constraints.NotNull; +import java.time.Instant; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +@Repository +public interface ApiKeyRepository extends JpaRepository { + + Optional findByIdAndUserId(UUID id, Long userId); + + Page findByUserId(Long userId, Pageable pageable); + + @Cacheable(cacheNames = "apikey", key = "#root.args[0]") + Optional findOneByApiKey(@NotNull String apiKey); + + Optional findByNameAndUserId(@NotNull String name, Long userId); + + List findAllByExpiresAtAfterAndExpiresAtLessThanEqual(Instant now, Instant fiveDaysFromNow); +} diff --git a/backend/src/main/java/com/park/utmstack/service/api_key/ApiKeyService.java b/backend/src/main/java/com/park/utmstack/service/api_key/ApiKeyService.java index 775d37f7d..cb402e356 100644 --- a/backend/src/main/java/com/park/utmstack/service/api_key/ApiKeyService.java +++ b/backend/src/main/java/com/park/utmstack/service/api_key/ApiKeyService.java @@ -1,88 +1,73 @@ -package com.utmstack.api.service; - -import com.utmstack.api.domain.User; -import com.utmstack.api.domain.api_key.ApiKey; -import com.utmstack.api.domain.api_key.ApiKeyUsageLog_; -import com.utmstack.api.domain.api_key.index.ApiKeyUsageLogIndexDocument; -import com.utmstack.api.domain.enumeration.NotificationMessageKeyEnum; -import com.utmstack.api.repository.elasticsearch.ApiKeyUsageLogRepository; -import com.utmstack.api.repository.jpa.ApiKeyRepository; -import com.utmstack.api.repository.jpa.UserRepository; -import com.utmstack.api.service.criteria.api_key.ApiKeyUsageLogCriteria; -import com.utmstack.api.service.dto.SearchHitsResponseDTO; -import com.utmstack.api.service.dto.api_key.ApiKeyResponseDTO; -import com.utmstack.api.service.dto.api_key.ApiKeyUpsertDTO; -import com.utmstack.api.service.exceptions.ApiKeyExistException; -import com.utmstack.api.service.exceptions.ApiKeyNotFoundException; -import com.utmstack.api.service.mapper.ApiKeyMapper; -import com.utmstack.api.service.user.UserNotificationService; +package com.park.utmstack.service.api_key; + +import com.park.utmstack.domain.api_keys.ApiKey; +import com.park.utmstack.domain.api_keys.ApiKeyUsageLog; +import com.park.utmstack.repository.api_key.ApiKeyRepository; +import com.park.utmstack.service.dto.api_key.ApiKeyResponseDTO; +import com.park.utmstack.service.dto.api_key.ApiKeyUpsertDTO; +import com.park.utmstack.service.elasticsearch.OpensearchClientBuilder; +import com.park.utmstack.service.mapper.ApiKeyMapper; +import com.park.utmstack.util.exceptions.ApiKeyExistException; +import com.park.utmstack.util.exceptions.ApiKeyNotFoundException; import lombok.AllArgsConstructor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; -import org.springframework.data.elasticsearch.core.ElasticsearchOperations; -import org.springframework.data.elasticsearch.core.SearchHit; -import org.springframework.data.elasticsearch.core.SearchHits; -import org.springframework.data.elasticsearch.core.query.Criteria; -import org.springframework.data.elasticsearch.core.query.CriteriaQuery; + import org.springframework.scheduling.annotation.Async; -import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import java.security.SecureRandom; import java.time.Instant; -import java.time.temporal.ChronoUnit; import java.util.Base64; -import java.util.List; -import java.util.Map; import java.util.Optional; import java.util.UUID; -import java.util.stream.Collectors; + +import static com.park.utmstack.config.Constants.V11_API_ACCESS_LOGS; @Service @AllArgsConstructor public class ApiKeyService { + private static final String CLASSNAME = "ApiKeyService"; private final Logger log = LoggerFactory.getLogger(ApiKeyService.class); private final ApiKeyRepository apiKeyRepository; private final ApiKeyMapper apiKeyMapper; - private final ApiKeyUsageLogRepository apiUsageLogRepository; - private final ElasticsearchOperations elasticsearchOperations; - private final UserRepository userRepository; - private final UserNotificationService userNotificationService; - private final MailService mailService; + private final OpensearchClientBuilder client; - public ApiKeyResponseDTO createApiKey(UUID accountId, ApiKeyUpsertDTO dto) { + public ApiKeyResponseDTO createApiKey(Long userId, ApiKeyUpsertDTO dto) { final String ctx = CLASSNAME + ".createApiKey"; try { - apiKeyRepository.findByNameAndAccountId(dto.getName(), accountId) + apiKeyRepository.findByNameAndUserId(dto.getName(), userId) .ifPresent(apiKey -> { throw new ApiKeyExistException("Api key already exists"); }); - var apiKey = api_key.builder() - .accountId(accountId) + + var apiKey = ApiKey.builder() + .userId(userId) .name(dto.getName()) .expiresAt(dto.getExpiresAt()) .allowedIp(String.join(",", dto.getAllowedIp())) .createdAt(Instant.now()) .apiKey(generateRandomKey()) .build(); + return apiKeyMapper.toDto(apiKeyRepository.save(apiKey)); } catch (Exception e) { throw new RuntimeException(ctx + ": " + e.getMessage()); } } - public String generateApiKey(UUID accountId, UUID apiKeyId) { + public String generateApiKey(Long userId, UUID apiKeyId) { final String ctx = CLASSNAME + ".generateApiKey"; try { - ApiKey apiKey = apiKeyRepository.findByIdAndAccountId(apiKeyId, accountId) + ApiKey apiKey = apiKeyRepository.findByIdAndUserId(apiKeyId, userId) .orElseThrow(() -> new ApiKeyNotFoundException("API key not found")); String plainKey = generateRandomKey(); - api_key.setApiKey(plainKey); - api_key.setGeneratedAt(Instant.now()); + apiKey.setApiKey(plainKey); + apiKey.setGeneratedAt(Instant.now()); apiKeyRepository.save(apiKey); return plainKey; } catch (Exception e) { @@ -90,18 +75,18 @@ public String generateApiKey(UUID accountId, UUID apiKeyId) { } } - public ApiKeyResponseDTO updateApiKey(UUID accountId, UUID apiKeyId, ApiKeyUpsertDTO dto) { + public ApiKeyResponseDTO updateApiKey(Long userId, UUID apiKeyId, ApiKeyUpsertDTO dto) { final String ctx = CLASSNAME + ".updateApiKey"; try { - ApiKey apiKey = apiKeyRepository.findByIdAndAccountId(apiKeyId, accountId) + ApiKey apiKey = apiKeyRepository.findByIdAndUserId(apiKeyId, userId) .orElseThrow(() -> new ApiKeyNotFoundException("API key not found")); - api_key.setName(dto.getName()); + apiKey.setName(dto.getName()); if (dto.getAllowedIp() != null) { - api_key.setAllowedIp(String.join(",", dto.getAllowedIp())); + apiKey.setAllowedIp(String.join(",", dto.getAllowedIp())); } else { - api_key.setAllowedIp(null); + apiKey.setAllowedIp(null); } - api_key.setExpiresAt(dto.getExpiresAt()); + apiKey.setExpiresAt(dto.getExpiresAt()); ApiKey updated = apiKeyRepository.save(apiKey); return apiKeyMapper.toDto(updated); } catch (Exception e) { @@ -109,10 +94,10 @@ public ApiKeyResponseDTO updateApiKey(UUID accountId, UUID apiKeyId, ApiKeyUpser } } - public ApiKeyResponseDTO getApiKey(UUID accountId, UUID apiKeyId) { + public ApiKeyResponseDTO getApiKey(Long userId, UUID apiKeyId) { final String ctx = CLASSNAME + ".getApiKey"; try { - ApiKey apiKey = apiKeyRepository.findByIdAndAccountId(apiKeyId, accountId) + ApiKey apiKey = apiKeyRepository.findByIdAndUserId(apiKeyId, userId) .orElseThrow(() -> new ApiKeyNotFoundException("API key not found")); return apiKeyMapper.toDto(apiKey); } catch (Exception e) { @@ -120,20 +105,20 @@ public ApiKeyResponseDTO getApiKey(UUID accountId, UUID apiKeyId) { } } - public Page listApiKeys(UUID accountId, Pageable pageable) { + public Page listApiKeys(Long userId, Pageable pageable) { final String ctx = CLASSNAME + ".listApiKeys"; try { - return apiKeyRepository.findByAccountId(accountId, pageable).map(apiKeyMapper::toDto); + return apiKeyRepository.findByUserId(userId, pageable).map(apiKeyMapper::toDto); } catch (Exception e) { throw new RuntimeException(ctx + ": " + e.getMessage()); } } - public void deleteApiKey(UUID accountId, UUID apiKeyId) { + public void deleteApiKey(Long userId, UUID apiKeyId) { final String ctx = CLASSNAME + ".deleteApiKey"; try { - ApiKey apiKey = apiKeyRepository.findByIdAndAccountId(apiKeyId, accountId) + ApiKey apiKey = apiKeyRepository.findByIdAndUserId(apiKeyId, userId) .orElseThrow(() -> new ApiKeyNotFoundException("API key not found")); apiKeyRepository.delete(apiKey); } catch (Exception e) { @@ -154,10 +139,10 @@ private String generateRandomKey() { } @Async - public void logUsage(ApiKeyUsageLogIndexDocument apiKeyUsageLog) { + public void logUsage(ApiKeyUsageLog apiKeyUsageLog) { final String ctx = CLASSNAME + ".logUsage"; try { - apiUsageLogRepository.save(apiKeyUsageLog); + client.getClient().index(V11_API_ACCESS_LOGS, apiKeyUsageLog); } catch (Exception e) { log.error(ctx + ": {}", e.getMessage()); } @@ -167,46 +152,28 @@ public Optional findOneByApiKey(String apiKey) { return apiKeyRepository.findOneByApiKey(apiKey); } - public SearchHitsResponseDTO getApiKeyUsageLogs(User user, - ApiKeyUsageLogCriteria criteria, - Pageable pageable) { - final String ctx = CLASSNAME + ".getApiKeyUsageLogs"; - try { - CriteriaQuery query = new CriteriaQuery(criteria != null ? criteria.toCriteriaQuery() : new Criteria(), pageable); - query.addCriteria(new Criteria(ApiKeyUsageLog_.accountId).is(user.getAccountId())); - SearchHits result = elasticsearchOperations.search(query, ApiKeyUsageLogIndexDocument.class); - return SearchHitsResponseDTO.builder() - .totalHits(result.getTotalHits()) - .items(result.stream() - .map(SearchHit::getContent) - .collect(Collectors.toList())) - .build(); - } catch (Exception e) { - throw new RuntimeException(ctx + ": " + e.getMessage()); - } - } - @Scheduled(cron = "0 0 9 * * ?") + /*@Scheduled(cron = "0 0 9 * * ?") public void checkExpiringApiKeys() { Instant fiveDaysFromNow = Instant.now().plus(5, ChronoUnit.DAYS); Instant now = Instant.now(); List expiringKeys = apiKeyRepository.findAllByExpiresAtAfterAndExpiresAtLessThanEqual(now, fiveDaysFromNow); if (!expiringKeys.isEmpty()) { - Map> expiringKeysByAccount = expiringKeys.stream() - .collect(Collectors.groupingBy(ApiKey::getAccountId)); + Map> expiringKeysByAccount = expiringKeys.stream() + .collect(Collectors.groupingBy(ApiKey::getUserId)); - expiringKeysByAccount.forEach((accountId, apiKeys) -> { - var principal = userRepository.findByAccountIdAndAccountOwnerIsTrue(accountId.toString()).orElse(null); + expiringKeysByAccount.forEach((userId, apiKeys) -> { + var principal = userRepository.findByuserIdAndAccountOwnerIsTrue(userId.toString()).orElse(null); if (principal == null) { return; } mailService.sendKeyExpirationEmail(principal, apiKeys); userNotificationService.createAndSendNotification(principal.getUuid(), - NotificationMessageKeyEnum.API_KEY_EXPIRATION, + NotificationMessageKeyEnum.apiKey_EXPIRATION, Map.of("names", apiKeys.stream().map(ApiKey::getName).collect(Collectors.joining(",")))); }); } - } + }*/ } diff --git a/backend/src/main/java/com/park/utmstack/service/dto/api_key/ApiKeyResponseDTO.java b/backend/src/main/java/com/park/utmstack/service/dto/api_key/ApiKeyResponseDTO.java index 232e77f56..024f7282c 100644 --- a/backend/src/main/java/com/park/utmstack/service/dto/api_key/ApiKeyResponseDTO.java +++ b/backend/src/main/java/com/park/utmstack/service/dto/api_key/ApiKeyResponseDTO.java @@ -1,4 +1,4 @@ -package com.utmstack.api.service.dto.apikey; +package com.park.utmstack.service.dto.api_key; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; diff --git a/backend/src/main/java/com/park/utmstack/service/dto/api_key/ApiKeyUpsertDTO.java b/backend/src/main/java/com/park/utmstack/service/dto/api_key/ApiKeyUpsertDTO.java index 518aa6e35..6345f2032 100644 --- a/backend/src/main/java/com/park/utmstack/service/dto/api_key/ApiKeyUpsertDTO.java +++ b/backend/src/main/java/com/park/utmstack/service/dto/api_key/ApiKeyUpsertDTO.java @@ -1,14 +1,13 @@ -package com.utmstack.api.service.dto.apikey; +package com.park.utmstack.service.dto.api_key; - -import com.utmstack.api.annotation.ValidIPOrCIDR; +import com.park.utmstack.validation.api_key.ValidIPOrCIDR; import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotNull; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import javax.validation.constraints.NotNull; import java.time.Instant; import java.util.List; diff --git a/backend/src/main/java/com/park/utmstack/service/mapper/ApiKeyMapper.java b/backend/src/main/java/com/park/utmstack/service/mapper/ApiKeyMapper.java new file mode 100644 index 000000000..0439c563b --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/service/mapper/ApiKeyMapper.java @@ -0,0 +1,31 @@ +package com.park.utmstack.service.mapper; + +import com.park.utmstack.domain.api_keys.ApiKey; +import com.park.utmstack.service.dto.api_key.ApiKeyResponseDTO; +import org.mapstruct.Mapper; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Optional; +import java.util.stream.Collectors; + +@Mapper(componentModel = "spring") +public class ApiKeyMapper { + + public ApiKeyResponseDTO toDto(ApiKey apiKey){ + return ApiKeyResponseDTO.builder() + .id(apiKey.getId()) + .name(apiKey.getName()) + .createdAt(apiKey.getCreatedAt()) + .expiresAt(apiKey.getExpiresAt()) + .allowedIp( + Optional.ofNullable(apiKey.getAllowedIp()) + .map(s -> Arrays.stream(s.split(",")) + .map(String::trim) + .filter(str -> !str.isEmpty()) + .collect(Collectors.toList())) + .orElse(Collections.emptyList()) + ) + .build(); + } +} diff --git a/backend/src/main/java/com/park/utmstack/util/exceptions/ApiKeyExistException.java b/backend/src/main/java/com/park/utmstack/util/exceptions/ApiKeyExistException.java new file mode 100644 index 000000000..20577f508 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/util/exceptions/ApiKeyExistException.java @@ -0,0 +1,7 @@ +package com.park.utmstack.util.exceptions; + +public class ApiKeyExistException extends RuntimeException { + public ApiKeyExistException(String message) { + super(message); + } +} diff --git a/backend/src/main/java/com/park/utmstack/util/exceptions/ApiKeyInvalidAccessException.java b/backend/src/main/java/com/park/utmstack/util/exceptions/ApiKeyInvalidAccessException.java new file mode 100644 index 000000000..c5a13ead4 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/util/exceptions/ApiKeyInvalidAccessException.java @@ -0,0 +1,9 @@ +package com.park.utmstack.util.exceptions; + +import org.springframework.security.core.AuthenticationException; + +public class ApiKeyInvalidAccessException extends AuthenticationException { + public ApiKeyInvalidAccessException(String message) { + super(message); + } +} diff --git a/backend/src/main/java/com/park/utmstack/util/exceptions/ApiKeyNotFoundException.java b/backend/src/main/java/com/park/utmstack/util/exceptions/ApiKeyNotFoundException.java new file mode 100644 index 000000000..173d8f442 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/util/exceptions/ApiKeyNotFoundException.java @@ -0,0 +1,7 @@ +package com.park.utmstack.util.exceptions; + +public class ApiKeyNotFoundException extends RuntimeException { + public ApiKeyNotFoundException(String message) { + super(message); + } +} diff --git a/backend/src/main/java/com/park/utmstack/validation/api_key/ValidIPOrCIDR.java b/backend/src/main/java/com/park/utmstack/validation/api_key/ValidIPOrCIDR.java new file mode 100644 index 000000000..55dfe9593 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/validation/api_key/ValidIPOrCIDR.java @@ -0,0 +1,23 @@ +package com.park.utmstack.validation.api_key; + + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Documented +@Constraint(validatedBy = ValidIPOrCIDRValidator.class) +@Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE, TYPE_USE}) +@Retention(RUNTIME) +public @interface ValidIPOrCIDR { + String message() default "Invalid IP address or CIDR notation"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/backend/src/main/java/com/park/utmstack/validation/api_key/ValidIPOrCIDRValidator.java b/backend/src/main/java/com/park/utmstack/validation/api_key/ValidIPOrCIDRValidator.java new file mode 100644 index 000000000..8324094f8 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/validation/api_key/ValidIPOrCIDRValidator.java @@ -0,0 +1,37 @@ +package com.park.utmstack.validation.api_key; + + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import java.util.regex.Pattern; + +public class ValidIPOrCIDRValidator implements ConstraintValidator { + + private static final Pattern IPV4_PATTERN = Pattern.compile( + "^(?:(?:25[0-5]|2[0-4]\\d|[01]?\\d?\\d)\\.){3}(?:25[0-5]|2[0-4]\\d|[01]?\\d?\\d)$" + ); + + private static final Pattern IPV4_CIDR_PATTERN = Pattern.compile( + "^(?:(?:25[0-5]|2[0-4]\\d|[01]?\\d?\\d)\\.){3}(?:25[0-5]|2[0-4]\\d|[01]?\\d?\\d)/(\\d|[1-2]\\d|3[0-2])$" + ); + private static final Pattern IPV6_PATTERN = Pattern.compile( + "^(?:[\\da-fA-F]{1,4}:){7}[\\da-fA-F]{1,4}$" + ); + + private static final Pattern IPV6_CIDR_PATTERN = Pattern.compile( + "^(?:[\\da-fA-F]{1,4}:){7}[\\da-fA-F]{1,4}/(\\d|[1-9]\\d|1[01]\\d|12[0-8])$" + ); + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + // Allow null or empty values; use @NotNull/@NotEmpty to enforce non-null if needed. + if (value == null || value.trim().isEmpty()) { + return true; + } + String trimmed = value.trim(); + if (IPV4_PATTERN.matcher(trimmed).matches() || IPV4_CIDR_PATTERN.matcher(trimmed).matches()) { + return true; + } + return IPV6_PATTERN.matcher(trimmed).matches() || IPV6_CIDR_PATTERN.matcher(trimmed).matches(); + } +} diff --git a/backend/src/main/java/com/park/utmstack/web/rest/api-keys/UtmApiKeyController.java b/backend/src/main/java/com/park/utmstack/web/rest/api_key/ApiKeyResource.java similarity index 70% rename from backend/src/main/java/com/park/utmstack/web/rest/api-keys/UtmApiKeyController.java rename to backend/src/main/java/com/park/utmstack/web/rest/api_key/ApiKeyResource.java index 253fabc76..c5f17588d 100644 --- a/backend/src/main/java/com/park/utmstack/web/rest/api-keys/UtmApiKeyController.java +++ b/backend/src/main/java/com/park/utmstack/web/rest/api_key/ApiKeyResource.java @@ -1,3 +1,49 @@ +package com.park.utmstack.web.rest.api_key; + +import com.insecureweb.api.domain.User; +import com.insecureweb.api.domain.apikey.index.ApiKeyUsageLogIndexDocument; +import com.insecureweb.api.security.AuthoritiesConstants; +import com.insecureweb.api.security.SecurityUtils; +import com.insecureweb.api.service.ApiKeyService; +import com.insecureweb.api.service.criteria.apikey.ApiKeyUsageLogCriteria; +import com.insecureweb.api.service.dto.SearchHitsResponseDTO; +import com.insecureweb.api.service.dto.apikey.ApiKeyResponseDTO; +import com.insecureweb.api.service.dto.apikey.ApiKeyUpsertDTO; +import com.insecureweb.api.service.exceptions.*; +import com.insecureweb.api.service.user.UserService; +import com.insecureweb.api.util.ResponseUtil; +import com.insecureweb.api.web.rest.restutil.ResponseSearchHitsUtil; +import com.park.utmstack.domain.application_events.enums.ApplicationEventType; +import com.park.utmstack.domain.chart_builder.types.query.FilterType; +import com.park.utmstack.domain.chart_builder.types.query.OperatorType; +import com.park.utmstack.util.ResponseUtil; +import com.park.utmstack.util.UtilPagination; +import io.swagger.v3.oas.annotations.Hidden; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.headers.Header; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import lombok.AllArgsConstructor; +import org.opensearch.client.opensearch.core.SearchResponse; +import org.opensearch.client.opensearch.core.search.Hit; +import org.opensearch.client.opensearch.core.search.HitsMetadata; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springdoc.core.annotations.ParameterObject; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; +import tech.jhipster.web.util.PaginationUtil; + +import java.util.*; + @RestController @RequestMapping("/api/api-keys") @PreAuthorize("hasAuthority(\"" + AuthoritiesConstants.USER + "\")") @@ -176,4 +222,30 @@ public ResponseEntity deleteApiKey(@PathVariable("id") UUID apiKeyId) { return ResponseUtil.buildInternalServerErrorResponse(msg); } } + + @GetMapping("/usage") + public ResponseEntity> search(@RequestBody(required = false) List filters, + @RequestParam Integer top, @RequestParam String indexPattern, + @RequestParam(required = false, defaultValue = "false") boolean includeChildren, + Pageable pageable) { + final String ctx = CLASSNAME + ".search"; + try { + SearchResponse searchResponse = elasticsearchService.search(filters, top, indexPattern, + pageable, Map.class); + + if (Objects.isNull(searchResponse) || Objects.isNull(searchResponse.hits()) || searchResponse.hits().total().value() == 0) + return ResponseEntity.ok(Collections.emptyList()); + + HitsMetadata hits = searchResponse.hits(); + HttpHeaders headers = UtilPagination.generatePaginationHttpHeaders(Math.min(hits.total().value(), top), + pageable.getPageNumber(), pageable.getPageSize(), "/api/elasticsearch/search"); + + return ResponseEntity.ok().headers(headers).body(results); + } catch (Exception e) { + String msg = ctx + ": " + e.getMessage(); + log.error(msg); + applicationEventService.createEvent(msg, ApplicationEventType.ERROR); + return com.park.utmstack.util.ResponseUtil.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); + } + } } diff --git a/backend/src/main/resources/config/liquibase/changelog/20250917001_adding_api_keys.xml b/backend/src/main/resources/config/liquibase/changelog/20251017001_create_api_keys_table.xml similarity index 100% rename from backend/src/main/resources/config/liquibase/changelog/20250917001_adding_api_keys.xml rename to backend/src/main/resources/config/liquibase/changelog/20251017001_create_api_keys_table.xml diff --git a/backend/src/main/resources/config/liquibase/master.xml b/backend/src/main/resources/config/liquibase/master.xml index b7ab36816..8735ef517 100644 --- a/backend/src/main/resources/config/liquibase/master.xml +++ b/backend/src/main/resources/config/liquibase/master.xml @@ -253,7 +253,7 @@ - + From 158462ca5e186fa75b21a464da65b392328dce8a Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Fri, 17 Oct 2025 13:31:26 -0500 Subject: [PATCH 043/128] refactor(api_keys): simplify API key management by removing user ID dependency in service methods --- .../advice/GlobalExceptionHandler.java | 15 +- .../park/utmstack/service/UserService.java | 3 +- .../service/api_key/ApiKeyService.java | 20 ++- .../web/rest/api_key/ApiKeyResource.java | 159 +++++------------- 4 files changed, 65 insertions(+), 132 deletions(-) diff --git a/backend/src/main/java/com/park/utmstack/advice/GlobalExceptionHandler.java b/backend/src/main/java/com/park/utmstack/advice/GlobalExceptionHandler.java index 937bdde86..b84c2ea14 100644 --- a/backend/src/main/java/com/park/utmstack/advice/GlobalExceptionHandler.java +++ b/backend/src/main/java/com/park/utmstack/advice/GlobalExceptionHandler.java @@ -4,10 +4,7 @@ import com.park.utmstack.security.TooMuchLoginAttemptsException; import com.park.utmstack.service.application_events.ApplicationEventService; import com.park.utmstack.util.ResponseUtil; -import com.park.utmstack.util.exceptions.IncidentAlertConflictException; -import com.park.utmstack.util.exceptions.NoAlertsProvidedException; -import com.park.utmstack.util.exceptions.TfaVerificationException; -import com.park.utmstack.util.exceptions.TooManyRequestsException; +import com.park.utmstack.util.exceptions.*; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; @@ -41,8 +38,9 @@ public ResponseEntity handleTooManyLoginAttempts(TooMuchLoginAttemptsExceptio return ResponseUtil.buildLockedResponse(e.getMessage()); } - @ExceptionHandler(NoSuchElementException.class) - public ResponseEntity handleNotFound(NoSuchElementException e, HttpServletRequest request) { + @ExceptionHandler({NoSuchElementException.class, + ApiKeyNotFoundException.class}) + public ResponseEntity handleNotFound(Exception e, HttpServletRequest request) { return ResponseUtil.buildNotFoundResponse(e.getMessage()); } @@ -56,8 +54,9 @@ public ResponseEntity handleNoAlertsProvided(Exception e, HttpServletRequest return ResponseUtil.buildErrorResponse(HttpStatus.BAD_REQUEST, e.getMessage()); } - @ExceptionHandler(IncidentAlertConflictException.class) - public ResponseEntity handleConflict(IncidentAlertConflictException e, HttpServletRequest request) { + @ExceptionHandler({IncidentAlertConflictException.class, + ApiKeyExistException.class}) + public ResponseEntity handleConflict(Exception e, HttpServletRequest request) { return ResponseUtil.buildErrorResponse(HttpStatus.CONFLICT, e.getMessage()); } diff --git a/backend/src/main/java/com/park/utmstack/service/UserService.java b/backend/src/main/java/com/park/utmstack/service/UserService.java index ed1a6379b..5dacde192 100644 --- a/backend/src/main/java/com/park/utmstack/service/UserService.java +++ b/backend/src/main/java/com/park/utmstack/service/UserService.java @@ -301,6 +301,7 @@ public List getAuthorities() { public User getCurrentUserLogin() { String userLogin = SecurityUtils.getCurrentUserLogin().orElseThrow(() -> new CurrentUserLoginNotFoundException("No current user login was found")); - return userRepository.findOneWithAuthoritiesByLogin(userLogin).orElseThrow(() -> new CurrentUserLoginNotFoundException(String.format("No user with login %1$s was found", userLogin))); + return userRepository.findOneWithAuthoritiesByLogin(userLogin) + .orElseThrow(() -> new CurrentUserLoginNotFoundException(String.format("No user with login %1$s was found", userLogin))); } } diff --git a/backend/src/main/java/com/park/utmstack/service/api_key/ApiKeyService.java b/backend/src/main/java/com/park/utmstack/service/api_key/ApiKeyService.java index cb402e356..71310d26d 100644 --- a/backend/src/main/java/com/park/utmstack/service/api_key/ApiKeyService.java +++ b/backend/src/main/java/com/park/utmstack/service/api_key/ApiKeyService.java @@ -3,6 +3,7 @@ import com.park.utmstack.domain.api_keys.ApiKey; import com.park.utmstack.domain.api_keys.ApiKeyUsageLog; import com.park.utmstack.repository.api_key.ApiKeyRepository; +import com.park.utmstack.service.UserService; import com.park.utmstack.service.dto.api_key.ApiKeyResponseDTO; import com.park.utmstack.service.dto.api_key.ApiKeyUpsertDTO; import com.park.utmstack.service.elasticsearch.OpensearchClientBuilder; @@ -35,11 +36,13 @@ public class ApiKeyService { private final ApiKeyRepository apiKeyRepository; private final ApiKeyMapper apiKeyMapper; private final OpensearchClientBuilder client; + private final UserService userService; - public ApiKeyResponseDTO createApiKey(Long userId, ApiKeyUpsertDTO dto) { + public ApiKeyResponseDTO createApiKey(ApiKeyUpsertDTO dto) { final String ctx = CLASSNAME + ".createApiKey"; try { + Long userId = userService.getCurrentUserLogin().getId(); apiKeyRepository.findByNameAndUserId(dto.getName(), userId) .ifPresent(apiKey -> { throw new ApiKeyExistException("Api key already exists"); @@ -60,9 +63,10 @@ public ApiKeyResponseDTO createApiKey(Long userId, ApiKeyUpsertDTO dto) { } } - public String generateApiKey(Long userId, UUID apiKeyId) { + public String generateApiKey(UUID apiKeyId) { final String ctx = CLASSNAME + ".generateApiKey"; try { + Long userId = userService.getCurrentUserLogin().getId(); ApiKey apiKey = apiKeyRepository.findByIdAndUserId(apiKeyId, userId) .orElseThrow(() -> new ApiKeyNotFoundException("API key not found")); String plainKey = generateRandomKey(); @@ -75,9 +79,10 @@ public String generateApiKey(Long userId, UUID apiKeyId) { } } - public ApiKeyResponseDTO updateApiKey(Long userId, UUID apiKeyId, ApiKeyUpsertDTO dto) { + public ApiKeyResponseDTO updateApiKey(UUID apiKeyId, ApiKeyUpsertDTO dto) { final String ctx = CLASSNAME + ".updateApiKey"; try { + Long userId = userService.getCurrentUserLogin().getId(); ApiKey apiKey = apiKeyRepository.findByIdAndUserId(apiKeyId, userId) .orElseThrow(() -> new ApiKeyNotFoundException("API key not found")); apiKey.setName(dto.getName()); @@ -94,9 +99,10 @@ public ApiKeyResponseDTO updateApiKey(Long userId, UUID apiKeyId, ApiKeyUpsertDT } } - public ApiKeyResponseDTO getApiKey(Long userId, UUID apiKeyId) { + public ApiKeyResponseDTO getApiKey(UUID apiKeyId) { final String ctx = CLASSNAME + ".getApiKey"; try { + Long userId = userService.getCurrentUserLogin().getId(); ApiKey apiKey = apiKeyRepository.findByIdAndUserId(apiKeyId, userId) .orElseThrow(() -> new ApiKeyNotFoundException("API key not found")); return apiKeyMapper.toDto(apiKey); @@ -105,9 +111,10 @@ public ApiKeyResponseDTO getApiKey(Long userId, UUID apiKeyId) { } } - public Page listApiKeys(Long userId, Pageable pageable) { + public Page listApiKeys(Pageable pageable) { final String ctx = CLASSNAME + ".listApiKeys"; try { + Long userId = userService.getCurrentUserLogin().getId(); return apiKeyRepository.findByUserId(userId, pageable).map(apiKeyMapper::toDto); } catch (Exception e) { throw new RuntimeException(ctx + ": " + e.getMessage()); @@ -115,9 +122,10 @@ public Page listApiKeys(Long userId, Pageable pageable) { } - public void deleteApiKey(Long userId, UUID apiKeyId) { + public void deleteApiKey(UUID apiKeyId) { final String ctx = CLASSNAME + ".deleteApiKey"; try { + Long userId = userService.getCurrentUserLogin().getId(); ApiKey apiKey = apiKeyRepository.findByIdAndUserId(apiKeyId, userId) .orElseThrow(() -> new ApiKeyNotFoundException("API key not found")); apiKeyRepository.delete(apiKey); diff --git a/backend/src/main/java/com/park/utmstack/web/rest/api_key/ApiKeyResource.java b/backend/src/main/java/com/park/utmstack/web/rest/api_key/ApiKeyResource.java index c5f17588d..9f012f765 100644 --- a/backend/src/main/java/com/park/utmstack/web/rest/api_key/ApiKeyResource.java +++ b/backend/src/main/java/com/park/utmstack/web/rest/api_key/ApiKeyResource.java @@ -1,23 +1,14 @@ package com.park.utmstack.web.rest.api_key; -import com.insecureweb.api.domain.User; -import com.insecureweb.api.domain.apikey.index.ApiKeyUsageLogIndexDocument; -import com.insecureweb.api.security.AuthoritiesConstants; -import com.insecureweb.api.security.SecurityUtils; -import com.insecureweb.api.service.ApiKeyService; -import com.insecureweb.api.service.criteria.apikey.ApiKeyUsageLogCriteria; -import com.insecureweb.api.service.dto.SearchHitsResponseDTO; -import com.insecureweb.api.service.dto.apikey.ApiKeyResponseDTO; -import com.insecureweb.api.service.dto.apikey.ApiKeyUpsertDTO; -import com.insecureweb.api.service.exceptions.*; -import com.insecureweb.api.service.user.UserService; -import com.insecureweb.api.util.ResponseUtil; -import com.insecureweb.api.web.rest.restutil.ResponseSearchHitsUtil; -import com.park.utmstack.domain.application_events.enums.ApplicationEventType; + import com.park.utmstack.domain.chart_builder.types.query.FilterType; -import com.park.utmstack.domain.chart_builder.types.query.OperatorType; -import com.park.utmstack.util.ResponseUtil; +import com.park.utmstack.security.AuthoritiesConstants; +import com.park.utmstack.service.api_key.ApiKeyService; +import com.park.utmstack.service.dto.api_key.ApiKeyResponseDTO; +import com.park.utmstack.service.dto.api_key.ApiKeyUpsertDTO; +import com.park.utmstack.service.elasticsearch.ElasticsearchService; import com.park.utmstack.util.UtilPagination; +import com.park.utmstack.web.rest.elasticsearch.ElasticsearchResource; import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.headers.Header; @@ -26,12 +17,11 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.opensearch.client.opensearch.core.SearchResponse; import org.opensearch.client.opensearch.core.search.Hit; import org.opensearch.client.opensearch.core.search.HitsMetadata; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springdoc.core.annotations.ParameterObject; +import org.springdoc.api.annotations.ParameterObject; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.http.HttpHeaders; @@ -43,24 +33,18 @@ import tech.jhipster.web.util.PaginationUtil; import java.util.*; +import java.util.stream.Collectors; @RestController @RequestMapping("/api/api-keys") @PreAuthorize("hasAuthority(\"" + AuthoritiesConstants.USER + "\")") +@Slf4j @AllArgsConstructor @Hidden public class ApiKeyResource { - private static final String CLASSNAME = "ApiKeyResource"; - private final Logger log = LoggerFactory.getLogger(ApiKeyResource.class); - private final ApiKeyService apiKeyService; - private final UserService userService; - - private UUID getCurrentAccountId() throws CurrentUserLoginNotFoundException { - User user = userService.getUserWithAuthoritiesByLogin(SecurityUtils.currentUserLogin()); - return UUID.fromString(user.getAccountId()); - } + private final ElasticsearchService elasticsearchService; @Operation(summary = "Create API key", description = "Creates a new API key record using the provided settings. The plain text key is not generated at creation.") @@ -75,18 +59,8 @@ private UUID getCurrentAccountId() throws CurrentUserLoginNotFoundException { }) @PostMapping public ResponseEntity createApiKey(@RequestBody ApiKeyUpsertDTO dto) { - final String ctx = CLASSNAME + ".createApiKey"; - try { - UUID accountId = getCurrentAccountId(); - ApiKeyResponseDTO responseDTO = apiKeyService.createApiKey(accountId, dto); + ApiKeyResponseDTO responseDTO = apiKeyService.createApiKey(dto); return ResponseEntity.status(HttpStatus.CREATED).body(responseDTO); - } catch (ApiKeyExistException e) { - return ResponseUtil.buildConflictResponse(e.getMessage()); - } catch (Exception e) { - String msg = ctx + ": " + e.getMessage(); - log.error(msg, e); - return ResponseUtil.buildInternalServerErrorResponse(msg); - } } @Operation(summary = "Generate a new API key", @@ -102,18 +76,8 @@ public ResponseEntity createApiKey(@RequestBody ApiKeyUpsertD }) @PostMapping("/{id}/generate") public ResponseEntity generateApiKey(@PathVariable("id") UUID apiKeyId) { - final String ctx = CLASSNAME + ".generateApiKey"; - try { - UUID accountId = getCurrentAccountId(); - String plainKey = apiKeyService.generateApiKey(accountId, apiKeyId); - return ResponseEntity.ok(plainKey); - } catch (ApiKeyNotFoundException e) { - return ResponseUtil.buildNotFoundResponse(e.getMessage()); - } catch (Exception e) { - String msg = ctx + ": " + e.getMessage(); - log.error(msg, e); - return ResponseUtil.buildInternalServerErrorResponse(msg); - } + String plainKey = apiKeyService.generateApiKey(apiKeyId); + return ResponseEntity.ok(plainKey); } @Operation(summary = "Retrieve API key", @@ -129,18 +93,8 @@ public ResponseEntity generateApiKey(@PathVariable("id") UUID apiKeyId) }) @GetMapping("/{id}") public ResponseEntity getApiKey(@PathVariable("id") UUID apiKeyId) { - final String ctx = CLASSNAME + ".getApiKey"; - try { - UUID accountId = getCurrentAccountId(); - ApiKeyResponseDTO responseDTO = apiKeyService.getApiKey(accountId, apiKeyId); - return ResponseEntity.ok(responseDTO); - } catch (ApiKeyNotFoundException e) { - return ResponseUtil.buildNotFoundResponse(e.getMessage()); - } catch (Exception e) { - String msg = ctx + ": " + e.getMessage(); - log.error(msg, e); - return ResponseUtil.buildInternalServerErrorResponse(msg); - } + ApiKeyResponseDTO responseDTO = apiKeyService.getApiKey(apiKeyId); + return ResponseEntity.ok(responseDTO); } @Operation(summary = "List API keys", @@ -156,17 +110,10 @@ public ResponseEntity getApiKey(@PathVariable("id") UUID apiK }) @GetMapping("") public ResponseEntity> listApiKeys(@ParameterObject Pageable pageable) { - final String ctx = CLASSNAME + ".listApiKeys"; - try { - UUID accountId = getCurrentAccountId(); - Page page = apiKeyService.listApiKeys(accountId, pageable); - HttpHeaders headers = PaginationUtil.generatePaginationHttpHeaders(ServletUriComponentsBuilder.fromCurrentRequest(), page); - return ResponseEntity.ok().headers(headers).body(page.getContent()); - } catch (Exception e) { - String msg = ctx + ": " + e.getMessage(); - log.error(msg, e); - return ResponseUtil.buildInternalServerErrorResponse(msg); - } + Page page = apiKeyService.listApiKeys(pageable); + HttpHeaders headers = PaginationUtil.generatePaginationHttpHeaders(ServletUriComponentsBuilder.fromCurrentRequest(), page); + + return ResponseEntity.ok().headers(headers).body(page.getContent()); } @Operation(summary = "Update API key", @@ -183,18 +130,10 @@ public ResponseEntity> listApiKeys(@ParameterObject Page @PutMapping("/{id}") public ResponseEntity updateApiKey(@PathVariable("id") UUID apiKeyId, @RequestBody ApiKeyUpsertDTO dto) { - final String ctx = CLASSNAME + ".updateApiKey"; - try { - UUID accountId = getCurrentAccountId(); - ApiKeyResponseDTO responseDTO = apiKeyService.updateApiKey(accountId, apiKeyId, dto); - return ResponseEntity.ok(responseDTO); - } catch (ApiKeyNotFoundException e) { - return ResponseUtil.buildNotFoundResponse(e.getMessage()); - } catch (Exception e) { - String msg = ctx + ": " + e.getMessage(); - log.error(msg, e); - return ResponseUtil.buildInternalServerErrorResponse(msg); - } + + ApiKeyResponseDTO responseDTO = apiKeyService.updateApiKey(apiKeyId, dto); + return ResponseEntity.ok(responseDTO); + } @Operation(summary = "Delete API key", @@ -209,18 +148,9 @@ public ResponseEntity updateApiKey(@PathVariable("id") UUID a }) @DeleteMapping("/{id}") public ResponseEntity deleteApiKey(@PathVariable("id") UUID apiKeyId) { - final String ctx = CLASSNAME + ".deleteApiKey"; - try { - UUID accountId = getCurrentAccountId(); - apiKeyService.deleteApiKey(accountId, apiKeyId); - return ResponseEntity.noContent().build(); - } catch (ApiKeyNotFoundException e) { - return ResponseUtil.buildNotFoundResponse(e.getMessage()); - } catch (Exception e) { - String msg = ctx + ": " + e.getMessage(); - log.error(msg, e); - return ResponseUtil.buildInternalServerErrorResponse(msg); - } + + apiKeyService.deleteApiKey(apiKeyId); + return ResponseEntity.noContent().build(); } @GetMapping("/usage") @@ -228,24 +158,19 @@ public ResponseEntity> search(@RequestBody(required = false) List searchResponse = elasticsearchService.search(filters, top, indexPattern, - pageable, Map.class); - - if (Objects.isNull(searchResponse) || Objects.isNull(searchResponse.hits()) || searchResponse.hits().total().value() == 0) - return ResponseEntity.ok(Collections.emptyList()); - - HitsMetadata hits = searchResponse.hits(); - HttpHeaders headers = UtilPagination.generatePaginationHttpHeaders(Math.min(hits.total().value(), top), - pageable.getPageNumber(), pageable.getPageSize(), "/api/elasticsearch/search"); - - return ResponseEntity.ok().headers(headers).body(results); - } catch (Exception e) { - String msg = ctx + ": " + e.getMessage(); - log.error(msg); - applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return com.park.utmstack.util.ResponseUtil.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); - } + + SearchResponse searchResponse = elasticsearchService.search(filters, top, indexPattern, + pageable, Map.class); + + if (Objects.isNull(searchResponse) || Objects.isNull(searchResponse.hits()) || searchResponse.hits().total().value() == 0) + return ResponseEntity.ok(Collections.emptyList()); + + HitsMetadata hits = searchResponse.hits(); + HttpHeaders headers = UtilPagination.generatePaginationHttpHeaders(Math.min(hits.total().value(), top), + pageable.getPageNumber(), pageable.getPageSize(), "/api/elasticsearch/search"); + + return ResponseEntity.ok().headers(headers).body(hits.hits().stream() + .map(Hit::source).collect(Collectors.toList())); + } } From 00ea73224bc5f9980c82b37638429bf6059a1315 Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Sun, 19 Oct 2025 10:07:52 -0500 Subject: [PATCH 044/128] feat(api_keys): implement API key filtering and usage logging for enhanced security --- backend/pom.xml | 6 + .../com/park/utmstack/config/Constants.java | 5 + .../domain/api_keys/ApiKeyUsageLog.java | 53 +++--- .../enums/ApplicationEventType.java | 6 +- .../api_key/ApiKeyUsageLoggingService.java | 138 ++++++++++++++ .../security/api_key/ApiKeyFilter.java | 176 ++++++++++++++++++ .../ApplicationEventService.java | 2 +- 7 files changed, 362 insertions(+), 24 deletions(-) create mode 100644 backend/src/main/java/com/park/utmstack/loggin/api_key/ApiKeyUsageLoggingService.java create mode 100644 backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java diff --git a/backend/pom.xml b/backend/pom.xml index 325396715..2109f1376 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -376,6 +376,12 @@ 3.0.5 + + commons-net + commons-net + 3.9.0 + + diff --git a/backend/src/main/java/com/park/utmstack/config/Constants.java b/backend/src/main/java/com/park/utmstack/config/Constants.java index f69adf682..d12b6b06a 100644 --- a/backend/src/main/java/com/park/utmstack/config/Constants.java +++ b/backend/src/main/java/com/park/utmstack/config/Constants.java @@ -2,7 +2,9 @@ import com.park.utmstack.domain.index_pattern.enums.SystemIndexPattern; +import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; public final class Constants { @@ -157,6 +159,9 @@ public final class Constants { public static final String CONF_TYPE_PASSWORD = "password"; public static final String CONF_TYPE_FILE = "file"; + public static final String API_KEY_HEADER = "api-key"; + public static final List API_ENDPOINT_IGNORE = Collections.emptyList(); + private Constants() { } } diff --git a/backend/src/main/java/com/park/utmstack/domain/api_keys/ApiKeyUsageLog.java b/backend/src/main/java/com/park/utmstack/domain/api_keys/ApiKeyUsageLog.java index d097aa4fb..54e0c8d15 100644 --- a/backend/src/main/java/com/park/utmstack/domain/api_keys/ApiKeyUsageLog.java +++ b/backend/src/main/java/com/park/utmstack/domain/api_keys/ApiKeyUsageLog.java @@ -1,40 +1,49 @@ package com.park.utmstack.domain.api_keys; - +import com.park.utmstack.service.dto.auditable.AuditableDTO; import lombok.*; -import java.time.Instant; -import java.util.UUID; +import java.util.HashMap; +import java.util.Map; + +@Builder @Getter @Setter @NoArgsConstructor @AllArgsConstructor -@Builder -public class ApiKeyUsageLog { - - private UUID id; - - private UUID apiKeyId; +public class ApiKeyUsageLog implements AuditableDTO { + private String id; + private String apiKeyId; private String apiKeyName; - - private Long userId; - - private Instant timestamp; - + private String userId; + private String timestamp; private String endpoint; - private String address; - private String errorMessage; - private String queryParams; - private String payload; - private String userAgent; - private String httpMethod; - - private Integer statusCode; + private String statusCode; + + @Override + public Map toAuditMap() { + Map map = new HashMap<>(); + + map.put("id", id); + map.put("api_key_id", apiKeyId); + map.put("api_key_name", apiKeyName); + map.put("user_id", userId); + map.put("timestamp", timestamp != null ? timestamp : null); + map.put("endpoint", endpoint); + map.put("address", address); + map.put("error_message", errorMessage); + map.put("query_params", queryParams); + map.put("user_agent", userAgent); + map.put("http_method", httpMethod); + map.put("status_code", statusCode); + + return map; + } } diff --git a/backend/src/main/java/com/park/utmstack/domain/application_events/enums/ApplicationEventType.java b/backend/src/main/java/com/park/utmstack/domain/application_events/enums/ApplicationEventType.java index 9df92c191..eca45ec19 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_events/enums/ApplicationEventType.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_events/enums/ApplicationEventType.java @@ -42,5 +42,9 @@ public enum ApplicationEventType { ERROR, WARNING, INFO, - MODULE_ACTIVATION_ATTEMPT, MODULE_ACTIVATION_SUCCESS, UNDEFINED + MODULE_ACTIVATION_ATTEMPT, + MODULE_ACTIVATION_SUCCESS, + API_KEY_ACCESS_SUCCESS, + API_KEY_ACCESS_FAILURE, + UNDEFINED } diff --git a/backend/src/main/java/com/park/utmstack/loggin/api_key/ApiKeyUsageLoggingService.java b/backend/src/main/java/com/park/utmstack/loggin/api_key/ApiKeyUsageLoggingService.java new file mode 100644 index 000000000..e0969e2c0 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/loggin/api_key/ApiKeyUsageLoggingService.java @@ -0,0 +1,138 @@ +package com.park.utmstack.loggin.api_key; + +import com.park.utmstack.domain.api_keys.ApiKey; +import com.park.utmstack.domain.api_keys.ApiKeyUsageLog; +import com.park.utmstack.domain.application_events.enums.ApplicationEventType; +import com.park.utmstack.service.api_key.ApiKeyService; +import com.park.utmstack.service.application_events.ApplicationEventService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.web.util.ContentCachingRequestWrapper; +import org.springframework.web.util.ContentCachingResponseWrapper; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.UUID; + +@Service +@Slf4j +@RequiredArgsConstructor +public class ApiKeyUsageLoggingService { + + private final ApiKeyService apiKeyService; + private final ApplicationEventService applicationEventService; + private static final String LOG_USAGE_FLAG = "LOG_USAGE_DONE"; + + public void logUsage(HttpServletRequest request, + HttpServletResponse response, + ApiKey apiKey, + String ipAddress, + String message) { + + if (Boolean.TRUE.equals(request.getAttribute(LOG_USAGE_FLAG))) { + return; + } + + try { + String payload = extractPayload(request); + String errorText = extractErrorText(response); + int status = safeStatus(response); + + ApiKeyUsageLog usage = buildUsageLog(apiKey, ipAddress, request, status, errorText, payload, message); + + apiKeyService.logUsage(usage); + + ApplicationEventType eventType = (status >= 400) + ? ApplicationEventType.API_KEY_ACCESS_FAILURE + : ApplicationEventType.API_KEY_ACCESS_SUCCESS; + + String eventMessage = (status >= 400) + ? "API key access failure" + : "API key access"; + + applicationEventService.createEvent(eventMessage, eventType, usage.toAuditMap()); + + } catch (Exception e) { + log.error("Error while logging API key usage: {}", e.getMessage(), e); + } finally { + request.setAttribute(LOG_USAGE_FLAG, Boolean.TRUE); + } + } + + private int safeStatus(HttpServletResponse response) { + try { + return response.getStatus(); + } catch (Exception e) { + return 0; + } + } + + private String extractPayload(HttpServletRequest request) { + if (request instanceof ContentCachingRequestWrapper wrapper) { + byte[] buf = wrapper.getContentAsByteArray(); + if (buf.length > 0) { + return extractBody(buf); + } + } + return null; + } + + private String extractErrorText(HttpServletResponse response) { + if (response instanceof ContentCachingResponseWrapper wrapper) { + byte[] buf = wrapper.getContentAsByteArray(); + if (buf.length > 0) { + return extractBody(buf); + } + } + return null; + } + + private String extractBody(byte[] buf) { + return buf.length > 0 ? new String(buf, StandardCharsets.UTF_8) : null; + } + + private ApiKeyUsageLog buildUsageLog(ApiKey apiKey, + String ipAddress, + HttpServletRequest request, + int status, + String errorText, + String payload, + String message) { + + String id = UUID.randomUUID().toString(); + String apiKeyId = apiKey != null && apiKey.getId() != null ? apiKey.getId().toString() : null; + String apiKeyName = apiKey != null ? apiKey.getName() : null; + String userId = apiKey != null && apiKey.getUserId() != null ? apiKey.getUserId().toString() : null; + String timestamp = Instant.now().toString(); + String endpoint = request != null ? request.getRequestURI() : null; + String queryParams = request != null ? request.getQueryString() : null; + String userAgent = request != null ? request.getHeader("User-Agent") : null; + String httpMethod = request != null ? request.getMethod() : null; + String statusCode = String.valueOf(status); + + String safePayload = null; + if (payload != null) { + int PAYLOAD_MAX_LENGTH = 2000; + safePayload = payload.length() > PAYLOAD_MAX_LENGTH ? payload.substring(0, PAYLOAD_MAX_LENGTH) : payload; + } + + return ApiKeyUsageLog.builder() + .id(id) + .apiKeyId(apiKeyId) + .apiKeyName(apiKeyName) + .userId(userId) + .timestamp(timestamp) + .endpoint(endpoint) + .address(ipAddress) + .errorMessage(errorText) + .queryParams(queryParams) + .payload(safePayload) + .userAgent(userAgent) + .httpMethod(httpMethod) + .statusCode(statusCode) + .build(); + } +} diff --git a/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java b/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java new file mode 100644 index 000000000..f629da7fc --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java @@ -0,0 +1,176 @@ +package com.park.utmstack.security.api_key; + + +import com.park.utmstack.config.Constants; +import com.park.utmstack.domain.api_keys.ApiKey; +import com.park.utmstack.loggin.api_key.ApiKeyUsageLoggingService; +import com.park.utmstack.repository.UserRepository; +import com.park.utmstack.service.api_key.ApiKeyService; +import com.park.utmstack.service.application_events.ApplicationEventService; +import com.park.utmstack.util.exceptions.ApiKeyInvalidAccessException; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.net.util.SubnetUtils; +import org.springframework.core.annotation.Order; +import org.springframework.lang.NonNull; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.util.ContentCachingRequestWrapper; +import org.springframework.web.util.ContentCachingResponseWrapper; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.time.Instant; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.regex.Pattern; + +import static com.park.utmstack.config.Constants.API_ENDPOINT_IGNORE; + +@Order(1) +@AllArgsConstructor +@Slf4j +@Component +public class ApiKeyFilter extends OncePerRequestFilter { + + private static final String LOG_USAGE_FLAG = "LOG_USAGE_DONE"; + private static final Pattern CIDR_PATTERN = Pattern.compile( + "^((25[0-5]|2[0-4]\\d|[01]?\\d\\d?)(\\.(?!$)|$)){4}/(\\d|[1-2]\\d|3[0-2])$" + ); + + private final UserRepository userRepository; + private final ApiKeyService apiKeyService; + private final ConcurrentMap invalidApiKeyBlackList = new ConcurrentHashMap<>(); + private final ConcurrentMap cidrCache = new ConcurrentHashMap<>(); + private final ApplicationEventService applicationEventService; + private final ApiKeyUsageLoggingService apiKeyUsageLoggingService; + + @Override + protected void doFilterInternal(@NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain) throws ServletException, IOException { + + if (API_ENDPOINT_IGNORE.contains(request.getRequestURI())) { + filterChain.doFilter(request, response); + return; + } + + if (request.getAttribute(LOG_USAGE_FLAG) != null) { + filterChain.doFilter(request, response); + return; + } + + String apiKey = request.getHeader(Constants.API_KEY_HEADER); + + if (!StringUtils.hasText(apiKey)) { + filterChain.doFilter(request, response); + return; + } + + String ipAddress = request.getRemoteAddr(); + var key = getApiKey(apiKey); + + var wrappedRequest = new ContentCachingRequestWrapper(request); + var wrappedResponse = new ContentCachingResponseWrapper(response); + + UsernamePasswordAuthenticationToken authentication; + + try { + authentication = getAuthentication(key, ipAddress); + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(wrappedRequest)); + SecurityContextHolder.getContext().setAuthentication(authentication); + + } catch (ApiKeyInvalidAccessException e) { + apiKeyUsageLoggingService.logUsage(wrappedRequest, response, key, ipAddress, e.getMessage()); + throw e; + } + + filterChain.doFilter(wrappedRequest, wrappedResponse); + wrappedResponse.copyBodyToResponse(); + + apiKeyUsageLoggingService.logUsage(wrappedRequest, response, key, ipAddress, null); + } + + private ApiKey getApiKey(String apiKey) { + if (invalidApiKeyBlackList.containsKey(apiKey)) { + throw new ApiKeyInvalidAccessException("Invalid API key"); + } + return apiKeyService.findOneByApiKey(apiKey) + .orElseThrow(() -> { + invalidApiKeyBlackList.put(apiKey, Boolean.TRUE); + return new ApiKeyInvalidAccessException("Invalid API key"); + }); + } + + public UsernamePasswordAuthenticationToken getAuthentication(ApiKey apiKey, String remoteIpAddress) { + try { + + if (!allowAccessToRemoteIp(apiKey.getAllowedIp(), remoteIpAddress)) { + throw new ApiKeyInvalidAccessException("Invalid IP address, if you recognize this IP address, add to allowed ip list"); + } + + if (apiKey.getExpiresAt() != null && !apiKey.getExpiresAt().isAfter(Instant.now())) { + throw new ApiKeyInvalidAccessException("API key expired at " + apiKey.getExpiresAt()); + } + + var userEntity = userRepository + .findById(apiKey.getUserId()) + .orElseThrow(() -> new ApiKeyInvalidAccessException("User not found for api key")); + + if (!userEntity.getActivated()) { + throw new ApiKeyInvalidAccessException("User not activated"); + } + + List authorities = userEntity.getAuthorities().stream() + .map(auth -> new SimpleGrantedAuthority(auth.getName())) + .toList(); + + User principal = new User(userEntity.getLogin(), "", authorities); + + return new UsernamePasswordAuthenticationToken(principal, apiKey.getApiKey(), authorities); + + } catch (Exception e) { + throw new ApiKeyInvalidAccessException(e.getMessage()); + } + } + + public boolean allowAccessToRemoteIp(String allowedIpList, String remoteIp) { + if (allowedIpList == null || allowedIpList.trim().isEmpty()) { + return true; + } + String[] whitelistIps = allowedIpList.split(","); + for (String ip : whitelistIps) { + String allowed = ip.trim(); + if (allowed.isEmpty()) { + continue; + } + if (CIDR_PATTERN.matcher(allowed).matches()) { + try { + SubnetUtils subnetUtils = cidrCache.computeIfAbsent(allowed, key -> { + SubnetUtils su = new SubnetUtils(key); + su.setInclusiveHostCount(true); + return su; + }); + if (subnetUtils.getInfo().isInRange(remoteIp)) { + return true; + } + } catch (IllegalArgumentException e) { + log.error("Invalid CIDR notation: {}", allowed); + } + } else if (allowed.equals(remoteIp)) { + return true; + } + } + return false; + } +} diff --git a/backend/src/main/java/com/park/utmstack/service/application_events/ApplicationEventService.java b/backend/src/main/java/com/park/utmstack/service/application_events/ApplicationEventService.java index 149e387c3..450f60d37 100644 --- a/backend/src/main/java/com/park/utmstack/service/application_events/ApplicationEventService.java +++ b/backend/src/main/java/com/park/utmstack/service/application_events/ApplicationEventService.java @@ -40,7 +40,7 @@ public void createEvent(String message, ApplicationEventType type) { .message(message).timestamp(Instant.now().toString()) .source(ApplicationEventSource.PANEL.name()).type(type.name()) .build(); - /*client.getClient().index(".utmstack-logs", applicationEvent);*/ + client.getClient().index(".utmstack-logs", applicationEvent); } catch (Exception e) { log.error(ctx + ": {}", e.getMessage()); } From 270d9f16a664e27d87334149a5c4e439cdb7da54 Mon Sep 17 00:00:00 2001 From: AlexSanchez-bit Date: Fri, 17 Oct 2025 17:04:44 -0400 Subject: [PATCH 045/128] feat[frontend](api_key): added api key list/creation components --- .../api-keys/api-keys.component.html | 53 +++++++++ .../api-keys/api-keys.component.scss | 0 .../api-keys/api-keys.component.ts | 53 +++++++++ .../api-key-modal.component.html | 109 ++++++++++++++++++ .../api-key-modal.component.scss | 1 + .../api-key-modal/api-key-modal.component.ts | 68 +++++++++++ .../api-keys/shared/models/ApiKeyResponse.ts | 8 ++ .../api-keys/shared/models/ApiKeyUpsert.ts | 5 + .../shared/service/api-keys.service.ts | 108 +++++++++++++++++ .../app-management/app-management.module.ts | 5 + .../connection-key.component.html | 20 +++- 11 files changed, 429 insertions(+), 1 deletion(-) create mode 100644 frontend/src/app/app-management/api-keys/api-keys.component.html create mode 100644 frontend/src/app/app-management/api-keys/api-keys.component.scss create mode 100644 frontend/src/app/app-management/api-keys/api-keys.component.ts create mode 100644 frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.html create mode 100644 frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.scss create mode 100644 frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts create mode 100644 frontend/src/app/app-management/api-keys/shared/models/ApiKeyResponse.ts create mode 100644 frontend/src/app/app-management/api-keys/shared/models/ApiKeyUpsert.ts create mode 100644 frontend/src/app/app-management/api-keys/shared/service/api-keys.service.ts diff --git a/frontend/src/app/app-management/api-keys/api-keys.component.html b/frontend/src/app/app-management/api-keys/api-keys.component.html new file mode 100644 index 000000000..677d5cde5 --- /dev/null +++ b/frontend/src/app/app-management/api-keys/api-keys.component.html @@ -0,0 +1,53 @@ +
+
+
+
+

API Keys

+

+ The API key is a simple encrypted string that identifies you in the application. +

+
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
NAMEALLOWED IPsCREATED ATEXPIRES ATACTIONS
{{ key.name }}{{ key.allowedIp?.join(', ') || '—' }}{{ key.createdAt | date:'M/d/yy, h:mm a' }}{{ key.expiresAt ? (key.expiresAt | date:'M/d/yy, h:mm a') : '—' }} + + + +
+
+ + + Loading... + No API Keys found. + +
+
diff --git a/frontend/src/app/app-management/api-keys/api-keys.component.scss b/frontend/src/app/app-management/api-keys/api-keys.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/app/app-management/api-keys/api-keys.component.ts b/frontend/src/app/app-management/api-keys/api-keys.component.ts new file mode 100644 index 000000000..e55e0600e --- /dev/null +++ b/frontend/src/app/app-management/api-keys/api-keys.component.ts @@ -0,0 +1,53 @@ +import { Component, OnInit } from '@angular/core'; +import { ApiKeysService } from './shared/service/api-keys.service'; +import { ApiKeyResponse } from './shared/models/ApiKeyResponse'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { ApiKeyModalComponent } from './shared/components/api-key-modal/api-key-modal.component'; + +@Component({ + selector: 'app-api-keys', + templateUrl: './api-keys.component.html', + styleUrls: ['./api-keys.component.scss'] +}) +export class ApiKeysComponent implements OnInit { + apiKeys: ApiKeyResponse[] = []; + loading = false; + + constructor( + private apiKeyService: ApiKeysService, + private modalService: NgbModal + ) {} + + ngOnInit(): void { + this.loadKeys(); + } + + loadKeys(): void { + this.loading = true; + this.apiKeyService.list().subscribe({ + next: (res) => { + this.apiKeys = res.body || []; + this.loading = false; + }, + error: () => (this.loading = false) + }); + } + + openCreateModal(): void { + const modalRef = this.modalService.open(ApiKeyModalComponent, { centered: true, size: 'lg' }); + modalRef.result.then((result) => { + if (result === 'created') this.loadKeys(); + }).catch(() => {}); + } + + deleteKey(id: string): void { + if (!confirm('Are you sure you want to delete this API key?')) return; + this.apiKeyService.delete(id).subscribe(() => this.loadKeys()); + } + + regenerateKey(id: string): void { + this.apiKeyService.generate(id).subscribe((res) => { + alert('New API Key: ' + res.body); + }); + } +} diff --git a/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.html b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.html new file mode 100644 index 000000000..28ae3a6a6 --- /dev/null +++ b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.html @@ -0,0 +1,109 @@ + + + + + + diff --git a/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.scss b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.scss new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.scss @@ -0,0 +1 @@ + diff --git a/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts new file mode 100644 index 000000000..c2944f8d1 --- /dev/null +++ b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts @@ -0,0 +1,68 @@ +import { Component, OnInit } from '@angular/core'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { ApiKeysService } from '../../service/api-keys.service'; +import { FormBuilder, FormGroup, Validators, FormArray } from '@angular/forms'; + +@Component({ + selector: 'app-api-key-modal', + templateUrl: './api-key-modal.component.html', + styleUrls: ['./api-key-modal.component.scss'] +}) +export class ApiKeyModalComponent implements OnInit { + apiKeyForm: FormGroup; + ipInput = ''; + loading = false; + errorMsg = ''; + + constructor( + public activeModal: NgbActiveModal, + private apiKeyService: ApiKeysService, + private fb: FormBuilder + ) { + this.apiKeyForm = this.fb.group({ + name: ['', Validators.required], + allowedIp: this.fb.array([]), + expiresAt: [null] + }); + } + + ngOnInit(): void {} + + get allowedIp(): FormArray { + return this.apiKeyForm.get('allowedIp') as FormArray; + } + + addIp(): void { + const ip = this.ipInput.trim(); + if (ip) { + this.allowedIp.push(this.fb.control(ip)); + this.ipInput = ''; + } + } + + removeIp(index: number): void { + this.allowedIp.removeAt(index); + } + + create(): void { + this.errorMsg = ''; + + if (this.apiKeyForm.invalid) { + this.errorMsg = 'Name is required.'; + return; + } + + this.loading = true; + this.apiKeyService.create(this.apiKeyForm.value).subscribe({ + next: () => { + this.loading = false; + this.activeModal.close('created'); + }, + error: (err) => { + this.loading = false; + this.errorMsg = err.error.message || 'An error occurred while creating the API key.'; + } + }); + } +} + diff --git a/frontend/src/app/app-management/api-keys/shared/models/ApiKeyResponse.ts b/frontend/src/app/app-management/api-keys/shared/models/ApiKeyResponse.ts new file mode 100644 index 000000000..8026a23ab --- /dev/null +++ b/frontend/src/app/app-management/api-keys/shared/models/ApiKeyResponse.ts @@ -0,0 +1,8 @@ +export interface ApiKeyResponse { + id: string; + name: string; + allowedIp: string[]; + createdAt: Date; + expiresAt?: Date; + generatedAt?: Date; +} diff --git a/frontend/src/app/app-management/api-keys/shared/models/ApiKeyUpsert.ts b/frontend/src/app/app-management/api-keys/shared/models/ApiKeyUpsert.ts new file mode 100644 index 000000000..f706ba1dc --- /dev/null +++ b/frontend/src/app/app-management/api-keys/shared/models/ApiKeyUpsert.ts @@ -0,0 +1,5 @@ +export interface ApiKeyUpsert { + name: string; + allowedIp?: string[]; + expiresAt?: Date; +} diff --git a/frontend/src/app/app-management/api-keys/shared/service/api-keys.service.ts b/frontend/src/app/app-management/api-keys/shared/service/api-keys.service.ts new file mode 100644 index 000000000..210755994 --- /dev/null +++ b/frontend/src/app/app-management/api-keys/shared/service/api-keys.service.ts @@ -0,0 +1,108 @@ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpResponse } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { SERVER_API_URL } from '../../../../app.constants'; +import { ApiKeyResponse } from '../models/ApiKeyResponse'; +import { ApiKeyUpsert } from '../models/ApiKeyUpsert'; + +/** + * Service for managing API keys + */ +@Injectable({ + providedIn: 'root' +}) +export class ApiKeysService { + public resourceUrl = SERVER_API_URL + 'api/api-keys'; + + constructor(private http: HttpClient) {} + + /** + * Create a new API key + */ + create(dto: ApiKeyUpsert): Observable> { + return this.http.post( + this.resourceUrl, + dto, + { observe: 'response' } + ); + } + + /** + * Generate (or renew) a plain API key for the given id + * Returns the plain text key (only once) + */ + generate(id: string): Observable> { + return this.http.post( + `${this.resourceUrl}/${id}/generate`, + {}, + { observe: 'response', responseType: 'text' } + ); + } + + /** + * Get API key by id + */ + get(id: string): Observable> { + return this.http.get( + `${this.resourceUrl}/${id}`, + { observe: 'response' } + ); + } + + /** + * List all API keys (with optional pagination) + */ + list(params?: any): Observable> { + return this.http.get( + this.resourceUrl, + { observe: 'response', params } + ); + } + + /** + * Update an existing API key + */ + update(id: string, dto: ApiKeyUpsert): Observable> { + return this.http.put( + `${this.resourceUrl}/${id}`, + dto, + { observe: 'response' } + ); + } + + /** + * Delete API key + */ + delete(id: string): Observable> { + return this.http.delete( + `${this.resourceUrl}/${id}`, + { observe: 'response' } + ); + } + + /** + * Search API key usage in Elasticsearch + */ + usage(params: { + filters?: any[]; + top: number; + indexPattern: string; + includeChildren?: boolean; + page?: number; + size?: number; + }): Observable { + return this.http.get( + `${this.resourceUrl}/usage`, + { + params: { + top: params.top.toString(), + indexPattern: params.indexPattern, + includeChildren: params.includeChildren.toString() || 'false', + page: params.page.toString() || '0', + size: params.size.toString() || '10' + } + } + ); + } +} + diff --git a/frontend/src/app/app-management/app-management.module.ts b/frontend/src/app/app-management/app-management.module.ts index 32484f1c9..5841a0f91 100644 --- a/frontend/src/app/app-management/app-management.module.ts +++ b/frontend/src/app/app-management/app-management.module.ts @@ -45,9 +45,13 @@ import {UtmApiDocComponent} from './utm-api-doc/utm-api-doc.component'; import { UtmNotificationViewComponent } from "./utm-notification/components/notifications-view/utm-notification-view.component"; +import { ApiKeysComponent } from './api-keys/api-keys.component'; +import { ApiKeyModalComponent } from './api-keys/shared/components/api-key-modal/api-key-modal.component'; @NgModule({ declarations: [ + ApiKeysComponent, + ApiKeyModalComponent, AppManagementComponent, AppManagementSidebarComponent, IndexPatternHelpComponent, @@ -84,6 +88,7 @@ import { HealthDetailComponent, MenuDeleteDialogComponent, TokenActivateComponent, + ApiKeyModalComponent, IndexDeleteComponent], imports: [ CommonModule, diff --git a/frontend/src/app/app-management/connection-key/connection-key.component.html b/frontend/src/app/app-management/connection-key/connection-key.component.html index aa95f0ae7..441546517 100644 --- a/frontend/src/app/app-management/connection-key/connection-key.component.html +++ b/frontend/src/app/app-management/connection-key/connection-key.component.html @@ -1,4 +1,6 @@ -
+
+ +
Connection key @@ -71,5 +73,21 @@
Connection Key
+
+ + + +
+
+
+ For developers +
+
+ +
+ +
+
+
From 28551bc916972a7c64575b47c5f8c4f36dc5af20 Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Mon, 20 Oct 2025 08:33:33 -0500 Subject: [PATCH 046/128] refactor(api_keys): remove unused ApplicationEventService from ApiKeyFilter --- .../java/com/park/utmstack/security/api_key/ApiKeyFilter.java | 1 - backend/src/main/resources/config/liquibase/scripts/tables.sql | 1 - 2 files changed, 2 deletions(-) diff --git a/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java b/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java index f629da7fc..05950f08b 100644 --- a/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java +++ b/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java @@ -52,7 +52,6 @@ public class ApiKeyFilter extends OncePerRequestFilter { private final ApiKeyService apiKeyService; private final ConcurrentMap invalidApiKeyBlackList = new ConcurrentHashMap<>(); private final ConcurrentMap cidrCache = new ConcurrentHashMap<>(); - private final ApplicationEventService applicationEventService; private final ApiKeyUsageLoggingService apiKeyUsageLoggingService; @Override diff --git a/backend/src/main/resources/config/liquibase/scripts/tables.sql b/backend/src/main/resources/config/liquibase/scripts/tables.sql index 629971010..7500ca2cf 100644 --- a/backend/src/main/resources/config/liquibase/scripts/tables.sql +++ b/backend/src/main/resources/config/liquibase/scripts/tables.sql @@ -12,7 +12,6 @@ DROP TABLE IF EXISTS public.utm_gvm_scan_result; DROP TABLE IF EXISTS public.utm_gvm_task; DROP TABLE IF EXISTS public.utm_module_modal; DROP TABLE IF EXISTS public.utm_system_restart; -DROP TABLE IF EXISTS public.utm_api_keys; CREATE TABLE IF NOT EXISTS public.jhi_authority ( From 2219a5122d70c6f8df8fe7dbd045840a3ccb34d3 Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Mon, 20 Oct 2025 08:46:16 -0500 Subject: [PATCH 047/128] refactor(api_keys): update API key table schema and change ID type to BIGINT --- .../park/utmstack/domain/api_keys/ApiKey.java | 5 ++--- .../dto/api_key/ApiKeyResponseDTO.java | 2 +- .../20251017001_create_api_keys_table.xml | 4 ++-- .../config/liquibase/scripts/tables.sql | 20 ------------------- 4 files changed, 5 insertions(+), 26 deletions(-) diff --git a/backend/src/main/java/com/park/utmstack/domain/api_keys/ApiKey.java b/backend/src/main/java/com/park/utmstack/domain/api_keys/ApiKey.java index 8d0585a3d..4986a7b58 100644 --- a/backend/src/main/java/com/park/utmstack/domain/api_keys/ApiKey.java +++ b/backend/src/main/java/com/park/utmstack/domain/api_keys/ApiKey.java @@ -19,9 +19,8 @@ public class ApiKey implements Serializable { @Id - @GeneratedValue(strategy = GenerationType.AUTO) - @Column(columnDefinition = "UUID") - private UUID id; + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; @Column(nullable = false) private Long userId; diff --git a/backend/src/main/java/com/park/utmstack/service/dto/api_key/ApiKeyResponseDTO.java b/backend/src/main/java/com/park/utmstack/service/dto/api_key/ApiKeyResponseDTO.java index 024f7282c..abfa4d02d 100644 --- a/backend/src/main/java/com/park/utmstack/service/dto/api_key/ApiKeyResponseDTO.java +++ b/backend/src/main/java/com/park/utmstack/service/dto/api_key/ApiKeyResponseDTO.java @@ -17,7 +17,7 @@ public class ApiKeyResponseDTO { @Schema(description = "Unique identifier of the API key") - private UUID id; + private Long id; @Schema(description = "User-friendly API key name") private String name; diff --git a/backend/src/main/resources/config/liquibase/changelog/20251017001_create_api_keys_table.xml b/backend/src/main/resources/config/liquibase/changelog/20251017001_create_api_keys_table.xml index 2b996f83c..ad2fbaf47 100644 --- a/backend/src/main/resources/config/liquibase/changelog/20251017001_create_api_keys_table.xml +++ b/backend/src/main/resources/config/liquibase/changelog/20251017001_create_api_keys_table.xml @@ -4,9 +4,9 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd"> - + - + diff --git a/backend/src/main/resources/config/liquibase/scripts/tables.sql b/backend/src/main/resources/config/liquibase/scripts/tables.sql index 7500ca2cf..f50a695d1 100644 --- a/backend/src/main/resources/config/liquibase/scripts/tables.sql +++ b/backend/src/main/resources/config/liquibase/scripts/tables.sql @@ -830,23 +830,3 @@ CREATE TABLE IF NOT EXISTS public.utm_space_notification_control next_notification timestamp without time zone NOT NULL, CONSTRAINT utm_space_notification_control_pkey PRIMARY KEY (id) ); - -CREATE TABLE IF NOT EXISTS public.utm_api_keys -( - id uuid default uuid_generate_v4() not null - primary key, - account_id uuid not null, - name varchar(255) not null, - api_key varchar(255) not null, - allowed_ip text, - created_at timestamp not null, - expires_at timestamp, - generated_at timestamp -); - -alter table utm_api_keys - owner to postgres; - -create unique index uk_api_keys_api_key - on utm_api_keys (api_key); - From f3f3789e90fc8bbae94e5c61370884b3d91f498e Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Thu, 23 Oct 2025 14:26:15 -0500 Subject: [PATCH 048/128] feat(api_keys): enhance API key management UI Signed-off-by: Manuel Abascal --- .../api-keys/api-keys.component.html | 78 ++++++++++++------- .../api-keys/api-keys.component.scss | 7 ++ .../api-keys/api-keys.component.ts | 5 ++ .../api-key-modal/api-key-modal.component.ts | 6 +- .../app-management-routing.module.ts | 12 ++- .../connection-key.component.html | 20 +---- .../app-management-sidebar.component.html | 10 +++ 7 files changed, 89 insertions(+), 49 deletions(-) diff --git a/frontend/src/app/app-management/api-keys/api-keys.component.html b/frontend/src/app/app-management/api-keys/api-keys.component.html index 677d5cde5..418bbd87d 100644 --- a/frontend/src/app/app-management/api-keys/api-keys.component.html +++ b/frontend/src/app/app-management/api-keys/api-keys.component.html @@ -1,25 +1,47 @@ -
-
-
-
-

API Keys

-

- The API key is a simple encrypted string that identifies you in the application. -

-
- +
+
+
+
+ API Keys +
+ + The API key is a simple encrypted string that identifies you in the application. With this key, you can access the REST API. +
- -
- - + + +
+
+
+ - - - - + + + + @@ -27,17 +49,17 @@

API Keys

- - + + diff --git a/frontend/src/app/app-management/api-keys/api-keys.component.scss b/frontend/src/app/app-management/api-keys/api-keys.component.scss index e69de29bb..b3751c83a 100644 --- a/frontend/src/app/app-management/api-keys/api-keys.component.scss +++ b/frontend/src/app/app-management/api-keys/api-keys.component.scss @@ -0,0 +1,7 @@ +:host{ + display: flex; + flex-direction: column; + flex: 1 1 auto; + min-height: 0; + height: 100%; +} diff --git a/frontend/src/app/app-management/api-keys/api-keys.component.ts b/frontend/src/app/app-management/api-keys/api-keys.component.ts index e55e0600e..cea8476b4 100644 --- a/frontend/src/app/app-management/api-keys/api-keys.component.ts +++ b/frontend/src/app/app-management/api-keys/api-keys.component.ts @@ -3,6 +3,7 @@ import { ApiKeysService } from './shared/service/api-keys.service'; import { ApiKeyResponse } from './shared/models/ApiKeyResponse'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { ApiKeyModalComponent } from './shared/components/api-key-modal/api-key-modal.component'; +import {SortEvent} from "../../shared/directives/sortable/type/sort-event"; @Component({ selector: 'app-api-keys', @@ -50,4 +51,8 @@ export class ApiKeysComponent implements OnInit { alert('New API Key: ' + res.body); }); } + + onSortBy($event: SortEvent | string) { + + } } diff --git a/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts index c2944f8d1..d9b97f091 100644 --- a/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts +++ b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts @@ -53,7 +53,11 @@ export class ApiKeyModalComponent implements OnInit { } this.loading = true; - this.apiKeyService.create(this.apiKeyForm.value).subscribe({ + const payload = { + ...this.apiKeyForm.value, + expiresAt: this.apiKeyForm.value.expiresAt + ':00.000Z', + }; + this.apiKeyService.create(payload).subscribe({ next: () => { this.loading = false; this.activeModal.close('created'); diff --git a/frontend/src/app/app-management/app-management-routing.module.ts b/frontend/src/app/app-management/app-management-routing.module.ts index 616d6fa01..d498dae62 100644 --- a/frontend/src/app/app-management/app-management-routing.module.ts +++ b/frontend/src/app/app-management/app-management-routing.module.ts @@ -16,6 +16,7 @@ import {MenuComponent} from './menu/menu.component'; import {RolloverConfigComponent} from './rollover-config/rollover-config.component'; import {UtmApiDocComponent} from './utm-api-doc/utm-api-doc.component'; import {UtmNotificationViewComponent} from './utm-notification/components/notifications-view/utm-notification-view.component'; +import {ApiKeysComponent} from "./api-keys/api-keys.component"; const routes: Routes = [ {path: '', redirectTo: 'settings', pathMatch: 'full'}, @@ -123,7 +124,16 @@ const routes: Routes = [ data: { authorities: [ADMIN_ROLE] }, - }], + }, + { + path: 'api-keys', + component: ApiKeysComponent, + canActivate: [UserRouteAccessService], + data: { + authorities: [ADMIN_ROLE] + }, + } + ], }, ]; diff --git a/frontend/src/app/app-management/connection-key/connection-key.component.html b/frontend/src/app/app-management/connection-key/connection-key.component.html index 441546517..aa95f0ae7 100644 --- a/frontend/src/app/app-management/connection-key/connection-key.component.html +++ b/frontend/src/app/app-management/connection-key/connection-key.component.html @@ -1,6 +1,4 @@ -
- -
+
Connection key @@ -73,21 +71,5 @@
Connection Key
-
- - - -
-
-
- For developers -
-
- -
- -
-
-
diff --git a/frontend/src/app/app-management/layout/app-management-sidebar/app-management-sidebar.component.html b/frontend/src/app/app-management/layout/app-management-sidebar/app-management-sidebar.component.html index 66efbcbfb..a67af0739 100644 --- a/frontend/src/app/app-management/layout/app-management-sidebar/app-management-sidebar.component.html +++ b/frontend/src/app/app-management/layout/app-management-sidebar/app-management-sidebar.component.html @@ -125,6 +125,16 @@ + + +   + API Keys + + + Date: Thu, 23 Oct 2025 14:52:05 -0500 Subject: [PATCH 049/128] feat(api_keys): enhance API key management UI Signed-off-by: Manuel Abascal --- .../api-keys/api-keys.component.ts | 7 ++- .../api-key-modal.component.html | 59 +++++++------------ .../api-key-modal/api-key-modal.component.ts | 7 ++- .../api-keys/shared/models/ApiKeyUpsert.ts | 1 + 4 files changed, 33 insertions(+), 41 deletions(-) diff --git a/frontend/src/app/app-management/api-keys/api-keys.component.ts b/frontend/src/app/app-management/api-keys/api-keys.component.ts index cea8476b4..929808937 100644 --- a/frontend/src/app/app-management/api-keys/api-keys.component.ts +++ b/frontend/src/app/app-management/api-keys/api-keys.component.ts @@ -30,12 +30,15 @@ export class ApiKeysComponent implements OnInit { this.apiKeys = res.body || []; this.loading = false; }, - error: () => (this.loading = false) + error: () => { + this.loading = false; + this.apiKeys = []; + } }); } openCreateModal(): void { - const modalRef = this.modalService.open(ApiKeyModalComponent, { centered: true, size: 'lg' }); + const modalRef = this.modalService.open(ApiKeyModalComponent, { centered: true }); modalRef.result.then((result) => { if (result === 'created') this.loadKeys(); }).catch(() => {}); diff --git a/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.html b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.html index 28ae3a6a6..6047a4749 100644 --- a/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.html +++ b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.html @@ -1,16 +1,6 @@ - + -
- +
-
    -
  • - {{ ip.value }} - -
  • -
- - + + +
+ + +
+ +
+ {{ ipInputError }} +
+ +
    +
  • + +
    +
    + + {{ ip.value }} + {{ getIpType(ip.value) }} +
    +
    + +
    +
    +
  • +
+ +
diff --git a/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.scss b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.scss index 8b1378917..e31427b61 100644 --- a/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.scss +++ b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.scss @@ -1 +1,12 @@ +.disabled-rounded-start { + border-top-left-radius: 0 !important; + border-bottom-left-radius: 0 !important; +} +.disabled-rounded-end { + border-top-right-radius: 0 !important; + border-bottom-right-radius: 0 !important; +} +.mt-4 { + margin-top: 9rem !important; +} diff --git a/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts index dd4435cec..b72d4017a 100644 --- a/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts +++ b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts @@ -3,6 +3,7 @@ import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import { ApiKeysService } from '../../service/api-keys.service'; import { FormBuilder, FormGroup, Validators, FormArray } from '@angular/forms'; import {ApiKeyUpsert} from '../../models/ApiKeyUpsert'; +import {IpFormsValidators} from "../../../../../rule-management/app-rule/validators/ip.forms.validators"; @Component({ selector: 'app-api-key-modal', @@ -18,17 +19,19 @@ export class ApiKeyModalComponent implements OnInit { loading = false; errorMsg = ''; isSaving: string | string[] | Set | { [p: string]: any }; + ipInputError: string = ''; constructor( public activeModal: NgbActiveModal, private apiKeyService: ApiKeysService, - private fb: FormBuilder - ) { + private fb: FormBuilder) { + this.apiKeyForm = this.fb.group({ name: ['', Validators.required], allowedIp: this.fb.array([]), - expiresAt: [null] + expiresAt: ['', Validators.required], }); + } ngOnInit(): void {} @@ -38,11 +41,36 @@ export class ApiKeyModalComponent implements OnInit { } addIp(): void { - const ip = this.ipInput.trim(); - if (ip) { - this.allowedIp.push(this.fb.control(ip)); - this.ipInput = ''; + const trimmedIp = this.ipInput.trim(); + + if (!trimmedIp) { + this.ipInputError = 'Please enter an IP address or CIDR'; // Se asigna el error + return; + } + + const tempControl = this.fb.control(trimmedIp, [IpFormsValidators.ipOrCidr()]); + + if (tempControl.invalid) { + if (tempControl.hasError('invalidIp')) { + this.ipInputError = 'Invalid IP address format'; + } else if (tempControl.hasError('invalidCidr')) { + this.ipInputError = 'Invalid CIDR format'; + } + return; } + + const isDuplicate = this.allowedIp.controls.some( + control => control.value === trimmedIp + ); + + if (isDuplicate) { + this.ipInputError = 'This IP is already added'; + return; + } + + this.allowedIp.push(this.fb.control(trimmedIp, [IpFormsValidators.ipOrCidr()])); + this.ipInput = ''; + this.ipInputError = ''; } removeIp(index: number): void { @@ -73,5 +101,13 @@ export class ApiKeyModalComponent implements OnInit { } }); } + + getIpType(value: string): string { + if (!value) { return ''; } + if (value.includes('/')) { + return value.includes(':') ? 'IPv6 CIDR' : 'IPv4 CIDR'; + } + return value.includes(':') ? 'IPv6' : 'IPv4'; + } } diff --git a/frontend/src/app/rule-management/app-rule/validators/ip.forms.validators.ts b/frontend/src/app/rule-management/app-rule/validators/ip.forms.validators.ts new file mode 100644 index 000000000..b82b574d6 --- /dev/null +++ b/frontend/src/app/rule-management/app-rule/validators/ip.forms.validators.ts @@ -0,0 +1,86 @@ +import {AbstractControl, ValidationErrors, ValidatorFn} from '@angular/forms'; + +export class IpFormsValidators { + + static ipOrCidr(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + if (!control.value) { + return null; + } + + const value = control.value.trim(); + + if (value.includes('/')) { + return IpFormsValidators.validateCIDR(value) ? null : { invalidCidr: true }; + } + + const isValidIPv4 = IpFormsValidators.validateIPv4(value); + const isValidIPv6 = IpFormsValidators.validateIPv6(value); + + return (isValidIPv4 || isValidIPv6) ? null : { invalidIp: true }; + }; + } + + private static validateIPv4(ip: string): boolean { + const ipv4Regex = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/; + const match = ip.match(ipv4Regex); + + if (!match) { + return false; + } + + for (let i = 1; i <= 4; i++) { + const octet = parseInt(match[i], 10); + if (octet < 0 || octet > 255) { + return false; + } + } + + return true; + } + + private static validateIPv6(ip: string): boolean { + // tslint:disable-next-line:max-line-length + const ipv6Regex = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/; + + return ipv6Regex.test(ip); + } + + private static validateCIDR(cidr: string): boolean { + const parts = cidr.split('/'); + + if (parts.length !== 2) { + return false; + } + + const [ip, prefix] = parts; + const prefixNum = parseInt(prefix, 10); + + const isIPv4 = ip.includes('.') && !ip.includes(':'); + const isIPv6 = ip.includes(':'); + + if (isIPv4) { + if (!IpFormsValidators.validateIPv4(ip)) { + return false; + } + + if (isNaN(prefixNum) || prefixNum < 0 || prefixNum > 32) { + return false; + } + } else if (isIPv6) { + + if (!IpFormsValidators.validateIPv6(ip)) { + return false; + } + + if (isNaN(prefixNum) || prefixNum < 0 || prefixNum > 128) { + return false; + } + } else { + return false; + } + + return true; + } + +} From 6891a884267a64fdeea0473f1e5563b50ab3b258 Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Fri, 24 Oct 2025 11:51:36 -0500 Subject: [PATCH 053/128] feat(api_keys): add API key generation and expiration handling with user feedback Signed-off-by: Manuel Abascal --- .../api-keys/api-keys.component.html | 43 ++++++++- .../api-keys/api-keys.component.ts | 89 ++++++++++++++++--- .../api-key-modal.component.html | 2 + .../api-key-modal/api-key-modal.component.ts | 43 ++++++--- .../api-keys/shared/models/ApiKeyResponse.ts | 6 +- .../shared/service/api-keys.service.ts | 7 ++ 6 files changed, 158 insertions(+), 32 deletions(-) diff --git a/frontend/src/app/app-management/api-keys/api-keys.component.html b/frontend/src/app/app-management/api-keys/api-keys.component.html index 418bbd87d..22b6db422 100644 --- a/frontend/src/app/app-management/api-keys/api-keys.component.html +++ b/frontend/src/app/app-management/api-keys/api-keys.component.html @@ -47,12 +47,20 @@
- + - + diff --git a/frontend/src/app/app-management/api-keys/api-keys.component.ts b/frontend/src/app/app-management/api-keys/api-keys.component.ts index 20f256fcd..c2d305776 100644 --- a/frontend/src/app/app-management/api-keys/api-keys.component.ts +++ b/frontend/src/app/app-management/api-keys/api-keys.component.ts @@ -1,6 +1,10 @@ import {Component, OnInit, TemplateRef, ViewChild} from '@angular/core'; import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; import * as moment from 'moment'; +import {UtmToastService} from "../../shared/alert/utm-toast.service"; +import { + ModalConfirmationComponent +} from "../../shared/components/utm/util/modal-confirmation/modal-confirmation.component"; import {ITEMS_PER_PAGE} from '../../shared/constants/pagination.constants'; import {SortEvent} from '../../shared/directives/sortable/type/sort-event'; import { ApiKeyModalComponent } from './shared/components/api-key-modal/api-key-modal.component'; @@ -28,9 +32,9 @@ export class ApiKeysComponent implements OnInit { generatedModalRef!: NgbModalRef; copied = false; - constructor( - private apiKeyService: ApiKeysService, - private modalService: NgbModal + constructor( private toastService: UtmToastService, + private apiKeyService: ApiKeysService, + private modalService: NgbModal ) {} ngOnInit(): void { @@ -67,9 +71,44 @@ export class ApiKeysComponent implements OnInit { }); } - deleteKey(id: string): void { - if (!confirm('Are you sure you want to delete this API key?')) { return; } - this.apiKeyService.delete(id).subscribe(() => this.loadKeys()); + editKey(key: ApiKeyResponse): void { + const modalRef = this.modalService.open(ApiKeyModalComponent, {centered: true}); + modalRef.componentInstance.apiKey = key; + + modalRef.result.then((key: ApiKeyResponse) => { + if (key) { + this.generateKey(key); + } + }); + } + + deleteKey(apiKey: ApiKeyResponse): void { + const modalRef = this.modalService.open(ModalConfirmationComponent, {centered: true}); + modalRef.componentInstance.header = `Delete API Key: ${apiKey.name}`; + modalRef.componentInstance.message = 'Are you sure you want to delete this API key?'; + modalRef.componentInstance.confirmBtnType = 'delete'; + modalRef.componentInstance.type = 'danger'; + modalRef.componentInstance.confirmBtnText = 'Delete'; + modalRef.componentInstance.confirmBtnIcon = 'icon-cross-circle'; + + modalRef.result.then(reason => { + if (reason === 'ok') { + this.delete(apiKey); + } + }); + } + + delete(apiKey: ApiKeyResponse): void { + this.apiKeyService.delete(apiKey.id).subscribe({ + next: () => { + this.toastService.showSuccess('API key deleted successfully.'); + this.loadKeys(); + }, + error: (err) => { + this.toastService.showError('Error', 'An error occurred while deleting the API key.'); + throw err; + } + }); } getDaysUntilExpire(expiresAt: string): number { diff --git a/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts index e78f53d42..87289aee6 100644 --- a/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts +++ b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts @@ -4,9 +4,8 @@ import { FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import {IpFormsValidators} from '../../../../../rule-management/app-rule/validators/ip.forms.validators'; import {UtmToastService} from '../../../../../shared/alert/utm-toast.service'; -import {ApiKeyUpsert} from '../../models/ApiKeyUpsert'; +import {ApiKeyResponse} from '../../models/ApiKeyResponse'; import { ApiKeysService } from '../../service/api-keys.service'; -import {ApiKeyResponse} from "../../models/ApiKeyResponse"; @Component({ selector: 'app-api-key-modal', @@ -15,7 +14,7 @@ import {ApiKeyResponse} from "../../models/ApiKeyResponse"; }) export class ApiKeyModalComponent implements OnInit { - @Input() apiKey: ApiKeyUpsert = null; + @Input() apiKey: ApiKeyResponse = null; apiKeyForm: FormGroup; ipInput = ''; @@ -29,17 +28,24 @@ export class ApiKeyModalComponent implements OnInit { private apiKeyService: ApiKeysService, private fb: FormBuilder, private toastService: UtmToastService) { + } + + ngOnInit(): void { + + const expiresAtDate = this.apiKey && this.apiKey.expiresAt ? new Date(this.apiKey.expiresAt) : null; + const expiresAtNgbDate = expiresAtDate ? { + year: expiresAtDate.getUTCFullYear(), + month: expiresAtDate.getUTCMonth() + 1, + day: expiresAtDate.getUTCDate() + } : null; this.apiKeyForm = this.fb.group({ - name: ['', Validators.required], - allowedIp: this.fb.array([]), - expiresAt: ['', Validators.required], + name: [ this.apiKey ? this.apiKey.name : '', Validators.required], + allowedIp: this.fb.array(this.apiKey ? this.apiKey.allowedIp : []), + expiresAt: [expiresAtNgbDate, Validators.required], }); - } - ngOnInit(): void {} - get allowedIp(): FormArray { return this.apiKeyForm.get('allowedIp') as FormArray; } @@ -102,7 +108,11 @@ export class ApiKeyModalComponent implements OnInit { ...this.apiKeyForm.value, expiresAt: formattedDate, }; - this.apiKeyService.create(payload).subscribe({ + + const save = this.apiKey ? this.apiKeyService.update(this.apiKey.id, payload) : + this.apiKeyService.create(payload); + + save.subscribe({ next: (response) => { this.loading = false; this.activeModal.close(response.body as ApiKeyResponse); @@ -114,7 +124,6 @@ export class ApiKeyModalComponent implements OnInit { } else if (err.status === 500) { this.toastService.showError('Error', 'Server error occurred while creating the API key.'); } - } }); } From beff9456f3f4bac5b2c95d526ff75fcb6a5d977b Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Mon, 27 Oct 2025 09:09:45 -0500 Subject: [PATCH 055/128] refactor(api_keys): change API key identifier type from UUID to Long for consistency --- .../utmstack/repository/api_key/ApiKeyRepository.java | 4 ++-- .../park/utmstack/service/api_key/ApiKeyService.java | 10 +++++----- .../park/utmstack/web/rest/api_key/ApiKeyResource.java | 8 ++++---- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/backend/src/main/java/com/park/utmstack/repository/api_key/ApiKeyRepository.java b/backend/src/main/java/com/park/utmstack/repository/api_key/ApiKeyRepository.java index e76001064..ece32eba9 100644 --- a/backend/src/main/java/com/park/utmstack/repository/api_key/ApiKeyRepository.java +++ b/backend/src/main/java/com/park/utmstack/repository/api_key/ApiKeyRepository.java @@ -14,9 +14,9 @@ import java.util.UUID; @Repository -public interface ApiKeyRepository extends JpaRepository { +public interface ApiKeyRepository extends JpaRepository { - Optional findByIdAndUserId(UUID id, Long userId); + Optional findByIdAndUserId(Long id, Long userId); Page findByUserId(Long userId, Pageable pageable); diff --git a/backend/src/main/java/com/park/utmstack/service/api_key/ApiKeyService.java b/backend/src/main/java/com/park/utmstack/service/api_key/ApiKeyService.java index 5ed530e3f..56642f183 100644 --- a/backend/src/main/java/com/park/utmstack/service/api_key/ApiKeyService.java +++ b/backend/src/main/java/com/park/utmstack/service/api_key/ApiKeyService.java @@ -58,11 +58,11 @@ public ApiKeyResponseDTO createApiKey(Long userId,ApiKeyUpsertDTO dto) { return apiKeyMapper.toDto(apiKeyRepository.save(apiKey)); } catch (Exception e) { - throw new RuntimeException(ctx + ": " + e.getMessage()); + throw new ApiKeyExistException(ctx + ": " + e.getMessage()); } } - public String generateApiKey(Long userId, UUID apiKeyId) { + public String generateApiKey(Long userId, Long apiKeyId) { final String ctx = CLASSNAME + ".generateApiKey"; try { ApiKey apiKey = apiKeyRepository.findByIdAndUserId(apiKeyId, userId) @@ -77,7 +77,7 @@ public String generateApiKey(Long userId, UUID apiKeyId) { } } - public ApiKeyResponseDTO updateApiKey(Long userId, UUID apiKeyId, ApiKeyUpsertDTO dto) { + public ApiKeyResponseDTO updateApiKey(Long userId, Long apiKeyId, ApiKeyUpsertDTO dto) { final String ctx = CLASSNAME + ".updateApiKey"; try { ApiKey apiKey = apiKeyRepository.findByIdAndUserId(apiKeyId, userId) @@ -96,7 +96,7 @@ public ApiKeyResponseDTO updateApiKey(Long userId, UUID apiKeyId, ApiKeyUpsertDT } } - public ApiKeyResponseDTO getApiKey(Long userId, UUID apiKeyId) { + public ApiKeyResponseDTO getApiKey(Long userId, Long apiKeyId) { final String ctx = CLASSNAME + ".getApiKey"; try { ApiKey apiKey = apiKeyRepository.findByIdAndUserId(apiKeyId, userId) @@ -117,7 +117,7 @@ public Page listApiKeys(Long userId, Pageable pageable) { } - public void deleteApiKey(Long userId, UUID apiKeyId) { + public void deleteApiKey(Long userId, Long apiKeyId) { final String ctx = CLASSNAME + ".deleteApiKey"; try { ApiKey apiKey = apiKeyRepository.findByIdAndUserId(apiKeyId, userId) diff --git a/backend/src/main/java/com/park/utmstack/web/rest/api_key/ApiKeyResource.java b/backend/src/main/java/com/park/utmstack/web/rest/api_key/ApiKeyResource.java index 147ab5254..8453f9ab2 100644 --- a/backend/src/main/java/com/park/utmstack/web/rest/api_key/ApiKeyResource.java +++ b/backend/src/main/java/com/park/utmstack/web/rest/api_key/ApiKeyResource.java @@ -78,7 +78,7 @@ public ResponseEntity createApiKey(@RequestBody ApiKeyUpsertD }) }) @PostMapping("/{id}/generate") - public ResponseEntity generateApiKey(@PathVariable("id") UUID apiKeyId) { + public ResponseEntity generateApiKey(@PathVariable("id") Long apiKeyId) { Long userId = userService.getCurrentUserLogin().getId(); String plainKey = apiKeyService.generateApiKey(userId, apiKeyId); return ResponseEntity.ok(plainKey); @@ -96,7 +96,7 @@ public ResponseEntity generateApiKey(@PathVariable("id") UUID apiKeyId) }) }) @GetMapping("/{id}") - public ResponseEntity getApiKey(@PathVariable("id") UUID apiKeyId) { + public ResponseEntity getApiKey(@PathVariable("id") Long apiKeyId) { Long userId = userService.getCurrentUserLogin().getId(); ApiKeyResponseDTO responseDTO = apiKeyService.getApiKey(userId, apiKeyId); return ResponseEntity.ok(responseDTO); @@ -134,7 +134,7 @@ public ResponseEntity> listApiKeys(@ParameterObject Page }) }) @PutMapping("/{id}") - public ResponseEntity updateApiKey(@PathVariable("id") UUID apiKeyId, + public ResponseEntity updateApiKey(@PathVariable("id") Long apiKeyId, @RequestBody ApiKeyUpsertDTO dto) { Long userId = userService.getCurrentUserLogin().getId(); @@ -154,7 +154,7 @@ public ResponseEntity updateApiKey(@PathVariable("id") UUID a }) }) @DeleteMapping("/{id}") - public ResponseEntity deleteApiKey(@PathVariable("id") UUID apiKeyId) { + public ResponseEntity deleteApiKey(@PathVariable("id") Long apiKeyId) { Long userId = userService.getCurrentUserLogin().getId(); apiKeyService.deleteApiKey(userId, apiKeyId); From 90d68741e29d44a4d3bbcf012c0d126861d98782 Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Mon, 27 Oct 2025 12:12:02 -0500 Subject: [PATCH 056/128] feat(api_keys): simplify API key listing endpoint and enhance expiration handling --- .../utmstack/service/api_key/ApiKeyService.java | 14 ++++++++++++++ .../utmstack/web/rest/api_key/ApiKeyResource.java | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/backend/src/main/java/com/park/utmstack/service/api_key/ApiKeyService.java b/backend/src/main/java/com/park/utmstack/service/api_key/ApiKeyService.java index 56642f183..cbc2784a5 100644 --- a/backend/src/main/java/com/park/utmstack/service/api_key/ApiKeyService.java +++ b/backend/src/main/java/com/park/utmstack/service/api_key/ApiKeyService.java @@ -20,6 +20,7 @@ import org.springframework.stereotype.Service; import java.security.SecureRandom; +import java.time.Duration; import java.time.Instant; import java.util.Base64; import java.util.Optional; @@ -67,9 +68,22 @@ public String generateApiKey(Long userId, Long apiKeyId) { try { ApiKey apiKey = apiKeyRepository.findByIdAndUserId(apiKeyId, userId) .orElseThrow(() -> new ApiKeyNotFoundException("API key not found")); + + Instant now = Instant.now(); + Instant originalCreated = apiKey.getGeneratedAt() != null ? apiKey.getGeneratedAt() : apiKey.getCreatedAt(); + Instant originalExpires = apiKey.getExpiresAt(); + + Duration duration; + if (originalCreated != null && originalExpires != null && !originalExpires.isBefore(originalCreated)) { + duration = Duration.between(originalCreated, originalExpires); + } else { + duration = Duration.ofDays(7); + } + String plainKey = generateRandomKey(); apiKey.setApiKey(plainKey); apiKey.setGeneratedAt(Instant.now()); + apiKey.setExpiresAt(now.plus(duration)); apiKeyRepository.save(apiKey); return plainKey; } catch (Exception e) { diff --git a/backend/src/main/java/com/park/utmstack/web/rest/api_key/ApiKeyResource.java b/backend/src/main/java/com/park/utmstack/web/rest/api_key/ApiKeyResource.java index 8453f9ab2..d0bdd0d2b 100644 --- a/backend/src/main/java/com/park/utmstack/web/rest/api_key/ApiKeyResource.java +++ b/backend/src/main/java/com/park/utmstack/web/rest/api_key/ApiKeyResource.java @@ -113,7 +113,7 @@ public ResponseEntity getApiKey(@PathVariable("id") Long apiK @Header(name = "X-App-Error", description = "Technical error details") }) }) - @GetMapping("") + @GetMapping public ResponseEntity> listApiKeys(@ParameterObject Pageable pageable) { Long userId = userService.getCurrentUserLogin().getId(); Page page = apiKeyService.listApiKeys(userId,pageable); From 8425c2fc431e9b9c32dd3b5fd9dfe848b75ef962 Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Mon, 27 Oct 2025 12:12:24 -0500 Subject: [PATCH 057/128] feat(api_keys): improve API key listing with pagination, loading states, and expiration indicators Signed-off-by: Manuel Abascal --- .../api-keys/api-keys.component.html | 57 ++++++++++++++--- .../api-keys/api-keys.component.ts | 62 ++++++++++++++----- .../shared/service/api-keys.service.ts | 4 +- 3 files changed, 96 insertions(+), 27 deletions(-) diff --git a/frontend/src/app/app-management/api-keys/api-keys.component.html b/frontend/src/app/app-management/api-keys/api-keys.component.html index d31f55a06..e2a5158ff 100644 --- a/frontend/src/app/app-management/api-keys/api-keys.component.html +++ b/frontend/src/app/app-management/api-keys/api-keys.component.html @@ -45,19 +45,26 @@
- + + + + + + + + + + + + +
NAMEALLOWED IPsCREATED ATEXPIRES AT + Name + + Allowed IPs + + Expires At + + Created At + ACTIONS
{{ key.name }} {{ key.allowedIp?.join(', ') || '—' }}{{ key.createdAt | date:'M/d/yy, h:mm a' }}{{ key.expiresAt ? (key.expiresAt | date:'M/d/yy, h:mm a') : '—' }}{{ key.expiresAt ? (key.expiresAt | date:'dd/MM/yy HH:mm' :'UTC') : '—' }}{{ key.createdAt | date:'dd/MM/yy HH:mm' :'UTC' }} - - -
{{ key.name }} {{ key.name }} {{ key.allowedIp?.join(', ') || '—' }}{{ key.expiresAt ? (key.expiresAt | date:'dd/MM/yy HH:mm' :'UTC') : '—' }} + + + + + + {{ key.expiresAt ? (key.expiresAt | date:'dd/MM/yy HH:mm' :'UTC') : '—' }} + {{ key.createdAt | date:'dd/MM/yy HH:mm' :'UTC' }} - + +
+ + {{ 'Keep this key safeKeep this key safe' }} +
+ +
+ +
+ diff --git a/frontend/src/app/app-management/api-keys/api-keys.component.ts b/frontend/src/app/app-management/api-keys/api-keys.component.ts index 929808937..20f256fcd 100644 --- a/frontend/src/app/app-management/api-keys/api-keys.component.ts +++ b/frontend/src/app/app-management/api-keys/api-keys.component.ts @@ -1,9 +1,11 @@ -import { Component, OnInit } from '@angular/core'; -import { ApiKeysService } from './shared/service/api-keys.service'; -import { ApiKeyResponse } from './shared/models/ApiKeyResponse'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import {Component, OnInit, TemplateRef, ViewChild} from '@angular/core'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import * as moment from 'moment'; +import {ITEMS_PER_PAGE} from '../../shared/constants/pagination.constants'; +import {SortEvent} from '../../shared/directives/sortable/type/sort-event'; import { ApiKeyModalComponent } from './shared/components/api-key-modal/api-key-modal.component'; -import {SortEvent} from "../../shared/directives/sortable/type/sort-event"; +import { ApiKeyResponse } from './shared/models/ApiKeyResponse'; +import { ApiKeysService } from './shared/service/api-keys.service'; @Component({ selector: 'app-api-keys', @@ -11,8 +13,20 @@ import {SortEvent} from "../../shared/directives/sortable/type/sort-event"; styleUrls: ['./api-keys.component.scss'] }) export class ApiKeysComponent implements OnInit { + + generating: string[] = []; apiKeys: ApiKeyResponse[] = []; loading = false; + generatedApiKey = ''; + @ViewChild('generatedModal') generatedModal!: TemplateRef; + request = + { + sort: 'createdAt,desc', + page: 0, + size: ITEMS_PER_PAGE + }; + generatedModalRef!: NgbModalRef; + copied = false; constructor( private apiKeyService: ApiKeysService, @@ -37,25 +51,74 @@ export class ApiKeysComponent implements OnInit { }); } + copyToClipboard(): void { + (navigator as any).clipboard.writeText(this.generatedApiKey).then(() => { + this.copied = true; + }); + } + openCreateModal(): void { const modalRef = this.modalService.open(ApiKeyModalComponent, { centered: true }); - modalRef.result.then((result) => { - if (result === 'created') this.loadKeys(); - }).catch(() => {}); + + modalRef.result.then((key: ApiKeyResponse) => { + if (key) { + this.generateKey(key); + } + }); } deleteKey(id: string): void { - if (!confirm('Are you sure you want to delete this API key?')) return; + if (!confirm('Are you sure you want to delete this API key?')) { return; } this.apiKeyService.delete(id).subscribe(() => this.loadKeys()); } - regenerateKey(id: string): void { - this.apiKeyService.generate(id).subscribe((res) => { - alert('New API Key: ' + res.body); - }); + getDaysUntilExpire(expiresAt: string): number { + if (!expiresAt) { + return 0; + } + const today = moment(); + const expireDate = moment(expiresAt); + return expireDate.diff(today, 'days'); } onSortBy($event: SortEvent | string) { } + + maskSecrets(str: string): string { + if (!str || str.length <= 10) { + return str; + } + const prefix = str.substring(0, 10); + const maskLength = str.length - 30; + const maskedPart = '*'.repeat(maskLength); + return prefix + maskedPart; + } + + generateKey(apiKey: ApiKeyResponse): void { + this.generating.push(apiKey.id); + this.apiKeyService.generateApiKey(apiKey.id).subscribe(response => { + this.generatedApiKey = response.body ? response.body : ""; + this.generatedModalRef = this.modalService.open(this.generatedModal, {centered: true}); + const index = this.generating.indexOf(apiKey.id); + if (index > -1) { + this.generating.splice(index, 1); + } + this.loadKeys(); + }); + } + + isApiKeyExpired(expiresAt?: string | null ): boolean { + if (!expiresAt) { + return false; + } + const expirationTime = new Date(expiresAt).getTime(); + return expirationTime < Date.now(); + } + + close() { + this.generatedModalRef.close(); + this.copied = false; + this.generatedApiKey = ''; + } } diff --git a/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.html b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.html index 6e7cad101..7e1399bbb 100644 --- a/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.html +++ b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.html @@ -26,6 +26,7 @@ class="form-control" id="expiresAt" name="expiresAt" + [minDate]="minDate" placeholder="yyyy-mm-dd" formControlName="expiresAt" ngbDatepicker @@ -123,3 +124,4 @@ + diff --git a/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts index b72d4017a..e78f53d42 100644 --- a/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts +++ b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts @@ -1,9 +1,12 @@ +import {HttpErrorResponse} from '@angular/common/http'; import {Component, Input, OnInit} from '@angular/core'; +import { FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ApiKeysService } from '../../service/api-keys.service'; -import { FormBuilder, FormGroup, Validators, FormArray } from '@angular/forms'; +import {IpFormsValidators} from '../../../../../rule-management/app-rule/validators/ip.forms.validators'; +import {UtmToastService} from '../../../../../shared/alert/utm-toast.service'; import {ApiKeyUpsert} from '../../models/ApiKeyUpsert'; -import {IpFormsValidators} from "../../../../../rule-management/app-rule/validators/ip.forms.validators"; +import { ApiKeysService } from '../../service/api-keys.service'; +import {ApiKeyResponse} from "../../models/ApiKeyResponse"; @Component({ selector: 'app-api-key-modal', @@ -19,12 +22,13 @@ export class ApiKeyModalComponent implements OnInit { loading = false; errorMsg = ''; isSaving: string | string[] | Set | { [p: string]: any }; - ipInputError: string = ''; + minDate = { year: new Date().getFullYear(), month: new Date().getMonth() + 1, day: new Date().getDate() }; + ipInputError = ''; - constructor( - public activeModal: NgbActiveModal, - private apiKeyService: ApiKeysService, - private fb: FormBuilder) { + constructor( public activeModal: NgbActiveModal, + private apiKeyService: ApiKeysService, + private fb: FormBuilder, + private toastService: UtmToastService) { this.apiKeyForm = this.fb.group({ name: ['', Validators.required], @@ -86,18 +90,31 @@ export class ApiKeyModalComponent implements OnInit { } this.loading = true; + + const rawDate = this.apiKeyForm.get('expiresAt').value; + let formattedDate = rawDate; + + if (rawDate && typeof rawDate === 'object') { + formattedDate = `${rawDate.year}-${String(rawDate.month).padStart(2, '0')}-${String(rawDate.day).padStart(2, '0')}T00:00:00.000Z`; + } + const payload = { ...this.apiKeyForm.value, - expiresAt: this.apiKeyForm.value.expiresAt + ':00.000Z', + expiresAt: formattedDate, }; this.apiKeyService.create(payload).subscribe({ - next: () => { + next: (response) => { this.loading = false; - this.activeModal.close('created'); + this.activeModal.close(response.body as ApiKeyResponse); }, - error: (err) => { + error: (err: HttpErrorResponse) => { this.loading = false; - this.errorMsg = err.error.message || 'An error occurred while creating the API key.'; + if (err.status === 409) { + this.toastService.showError('Error', 'An API key with this name already exists.'); + } else if (err.status === 500) { + this.toastService.showError('Error', 'Server error occurred while creating the API key.'); + } + } }); } diff --git a/frontend/src/app/app-management/api-keys/shared/models/ApiKeyResponse.ts b/frontend/src/app/app-management/api-keys/shared/models/ApiKeyResponse.ts index 8026a23ab..3f6b890d7 100644 --- a/frontend/src/app/app-management/api-keys/shared/models/ApiKeyResponse.ts +++ b/frontend/src/app/app-management/api-keys/shared/models/ApiKeyResponse.ts @@ -2,7 +2,7 @@ export interface ApiKeyResponse { id: string; name: string; allowedIp: string[]; - createdAt: Date; - expiresAt?: Date; - generatedAt?: Date; + createdAt: string; + expiresAt?: string; + generatedAt?: string; } diff --git a/frontend/src/app/app-management/api-keys/shared/service/api-keys.service.ts b/frontend/src/app/app-management/api-keys/shared/service/api-keys.service.ts index 210755994..e668367ba 100644 --- a/frontend/src/app/app-management/api-keys/shared/service/api-keys.service.ts +++ b/frontend/src/app/app-management/api-keys/shared/service/api-keys.service.ts @@ -80,6 +80,13 @@ export class ApiKeysService { ); } + generateApiKey(apiKeyId: string): Observable> { + return this.http.post(`${this.resourceUrl}/${apiKeyId}/generate`, null, { + observe: 'response', + responseType: 'text' + }); + } + /** * Search API key usage in Elasticsearch */ From e25681adde810f784437df7dc746eb4a9bbe1a03 Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Fri, 24 Oct 2025 12:20:24 -0500 Subject: [PATCH 054/128] feat(api_keys): update API key modal for editing and improved deletion confirmation Signed-off-by: Manuel Abascal --- .../api-keys/api-keys.component.html | 4 +- .../api-keys/api-keys.component.ts | 51 ++++++++++++++++--- .../api-key-modal/api-key-modal.component.ts | 31 +++++++---- 3 files changed, 67 insertions(+), 19 deletions(-) diff --git a/frontend/src/app/app-management/api-keys/api-keys.component.html b/frontend/src/app/app-management/api-keys/api-keys.component.html index 22b6db422..d31f55a06 100644 --- a/frontend/src/app/app-management/api-keys/api-keys.component.html +++ b/frontend/src/app/app-management/api-keys/api-keys.component.html @@ -63,10 +63,10 @@
- -
ACTIONS
{{ key.name }} {{ key.allowedIp?.join(', ') || '—' }} - - + + - {{ key.expiresAt ? (key.expiresAt | date:'dd/MM/yy HH:mm' :'UTC') : '—' }} + + + + + + + + {{ key.expiresAt ? (key.expiresAt | date:'dd/MM/yy HH:mm':'UTC') : '—' }} {{ key.createdAt | date:'dd/MM/yy HH:mm' :'UTC' }}
+ +
+
+ + +
+
- - Loading... - No API Keys found. - +
+
+ + + + +
+ +
diff --git a/frontend/src/app/app-management/api-keys/api-keys.component.ts b/frontend/src/app/app-management/api-keys/api-keys.component.ts index c2d305776..f3ac14e08 100644 --- a/frontend/src/app/app-management/api-keys/api-keys.component.ts +++ b/frontend/src/app/app-management/api-keys/api-keys.component.ts @@ -1,15 +1,15 @@ import {Component, OnInit, TemplateRef, ViewChild} from '@angular/core'; import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; import * as moment from 'moment'; -import {UtmToastService} from "../../shared/alert/utm-toast.service"; +import {UtmToastService} from '../../shared/alert/utm-toast.service'; import { ModalConfirmationComponent -} from "../../shared/components/utm/util/modal-confirmation/modal-confirmation.component"; +} from '../../shared/components/utm/util/modal-confirmation/modal-confirmation.component'; import {ITEMS_PER_PAGE} from '../../shared/constants/pagination.constants'; import {SortEvent} from '../../shared/directives/sortable/type/sort-event'; -import { ApiKeyModalComponent } from './shared/components/api-key-modal/api-key-modal.component'; -import { ApiKeyResponse } from './shared/models/ApiKeyResponse'; -import { ApiKeysService } from './shared/service/api-keys.service'; +import {ApiKeyModalComponent} from './shared/components/api-key-modal/api-key-modal.component'; +import {ApiKeyResponse} from './shared/models/ApiKeyResponse'; +import {ApiKeysService} from './shared/service/api-keys.service'; @Component({ selector: 'app-api-keys', @@ -19,18 +19,23 @@ import { ApiKeysService } from './shared/service/api-keys.service'; export class ApiKeysComponent implements OnInit { generating: string[] = []; + noData = false; apiKeys: ApiKeyResponse[] = []; loading = false; generatedApiKey = ''; @ViewChild('generatedModal') generatedModal!: TemplateRef; - request = - { - sort: 'createdAt,desc', - page: 0, - size: ITEMS_PER_PAGE - }; generatedModalRef!: NgbModalRef; copied = false; + readonly itemsPerPage = ITEMS_PER_PAGE; + totalItems = 0; + page = 0; + size = this.itemsPerPage; + + request = { + sort: 'createdAt,desc', + page: this.page, + size: this.size + }; constructor( private toastService: UtmToastService, private apiKeyService: ApiKeysService, @@ -43,9 +48,11 @@ export class ApiKeysComponent implements OnInit { loadKeys(): void { this.loading = true; - this.apiKeyService.list().subscribe({ + this.apiKeyService.list(this.request).subscribe({ next: (res) => { + this.totalItems = Number(res.headers.get('X-Total-Count')); this.apiKeys = res.body || []; + this.noData = this.apiKeys.length === 0; this.loading = false; }, error: () => { @@ -113,15 +120,17 @@ export class ApiKeysComponent implements OnInit { getDaysUntilExpire(expiresAt: string): number { if (!expiresAt) { - return 0; + return -1; } - const today = moment(); - const expireDate = moment(expiresAt); + + const today = moment().startOf('day'); + const expireDate = moment(expiresAt).startOf('day'); return expireDate.diff(today, 'days'); } - onSortBy($event: SortEvent | string) { - + onSortBy($event: SortEvent) { + this.request.sort = $event.column + ',' + $event.direction; + this.loadKeys(); } maskSecrets(str: string): string { @@ -160,4 +169,23 @@ export class ApiKeysComponent implements OnInit { this.copied = false; this.generatedApiKey = ''; } + + loadPage($event: number) { + this.page = $event - 1; + this.request = { + ...this.request, + page: this.page + }; + this.loadKeys(); + } + + onItemsPerPageChange($event: number) { + this.request = { + ...this.request, + size: $event, + page: 0 + }; + this.page = 0; + this.loadKeys(); + } } diff --git a/frontend/src/app/app-management/api-keys/shared/service/api-keys.service.ts b/frontend/src/app/app-management/api-keys/shared/service/api-keys.service.ts index e668367ba..e239d5e4d 100644 --- a/frontend/src/app/app-management/api-keys/shared/service/api-keys.service.ts +++ b/frontend/src/app/app-management/api-keys/shared/service/api-keys.service.ts @@ -4,6 +4,7 @@ import { Observable } from 'rxjs'; import { SERVER_API_URL } from '../../../../app.constants'; import { ApiKeyResponse } from '../models/ApiKeyResponse'; import { ApiKeyUpsert } from '../models/ApiKeyUpsert'; +import {createRequestOption} from "../../../../shared/util/request-util"; /** * Service for managing API keys @@ -53,9 +54,10 @@ export class ApiKeysService { * List all API keys (with optional pagination) */ list(params?: any): Observable> { + const httpParams = createRequestOption(params); return this.http.get( this.resourceUrl, - { observe: 'response', params } + { observe: 'response', params: httpParams }, ); } From ab3d3bf7a53b6d5cdc4f9f2fc2ef34f79956e06f Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Tue, 28 Oct 2025 14:29:59 -0500 Subject: [PATCH 058/128] feat(api_keys): simplify API key listing endpoint and enhance expiration handling --- backend/.gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/.gitignore b/backend/.gitignore index 834dd8821..518edd788 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -62,6 +62,7 @@ local.properties *.orig classes/ out/ +.nvim/ ###################### # Visual Studio Code From 68a6ed67999bd849c54e635ce0eeaf4aa6f7779e Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Tue, 28 Oct 2025 14:30:48 -0500 Subject: [PATCH 059/128] feat(api_keys): simplify API key listing endpoint and enhance expiration handling --- backend/.nvim/.env | 18 --- backend/mvnw | 286 --------------------------------------------- backend/mvnw.cmd | 161 ------------------------- 3 files changed, 465 deletions(-) delete mode 100644 backend/.nvim/.env delete mode 100755 backend/mvnw delete mode 100755 backend/mvnw.cmd diff --git a/backend/.nvim/.env b/backend/.nvim/.env deleted file mode 100644 index 628f21921..000000000 --- a/backend/.nvim/.env +++ /dev/null @@ -1,18 +0,0 @@ -revision=4 -AD_AUDIT_SERVICE=http://localhost:8081/api -DB_HOST=192.168.1.18 -DB_NAME=utmstack -DB_PASS=DF7JOMKyU6oNwmy4 -DB_PORT=5432 -DB_USER=postgres -ELASTICSEARCH_HOST=192.168.1.18 -ELASTICSEARCH_PORT=9200 -ENCRYPTION_KEY=nWkGxX1vRTDyZc1Jm59YUkR3KsUmbYrJ -EVENT_PROCESSOR_HOST=192.168.1.18 -EVENT_PROCESSOR_PORT=9002 -GRPC_AGENT_MANAGER_HOST=192.168.1.18 -GRPC_AGENT_MANAGER_PORT=9000 -INTERNAL_KEY=qNANPjjNm7eantt7sgld0iSWFFeGKz5i -LOGSTASH_URL=http://localhost:9600 -SERVER_NAME=UTM -SOC_AI_BASE_URL=http://localhost:8081/process diff --git a/backend/mvnw b/backend/mvnw deleted file mode 100755 index 5551fde8e..000000000 --- a/backend/mvnw +++ /dev/null @@ -1,286 +0,0 @@ -#!/bin/sh -# ---------------------------------------------------------------------------- -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you 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. -# ---------------------------------------------------------------------------- - -# ---------------------------------------------------------------------------- -# Maven2 Start Up Batch script -# -# Required ENV vars: -# ------------------ -# JAVA_HOME - location of a JDK home dir -# -# Optional ENV vars -# ----------------- -# M2_HOME - location of maven2's installed home dir -# MAVEN_OPTS - parameters passed to the Java VM when running Maven -# e.g. to debug Maven itself, use -# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -# MAVEN_SKIP_RC - flag to disable loading of mavenrc files -# ---------------------------------------------------------------------------- - -if [ -z "$MAVEN_SKIP_RC" ] ; then - - if [ -f /etc/mavenrc ] ; then - . /etc/mavenrc - fi - - if [ -f "$HOME/.mavenrc" ] ; then - . "$HOME/.mavenrc" - fi - -fi - -# OS specific support. $var _must_ be set to either true or false. -cygwin=false; -darwin=false; -mingw=false -case "`uname`" in - CYGWIN*) cygwin=true ;; - MINGW*) mingw=true;; - Darwin*) darwin=true - # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home - # See https://developer.apple.com/library/mac/qa/qa1170/_index.html - if [ -z "$JAVA_HOME" ]; then - if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" - else - export JAVA_HOME="/Library/Java/Home" - fi - fi - ;; -esac - -if [ -z "$JAVA_HOME" ] ; then - if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` - fi -fi - -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - PRG="$0" - - # need this for relative symlinks - while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG="`dirname "$PRG"`/$link" - fi - done - - saveddir=`pwd` - - M2_HOME=`dirname "$PRG"`/.. - - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` - - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi - -# For Cygwin, ensure paths are in UNIX format before anything is touched -if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` -fi - -# For Mingw, ensure paths are in UNIX format before anything is touched -if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" - # TODO classpath? -fi - -if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then - # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then - if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" - else - javaExecutable="`readlink -f \"$javaExecutable\"`" - fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` - JAVA_HOME="$javaHome" - export JAVA_HOME - fi - fi -fi - -if [ -z "$JAVACMD" ] ; then - if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - else - JAVACMD="`which java`" - fi -fi - -if [ ! -x "$JAVACMD" ] ; then - echo "Error: JAVA_HOME is not defined correctly." >&2 - echo " We cannot execute $JAVACMD" >&2 - exit 1 -fi - -if [ -z "$JAVA_HOME" ] ; then - echo "Warning: JAVA_HOME environment variable is not set." -fi - -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher - -# traverses directory structure from process work directory to filesystem root -# first directory with .mvn subdirectory is considered project base directory -find_maven_basedir() { - - if [ -z "$1" ] - then - echo "Path not specified to find_maven_basedir" - return 1 - fi - - basedir="$1" - wdir="$1" - while [ "$wdir" != '/' ] ; do - if [ -d "$wdir"/.mvn ] ; then - basedir=$wdir - break - fi - # workaround for JBEAP-8937 (on Solaris 10/Sparc) - if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` - fi - # end of workaround - done - echo "${basedir}" -} - -# concatenates all lines of a file -concat_lines() { - if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" - fi -} - -BASE_DIR=`find_maven_basedir "$(pwd)"` -if [ -z "$BASE_DIR" ]; then - exit 1; -fi - -########################################################################################## -# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -# This allows using the maven wrapper in projects that prohibit checking in binary data. -########################################################################################## -if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found .mvn/wrapper/maven-wrapper.jar" - fi -else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." - fi - jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" - while IFS="=" read key value; do - case "$key" in (wrapperUrl) jarUrl="$value"; break ;; - esac - done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" - if [ "$MVNW_VERBOSE" = true ]; then - echo "Downloading from: $jarUrl" - fi - wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" - - if command -v wget > /dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found wget ... using wget" - fi - wget "$jarUrl" -O "$wrapperJarPath" - elif command -v curl > /dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found curl ... using curl" - fi - curl -o "$wrapperJarPath" "$jarUrl" - else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Falling back to using Java to download" - fi - javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" - if [ -e "$javaClass" ]; then - if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Compiling MavenWrapperDownloader.java ..." - fi - # Compiling the Java class - ("$JAVA_HOME/bin/javac" "$javaClass") - fi - if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - # Running the downloader - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Running MavenWrapperDownloader.java ..." - fi - ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") - fi - fi - fi -fi -########################################################################################## -# End of extension -########################################################################################## - -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -if [ "$MVNW_VERBOSE" = true ]; then - echo $MAVEN_PROJECTBASEDIR -fi -MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" - -# For Cygwin, switch paths to Windows format before running java -if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` - [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` -fi - -WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -exec "$JAVACMD" \ - $MAVEN_OPTS \ - -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ - ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/backend/mvnw.cmd b/backend/mvnw.cmd deleted file mode 100755 index e5cfb0ae9..000000000 --- a/backend/mvnw.cmd +++ /dev/null @@ -1,161 +0,0 @@ -@REM ---------------------------------------------------------------------------- -@REM Licensed to the Apache Software Foundation (ASF) under one -@REM or more contributor license agreements. See the NOTICE file -@REM distributed with this work for additional information -@REM regarding copyright ownership. The ASF licenses this file -@REM to you under the Apache License, Version 2.0 (the -@REM "License"); you may not use this file except in compliance -@REM with the License. You may obtain a copy of the License at -@REM -@REM http://www.apache.org/licenses/LICENSE-2.0 -@REM -@REM Unless required by applicable law or agreed to in writing, -@REM software distributed under the License is distributed on an -@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -@REM KIND, either express or implied. See the License for the -@REM specific language governing permissions and limitations -@REM under the License. -@REM ---------------------------------------------------------------------------- - -@REM ---------------------------------------------------------------------------- -@REM Maven2 Start Up Batch script -@REM -@REM Required ENV vars: -@REM JAVA_HOME - location of a JDK home dir -@REM -@REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir -@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending -@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven -@REM e.g. to debug Maven itself, use -@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files -@REM ---------------------------------------------------------------------------- - -@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' -@echo off -@REM set title of command window -title %0 -@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' -@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% - -@REM set %HOME% to equivalent of $HOME -if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") - -@REM Execute a user defined script before this one -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre -@REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" -:skipRcPre - -@setlocal - -set ERROR_CODE=0 - -@REM To isolate internal variables from possible post scripts, we use another setlocal -@setlocal - -@REM ==== START VALIDATION ==== -if not "%JAVA_HOME%" == "" goto OkJHome - -echo. -echo Error: JAVA_HOME not found in your environment. >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -:OkJHome -if exist "%JAVA_HOME%\bin\java.exe" goto init - -echo. -echo Error: JAVA_HOME is set to an invalid directory. >&2 -echo JAVA_HOME = "%JAVA_HOME%" >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -@REM ==== END VALIDATION ==== - -:init - -@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". -@REM Fallback to current working directory if not found. - -set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% -IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir - -set EXEC_DIR=%CD% -set WDIR=%EXEC_DIR% -:findBaseDir -IF EXIST "%WDIR%"\.mvn goto baseDirFound -cd .. -IF "%WDIR%"=="%CD%" goto baseDirNotFound -set WDIR=%CD% -goto findBaseDir - -:baseDirFound -set MAVEN_PROJECTBASEDIR=%WDIR% -cd "%EXEC_DIR%" -goto endDetectBaseDir - -:baseDirNotFound -set MAVEN_PROJECTBASEDIR=%EXEC_DIR% -cd "%EXEC_DIR%" - -:endDetectBaseDir - -IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig - -@setlocal EnableExtensions EnableDelayedExpansion -for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a -@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% - -:endReadAdditionalConfig - -SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" -set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" -set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" -FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO ( - IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B -) - -@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -@REM This allows using the maven wrapper in projects that prohibit checking in binary data. -if exist %WRAPPER_JAR% ( - echo Found %WRAPPER_JAR% -) else ( - echo Couldn't find %WRAPPER_JAR%, downloading it ... - echo Downloading from: %DOWNLOAD_URL% - powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')" - echo Finished downloading %WRAPPER_JAR% -) -@REM End of extension - -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* -if ERRORLEVEL 1 goto error -goto end - -:error -set ERROR_CODE=1 - -:end -@endlocal & set ERROR_CODE=%ERROR_CODE% - -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost -@REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" -:skipRcPost - -@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause - -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% - -exit /B %ERROR_CODE% From 37e451140a0fefdb9fbeb9171810df9c93e0634e Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Tue, 28 Oct 2025 14:32:44 -0500 Subject: [PATCH 060/128] feat(api_keys): simplify API key listing endpoint and enhance expiration handling --- backend/mvnw | 286 +++++++++++++++++++++++++++++++++++++++++++++++ backend/mvnw.cmd | 161 ++++++++++++++++++++++++++ 2 files changed, 447 insertions(+) create mode 100755 backend/mvnw create mode 100755 backend/mvnw.cmd diff --git a/backend/mvnw b/backend/mvnw new file mode 100755 index 000000000..5551fde8e --- /dev/null +++ b/backend/mvnw @@ -0,0 +1,286 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven2 Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" + # TODO classpath? +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + wget "$jarUrl" -O "$wrapperJarPath" + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + curl -o "$wrapperJarPath" "$jarUrl" + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/backend/mvnw.cmd b/backend/mvnw.cmd new file mode 100755 index 000000000..e5cfb0ae9 --- /dev/null +++ b/backend/mvnw.cmd @@ -0,0 +1,161 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven2 Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" +FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + echo Found %WRAPPER_JAR% +) else ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')" + echo Finished downloading %WRAPPER_JAR% +) +@REM End of extension + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% From b59c8ad7a6eb77ac2d6537b2f2b0ab390c8ae316 Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Tue, 28 Oct 2025 14:37:48 -0500 Subject: [PATCH 061/128] feat(api_keys): simplify API key listing endpoint and enhance expiration handling --- backend/mvnw | 286 ----------------------------------------------- backend/mvnw.cmd | 161 -------------------------- 2 files changed, 447 deletions(-) delete mode 100755 backend/mvnw delete mode 100755 backend/mvnw.cmd diff --git a/backend/mvnw b/backend/mvnw deleted file mode 100755 index 5551fde8e..000000000 --- a/backend/mvnw +++ /dev/null @@ -1,286 +0,0 @@ -#!/bin/sh -# ---------------------------------------------------------------------------- -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you 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. -# ---------------------------------------------------------------------------- - -# ---------------------------------------------------------------------------- -# Maven2 Start Up Batch script -# -# Required ENV vars: -# ------------------ -# JAVA_HOME - location of a JDK home dir -# -# Optional ENV vars -# ----------------- -# M2_HOME - location of maven2's installed home dir -# MAVEN_OPTS - parameters passed to the Java VM when running Maven -# e.g. to debug Maven itself, use -# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -# MAVEN_SKIP_RC - flag to disable loading of mavenrc files -# ---------------------------------------------------------------------------- - -if [ -z "$MAVEN_SKIP_RC" ] ; then - - if [ -f /etc/mavenrc ] ; then - . /etc/mavenrc - fi - - if [ -f "$HOME/.mavenrc" ] ; then - . "$HOME/.mavenrc" - fi - -fi - -# OS specific support. $var _must_ be set to either true or false. -cygwin=false; -darwin=false; -mingw=false -case "`uname`" in - CYGWIN*) cygwin=true ;; - MINGW*) mingw=true;; - Darwin*) darwin=true - # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home - # See https://developer.apple.com/library/mac/qa/qa1170/_index.html - if [ -z "$JAVA_HOME" ]; then - if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" - else - export JAVA_HOME="/Library/Java/Home" - fi - fi - ;; -esac - -if [ -z "$JAVA_HOME" ] ; then - if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` - fi -fi - -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - PRG="$0" - - # need this for relative symlinks - while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG="`dirname "$PRG"`/$link" - fi - done - - saveddir=`pwd` - - M2_HOME=`dirname "$PRG"`/.. - - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` - - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi - -# For Cygwin, ensure paths are in UNIX format before anything is touched -if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` -fi - -# For Mingw, ensure paths are in UNIX format before anything is touched -if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" - # TODO classpath? -fi - -if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then - # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then - if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" - else - javaExecutable="`readlink -f \"$javaExecutable\"`" - fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` - JAVA_HOME="$javaHome" - export JAVA_HOME - fi - fi -fi - -if [ -z "$JAVACMD" ] ; then - if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - else - JAVACMD="`which java`" - fi -fi - -if [ ! -x "$JAVACMD" ] ; then - echo "Error: JAVA_HOME is not defined correctly." >&2 - echo " We cannot execute $JAVACMD" >&2 - exit 1 -fi - -if [ -z "$JAVA_HOME" ] ; then - echo "Warning: JAVA_HOME environment variable is not set." -fi - -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher - -# traverses directory structure from process work directory to filesystem root -# first directory with .mvn subdirectory is considered project base directory -find_maven_basedir() { - - if [ -z "$1" ] - then - echo "Path not specified to find_maven_basedir" - return 1 - fi - - basedir="$1" - wdir="$1" - while [ "$wdir" != '/' ] ; do - if [ -d "$wdir"/.mvn ] ; then - basedir=$wdir - break - fi - # workaround for JBEAP-8937 (on Solaris 10/Sparc) - if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` - fi - # end of workaround - done - echo "${basedir}" -} - -# concatenates all lines of a file -concat_lines() { - if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" - fi -} - -BASE_DIR=`find_maven_basedir "$(pwd)"` -if [ -z "$BASE_DIR" ]; then - exit 1; -fi - -########################################################################################## -# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -# This allows using the maven wrapper in projects that prohibit checking in binary data. -########################################################################################## -if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found .mvn/wrapper/maven-wrapper.jar" - fi -else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." - fi - jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" - while IFS="=" read key value; do - case "$key" in (wrapperUrl) jarUrl="$value"; break ;; - esac - done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" - if [ "$MVNW_VERBOSE" = true ]; then - echo "Downloading from: $jarUrl" - fi - wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" - - if command -v wget > /dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found wget ... using wget" - fi - wget "$jarUrl" -O "$wrapperJarPath" - elif command -v curl > /dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found curl ... using curl" - fi - curl -o "$wrapperJarPath" "$jarUrl" - else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Falling back to using Java to download" - fi - javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" - if [ -e "$javaClass" ]; then - if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Compiling MavenWrapperDownloader.java ..." - fi - # Compiling the Java class - ("$JAVA_HOME/bin/javac" "$javaClass") - fi - if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - # Running the downloader - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Running MavenWrapperDownloader.java ..." - fi - ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") - fi - fi - fi -fi -########################################################################################## -# End of extension -########################################################################################## - -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -if [ "$MVNW_VERBOSE" = true ]; then - echo $MAVEN_PROJECTBASEDIR -fi -MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" - -# For Cygwin, switch paths to Windows format before running java -if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` - [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` -fi - -WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -exec "$JAVACMD" \ - $MAVEN_OPTS \ - -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ - ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/backend/mvnw.cmd b/backend/mvnw.cmd deleted file mode 100755 index e5cfb0ae9..000000000 --- a/backend/mvnw.cmd +++ /dev/null @@ -1,161 +0,0 @@ -@REM ---------------------------------------------------------------------------- -@REM Licensed to the Apache Software Foundation (ASF) under one -@REM or more contributor license agreements. See the NOTICE file -@REM distributed with this work for additional information -@REM regarding copyright ownership. The ASF licenses this file -@REM to you under the Apache License, Version 2.0 (the -@REM "License"); you may not use this file except in compliance -@REM with the License. You may obtain a copy of the License at -@REM -@REM http://www.apache.org/licenses/LICENSE-2.0 -@REM -@REM Unless required by applicable law or agreed to in writing, -@REM software distributed under the License is distributed on an -@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -@REM KIND, either express or implied. See the License for the -@REM specific language governing permissions and limitations -@REM under the License. -@REM ---------------------------------------------------------------------------- - -@REM ---------------------------------------------------------------------------- -@REM Maven2 Start Up Batch script -@REM -@REM Required ENV vars: -@REM JAVA_HOME - location of a JDK home dir -@REM -@REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir -@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending -@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven -@REM e.g. to debug Maven itself, use -@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files -@REM ---------------------------------------------------------------------------- - -@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' -@echo off -@REM set title of command window -title %0 -@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' -@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% - -@REM set %HOME% to equivalent of $HOME -if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") - -@REM Execute a user defined script before this one -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre -@REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" -:skipRcPre - -@setlocal - -set ERROR_CODE=0 - -@REM To isolate internal variables from possible post scripts, we use another setlocal -@setlocal - -@REM ==== START VALIDATION ==== -if not "%JAVA_HOME%" == "" goto OkJHome - -echo. -echo Error: JAVA_HOME not found in your environment. >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -:OkJHome -if exist "%JAVA_HOME%\bin\java.exe" goto init - -echo. -echo Error: JAVA_HOME is set to an invalid directory. >&2 -echo JAVA_HOME = "%JAVA_HOME%" >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -@REM ==== END VALIDATION ==== - -:init - -@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". -@REM Fallback to current working directory if not found. - -set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% -IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir - -set EXEC_DIR=%CD% -set WDIR=%EXEC_DIR% -:findBaseDir -IF EXIST "%WDIR%"\.mvn goto baseDirFound -cd .. -IF "%WDIR%"=="%CD%" goto baseDirNotFound -set WDIR=%CD% -goto findBaseDir - -:baseDirFound -set MAVEN_PROJECTBASEDIR=%WDIR% -cd "%EXEC_DIR%" -goto endDetectBaseDir - -:baseDirNotFound -set MAVEN_PROJECTBASEDIR=%EXEC_DIR% -cd "%EXEC_DIR%" - -:endDetectBaseDir - -IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig - -@setlocal EnableExtensions EnableDelayedExpansion -for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a -@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% - -:endReadAdditionalConfig - -SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" -set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" -set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" -FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO ( - IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B -) - -@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -@REM This allows using the maven wrapper in projects that prohibit checking in binary data. -if exist %WRAPPER_JAR% ( - echo Found %WRAPPER_JAR% -) else ( - echo Couldn't find %WRAPPER_JAR%, downloading it ... - echo Downloading from: %DOWNLOAD_URL% - powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')" - echo Finished downloading %WRAPPER_JAR% -) -@REM End of extension - -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* -if ERRORLEVEL 1 goto error -goto end - -:error -set ERROR_CODE=1 - -:end -@endlocal & set ERROR_CODE=%ERROR_CODE% - -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost -@REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" -:skipRcPost - -@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause - -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% - -exit /B %ERROR_CODE% From fea3bc998b751969689b65d3a85d80b5744db0a2 Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Tue, 28 Oct 2025 14:39:25 -0500 Subject: [PATCH 062/128] feat(api_keys): simplify API key listing endpoint and enhance expiration handling --- backend/.gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/.gitignore b/backend/.gitignore index 518edd788..8de108186 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -138,6 +138,8 @@ Desktop.ini # Maven Wrapper ###################### !.mvn/wrapper/maven-wrapper.jar +mvnw +mvn.cmd ###################### # ESLint From cb44de99d0a18351305ed9d91352e28bd298350b Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Tue, 28 Oct 2025 14:40:04 -0500 Subject: [PATCH 063/128] feat(api_keys): simplify API key listing endpoint and enhance expiration handling --- backend/.gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/.gitignore b/backend/.gitignore index 8de108186..1e4c485c4 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -139,7 +139,7 @@ Desktop.ini ###################### !.mvn/wrapper/maven-wrapper.jar mvnw -mvn.cmd +mvnw.cmd ###################### # ESLint From abbbc63116dc8791aad9f548c8493d59dfb26e59 Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Tue, 28 Oct 2025 14:40:51 -0500 Subject: [PATCH 064/128] feat(api_keys): simplify API key listing endpoint and enhance expiration handling --- backend/.gitignore | 2 -- 1 file changed, 2 deletions(-) diff --git a/backend/.gitignore b/backend/.gitignore index 1e4c485c4..518edd788 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -138,8 +138,6 @@ Desktop.ini # Maven Wrapper ###################### !.mvn/wrapper/maven-wrapper.jar -mvnw -mvnw.cmd ###################### # ESLint From 14455adfa8a6d39d6ec6495be20543cd28439655 Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Tue, 28 Oct 2025 14:41:55 -0500 Subject: [PATCH 065/128] feat(api_keys): simplify API key listing endpoint and enhance expiration handling --- backend/mvnw | 286 +++++++++++++++++++++++++++++++++++++++++++++++ backend/mvnw.cmd | 161 ++++++++++++++++++++++++++ 2 files changed, 447 insertions(+) create mode 100644 backend/mvnw create mode 100644 backend/mvnw.cmd diff --git a/backend/mvnw b/backend/mvnw new file mode 100644 index 000000000..5551fde8e --- /dev/null +++ b/backend/mvnw @@ -0,0 +1,286 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven2 Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" + # TODO classpath? +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + wget "$jarUrl" -O "$wrapperJarPath" + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + curl -o "$wrapperJarPath" "$jarUrl" + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/backend/mvnw.cmd b/backend/mvnw.cmd new file mode 100644 index 000000000..e5cfb0ae9 --- /dev/null +++ b/backend/mvnw.cmd @@ -0,0 +1,161 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven2 Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" +FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + echo Found %WRAPPER_JAR% +) else ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')" + echo Finished downloading %WRAPPER_JAR% +) +@REM End of extension + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% From 3786d003d889f6daaf2d6cd1fe49e045736fb107 Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Tue, 28 Oct 2025 14:43:23 -0500 Subject: [PATCH 066/128] Update frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../shared/components/api-key-modal/api-key-modal.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts index 87289aee6..f2378c13d 100644 --- a/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts +++ b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts @@ -54,7 +54,7 @@ export class ApiKeyModalComponent implements OnInit { const trimmedIp = this.ipInput.trim(); if (!trimmedIp) { - this.ipInputError = 'Please enter an IP address or CIDR'; // Se asigna el error + this.ipInputError = 'Please enter an IP address or CIDR'; // Error is assigned return; } From 7029ca79ce5f570ab0b3ab731fcb4dfca82d8242 Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Tue, 28 Oct 2025 14:45:14 -0500 Subject: [PATCH 067/128] Update backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../java/com/park/utmstack/security/api_key/ApiKeyFilter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java b/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java index 576a2556a..185db7321 100644 --- a/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java +++ b/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java @@ -49,7 +49,7 @@ public class ApiKeyFilter extends OncePerRequestFilter { private final UserRepository userRepository; private final ApiKeyService apiKeyService; - private final ConcurrentMap invalidApiKeyBlackList = new ConcurrentHashMap<>();; + private final ConcurrentMap invalidApiKeyBlackList = new ConcurrentHashMap<>(); private final ConcurrentMap cidrCache = new ConcurrentHashMap<>(); private final ApplicationEventService applicationEventService; private final ApiKeyUsageLoggingService apiKeyUsageLoggingService; From 1420ceb92df439ae2be8d1aa5e63f8250567f88a Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Tue, 28 Oct 2025 14:46:12 -0500 Subject: [PATCH 068/128] feat(api_keys): simplify API key listing endpoint and enhance expiration handling --- .../java/com/park/utmstack/web/rest/api_key/ApiKeyResource.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/java/com/park/utmstack/web/rest/api_key/ApiKeyResource.java b/backend/src/main/java/com/park/utmstack/web/rest/api_key/ApiKeyResource.java index d0bdd0d2b..aa6b2052a 100644 --- a/backend/src/main/java/com/park/utmstack/web/rest/api_key/ApiKeyResource.java +++ b/backend/src/main/java/com/park/utmstack/web/rest/api_key/ApiKeyResource.java @@ -161,7 +161,7 @@ public ResponseEntity deleteApiKey(@PathVariable("id") Long apiKeyId) { return ResponseEntity.noContent().build(); } - @GetMapping("/usage") + @PostMapping("/usage") public ResponseEntity> search(@RequestBody(required = false) List filters, @RequestParam Integer top, @RequestParam String indexPattern, @RequestParam(required = false, defaultValue = "false") boolean includeChildren, From 1e43d5f181ec51a4e93f5db4c720894a1eb5bfde Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Wed, 29 Oct 2025 10:48:15 -0500 Subject: [PATCH 069/128] feat(api_keys): enhance clipboard functionality with fallback support and feedback Signed-off-by: Manuel Abascal --- .../api-keys/api-keys.component.ts | 47 +++++++++++++++++-- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/app-management/api-keys/api-keys.component.ts b/frontend/src/app/app-management/api-keys/api-keys.component.ts index f3ac14e08..82d9e3450 100644 --- a/frontend/src/app/app-management/api-keys/api-keys.component.ts +++ b/frontend/src/app/app-management/api-keys/api-keys.component.ts @@ -63,9 +63,50 @@ export class ApiKeysComponent implements OnInit { } copyToClipboard(): void { - (navigator as any).clipboard.writeText(this.generatedApiKey).then(() => { - this.copied = true; - }); + if (!this.generatedApiKey) { return; } + + if (navigator && (navigator as any).clipboard && (navigator as any).clipboard.writeText) { + (navigator as any).clipboard.writeText(this.generatedApiKey) + .then(() => this.copied = true) + .catch(err => { + console.error('Error al copiar con clipboard API', err); + this.fallbackCopy(this.generatedApiKey); + }); + } else { + this.fallbackCopy(this.generatedApiKey); + } + } + + private fallbackCopy(text: string): void { + try { + const textarea = document.createElement('textarea'); + textarea.value = text; + + textarea.style.position = 'fixed'; + textarea.style.top = '0'; + textarea.style.left = '0'; + textarea.style.opacity = '0'; + + document.body.appendChild(textarea); + textarea.focus(); + textarea.select(); + + const successful = document.execCommand('copy'); + document.body.removeChild(textarea); + + if (successful) { + this.showCopiedFeedback(); + } else { + console.warn('Fallback copy failed'); + } + } catch (err) { + console.error('Error en fallback copy', err); + } + } + + private showCopiedFeedback(): void { + this.copied = true; + setTimeout(() => this.copied = false, 2000); } openCreateModal(): void { From 2de870c25fd105e10ebbd38e7d94f5cab7b9e2ad Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Wed, 29 Oct 2025 10:52:59 -0500 Subject: [PATCH 070/128] feat(api_key): enhance ApiKeyFilter with improved logging and validation checks --- .../domain/shared_types/ApplicationLayer.java | 1 + .../security/api_key/ApiKeyFilter.java | 67 +++++++++++-------- 2 files changed, 41 insertions(+), 27 deletions(-) diff --git a/backend/src/main/java/com/park/utmstack/domain/shared_types/ApplicationLayer.java b/backend/src/main/java/com/park/utmstack/domain/shared_types/ApplicationLayer.java index 16bfac1cb..ed1a2af7f 100644 --- a/backend/src/main/java/com/park/utmstack/domain/shared_types/ApplicationLayer.java +++ b/backend/src/main/java/com/park/utmstack/domain/shared_types/ApplicationLayer.java @@ -7,6 +7,7 @@ @Getter public enum ApplicationLayer { SERVICE ("SERVICE"), + API ("API"), CONTROLLER ("CONTROLLER"); private final String value; diff --git a/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java b/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java index 185db7321..acefca689 100644 --- a/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java +++ b/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java @@ -3,6 +3,7 @@ import com.park.utmstack.config.Constants; import com.park.utmstack.domain.api_keys.ApiKey; +import com.park.utmstack.domain.shared_types.ApplicationLayer; import com.park.utmstack.loggin.api_key.ApiKeyUsageLoggingService; import com.park.utmstack.repository.UserRepository; import com.park.utmstack.service.api_key.ApiKeyService; @@ -31,6 +32,8 @@ import java.io.IOException; import java.time.Instant; import java.util.List; +import java.util.Map; +import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.regex.Pattern; @@ -44,9 +47,10 @@ public class ApiKeyFilter extends OncePerRequestFilter { private static final String LOG_USAGE_FLAG = "LOG_USAGE_DONE"; private static final Pattern CIDR_PATTERN = Pattern.compile( - "^((25[0-5]|2[0-4]\\d|[01]?\\d\\d?)(\\.(?!$)|$)){4}/(\\d|[1-2]\\d|3[0-2])$" + "^((25[0-5]|2[0-4]\\d|1\\d{2}|[1-9]?\\d)\\.){3}(25[0-5]|2[0-4]\\d|1\\d{2}|[1-9]?\\d)/(\\d|[1-2]\\d|3[0-2])$" ); + private final UserRepository userRepository; private final ApiKeyService apiKeyService; private final ConcurrentMap invalidApiKeyBlackList = new ConcurrentHashMap<>(); @@ -103,45 +107,54 @@ protected void doFilterInternal(@NonNull HttpServletRequest request, private ApiKey getApiKey(String apiKey) { if (invalidApiKeyBlackList.containsKey(apiKey)) { + log.info("API key invalid (cached)"); throw new ApiKeyInvalidAccessException("Invalid API key"); } + return apiKeyService.findOneByApiKey(apiKey) - .orElseThrow(() -> { - invalidApiKeyBlackList.put(apiKey, Boolean.TRUE); - return new ApiKeyInvalidAccessException("Invalid API key"); - }); + .orElseGet(() -> { + invalidApiKeyBlackList.put(apiKey, Boolean.TRUE); + log.info("API key invalid (not found)"); + throw new ApiKeyInvalidAccessException("Invalid API key"); + }); } public UsernamePasswordAuthenticationToken getAuthentication(ApiKey apiKey, String remoteIpAddress) { - try { - - if (!allowAccessToRemoteIp(apiKey.getAllowedIp(), remoteIpAddress)) { - throw new ApiKeyInvalidAccessException("Invalid IP address, if you recognize this IP address, add to allowed ip list"); - } + Objects.requireNonNull(apiKey, "API key must not be null"); + Objects.requireNonNull(remoteIpAddress, "Remote IP address must not be null"); + + if (!allowAccessToRemoteIp(apiKey.getAllowedIp(), remoteIpAddress)) { + log.warn("Access denied: IP [{}] not allowed for API key [{}]", remoteIpAddress, apiKey.getApiKey()); + throw new ApiKeyInvalidAccessException( + "Invalid IP address: " + remoteIpAddress + ". If you recognize this IP, add it to allowed IP list." + ); + } - if (apiKey.getExpiresAt() != null && !apiKey.getExpiresAt().isAfter(Instant.now())) { - throw new ApiKeyInvalidAccessException("API key expired at " + apiKey.getExpiresAt()); - } + if (apiKey.getExpiresAt() != null && !apiKey.getExpiresAt().isAfter(Instant.now())) { + log.warn("Access denied: API key [{}] expired at {}", apiKey.getApiKey(), apiKey.getExpiresAt()); + throw new ApiKeyInvalidAccessException("API key expired at " + apiKey.getExpiresAt()); + } - var userEntity = userRepository - .findById(apiKey.getUserId()) - .orElseThrow(() -> new ApiKeyInvalidAccessException("User not found for api key")); + var userEntityOpt = userRepository.findById(apiKey.getUserId()); + if (userEntityOpt.isEmpty()) { + log.warn("Access denied: User [{}] not found for API key [{}]", apiKey.getUserId(), apiKey.getApiKey()); + throw new ApiKeyInvalidAccessException("User not found for API key"); + } - if (!userEntity.getActivated()) { - throw new ApiKeyInvalidAccessException("User not activated"); - } + var userEntity = userEntityOpt.get(); - List authorities = userEntity.getAuthorities().stream() - .map(auth -> new SimpleGrantedAuthority(auth.getName())) - .toList(); + if (!userEntity.getActivated()) { + log.warn("Access denied: User [{}] not activated", userEntity.getLogin()); + throw new ApiKeyInvalidAccessException("User not activated"); + } - User principal = new User(userEntity.getLogin(), "", authorities); + List authorities = userEntity.getAuthorities().stream() + .map(auth -> new SimpleGrantedAuthority(auth.getName())) + .toList(); - return new UsernamePasswordAuthenticationToken(principal, apiKey.getApiKey(), authorities); + User principal = new User(userEntity.getLogin(), "", authorities); - } catch (Exception e) { - throw new ApiKeyInvalidAccessException(e.getMessage()); - } + return new UsernamePasswordAuthenticationToken(principal, apiKey.getApiKey(), authorities); } public boolean allowAccessToRemoteIp(String allowedIpList, String remoteIp) { From 1d34358bb9ccb22583819567500237192ffc3e93 Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Wed, 29 Oct 2025 11:06:00 -0500 Subject: [PATCH 071/128] feat(api_key): enhance ApiKeyFilter with improved logging and validation checks --- .../java/com/park/utmstack/security/api_key/ApiKeyFilter.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java b/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java index acefca689..8f5582528 100644 --- a/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java +++ b/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java @@ -107,14 +107,14 @@ protected void doFilterInternal(@NonNull HttpServletRequest request, private ApiKey getApiKey(String apiKey) { if (invalidApiKeyBlackList.containsKey(apiKey)) { - log.info("API key invalid (cached)"); + log.warn("Access attempt with invalid API key (cached)"); throw new ApiKeyInvalidAccessException("Invalid API key"); } return apiKeyService.findOneByApiKey(apiKey) .orElseGet(() -> { invalidApiKeyBlackList.put(apiKey, Boolean.TRUE); - log.info("API key invalid (not found)"); + log.warn("Access attempt with invalid API key (not found in DB)"); throw new ApiKeyInvalidAccessException("Invalid API key"); }); } From 5e587c86f78c34eb12fcb135d6e146815bc3e68b Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Wed, 29 Oct 2025 11:10:24 -0500 Subject: [PATCH 072/128] feat(api_key): enhance ApiKeyFilter with improved logging and validation checks --- .../java/com/park/utmstack/security/api_key/ApiKeyFilter.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java b/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java index 8f5582528..5e5fd9100 100644 --- a/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java +++ b/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java @@ -3,7 +3,6 @@ import com.park.utmstack.config.Constants; import com.park.utmstack.domain.api_keys.ApiKey; -import com.park.utmstack.domain.shared_types.ApplicationLayer; import com.park.utmstack.loggin.api_key.ApiKeyUsageLoggingService; import com.park.utmstack.repository.UserRepository; import com.park.utmstack.service.api_key.ApiKeyService; @@ -12,7 +11,6 @@ import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.net.util.SubnetUtils; -import org.springframework.core.annotation.Order; import org.springframework.lang.NonNull; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.authority.SimpleGrantedAuthority; @@ -32,7 +30,6 @@ import java.io.IOException; import java.time.Instant; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -55,7 +52,6 @@ public class ApiKeyFilter extends OncePerRequestFilter { private final ApiKeyService apiKeyService; private final ConcurrentMap invalidApiKeyBlackList = new ConcurrentHashMap<>(); private final ConcurrentMap cidrCache = new ConcurrentHashMap<>(); - private final ApplicationEventService applicationEventService; private final ApiKeyUsageLoggingService apiKeyUsageLoggingService; From 1722168e752d89a39ae4340a8fedaa2232aadb09 Mon Sep 17 00:00:00 2001 From: Yadian Llada Lopez Date: Fri, 31 Oct 2025 08:56:06 -0400 Subject: [PATCH 073/128] refactor(agent): remove unused TLS certificate validation and related functions; --- agent/main.go | 36 ------- agent/serv/service.go | 13 --- agent/utils/files.go | 17 ++++ .../utils/{integration_tls.go => int_tls.go} | 97 ------------------- 4 files changed, 17 insertions(+), 146 deletions(-) rename agent/utils/{integration_tls.go => int_tls.go} (67%) diff --git a/agent/main.go b/agent/main.go index 0ea3404c9..60bd7a575 100644 --- a/agent/main.go +++ b/agent/main.go @@ -3,7 +3,6 @@ package main import ( "fmt" "os" - "strings" "time" pb "github.com/utmstack/UTMStack/agent/agent" @@ -170,41 +169,6 @@ func main() { fmt.Println("TLS certificates loaded successfully!") time.Sleep(5 * time.Second) - case "check-tls-certs": - fmt.Println("Checking TLS certificates status ...") - - status := utils.GetTLSStatus( - config.IntegrationCertPath, - config.IntegrationKeyPath, - config.IntegrationCAPath, - ) - - fmt.Printf("Certificate file exists: %v\n", status.CertExists) - fmt.Printf("Private key file exists: %v\n", status.KeyExists) - fmt.Printf("CA certificate file exists: %v\n", status.CAExists) - fmt.Printf("TLS Available: %v\n", status.Available) - fmt.Printf("Certificates Valid: %v\n", status.Valid) - - if status.Error != "" { - fmt.Printf("Error: %s\n", status.Error) - } - - if status.Available { - fmt.Println("\n" + strings.Repeat("=", 50)) - details, err := utils.GetCertificateDetails(config.IntegrationCertPath) - if err == nil { - fmt.Println(details) - } - fmt.Println("TLS is ready for use!") - fmt.Println("Use: ./utmstack_agent enable-tls tcp") - } else { - fmt.Println("\n" + strings.Repeat("=", 50)) - fmt.Println("TLS is NOT available.") - fmt.Println("To enable TLS:") - fmt.Println(" Load your certificates: ./utmstack_agent load-tls-certs [ca]") - } - time.Sleep(5 * time.Second) - case "change-port": fmt.Println("Changing integration port ...") integration := os.Args[2] diff --git a/agent/serv/service.go b/agent/serv/service.go index ea8abdbab..71b021621 100644 --- a/agent/serv/service.go +++ b/agent/serv/service.go @@ -40,19 +40,6 @@ func (p *program) run() { utils.Logger.Fatal("error getting config: %v", err) } - if utils.CheckIfPathExist(config.IntegrationCertPath) && utils.CheckIfPathExist(config.IntegrationKeyPath) { - err = utils.ValidateIntegrationCertificates(config.IntegrationCertPath, config.IntegrationKeyPath) - if err != nil { - utils.Logger.ErrorF("TLS certificates are invalid: %v", err) - utils.Logger.Info("TLS functionality will be disabled. Use 'load-tls-certs' command to fix this.") - } else { - utils.Logger.Info("TLS certificates are valid and ready for use") - } - } else { - utils.Logger.Info("No TLS certificates found. TLS functionality is disabled.") - utils.Logger.Info("Use 'load-tls-certs' command to load your certificates.") - } - db := database.GetDB() err = db.Migrate(models.Log{}) if err != nil { diff --git a/agent/utils/files.go b/agent/utils/files.go index 977b5689c..51da99255 100644 --- a/agent/utils/files.go +++ b/agent/utils/files.go @@ -157,3 +157,20 @@ func IsDirEmpty(path string) (bool, error) { } return false, err } + +func copyFile(src, dst string) error { + sourceFile, err := os.Open(src) + if err != nil { + return err + } + defer sourceFile.Close() + + destFile, err := os.Create(dst) + if err != nil { + return err + } + defer destFile.Close() + + _, err = io.Copy(destFile, sourceFile) + return err +} diff --git a/agent/utils/integration_tls.go b/agent/utils/int_tls.go similarity index 67% rename from agent/utils/integration_tls.go rename to agent/utils/int_tls.go index 741974e9a..5ca5928f3 100644 --- a/agent/utils/integration_tls.go +++ b/agent/utils/int_tls.go @@ -4,12 +4,9 @@ import ( "crypto/rsa" "crypto/tls" "crypto/x509" - "encoding/pem" "fmt" - "io" "os" "path/filepath" - "strings" "time" ) @@ -170,97 +167,3 @@ func LoadUserCertificatesWithStruct(src, dest CertificateFiles) error { return nil } - -func GetTLSStatus(certPath, keyPath, caPath string) TLSStatus { - status := TLSStatus{ - CertExists: CheckIfPathExist(certPath), - KeyExists: CheckIfPathExist(keyPath), - CAExists: CheckIfPathExist(caPath), - } - - if status.CertExists && status.KeyExists { - err := ValidateIntegrationCertificates(certPath, keyPath) - if err == nil { - status.Available = true - status.Valid = true - } else { - status.Error = err.Error() - } - } else { - status.Error = "Certificate or private key files not found" - } - - return status -} - -func GetCertificateDetails(certPath string) (string, error) { - if !CheckIfPathExist(certPath) { - return "", fmt.Errorf("certificate file not found: %s", certPath) - } - - // Parse certificate directly - certData, err := os.ReadFile(certPath) - if err != nil { - return "", fmt.Errorf("error reading certificate: %w", err) - } - - block, _ := pem.Decode(certData) - if block == nil { - return "", fmt.Errorf("failed to decode certificate PEM") - } - - cert, err := x509.ParseCertificate(block.Bytes) - if err != nil { - return "", fmt.Errorf("failed to parse certificate: %w", err) - } - - return formatCertificateDetails(cert), nil -} - -func formatCertificateDetails(cert *x509.Certificate) string { - var builder strings.Builder - - builder.WriteString(fmt.Sprintf("Subject: %s\n", cert.Subject.String())) - builder.WriteString(fmt.Sprintf("Issuer: %s\n", cert.Issuer.String())) - builder.WriteString(fmt.Sprintf("Valid from: %s\n", cert.NotBefore.Format("2006-01-02 15:04:05 UTC"))) - builder.WriteString(fmt.Sprintf("Valid until: %s\n", cert.NotAfter.Format("2006-01-02 15:04:05 UTC"))) - builder.WriteString(fmt.Sprintf("Serial Number: %s\n", cert.SerialNumber.String())) - - if len(cert.DNSNames) > 0 || len(cert.IPAddresses) > 0 { - builder.WriteString("Subject Alternative Names: ") - - for i, dns := range cert.DNSNames { - if i > 0 { - builder.WriteString(", ") - } - builder.WriteString(dns) - } - - for i, ip := range cert.IPAddresses { - if i > 0 || len(cert.DNSNames) > 0 { - builder.WriteString(", ") - } - builder.WriteString(ip.String()) - } - builder.WriteString("\n") - } - - return builder.String() -} - -func copyFile(src, dst string) error { - sourceFile, err := os.Open(src) - if err != nil { - return err - } - defer sourceFile.Close() - - destFile, err := os.Create(dst) - if err != nil { - return err - } - defer destFile.Close() - - _, err = io.Copy(destFile, sourceFile) - return err -} From cd47f703da77f52d71d66a75b1cc7a1a1de033e7 Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Fri, 31 Oct 2025 09:04:46 -0500 Subject: [PATCH 074/128] Update frontend/src/app/core/auth/account.service.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- frontend/src/app/core/auth/account.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/core/auth/account.service.ts b/frontend/src/app/core/auth/account.service.ts index 02ec14cb5..194090805 100644 --- a/frontend/src/app/core/auth/account.service.ts +++ b/frontend/src/app/core/auth/account.service.ts @@ -37,8 +37,8 @@ export class AccountService { } checkPassword(password: string, uuid: string): Observable> { - const sanitized_password = encodeURIComponent(password) - return this.http.get(SERVER_API_URL + `api/check-credentials?password=${sanitized_password}&checkUUID=${uuid}`, { + const body = { password, checkUUID: uuid }; + return this.http.post(SERVER_API_URL + 'api/check-credentials', body, { observe: 'response', responseType: 'text' }); From 164c851aee5442e11ff53a260f056e597fcbd329 Mon Sep 17 00:00:00 2001 From: Yadian Llada Lopez Date: Fri, 31 Oct 2025 13:30:37 -0400 Subject: [PATCH 075/128] feat(agent): validate TLS certificates before enabling TLS for integrations --- agent/modules/configuration.go | 3 +++ agent/modules/syslog.go | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/agent/modules/configuration.go b/agent/modules/configuration.go index 2274c3535..dd5bf90e6 100644 --- a/agent/modules/configuration.go +++ b/agent/modules/configuration.go @@ -69,6 +69,9 @@ func ChangeIntegrationStatus(logTyp string, proto string, isEnabled bool, tlsOpt // Handle TLS configuration if specified if len(tlsOptions) > 0 && isEnabled { if tlsOptions[0] { + if !utils.CheckIfPathExist(config.IntegrationCertPath) || !utils.CheckIfPathExist(config.IntegrationKeyPath) { + return "", fmt.Errorf("TLS certificates not found. Please load certificates first") + } // Enable TLS integration.TCP.TLSEnabled = true mod := GetModule(logTyp) diff --git a/agent/modules/syslog.go b/agent/modules/syslog.go index e2ed0d776..1efdb31aa 100644 --- a/agent/modules/syslog.go +++ b/agent/modules/syslog.go @@ -98,6 +98,12 @@ func (m *SyslogModule) GetPort(proto string) string { func (m *SyslogModule) EnablePort(proto string, enableTLS bool) error { switch proto { case "tcp": + if enableTLS { + if !utils.CheckIfPathExist(config.IntegrationCertPath) || !utils.CheckIfPathExist(config.IntegrationKeyPath) { + return fmt.Errorf("TLS certificates not found. Please load certificates first") + } + } + m.TCPListener.TLSEnabled = enableTLS go m.enableTCP() return nil From c86d8de22298b45bb93ff3e5765208c60990a41d Mon Sep 17 00:00:00 2001 From: AlexSanchez-bit Date: Thu, 16 Oct 2025 10:56:40 -0400 Subject: [PATCH 076/128] fix[frontend](web_console): sanitized password parameter to admit all utf8 characters even url structure ones --- frontend/src/app/core/auth/account.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/core/auth/account.service.ts b/frontend/src/app/core/auth/account.service.ts index 194090805..02ec14cb5 100644 --- a/frontend/src/app/core/auth/account.service.ts +++ b/frontend/src/app/core/auth/account.service.ts @@ -37,8 +37,8 @@ export class AccountService { } checkPassword(password: string, uuid: string): Observable> { - const body = { password, checkUUID: uuid }; - return this.http.post(SERVER_API_URL + 'api/check-credentials', body, { + const sanitized_password = encodeURIComponent(password) + return this.http.get(SERVER_API_URL + `api/check-credentials?password=${sanitized_password}&checkUUID=${uuid}`, { observe: 'response', responseType: 'text' }); From e35286b4c4cee0fb923b97b75caddbb950c26147 Mon Sep 17 00:00:00 2001 From: AlexSanchez-bit Date: Fri, 17 Oct 2025 10:08:23 -0400 Subject: [PATCH 077/128] feat[backend](api-keys): added api keys dto, controllers and entities --- backend/.nvim/.env | 18 ++ backend/mvnw | 0 backend/mvnw.cmd | 0 .../domain/api_keys/UtmApiKeyModel.java | 50 +++++ .../dto/api_key/UtmApiKeyResponseDto.java | 36 ++++ .../dto/api_key/UtmApiKeyUpsertDto.java | 29 +++ .../rest/api-keys/UtmApiKeyController.java | 178 ++++++++++++++++++ .../changelog/20250917001_adding_api_keys.xml | 43 +++++ .../config/liquibase/scripts/tables.sql | 21 +++ 9 files changed, 375 insertions(+) create mode 100644 backend/.nvim/.env mode change 100644 => 100755 backend/mvnw mode change 100644 => 100755 backend/mvnw.cmd create mode 100644 backend/src/main/java/com/park/utmstack/domain/api_keys/UtmApiKeyModel.java create mode 100644 backend/src/main/java/com/park/utmstack/service/dto/api_key/UtmApiKeyResponseDto.java create mode 100644 backend/src/main/java/com/park/utmstack/service/dto/api_key/UtmApiKeyUpsertDto.java create mode 100644 backend/src/main/java/com/park/utmstack/web/rest/api-keys/UtmApiKeyController.java create mode 100644 backend/src/main/resources/config/liquibase/changelog/20250917001_adding_api_keys.xml diff --git a/backend/.nvim/.env b/backend/.nvim/.env new file mode 100644 index 000000000..628f21921 --- /dev/null +++ b/backend/.nvim/.env @@ -0,0 +1,18 @@ +revision=4 +AD_AUDIT_SERVICE=http://localhost:8081/api +DB_HOST=192.168.1.18 +DB_NAME=utmstack +DB_PASS=DF7JOMKyU6oNwmy4 +DB_PORT=5432 +DB_USER=postgres +ELASTICSEARCH_HOST=192.168.1.18 +ELASTICSEARCH_PORT=9200 +ENCRYPTION_KEY=nWkGxX1vRTDyZc1Jm59YUkR3KsUmbYrJ +EVENT_PROCESSOR_HOST=192.168.1.18 +EVENT_PROCESSOR_PORT=9002 +GRPC_AGENT_MANAGER_HOST=192.168.1.18 +GRPC_AGENT_MANAGER_PORT=9000 +INTERNAL_KEY=qNANPjjNm7eantt7sgld0iSWFFeGKz5i +LOGSTASH_URL=http://localhost:9600 +SERVER_NAME=UTM +SOC_AI_BASE_URL=http://localhost:8081/process diff --git a/backend/mvnw b/backend/mvnw old mode 100644 new mode 100755 diff --git a/backend/mvnw.cmd b/backend/mvnw.cmd old mode 100644 new mode 100755 diff --git a/backend/src/main/java/com/park/utmstack/domain/api_keys/UtmApiKeyModel.java b/backend/src/main/java/com/park/utmstack/domain/api_keys/UtmApiKeyModel.java new file mode 100644 index 000000000..8f532da04 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/domain/api_keys/UtmApiKeyModel.java @@ -0,0 +1,50 @@ +package com.park.utmstack.domain.api_keys; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.time.Instant; +import java.util.UUID; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "api_keys") +public class ApiKey implements Serializable { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + @Column(columnDefinition = "UUID") + private UUID id; + + @Column(nullable = false) + private UUID accountId; + + @Column(nullable = false) + private String name; + + @Column(nullable = false) + private String apiKey; + + @Column + private String allowedIp; + + @Column(nullable = false) + private Instant createdAt; + + private Instant generatedAt; + + @Column + private Instant expiresAt; +} diff --git a/backend/src/main/java/com/park/utmstack/service/dto/api_key/UtmApiKeyResponseDto.java b/backend/src/main/java/com/park/utmstack/service/dto/api_key/UtmApiKeyResponseDto.java new file mode 100644 index 000000000..232e77f56 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/service/dto/api_key/UtmApiKeyResponseDto.java @@ -0,0 +1,36 @@ +package com.utmstack.api.service.dto.apikey; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.Instant; +import java.util.List; +import java.util.UUID; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ApiKeyResponseDTO { + + @Schema(description = "Unique identifier of the API key") + private UUID id; + + @Schema(description = "User-friendly API key name") + private String name; + + @Schema(description = "Allowed IP address or IP range in CIDR notation (e.g., '192.168.1.100' or '192.168.1.0/24')") + private List allowedIp; + + @Schema(description = "API key creation timestamp") + private Instant createdAt; + + @Schema(description = "API key expiration timestamp (if applicable)") + private Instant expiresAt; + + @Schema(description = "Generated At") + private Instant generatedAt; +} diff --git a/backend/src/main/java/com/park/utmstack/service/dto/api_key/UtmApiKeyUpsertDto.java b/backend/src/main/java/com/park/utmstack/service/dto/api_key/UtmApiKeyUpsertDto.java new file mode 100644 index 000000000..518aa6e35 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/service/dto/api_key/UtmApiKeyUpsertDto.java @@ -0,0 +1,29 @@ +package com.utmstack.api.service.dto.apikey; + + +import com.utmstack.api.annotation.ValidIPOrCIDR; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.Instant; +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ApiKeyUpsertDTO { + @NotNull + @Schema(description = "API Key name", requiredMode = Schema.RequiredMode.REQUIRED) + private String name; + + @Schema(description = "Allowed IP address or IP range in CIDR notation (e.g., '192.168.1.100' or '192.168.1.0/24'). If null, no IP restrictions are applied.") + private List<@ValidIPOrCIDR String> allowedIp; + + @Schema(description = "Expiration timestamp of the API key") + private Instant expiresAt; +} diff --git a/backend/src/main/java/com/park/utmstack/web/rest/api-keys/UtmApiKeyController.java b/backend/src/main/java/com/park/utmstack/web/rest/api-keys/UtmApiKeyController.java new file mode 100644 index 000000000..27c48af2f --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/web/rest/api-keys/UtmApiKeyController.java @@ -0,0 +1,178 @@ +@RestController +@RequestMapping("/api/api-keys") +@PreAuthorize("hasAuthority(\"" + AuthoritiesConstants.USER + "\")") +@AllArgsConstructor +@Hidden +public class ApiKeyResource { + + private static final String CLASSNAME = "ApiKeyResource"; + private final Logger log = LoggerFactory.getLogger(ApiKeyResource.class); + + private final ApiKeyService apiKeyService; + private final UserService userService; + + private UUID getCurrentAccountId() throws CurrentUserLoginNotFoundException { + User user = userService.getUserWithAuthoritiesByLogin(SecurityUtils.currentUserLogin()); + return UUID.fromString(user.getAccountId()); + } + + @Operation(summary = "Create API key", + description = "Creates a new API key record using the provided settings. The plain text key is not generated at creation.") + @ApiResponses({ + @ApiResponse(responseCode = "201", description = "API key created successfully", + content = @Content(schema = @Schema(implementation = ApiKeyResponseDTO.class))), + @ApiResponse(responseCode = "409", description = "API key already exists", content = @Content), + @ApiResponse(responseCode = "500", description = "Internal server error", + content = @Content, headers = { + @Header(name = "X-App-Error", description = "Technical error details") + }) + }) + @PostMapping + public ResponseEntity createApiKey(@RequestBody ApiKeyUpsertDTO dto) { + final String ctx = CLASSNAME + ".createApiKey"; + try { + UUID accountId = getCurrentAccountId(); + ApiKeyResponseDTO responseDTO = apiKeyService.createApiKey(accountId, dto); + return ResponseEntity.status(HttpStatus.CREATED).body(responseDTO); + } catch (ApiKeyExistException e) { + return ResponseUtil.buildConflictResponse(e.getMessage()); + } catch (Exception e) { + String msg = ctx + ": " + e.getMessage(); + log.error(msg, e); + return ResponseUtil.buildInternalServerErrorResponse(msg); + } + } + + @Operation(summary = "Generate a new API key", + description = "Generates (or renews) a new random API key for the specified API key record. The plain text key is returned only once.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "API key generated successfully", + content = @Content(schema = @Schema(type = "string"))), + @ApiResponse(responseCode = "404", description = "API key not found", content = @Content), + @ApiResponse(responseCode = "500", description = "Internal server error", + content = @Content, headers = { + @Header(name = "X-App-Error", description = "Technical error details") + }) + }) + @PostMapping("/{id}/generate") + public ResponseEntity generateApiKey(@PathVariable("id") UUID apiKeyId) { + final String ctx = CLASSNAME + ".generateApiKey"; + try { + UUID accountId = getCurrentAccountId(); + String plainKey = apiKeyService.generateApiKey(accountId, apiKeyId); + return ResponseEntity.ok(plainKey); + } catch (ApiKeyNotFoundException e) { + return ResponseUtil.buildNotFoundResponse(e.getMessage()); + } catch (Exception e) { + String msg = ctx + ": " + e.getMessage(); + log.error(msg, e); + return ResponseUtil.buildInternalServerErrorResponse(msg); + } + } + + @Operation(summary = "Retrieve API key", + description = "Retrieves the API key details for the specified API key record.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "API key retrieved successfully", + content = @Content(schema = @Schema(implementation = ApiKeyResponseDTO.class))), + @ApiResponse(responseCode = "404", description = "API key not found", content = @Content), + @ApiResponse(responseCode = "500", description = "Internal server error", + content = @Content, headers = { + @Header(name = "X-App-Error", description = "Technical error details") + }) + }) + @GetMapping("/{id}") + public ResponseEntity getApiKey(@PathVariable("id") UUID apiKeyId) { + final String ctx = CLASSNAME + ".getApiKey"; + try { + UUID accountId = getCurrentAccountId(); + ApiKeyResponseDTO responseDTO = apiKeyService.getApiKey(accountId, apiKeyId); + return ResponseEntity.ok(responseDTO); + } catch (ApiKeyNotFoundException e) { + return ResponseUtil.buildNotFoundResponse(e.getMessage()); + } catch (Exception e) { + String msg = ctx + ": " + e.getMessage(); + log.error(msg, e); + return ResponseUtil.buildInternalServerErrorResponse(msg); + } + } + + @Operation(summary = "List API keys", + description = "Retrieves the API key list.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "API key retrieved successfully", + content = @Content(schema = @Schema(implementation = ApiKeyResponseDTO.class))), + @ApiResponse(responseCode = "404", description = "API key not found", content = @Content), + @ApiResponse(responseCode = "500", description = "Internal server error", + content = @Content, headers = { + @Header(name = "X-App-Error", description = "Technical error details") + }) + }) + @GetMapping("") + public ResponseEntity> listApiKeys(@ParameterObject Pageable pageable) { + final String ctx = CLASSNAME + ".listApiKeys"; + try { + UUID accountId = getCurrentAccountId(); + Page page = apiKeyService.listApiKeys(accountId, pageable); + HttpHeaders headers = PaginationUtil.generatePaginationHttpHeaders(ServletUriComponentsBuilder.fromCurrentRequest(), page); + return ResponseEntity.ok().headers(headers).body(page.getContent()); + } catch (Exception e) { + String msg = ctx + ": " + e.getMessage(); + log.error(msg, e); + return ResponseUtil.buildInternalServerErrorResponse(msg); + } + } + + @Operation(summary = "Update API key", + description = "Updates mutable fields (name, allowed IPs, expiration) for the specified API key record.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "API key updated successfully", + content = @Content(schema = @Schema(implementation = ApiKeyResponseDTO.class))), + @ApiResponse(responseCode = "404", description = "API key not found", content = @Content), + @ApiResponse(responseCode = "500", description = "Internal server error", + content = @Content, headers = { + @Header(name = "X-App-Error", description = "Technical error details") + }) + }) + @PutMapping("/{id}") + public ResponseEntity updateApiKey(@PathVariable("id") UUID apiKeyId, + @RequestBody ApiKeyUpsertDTO dto) { + final String ctx = CLASSNAME + ".updateApiKey"; + try { + UUID accountId = getCurrentAccountId(); + ApiKeyResponseDTO responseDTO = apiKeyService.updateApiKey(accountId, apiKeyId, dto); + return ResponseEntity.ok(responseDTO); + } catch (ApiKeyNotFoundException e) { + return ResponseUtil.buildNotFoundResponse(e.getMessage()); + } catch (Exception e) { + String msg = ctx + ": " + e.getMessage(); + log.error(msg, e); + return ResponseUtil.buildInternalServerErrorResponse(msg); + } + } + + @Operation(summary = "Delete API key", + description = "Deletes the specified API key record for the authenticated user.") + @ApiResponses({ + @ApiResponse(responseCode = "204", description = "API key deleted successfully", content = @Content), + @ApiResponse(responseCode = "404", description = "API key not found", content = @Content), + @ApiResponse(responseCode = "500", description = "Internal server error", + content = @Content, headers = { + @Header(name = "X-App-Error", description = "Technical error details") + }) + }) + @DeleteMapping("/{id}") + public ResponseEntity deleteApiKey(@PathVariable("id") UUID apiKeyId) { + final String ctx = CLASSNAME + ".deleteApiKey"; + try { + UUID accountId = getCurrentAccountId(); + apiKeyService.deleteApiKey(accountId, apiKeyId); + return ResponseEntity.noContent().build(); + } catch (ApiKeyNotFoundException e) { + return ResponseUtil.buildNotFoundResponse(e.getMessage()); + } catch (Exception e) { + String msg = ctx + ": " + e.getMessage(); + log.error(msg, e); + return ResponseUtil.buildInternalServerErrorResponse(msg); + } + } diff --git a/backend/src/main/resources/config/liquibase/changelog/20250917001_adding_api_keys.xml b/backend/src/main/resources/config/liquibase/changelog/20250917001_adding_api_keys.xml new file mode 100644 index 000000000..cb1c2cd75 --- /dev/null +++ b/backend/src/main/resources/config/liquibase/changelog/20250917001_adding_api_keys.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO api_keys (account_id, name, api_key, created_at) + SELECT account_id::uuid, 'DefaultApiKey', api_key, CURRENT_TIMESTAMP + FROM jhi_user + WHERE api_key IS NOT NULL; + + + + + + + diff --git a/backend/src/main/resources/config/liquibase/scripts/tables.sql b/backend/src/main/resources/config/liquibase/scripts/tables.sql index f50a695d1..629971010 100644 --- a/backend/src/main/resources/config/liquibase/scripts/tables.sql +++ b/backend/src/main/resources/config/liquibase/scripts/tables.sql @@ -12,6 +12,7 @@ DROP TABLE IF EXISTS public.utm_gvm_scan_result; DROP TABLE IF EXISTS public.utm_gvm_task; DROP TABLE IF EXISTS public.utm_module_modal; DROP TABLE IF EXISTS public.utm_system_restart; +DROP TABLE IF EXISTS public.utm_api_keys; CREATE TABLE IF NOT EXISTS public.jhi_authority ( @@ -830,3 +831,23 @@ CREATE TABLE IF NOT EXISTS public.utm_space_notification_control next_notification timestamp without time zone NOT NULL, CONSTRAINT utm_space_notification_control_pkey PRIMARY KEY (id) ); + +CREATE TABLE IF NOT EXISTS public.utm_api_keys +( + id uuid default uuid_generate_v4() not null + primary key, + account_id uuid not null, + name varchar(255) not null, + api_key varchar(255) not null, + allowed_ip text, + created_at timestamp not null, + expires_at timestamp, + generated_at timestamp +); + +alter table utm_api_keys + owner to postgres; + +create unique index uk_api_keys_api_key + on utm_api_keys (api_key); + From 2ab5f1493ba7a0ed4a2038abc333aede294269d3 Mon Sep 17 00:00:00 2001 From: AlexSanchez-bit Date: Fri, 17 Oct 2025 11:48:55 -0400 Subject: [PATCH 078/128] feat[backend](api_keys): added api keys --- .../{UtmApiKeyModel.java => ApiKey.java} | 0 .../service/api_key/ApiKeyService.java | 212 ++++++++++++++++++ ...esponseDto.java => ApiKeyResponseDTO.java} | 0 ...KeyUpsertDto.java => ApiKeyUpsertDTO.java} | 0 .../rest/api-keys/UtmApiKeyController.java | 1 + 5 files changed, 213 insertions(+) rename backend/src/main/java/com/park/utmstack/domain/api_keys/{UtmApiKeyModel.java => ApiKey.java} (100%) create mode 100644 backend/src/main/java/com/park/utmstack/service/api_key/ApiKeyService.java rename backend/src/main/java/com/park/utmstack/service/dto/api_key/{UtmApiKeyResponseDto.java => ApiKeyResponseDTO.java} (100%) rename backend/src/main/java/com/park/utmstack/service/dto/api_key/{UtmApiKeyUpsertDto.java => ApiKeyUpsertDTO.java} (100%) diff --git a/backend/src/main/java/com/park/utmstack/domain/api_keys/UtmApiKeyModel.java b/backend/src/main/java/com/park/utmstack/domain/api_keys/ApiKey.java similarity index 100% rename from backend/src/main/java/com/park/utmstack/domain/api_keys/UtmApiKeyModel.java rename to backend/src/main/java/com/park/utmstack/domain/api_keys/ApiKey.java diff --git a/backend/src/main/java/com/park/utmstack/service/api_key/ApiKeyService.java b/backend/src/main/java/com/park/utmstack/service/api_key/ApiKeyService.java new file mode 100644 index 000000000..775d37f7d --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/service/api_key/ApiKeyService.java @@ -0,0 +1,212 @@ +package com.utmstack.api.service; + +import com.utmstack.api.domain.User; +import com.utmstack.api.domain.api_key.ApiKey; +import com.utmstack.api.domain.api_key.ApiKeyUsageLog_; +import com.utmstack.api.domain.api_key.index.ApiKeyUsageLogIndexDocument; +import com.utmstack.api.domain.enumeration.NotificationMessageKeyEnum; +import com.utmstack.api.repository.elasticsearch.ApiKeyUsageLogRepository; +import com.utmstack.api.repository.jpa.ApiKeyRepository; +import com.utmstack.api.repository.jpa.UserRepository; +import com.utmstack.api.service.criteria.api_key.ApiKeyUsageLogCriteria; +import com.utmstack.api.service.dto.SearchHitsResponseDTO; +import com.utmstack.api.service.dto.api_key.ApiKeyResponseDTO; +import com.utmstack.api.service.dto.api_key.ApiKeyUpsertDTO; +import com.utmstack.api.service.exceptions.ApiKeyExistException; +import com.utmstack.api.service.exceptions.ApiKeyNotFoundException; +import com.utmstack.api.service.mapper.ApiKeyMapper; +import com.utmstack.api.service.user.UserNotificationService; +import lombok.AllArgsConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.elasticsearch.core.ElasticsearchOperations; +import org.springframework.data.elasticsearch.core.SearchHit; +import org.springframework.data.elasticsearch.core.SearchHits; +import org.springframework.data.elasticsearch.core.query.Criteria; +import org.springframework.data.elasticsearch.core.query.CriteriaQuery; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import java.security.SecureRandom; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Base64; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Collectors; + +@Service +@AllArgsConstructor +public class ApiKeyService { + private static final String CLASSNAME = "ApiKeyService"; + private final Logger log = LoggerFactory.getLogger(ApiKeyService.class); + private final ApiKeyRepository apiKeyRepository; + private final ApiKeyMapper apiKeyMapper; + private final ApiKeyUsageLogRepository apiUsageLogRepository; + private final ElasticsearchOperations elasticsearchOperations; + private final UserRepository userRepository; + private final UserNotificationService userNotificationService; + private final MailService mailService; + + + public ApiKeyResponseDTO createApiKey(UUID accountId, ApiKeyUpsertDTO dto) { + final String ctx = CLASSNAME + ".createApiKey"; + try { + apiKeyRepository.findByNameAndAccountId(dto.getName(), accountId) + .ifPresent(apiKey -> { + throw new ApiKeyExistException("Api key already exists"); + }); + var apiKey = api_key.builder() + .accountId(accountId) + .name(dto.getName()) + .expiresAt(dto.getExpiresAt()) + .allowedIp(String.join(",", dto.getAllowedIp())) + .createdAt(Instant.now()) + .apiKey(generateRandomKey()) + .build(); + return apiKeyMapper.toDto(apiKeyRepository.save(apiKey)); + } catch (Exception e) { + throw new RuntimeException(ctx + ": " + e.getMessage()); + } + } + + public String generateApiKey(UUID accountId, UUID apiKeyId) { + final String ctx = CLASSNAME + ".generateApiKey"; + try { + ApiKey apiKey = apiKeyRepository.findByIdAndAccountId(apiKeyId, accountId) + .orElseThrow(() -> new ApiKeyNotFoundException("API key not found")); + String plainKey = generateRandomKey(); + api_key.setApiKey(plainKey); + api_key.setGeneratedAt(Instant.now()); + apiKeyRepository.save(apiKey); + return plainKey; + } catch (Exception e) { + throw new RuntimeException(ctx + ": " + e.getMessage()); + } + } + + public ApiKeyResponseDTO updateApiKey(UUID accountId, UUID apiKeyId, ApiKeyUpsertDTO dto) { + final String ctx = CLASSNAME + ".updateApiKey"; + try { + ApiKey apiKey = apiKeyRepository.findByIdAndAccountId(apiKeyId, accountId) + .orElseThrow(() -> new ApiKeyNotFoundException("API key not found")); + api_key.setName(dto.getName()); + if (dto.getAllowedIp() != null) { + api_key.setAllowedIp(String.join(",", dto.getAllowedIp())); + } else { + api_key.setAllowedIp(null); + } + api_key.setExpiresAt(dto.getExpiresAt()); + ApiKey updated = apiKeyRepository.save(apiKey); + return apiKeyMapper.toDto(updated); + } catch (Exception e) { + throw new RuntimeException(ctx + ": " + e.getMessage()); + } + } + + public ApiKeyResponseDTO getApiKey(UUID accountId, UUID apiKeyId) { + final String ctx = CLASSNAME + ".getApiKey"; + try { + ApiKey apiKey = apiKeyRepository.findByIdAndAccountId(apiKeyId, accountId) + .orElseThrow(() -> new ApiKeyNotFoundException("API key not found")); + return apiKeyMapper.toDto(apiKey); + } catch (Exception e) { + throw new RuntimeException(ctx + ": " + e.getMessage()); + } + } + + public Page listApiKeys(UUID accountId, Pageable pageable) { + final String ctx = CLASSNAME + ".listApiKeys"; + try { + return apiKeyRepository.findByAccountId(accountId, pageable).map(apiKeyMapper::toDto); + } catch (Exception e) { + throw new RuntimeException(ctx + ": " + e.getMessage()); + } + } + + + public void deleteApiKey(UUID accountId, UUID apiKeyId) { + final String ctx = CLASSNAME + ".deleteApiKey"; + try { + ApiKey apiKey = apiKeyRepository.findByIdAndAccountId(apiKeyId, accountId) + .orElseThrow(() -> new ApiKeyNotFoundException("API key not found")); + apiKeyRepository.delete(apiKey); + } catch (Exception e) { + throw new RuntimeException(ctx + ": " + e.getMessage()); + } + } + + private String generateRandomKey() { + final String ctx = CLASSNAME + ".generateRandomKey"; + try { + SecureRandom random = new SecureRandom(); + byte[] keyBytes = new byte[32]; + random.nextBytes(keyBytes); + return Base64.getUrlEncoder().withoutPadding().encodeToString(keyBytes); + } catch (Exception e) { + throw new RuntimeException(ctx + ": " + e.getMessage()); + } + } + + @Async + public void logUsage(ApiKeyUsageLogIndexDocument apiKeyUsageLog) { + final String ctx = CLASSNAME + ".logUsage"; + try { + apiUsageLogRepository.save(apiKeyUsageLog); + } catch (Exception e) { + log.error(ctx + ": {}", e.getMessage()); + } + } + + public Optional findOneByApiKey(String apiKey) { + return apiKeyRepository.findOneByApiKey(apiKey); + } + + public SearchHitsResponseDTO getApiKeyUsageLogs(User user, + ApiKeyUsageLogCriteria criteria, + Pageable pageable) { + final String ctx = CLASSNAME + ".getApiKeyUsageLogs"; + try { + CriteriaQuery query = new CriteriaQuery(criteria != null ? criteria.toCriteriaQuery() : new Criteria(), pageable); + query.addCriteria(new Criteria(ApiKeyUsageLog_.accountId).is(user.getAccountId())); + SearchHits result = elasticsearchOperations.search(query, ApiKeyUsageLogIndexDocument.class); + return SearchHitsResponseDTO.builder() + .totalHits(result.getTotalHits()) + .items(result.stream() + .map(SearchHit::getContent) + .collect(Collectors.toList())) + .build(); + } catch (Exception e) { + throw new RuntimeException(ctx + ": " + e.getMessage()); + } + } + + @Scheduled(cron = "0 0 9 * * ?") + public void checkExpiringApiKeys() { + Instant fiveDaysFromNow = Instant.now().plus(5, ChronoUnit.DAYS); + Instant now = Instant.now(); + List expiringKeys = apiKeyRepository.findAllByExpiresAtAfterAndExpiresAtLessThanEqual(now, fiveDaysFromNow); + + if (!expiringKeys.isEmpty()) { + Map> expiringKeysByAccount = expiringKeys.stream() + .collect(Collectors.groupingBy(ApiKey::getAccountId)); + + expiringKeysByAccount.forEach((accountId, apiKeys) -> { + var principal = userRepository.findByAccountIdAndAccountOwnerIsTrue(accountId.toString()).orElse(null); + if (principal == null) { + return; + } + mailService.sendKeyExpirationEmail(principal, apiKeys); + + userNotificationService.createAndSendNotification(principal.getUuid(), + NotificationMessageKeyEnum.API_KEY_EXPIRATION, + Map.of("names", apiKeys.stream().map(ApiKey::getName).collect(Collectors.joining(",")))); + }); + } + } +} diff --git a/backend/src/main/java/com/park/utmstack/service/dto/api_key/UtmApiKeyResponseDto.java b/backend/src/main/java/com/park/utmstack/service/dto/api_key/ApiKeyResponseDTO.java similarity index 100% rename from backend/src/main/java/com/park/utmstack/service/dto/api_key/UtmApiKeyResponseDto.java rename to backend/src/main/java/com/park/utmstack/service/dto/api_key/ApiKeyResponseDTO.java diff --git a/backend/src/main/java/com/park/utmstack/service/dto/api_key/UtmApiKeyUpsertDto.java b/backend/src/main/java/com/park/utmstack/service/dto/api_key/ApiKeyUpsertDTO.java similarity index 100% rename from backend/src/main/java/com/park/utmstack/service/dto/api_key/UtmApiKeyUpsertDto.java rename to backend/src/main/java/com/park/utmstack/service/dto/api_key/ApiKeyUpsertDTO.java diff --git a/backend/src/main/java/com/park/utmstack/web/rest/api-keys/UtmApiKeyController.java b/backend/src/main/java/com/park/utmstack/web/rest/api-keys/UtmApiKeyController.java index 27c48af2f..253fabc76 100644 --- a/backend/src/main/java/com/park/utmstack/web/rest/api-keys/UtmApiKeyController.java +++ b/backend/src/main/java/com/park/utmstack/web/rest/api-keys/UtmApiKeyController.java @@ -176,3 +176,4 @@ public ResponseEntity deleteApiKey(@PathVariable("id") UUID apiKeyId) { return ResponseUtil.buildInternalServerErrorResponse(msg); } } +} From 4fbec89e944b0323b293129c9729cc97c16e6139 Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Fri, 17 Oct 2025 11:02:50 -0500 Subject: [PATCH 079/128] feat(api_keys): create api_keys table with user_id and add foreign key constraint --- .../park/utmstack/domain/api_keys/ApiKey.java | 9 ++----- .../changelog/20250917001_adding_api_keys.xml | 25 ++++++++----------- .../resources/config/liquibase/master.xml | 2 ++ 3 files changed, 15 insertions(+), 21 deletions(-) diff --git a/backend/src/main/java/com/park/utmstack/domain/api_keys/ApiKey.java b/backend/src/main/java/com/park/utmstack/domain/api_keys/ApiKey.java index 8f532da04..8d0585a3d 100644 --- a/backend/src/main/java/com/park/utmstack/domain/api_keys/ApiKey.java +++ b/backend/src/main/java/com/park/utmstack/domain/api_keys/ApiKey.java @@ -1,16 +1,11 @@ package com.park.utmstack.domain.api_keys; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.Table; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import javax.persistence.*; import java.io.Serializable; import java.time.Instant; import java.util.UUID; @@ -29,7 +24,7 @@ public class ApiKey implements Serializable { private UUID id; @Column(nullable = false) - private UUID accountId; + private Long userId; @Column(nullable = false) private String name; diff --git a/backend/src/main/resources/config/liquibase/changelog/20250917001_adding_api_keys.xml b/backend/src/main/resources/config/liquibase/changelog/20250917001_adding_api_keys.xml index cb1c2cd75..2b996f83c 100644 --- a/backend/src/main/resources/config/liquibase/changelog/20250917001_adding_api_keys.xml +++ b/backend/src/main/resources/config/liquibase/changelog/20250917001_adding_api_keys.xml @@ -4,12 +4,12 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd"> - + - + @@ -24,20 +24,17 @@ + - - - - INSERT INTO api_keys (account_id, name, api_key, created_at) - SELECT account_id::uuid, 'DefaultApiKey', api_key, CURRENT_TIMESTAMP - FROM jhi_user - WHERE api_key IS NOT NULL; - - - - - + + diff --git a/backend/src/main/resources/config/liquibase/master.xml b/backend/src/main/resources/config/liquibase/master.xml index 5201db735..b7ab36816 100644 --- a/backend/src/main/resources/config/liquibase/master.xml +++ b/backend/src/main/resources/config/liquibase/master.xml @@ -253,5 +253,7 @@ + + From 0342cfb1d1ef4f870e44b80993afede0b751adf0 Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Fri, 17 Oct 2025 12:13:17 -0500 Subject: [PATCH 080/128] feat(api_keys): implement API key management with CRUD operations and validation --- .../com/park/utmstack/config/Constants.java | 1 + .../domain/api_keys/ApiKeyUsageLog.java | 40 ++++++ .../repository/api_key/ApiKeyRepository.java | 29 ++++ .../service/api_key/ApiKeyService.java | 127 +++++++----------- .../dto/api_key/ApiKeyResponseDTO.java | 2 +- .../service/dto/api_key/ApiKeyUpsertDTO.java | 7 +- .../utmstack/service/mapper/ApiKeyMapper.java | 31 +++++ .../util/exceptions/ApiKeyExistException.java | 7 + .../ApiKeyInvalidAccessException.java | 9 ++ .../exceptions/ApiKeyNotFoundException.java | 7 + .../validation/api_key/ValidIPOrCIDR.java | 23 ++++ .../api_key/ValidIPOrCIDRValidator.java | 37 +++++ .../ApiKeyResource.java} | 72 ++++++++++ ... => 20251017001_create_api_keys_table.xml} | 0 .../resources/config/liquibase/master.xml | 2 +- 15 files changed, 308 insertions(+), 86 deletions(-) create mode 100644 backend/src/main/java/com/park/utmstack/domain/api_keys/ApiKeyUsageLog.java create mode 100644 backend/src/main/java/com/park/utmstack/repository/api_key/ApiKeyRepository.java create mode 100644 backend/src/main/java/com/park/utmstack/service/mapper/ApiKeyMapper.java create mode 100644 backend/src/main/java/com/park/utmstack/util/exceptions/ApiKeyExistException.java create mode 100644 backend/src/main/java/com/park/utmstack/util/exceptions/ApiKeyInvalidAccessException.java create mode 100644 backend/src/main/java/com/park/utmstack/util/exceptions/ApiKeyNotFoundException.java create mode 100644 backend/src/main/java/com/park/utmstack/validation/api_key/ValidIPOrCIDR.java create mode 100644 backend/src/main/java/com/park/utmstack/validation/api_key/ValidIPOrCIDRValidator.java rename backend/src/main/java/com/park/utmstack/web/rest/{api-keys/UtmApiKeyController.java => api_key/ApiKeyResource.java} (70%) rename backend/src/main/resources/config/liquibase/changelog/{20250917001_adding_api_keys.xml => 20251017001_create_api_keys_table.xml} (100%) diff --git a/backend/src/main/java/com/park/utmstack/config/Constants.java b/backend/src/main/java/com/park/utmstack/config/Constants.java index 61d6c52a9..f69adf682 100644 --- a/backend/src/main/java/com/park/utmstack/config/Constants.java +++ b/backend/src/main/java/com/park/utmstack/config/Constants.java @@ -137,6 +137,7 @@ public final class Constants { // Defines the index pattern for querying Elasticsearch statistics indexes. // ---------------------------------------------------------------------------------- public static final String STATISTICS_INDEX_PATTERN = "v11-statistics-*"; + public static final String V11_API_ACCESS_LOGS = "v11-api-access-logs-*"; // Logging public static final String TRACE_ID_KEY = "traceId"; diff --git a/backend/src/main/java/com/park/utmstack/domain/api_keys/ApiKeyUsageLog.java b/backend/src/main/java/com/park/utmstack/domain/api_keys/ApiKeyUsageLog.java new file mode 100644 index 000000000..d097aa4fb --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/domain/api_keys/ApiKeyUsageLog.java @@ -0,0 +1,40 @@ +package com.park.utmstack.domain.api_keys; + + +import lombok.*; +import java.time.Instant; +import java.util.UUID; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class ApiKeyUsageLog { + + private UUID id; + + private UUID apiKeyId; + + private String apiKeyName; + + private Long userId; + + private Instant timestamp; + + private String endpoint; + + private String address; + + private String errorMessage; + + private String queryParams; + + private String payload; + + private String userAgent; + + private String httpMethod; + + private Integer statusCode; +} diff --git a/backend/src/main/java/com/park/utmstack/repository/api_key/ApiKeyRepository.java b/backend/src/main/java/com/park/utmstack/repository/api_key/ApiKeyRepository.java new file mode 100644 index 000000000..e76001064 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/repository/api_key/ApiKeyRepository.java @@ -0,0 +1,29 @@ +package com.park.utmstack.repository.api_key; + +import com.park.utmstack.domain.api_keys.ApiKey; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import javax.validation.constraints.NotNull; +import java.time.Instant; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +@Repository +public interface ApiKeyRepository extends JpaRepository { + + Optional findByIdAndUserId(UUID id, Long userId); + + Page findByUserId(Long userId, Pageable pageable); + + @Cacheable(cacheNames = "apikey", key = "#root.args[0]") + Optional findOneByApiKey(@NotNull String apiKey); + + Optional findByNameAndUserId(@NotNull String name, Long userId); + + List findAllByExpiresAtAfterAndExpiresAtLessThanEqual(Instant now, Instant fiveDaysFromNow); +} diff --git a/backend/src/main/java/com/park/utmstack/service/api_key/ApiKeyService.java b/backend/src/main/java/com/park/utmstack/service/api_key/ApiKeyService.java index 775d37f7d..cb402e356 100644 --- a/backend/src/main/java/com/park/utmstack/service/api_key/ApiKeyService.java +++ b/backend/src/main/java/com/park/utmstack/service/api_key/ApiKeyService.java @@ -1,88 +1,73 @@ -package com.utmstack.api.service; - -import com.utmstack.api.domain.User; -import com.utmstack.api.domain.api_key.ApiKey; -import com.utmstack.api.domain.api_key.ApiKeyUsageLog_; -import com.utmstack.api.domain.api_key.index.ApiKeyUsageLogIndexDocument; -import com.utmstack.api.domain.enumeration.NotificationMessageKeyEnum; -import com.utmstack.api.repository.elasticsearch.ApiKeyUsageLogRepository; -import com.utmstack.api.repository.jpa.ApiKeyRepository; -import com.utmstack.api.repository.jpa.UserRepository; -import com.utmstack.api.service.criteria.api_key.ApiKeyUsageLogCriteria; -import com.utmstack.api.service.dto.SearchHitsResponseDTO; -import com.utmstack.api.service.dto.api_key.ApiKeyResponseDTO; -import com.utmstack.api.service.dto.api_key.ApiKeyUpsertDTO; -import com.utmstack.api.service.exceptions.ApiKeyExistException; -import com.utmstack.api.service.exceptions.ApiKeyNotFoundException; -import com.utmstack.api.service.mapper.ApiKeyMapper; -import com.utmstack.api.service.user.UserNotificationService; +package com.park.utmstack.service.api_key; + +import com.park.utmstack.domain.api_keys.ApiKey; +import com.park.utmstack.domain.api_keys.ApiKeyUsageLog; +import com.park.utmstack.repository.api_key.ApiKeyRepository; +import com.park.utmstack.service.dto.api_key.ApiKeyResponseDTO; +import com.park.utmstack.service.dto.api_key.ApiKeyUpsertDTO; +import com.park.utmstack.service.elasticsearch.OpensearchClientBuilder; +import com.park.utmstack.service.mapper.ApiKeyMapper; +import com.park.utmstack.util.exceptions.ApiKeyExistException; +import com.park.utmstack.util.exceptions.ApiKeyNotFoundException; import lombok.AllArgsConstructor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; -import org.springframework.data.elasticsearch.core.ElasticsearchOperations; -import org.springframework.data.elasticsearch.core.SearchHit; -import org.springframework.data.elasticsearch.core.SearchHits; -import org.springframework.data.elasticsearch.core.query.Criteria; -import org.springframework.data.elasticsearch.core.query.CriteriaQuery; + import org.springframework.scheduling.annotation.Async; -import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import java.security.SecureRandom; import java.time.Instant; -import java.time.temporal.ChronoUnit; import java.util.Base64; -import java.util.List; -import java.util.Map; import java.util.Optional; import java.util.UUID; -import java.util.stream.Collectors; + +import static com.park.utmstack.config.Constants.V11_API_ACCESS_LOGS; @Service @AllArgsConstructor public class ApiKeyService { + private static final String CLASSNAME = "ApiKeyService"; private final Logger log = LoggerFactory.getLogger(ApiKeyService.class); private final ApiKeyRepository apiKeyRepository; private final ApiKeyMapper apiKeyMapper; - private final ApiKeyUsageLogRepository apiUsageLogRepository; - private final ElasticsearchOperations elasticsearchOperations; - private final UserRepository userRepository; - private final UserNotificationService userNotificationService; - private final MailService mailService; + private final OpensearchClientBuilder client; - public ApiKeyResponseDTO createApiKey(UUID accountId, ApiKeyUpsertDTO dto) { + public ApiKeyResponseDTO createApiKey(Long userId, ApiKeyUpsertDTO dto) { final String ctx = CLASSNAME + ".createApiKey"; try { - apiKeyRepository.findByNameAndAccountId(dto.getName(), accountId) + apiKeyRepository.findByNameAndUserId(dto.getName(), userId) .ifPresent(apiKey -> { throw new ApiKeyExistException("Api key already exists"); }); - var apiKey = api_key.builder() - .accountId(accountId) + + var apiKey = ApiKey.builder() + .userId(userId) .name(dto.getName()) .expiresAt(dto.getExpiresAt()) .allowedIp(String.join(",", dto.getAllowedIp())) .createdAt(Instant.now()) .apiKey(generateRandomKey()) .build(); + return apiKeyMapper.toDto(apiKeyRepository.save(apiKey)); } catch (Exception e) { throw new RuntimeException(ctx + ": " + e.getMessage()); } } - public String generateApiKey(UUID accountId, UUID apiKeyId) { + public String generateApiKey(Long userId, UUID apiKeyId) { final String ctx = CLASSNAME + ".generateApiKey"; try { - ApiKey apiKey = apiKeyRepository.findByIdAndAccountId(apiKeyId, accountId) + ApiKey apiKey = apiKeyRepository.findByIdAndUserId(apiKeyId, userId) .orElseThrow(() -> new ApiKeyNotFoundException("API key not found")); String plainKey = generateRandomKey(); - api_key.setApiKey(plainKey); - api_key.setGeneratedAt(Instant.now()); + apiKey.setApiKey(plainKey); + apiKey.setGeneratedAt(Instant.now()); apiKeyRepository.save(apiKey); return plainKey; } catch (Exception e) { @@ -90,18 +75,18 @@ public String generateApiKey(UUID accountId, UUID apiKeyId) { } } - public ApiKeyResponseDTO updateApiKey(UUID accountId, UUID apiKeyId, ApiKeyUpsertDTO dto) { + public ApiKeyResponseDTO updateApiKey(Long userId, UUID apiKeyId, ApiKeyUpsertDTO dto) { final String ctx = CLASSNAME + ".updateApiKey"; try { - ApiKey apiKey = apiKeyRepository.findByIdAndAccountId(apiKeyId, accountId) + ApiKey apiKey = apiKeyRepository.findByIdAndUserId(apiKeyId, userId) .orElseThrow(() -> new ApiKeyNotFoundException("API key not found")); - api_key.setName(dto.getName()); + apiKey.setName(dto.getName()); if (dto.getAllowedIp() != null) { - api_key.setAllowedIp(String.join(",", dto.getAllowedIp())); + apiKey.setAllowedIp(String.join(",", dto.getAllowedIp())); } else { - api_key.setAllowedIp(null); + apiKey.setAllowedIp(null); } - api_key.setExpiresAt(dto.getExpiresAt()); + apiKey.setExpiresAt(dto.getExpiresAt()); ApiKey updated = apiKeyRepository.save(apiKey); return apiKeyMapper.toDto(updated); } catch (Exception e) { @@ -109,10 +94,10 @@ public ApiKeyResponseDTO updateApiKey(UUID accountId, UUID apiKeyId, ApiKeyUpser } } - public ApiKeyResponseDTO getApiKey(UUID accountId, UUID apiKeyId) { + public ApiKeyResponseDTO getApiKey(Long userId, UUID apiKeyId) { final String ctx = CLASSNAME + ".getApiKey"; try { - ApiKey apiKey = apiKeyRepository.findByIdAndAccountId(apiKeyId, accountId) + ApiKey apiKey = apiKeyRepository.findByIdAndUserId(apiKeyId, userId) .orElseThrow(() -> new ApiKeyNotFoundException("API key not found")); return apiKeyMapper.toDto(apiKey); } catch (Exception e) { @@ -120,20 +105,20 @@ public ApiKeyResponseDTO getApiKey(UUID accountId, UUID apiKeyId) { } } - public Page listApiKeys(UUID accountId, Pageable pageable) { + public Page listApiKeys(Long userId, Pageable pageable) { final String ctx = CLASSNAME + ".listApiKeys"; try { - return apiKeyRepository.findByAccountId(accountId, pageable).map(apiKeyMapper::toDto); + return apiKeyRepository.findByUserId(userId, pageable).map(apiKeyMapper::toDto); } catch (Exception e) { throw new RuntimeException(ctx + ": " + e.getMessage()); } } - public void deleteApiKey(UUID accountId, UUID apiKeyId) { + public void deleteApiKey(Long userId, UUID apiKeyId) { final String ctx = CLASSNAME + ".deleteApiKey"; try { - ApiKey apiKey = apiKeyRepository.findByIdAndAccountId(apiKeyId, accountId) + ApiKey apiKey = apiKeyRepository.findByIdAndUserId(apiKeyId, userId) .orElseThrow(() -> new ApiKeyNotFoundException("API key not found")); apiKeyRepository.delete(apiKey); } catch (Exception e) { @@ -154,10 +139,10 @@ private String generateRandomKey() { } @Async - public void logUsage(ApiKeyUsageLogIndexDocument apiKeyUsageLog) { + public void logUsage(ApiKeyUsageLog apiKeyUsageLog) { final String ctx = CLASSNAME + ".logUsage"; try { - apiUsageLogRepository.save(apiKeyUsageLog); + client.getClient().index(V11_API_ACCESS_LOGS, apiKeyUsageLog); } catch (Exception e) { log.error(ctx + ": {}", e.getMessage()); } @@ -167,46 +152,28 @@ public Optional findOneByApiKey(String apiKey) { return apiKeyRepository.findOneByApiKey(apiKey); } - public SearchHitsResponseDTO getApiKeyUsageLogs(User user, - ApiKeyUsageLogCriteria criteria, - Pageable pageable) { - final String ctx = CLASSNAME + ".getApiKeyUsageLogs"; - try { - CriteriaQuery query = new CriteriaQuery(criteria != null ? criteria.toCriteriaQuery() : new Criteria(), pageable); - query.addCriteria(new Criteria(ApiKeyUsageLog_.accountId).is(user.getAccountId())); - SearchHits result = elasticsearchOperations.search(query, ApiKeyUsageLogIndexDocument.class); - return SearchHitsResponseDTO.builder() - .totalHits(result.getTotalHits()) - .items(result.stream() - .map(SearchHit::getContent) - .collect(Collectors.toList())) - .build(); - } catch (Exception e) { - throw new RuntimeException(ctx + ": " + e.getMessage()); - } - } - @Scheduled(cron = "0 0 9 * * ?") + /*@Scheduled(cron = "0 0 9 * * ?") public void checkExpiringApiKeys() { Instant fiveDaysFromNow = Instant.now().plus(5, ChronoUnit.DAYS); Instant now = Instant.now(); List expiringKeys = apiKeyRepository.findAllByExpiresAtAfterAndExpiresAtLessThanEqual(now, fiveDaysFromNow); if (!expiringKeys.isEmpty()) { - Map> expiringKeysByAccount = expiringKeys.stream() - .collect(Collectors.groupingBy(ApiKey::getAccountId)); + Map> expiringKeysByAccount = expiringKeys.stream() + .collect(Collectors.groupingBy(ApiKey::getUserId)); - expiringKeysByAccount.forEach((accountId, apiKeys) -> { - var principal = userRepository.findByAccountIdAndAccountOwnerIsTrue(accountId.toString()).orElse(null); + expiringKeysByAccount.forEach((userId, apiKeys) -> { + var principal = userRepository.findByuserIdAndAccountOwnerIsTrue(userId.toString()).orElse(null); if (principal == null) { return; } mailService.sendKeyExpirationEmail(principal, apiKeys); userNotificationService.createAndSendNotification(principal.getUuid(), - NotificationMessageKeyEnum.API_KEY_EXPIRATION, + NotificationMessageKeyEnum.apiKey_EXPIRATION, Map.of("names", apiKeys.stream().map(ApiKey::getName).collect(Collectors.joining(",")))); }); } - } + }*/ } diff --git a/backend/src/main/java/com/park/utmstack/service/dto/api_key/ApiKeyResponseDTO.java b/backend/src/main/java/com/park/utmstack/service/dto/api_key/ApiKeyResponseDTO.java index 232e77f56..024f7282c 100644 --- a/backend/src/main/java/com/park/utmstack/service/dto/api_key/ApiKeyResponseDTO.java +++ b/backend/src/main/java/com/park/utmstack/service/dto/api_key/ApiKeyResponseDTO.java @@ -1,4 +1,4 @@ -package com.utmstack.api.service.dto.apikey; +package com.park.utmstack.service.dto.api_key; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; diff --git a/backend/src/main/java/com/park/utmstack/service/dto/api_key/ApiKeyUpsertDTO.java b/backend/src/main/java/com/park/utmstack/service/dto/api_key/ApiKeyUpsertDTO.java index 518aa6e35..6345f2032 100644 --- a/backend/src/main/java/com/park/utmstack/service/dto/api_key/ApiKeyUpsertDTO.java +++ b/backend/src/main/java/com/park/utmstack/service/dto/api_key/ApiKeyUpsertDTO.java @@ -1,14 +1,13 @@ -package com.utmstack.api.service.dto.apikey; +package com.park.utmstack.service.dto.api_key; - -import com.utmstack.api.annotation.ValidIPOrCIDR; +import com.park.utmstack.validation.api_key.ValidIPOrCIDR; import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotNull; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import javax.validation.constraints.NotNull; import java.time.Instant; import java.util.List; diff --git a/backend/src/main/java/com/park/utmstack/service/mapper/ApiKeyMapper.java b/backend/src/main/java/com/park/utmstack/service/mapper/ApiKeyMapper.java new file mode 100644 index 000000000..0439c563b --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/service/mapper/ApiKeyMapper.java @@ -0,0 +1,31 @@ +package com.park.utmstack.service.mapper; + +import com.park.utmstack.domain.api_keys.ApiKey; +import com.park.utmstack.service.dto.api_key.ApiKeyResponseDTO; +import org.mapstruct.Mapper; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Optional; +import java.util.stream.Collectors; + +@Mapper(componentModel = "spring") +public class ApiKeyMapper { + + public ApiKeyResponseDTO toDto(ApiKey apiKey){ + return ApiKeyResponseDTO.builder() + .id(apiKey.getId()) + .name(apiKey.getName()) + .createdAt(apiKey.getCreatedAt()) + .expiresAt(apiKey.getExpiresAt()) + .allowedIp( + Optional.ofNullable(apiKey.getAllowedIp()) + .map(s -> Arrays.stream(s.split(",")) + .map(String::trim) + .filter(str -> !str.isEmpty()) + .collect(Collectors.toList())) + .orElse(Collections.emptyList()) + ) + .build(); + } +} diff --git a/backend/src/main/java/com/park/utmstack/util/exceptions/ApiKeyExistException.java b/backend/src/main/java/com/park/utmstack/util/exceptions/ApiKeyExistException.java new file mode 100644 index 000000000..20577f508 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/util/exceptions/ApiKeyExistException.java @@ -0,0 +1,7 @@ +package com.park.utmstack.util.exceptions; + +public class ApiKeyExistException extends RuntimeException { + public ApiKeyExistException(String message) { + super(message); + } +} diff --git a/backend/src/main/java/com/park/utmstack/util/exceptions/ApiKeyInvalidAccessException.java b/backend/src/main/java/com/park/utmstack/util/exceptions/ApiKeyInvalidAccessException.java new file mode 100644 index 000000000..c5a13ead4 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/util/exceptions/ApiKeyInvalidAccessException.java @@ -0,0 +1,9 @@ +package com.park.utmstack.util.exceptions; + +import org.springframework.security.core.AuthenticationException; + +public class ApiKeyInvalidAccessException extends AuthenticationException { + public ApiKeyInvalidAccessException(String message) { + super(message); + } +} diff --git a/backend/src/main/java/com/park/utmstack/util/exceptions/ApiKeyNotFoundException.java b/backend/src/main/java/com/park/utmstack/util/exceptions/ApiKeyNotFoundException.java new file mode 100644 index 000000000..173d8f442 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/util/exceptions/ApiKeyNotFoundException.java @@ -0,0 +1,7 @@ +package com.park.utmstack.util.exceptions; + +public class ApiKeyNotFoundException extends RuntimeException { + public ApiKeyNotFoundException(String message) { + super(message); + } +} diff --git a/backend/src/main/java/com/park/utmstack/validation/api_key/ValidIPOrCIDR.java b/backend/src/main/java/com/park/utmstack/validation/api_key/ValidIPOrCIDR.java new file mode 100644 index 000000000..55dfe9593 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/validation/api_key/ValidIPOrCIDR.java @@ -0,0 +1,23 @@ +package com.park.utmstack.validation.api_key; + + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Documented +@Constraint(validatedBy = ValidIPOrCIDRValidator.class) +@Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE, TYPE_USE}) +@Retention(RUNTIME) +public @interface ValidIPOrCIDR { + String message() default "Invalid IP address or CIDR notation"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/backend/src/main/java/com/park/utmstack/validation/api_key/ValidIPOrCIDRValidator.java b/backend/src/main/java/com/park/utmstack/validation/api_key/ValidIPOrCIDRValidator.java new file mode 100644 index 000000000..8324094f8 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/validation/api_key/ValidIPOrCIDRValidator.java @@ -0,0 +1,37 @@ +package com.park.utmstack.validation.api_key; + + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import java.util.regex.Pattern; + +public class ValidIPOrCIDRValidator implements ConstraintValidator { + + private static final Pattern IPV4_PATTERN = Pattern.compile( + "^(?:(?:25[0-5]|2[0-4]\\d|[01]?\\d?\\d)\\.){3}(?:25[0-5]|2[0-4]\\d|[01]?\\d?\\d)$" + ); + + private static final Pattern IPV4_CIDR_PATTERN = Pattern.compile( + "^(?:(?:25[0-5]|2[0-4]\\d|[01]?\\d?\\d)\\.){3}(?:25[0-5]|2[0-4]\\d|[01]?\\d?\\d)/(\\d|[1-2]\\d|3[0-2])$" + ); + private static final Pattern IPV6_PATTERN = Pattern.compile( + "^(?:[\\da-fA-F]{1,4}:){7}[\\da-fA-F]{1,4}$" + ); + + private static final Pattern IPV6_CIDR_PATTERN = Pattern.compile( + "^(?:[\\da-fA-F]{1,4}:){7}[\\da-fA-F]{1,4}/(\\d|[1-9]\\d|1[01]\\d|12[0-8])$" + ); + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + // Allow null or empty values; use @NotNull/@NotEmpty to enforce non-null if needed. + if (value == null || value.trim().isEmpty()) { + return true; + } + String trimmed = value.trim(); + if (IPV4_PATTERN.matcher(trimmed).matches() || IPV4_CIDR_PATTERN.matcher(trimmed).matches()) { + return true; + } + return IPV6_PATTERN.matcher(trimmed).matches() || IPV6_CIDR_PATTERN.matcher(trimmed).matches(); + } +} diff --git a/backend/src/main/java/com/park/utmstack/web/rest/api-keys/UtmApiKeyController.java b/backend/src/main/java/com/park/utmstack/web/rest/api_key/ApiKeyResource.java similarity index 70% rename from backend/src/main/java/com/park/utmstack/web/rest/api-keys/UtmApiKeyController.java rename to backend/src/main/java/com/park/utmstack/web/rest/api_key/ApiKeyResource.java index 253fabc76..c5f17588d 100644 --- a/backend/src/main/java/com/park/utmstack/web/rest/api-keys/UtmApiKeyController.java +++ b/backend/src/main/java/com/park/utmstack/web/rest/api_key/ApiKeyResource.java @@ -1,3 +1,49 @@ +package com.park.utmstack.web.rest.api_key; + +import com.insecureweb.api.domain.User; +import com.insecureweb.api.domain.apikey.index.ApiKeyUsageLogIndexDocument; +import com.insecureweb.api.security.AuthoritiesConstants; +import com.insecureweb.api.security.SecurityUtils; +import com.insecureweb.api.service.ApiKeyService; +import com.insecureweb.api.service.criteria.apikey.ApiKeyUsageLogCriteria; +import com.insecureweb.api.service.dto.SearchHitsResponseDTO; +import com.insecureweb.api.service.dto.apikey.ApiKeyResponseDTO; +import com.insecureweb.api.service.dto.apikey.ApiKeyUpsertDTO; +import com.insecureweb.api.service.exceptions.*; +import com.insecureweb.api.service.user.UserService; +import com.insecureweb.api.util.ResponseUtil; +import com.insecureweb.api.web.rest.restutil.ResponseSearchHitsUtil; +import com.park.utmstack.domain.application_events.enums.ApplicationEventType; +import com.park.utmstack.domain.chart_builder.types.query.FilterType; +import com.park.utmstack.domain.chart_builder.types.query.OperatorType; +import com.park.utmstack.util.ResponseUtil; +import com.park.utmstack.util.UtilPagination; +import io.swagger.v3.oas.annotations.Hidden; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.headers.Header; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import lombok.AllArgsConstructor; +import org.opensearch.client.opensearch.core.SearchResponse; +import org.opensearch.client.opensearch.core.search.Hit; +import org.opensearch.client.opensearch.core.search.HitsMetadata; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springdoc.core.annotations.ParameterObject; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; +import tech.jhipster.web.util.PaginationUtil; + +import java.util.*; + @RestController @RequestMapping("/api/api-keys") @PreAuthorize("hasAuthority(\"" + AuthoritiesConstants.USER + "\")") @@ -176,4 +222,30 @@ public ResponseEntity deleteApiKey(@PathVariable("id") UUID apiKeyId) { return ResponseUtil.buildInternalServerErrorResponse(msg); } } + + @GetMapping("/usage") + public ResponseEntity> search(@RequestBody(required = false) List filters, + @RequestParam Integer top, @RequestParam String indexPattern, + @RequestParam(required = false, defaultValue = "false") boolean includeChildren, + Pageable pageable) { + final String ctx = CLASSNAME + ".search"; + try { + SearchResponse searchResponse = elasticsearchService.search(filters, top, indexPattern, + pageable, Map.class); + + if (Objects.isNull(searchResponse) || Objects.isNull(searchResponse.hits()) || searchResponse.hits().total().value() == 0) + return ResponseEntity.ok(Collections.emptyList()); + + HitsMetadata hits = searchResponse.hits(); + HttpHeaders headers = UtilPagination.generatePaginationHttpHeaders(Math.min(hits.total().value(), top), + pageable.getPageNumber(), pageable.getPageSize(), "/api/elasticsearch/search"); + + return ResponseEntity.ok().headers(headers).body(results); + } catch (Exception e) { + String msg = ctx + ": " + e.getMessage(); + log.error(msg); + applicationEventService.createEvent(msg, ApplicationEventType.ERROR); + return com.park.utmstack.util.ResponseUtil.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); + } + } } diff --git a/backend/src/main/resources/config/liquibase/changelog/20250917001_adding_api_keys.xml b/backend/src/main/resources/config/liquibase/changelog/20251017001_create_api_keys_table.xml similarity index 100% rename from backend/src/main/resources/config/liquibase/changelog/20250917001_adding_api_keys.xml rename to backend/src/main/resources/config/liquibase/changelog/20251017001_create_api_keys_table.xml diff --git a/backend/src/main/resources/config/liquibase/master.xml b/backend/src/main/resources/config/liquibase/master.xml index b7ab36816..8735ef517 100644 --- a/backend/src/main/resources/config/liquibase/master.xml +++ b/backend/src/main/resources/config/liquibase/master.xml @@ -253,7 +253,7 @@ - + From 3752e6d72e968e6490000b2ebb6e86caaa326f9e Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Fri, 17 Oct 2025 13:31:26 -0500 Subject: [PATCH 081/128] refactor(api_keys): simplify API key management by removing user ID dependency in service methods --- .../advice/GlobalExceptionHandler.java | 15 +- .../park/utmstack/service/UserService.java | 3 +- .../service/api_key/ApiKeyService.java | 20 ++- .../web/rest/api_key/ApiKeyResource.java | 159 +++++------------- 4 files changed, 65 insertions(+), 132 deletions(-) diff --git a/backend/src/main/java/com/park/utmstack/advice/GlobalExceptionHandler.java b/backend/src/main/java/com/park/utmstack/advice/GlobalExceptionHandler.java index 937bdde86..b84c2ea14 100644 --- a/backend/src/main/java/com/park/utmstack/advice/GlobalExceptionHandler.java +++ b/backend/src/main/java/com/park/utmstack/advice/GlobalExceptionHandler.java @@ -4,10 +4,7 @@ import com.park.utmstack.security.TooMuchLoginAttemptsException; import com.park.utmstack.service.application_events.ApplicationEventService; import com.park.utmstack.util.ResponseUtil; -import com.park.utmstack.util.exceptions.IncidentAlertConflictException; -import com.park.utmstack.util.exceptions.NoAlertsProvidedException; -import com.park.utmstack.util.exceptions.TfaVerificationException; -import com.park.utmstack.util.exceptions.TooManyRequestsException; +import com.park.utmstack.util.exceptions.*; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; @@ -41,8 +38,9 @@ public ResponseEntity handleTooManyLoginAttempts(TooMuchLoginAttemptsExceptio return ResponseUtil.buildLockedResponse(e.getMessage()); } - @ExceptionHandler(NoSuchElementException.class) - public ResponseEntity handleNotFound(NoSuchElementException e, HttpServletRequest request) { + @ExceptionHandler({NoSuchElementException.class, + ApiKeyNotFoundException.class}) + public ResponseEntity handleNotFound(Exception e, HttpServletRequest request) { return ResponseUtil.buildNotFoundResponse(e.getMessage()); } @@ -56,8 +54,9 @@ public ResponseEntity handleNoAlertsProvided(Exception e, HttpServletRequest return ResponseUtil.buildErrorResponse(HttpStatus.BAD_REQUEST, e.getMessage()); } - @ExceptionHandler(IncidentAlertConflictException.class) - public ResponseEntity handleConflict(IncidentAlertConflictException e, HttpServletRequest request) { + @ExceptionHandler({IncidentAlertConflictException.class, + ApiKeyExistException.class}) + public ResponseEntity handleConflict(Exception e, HttpServletRequest request) { return ResponseUtil.buildErrorResponse(HttpStatus.CONFLICT, e.getMessage()); } diff --git a/backend/src/main/java/com/park/utmstack/service/UserService.java b/backend/src/main/java/com/park/utmstack/service/UserService.java index ed1a6379b..5dacde192 100644 --- a/backend/src/main/java/com/park/utmstack/service/UserService.java +++ b/backend/src/main/java/com/park/utmstack/service/UserService.java @@ -301,6 +301,7 @@ public List getAuthorities() { public User getCurrentUserLogin() { String userLogin = SecurityUtils.getCurrentUserLogin().orElseThrow(() -> new CurrentUserLoginNotFoundException("No current user login was found")); - return userRepository.findOneWithAuthoritiesByLogin(userLogin).orElseThrow(() -> new CurrentUserLoginNotFoundException(String.format("No user with login %1$s was found", userLogin))); + return userRepository.findOneWithAuthoritiesByLogin(userLogin) + .orElseThrow(() -> new CurrentUserLoginNotFoundException(String.format("No user with login %1$s was found", userLogin))); } } diff --git a/backend/src/main/java/com/park/utmstack/service/api_key/ApiKeyService.java b/backend/src/main/java/com/park/utmstack/service/api_key/ApiKeyService.java index cb402e356..71310d26d 100644 --- a/backend/src/main/java/com/park/utmstack/service/api_key/ApiKeyService.java +++ b/backend/src/main/java/com/park/utmstack/service/api_key/ApiKeyService.java @@ -3,6 +3,7 @@ import com.park.utmstack.domain.api_keys.ApiKey; import com.park.utmstack.domain.api_keys.ApiKeyUsageLog; import com.park.utmstack.repository.api_key.ApiKeyRepository; +import com.park.utmstack.service.UserService; import com.park.utmstack.service.dto.api_key.ApiKeyResponseDTO; import com.park.utmstack.service.dto.api_key.ApiKeyUpsertDTO; import com.park.utmstack.service.elasticsearch.OpensearchClientBuilder; @@ -35,11 +36,13 @@ public class ApiKeyService { private final ApiKeyRepository apiKeyRepository; private final ApiKeyMapper apiKeyMapper; private final OpensearchClientBuilder client; + private final UserService userService; - public ApiKeyResponseDTO createApiKey(Long userId, ApiKeyUpsertDTO dto) { + public ApiKeyResponseDTO createApiKey(ApiKeyUpsertDTO dto) { final String ctx = CLASSNAME + ".createApiKey"; try { + Long userId = userService.getCurrentUserLogin().getId(); apiKeyRepository.findByNameAndUserId(dto.getName(), userId) .ifPresent(apiKey -> { throw new ApiKeyExistException("Api key already exists"); @@ -60,9 +63,10 @@ public ApiKeyResponseDTO createApiKey(Long userId, ApiKeyUpsertDTO dto) { } } - public String generateApiKey(Long userId, UUID apiKeyId) { + public String generateApiKey(UUID apiKeyId) { final String ctx = CLASSNAME + ".generateApiKey"; try { + Long userId = userService.getCurrentUserLogin().getId(); ApiKey apiKey = apiKeyRepository.findByIdAndUserId(apiKeyId, userId) .orElseThrow(() -> new ApiKeyNotFoundException("API key not found")); String plainKey = generateRandomKey(); @@ -75,9 +79,10 @@ public String generateApiKey(Long userId, UUID apiKeyId) { } } - public ApiKeyResponseDTO updateApiKey(Long userId, UUID apiKeyId, ApiKeyUpsertDTO dto) { + public ApiKeyResponseDTO updateApiKey(UUID apiKeyId, ApiKeyUpsertDTO dto) { final String ctx = CLASSNAME + ".updateApiKey"; try { + Long userId = userService.getCurrentUserLogin().getId(); ApiKey apiKey = apiKeyRepository.findByIdAndUserId(apiKeyId, userId) .orElseThrow(() -> new ApiKeyNotFoundException("API key not found")); apiKey.setName(dto.getName()); @@ -94,9 +99,10 @@ public ApiKeyResponseDTO updateApiKey(Long userId, UUID apiKeyId, ApiKeyUpsertDT } } - public ApiKeyResponseDTO getApiKey(Long userId, UUID apiKeyId) { + public ApiKeyResponseDTO getApiKey(UUID apiKeyId) { final String ctx = CLASSNAME + ".getApiKey"; try { + Long userId = userService.getCurrentUserLogin().getId(); ApiKey apiKey = apiKeyRepository.findByIdAndUserId(apiKeyId, userId) .orElseThrow(() -> new ApiKeyNotFoundException("API key not found")); return apiKeyMapper.toDto(apiKey); @@ -105,9 +111,10 @@ public ApiKeyResponseDTO getApiKey(Long userId, UUID apiKeyId) { } } - public Page listApiKeys(Long userId, Pageable pageable) { + public Page listApiKeys(Pageable pageable) { final String ctx = CLASSNAME + ".listApiKeys"; try { + Long userId = userService.getCurrentUserLogin().getId(); return apiKeyRepository.findByUserId(userId, pageable).map(apiKeyMapper::toDto); } catch (Exception e) { throw new RuntimeException(ctx + ": " + e.getMessage()); @@ -115,9 +122,10 @@ public Page listApiKeys(Long userId, Pageable pageable) { } - public void deleteApiKey(Long userId, UUID apiKeyId) { + public void deleteApiKey(UUID apiKeyId) { final String ctx = CLASSNAME + ".deleteApiKey"; try { + Long userId = userService.getCurrentUserLogin().getId(); ApiKey apiKey = apiKeyRepository.findByIdAndUserId(apiKeyId, userId) .orElseThrow(() -> new ApiKeyNotFoundException("API key not found")); apiKeyRepository.delete(apiKey); diff --git a/backend/src/main/java/com/park/utmstack/web/rest/api_key/ApiKeyResource.java b/backend/src/main/java/com/park/utmstack/web/rest/api_key/ApiKeyResource.java index c5f17588d..9f012f765 100644 --- a/backend/src/main/java/com/park/utmstack/web/rest/api_key/ApiKeyResource.java +++ b/backend/src/main/java/com/park/utmstack/web/rest/api_key/ApiKeyResource.java @@ -1,23 +1,14 @@ package com.park.utmstack.web.rest.api_key; -import com.insecureweb.api.domain.User; -import com.insecureweb.api.domain.apikey.index.ApiKeyUsageLogIndexDocument; -import com.insecureweb.api.security.AuthoritiesConstants; -import com.insecureweb.api.security.SecurityUtils; -import com.insecureweb.api.service.ApiKeyService; -import com.insecureweb.api.service.criteria.apikey.ApiKeyUsageLogCriteria; -import com.insecureweb.api.service.dto.SearchHitsResponseDTO; -import com.insecureweb.api.service.dto.apikey.ApiKeyResponseDTO; -import com.insecureweb.api.service.dto.apikey.ApiKeyUpsertDTO; -import com.insecureweb.api.service.exceptions.*; -import com.insecureweb.api.service.user.UserService; -import com.insecureweb.api.util.ResponseUtil; -import com.insecureweb.api.web.rest.restutil.ResponseSearchHitsUtil; -import com.park.utmstack.domain.application_events.enums.ApplicationEventType; + import com.park.utmstack.domain.chart_builder.types.query.FilterType; -import com.park.utmstack.domain.chart_builder.types.query.OperatorType; -import com.park.utmstack.util.ResponseUtil; +import com.park.utmstack.security.AuthoritiesConstants; +import com.park.utmstack.service.api_key.ApiKeyService; +import com.park.utmstack.service.dto.api_key.ApiKeyResponseDTO; +import com.park.utmstack.service.dto.api_key.ApiKeyUpsertDTO; +import com.park.utmstack.service.elasticsearch.ElasticsearchService; import com.park.utmstack.util.UtilPagination; +import com.park.utmstack.web.rest.elasticsearch.ElasticsearchResource; import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.headers.Header; @@ -26,12 +17,11 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.opensearch.client.opensearch.core.SearchResponse; import org.opensearch.client.opensearch.core.search.Hit; import org.opensearch.client.opensearch.core.search.HitsMetadata; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springdoc.core.annotations.ParameterObject; +import org.springdoc.api.annotations.ParameterObject; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.http.HttpHeaders; @@ -43,24 +33,18 @@ import tech.jhipster.web.util.PaginationUtil; import java.util.*; +import java.util.stream.Collectors; @RestController @RequestMapping("/api/api-keys") @PreAuthorize("hasAuthority(\"" + AuthoritiesConstants.USER + "\")") +@Slf4j @AllArgsConstructor @Hidden public class ApiKeyResource { - private static final String CLASSNAME = "ApiKeyResource"; - private final Logger log = LoggerFactory.getLogger(ApiKeyResource.class); - private final ApiKeyService apiKeyService; - private final UserService userService; - - private UUID getCurrentAccountId() throws CurrentUserLoginNotFoundException { - User user = userService.getUserWithAuthoritiesByLogin(SecurityUtils.currentUserLogin()); - return UUID.fromString(user.getAccountId()); - } + private final ElasticsearchService elasticsearchService; @Operation(summary = "Create API key", description = "Creates a new API key record using the provided settings. The plain text key is not generated at creation.") @@ -75,18 +59,8 @@ private UUID getCurrentAccountId() throws CurrentUserLoginNotFoundException { }) @PostMapping public ResponseEntity createApiKey(@RequestBody ApiKeyUpsertDTO dto) { - final String ctx = CLASSNAME + ".createApiKey"; - try { - UUID accountId = getCurrentAccountId(); - ApiKeyResponseDTO responseDTO = apiKeyService.createApiKey(accountId, dto); + ApiKeyResponseDTO responseDTO = apiKeyService.createApiKey(dto); return ResponseEntity.status(HttpStatus.CREATED).body(responseDTO); - } catch (ApiKeyExistException e) { - return ResponseUtil.buildConflictResponse(e.getMessage()); - } catch (Exception e) { - String msg = ctx + ": " + e.getMessage(); - log.error(msg, e); - return ResponseUtil.buildInternalServerErrorResponse(msg); - } } @Operation(summary = "Generate a new API key", @@ -102,18 +76,8 @@ public ResponseEntity createApiKey(@RequestBody ApiKeyUpsertD }) @PostMapping("/{id}/generate") public ResponseEntity generateApiKey(@PathVariable("id") UUID apiKeyId) { - final String ctx = CLASSNAME + ".generateApiKey"; - try { - UUID accountId = getCurrentAccountId(); - String plainKey = apiKeyService.generateApiKey(accountId, apiKeyId); - return ResponseEntity.ok(plainKey); - } catch (ApiKeyNotFoundException e) { - return ResponseUtil.buildNotFoundResponse(e.getMessage()); - } catch (Exception e) { - String msg = ctx + ": " + e.getMessage(); - log.error(msg, e); - return ResponseUtil.buildInternalServerErrorResponse(msg); - } + String plainKey = apiKeyService.generateApiKey(apiKeyId); + return ResponseEntity.ok(plainKey); } @Operation(summary = "Retrieve API key", @@ -129,18 +93,8 @@ public ResponseEntity generateApiKey(@PathVariable("id") UUID apiKeyId) }) @GetMapping("/{id}") public ResponseEntity getApiKey(@PathVariable("id") UUID apiKeyId) { - final String ctx = CLASSNAME + ".getApiKey"; - try { - UUID accountId = getCurrentAccountId(); - ApiKeyResponseDTO responseDTO = apiKeyService.getApiKey(accountId, apiKeyId); - return ResponseEntity.ok(responseDTO); - } catch (ApiKeyNotFoundException e) { - return ResponseUtil.buildNotFoundResponse(e.getMessage()); - } catch (Exception e) { - String msg = ctx + ": " + e.getMessage(); - log.error(msg, e); - return ResponseUtil.buildInternalServerErrorResponse(msg); - } + ApiKeyResponseDTO responseDTO = apiKeyService.getApiKey(apiKeyId); + return ResponseEntity.ok(responseDTO); } @Operation(summary = "List API keys", @@ -156,17 +110,10 @@ public ResponseEntity getApiKey(@PathVariable("id") UUID apiK }) @GetMapping("") public ResponseEntity> listApiKeys(@ParameterObject Pageable pageable) { - final String ctx = CLASSNAME + ".listApiKeys"; - try { - UUID accountId = getCurrentAccountId(); - Page page = apiKeyService.listApiKeys(accountId, pageable); - HttpHeaders headers = PaginationUtil.generatePaginationHttpHeaders(ServletUriComponentsBuilder.fromCurrentRequest(), page); - return ResponseEntity.ok().headers(headers).body(page.getContent()); - } catch (Exception e) { - String msg = ctx + ": " + e.getMessage(); - log.error(msg, e); - return ResponseUtil.buildInternalServerErrorResponse(msg); - } + Page page = apiKeyService.listApiKeys(pageable); + HttpHeaders headers = PaginationUtil.generatePaginationHttpHeaders(ServletUriComponentsBuilder.fromCurrentRequest(), page); + + return ResponseEntity.ok().headers(headers).body(page.getContent()); } @Operation(summary = "Update API key", @@ -183,18 +130,10 @@ public ResponseEntity> listApiKeys(@ParameterObject Page @PutMapping("/{id}") public ResponseEntity updateApiKey(@PathVariable("id") UUID apiKeyId, @RequestBody ApiKeyUpsertDTO dto) { - final String ctx = CLASSNAME + ".updateApiKey"; - try { - UUID accountId = getCurrentAccountId(); - ApiKeyResponseDTO responseDTO = apiKeyService.updateApiKey(accountId, apiKeyId, dto); - return ResponseEntity.ok(responseDTO); - } catch (ApiKeyNotFoundException e) { - return ResponseUtil.buildNotFoundResponse(e.getMessage()); - } catch (Exception e) { - String msg = ctx + ": " + e.getMessage(); - log.error(msg, e); - return ResponseUtil.buildInternalServerErrorResponse(msg); - } + + ApiKeyResponseDTO responseDTO = apiKeyService.updateApiKey(apiKeyId, dto); + return ResponseEntity.ok(responseDTO); + } @Operation(summary = "Delete API key", @@ -209,18 +148,9 @@ public ResponseEntity updateApiKey(@PathVariable("id") UUID a }) @DeleteMapping("/{id}") public ResponseEntity deleteApiKey(@PathVariable("id") UUID apiKeyId) { - final String ctx = CLASSNAME + ".deleteApiKey"; - try { - UUID accountId = getCurrentAccountId(); - apiKeyService.deleteApiKey(accountId, apiKeyId); - return ResponseEntity.noContent().build(); - } catch (ApiKeyNotFoundException e) { - return ResponseUtil.buildNotFoundResponse(e.getMessage()); - } catch (Exception e) { - String msg = ctx + ": " + e.getMessage(); - log.error(msg, e); - return ResponseUtil.buildInternalServerErrorResponse(msg); - } + + apiKeyService.deleteApiKey(apiKeyId); + return ResponseEntity.noContent().build(); } @GetMapping("/usage") @@ -228,24 +158,19 @@ public ResponseEntity> search(@RequestBody(required = false) List searchResponse = elasticsearchService.search(filters, top, indexPattern, - pageable, Map.class); - - if (Objects.isNull(searchResponse) || Objects.isNull(searchResponse.hits()) || searchResponse.hits().total().value() == 0) - return ResponseEntity.ok(Collections.emptyList()); - - HitsMetadata hits = searchResponse.hits(); - HttpHeaders headers = UtilPagination.generatePaginationHttpHeaders(Math.min(hits.total().value(), top), - pageable.getPageNumber(), pageable.getPageSize(), "/api/elasticsearch/search"); - - return ResponseEntity.ok().headers(headers).body(results); - } catch (Exception e) { - String msg = ctx + ": " + e.getMessage(); - log.error(msg); - applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return com.park.utmstack.util.ResponseUtil.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); - } + + SearchResponse searchResponse = elasticsearchService.search(filters, top, indexPattern, + pageable, Map.class); + + if (Objects.isNull(searchResponse) || Objects.isNull(searchResponse.hits()) || searchResponse.hits().total().value() == 0) + return ResponseEntity.ok(Collections.emptyList()); + + HitsMetadata hits = searchResponse.hits(); + HttpHeaders headers = UtilPagination.generatePaginationHttpHeaders(Math.min(hits.total().value(), top), + pageable.getPageNumber(), pageable.getPageSize(), "/api/elasticsearch/search"); + + return ResponseEntity.ok().headers(headers).body(hits.hits().stream() + .map(Hit::source).collect(Collectors.toList())); + } } From fbbd719c551b480fc3bcb501782eaff222fd6ed2 Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Sun, 19 Oct 2025 10:07:52 -0500 Subject: [PATCH 082/128] feat(api_keys): implement API key filtering and usage logging for enhanced security --- backend/pom.xml | 6 + .../com/park/utmstack/config/Constants.java | 5 + .../domain/api_keys/ApiKeyUsageLog.java | 53 +++--- .../enums/ApplicationEventType.java | 6 +- .../api_key/ApiKeyUsageLoggingService.java | 138 ++++++++++++++ .../security/api_key/ApiKeyFilter.java | 176 ++++++++++++++++++ .../ApplicationEventService.java | 2 +- 7 files changed, 362 insertions(+), 24 deletions(-) create mode 100644 backend/src/main/java/com/park/utmstack/loggin/api_key/ApiKeyUsageLoggingService.java create mode 100644 backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java diff --git a/backend/pom.xml b/backend/pom.xml index 325396715..2109f1376 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -376,6 +376,12 @@ 3.0.5 + + commons-net + commons-net + 3.9.0 + + diff --git a/backend/src/main/java/com/park/utmstack/config/Constants.java b/backend/src/main/java/com/park/utmstack/config/Constants.java index f69adf682..d12b6b06a 100644 --- a/backend/src/main/java/com/park/utmstack/config/Constants.java +++ b/backend/src/main/java/com/park/utmstack/config/Constants.java @@ -2,7 +2,9 @@ import com.park.utmstack.domain.index_pattern.enums.SystemIndexPattern; +import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; public final class Constants { @@ -157,6 +159,9 @@ public final class Constants { public static final String CONF_TYPE_PASSWORD = "password"; public static final String CONF_TYPE_FILE = "file"; + public static final String API_KEY_HEADER = "api-key"; + public static final List API_ENDPOINT_IGNORE = Collections.emptyList(); + private Constants() { } } diff --git a/backend/src/main/java/com/park/utmstack/domain/api_keys/ApiKeyUsageLog.java b/backend/src/main/java/com/park/utmstack/domain/api_keys/ApiKeyUsageLog.java index d097aa4fb..54e0c8d15 100644 --- a/backend/src/main/java/com/park/utmstack/domain/api_keys/ApiKeyUsageLog.java +++ b/backend/src/main/java/com/park/utmstack/domain/api_keys/ApiKeyUsageLog.java @@ -1,40 +1,49 @@ package com.park.utmstack.domain.api_keys; - +import com.park.utmstack.service.dto.auditable.AuditableDTO; import lombok.*; -import java.time.Instant; -import java.util.UUID; +import java.util.HashMap; +import java.util.Map; + +@Builder @Getter @Setter @NoArgsConstructor @AllArgsConstructor -@Builder -public class ApiKeyUsageLog { - - private UUID id; - - private UUID apiKeyId; +public class ApiKeyUsageLog implements AuditableDTO { + private String id; + private String apiKeyId; private String apiKeyName; - - private Long userId; - - private Instant timestamp; - + private String userId; + private String timestamp; private String endpoint; - private String address; - private String errorMessage; - private String queryParams; - private String payload; - private String userAgent; - private String httpMethod; - - private Integer statusCode; + private String statusCode; + + @Override + public Map toAuditMap() { + Map map = new HashMap<>(); + + map.put("id", id); + map.put("api_key_id", apiKeyId); + map.put("api_key_name", apiKeyName); + map.put("user_id", userId); + map.put("timestamp", timestamp != null ? timestamp : null); + map.put("endpoint", endpoint); + map.put("address", address); + map.put("error_message", errorMessage); + map.put("query_params", queryParams); + map.put("user_agent", userAgent); + map.put("http_method", httpMethod); + map.put("status_code", statusCode); + + return map; + } } diff --git a/backend/src/main/java/com/park/utmstack/domain/application_events/enums/ApplicationEventType.java b/backend/src/main/java/com/park/utmstack/domain/application_events/enums/ApplicationEventType.java index 9df92c191..eca45ec19 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_events/enums/ApplicationEventType.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_events/enums/ApplicationEventType.java @@ -42,5 +42,9 @@ public enum ApplicationEventType { ERROR, WARNING, INFO, - MODULE_ACTIVATION_ATTEMPT, MODULE_ACTIVATION_SUCCESS, UNDEFINED + MODULE_ACTIVATION_ATTEMPT, + MODULE_ACTIVATION_SUCCESS, + API_KEY_ACCESS_SUCCESS, + API_KEY_ACCESS_FAILURE, + UNDEFINED } diff --git a/backend/src/main/java/com/park/utmstack/loggin/api_key/ApiKeyUsageLoggingService.java b/backend/src/main/java/com/park/utmstack/loggin/api_key/ApiKeyUsageLoggingService.java new file mode 100644 index 000000000..e0969e2c0 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/loggin/api_key/ApiKeyUsageLoggingService.java @@ -0,0 +1,138 @@ +package com.park.utmstack.loggin.api_key; + +import com.park.utmstack.domain.api_keys.ApiKey; +import com.park.utmstack.domain.api_keys.ApiKeyUsageLog; +import com.park.utmstack.domain.application_events.enums.ApplicationEventType; +import com.park.utmstack.service.api_key.ApiKeyService; +import com.park.utmstack.service.application_events.ApplicationEventService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.web.util.ContentCachingRequestWrapper; +import org.springframework.web.util.ContentCachingResponseWrapper; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.UUID; + +@Service +@Slf4j +@RequiredArgsConstructor +public class ApiKeyUsageLoggingService { + + private final ApiKeyService apiKeyService; + private final ApplicationEventService applicationEventService; + private static final String LOG_USAGE_FLAG = "LOG_USAGE_DONE"; + + public void logUsage(HttpServletRequest request, + HttpServletResponse response, + ApiKey apiKey, + String ipAddress, + String message) { + + if (Boolean.TRUE.equals(request.getAttribute(LOG_USAGE_FLAG))) { + return; + } + + try { + String payload = extractPayload(request); + String errorText = extractErrorText(response); + int status = safeStatus(response); + + ApiKeyUsageLog usage = buildUsageLog(apiKey, ipAddress, request, status, errorText, payload, message); + + apiKeyService.logUsage(usage); + + ApplicationEventType eventType = (status >= 400) + ? ApplicationEventType.API_KEY_ACCESS_FAILURE + : ApplicationEventType.API_KEY_ACCESS_SUCCESS; + + String eventMessage = (status >= 400) + ? "API key access failure" + : "API key access"; + + applicationEventService.createEvent(eventMessage, eventType, usage.toAuditMap()); + + } catch (Exception e) { + log.error("Error while logging API key usage: {}", e.getMessage(), e); + } finally { + request.setAttribute(LOG_USAGE_FLAG, Boolean.TRUE); + } + } + + private int safeStatus(HttpServletResponse response) { + try { + return response.getStatus(); + } catch (Exception e) { + return 0; + } + } + + private String extractPayload(HttpServletRequest request) { + if (request instanceof ContentCachingRequestWrapper wrapper) { + byte[] buf = wrapper.getContentAsByteArray(); + if (buf.length > 0) { + return extractBody(buf); + } + } + return null; + } + + private String extractErrorText(HttpServletResponse response) { + if (response instanceof ContentCachingResponseWrapper wrapper) { + byte[] buf = wrapper.getContentAsByteArray(); + if (buf.length > 0) { + return extractBody(buf); + } + } + return null; + } + + private String extractBody(byte[] buf) { + return buf.length > 0 ? new String(buf, StandardCharsets.UTF_8) : null; + } + + private ApiKeyUsageLog buildUsageLog(ApiKey apiKey, + String ipAddress, + HttpServletRequest request, + int status, + String errorText, + String payload, + String message) { + + String id = UUID.randomUUID().toString(); + String apiKeyId = apiKey != null && apiKey.getId() != null ? apiKey.getId().toString() : null; + String apiKeyName = apiKey != null ? apiKey.getName() : null; + String userId = apiKey != null && apiKey.getUserId() != null ? apiKey.getUserId().toString() : null; + String timestamp = Instant.now().toString(); + String endpoint = request != null ? request.getRequestURI() : null; + String queryParams = request != null ? request.getQueryString() : null; + String userAgent = request != null ? request.getHeader("User-Agent") : null; + String httpMethod = request != null ? request.getMethod() : null; + String statusCode = String.valueOf(status); + + String safePayload = null; + if (payload != null) { + int PAYLOAD_MAX_LENGTH = 2000; + safePayload = payload.length() > PAYLOAD_MAX_LENGTH ? payload.substring(0, PAYLOAD_MAX_LENGTH) : payload; + } + + return ApiKeyUsageLog.builder() + .id(id) + .apiKeyId(apiKeyId) + .apiKeyName(apiKeyName) + .userId(userId) + .timestamp(timestamp) + .endpoint(endpoint) + .address(ipAddress) + .errorMessage(errorText) + .queryParams(queryParams) + .payload(safePayload) + .userAgent(userAgent) + .httpMethod(httpMethod) + .statusCode(statusCode) + .build(); + } +} diff --git a/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java b/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java new file mode 100644 index 000000000..f629da7fc --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java @@ -0,0 +1,176 @@ +package com.park.utmstack.security.api_key; + + +import com.park.utmstack.config.Constants; +import com.park.utmstack.domain.api_keys.ApiKey; +import com.park.utmstack.loggin.api_key.ApiKeyUsageLoggingService; +import com.park.utmstack.repository.UserRepository; +import com.park.utmstack.service.api_key.ApiKeyService; +import com.park.utmstack.service.application_events.ApplicationEventService; +import com.park.utmstack.util.exceptions.ApiKeyInvalidAccessException; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.net.util.SubnetUtils; +import org.springframework.core.annotation.Order; +import org.springframework.lang.NonNull; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.util.ContentCachingRequestWrapper; +import org.springframework.web.util.ContentCachingResponseWrapper; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.time.Instant; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.regex.Pattern; + +import static com.park.utmstack.config.Constants.API_ENDPOINT_IGNORE; + +@Order(1) +@AllArgsConstructor +@Slf4j +@Component +public class ApiKeyFilter extends OncePerRequestFilter { + + private static final String LOG_USAGE_FLAG = "LOG_USAGE_DONE"; + private static final Pattern CIDR_PATTERN = Pattern.compile( + "^((25[0-5]|2[0-4]\\d|[01]?\\d\\d?)(\\.(?!$)|$)){4}/(\\d|[1-2]\\d|3[0-2])$" + ); + + private final UserRepository userRepository; + private final ApiKeyService apiKeyService; + private final ConcurrentMap invalidApiKeyBlackList = new ConcurrentHashMap<>(); + private final ConcurrentMap cidrCache = new ConcurrentHashMap<>(); + private final ApplicationEventService applicationEventService; + private final ApiKeyUsageLoggingService apiKeyUsageLoggingService; + + @Override + protected void doFilterInternal(@NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain) throws ServletException, IOException { + + if (API_ENDPOINT_IGNORE.contains(request.getRequestURI())) { + filterChain.doFilter(request, response); + return; + } + + if (request.getAttribute(LOG_USAGE_FLAG) != null) { + filterChain.doFilter(request, response); + return; + } + + String apiKey = request.getHeader(Constants.API_KEY_HEADER); + + if (!StringUtils.hasText(apiKey)) { + filterChain.doFilter(request, response); + return; + } + + String ipAddress = request.getRemoteAddr(); + var key = getApiKey(apiKey); + + var wrappedRequest = new ContentCachingRequestWrapper(request); + var wrappedResponse = new ContentCachingResponseWrapper(response); + + UsernamePasswordAuthenticationToken authentication; + + try { + authentication = getAuthentication(key, ipAddress); + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(wrappedRequest)); + SecurityContextHolder.getContext().setAuthentication(authentication); + + } catch (ApiKeyInvalidAccessException e) { + apiKeyUsageLoggingService.logUsage(wrappedRequest, response, key, ipAddress, e.getMessage()); + throw e; + } + + filterChain.doFilter(wrappedRequest, wrappedResponse); + wrappedResponse.copyBodyToResponse(); + + apiKeyUsageLoggingService.logUsage(wrappedRequest, response, key, ipAddress, null); + } + + private ApiKey getApiKey(String apiKey) { + if (invalidApiKeyBlackList.containsKey(apiKey)) { + throw new ApiKeyInvalidAccessException("Invalid API key"); + } + return apiKeyService.findOneByApiKey(apiKey) + .orElseThrow(() -> { + invalidApiKeyBlackList.put(apiKey, Boolean.TRUE); + return new ApiKeyInvalidAccessException("Invalid API key"); + }); + } + + public UsernamePasswordAuthenticationToken getAuthentication(ApiKey apiKey, String remoteIpAddress) { + try { + + if (!allowAccessToRemoteIp(apiKey.getAllowedIp(), remoteIpAddress)) { + throw new ApiKeyInvalidAccessException("Invalid IP address, if you recognize this IP address, add to allowed ip list"); + } + + if (apiKey.getExpiresAt() != null && !apiKey.getExpiresAt().isAfter(Instant.now())) { + throw new ApiKeyInvalidAccessException("API key expired at " + apiKey.getExpiresAt()); + } + + var userEntity = userRepository + .findById(apiKey.getUserId()) + .orElseThrow(() -> new ApiKeyInvalidAccessException("User not found for api key")); + + if (!userEntity.getActivated()) { + throw new ApiKeyInvalidAccessException("User not activated"); + } + + List authorities = userEntity.getAuthorities().stream() + .map(auth -> new SimpleGrantedAuthority(auth.getName())) + .toList(); + + User principal = new User(userEntity.getLogin(), "", authorities); + + return new UsernamePasswordAuthenticationToken(principal, apiKey.getApiKey(), authorities); + + } catch (Exception e) { + throw new ApiKeyInvalidAccessException(e.getMessage()); + } + } + + public boolean allowAccessToRemoteIp(String allowedIpList, String remoteIp) { + if (allowedIpList == null || allowedIpList.trim().isEmpty()) { + return true; + } + String[] whitelistIps = allowedIpList.split(","); + for (String ip : whitelistIps) { + String allowed = ip.trim(); + if (allowed.isEmpty()) { + continue; + } + if (CIDR_PATTERN.matcher(allowed).matches()) { + try { + SubnetUtils subnetUtils = cidrCache.computeIfAbsent(allowed, key -> { + SubnetUtils su = new SubnetUtils(key); + su.setInclusiveHostCount(true); + return su; + }); + if (subnetUtils.getInfo().isInRange(remoteIp)) { + return true; + } + } catch (IllegalArgumentException e) { + log.error("Invalid CIDR notation: {}", allowed); + } + } else if (allowed.equals(remoteIp)) { + return true; + } + } + return false; + } +} diff --git a/backend/src/main/java/com/park/utmstack/service/application_events/ApplicationEventService.java b/backend/src/main/java/com/park/utmstack/service/application_events/ApplicationEventService.java index 149e387c3..450f60d37 100644 --- a/backend/src/main/java/com/park/utmstack/service/application_events/ApplicationEventService.java +++ b/backend/src/main/java/com/park/utmstack/service/application_events/ApplicationEventService.java @@ -40,7 +40,7 @@ public void createEvent(String message, ApplicationEventType type) { .message(message).timestamp(Instant.now().toString()) .source(ApplicationEventSource.PANEL.name()).type(type.name()) .build(); - /*client.getClient().index(".utmstack-logs", applicationEvent);*/ + client.getClient().index(".utmstack-logs", applicationEvent); } catch (Exception e) { log.error(ctx + ": {}", e.getMessage()); } From dade6090f802351d2d6b1f8af9b049761aca7c0a Mon Sep 17 00:00:00 2001 From: AlexSanchez-bit Date: Fri, 17 Oct 2025 17:04:44 -0400 Subject: [PATCH 083/128] feat[frontend](api_key): added api key list/creation components --- .../api-keys/api-keys.component.html | 53 +++++++++ .../api-keys/api-keys.component.scss | 0 .../api-keys/api-keys.component.ts | 53 +++++++++ .../api-key-modal.component.html | 109 ++++++++++++++++++ .../api-key-modal.component.scss | 1 + .../api-key-modal/api-key-modal.component.ts | 68 +++++++++++ .../api-keys/shared/models/ApiKeyResponse.ts | 8 ++ .../api-keys/shared/models/ApiKeyUpsert.ts | 5 + .../shared/service/api-keys.service.ts | 108 +++++++++++++++++ .../app-management/app-management.module.ts | 5 + .../connection-key.component.html | 20 +++- 11 files changed, 429 insertions(+), 1 deletion(-) create mode 100644 frontend/src/app/app-management/api-keys/api-keys.component.html create mode 100644 frontend/src/app/app-management/api-keys/api-keys.component.scss create mode 100644 frontend/src/app/app-management/api-keys/api-keys.component.ts create mode 100644 frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.html create mode 100644 frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.scss create mode 100644 frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts create mode 100644 frontend/src/app/app-management/api-keys/shared/models/ApiKeyResponse.ts create mode 100644 frontend/src/app/app-management/api-keys/shared/models/ApiKeyUpsert.ts create mode 100644 frontend/src/app/app-management/api-keys/shared/service/api-keys.service.ts diff --git a/frontend/src/app/app-management/api-keys/api-keys.component.html b/frontend/src/app/app-management/api-keys/api-keys.component.html new file mode 100644 index 000000000..677d5cde5 --- /dev/null +++ b/frontend/src/app/app-management/api-keys/api-keys.component.html @@ -0,0 +1,53 @@ +
+
+
+
+

API Keys

+

+ The API key is a simple encrypted string that identifies you in the application. +

+
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
NAMEALLOWED IPsCREATED ATEXPIRES ATACTIONS
{{ key.name }}{{ key.allowedIp?.join(', ') || '—' }}{{ key.createdAt | date:'M/d/yy, h:mm a' }}{{ key.expiresAt ? (key.expiresAt | date:'M/d/yy, h:mm a') : '—' }} + + + +
+
+ + + Loading... + No API Keys found. + +
+
diff --git a/frontend/src/app/app-management/api-keys/api-keys.component.scss b/frontend/src/app/app-management/api-keys/api-keys.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/app/app-management/api-keys/api-keys.component.ts b/frontend/src/app/app-management/api-keys/api-keys.component.ts new file mode 100644 index 000000000..e55e0600e --- /dev/null +++ b/frontend/src/app/app-management/api-keys/api-keys.component.ts @@ -0,0 +1,53 @@ +import { Component, OnInit } from '@angular/core'; +import { ApiKeysService } from './shared/service/api-keys.service'; +import { ApiKeyResponse } from './shared/models/ApiKeyResponse'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { ApiKeyModalComponent } from './shared/components/api-key-modal/api-key-modal.component'; + +@Component({ + selector: 'app-api-keys', + templateUrl: './api-keys.component.html', + styleUrls: ['./api-keys.component.scss'] +}) +export class ApiKeysComponent implements OnInit { + apiKeys: ApiKeyResponse[] = []; + loading = false; + + constructor( + private apiKeyService: ApiKeysService, + private modalService: NgbModal + ) {} + + ngOnInit(): void { + this.loadKeys(); + } + + loadKeys(): void { + this.loading = true; + this.apiKeyService.list().subscribe({ + next: (res) => { + this.apiKeys = res.body || []; + this.loading = false; + }, + error: () => (this.loading = false) + }); + } + + openCreateModal(): void { + const modalRef = this.modalService.open(ApiKeyModalComponent, { centered: true, size: 'lg' }); + modalRef.result.then((result) => { + if (result === 'created') this.loadKeys(); + }).catch(() => {}); + } + + deleteKey(id: string): void { + if (!confirm('Are you sure you want to delete this API key?')) return; + this.apiKeyService.delete(id).subscribe(() => this.loadKeys()); + } + + regenerateKey(id: string): void { + this.apiKeyService.generate(id).subscribe((res) => { + alert('New API Key: ' + res.body); + }); + } +} diff --git a/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.html b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.html new file mode 100644 index 000000000..28ae3a6a6 --- /dev/null +++ b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.html @@ -0,0 +1,109 @@ + + + + + + diff --git a/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.scss b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.scss new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.scss @@ -0,0 +1 @@ + diff --git a/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts new file mode 100644 index 000000000..c2944f8d1 --- /dev/null +++ b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts @@ -0,0 +1,68 @@ +import { Component, OnInit } from '@angular/core'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { ApiKeysService } from '../../service/api-keys.service'; +import { FormBuilder, FormGroup, Validators, FormArray } from '@angular/forms'; + +@Component({ + selector: 'app-api-key-modal', + templateUrl: './api-key-modal.component.html', + styleUrls: ['./api-key-modal.component.scss'] +}) +export class ApiKeyModalComponent implements OnInit { + apiKeyForm: FormGroup; + ipInput = ''; + loading = false; + errorMsg = ''; + + constructor( + public activeModal: NgbActiveModal, + private apiKeyService: ApiKeysService, + private fb: FormBuilder + ) { + this.apiKeyForm = this.fb.group({ + name: ['', Validators.required], + allowedIp: this.fb.array([]), + expiresAt: [null] + }); + } + + ngOnInit(): void {} + + get allowedIp(): FormArray { + return this.apiKeyForm.get('allowedIp') as FormArray; + } + + addIp(): void { + const ip = this.ipInput.trim(); + if (ip) { + this.allowedIp.push(this.fb.control(ip)); + this.ipInput = ''; + } + } + + removeIp(index: number): void { + this.allowedIp.removeAt(index); + } + + create(): void { + this.errorMsg = ''; + + if (this.apiKeyForm.invalid) { + this.errorMsg = 'Name is required.'; + return; + } + + this.loading = true; + this.apiKeyService.create(this.apiKeyForm.value).subscribe({ + next: () => { + this.loading = false; + this.activeModal.close('created'); + }, + error: (err) => { + this.loading = false; + this.errorMsg = err.error.message || 'An error occurred while creating the API key.'; + } + }); + } +} + diff --git a/frontend/src/app/app-management/api-keys/shared/models/ApiKeyResponse.ts b/frontend/src/app/app-management/api-keys/shared/models/ApiKeyResponse.ts new file mode 100644 index 000000000..8026a23ab --- /dev/null +++ b/frontend/src/app/app-management/api-keys/shared/models/ApiKeyResponse.ts @@ -0,0 +1,8 @@ +export interface ApiKeyResponse { + id: string; + name: string; + allowedIp: string[]; + createdAt: Date; + expiresAt?: Date; + generatedAt?: Date; +} diff --git a/frontend/src/app/app-management/api-keys/shared/models/ApiKeyUpsert.ts b/frontend/src/app/app-management/api-keys/shared/models/ApiKeyUpsert.ts new file mode 100644 index 000000000..f706ba1dc --- /dev/null +++ b/frontend/src/app/app-management/api-keys/shared/models/ApiKeyUpsert.ts @@ -0,0 +1,5 @@ +export interface ApiKeyUpsert { + name: string; + allowedIp?: string[]; + expiresAt?: Date; +} diff --git a/frontend/src/app/app-management/api-keys/shared/service/api-keys.service.ts b/frontend/src/app/app-management/api-keys/shared/service/api-keys.service.ts new file mode 100644 index 000000000..210755994 --- /dev/null +++ b/frontend/src/app/app-management/api-keys/shared/service/api-keys.service.ts @@ -0,0 +1,108 @@ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpResponse } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { SERVER_API_URL } from '../../../../app.constants'; +import { ApiKeyResponse } from '../models/ApiKeyResponse'; +import { ApiKeyUpsert } from '../models/ApiKeyUpsert'; + +/** + * Service for managing API keys + */ +@Injectable({ + providedIn: 'root' +}) +export class ApiKeysService { + public resourceUrl = SERVER_API_URL + 'api/api-keys'; + + constructor(private http: HttpClient) {} + + /** + * Create a new API key + */ + create(dto: ApiKeyUpsert): Observable> { + return this.http.post( + this.resourceUrl, + dto, + { observe: 'response' } + ); + } + + /** + * Generate (or renew) a plain API key for the given id + * Returns the plain text key (only once) + */ + generate(id: string): Observable> { + return this.http.post( + `${this.resourceUrl}/${id}/generate`, + {}, + { observe: 'response', responseType: 'text' } + ); + } + + /** + * Get API key by id + */ + get(id: string): Observable> { + return this.http.get( + `${this.resourceUrl}/${id}`, + { observe: 'response' } + ); + } + + /** + * List all API keys (with optional pagination) + */ + list(params?: any): Observable> { + return this.http.get( + this.resourceUrl, + { observe: 'response', params } + ); + } + + /** + * Update an existing API key + */ + update(id: string, dto: ApiKeyUpsert): Observable> { + return this.http.put( + `${this.resourceUrl}/${id}`, + dto, + { observe: 'response' } + ); + } + + /** + * Delete API key + */ + delete(id: string): Observable> { + return this.http.delete( + `${this.resourceUrl}/${id}`, + { observe: 'response' } + ); + } + + /** + * Search API key usage in Elasticsearch + */ + usage(params: { + filters?: any[]; + top: number; + indexPattern: string; + includeChildren?: boolean; + page?: number; + size?: number; + }): Observable { + return this.http.get( + `${this.resourceUrl}/usage`, + { + params: { + top: params.top.toString(), + indexPattern: params.indexPattern, + includeChildren: params.includeChildren.toString() || 'false', + page: params.page.toString() || '0', + size: params.size.toString() || '10' + } + } + ); + } +} + diff --git a/frontend/src/app/app-management/app-management.module.ts b/frontend/src/app/app-management/app-management.module.ts index 32484f1c9..5841a0f91 100644 --- a/frontend/src/app/app-management/app-management.module.ts +++ b/frontend/src/app/app-management/app-management.module.ts @@ -45,9 +45,13 @@ import {UtmApiDocComponent} from './utm-api-doc/utm-api-doc.component'; import { UtmNotificationViewComponent } from "./utm-notification/components/notifications-view/utm-notification-view.component"; +import { ApiKeysComponent } from './api-keys/api-keys.component'; +import { ApiKeyModalComponent } from './api-keys/shared/components/api-key-modal/api-key-modal.component'; @NgModule({ declarations: [ + ApiKeysComponent, + ApiKeyModalComponent, AppManagementComponent, AppManagementSidebarComponent, IndexPatternHelpComponent, @@ -84,6 +88,7 @@ import { HealthDetailComponent, MenuDeleteDialogComponent, TokenActivateComponent, + ApiKeyModalComponent, IndexDeleteComponent], imports: [ CommonModule, diff --git a/frontend/src/app/app-management/connection-key/connection-key.component.html b/frontend/src/app/app-management/connection-key/connection-key.component.html index aa95f0ae7..441546517 100644 --- a/frontend/src/app/app-management/connection-key/connection-key.component.html +++ b/frontend/src/app/app-management/connection-key/connection-key.component.html @@ -1,4 +1,6 @@ -
+
+ +
Connection key @@ -71,5 +73,21 @@
Connection Key
+
+ + + +
+
+
+ For developers +
+
+ +
+ +
+
+
From a1f27a2fe8d15b19ca72bf4b977071a41747456a Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Mon, 20 Oct 2025 08:33:33 -0500 Subject: [PATCH 084/128] refactor(api_keys): remove unused ApplicationEventService from ApiKeyFilter --- .../java/com/park/utmstack/security/api_key/ApiKeyFilter.java | 1 - backend/src/main/resources/config/liquibase/scripts/tables.sql | 1 - 2 files changed, 2 deletions(-) diff --git a/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java b/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java index f629da7fc..05950f08b 100644 --- a/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java +++ b/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java @@ -52,7 +52,6 @@ public class ApiKeyFilter extends OncePerRequestFilter { private final ApiKeyService apiKeyService; private final ConcurrentMap invalidApiKeyBlackList = new ConcurrentHashMap<>(); private final ConcurrentMap cidrCache = new ConcurrentHashMap<>(); - private final ApplicationEventService applicationEventService; private final ApiKeyUsageLoggingService apiKeyUsageLoggingService; @Override diff --git a/backend/src/main/resources/config/liquibase/scripts/tables.sql b/backend/src/main/resources/config/liquibase/scripts/tables.sql index 629971010..7500ca2cf 100644 --- a/backend/src/main/resources/config/liquibase/scripts/tables.sql +++ b/backend/src/main/resources/config/liquibase/scripts/tables.sql @@ -12,7 +12,6 @@ DROP TABLE IF EXISTS public.utm_gvm_scan_result; DROP TABLE IF EXISTS public.utm_gvm_task; DROP TABLE IF EXISTS public.utm_module_modal; DROP TABLE IF EXISTS public.utm_system_restart; -DROP TABLE IF EXISTS public.utm_api_keys; CREATE TABLE IF NOT EXISTS public.jhi_authority ( From 0a8538ff9c32ff9a35ccae4e3c3d71353c70f7cf Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Mon, 20 Oct 2025 08:46:16 -0500 Subject: [PATCH 085/128] refactor(api_keys): update API key table schema and change ID type to BIGINT --- .../park/utmstack/domain/api_keys/ApiKey.java | 5 ++--- .../dto/api_key/ApiKeyResponseDTO.java | 2 +- .../20251017001_create_api_keys_table.xml | 4 ++-- .../config/liquibase/scripts/tables.sql | 20 ------------------- 4 files changed, 5 insertions(+), 26 deletions(-) diff --git a/backend/src/main/java/com/park/utmstack/domain/api_keys/ApiKey.java b/backend/src/main/java/com/park/utmstack/domain/api_keys/ApiKey.java index 8d0585a3d..4986a7b58 100644 --- a/backend/src/main/java/com/park/utmstack/domain/api_keys/ApiKey.java +++ b/backend/src/main/java/com/park/utmstack/domain/api_keys/ApiKey.java @@ -19,9 +19,8 @@ public class ApiKey implements Serializable { @Id - @GeneratedValue(strategy = GenerationType.AUTO) - @Column(columnDefinition = "UUID") - private UUID id; + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; @Column(nullable = false) private Long userId; diff --git a/backend/src/main/java/com/park/utmstack/service/dto/api_key/ApiKeyResponseDTO.java b/backend/src/main/java/com/park/utmstack/service/dto/api_key/ApiKeyResponseDTO.java index 024f7282c..abfa4d02d 100644 --- a/backend/src/main/java/com/park/utmstack/service/dto/api_key/ApiKeyResponseDTO.java +++ b/backend/src/main/java/com/park/utmstack/service/dto/api_key/ApiKeyResponseDTO.java @@ -17,7 +17,7 @@ public class ApiKeyResponseDTO { @Schema(description = "Unique identifier of the API key") - private UUID id; + private Long id; @Schema(description = "User-friendly API key name") private String name; diff --git a/backend/src/main/resources/config/liquibase/changelog/20251017001_create_api_keys_table.xml b/backend/src/main/resources/config/liquibase/changelog/20251017001_create_api_keys_table.xml index 2b996f83c..ad2fbaf47 100644 --- a/backend/src/main/resources/config/liquibase/changelog/20251017001_create_api_keys_table.xml +++ b/backend/src/main/resources/config/liquibase/changelog/20251017001_create_api_keys_table.xml @@ -4,9 +4,9 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd"> - + - + diff --git a/backend/src/main/resources/config/liquibase/scripts/tables.sql b/backend/src/main/resources/config/liquibase/scripts/tables.sql index 7500ca2cf..f50a695d1 100644 --- a/backend/src/main/resources/config/liquibase/scripts/tables.sql +++ b/backend/src/main/resources/config/liquibase/scripts/tables.sql @@ -830,23 +830,3 @@ CREATE TABLE IF NOT EXISTS public.utm_space_notification_control next_notification timestamp without time zone NOT NULL, CONSTRAINT utm_space_notification_control_pkey PRIMARY KEY (id) ); - -CREATE TABLE IF NOT EXISTS public.utm_api_keys -( - id uuid default uuid_generate_v4() not null - primary key, - account_id uuid not null, - name varchar(255) not null, - api_key varchar(255) not null, - allowed_ip text, - created_at timestamp not null, - expires_at timestamp, - generated_at timestamp -); - -alter table utm_api_keys - owner to postgres; - -create unique index uk_api_keys_api_key - on utm_api_keys (api_key); - From f22f18dce58ede75ff304be2c4524c3509119694 Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Thu, 23 Oct 2025 14:26:15 -0500 Subject: [PATCH 086/128] feat(api_keys): enhance API key management UI Signed-off-by: Manuel Abascal --- .../api-keys/api-keys.component.html | 78 ++++++++++++------- .../api-keys/api-keys.component.scss | 7 ++ .../api-keys/api-keys.component.ts | 5 ++ .../api-key-modal/api-key-modal.component.ts | 6 +- .../app-management-routing.module.ts | 12 ++- .../connection-key.component.html | 20 +---- .../app-management-sidebar.component.html | 10 +++ 7 files changed, 89 insertions(+), 49 deletions(-) diff --git a/frontend/src/app/app-management/api-keys/api-keys.component.html b/frontend/src/app/app-management/api-keys/api-keys.component.html index 677d5cde5..418bbd87d 100644 --- a/frontend/src/app/app-management/api-keys/api-keys.component.html +++ b/frontend/src/app/app-management/api-keys/api-keys.component.html @@ -1,25 +1,47 @@ -
-
-
-
-

API Keys

-

- The API key is a simple encrypted string that identifies you in the application. -

-
- +
+
+
+
+ API Keys +
+ + The API key is a simple encrypted string that identifies you in the application. With this key, you can access the REST API. +
- -
- - + + +
+
+
+ - - - - + + + + @@ -27,17 +49,17 @@

API Keys

- - + + diff --git a/frontend/src/app/app-management/api-keys/api-keys.component.scss b/frontend/src/app/app-management/api-keys/api-keys.component.scss index e69de29bb..b3751c83a 100644 --- a/frontend/src/app/app-management/api-keys/api-keys.component.scss +++ b/frontend/src/app/app-management/api-keys/api-keys.component.scss @@ -0,0 +1,7 @@ +:host{ + display: flex; + flex-direction: column; + flex: 1 1 auto; + min-height: 0; + height: 100%; +} diff --git a/frontend/src/app/app-management/api-keys/api-keys.component.ts b/frontend/src/app/app-management/api-keys/api-keys.component.ts index e55e0600e..cea8476b4 100644 --- a/frontend/src/app/app-management/api-keys/api-keys.component.ts +++ b/frontend/src/app/app-management/api-keys/api-keys.component.ts @@ -3,6 +3,7 @@ import { ApiKeysService } from './shared/service/api-keys.service'; import { ApiKeyResponse } from './shared/models/ApiKeyResponse'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { ApiKeyModalComponent } from './shared/components/api-key-modal/api-key-modal.component'; +import {SortEvent} from "../../shared/directives/sortable/type/sort-event"; @Component({ selector: 'app-api-keys', @@ -50,4 +51,8 @@ export class ApiKeysComponent implements OnInit { alert('New API Key: ' + res.body); }); } + + onSortBy($event: SortEvent | string) { + + } } diff --git a/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts index c2944f8d1..d9b97f091 100644 --- a/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts +++ b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts @@ -53,7 +53,11 @@ export class ApiKeyModalComponent implements OnInit { } this.loading = true; - this.apiKeyService.create(this.apiKeyForm.value).subscribe({ + const payload = { + ...this.apiKeyForm.value, + expiresAt: this.apiKeyForm.value.expiresAt + ':00.000Z', + }; + this.apiKeyService.create(payload).subscribe({ next: () => { this.loading = false; this.activeModal.close('created'); diff --git a/frontend/src/app/app-management/app-management-routing.module.ts b/frontend/src/app/app-management/app-management-routing.module.ts index 616d6fa01..d498dae62 100644 --- a/frontend/src/app/app-management/app-management-routing.module.ts +++ b/frontend/src/app/app-management/app-management-routing.module.ts @@ -16,6 +16,7 @@ import {MenuComponent} from './menu/menu.component'; import {RolloverConfigComponent} from './rollover-config/rollover-config.component'; import {UtmApiDocComponent} from './utm-api-doc/utm-api-doc.component'; import {UtmNotificationViewComponent} from './utm-notification/components/notifications-view/utm-notification-view.component'; +import {ApiKeysComponent} from "./api-keys/api-keys.component"; const routes: Routes = [ {path: '', redirectTo: 'settings', pathMatch: 'full'}, @@ -123,7 +124,16 @@ const routes: Routes = [ data: { authorities: [ADMIN_ROLE] }, - }], + }, + { + path: 'api-keys', + component: ApiKeysComponent, + canActivate: [UserRouteAccessService], + data: { + authorities: [ADMIN_ROLE] + }, + } + ], }, ]; diff --git a/frontend/src/app/app-management/connection-key/connection-key.component.html b/frontend/src/app/app-management/connection-key/connection-key.component.html index 441546517..aa95f0ae7 100644 --- a/frontend/src/app/app-management/connection-key/connection-key.component.html +++ b/frontend/src/app/app-management/connection-key/connection-key.component.html @@ -1,6 +1,4 @@ -
- -
+
Connection key @@ -73,21 +71,5 @@
Connection Key
-
- - - -
-
-
- For developers -
-
- -
- -
-
-
diff --git a/frontend/src/app/app-management/layout/app-management-sidebar/app-management-sidebar.component.html b/frontend/src/app/app-management/layout/app-management-sidebar/app-management-sidebar.component.html index 66efbcbfb..a67af0739 100644 --- a/frontend/src/app/app-management/layout/app-management-sidebar/app-management-sidebar.component.html +++ b/frontend/src/app/app-management/layout/app-management-sidebar/app-management-sidebar.component.html @@ -125,6 +125,16 @@ + + +   + API Keys + + + Date: Thu, 23 Oct 2025 14:52:05 -0500 Subject: [PATCH 087/128] feat(api_keys): enhance API key management UI Signed-off-by: Manuel Abascal --- .../api-keys/api-keys.component.ts | 7 ++- .../api-key-modal.component.html | 59 +++++++------------ .../api-key-modal/api-key-modal.component.ts | 7 ++- .../api-keys/shared/models/ApiKeyUpsert.ts | 1 + 4 files changed, 33 insertions(+), 41 deletions(-) diff --git a/frontend/src/app/app-management/api-keys/api-keys.component.ts b/frontend/src/app/app-management/api-keys/api-keys.component.ts index cea8476b4..929808937 100644 --- a/frontend/src/app/app-management/api-keys/api-keys.component.ts +++ b/frontend/src/app/app-management/api-keys/api-keys.component.ts @@ -30,12 +30,15 @@ export class ApiKeysComponent implements OnInit { this.apiKeys = res.body || []; this.loading = false; }, - error: () => (this.loading = false) + error: () => { + this.loading = false; + this.apiKeys = []; + } }); } openCreateModal(): void { - const modalRef = this.modalService.open(ApiKeyModalComponent, { centered: true, size: 'lg' }); + const modalRef = this.modalService.open(ApiKeyModalComponent, { centered: true }); modalRef.result.then((result) => { if (result === 'created') this.loadKeys(); }).catch(() => {}); diff --git a/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.html b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.html index 28ae3a6a6..6047a4749 100644 --- a/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.html +++ b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.html @@ -1,16 +1,6 @@ - + -
- +
-
    -
  • - {{ ip.value }} - -
  • -
- - + + +
+ + +
+ +
+ {{ ipInputError }} +
+ +
    +
  • + +
    +
    + + {{ ip.value }} + {{ getIpType(ip.value) }} +
    +
    + +
    +
    +
  • +
+ +
diff --git a/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.scss b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.scss index 8b1378917..e31427b61 100644 --- a/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.scss +++ b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.scss @@ -1 +1,12 @@ +.disabled-rounded-start { + border-top-left-radius: 0 !important; + border-bottom-left-radius: 0 !important; +} +.disabled-rounded-end { + border-top-right-radius: 0 !important; + border-bottom-right-radius: 0 !important; +} +.mt-4 { + margin-top: 9rem !important; +} diff --git a/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts index dd4435cec..b72d4017a 100644 --- a/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts +++ b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts @@ -3,6 +3,7 @@ import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import { ApiKeysService } from '../../service/api-keys.service'; import { FormBuilder, FormGroup, Validators, FormArray } from '@angular/forms'; import {ApiKeyUpsert} from '../../models/ApiKeyUpsert'; +import {IpFormsValidators} from "../../../../../rule-management/app-rule/validators/ip.forms.validators"; @Component({ selector: 'app-api-key-modal', @@ -18,17 +19,19 @@ export class ApiKeyModalComponent implements OnInit { loading = false; errorMsg = ''; isSaving: string | string[] | Set | { [p: string]: any }; + ipInputError: string = ''; constructor( public activeModal: NgbActiveModal, private apiKeyService: ApiKeysService, - private fb: FormBuilder - ) { + private fb: FormBuilder) { + this.apiKeyForm = this.fb.group({ name: ['', Validators.required], allowedIp: this.fb.array([]), - expiresAt: [null] + expiresAt: ['', Validators.required], }); + } ngOnInit(): void {} @@ -38,11 +41,36 @@ export class ApiKeyModalComponent implements OnInit { } addIp(): void { - const ip = this.ipInput.trim(); - if (ip) { - this.allowedIp.push(this.fb.control(ip)); - this.ipInput = ''; + const trimmedIp = this.ipInput.trim(); + + if (!trimmedIp) { + this.ipInputError = 'Please enter an IP address or CIDR'; // Se asigna el error + return; + } + + const tempControl = this.fb.control(trimmedIp, [IpFormsValidators.ipOrCidr()]); + + if (tempControl.invalid) { + if (tempControl.hasError('invalidIp')) { + this.ipInputError = 'Invalid IP address format'; + } else if (tempControl.hasError('invalidCidr')) { + this.ipInputError = 'Invalid CIDR format'; + } + return; } + + const isDuplicate = this.allowedIp.controls.some( + control => control.value === trimmedIp + ); + + if (isDuplicate) { + this.ipInputError = 'This IP is already added'; + return; + } + + this.allowedIp.push(this.fb.control(trimmedIp, [IpFormsValidators.ipOrCidr()])); + this.ipInput = ''; + this.ipInputError = ''; } removeIp(index: number): void { @@ -73,5 +101,13 @@ export class ApiKeyModalComponent implements OnInit { } }); } + + getIpType(value: string): string { + if (!value) { return ''; } + if (value.includes('/')) { + return value.includes(':') ? 'IPv6 CIDR' : 'IPv4 CIDR'; + } + return value.includes(':') ? 'IPv6' : 'IPv4'; + } } diff --git a/frontend/src/app/rule-management/app-rule/validators/ip.forms.validators.ts b/frontend/src/app/rule-management/app-rule/validators/ip.forms.validators.ts new file mode 100644 index 000000000..b82b574d6 --- /dev/null +++ b/frontend/src/app/rule-management/app-rule/validators/ip.forms.validators.ts @@ -0,0 +1,86 @@ +import {AbstractControl, ValidationErrors, ValidatorFn} from '@angular/forms'; + +export class IpFormsValidators { + + static ipOrCidr(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + if (!control.value) { + return null; + } + + const value = control.value.trim(); + + if (value.includes('/')) { + return IpFormsValidators.validateCIDR(value) ? null : { invalidCidr: true }; + } + + const isValidIPv4 = IpFormsValidators.validateIPv4(value); + const isValidIPv6 = IpFormsValidators.validateIPv6(value); + + return (isValidIPv4 || isValidIPv6) ? null : { invalidIp: true }; + }; + } + + private static validateIPv4(ip: string): boolean { + const ipv4Regex = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/; + const match = ip.match(ipv4Regex); + + if (!match) { + return false; + } + + for (let i = 1; i <= 4; i++) { + const octet = parseInt(match[i], 10); + if (octet < 0 || octet > 255) { + return false; + } + } + + return true; + } + + private static validateIPv6(ip: string): boolean { + // tslint:disable-next-line:max-line-length + const ipv6Regex = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/; + + return ipv6Regex.test(ip); + } + + private static validateCIDR(cidr: string): boolean { + const parts = cidr.split('/'); + + if (parts.length !== 2) { + return false; + } + + const [ip, prefix] = parts; + const prefixNum = parseInt(prefix, 10); + + const isIPv4 = ip.includes('.') && !ip.includes(':'); + const isIPv6 = ip.includes(':'); + + if (isIPv4) { + if (!IpFormsValidators.validateIPv4(ip)) { + return false; + } + + if (isNaN(prefixNum) || prefixNum < 0 || prefixNum > 32) { + return false; + } + } else if (isIPv6) { + + if (!IpFormsValidators.validateIPv6(ip)) { + return false; + } + + if (isNaN(prefixNum) || prefixNum < 0 || prefixNum > 128) { + return false; + } + } else { + return false; + } + + return true; + } + +} From eabbc02648e3e0f2c9db86c728312dc49b60fe1c Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Fri, 24 Oct 2025 11:51:36 -0500 Subject: [PATCH 091/128] feat(api_keys): add API key generation and expiration handling with user feedback Signed-off-by: Manuel Abascal --- .../api-keys/api-keys.component.html | 43 ++++++++- .../api-keys/api-keys.component.ts | 89 ++++++++++++++++--- .../api-key-modal.component.html | 2 + .../api-key-modal/api-key-modal.component.ts | 43 ++++++--- .../api-keys/shared/models/ApiKeyResponse.ts | 6 +- .../shared/service/api-keys.service.ts | 7 ++ 6 files changed, 158 insertions(+), 32 deletions(-) diff --git a/frontend/src/app/app-management/api-keys/api-keys.component.html b/frontend/src/app/app-management/api-keys/api-keys.component.html index 418bbd87d..22b6db422 100644 --- a/frontend/src/app/app-management/api-keys/api-keys.component.html +++ b/frontend/src/app/app-management/api-keys/api-keys.component.html @@ -47,12 +47,20 @@
- + - + diff --git a/frontend/src/app/app-management/api-keys/api-keys.component.ts b/frontend/src/app/app-management/api-keys/api-keys.component.ts index 20f256fcd..c2d305776 100644 --- a/frontend/src/app/app-management/api-keys/api-keys.component.ts +++ b/frontend/src/app/app-management/api-keys/api-keys.component.ts @@ -1,6 +1,10 @@ import {Component, OnInit, TemplateRef, ViewChild} from '@angular/core'; import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; import * as moment from 'moment'; +import {UtmToastService} from "../../shared/alert/utm-toast.service"; +import { + ModalConfirmationComponent +} from "../../shared/components/utm/util/modal-confirmation/modal-confirmation.component"; import {ITEMS_PER_PAGE} from '../../shared/constants/pagination.constants'; import {SortEvent} from '../../shared/directives/sortable/type/sort-event'; import { ApiKeyModalComponent } from './shared/components/api-key-modal/api-key-modal.component'; @@ -28,9 +32,9 @@ export class ApiKeysComponent implements OnInit { generatedModalRef!: NgbModalRef; copied = false; - constructor( - private apiKeyService: ApiKeysService, - private modalService: NgbModal + constructor( private toastService: UtmToastService, + private apiKeyService: ApiKeysService, + private modalService: NgbModal ) {} ngOnInit(): void { @@ -67,9 +71,44 @@ export class ApiKeysComponent implements OnInit { }); } - deleteKey(id: string): void { - if (!confirm('Are you sure you want to delete this API key?')) { return; } - this.apiKeyService.delete(id).subscribe(() => this.loadKeys()); + editKey(key: ApiKeyResponse): void { + const modalRef = this.modalService.open(ApiKeyModalComponent, {centered: true}); + modalRef.componentInstance.apiKey = key; + + modalRef.result.then((key: ApiKeyResponse) => { + if (key) { + this.generateKey(key); + } + }); + } + + deleteKey(apiKey: ApiKeyResponse): void { + const modalRef = this.modalService.open(ModalConfirmationComponent, {centered: true}); + modalRef.componentInstance.header = `Delete API Key: ${apiKey.name}`; + modalRef.componentInstance.message = 'Are you sure you want to delete this API key?'; + modalRef.componentInstance.confirmBtnType = 'delete'; + modalRef.componentInstance.type = 'danger'; + modalRef.componentInstance.confirmBtnText = 'Delete'; + modalRef.componentInstance.confirmBtnIcon = 'icon-cross-circle'; + + modalRef.result.then(reason => { + if (reason === 'ok') { + this.delete(apiKey); + } + }); + } + + delete(apiKey: ApiKeyResponse): void { + this.apiKeyService.delete(apiKey.id).subscribe({ + next: () => { + this.toastService.showSuccess('API key deleted successfully.'); + this.loadKeys(); + }, + error: (err) => { + this.toastService.showError('Error', 'An error occurred while deleting the API key.'); + throw err; + } + }); } getDaysUntilExpire(expiresAt: string): number { diff --git a/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts index e78f53d42..87289aee6 100644 --- a/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts +++ b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts @@ -4,9 +4,8 @@ import { FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import {IpFormsValidators} from '../../../../../rule-management/app-rule/validators/ip.forms.validators'; import {UtmToastService} from '../../../../../shared/alert/utm-toast.service'; -import {ApiKeyUpsert} from '../../models/ApiKeyUpsert'; +import {ApiKeyResponse} from '../../models/ApiKeyResponse'; import { ApiKeysService } from '../../service/api-keys.service'; -import {ApiKeyResponse} from "../../models/ApiKeyResponse"; @Component({ selector: 'app-api-key-modal', @@ -15,7 +14,7 @@ import {ApiKeyResponse} from "../../models/ApiKeyResponse"; }) export class ApiKeyModalComponent implements OnInit { - @Input() apiKey: ApiKeyUpsert = null; + @Input() apiKey: ApiKeyResponse = null; apiKeyForm: FormGroup; ipInput = ''; @@ -29,17 +28,24 @@ export class ApiKeyModalComponent implements OnInit { private apiKeyService: ApiKeysService, private fb: FormBuilder, private toastService: UtmToastService) { + } + + ngOnInit(): void { + + const expiresAtDate = this.apiKey && this.apiKey.expiresAt ? new Date(this.apiKey.expiresAt) : null; + const expiresAtNgbDate = expiresAtDate ? { + year: expiresAtDate.getUTCFullYear(), + month: expiresAtDate.getUTCMonth() + 1, + day: expiresAtDate.getUTCDate() + } : null; this.apiKeyForm = this.fb.group({ - name: ['', Validators.required], - allowedIp: this.fb.array([]), - expiresAt: ['', Validators.required], + name: [ this.apiKey ? this.apiKey.name : '', Validators.required], + allowedIp: this.fb.array(this.apiKey ? this.apiKey.allowedIp : []), + expiresAt: [expiresAtNgbDate, Validators.required], }); - } - ngOnInit(): void {} - get allowedIp(): FormArray { return this.apiKeyForm.get('allowedIp') as FormArray; } @@ -102,7 +108,11 @@ export class ApiKeyModalComponent implements OnInit { ...this.apiKeyForm.value, expiresAt: formattedDate, }; - this.apiKeyService.create(payload).subscribe({ + + const save = this.apiKey ? this.apiKeyService.update(this.apiKey.id, payload) : + this.apiKeyService.create(payload); + + save.subscribe({ next: (response) => { this.loading = false; this.activeModal.close(response.body as ApiKeyResponse); @@ -114,7 +124,6 @@ export class ApiKeyModalComponent implements OnInit { } else if (err.status === 500) { this.toastService.showError('Error', 'Server error occurred while creating the API key.'); } - } }); } From 9476b78f8af5d7f40d91910badbfcbb1074e1398 Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Mon, 27 Oct 2025 09:09:45 -0500 Subject: [PATCH 093/128] refactor(api_keys): change API key identifier type from UUID to Long for consistency --- .../utmstack/repository/api_key/ApiKeyRepository.java | 4 ++-- .../park/utmstack/service/api_key/ApiKeyService.java | 10 +++++----- .../park/utmstack/web/rest/api_key/ApiKeyResource.java | 8 ++++---- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/backend/src/main/java/com/park/utmstack/repository/api_key/ApiKeyRepository.java b/backend/src/main/java/com/park/utmstack/repository/api_key/ApiKeyRepository.java index e76001064..ece32eba9 100644 --- a/backend/src/main/java/com/park/utmstack/repository/api_key/ApiKeyRepository.java +++ b/backend/src/main/java/com/park/utmstack/repository/api_key/ApiKeyRepository.java @@ -14,9 +14,9 @@ import java.util.UUID; @Repository -public interface ApiKeyRepository extends JpaRepository { +public interface ApiKeyRepository extends JpaRepository { - Optional findByIdAndUserId(UUID id, Long userId); + Optional findByIdAndUserId(Long id, Long userId); Page findByUserId(Long userId, Pageable pageable); diff --git a/backend/src/main/java/com/park/utmstack/service/api_key/ApiKeyService.java b/backend/src/main/java/com/park/utmstack/service/api_key/ApiKeyService.java index 5ed530e3f..56642f183 100644 --- a/backend/src/main/java/com/park/utmstack/service/api_key/ApiKeyService.java +++ b/backend/src/main/java/com/park/utmstack/service/api_key/ApiKeyService.java @@ -58,11 +58,11 @@ public ApiKeyResponseDTO createApiKey(Long userId,ApiKeyUpsertDTO dto) { return apiKeyMapper.toDto(apiKeyRepository.save(apiKey)); } catch (Exception e) { - throw new RuntimeException(ctx + ": " + e.getMessage()); + throw new ApiKeyExistException(ctx + ": " + e.getMessage()); } } - public String generateApiKey(Long userId, UUID apiKeyId) { + public String generateApiKey(Long userId, Long apiKeyId) { final String ctx = CLASSNAME + ".generateApiKey"; try { ApiKey apiKey = apiKeyRepository.findByIdAndUserId(apiKeyId, userId) @@ -77,7 +77,7 @@ public String generateApiKey(Long userId, UUID apiKeyId) { } } - public ApiKeyResponseDTO updateApiKey(Long userId, UUID apiKeyId, ApiKeyUpsertDTO dto) { + public ApiKeyResponseDTO updateApiKey(Long userId, Long apiKeyId, ApiKeyUpsertDTO dto) { final String ctx = CLASSNAME + ".updateApiKey"; try { ApiKey apiKey = apiKeyRepository.findByIdAndUserId(apiKeyId, userId) @@ -96,7 +96,7 @@ public ApiKeyResponseDTO updateApiKey(Long userId, UUID apiKeyId, ApiKeyUpsertDT } } - public ApiKeyResponseDTO getApiKey(Long userId, UUID apiKeyId) { + public ApiKeyResponseDTO getApiKey(Long userId, Long apiKeyId) { final String ctx = CLASSNAME + ".getApiKey"; try { ApiKey apiKey = apiKeyRepository.findByIdAndUserId(apiKeyId, userId) @@ -117,7 +117,7 @@ public Page listApiKeys(Long userId, Pageable pageable) { } - public void deleteApiKey(Long userId, UUID apiKeyId) { + public void deleteApiKey(Long userId, Long apiKeyId) { final String ctx = CLASSNAME + ".deleteApiKey"; try { ApiKey apiKey = apiKeyRepository.findByIdAndUserId(apiKeyId, userId) diff --git a/backend/src/main/java/com/park/utmstack/web/rest/api_key/ApiKeyResource.java b/backend/src/main/java/com/park/utmstack/web/rest/api_key/ApiKeyResource.java index 147ab5254..8453f9ab2 100644 --- a/backend/src/main/java/com/park/utmstack/web/rest/api_key/ApiKeyResource.java +++ b/backend/src/main/java/com/park/utmstack/web/rest/api_key/ApiKeyResource.java @@ -78,7 +78,7 @@ public ResponseEntity createApiKey(@RequestBody ApiKeyUpsertD }) }) @PostMapping("/{id}/generate") - public ResponseEntity generateApiKey(@PathVariable("id") UUID apiKeyId) { + public ResponseEntity generateApiKey(@PathVariable("id") Long apiKeyId) { Long userId = userService.getCurrentUserLogin().getId(); String plainKey = apiKeyService.generateApiKey(userId, apiKeyId); return ResponseEntity.ok(plainKey); @@ -96,7 +96,7 @@ public ResponseEntity generateApiKey(@PathVariable("id") UUID apiKeyId) }) }) @GetMapping("/{id}") - public ResponseEntity getApiKey(@PathVariable("id") UUID apiKeyId) { + public ResponseEntity getApiKey(@PathVariable("id") Long apiKeyId) { Long userId = userService.getCurrentUserLogin().getId(); ApiKeyResponseDTO responseDTO = apiKeyService.getApiKey(userId, apiKeyId); return ResponseEntity.ok(responseDTO); @@ -134,7 +134,7 @@ public ResponseEntity> listApiKeys(@ParameterObject Page }) }) @PutMapping("/{id}") - public ResponseEntity updateApiKey(@PathVariable("id") UUID apiKeyId, + public ResponseEntity updateApiKey(@PathVariable("id") Long apiKeyId, @RequestBody ApiKeyUpsertDTO dto) { Long userId = userService.getCurrentUserLogin().getId(); @@ -154,7 +154,7 @@ public ResponseEntity updateApiKey(@PathVariable("id") UUID a }) }) @DeleteMapping("/{id}") - public ResponseEntity deleteApiKey(@PathVariable("id") UUID apiKeyId) { + public ResponseEntity deleteApiKey(@PathVariable("id") Long apiKeyId) { Long userId = userService.getCurrentUserLogin().getId(); apiKeyService.deleteApiKey(userId, apiKeyId); From 6e6ed5163d7d3b56d5006ab1da1a1bf171110f68 Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Mon, 27 Oct 2025 12:12:02 -0500 Subject: [PATCH 094/128] feat(api_keys): simplify API key listing endpoint and enhance expiration handling --- .../utmstack/service/api_key/ApiKeyService.java | 14 ++++++++++++++ .../utmstack/web/rest/api_key/ApiKeyResource.java | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/backend/src/main/java/com/park/utmstack/service/api_key/ApiKeyService.java b/backend/src/main/java/com/park/utmstack/service/api_key/ApiKeyService.java index 56642f183..cbc2784a5 100644 --- a/backend/src/main/java/com/park/utmstack/service/api_key/ApiKeyService.java +++ b/backend/src/main/java/com/park/utmstack/service/api_key/ApiKeyService.java @@ -20,6 +20,7 @@ import org.springframework.stereotype.Service; import java.security.SecureRandom; +import java.time.Duration; import java.time.Instant; import java.util.Base64; import java.util.Optional; @@ -67,9 +68,22 @@ public String generateApiKey(Long userId, Long apiKeyId) { try { ApiKey apiKey = apiKeyRepository.findByIdAndUserId(apiKeyId, userId) .orElseThrow(() -> new ApiKeyNotFoundException("API key not found")); + + Instant now = Instant.now(); + Instant originalCreated = apiKey.getGeneratedAt() != null ? apiKey.getGeneratedAt() : apiKey.getCreatedAt(); + Instant originalExpires = apiKey.getExpiresAt(); + + Duration duration; + if (originalCreated != null && originalExpires != null && !originalExpires.isBefore(originalCreated)) { + duration = Duration.between(originalCreated, originalExpires); + } else { + duration = Duration.ofDays(7); + } + String plainKey = generateRandomKey(); apiKey.setApiKey(plainKey); apiKey.setGeneratedAt(Instant.now()); + apiKey.setExpiresAt(now.plus(duration)); apiKeyRepository.save(apiKey); return plainKey; } catch (Exception e) { diff --git a/backend/src/main/java/com/park/utmstack/web/rest/api_key/ApiKeyResource.java b/backend/src/main/java/com/park/utmstack/web/rest/api_key/ApiKeyResource.java index 8453f9ab2..d0bdd0d2b 100644 --- a/backend/src/main/java/com/park/utmstack/web/rest/api_key/ApiKeyResource.java +++ b/backend/src/main/java/com/park/utmstack/web/rest/api_key/ApiKeyResource.java @@ -113,7 +113,7 @@ public ResponseEntity getApiKey(@PathVariable("id") Long apiK @Header(name = "X-App-Error", description = "Technical error details") }) }) - @GetMapping("") + @GetMapping public ResponseEntity> listApiKeys(@ParameterObject Pageable pageable) { Long userId = userService.getCurrentUserLogin().getId(); Page page = apiKeyService.listApiKeys(userId,pageable); From a1040ea8126e53448b8dab3b33a6b52bb7e8725e Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Mon, 27 Oct 2025 12:12:24 -0500 Subject: [PATCH 095/128] feat(api_keys): improve API key listing with pagination, loading states, and expiration indicators Signed-off-by: Manuel Abascal --- .../api-keys/api-keys.component.html | 57 ++++++++++++++--- .../api-keys/api-keys.component.ts | 62 ++++++++++++++----- .../shared/service/api-keys.service.ts | 4 +- 3 files changed, 96 insertions(+), 27 deletions(-) diff --git a/frontend/src/app/app-management/api-keys/api-keys.component.html b/frontend/src/app/app-management/api-keys/api-keys.component.html index d31f55a06..e2a5158ff 100644 --- a/frontend/src/app/app-management/api-keys/api-keys.component.html +++ b/frontend/src/app/app-management/api-keys/api-keys.component.html @@ -45,19 +45,26 @@
- + + + + + + + + + + + + +
NAMEALLOWED IPsCREATED ATEXPIRES AT + Name + + Allowed IPs + + Expires At + + Created At + ACTIONS
{{ key.name }} {{ key.allowedIp?.join(', ') || '—' }}{{ key.createdAt | date:'M/d/yy, h:mm a' }}{{ key.expiresAt ? (key.expiresAt | date:'M/d/yy, h:mm a') : '—' }}{{ key.expiresAt ? (key.expiresAt | date:'dd/MM/yy HH:mm' :'UTC') : '—' }}{{ key.createdAt | date:'dd/MM/yy HH:mm' :'UTC' }} - - -
{{ key.name }} {{ key.name }} {{ key.allowedIp?.join(', ') || '—' }}{{ key.expiresAt ? (key.expiresAt | date:'dd/MM/yy HH:mm' :'UTC') : '—' }} + + + + + + {{ key.expiresAt ? (key.expiresAt | date:'dd/MM/yy HH:mm' :'UTC') : '—' }} + {{ key.createdAt | date:'dd/MM/yy HH:mm' :'UTC' }} - + +
+ + {{ 'Keep this key safeKeep this key safe' }} +
+ +
+ +
+ diff --git a/frontend/src/app/app-management/api-keys/api-keys.component.ts b/frontend/src/app/app-management/api-keys/api-keys.component.ts index 929808937..20f256fcd 100644 --- a/frontend/src/app/app-management/api-keys/api-keys.component.ts +++ b/frontend/src/app/app-management/api-keys/api-keys.component.ts @@ -1,9 +1,11 @@ -import { Component, OnInit } from '@angular/core'; -import { ApiKeysService } from './shared/service/api-keys.service'; -import { ApiKeyResponse } from './shared/models/ApiKeyResponse'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import {Component, OnInit, TemplateRef, ViewChild} from '@angular/core'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import * as moment from 'moment'; +import {ITEMS_PER_PAGE} from '../../shared/constants/pagination.constants'; +import {SortEvent} from '../../shared/directives/sortable/type/sort-event'; import { ApiKeyModalComponent } from './shared/components/api-key-modal/api-key-modal.component'; -import {SortEvent} from "../../shared/directives/sortable/type/sort-event"; +import { ApiKeyResponse } from './shared/models/ApiKeyResponse'; +import { ApiKeysService } from './shared/service/api-keys.service'; @Component({ selector: 'app-api-keys', @@ -11,8 +13,20 @@ import {SortEvent} from "../../shared/directives/sortable/type/sort-event"; styleUrls: ['./api-keys.component.scss'] }) export class ApiKeysComponent implements OnInit { + + generating: string[] = []; apiKeys: ApiKeyResponse[] = []; loading = false; + generatedApiKey = ''; + @ViewChild('generatedModal') generatedModal!: TemplateRef; + request = + { + sort: 'createdAt,desc', + page: 0, + size: ITEMS_PER_PAGE + }; + generatedModalRef!: NgbModalRef; + copied = false; constructor( private apiKeyService: ApiKeysService, @@ -37,25 +51,74 @@ export class ApiKeysComponent implements OnInit { }); } + copyToClipboard(): void { + (navigator as any).clipboard.writeText(this.generatedApiKey).then(() => { + this.copied = true; + }); + } + openCreateModal(): void { const modalRef = this.modalService.open(ApiKeyModalComponent, { centered: true }); - modalRef.result.then((result) => { - if (result === 'created') this.loadKeys(); - }).catch(() => {}); + + modalRef.result.then((key: ApiKeyResponse) => { + if (key) { + this.generateKey(key); + } + }); } deleteKey(id: string): void { - if (!confirm('Are you sure you want to delete this API key?')) return; + if (!confirm('Are you sure you want to delete this API key?')) { return; } this.apiKeyService.delete(id).subscribe(() => this.loadKeys()); } - regenerateKey(id: string): void { - this.apiKeyService.generate(id).subscribe((res) => { - alert('New API Key: ' + res.body); - }); + getDaysUntilExpire(expiresAt: string): number { + if (!expiresAt) { + return 0; + } + const today = moment(); + const expireDate = moment(expiresAt); + return expireDate.diff(today, 'days'); } onSortBy($event: SortEvent | string) { } + + maskSecrets(str: string): string { + if (!str || str.length <= 10) { + return str; + } + const prefix = str.substring(0, 10); + const maskLength = str.length - 30; + const maskedPart = '*'.repeat(maskLength); + return prefix + maskedPart; + } + + generateKey(apiKey: ApiKeyResponse): void { + this.generating.push(apiKey.id); + this.apiKeyService.generateApiKey(apiKey.id).subscribe(response => { + this.generatedApiKey = response.body ? response.body : ""; + this.generatedModalRef = this.modalService.open(this.generatedModal, {centered: true}); + const index = this.generating.indexOf(apiKey.id); + if (index > -1) { + this.generating.splice(index, 1); + } + this.loadKeys(); + }); + } + + isApiKeyExpired(expiresAt?: string | null ): boolean { + if (!expiresAt) { + return false; + } + const expirationTime = new Date(expiresAt).getTime(); + return expirationTime < Date.now(); + } + + close() { + this.generatedModalRef.close(); + this.copied = false; + this.generatedApiKey = ''; + } } diff --git a/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.html b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.html index 6e7cad101..7e1399bbb 100644 --- a/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.html +++ b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.html @@ -26,6 +26,7 @@ class="form-control" id="expiresAt" name="expiresAt" + [minDate]="minDate" placeholder="yyyy-mm-dd" formControlName="expiresAt" ngbDatepicker @@ -123,3 +124,4 @@ + diff --git a/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts index b72d4017a..e78f53d42 100644 --- a/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts +++ b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts @@ -1,9 +1,12 @@ +import {HttpErrorResponse} from '@angular/common/http'; import {Component, Input, OnInit} from '@angular/core'; +import { FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { ApiKeysService } from '../../service/api-keys.service'; -import { FormBuilder, FormGroup, Validators, FormArray } from '@angular/forms'; +import {IpFormsValidators} from '../../../../../rule-management/app-rule/validators/ip.forms.validators'; +import {UtmToastService} from '../../../../../shared/alert/utm-toast.service'; import {ApiKeyUpsert} from '../../models/ApiKeyUpsert'; -import {IpFormsValidators} from "../../../../../rule-management/app-rule/validators/ip.forms.validators"; +import { ApiKeysService } from '../../service/api-keys.service'; +import {ApiKeyResponse} from "../../models/ApiKeyResponse"; @Component({ selector: 'app-api-key-modal', @@ -19,12 +22,13 @@ export class ApiKeyModalComponent implements OnInit { loading = false; errorMsg = ''; isSaving: string | string[] | Set | { [p: string]: any }; - ipInputError: string = ''; + minDate = { year: new Date().getFullYear(), month: new Date().getMonth() + 1, day: new Date().getDate() }; + ipInputError = ''; - constructor( - public activeModal: NgbActiveModal, - private apiKeyService: ApiKeysService, - private fb: FormBuilder) { + constructor( public activeModal: NgbActiveModal, + private apiKeyService: ApiKeysService, + private fb: FormBuilder, + private toastService: UtmToastService) { this.apiKeyForm = this.fb.group({ name: ['', Validators.required], @@ -86,18 +90,31 @@ export class ApiKeyModalComponent implements OnInit { } this.loading = true; + + const rawDate = this.apiKeyForm.get('expiresAt').value; + let formattedDate = rawDate; + + if (rawDate && typeof rawDate === 'object') { + formattedDate = `${rawDate.year}-${String(rawDate.month).padStart(2, '0')}-${String(rawDate.day).padStart(2, '0')}T00:00:00.000Z`; + } + const payload = { ...this.apiKeyForm.value, - expiresAt: this.apiKeyForm.value.expiresAt + ':00.000Z', + expiresAt: formattedDate, }; this.apiKeyService.create(payload).subscribe({ - next: () => { + next: (response) => { this.loading = false; - this.activeModal.close('created'); + this.activeModal.close(response.body as ApiKeyResponse); }, - error: (err) => { + error: (err: HttpErrorResponse) => { this.loading = false; - this.errorMsg = err.error.message || 'An error occurred while creating the API key.'; + if (err.status === 409) { + this.toastService.showError('Error', 'An API key with this name already exists.'); + } else if (err.status === 500) { + this.toastService.showError('Error', 'Server error occurred while creating the API key.'); + } + } }); } diff --git a/frontend/src/app/app-management/api-keys/shared/models/ApiKeyResponse.ts b/frontend/src/app/app-management/api-keys/shared/models/ApiKeyResponse.ts index 8026a23ab..3f6b890d7 100644 --- a/frontend/src/app/app-management/api-keys/shared/models/ApiKeyResponse.ts +++ b/frontend/src/app/app-management/api-keys/shared/models/ApiKeyResponse.ts @@ -2,7 +2,7 @@ export interface ApiKeyResponse { id: string; name: string; allowedIp: string[]; - createdAt: Date; - expiresAt?: Date; - generatedAt?: Date; + createdAt: string; + expiresAt?: string; + generatedAt?: string; } diff --git a/frontend/src/app/app-management/api-keys/shared/service/api-keys.service.ts b/frontend/src/app/app-management/api-keys/shared/service/api-keys.service.ts index 210755994..e668367ba 100644 --- a/frontend/src/app/app-management/api-keys/shared/service/api-keys.service.ts +++ b/frontend/src/app/app-management/api-keys/shared/service/api-keys.service.ts @@ -80,6 +80,13 @@ export class ApiKeysService { ); } + generateApiKey(apiKeyId: string): Observable> { + return this.http.post(`${this.resourceUrl}/${apiKeyId}/generate`, null, { + observe: 'response', + responseType: 'text' + }); + } + /** * Search API key usage in Elasticsearch */ From 910852ed57709fc5b33e0fa87637374b1552b668 Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Fri, 24 Oct 2025 12:20:24 -0500 Subject: [PATCH 092/128] feat(api_keys): update API key modal for editing and improved deletion confirmation Signed-off-by: Manuel Abascal --- .../api-keys/api-keys.component.html | 4 +- .../api-keys/api-keys.component.ts | 51 ++++++++++++++++--- .../api-key-modal/api-key-modal.component.ts | 31 +++++++---- 3 files changed, 67 insertions(+), 19 deletions(-) diff --git a/frontend/src/app/app-management/api-keys/api-keys.component.html b/frontend/src/app/app-management/api-keys/api-keys.component.html index 22b6db422..d31f55a06 100644 --- a/frontend/src/app/app-management/api-keys/api-keys.component.html +++ b/frontend/src/app/app-management/api-keys/api-keys.component.html @@ -63,10 +63,10 @@
- -
ACTIONS
{{ key.name }} {{ key.allowedIp?.join(', ') || '—' }} - - + + - {{ key.expiresAt ? (key.expiresAt | date:'dd/MM/yy HH:mm' :'UTC') : '—' }} + + + + + + + + {{ key.expiresAt ? (key.expiresAt | date:'dd/MM/yy HH:mm':'UTC') : '—' }} {{ key.createdAt | date:'dd/MM/yy HH:mm' :'UTC' }}
+ +
+
+ + +
+
- - Loading... - No API Keys found. - +
+
+ + + + +
+ +
diff --git a/frontend/src/app/app-management/api-keys/api-keys.component.ts b/frontend/src/app/app-management/api-keys/api-keys.component.ts index c2d305776..f3ac14e08 100644 --- a/frontend/src/app/app-management/api-keys/api-keys.component.ts +++ b/frontend/src/app/app-management/api-keys/api-keys.component.ts @@ -1,15 +1,15 @@ import {Component, OnInit, TemplateRef, ViewChild} from '@angular/core'; import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; import * as moment from 'moment'; -import {UtmToastService} from "../../shared/alert/utm-toast.service"; +import {UtmToastService} from '../../shared/alert/utm-toast.service'; import { ModalConfirmationComponent -} from "../../shared/components/utm/util/modal-confirmation/modal-confirmation.component"; +} from '../../shared/components/utm/util/modal-confirmation/modal-confirmation.component'; import {ITEMS_PER_PAGE} from '../../shared/constants/pagination.constants'; import {SortEvent} from '../../shared/directives/sortable/type/sort-event'; -import { ApiKeyModalComponent } from './shared/components/api-key-modal/api-key-modal.component'; -import { ApiKeyResponse } from './shared/models/ApiKeyResponse'; -import { ApiKeysService } from './shared/service/api-keys.service'; +import {ApiKeyModalComponent} from './shared/components/api-key-modal/api-key-modal.component'; +import {ApiKeyResponse} from './shared/models/ApiKeyResponse'; +import {ApiKeysService} from './shared/service/api-keys.service'; @Component({ selector: 'app-api-keys', @@ -19,18 +19,23 @@ import { ApiKeysService } from './shared/service/api-keys.service'; export class ApiKeysComponent implements OnInit { generating: string[] = []; + noData = false; apiKeys: ApiKeyResponse[] = []; loading = false; generatedApiKey = ''; @ViewChild('generatedModal') generatedModal!: TemplateRef; - request = - { - sort: 'createdAt,desc', - page: 0, - size: ITEMS_PER_PAGE - }; generatedModalRef!: NgbModalRef; copied = false; + readonly itemsPerPage = ITEMS_PER_PAGE; + totalItems = 0; + page = 0; + size = this.itemsPerPage; + + request = { + sort: 'createdAt,desc', + page: this.page, + size: this.size + }; constructor( private toastService: UtmToastService, private apiKeyService: ApiKeysService, @@ -43,9 +48,11 @@ export class ApiKeysComponent implements OnInit { loadKeys(): void { this.loading = true; - this.apiKeyService.list().subscribe({ + this.apiKeyService.list(this.request).subscribe({ next: (res) => { + this.totalItems = Number(res.headers.get('X-Total-Count')); this.apiKeys = res.body || []; + this.noData = this.apiKeys.length === 0; this.loading = false; }, error: () => { @@ -113,15 +120,17 @@ export class ApiKeysComponent implements OnInit { getDaysUntilExpire(expiresAt: string): number { if (!expiresAt) { - return 0; + return -1; } - const today = moment(); - const expireDate = moment(expiresAt); + + const today = moment().startOf('day'); + const expireDate = moment(expiresAt).startOf('day'); return expireDate.diff(today, 'days'); } - onSortBy($event: SortEvent | string) { - + onSortBy($event: SortEvent) { + this.request.sort = $event.column + ',' + $event.direction; + this.loadKeys(); } maskSecrets(str: string): string { @@ -160,4 +169,23 @@ export class ApiKeysComponent implements OnInit { this.copied = false; this.generatedApiKey = ''; } + + loadPage($event: number) { + this.page = $event - 1; + this.request = { + ...this.request, + page: this.page + }; + this.loadKeys(); + } + + onItemsPerPageChange($event: number) { + this.request = { + ...this.request, + size: $event, + page: 0 + }; + this.page = 0; + this.loadKeys(); + } } diff --git a/frontend/src/app/app-management/api-keys/shared/service/api-keys.service.ts b/frontend/src/app/app-management/api-keys/shared/service/api-keys.service.ts index e668367ba..e239d5e4d 100644 --- a/frontend/src/app/app-management/api-keys/shared/service/api-keys.service.ts +++ b/frontend/src/app/app-management/api-keys/shared/service/api-keys.service.ts @@ -4,6 +4,7 @@ import { Observable } from 'rxjs'; import { SERVER_API_URL } from '../../../../app.constants'; import { ApiKeyResponse } from '../models/ApiKeyResponse'; import { ApiKeyUpsert } from '../models/ApiKeyUpsert'; +import {createRequestOption} from "../../../../shared/util/request-util"; /** * Service for managing API keys @@ -53,9 +54,10 @@ export class ApiKeysService { * List all API keys (with optional pagination) */ list(params?: any): Observable> { + const httpParams = createRequestOption(params); return this.http.get( this.resourceUrl, - { observe: 'response', params } + { observe: 'response', params: httpParams }, ); } From c0884eb45c2483ff44e5f313ac6c11a86f9ac5c7 Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Tue, 28 Oct 2025 14:29:59 -0500 Subject: [PATCH 096/128] feat(api_keys): simplify API key listing endpoint and enhance expiration handling --- backend/.gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/.gitignore b/backend/.gitignore index 834dd8821..518edd788 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -62,6 +62,7 @@ local.properties *.orig classes/ out/ +.nvim/ ###################### # Visual Studio Code From f37cc493a438f58c061be71b12229642cffa162f Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Tue, 28 Oct 2025 14:30:48 -0500 Subject: [PATCH 097/128] feat(api_keys): simplify API key listing endpoint and enhance expiration handling --- backend/.nvim/.env | 18 --- backend/mvnw | 286 --------------------------------------------- backend/mvnw.cmd | 161 ------------------------- 3 files changed, 465 deletions(-) delete mode 100644 backend/.nvim/.env delete mode 100755 backend/mvnw delete mode 100755 backend/mvnw.cmd diff --git a/backend/.nvim/.env b/backend/.nvim/.env deleted file mode 100644 index 628f21921..000000000 --- a/backend/.nvim/.env +++ /dev/null @@ -1,18 +0,0 @@ -revision=4 -AD_AUDIT_SERVICE=http://localhost:8081/api -DB_HOST=192.168.1.18 -DB_NAME=utmstack -DB_PASS=DF7JOMKyU6oNwmy4 -DB_PORT=5432 -DB_USER=postgres -ELASTICSEARCH_HOST=192.168.1.18 -ELASTICSEARCH_PORT=9200 -ENCRYPTION_KEY=nWkGxX1vRTDyZc1Jm59YUkR3KsUmbYrJ -EVENT_PROCESSOR_HOST=192.168.1.18 -EVENT_PROCESSOR_PORT=9002 -GRPC_AGENT_MANAGER_HOST=192.168.1.18 -GRPC_AGENT_MANAGER_PORT=9000 -INTERNAL_KEY=qNANPjjNm7eantt7sgld0iSWFFeGKz5i -LOGSTASH_URL=http://localhost:9600 -SERVER_NAME=UTM -SOC_AI_BASE_URL=http://localhost:8081/process diff --git a/backend/mvnw b/backend/mvnw deleted file mode 100755 index 5551fde8e..000000000 --- a/backend/mvnw +++ /dev/null @@ -1,286 +0,0 @@ -#!/bin/sh -# ---------------------------------------------------------------------------- -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you 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. -# ---------------------------------------------------------------------------- - -# ---------------------------------------------------------------------------- -# Maven2 Start Up Batch script -# -# Required ENV vars: -# ------------------ -# JAVA_HOME - location of a JDK home dir -# -# Optional ENV vars -# ----------------- -# M2_HOME - location of maven2's installed home dir -# MAVEN_OPTS - parameters passed to the Java VM when running Maven -# e.g. to debug Maven itself, use -# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -# MAVEN_SKIP_RC - flag to disable loading of mavenrc files -# ---------------------------------------------------------------------------- - -if [ -z "$MAVEN_SKIP_RC" ] ; then - - if [ -f /etc/mavenrc ] ; then - . /etc/mavenrc - fi - - if [ -f "$HOME/.mavenrc" ] ; then - . "$HOME/.mavenrc" - fi - -fi - -# OS specific support. $var _must_ be set to either true or false. -cygwin=false; -darwin=false; -mingw=false -case "`uname`" in - CYGWIN*) cygwin=true ;; - MINGW*) mingw=true;; - Darwin*) darwin=true - # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home - # See https://developer.apple.com/library/mac/qa/qa1170/_index.html - if [ -z "$JAVA_HOME" ]; then - if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" - else - export JAVA_HOME="/Library/Java/Home" - fi - fi - ;; -esac - -if [ -z "$JAVA_HOME" ] ; then - if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` - fi -fi - -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - PRG="$0" - - # need this for relative symlinks - while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG="`dirname "$PRG"`/$link" - fi - done - - saveddir=`pwd` - - M2_HOME=`dirname "$PRG"`/.. - - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` - - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi - -# For Cygwin, ensure paths are in UNIX format before anything is touched -if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` -fi - -# For Mingw, ensure paths are in UNIX format before anything is touched -if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" - # TODO classpath? -fi - -if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then - # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then - if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" - else - javaExecutable="`readlink -f \"$javaExecutable\"`" - fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` - JAVA_HOME="$javaHome" - export JAVA_HOME - fi - fi -fi - -if [ -z "$JAVACMD" ] ; then - if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - else - JAVACMD="`which java`" - fi -fi - -if [ ! -x "$JAVACMD" ] ; then - echo "Error: JAVA_HOME is not defined correctly." >&2 - echo " We cannot execute $JAVACMD" >&2 - exit 1 -fi - -if [ -z "$JAVA_HOME" ] ; then - echo "Warning: JAVA_HOME environment variable is not set." -fi - -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher - -# traverses directory structure from process work directory to filesystem root -# first directory with .mvn subdirectory is considered project base directory -find_maven_basedir() { - - if [ -z "$1" ] - then - echo "Path not specified to find_maven_basedir" - return 1 - fi - - basedir="$1" - wdir="$1" - while [ "$wdir" != '/' ] ; do - if [ -d "$wdir"/.mvn ] ; then - basedir=$wdir - break - fi - # workaround for JBEAP-8937 (on Solaris 10/Sparc) - if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` - fi - # end of workaround - done - echo "${basedir}" -} - -# concatenates all lines of a file -concat_lines() { - if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" - fi -} - -BASE_DIR=`find_maven_basedir "$(pwd)"` -if [ -z "$BASE_DIR" ]; then - exit 1; -fi - -########################################################################################## -# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -# This allows using the maven wrapper in projects that prohibit checking in binary data. -########################################################################################## -if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found .mvn/wrapper/maven-wrapper.jar" - fi -else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." - fi - jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" - while IFS="=" read key value; do - case "$key" in (wrapperUrl) jarUrl="$value"; break ;; - esac - done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" - if [ "$MVNW_VERBOSE" = true ]; then - echo "Downloading from: $jarUrl" - fi - wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" - - if command -v wget > /dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found wget ... using wget" - fi - wget "$jarUrl" -O "$wrapperJarPath" - elif command -v curl > /dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found curl ... using curl" - fi - curl -o "$wrapperJarPath" "$jarUrl" - else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Falling back to using Java to download" - fi - javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" - if [ -e "$javaClass" ]; then - if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Compiling MavenWrapperDownloader.java ..." - fi - # Compiling the Java class - ("$JAVA_HOME/bin/javac" "$javaClass") - fi - if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - # Running the downloader - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Running MavenWrapperDownloader.java ..." - fi - ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") - fi - fi - fi -fi -########################################################################################## -# End of extension -########################################################################################## - -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -if [ "$MVNW_VERBOSE" = true ]; then - echo $MAVEN_PROJECTBASEDIR -fi -MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" - -# For Cygwin, switch paths to Windows format before running java -if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` - [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` -fi - -WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -exec "$JAVACMD" \ - $MAVEN_OPTS \ - -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ - ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/backend/mvnw.cmd b/backend/mvnw.cmd deleted file mode 100755 index e5cfb0ae9..000000000 --- a/backend/mvnw.cmd +++ /dev/null @@ -1,161 +0,0 @@ -@REM ---------------------------------------------------------------------------- -@REM Licensed to the Apache Software Foundation (ASF) under one -@REM or more contributor license agreements. See the NOTICE file -@REM distributed with this work for additional information -@REM regarding copyright ownership. The ASF licenses this file -@REM to you under the Apache License, Version 2.0 (the -@REM "License"); you may not use this file except in compliance -@REM with the License. You may obtain a copy of the License at -@REM -@REM http://www.apache.org/licenses/LICENSE-2.0 -@REM -@REM Unless required by applicable law or agreed to in writing, -@REM software distributed under the License is distributed on an -@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -@REM KIND, either express or implied. See the License for the -@REM specific language governing permissions and limitations -@REM under the License. -@REM ---------------------------------------------------------------------------- - -@REM ---------------------------------------------------------------------------- -@REM Maven2 Start Up Batch script -@REM -@REM Required ENV vars: -@REM JAVA_HOME - location of a JDK home dir -@REM -@REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir -@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending -@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven -@REM e.g. to debug Maven itself, use -@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files -@REM ---------------------------------------------------------------------------- - -@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' -@echo off -@REM set title of command window -title %0 -@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' -@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% - -@REM set %HOME% to equivalent of $HOME -if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") - -@REM Execute a user defined script before this one -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre -@REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" -:skipRcPre - -@setlocal - -set ERROR_CODE=0 - -@REM To isolate internal variables from possible post scripts, we use another setlocal -@setlocal - -@REM ==== START VALIDATION ==== -if not "%JAVA_HOME%" == "" goto OkJHome - -echo. -echo Error: JAVA_HOME not found in your environment. >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -:OkJHome -if exist "%JAVA_HOME%\bin\java.exe" goto init - -echo. -echo Error: JAVA_HOME is set to an invalid directory. >&2 -echo JAVA_HOME = "%JAVA_HOME%" >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -@REM ==== END VALIDATION ==== - -:init - -@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". -@REM Fallback to current working directory if not found. - -set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% -IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir - -set EXEC_DIR=%CD% -set WDIR=%EXEC_DIR% -:findBaseDir -IF EXIST "%WDIR%"\.mvn goto baseDirFound -cd .. -IF "%WDIR%"=="%CD%" goto baseDirNotFound -set WDIR=%CD% -goto findBaseDir - -:baseDirFound -set MAVEN_PROJECTBASEDIR=%WDIR% -cd "%EXEC_DIR%" -goto endDetectBaseDir - -:baseDirNotFound -set MAVEN_PROJECTBASEDIR=%EXEC_DIR% -cd "%EXEC_DIR%" - -:endDetectBaseDir - -IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig - -@setlocal EnableExtensions EnableDelayedExpansion -for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a -@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% - -:endReadAdditionalConfig - -SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" -set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" -set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" -FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO ( - IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B -) - -@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -@REM This allows using the maven wrapper in projects that prohibit checking in binary data. -if exist %WRAPPER_JAR% ( - echo Found %WRAPPER_JAR% -) else ( - echo Couldn't find %WRAPPER_JAR%, downloading it ... - echo Downloading from: %DOWNLOAD_URL% - powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')" - echo Finished downloading %WRAPPER_JAR% -) -@REM End of extension - -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* -if ERRORLEVEL 1 goto error -goto end - -:error -set ERROR_CODE=1 - -:end -@endlocal & set ERROR_CODE=%ERROR_CODE% - -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost -@REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" -:skipRcPost - -@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause - -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% - -exit /B %ERROR_CODE% From d6eaefb1e59c6ebe53b81a941df5d6d07d9593ef Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Tue, 28 Oct 2025 14:32:44 -0500 Subject: [PATCH 098/128] feat(api_keys): simplify API key listing endpoint and enhance expiration handling --- backend/mvnw | 286 +++++++++++++++++++++++++++++++++++++++++++++++ backend/mvnw.cmd | 161 ++++++++++++++++++++++++++ 2 files changed, 447 insertions(+) create mode 100755 backend/mvnw create mode 100755 backend/mvnw.cmd diff --git a/backend/mvnw b/backend/mvnw new file mode 100755 index 000000000..5551fde8e --- /dev/null +++ b/backend/mvnw @@ -0,0 +1,286 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven2 Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" + # TODO classpath? +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + wget "$jarUrl" -O "$wrapperJarPath" + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + curl -o "$wrapperJarPath" "$jarUrl" + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/backend/mvnw.cmd b/backend/mvnw.cmd new file mode 100755 index 000000000..e5cfb0ae9 --- /dev/null +++ b/backend/mvnw.cmd @@ -0,0 +1,161 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven2 Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" +FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + echo Found %WRAPPER_JAR% +) else ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')" + echo Finished downloading %WRAPPER_JAR% +) +@REM End of extension + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% From 09d3de3de6ad60414170f2985a897a7a5772321b Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Tue, 28 Oct 2025 14:37:48 -0500 Subject: [PATCH 099/128] feat(api_keys): simplify API key listing endpoint and enhance expiration handling --- backend/mvnw | 286 ----------------------------------------------- backend/mvnw.cmd | 161 -------------------------- 2 files changed, 447 deletions(-) delete mode 100755 backend/mvnw delete mode 100755 backend/mvnw.cmd diff --git a/backend/mvnw b/backend/mvnw deleted file mode 100755 index 5551fde8e..000000000 --- a/backend/mvnw +++ /dev/null @@ -1,286 +0,0 @@ -#!/bin/sh -# ---------------------------------------------------------------------------- -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you 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. -# ---------------------------------------------------------------------------- - -# ---------------------------------------------------------------------------- -# Maven2 Start Up Batch script -# -# Required ENV vars: -# ------------------ -# JAVA_HOME - location of a JDK home dir -# -# Optional ENV vars -# ----------------- -# M2_HOME - location of maven2's installed home dir -# MAVEN_OPTS - parameters passed to the Java VM when running Maven -# e.g. to debug Maven itself, use -# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -# MAVEN_SKIP_RC - flag to disable loading of mavenrc files -# ---------------------------------------------------------------------------- - -if [ -z "$MAVEN_SKIP_RC" ] ; then - - if [ -f /etc/mavenrc ] ; then - . /etc/mavenrc - fi - - if [ -f "$HOME/.mavenrc" ] ; then - . "$HOME/.mavenrc" - fi - -fi - -# OS specific support. $var _must_ be set to either true or false. -cygwin=false; -darwin=false; -mingw=false -case "`uname`" in - CYGWIN*) cygwin=true ;; - MINGW*) mingw=true;; - Darwin*) darwin=true - # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home - # See https://developer.apple.com/library/mac/qa/qa1170/_index.html - if [ -z "$JAVA_HOME" ]; then - if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" - else - export JAVA_HOME="/Library/Java/Home" - fi - fi - ;; -esac - -if [ -z "$JAVA_HOME" ] ; then - if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` - fi -fi - -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - PRG="$0" - - # need this for relative symlinks - while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG="`dirname "$PRG"`/$link" - fi - done - - saveddir=`pwd` - - M2_HOME=`dirname "$PRG"`/.. - - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` - - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi - -# For Cygwin, ensure paths are in UNIX format before anything is touched -if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` -fi - -# For Mingw, ensure paths are in UNIX format before anything is touched -if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" - # TODO classpath? -fi - -if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then - # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then - if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" - else - javaExecutable="`readlink -f \"$javaExecutable\"`" - fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` - JAVA_HOME="$javaHome" - export JAVA_HOME - fi - fi -fi - -if [ -z "$JAVACMD" ] ; then - if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - else - JAVACMD="`which java`" - fi -fi - -if [ ! -x "$JAVACMD" ] ; then - echo "Error: JAVA_HOME is not defined correctly." >&2 - echo " We cannot execute $JAVACMD" >&2 - exit 1 -fi - -if [ -z "$JAVA_HOME" ] ; then - echo "Warning: JAVA_HOME environment variable is not set." -fi - -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher - -# traverses directory structure from process work directory to filesystem root -# first directory with .mvn subdirectory is considered project base directory -find_maven_basedir() { - - if [ -z "$1" ] - then - echo "Path not specified to find_maven_basedir" - return 1 - fi - - basedir="$1" - wdir="$1" - while [ "$wdir" != '/' ] ; do - if [ -d "$wdir"/.mvn ] ; then - basedir=$wdir - break - fi - # workaround for JBEAP-8937 (on Solaris 10/Sparc) - if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` - fi - # end of workaround - done - echo "${basedir}" -} - -# concatenates all lines of a file -concat_lines() { - if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" - fi -} - -BASE_DIR=`find_maven_basedir "$(pwd)"` -if [ -z "$BASE_DIR" ]; then - exit 1; -fi - -########################################################################################## -# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -# This allows using the maven wrapper in projects that prohibit checking in binary data. -########################################################################################## -if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found .mvn/wrapper/maven-wrapper.jar" - fi -else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." - fi - jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" - while IFS="=" read key value; do - case "$key" in (wrapperUrl) jarUrl="$value"; break ;; - esac - done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" - if [ "$MVNW_VERBOSE" = true ]; then - echo "Downloading from: $jarUrl" - fi - wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" - - if command -v wget > /dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found wget ... using wget" - fi - wget "$jarUrl" -O "$wrapperJarPath" - elif command -v curl > /dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found curl ... using curl" - fi - curl -o "$wrapperJarPath" "$jarUrl" - else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Falling back to using Java to download" - fi - javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" - if [ -e "$javaClass" ]; then - if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Compiling MavenWrapperDownloader.java ..." - fi - # Compiling the Java class - ("$JAVA_HOME/bin/javac" "$javaClass") - fi - if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - # Running the downloader - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Running MavenWrapperDownloader.java ..." - fi - ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") - fi - fi - fi -fi -########################################################################################## -# End of extension -########################################################################################## - -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -if [ "$MVNW_VERBOSE" = true ]; then - echo $MAVEN_PROJECTBASEDIR -fi -MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" - -# For Cygwin, switch paths to Windows format before running java -if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` - [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` -fi - -WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -exec "$JAVACMD" \ - $MAVEN_OPTS \ - -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ - ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/backend/mvnw.cmd b/backend/mvnw.cmd deleted file mode 100755 index e5cfb0ae9..000000000 --- a/backend/mvnw.cmd +++ /dev/null @@ -1,161 +0,0 @@ -@REM ---------------------------------------------------------------------------- -@REM Licensed to the Apache Software Foundation (ASF) under one -@REM or more contributor license agreements. See the NOTICE file -@REM distributed with this work for additional information -@REM regarding copyright ownership. The ASF licenses this file -@REM to you under the Apache License, Version 2.0 (the -@REM "License"); you may not use this file except in compliance -@REM with the License. You may obtain a copy of the License at -@REM -@REM http://www.apache.org/licenses/LICENSE-2.0 -@REM -@REM Unless required by applicable law or agreed to in writing, -@REM software distributed under the License is distributed on an -@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -@REM KIND, either express or implied. See the License for the -@REM specific language governing permissions and limitations -@REM under the License. -@REM ---------------------------------------------------------------------------- - -@REM ---------------------------------------------------------------------------- -@REM Maven2 Start Up Batch script -@REM -@REM Required ENV vars: -@REM JAVA_HOME - location of a JDK home dir -@REM -@REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir -@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending -@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven -@REM e.g. to debug Maven itself, use -@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files -@REM ---------------------------------------------------------------------------- - -@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' -@echo off -@REM set title of command window -title %0 -@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' -@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% - -@REM set %HOME% to equivalent of $HOME -if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") - -@REM Execute a user defined script before this one -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre -@REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" -:skipRcPre - -@setlocal - -set ERROR_CODE=0 - -@REM To isolate internal variables from possible post scripts, we use another setlocal -@setlocal - -@REM ==== START VALIDATION ==== -if not "%JAVA_HOME%" == "" goto OkJHome - -echo. -echo Error: JAVA_HOME not found in your environment. >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -:OkJHome -if exist "%JAVA_HOME%\bin\java.exe" goto init - -echo. -echo Error: JAVA_HOME is set to an invalid directory. >&2 -echo JAVA_HOME = "%JAVA_HOME%" >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -@REM ==== END VALIDATION ==== - -:init - -@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". -@REM Fallback to current working directory if not found. - -set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% -IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir - -set EXEC_DIR=%CD% -set WDIR=%EXEC_DIR% -:findBaseDir -IF EXIST "%WDIR%"\.mvn goto baseDirFound -cd .. -IF "%WDIR%"=="%CD%" goto baseDirNotFound -set WDIR=%CD% -goto findBaseDir - -:baseDirFound -set MAVEN_PROJECTBASEDIR=%WDIR% -cd "%EXEC_DIR%" -goto endDetectBaseDir - -:baseDirNotFound -set MAVEN_PROJECTBASEDIR=%EXEC_DIR% -cd "%EXEC_DIR%" - -:endDetectBaseDir - -IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig - -@setlocal EnableExtensions EnableDelayedExpansion -for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a -@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% - -:endReadAdditionalConfig - -SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" -set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" -set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" -FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO ( - IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B -) - -@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -@REM This allows using the maven wrapper in projects that prohibit checking in binary data. -if exist %WRAPPER_JAR% ( - echo Found %WRAPPER_JAR% -) else ( - echo Couldn't find %WRAPPER_JAR%, downloading it ... - echo Downloading from: %DOWNLOAD_URL% - powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')" - echo Finished downloading %WRAPPER_JAR% -) -@REM End of extension - -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* -if ERRORLEVEL 1 goto error -goto end - -:error -set ERROR_CODE=1 - -:end -@endlocal & set ERROR_CODE=%ERROR_CODE% - -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost -@REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" -:skipRcPost - -@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause - -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% - -exit /B %ERROR_CODE% From 306e412255837f931f5ea7fe20e1910e9a7d6b0d Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Tue, 28 Oct 2025 14:39:25 -0500 Subject: [PATCH 100/128] feat(api_keys): simplify API key listing endpoint and enhance expiration handling --- backend/.gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/.gitignore b/backend/.gitignore index 518edd788..8de108186 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -138,6 +138,8 @@ Desktop.ini # Maven Wrapper ###################### !.mvn/wrapper/maven-wrapper.jar +mvnw +mvn.cmd ###################### # ESLint From ce46deb85e14372889e42437ac69c2fdcaefb6d4 Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Tue, 28 Oct 2025 14:40:04 -0500 Subject: [PATCH 101/128] feat(api_keys): simplify API key listing endpoint and enhance expiration handling --- backend/.gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/.gitignore b/backend/.gitignore index 8de108186..1e4c485c4 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -139,7 +139,7 @@ Desktop.ini ###################### !.mvn/wrapper/maven-wrapper.jar mvnw -mvn.cmd +mvnw.cmd ###################### # ESLint From 05206e19cfa93880cf11f8138903e72289a5dd1b Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Tue, 28 Oct 2025 14:40:51 -0500 Subject: [PATCH 102/128] feat(api_keys): simplify API key listing endpoint and enhance expiration handling --- backend/.gitignore | 2 -- 1 file changed, 2 deletions(-) diff --git a/backend/.gitignore b/backend/.gitignore index 1e4c485c4..518edd788 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -138,8 +138,6 @@ Desktop.ini # Maven Wrapper ###################### !.mvn/wrapper/maven-wrapper.jar -mvnw -mvnw.cmd ###################### # ESLint From 4a9bff74af6b0baf201ffa4d70f72890301eef92 Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Tue, 28 Oct 2025 14:41:55 -0500 Subject: [PATCH 103/128] feat(api_keys): simplify API key listing endpoint and enhance expiration handling --- backend/mvnw | 286 +++++++++++++++++++++++++++++++++++++++++++++++ backend/mvnw.cmd | 161 ++++++++++++++++++++++++++ 2 files changed, 447 insertions(+) create mode 100644 backend/mvnw create mode 100644 backend/mvnw.cmd diff --git a/backend/mvnw b/backend/mvnw new file mode 100644 index 000000000..5551fde8e --- /dev/null +++ b/backend/mvnw @@ -0,0 +1,286 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven2 Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" + # TODO classpath? +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + wget "$jarUrl" -O "$wrapperJarPath" + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + curl -o "$wrapperJarPath" "$jarUrl" + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/backend/mvnw.cmd b/backend/mvnw.cmd new file mode 100644 index 000000000..e5cfb0ae9 --- /dev/null +++ b/backend/mvnw.cmd @@ -0,0 +1,161 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven2 Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" +FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + echo Found %WRAPPER_JAR% +) else ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')" + echo Finished downloading %WRAPPER_JAR% +) +@REM End of extension + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% From 28c7340ace961b2f9c8455ce98e27a5d04721687 Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Tue, 28 Oct 2025 14:43:23 -0500 Subject: [PATCH 104/128] Update frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../shared/components/api-key-modal/api-key-modal.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts index 87289aee6..f2378c13d 100644 --- a/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts +++ b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts @@ -54,7 +54,7 @@ export class ApiKeyModalComponent implements OnInit { const trimmedIp = this.ipInput.trim(); if (!trimmedIp) { - this.ipInputError = 'Please enter an IP address or CIDR'; // Se asigna el error + this.ipInputError = 'Please enter an IP address or CIDR'; // Error is assigned return; } From 48034876e9ab2d8b2e344b1bd28663905eba0f52 Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Tue, 28 Oct 2025 14:45:14 -0500 Subject: [PATCH 105/128] Update backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../java/com/park/utmstack/security/api_key/ApiKeyFilter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java b/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java index 576a2556a..185db7321 100644 --- a/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java +++ b/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java @@ -49,7 +49,7 @@ public class ApiKeyFilter extends OncePerRequestFilter { private final UserRepository userRepository; private final ApiKeyService apiKeyService; - private final ConcurrentMap invalidApiKeyBlackList = new ConcurrentHashMap<>();; + private final ConcurrentMap invalidApiKeyBlackList = new ConcurrentHashMap<>(); private final ConcurrentMap cidrCache = new ConcurrentHashMap<>(); private final ApplicationEventService applicationEventService; private final ApiKeyUsageLoggingService apiKeyUsageLoggingService; From fba61ec493bb28539f2aea83d13ad46140537a73 Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Tue, 28 Oct 2025 14:46:12 -0500 Subject: [PATCH 106/128] feat(api_keys): simplify API key listing endpoint and enhance expiration handling --- .../java/com/park/utmstack/web/rest/api_key/ApiKeyResource.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/java/com/park/utmstack/web/rest/api_key/ApiKeyResource.java b/backend/src/main/java/com/park/utmstack/web/rest/api_key/ApiKeyResource.java index d0bdd0d2b..aa6b2052a 100644 --- a/backend/src/main/java/com/park/utmstack/web/rest/api_key/ApiKeyResource.java +++ b/backend/src/main/java/com/park/utmstack/web/rest/api_key/ApiKeyResource.java @@ -161,7 +161,7 @@ public ResponseEntity deleteApiKey(@PathVariable("id") Long apiKeyId) { return ResponseEntity.noContent().build(); } - @GetMapping("/usage") + @PostMapping("/usage") public ResponseEntity> search(@RequestBody(required = false) List filters, @RequestParam Integer top, @RequestParam String indexPattern, @RequestParam(required = false, defaultValue = "false") boolean includeChildren, From 147d702d721c0af863f58905dcf261367978170e Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Wed, 29 Oct 2025 10:48:15 -0500 Subject: [PATCH 107/128] feat(api_keys): enhance clipboard functionality with fallback support and feedback Signed-off-by: Manuel Abascal --- .../api-keys/api-keys.component.ts | 47 +++++++++++++++++-- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/app-management/api-keys/api-keys.component.ts b/frontend/src/app/app-management/api-keys/api-keys.component.ts index f3ac14e08..82d9e3450 100644 --- a/frontend/src/app/app-management/api-keys/api-keys.component.ts +++ b/frontend/src/app/app-management/api-keys/api-keys.component.ts @@ -63,9 +63,50 @@ export class ApiKeysComponent implements OnInit { } copyToClipboard(): void { - (navigator as any).clipboard.writeText(this.generatedApiKey).then(() => { - this.copied = true; - }); + if (!this.generatedApiKey) { return; } + + if (navigator && (navigator as any).clipboard && (navigator as any).clipboard.writeText) { + (navigator as any).clipboard.writeText(this.generatedApiKey) + .then(() => this.copied = true) + .catch(err => { + console.error('Error al copiar con clipboard API', err); + this.fallbackCopy(this.generatedApiKey); + }); + } else { + this.fallbackCopy(this.generatedApiKey); + } + } + + private fallbackCopy(text: string): void { + try { + const textarea = document.createElement('textarea'); + textarea.value = text; + + textarea.style.position = 'fixed'; + textarea.style.top = '0'; + textarea.style.left = '0'; + textarea.style.opacity = '0'; + + document.body.appendChild(textarea); + textarea.focus(); + textarea.select(); + + const successful = document.execCommand('copy'); + document.body.removeChild(textarea); + + if (successful) { + this.showCopiedFeedback(); + } else { + console.warn('Fallback copy failed'); + } + } catch (err) { + console.error('Error en fallback copy', err); + } + } + + private showCopiedFeedback(): void { + this.copied = true; + setTimeout(() => this.copied = false, 2000); } openCreateModal(): void { From 01ae5d56f415c4a30f2d6efa4cc43f0ced51fd30 Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Wed, 29 Oct 2025 10:52:59 -0500 Subject: [PATCH 108/128] feat(api_key): enhance ApiKeyFilter with improved logging and validation checks --- .../domain/shared_types/ApplicationLayer.java | 1 + .../security/api_key/ApiKeyFilter.java | 67 +++++++++++-------- 2 files changed, 41 insertions(+), 27 deletions(-) diff --git a/backend/src/main/java/com/park/utmstack/domain/shared_types/ApplicationLayer.java b/backend/src/main/java/com/park/utmstack/domain/shared_types/ApplicationLayer.java index 16bfac1cb..ed1a2af7f 100644 --- a/backend/src/main/java/com/park/utmstack/domain/shared_types/ApplicationLayer.java +++ b/backend/src/main/java/com/park/utmstack/domain/shared_types/ApplicationLayer.java @@ -7,6 +7,7 @@ @Getter public enum ApplicationLayer { SERVICE ("SERVICE"), + API ("API"), CONTROLLER ("CONTROLLER"); private final String value; diff --git a/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java b/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java index 185db7321..acefca689 100644 --- a/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java +++ b/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java @@ -3,6 +3,7 @@ import com.park.utmstack.config.Constants; import com.park.utmstack.domain.api_keys.ApiKey; +import com.park.utmstack.domain.shared_types.ApplicationLayer; import com.park.utmstack.loggin.api_key.ApiKeyUsageLoggingService; import com.park.utmstack.repository.UserRepository; import com.park.utmstack.service.api_key.ApiKeyService; @@ -31,6 +32,8 @@ import java.io.IOException; import java.time.Instant; import java.util.List; +import java.util.Map; +import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.regex.Pattern; @@ -44,9 +47,10 @@ public class ApiKeyFilter extends OncePerRequestFilter { private static final String LOG_USAGE_FLAG = "LOG_USAGE_DONE"; private static final Pattern CIDR_PATTERN = Pattern.compile( - "^((25[0-5]|2[0-4]\\d|[01]?\\d\\d?)(\\.(?!$)|$)){4}/(\\d|[1-2]\\d|3[0-2])$" + "^((25[0-5]|2[0-4]\\d|1\\d{2}|[1-9]?\\d)\\.){3}(25[0-5]|2[0-4]\\d|1\\d{2}|[1-9]?\\d)/(\\d|[1-2]\\d|3[0-2])$" ); + private final UserRepository userRepository; private final ApiKeyService apiKeyService; private final ConcurrentMap invalidApiKeyBlackList = new ConcurrentHashMap<>(); @@ -103,45 +107,54 @@ protected void doFilterInternal(@NonNull HttpServletRequest request, private ApiKey getApiKey(String apiKey) { if (invalidApiKeyBlackList.containsKey(apiKey)) { + log.info("API key invalid (cached)"); throw new ApiKeyInvalidAccessException("Invalid API key"); } + return apiKeyService.findOneByApiKey(apiKey) - .orElseThrow(() -> { - invalidApiKeyBlackList.put(apiKey, Boolean.TRUE); - return new ApiKeyInvalidAccessException("Invalid API key"); - }); + .orElseGet(() -> { + invalidApiKeyBlackList.put(apiKey, Boolean.TRUE); + log.info("API key invalid (not found)"); + throw new ApiKeyInvalidAccessException("Invalid API key"); + }); } public UsernamePasswordAuthenticationToken getAuthentication(ApiKey apiKey, String remoteIpAddress) { - try { - - if (!allowAccessToRemoteIp(apiKey.getAllowedIp(), remoteIpAddress)) { - throw new ApiKeyInvalidAccessException("Invalid IP address, if you recognize this IP address, add to allowed ip list"); - } + Objects.requireNonNull(apiKey, "API key must not be null"); + Objects.requireNonNull(remoteIpAddress, "Remote IP address must not be null"); + + if (!allowAccessToRemoteIp(apiKey.getAllowedIp(), remoteIpAddress)) { + log.warn("Access denied: IP [{}] not allowed for API key [{}]", remoteIpAddress, apiKey.getApiKey()); + throw new ApiKeyInvalidAccessException( + "Invalid IP address: " + remoteIpAddress + ". If you recognize this IP, add it to allowed IP list." + ); + } - if (apiKey.getExpiresAt() != null && !apiKey.getExpiresAt().isAfter(Instant.now())) { - throw new ApiKeyInvalidAccessException("API key expired at " + apiKey.getExpiresAt()); - } + if (apiKey.getExpiresAt() != null && !apiKey.getExpiresAt().isAfter(Instant.now())) { + log.warn("Access denied: API key [{}] expired at {}", apiKey.getApiKey(), apiKey.getExpiresAt()); + throw new ApiKeyInvalidAccessException("API key expired at " + apiKey.getExpiresAt()); + } - var userEntity = userRepository - .findById(apiKey.getUserId()) - .orElseThrow(() -> new ApiKeyInvalidAccessException("User not found for api key")); + var userEntityOpt = userRepository.findById(apiKey.getUserId()); + if (userEntityOpt.isEmpty()) { + log.warn("Access denied: User [{}] not found for API key [{}]", apiKey.getUserId(), apiKey.getApiKey()); + throw new ApiKeyInvalidAccessException("User not found for API key"); + } - if (!userEntity.getActivated()) { - throw new ApiKeyInvalidAccessException("User not activated"); - } + var userEntity = userEntityOpt.get(); - List authorities = userEntity.getAuthorities().stream() - .map(auth -> new SimpleGrantedAuthority(auth.getName())) - .toList(); + if (!userEntity.getActivated()) { + log.warn("Access denied: User [{}] not activated", userEntity.getLogin()); + throw new ApiKeyInvalidAccessException("User not activated"); + } - User principal = new User(userEntity.getLogin(), "", authorities); + List authorities = userEntity.getAuthorities().stream() + .map(auth -> new SimpleGrantedAuthority(auth.getName())) + .toList(); - return new UsernamePasswordAuthenticationToken(principal, apiKey.getApiKey(), authorities); + User principal = new User(userEntity.getLogin(), "", authorities); - } catch (Exception e) { - throw new ApiKeyInvalidAccessException(e.getMessage()); - } + return new UsernamePasswordAuthenticationToken(principal, apiKey.getApiKey(), authorities); } public boolean allowAccessToRemoteIp(String allowedIpList, String remoteIp) { From 32946dfc704d95fb4116b0625a28bfbda90127ca Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Wed, 29 Oct 2025 11:06:00 -0500 Subject: [PATCH 109/128] feat(api_key): enhance ApiKeyFilter with improved logging and validation checks --- .../java/com/park/utmstack/security/api_key/ApiKeyFilter.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java b/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java index acefca689..8f5582528 100644 --- a/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java +++ b/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java @@ -107,14 +107,14 @@ protected void doFilterInternal(@NonNull HttpServletRequest request, private ApiKey getApiKey(String apiKey) { if (invalidApiKeyBlackList.containsKey(apiKey)) { - log.info("API key invalid (cached)"); + log.warn("Access attempt with invalid API key (cached)"); throw new ApiKeyInvalidAccessException("Invalid API key"); } return apiKeyService.findOneByApiKey(apiKey) .orElseGet(() -> { invalidApiKeyBlackList.put(apiKey, Boolean.TRUE); - log.info("API key invalid (not found)"); + log.warn("Access attempt with invalid API key (not found in DB)"); throw new ApiKeyInvalidAccessException("Invalid API key"); }); } From 1da1e1871645406d5e5f7e9a7ce2a95632f29363 Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Wed, 29 Oct 2025 11:10:24 -0500 Subject: [PATCH 110/128] feat(api_key): enhance ApiKeyFilter with improved logging and validation checks --- .../java/com/park/utmstack/security/api_key/ApiKeyFilter.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java b/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java index 8f5582528..5e5fd9100 100644 --- a/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java +++ b/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java @@ -3,7 +3,6 @@ import com.park.utmstack.config.Constants; import com.park.utmstack.domain.api_keys.ApiKey; -import com.park.utmstack.domain.shared_types.ApplicationLayer; import com.park.utmstack.loggin.api_key.ApiKeyUsageLoggingService; import com.park.utmstack.repository.UserRepository; import com.park.utmstack.service.api_key.ApiKeyService; @@ -12,7 +11,6 @@ import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.net.util.SubnetUtils; -import org.springframework.core.annotation.Order; import org.springframework.lang.NonNull; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.authority.SimpleGrantedAuthority; @@ -32,7 +30,6 @@ import java.io.IOException; import java.time.Instant; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -55,7 +52,6 @@ public class ApiKeyFilter extends OncePerRequestFilter { private final ApiKeyService apiKeyService; private final ConcurrentMap invalidApiKeyBlackList = new ConcurrentHashMap<>(); private final ConcurrentMap cidrCache = new ConcurrentHashMap<>(); - private final ApplicationEventService applicationEventService; private final ApiKeyUsageLoggingService apiKeyUsageLoggingService; From fbd7af412f5bf19314d2b27fc3afeb75eb52508e Mon Sep 17 00:00:00 2001 From: AlexSanchez-bit Date: Fri, 17 Oct 2025 10:08:23 -0400 Subject: [PATCH 111/128] feat[backend](api-keys): added api keys dto, controllers and entities --- backend/.nvim/.env | 18 ++ backend/mvnw | 0 backend/mvnw.cmd | 0 .../domain/api_keys/UtmApiKeyModel.java | 50 +++++ .../dto/api_key/UtmApiKeyResponseDto.java | 36 ++++ .../dto/api_key/UtmApiKeyUpsertDto.java | 29 +++ .../rest/api-keys/UtmApiKeyController.java | 178 ++++++++++++++++++ .../changelog/20250917001_adding_api_keys.xml | 43 +++++ .../config/liquibase/scripts/tables.sql | 21 +++ 9 files changed, 375 insertions(+) create mode 100644 backend/.nvim/.env mode change 100644 => 100755 backend/mvnw mode change 100644 => 100755 backend/mvnw.cmd create mode 100644 backend/src/main/java/com/park/utmstack/domain/api_keys/UtmApiKeyModel.java create mode 100644 backend/src/main/java/com/park/utmstack/service/dto/api_key/UtmApiKeyResponseDto.java create mode 100644 backend/src/main/java/com/park/utmstack/service/dto/api_key/UtmApiKeyUpsertDto.java create mode 100644 backend/src/main/java/com/park/utmstack/web/rest/api-keys/UtmApiKeyController.java create mode 100644 backend/src/main/resources/config/liquibase/changelog/20250917001_adding_api_keys.xml diff --git a/backend/.nvim/.env b/backend/.nvim/.env new file mode 100644 index 000000000..628f21921 --- /dev/null +++ b/backend/.nvim/.env @@ -0,0 +1,18 @@ +revision=4 +AD_AUDIT_SERVICE=http://localhost:8081/api +DB_HOST=192.168.1.18 +DB_NAME=utmstack +DB_PASS=DF7JOMKyU6oNwmy4 +DB_PORT=5432 +DB_USER=postgres +ELASTICSEARCH_HOST=192.168.1.18 +ELASTICSEARCH_PORT=9200 +ENCRYPTION_KEY=nWkGxX1vRTDyZc1Jm59YUkR3KsUmbYrJ +EVENT_PROCESSOR_HOST=192.168.1.18 +EVENT_PROCESSOR_PORT=9002 +GRPC_AGENT_MANAGER_HOST=192.168.1.18 +GRPC_AGENT_MANAGER_PORT=9000 +INTERNAL_KEY=qNANPjjNm7eantt7sgld0iSWFFeGKz5i +LOGSTASH_URL=http://localhost:9600 +SERVER_NAME=UTM +SOC_AI_BASE_URL=http://localhost:8081/process diff --git a/backend/mvnw b/backend/mvnw old mode 100644 new mode 100755 diff --git a/backend/mvnw.cmd b/backend/mvnw.cmd old mode 100644 new mode 100755 diff --git a/backend/src/main/java/com/park/utmstack/domain/api_keys/UtmApiKeyModel.java b/backend/src/main/java/com/park/utmstack/domain/api_keys/UtmApiKeyModel.java new file mode 100644 index 000000000..8f532da04 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/domain/api_keys/UtmApiKeyModel.java @@ -0,0 +1,50 @@ +package com.park.utmstack.domain.api_keys; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.time.Instant; +import java.util.UUID; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "api_keys") +public class ApiKey implements Serializable { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + @Column(columnDefinition = "UUID") + private UUID id; + + @Column(nullable = false) + private UUID accountId; + + @Column(nullable = false) + private String name; + + @Column(nullable = false) + private String apiKey; + + @Column + private String allowedIp; + + @Column(nullable = false) + private Instant createdAt; + + private Instant generatedAt; + + @Column + private Instant expiresAt; +} diff --git a/backend/src/main/java/com/park/utmstack/service/dto/api_key/UtmApiKeyResponseDto.java b/backend/src/main/java/com/park/utmstack/service/dto/api_key/UtmApiKeyResponseDto.java new file mode 100644 index 000000000..232e77f56 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/service/dto/api_key/UtmApiKeyResponseDto.java @@ -0,0 +1,36 @@ +package com.utmstack.api.service.dto.apikey; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.Instant; +import java.util.List; +import java.util.UUID; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ApiKeyResponseDTO { + + @Schema(description = "Unique identifier of the API key") + private UUID id; + + @Schema(description = "User-friendly API key name") + private String name; + + @Schema(description = "Allowed IP address or IP range in CIDR notation (e.g., '192.168.1.100' or '192.168.1.0/24')") + private List allowedIp; + + @Schema(description = "API key creation timestamp") + private Instant createdAt; + + @Schema(description = "API key expiration timestamp (if applicable)") + private Instant expiresAt; + + @Schema(description = "Generated At") + private Instant generatedAt; +} diff --git a/backend/src/main/java/com/park/utmstack/service/dto/api_key/UtmApiKeyUpsertDto.java b/backend/src/main/java/com/park/utmstack/service/dto/api_key/UtmApiKeyUpsertDto.java new file mode 100644 index 000000000..518aa6e35 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/service/dto/api_key/UtmApiKeyUpsertDto.java @@ -0,0 +1,29 @@ +package com.utmstack.api.service.dto.apikey; + + +import com.utmstack.api.annotation.ValidIPOrCIDR; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.Instant; +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ApiKeyUpsertDTO { + @NotNull + @Schema(description = "API Key name", requiredMode = Schema.RequiredMode.REQUIRED) + private String name; + + @Schema(description = "Allowed IP address or IP range in CIDR notation (e.g., '192.168.1.100' or '192.168.1.0/24'). If null, no IP restrictions are applied.") + private List<@ValidIPOrCIDR String> allowedIp; + + @Schema(description = "Expiration timestamp of the API key") + private Instant expiresAt; +} diff --git a/backend/src/main/java/com/park/utmstack/web/rest/api-keys/UtmApiKeyController.java b/backend/src/main/java/com/park/utmstack/web/rest/api-keys/UtmApiKeyController.java new file mode 100644 index 000000000..27c48af2f --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/web/rest/api-keys/UtmApiKeyController.java @@ -0,0 +1,178 @@ +@RestController +@RequestMapping("/api/api-keys") +@PreAuthorize("hasAuthority(\"" + AuthoritiesConstants.USER + "\")") +@AllArgsConstructor +@Hidden +public class ApiKeyResource { + + private static final String CLASSNAME = "ApiKeyResource"; + private final Logger log = LoggerFactory.getLogger(ApiKeyResource.class); + + private final ApiKeyService apiKeyService; + private final UserService userService; + + private UUID getCurrentAccountId() throws CurrentUserLoginNotFoundException { + User user = userService.getUserWithAuthoritiesByLogin(SecurityUtils.currentUserLogin()); + return UUID.fromString(user.getAccountId()); + } + + @Operation(summary = "Create API key", + description = "Creates a new API key record using the provided settings. The plain text key is not generated at creation.") + @ApiResponses({ + @ApiResponse(responseCode = "201", description = "API key created successfully", + content = @Content(schema = @Schema(implementation = ApiKeyResponseDTO.class))), + @ApiResponse(responseCode = "409", description = "API key already exists", content = @Content), + @ApiResponse(responseCode = "500", description = "Internal server error", + content = @Content, headers = { + @Header(name = "X-App-Error", description = "Technical error details") + }) + }) + @PostMapping + public ResponseEntity createApiKey(@RequestBody ApiKeyUpsertDTO dto) { + final String ctx = CLASSNAME + ".createApiKey"; + try { + UUID accountId = getCurrentAccountId(); + ApiKeyResponseDTO responseDTO = apiKeyService.createApiKey(accountId, dto); + return ResponseEntity.status(HttpStatus.CREATED).body(responseDTO); + } catch (ApiKeyExistException e) { + return ResponseUtil.buildConflictResponse(e.getMessage()); + } catch (Exception e) { + String msg = ctx + ": " + e.getMessage(); + log.error(msg, e); + return ResponseUtil.buildInternalServerErrorResponse(msg); + } + } + + @Operation(summary = "Generate a new API key", + description = "Generates (or renews) a new random API key for the specified API key record. The plain text key is returned only once.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "API key generated successfully", + content = @Content(schema = @Schema(type = "string"))), + @ApiResponse(responseCode = "404", description = "API key not found", content = @Content), + @ApiResponse(responseCode = "500", description = "Internal server error", + content = @Content, headers = { + @Header(name = "X-App-Error", description = "Technical error details") + }) + }) + @PostMapping("/{id}/generate") + public ResponseEntity generateApiKey(@PathVariable("id") UUID apiKeyId) { + final String ctx = CLASSNAME + ".generateApiKey"; + try { + UUID accountId = getCurrentAccountId(); + String plainKey = apiKeyService.generateApiKey(accountId, apiKeyId); + return ResponseEntity.ok(plainKey); + } catch (ApiKeyNotFoundException e) { + return ResponseUtil.buildNotFoundResponse(e.getMessage()); + } catch (Exception e) { + String msg = ctx + ": " + e.getMessage(); + log.error(msg, e); + return ResponseUtil.buildInternalServerErrorResponse(msg); + } + } + + @Operation(summary = "Retrieve API key", + description = "Retrieves the API key details for the specified API key record.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "API key retrieved successfully", + content = @Content(schema = @Schema(implementation = ApiKeyResponseDTO.class))), + @ApiResponse(responseCode = "404", description = "API key not found", content = @Content), + @ApiResponse(responseCode = "500", description = "Internal server error", + content = @Content, headers = { + @Header(name = "X-App-Error", description = "Technical error details") + }) + }) + @GetMapping("/{id}") + public ResponseEntity getApiKey(@PathVariable("id") UUID apiKeyId) { + final String ctx = CLASSNAME + ".getApiKey"; + try { + UUID accountId = getCurrentAccountId(); + ApiKeyResponseDTO responseDTO = apiKeyService.getApiKey(accountId, apiKeyId); + return ResponseEntity.ok(responseDTO); + } catch (ApiKeyNotFoundException e) { + return ResponseUtil.buildNotFoundResponse(e.getMessage()); + } catch (Exception e) { + String msg = ctx + ": " + e.getMessage(); + log.error(msg, e); + return ResponseUtil.buildInternalServerErrorResponse(msg); + } + } + + @Operation(summary = "List API keys", + description = "Retrieves the API key list.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "API key retrieved successfully", + content = @Content(schema = @Schema(implementation = ApiKeyResponseDTO.class))), + @ApiResponse(responseCode = "404", description = "API key not found", content = @Content), + @ApiResponse(responseCode = "500", description = "Internal server error", + content = @Content, headers = { + @Header(name = "X-App-Error", description = "Technical error details") + }) + }) + @GetMapping("") + public ResponseEntity> listApiKeys(@ParameterObject Pageable pageable) { + final String ctx = CLASSNAME + ".listApiKeys"; + try { + UUID accountId = getCurrentAccountId(); + Page page = apiKeyService.listApiKeys(accountId, pageable); + HttpHeaders headers = PaginationUtil.generatePaginationHttpHeaders(ServletUriComponentsBuilder.fromCurrentRequest(), page); + return ResponseEntity.ok().headers(headers).body(page.getContent()); + } catch (Exception e) { + String msg = ctx + ": " + e.getMessage(); + log.error(msg, e); + return ResponseUtil.buildInternalServerErrorResponse(msg); + } + } + + @Operation(summary = "Update API key", + description = "Updates mutable fields (name, allowed IPs, expiration) for the specified API key record.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "API key updated successfully", + content = @Content(schema = @Schema(implementation = ApiKeyResponseDTO.class))), + @ApiResponse(responseCode = "404", description = "API key not found", content = @Content), + @ApiResponse(responseCode = "500", description = "Internal server error", + content = @Content, headers = { + @Header(name = "X-App-Error", description = "Technical error details") + }) + }) + @PutMapping("/{id}") + public ResponseEntity updateApiKey(@PathVariable("id") UUID apiKeyId, + @RequestBody ApiKeyUpsertDTO dto) { + final String ctx = CLASSNAME + ".updateApiKey"; + try { + UUID accountId = getCurrentAccountId(); + ApiKeyResponseDTO responseDTO = apiKeyService.updateApiKey(accountId, apiKeyId, dto); + return ResponseEntity.ok(responseDTO); + } catch (ApiKeyNotFoundException e) { + return ResponseUtil.buildNotFoundResponse(e.getMessage()); + } catch (Exception e) { + String msg = ctx + ": " + e.getMessage(); + log.error(msg, e); + return ResponseUtil.buildInternalServerErrorResponse(msg); + } + } + + @Operation(summary = "Delete API key", + description = "Deletes the specified API key record for the authenticated user.") + @ApiResponses({ + @ApiResponse(responseCode = "204", description = "API key deleted successfully", content = @Content), + @ApiResponse(responseCode = "404", description = "API key not found", content = @Content), + @ApiResponse(responseCode = "500", description = "Internal server error", + content = @Content, headers = { + @Header(name = "X-App-Error", description = "Technical error details") + }) + }) + @DeleteMapping("/{id}") + public ResponseEntity deleteApiKey(@PathVariable("id") UUID apiKeyId) { + final String ctx = CLASSNAME + ".deleteApiKey"; + try { + UUID accountId = getCurrentAccountId(); + apiKeyService.deleteApiKey(accountId, apiKeyId); + return ResponseEntity.noContent().build(); + } catch (ApiKeyNotFoundException e) { + return ResponseUtil.buildNotFoundResponse(e.getMessage()); + } catch (Exception e) { + String msg = ctx + ": " + e.getMessage(); + log.error(msg, e); + return ResponseUtil.buildInternalServerErrorResponse(msg); + } + } diff --git a/backend/src/main/resources/config/liquibase/changelog/20250917001_adding_api_keys.xml b/backend/src/main/resources/config/liquibase/changelog/20250917001_adding_api_keys.xml new file mode 100644 index 000000000..cb1c2cd75 --- /dev/null +++ b/backend/src/main/resources/config/liquibase/changelog/20250917001_adding_api_keys.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO api_keys (account_id, name, api_key, created_at) + SELECT account_id::uuid, 'DefaultApiKey', api_key, CURRENT_TIMESTAMP + FROM jhi_user + WHERE api_key IS NOT NULL; + + + + + + + diff --git a/backend/src/main/resources/config/liquibase/scripts/tables.sql b/backend/src/main/resources/config/liquibase/scripts/tables.sql index f50a695d1..629971010 100644 --- a/backend/src/main/resources/config/liquibase/scripts/tables.sql +++ b/backend/src/main/resources/config/liquibase/scripts/tables.sql @@ -12,6 +12,7 @@ DROP TABLE IF EXISTS public.utm_gvm_scan_result; DROP TABLE IF EXISTS public.utm_gvm_task; DROP TABLE IF EXISTS public.utm_module_modal; DROP TABLE IF EXISTS public.utm_system_restart; +DROP TABLE IF EXISTS public.utm_api_keys; CREATE TABLE IF NOT EXISTS public.jhi_authority ( @@ -830,3 +831,23 @@ CREATE TABLE IF NOT EXISTS public.utm_space_notification_control next_notification timestamp without time zone NOT NULL, CONSTRAINT utm_space_notification_control_pkey PRIMARY KEY (id) ); + +CREATE TABLE IF NOT EXISTS public.utm_api_keys +( + id uuid default uuid_generate_v4() not null + primary key, + account_id uuid not null, + name varchar(255) not null, + api_key varchar(255) not null, + allowed_ip text, + created_at timestamp not null, + expires_at timestamp, + generated_at timestamp +); + +alter table utm_api_keys + owner to postgres; + +create unique index uk_api_keys_api_key + on utm_api_keys (api_key); + From 9146c77c42e4464b7bd6f8ca84f5c0c046cbdb4e Mon Sep 17 00:00:00 2001 From: AlexSanchez-bit Date: Fri, 17 Oct 2025 11:48:55 -0400 Subject: [PATCH 112/128] feat[backend](api_keys): added api keys --- .../domain/api_keys/UtmApiKeyModel.java | 50 ------------------- .../dto/api_key/UtmApiKeyResponseDto.java | 36 ------------- .../dto/api_key/UtmApiKeyUpsertDto.java | 29 ----------- .../rest/api-keys/UtmApiKeyController.java | 1 + 4 files changed, 1 insertion(+), 115 deletions(-) delete mode 100644 backend/src/main/java/com/park/utmstack/domain/api_keys/UtmApiKeyModel.java delete mode 100644 backend/src/main/java/com/park/utmstack/service/dto/api_key/UtmApiKeyResponseDto.java delete mode 100644 backend/src/main/java/com/park/utmstack/service/dto/api_key/UtmApiKeyUpsertDto.java diff --git a/backend/src/main/java/com/park/utmstack/domain/api_keys/UtmApiKeyModel.java b/backend/src/main/java/com/park/utmstack/domain/api_keys/UtmApiKeyModel.java deleted file mode 100644 index 8f532da04..000000000 --- a/backend/src/main/java/com/park/utmstack/domain/api_keys/UtmApiKeyModel.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.park.utmstack.domain.api_keys; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.Table; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.io.Serializable; -import java.time.Instant; -import java.util.UUID; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -@Entity -@Table(name = "api_keys") -public class ApiKey implements Serializable { - - @Id - @GeneratedValue(strategy = GenerationType.AUTO) - @Column(columnDefinition = "UUID") - private UUID id; - - @Column(nullable = false) - private UUID accountId; - - @Column(nullable = false) - private String name; - - @Column(nullable = false) - private String apiKey; - - @Column - private String allowedIp; - - @Column(nullable = false) - private Instant createdAt; - - private Instant generatedAt; - - @Column - private Instant expiresAt; -} diff --git a/backend/src/main/java/com/park/utmstack/service/dto/api_key/UtmApiKeyResponseDto.java b/backend/src/main/java/com/park/utmstack/service/dto/api_key/UtmApiKeyResponseDto.java deleted file mode 100644 index 232e77f56..000000000 --- a/backend/src/main/java/com/park/utmstack/service/dto/api_key/UtmApiKeyResponseDto.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.utmstack.api.service.dto.apikey; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.time.Instant; -import java.util.List; -import java.util.UUID; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class ApiKeyResponseDTO { - - @Schema(description = "Unique identifier of the API key") - private UUID id; - - @Schema(description = "User-friendly API key name") - private String name; - - @Schema(description = "Allowed IP address or IP range in CIDR notation (e.g., '192.168.1.100' or '192.168.1.0/24')") - private List allowedIp; - - @Schema(description = "API key creation timestamp") - private Instant createdAt; - - @Schema(description = "API key expiration timestamp (if applicable)") - private Instant expiresAt; - - @Schema(description = "Generated At") - private Instant generatedAt; -} diff --git a/backend/src/main/java/com/park/utmstack/service/dto/api_key/UtmApiKeyUpsertDto.java b/backend/src/main/java/com/park/utmstack/service/dto/api_key/UtmApiKeyUpsertDto.java deleted file mode 100644 index 518aa6e35..000000000 --- a/backend/src/main/java/com/park/utmstack/service/dto/api_key/UtmApiKeyUpsertDto.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.utmstack.api.service.dto.apikey; - - -import com.utmstack.api.annotation.ValidIPOrCIDR; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotNull; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.time.Instant; -import java.util.List; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class ApiKeyUpsertDTO { - @NotNull - @Schema(description = "API Key name", requiredMode = Schema.RequiredMode.REQUIRED) - private String name; - - @Schema(description = "Allowed IP address or IP range in CIDR notation (e.g., '192.168.1.100' or '192.168.1.0/24'). If null, no IP restrictions are applied.") - private List<@ValidIPOrCIDR String> allowedIp; - - @Schema(description = "Expiration timestamp of the API key") - private Instant expiresAt; -} diff --git a/backend/src/main/java/com/park/utmstack/web/rest/api-keys/UtmApiKeyController.java b/backend/src/main/java/com/park/utmstack/web/rest/api-keys/UtmApiKeyController.java index 27c48af2f..253fabc76 100644 --- a/backend/src/main/java/com/park/utmstack/web/rest/api-keys/UtmApiKeyController.java +++ b/backend/src/main/java/com/park/utmstack/web/rest/api-keys/UtmApiKeyController.java @@ -176,3 +176,4 @@ public ResponseEntity deleteApiKey(@PathVariable("id") UUID apiKeyId) { return ResponseUtil.buildInternalServerErrorResponse(msg); } } +} From 7b7a279bad00d785baa5d185cf0a385cb8e19d9b Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Fri, 17 Oct 2025 11:02:50 -0500 Subject: [PATCH 113/128] feat(api_keys): create api_keys table with user_id and add foreign key constraint --- .../changelog/20250917001_adding_api_keys.xml | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/backend/src/main/resources/config/liquibase/changelog/20250917001_adding_api_keys.xml b/backend/src/main/resources/config/liquibase/changelog/20250917001_adding_api_keys.xml index cb1c2cd75..2b996f83c 100644 --- a/backend/src/main/resources/config/liquibase/changelog/20250917001_adding_api_keys.xml +++ b/backend/src/main/resources/config/liquibase/changelog/20250917001_adding_api_keys.xml @@ -4,12 +4,12 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd"> - + - + @@ -24,20 +24,17 @@ + - - - - INSERT INTO api_keys (account_id, name, api_key, created_at) - SELECT account_id::uuid, 'DefaultApiKey', api_key, CURRENT_TIMESTAMP - FROM jhi_user - WHERE api_key IS NOT NULL; - - - - - + + From ff1f189b82baf67f55126bdcbb738853959bff6f Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Fri, 17 Oct 2025 12:13:17 -0500 Subject: [PATCH 114/128] feat(api_keys): implement API key management with CRUD operations and validation --- .../rest/api-keys/UtmApiKeyController.java | 179 ------------------ .../changelog/20250917001_adding_api_keys.xml | 40 ---- 2 files changed, 219 deletions(-) delete mode 100644 backend/src/main/java/com/park/utmstack/web/rest/api-keys/UtmApiKeyController.java delete mode 100644 backend/src/main/resources/config/liquibase/changelog/20250917001_adding_api_keys.xml diff --git a/backend/src/main/java/com/park/utmstack/web/rest/api-keys/UtmApiKeyController.java b/backend/src/main/java/com/park/utmstack/web/rest/api-keys/UtmApiKeyController.java deleted file mode 100644 index 253fabc76..000000000 --- a/backend/src/main/java/com/park/utmstack/web/rest/api-keys/UtmApiKeyController.java +++ /dev/null @@ -1,179 +0,0 @@ -@RestController -@RequestMapping("/api/api-keys") -@PreAuthorize("hasAuthority(\"" + AuthoritiesConstants.USER + "\")") -@AllArgsConstructor -@Hidden -public class ApiKeyResource { - - private static final String CLASSNAME = "ApiKeyResource"; - private final Logger log = LoggerFactory.getLogger(ApiKeyResource.class); - - private final ApiKeyService apiKeyService; - private final UserService userService; - - private UUID getCurrentAccountId() throws CurrentUserLoginNotFoundException { - User user = userService.getUserWithAuthoritiesByLogin(SecurityUtils.currentUserLogin()); - return UUID.fromString(user.getAccountId()); - } - - @Operation(summary = "Create API key", - description = "Creates a new API key record using the provided settings. The plain text key is not generated at creation.") - @ApiResponses({ - @ApiResponse(responseCode = "201", description = "API key created successfully", - content = @Content(schema = @Schema(implementation = ApiKeyResponseDTO.class))), - @ApiResponse(responseCode = "409", description = "API key already exists", content = @Content), - @ApiResponse(responseCode = "500", description = "Internal server error", - content = @Content, headers = { - @Header(name = "X-App-Error", description = "Technical error details") - }) - }) - @PostMapping - public ResponseEntity createApiKey(@RequestBody ApiKeyUpsertDTO dto) { - final String ctx = CLASSNAME + ".createApiKey"; - try { - UUID accountId = getCurrentAccountId(); - ApiKeyResponseDTO responseDTO = apiKeyService.createApiKey(accountId, dto); - return ResponseEntity.status(HttpStatus.CREATED).body(responseDTO); - } catch (ApiKeyExistException e) { - return ResponseUtil.buildConflictResponse(e.getMessage()); - } catch (Exception e) { - String msg = ctx + ": " + e.getMessage(); - log.error(msg, e); - return ResponseUtil.buildInternalServerErrorResponse(msg); - } - } - - @Operation(summary = "Generate a new API key", - description = "Generates (or renews) a new random API key for the specified API key record. The plain text key is returned only once.") - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "API key generated successfully", - content = @Content(schema = @Schema(type = "string"))), - @ApiResponse(responseCode = "404", description = "API key not found", content = @Content), - @ApiResponse(responseCode = "500", description = "Internal server error", - content = @Content, headers = { - @Header(name = "X-App-Error", description = "Technical error details") - }) - }) - @PostMapping("/{id}/generate") - public ResponseEntity generateApiKey(@PathVariable("id") UUID apiKeyId) { - final String ctx = CLASSNAME + ".generateApiKey"; - try { - UUID accountId = getCurrentAccountId(); - String plainKey = apiKeyService.generateApiKey(accountId, apiKeyId); - return ResponseEntity.ok(plainKey); - } catch (ApiKeyNotFoundException e) { - return ResponseUtil.buildNotFoundResponse(e.getMessage()); - } catch (Exception e) { - String msg = ctx + ": " + e.getMessage(); - log.error(msg, e); - return ResponseUtil.buildInternalServerErrorResponse(msg); - } - } - - @Operation(summary = "Retrieve API key", - description = "Retrieves the API key details for the specified API key record.") - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "API key retrieved successfully", - content = @Content(schema = @Schema(implementation = ApiKeyResponseDTO.class))), - @ApiResponse(responseCode = "404", description = "API key not found", content = @Content), - @ApiResponse(responseCode = "500", description = "Internal server error", - content = @Content, headers = { - @Header(name = "X-App-Error", description = "Technical error details") - }) - }) - @GetMapping("/{id}") - public ResponseEntity getApiKey(@PathVariable("id") UUID apiKeyId) { - final String ctx = CLASSNAME + ".getApiKey"; - try { - UUID accountId = getCurrentAccountId(); - ApiKeyResponseDTO responseDTO = apiKeyService.getApiKey(accountId, apiKeyId); - return ResponseEntity.ok(responseDTO); - } catch (ApiKeyNotFoundException e) { - return ResponseUtil.buildNotFoundResponse(e.getMessage()); - } catch (Exception e) { - String msg = ctx + ": " + e.getMessage(); - log.error(msg, e); - return ResponseUtil.buildInternalServerErrorResponse(msg); - } - } - - @Operation(summary = "List API keys", - description = "Retrieves the API key list.") - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "API key retrieved successfully", - content = @Content(schema = @Schema(implementation = ApiKeyResponseDTO.class))), - @ApiResponse(responseCode = "404", description = "API key not found", content = @Content), - @ApiResponse(responseCode = "500", description = "Internal server error", - content = @Content, headers = { - @Header(name = "X-App-Error", description = "Technical error details") - }) - }) - @GetMapping("") - public ResponseEntity> listApiKeys(@ParameterObject Pageable pageable) { - final String ctx = CLASSNAME + ".listApiKeys"; - try { - UUID accountId = getCurrentAccountId(); - Page page = apiKeyService.listApiKeys(accountId, pageable); - HttpHeaders headers = PaginationUtil.generatePaginationHttpHeaders(ServletUriComponentsBuilder.fromCurrentRequest(), page); - return ResponseEntity.ok().headers(headers).body(page.getContent()); - } catch (Exception e) { - String msg = ctx + ": " + e.getMessage(); - log.error(msg, e); - return ResponseUtil.buildInternalServerErrorResponse(msg); - } - } - - @Operation(summary = "Update API key", - description = "Updates mutable fields (name, allowed IPs, expiration) for the specified API key record.") - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "API key updated successfully", - content = @Content(schema = @Schema(implementation = ApiKeyResponseDTO.class))), - @ApiResponse(responseCode = "404", description = "API key not found", content = @Content), - @ApiResponse(responseCode = "500", description = "Internal server error", - content = @Content, headers = { - @Header(name = "X-App-Error", description = "Technical error details") - }) - }) - @PutMapping("/{id}") - public ResponseEntity updateApiKey(@PathVariable("id") UUID apiKeyId, - @RequestBody ApiKeyUpsertDTO dto) { - final String ctx = CLASSNAME + ".updateApiKey"; - try { - UUID accountId = getCurrentAccountId(); - ApiKeyResponseDTO responseDTO = apiKeyService.updateApiKey(accountId, apiKeyId, dto); - return ResponseEntity.ok(responseDTO); - } catch (ApiKeyNotFoundException e) { - return ResponseUtil.buildNotFoundResponse(e.getMessage()); - } catch (Exception e) { - String msg = ctx + ": " + e.getMessage(); - log.error(msg, e); - return ResponseUtil.buildInternalServerErrorResponse(msg); - } - } - - @Operation(summary = "Delete API key", - description = "Deletes the specified API key record for the authenticated user.") - @ApiResponses({ - @ApiResponse(responseCode = "204", description = "API key deleted successfully", content = @Content), - @ApiResponse(responseCode = "404", description = "API key not found", content = @Content), - @ApiResponse(responseCode = "500", description = "Internal server error", - content = @Content, headers = { - @Header(name = "X-App-Error", description = "Technical error details") - }) - }) - @DeleteMapping("/{id}") - public ResponseEntity deleteApiKey(@PathVariable("id") UUID apiKeyId) { - final String ctx = CLASSNAME + ".deleteApiKey"; - try { - UUID accountId = getCurrentAccountId(); - apiKeyService.deleteApiKey(accountId, apiKeyId); - return ResponseEntity.noContent().build(); - } catch (ApiKeyNotFoundException e) { - return ResponseUtil.buildNotFoundResponse(e.getMessage()); - } catch (Exception e) { - String msg = ctx + ": " + e.getMessage(); - log.error(msg, e); - return ResponseUtil.buildInternalServerErrorResponse(msg); - } - } -} diff --git a/backend/src/main/resources/config/liquibase/changelog/20250917001_adding_api_keys.xml b/backend/src/main/resources/config/liquibase/changelog/20250917001_adding_api_keys.xml deleted file mode 100644 index 2b996f83c..000000000 --- a/backend/src/main/resources/config/liquibase/changelog/20250917001_adding_api_keys.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From 026266c82432c66a79157167fbb31660a32557b7 Mon Sep 17 00:00:00 2001 From: AlexSanchez-bit Date: Fri, 17 Oct 2025 17:04:44 -0400 Subject: [PATCH 115/128] feat[frontend](api_key): added api key list/creation components --- .../connection-key.component.html | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/app-management/connection-key/connection-key.component.html b/frontend/src/app/app-management/connection-key/connection-key.component.html index aa95f0ae7..441546517 100644 --- a/frontend/src/app/app-management/connection-key/connection-key.component.html +++ b/frontend/src/app/app-management/connection-key/connection-key.component.html @@ -1,4 +1,6 @@ -
+
+ +
Connection key @@ -71,5 +73,21 @@
Connection Key
+
+ + + +
+
+
+ For developers +
+
+ +
+ +
+
+
From ba0ba8902a46bb5a2ebef151156f08719f5def76 Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Mon, 20 Oct 2025 08:33:33 -0500 Subject: [PATCH 116/128] refactor(api_keys): remove unused ApplicationEventService from ApiKeyFilter --- backend/src/main/resources/config/liquibase/scripts/tables.sql | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/src/main/resources/config/liquibase/scripts/tables.sql b/backend/src/main/resources/config/liquibase/scripts/tables.sql index 629971010..7500ca2cf 100644 --- a/backend/src/main/resources/config/liquibase/scripts/tables.sql +++ b/backend/src/main/resources/config/liquibase/scripts/tables.sql @@ -12,7 +12,6 @@ DROP TABLE IF EXISTS public.utm_gvm_scan_result; DROP TABLE IF EXISTS public.utm_gvm_task; DROP TABLE IF EXISTS public.utm_module_modal; DROP TABLE IF EXISTS public.utm_system_restart; -DROP TABLE IF EXISTS public.utm_api_keys; CREATE TABLE IF NOT EXISTS public.jhi_authority ( From 0b9b3a59f368a7cfbc256ad059f4430d3b4bfd15 Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Mon, 20 Oct 2025 08:46:16 -0500 Subject: [PATCH 117/128] refactor(api_keys): update API key table schema and change ID type to BIGINT --- .../config/liquibase/scripts/tables.sql | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/backend/src/main/resources/config/liquibase/scripts/tables.sql b/backend/src/main/resources/config/liquibase/scripts/tables.sql index 7500ca2cf..f50a695d1 100644 --- a/backend/src/main/resources/config/liquibase/scripts/tables.sql +++ b/backend/src/main/resources/config/liquibase/scripts/tables.sql @@ -830,23 +830,3 @@ CREATE TABLE IF NOT EXISTS public.utm_space_notification_control next_notification timestamp without time zone NOT NULL, CONSTRAINT utm_space_notification_control_pkey PRIMARY KEY (id) ); - -CREATE TABLE IF NOT EXISTS public.utm_api_keys -( - id uuid default uuid_generate_v4() not null - primary key, - account_id uuid not null, - name varchar(255) not null, - api_key varchar(255) not null, - allowed_ip text, - created_at timestamp not null, - expires_at timestamp, - generated_at timestamp -); - -alter table utm_api_keys - owner to postgres; - -create unique index uk_api_keys_api_key - on utm_api_keys (api_key); - From 3be2b4becdabd8e9ded3459eff31784d50372d34 Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Thu, 23 Oct 2025 14:26:15 -0500 Subject: [PATCH 118/128] feat(api_keys): enhance API key management UI Signed-off-by: Manuel Abascal --- .../connection-key.component.html | 20 +------------------ 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/frontend/src/app/app-management/connection-key/connection-key.component.html b/frontend/src/app/app-management/connection-key/connection-key.component.html index 441546517..aa95f0ae7 100644 --- a/frontend/src/app/app-management/connection-key/connection-key.component.html +++ b/frontend/src/app/app-management/connection-key/connection-key.component.html @@ -1,6 +1,4 @@ -
- -
+
Connection key @@ -73,21 +71,5 @@
Connection Key
-
- - - -
-
-
- For developers -
-
- -
- -
-
-
From 97913230bd883810dd58e77f7d60a53727b1fde9 Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Sun, 19 Oct 2025 10:07:52 -0500 Subject: [PATCH 119/128] feat(api_keys): implement API key filtering and usage logging for enhanced security --- .../java/com/park/utmstack/security/api_key/ApiKeyFilter.java | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java b/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java index 5e5fd9100..4ab3f5a95 100644 --- a/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java +++ b/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java @@ -52,6 +52,7 @@ public class ApiKeyFilter extends OncePerRequestFilter { private final ApiKeyService apiKeyService; private final ConcurrentMap invalidApiKeyBlackList = new ConcurrentHashMap<>(); private final ConcurrentMap cidrCache = new ConcurrentHashMap<>(); + private final ApplicationEventService applicationEventService; private final ApiKeyUsageLoggingService apiKeyUsageLoggingService; From f249dcecd28477a092cedfcb0757d9976d52eabc Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Thu, 23 Oct 2025 13:41:49 -0500 Subject: [PATCH 120/128] feat(api_keys): enhance API key management with new fields and logging improvements --- .../java/com/park/utmstack/security/api_key/ApiKeyFilter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java b/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java index 4ab3f5a95..352199301 100644 --- a/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java +++ b/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java @@ -50,7 +50,7 @@ public class ApiKeyFilter extends OncePerRequestFilter { private final UserRepository userRepository; private final ApiKeyService apiKeyService; - private final ConcurrentMap invalidApiKeyBlackList = new ConcurrentHashMap<>(); + private final ConcurrentMap invalidApiKeyBlackList = new ConcurrentHashMap<>();; private final ConcurrentMap cidrCache = new ConcurrentHashMap<>(); private final ApplicationEventService applicationEventService; private final ApiKeyUsageLoggingService apiKeyUsageLoggingService; From 686c9faa6bc1342ebc481f0143da60a8b5db4a88 Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Tue, 28 Oct 2025 14:30:48 -0500 Subject: [PATCH 121/128] feat(api_keys): simplify API key listing endpoint and enhance expiration handling --- backend/.nvim/.env | 18 --- backend/mvnw | 286 --------------------------------------------- backend/mvnw.cmd | 161 ------------------------- 3 files changed, 465 deletions(-) delete mode 100644 backend/.nvim/.env delete mode 100755 backend/mvnw delete mode 100755 backend/mvnw.cmd diff --git a/backend/.nvim/.env b/backend/.nvim/.env deleted file mode 100644 index 628f21921..000000000 --- a/backend/.nvim/.env +++ /dev/null @@ -1,18 +0,0 @@ -revision=4 -AD_AUDIT_SERVICE=http://localhost:8081/api -DB_HOST=192.168.1.18 -DB_NAME=utmstack -DB_PASS=DF7JOMKyU6oNwmy4 -DB_PORT=5432 -DB_USER=postgres -ELASTICSEARCH_HOST=192.168.1.18 -ELASTICSEARCH_PORT=9200 -ENCRYPTION_KEY=nWkGxX1vRTDyZc1Jm59YUkR3KsUmbYrJ -EVENT_PROCESSOR_HOST=192.168.1.18 -EVENT_PROCESSOR_PORT=9002 -GRPC_AGENT_MANAGER_HOST=192.168.1.18 -GRPC_AGENT_MANAGER_PORT=9000 -INTERNAL_KEY=qNANPjjNm7eantt7sgld0iSWFFeGKz5i -LOGSTASH_URL=http://localhost:9600 -SERVER_NAME=UTM -SOC_AI_BASE_URL=http://localhost:8081/process diff --git a/backend/mvnw b/backend/mvnw deleted file mode 100755 index 5551fde8e..000000000 --- a/backend/mvnw +++ /dev/null @@ -1,286 +0,0 @@ -#!/bin/sh -# ---------------------------------------------------------------------------- -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you 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. -# ---------------------------------------------------------------------------- - -# ---------------------------------------------------------------------------- -# Maven2 Start Up Batch script -# -# Required ENV vars: -# ------------------ -# JAVA_HOME - location of a JDK home dir -# -# Optional ENV vars -# ----------------- -# M2_HOME - location of maven2's installed home dir -# MAVEN_OPTS - parameters passed to the Java VM when running Maven -# e.g. to debug Maven itself, use -# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -# MAVEN_SKIP_RC - flag to disable loading of mavenrc files -# ---------------------------------------------------------------------------- - -if [ -z "$MAVEN_SKIP_RC" ] ; then - - if [ -f /etc/mavenrc ] ; then - . /etc/mavenrc - fi - - if [ -f "$HOME/.mavenrc" ] ; then - . "$HOME/.mavenrc" - fi - -fi - -# OS specific support. $var _must_ be set to either true or false. -cygwin=false; -darwin=false; -mingw=false -case "`uname`" in - CYGWIN*) cygwin=true ;; - MINGW*) mingw=true;; - Darwin*) darwin=true - # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home - # See https://developer.apple.com/library/mac/qa/qa1170/_index.html - if [ -z "$JAVA_HOME" ]; then - if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" - else - export JAVA_HOME="/Library/Java/Home" - fi - fi - ;; -esac - -if [ -z "$JAVA_HOME" ] ; then - if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` - fi -fi - -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - PRG="$0" - - # need this for relative symlinks - while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG="`dirname "$PRG"`/$link" - fi - done - - saveddir=`pwd` - - M2_HOME=`dirname "$PRG"`/.. - - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` - - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi - -# For Cygwin, ensure paths are in UNIX format before anything is touched -if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` -fi - -# For Mingw, ensure paths are in UNIX format before anything is touched -if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" - # TODO classpath? -fi - -if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then - # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then - if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" - else - javaExecutable="`readlink -f \"$javaExecutable\"`" - fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` - JAVA_HOME="$javaHome" - export JAVA_HOME - fi - fi -fi - -if [ -z "$JAVACMD" ] ; then - if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - else - JAVACMD="`which java`" - fi -fi - -if [ ! -x "$JAVACMD" ] ; then - echo "Error: JAVA_HOME is not defined correctly." >&2 - echo " We cannot execute $JAVACMD" >&2 - exit 1 -fi - -if [ -z "$JAVA_HOME" ] ; then - echo "Warning: JAVA_HOME environment variable is not set." -fi - -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher - -# traverses directory structure from process work directory to filesystem root -# first directory with .mvn subdirectory is considered project base directory -find_maven_basedir() { - - if [ -z "$1" ] - then - echo "Path not specified to find_maven_basedir" - return 1 - fi - - basedir="$1" - wdir="$1" - while [ "$wdir" != '/' ] ; do - if [ -d "$wdir"/.mvn ] ; then - basedir=$wdir - break - fi - # workaround for JBEAP-8937 (on Solaris 10/Sparc) - if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` - fi - # end of workaround - done - echo "${basedir}" -} - -# concatenates all lines of a file -concat_lines() { - if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" - fi -} - -BASE_DIR=`find_maven_basedir "$(pwd)"` -if [ -z "$BASE_DIR" ]; then - exit 1; -fi - -########################################################################################## -# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -# This allows using the maven wrapper in projects that prohibit checking in binary data. -########################################################################################## -if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found .mvn/wrapper/maven-wrapper.jar" - fi -else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." - fi - jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" - while IFS="=" read key value; do - case "$key" in (wrapperUrl) jarUrl="$value"; break ;; - esac - done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" - if [ "$MVNW_VERBOSE" = true ]; then - echo "Downloading from: $jarUrl" - fi - wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" - - if command -v wget > /dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found wget ... using wget" - fi - wget "$jarUrl" -O "$wrapperJarPath" - elif command -v curl > /dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found curl ... using curl" - fi - curl -o "$wrapperJarPath" "$jarUrl" - else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Falling back to using Java to download" - fi - javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" - if [ -e "$javaClass" ]; then - if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Compiling MavenWrapperDownloader.java ..." - fi - # Compiling the Java class - ("$JAVA_HOME/bin/javac" "$javaClass") - fi - if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - # Running the downloader - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Running MavenWrapperDownloader.java ..." - fi - ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") - fi - fi - fi -fi -########################################################################################## -# End of extension -########################################################################################## - -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -if [ "$MVNW_VERBOSE" = true ]; then - echo $MAVEN_PROJECTBASEDIR -fi -MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" - -# For Cygwin, switch paths to Windows format before running java -if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` - [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` -fi - -WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -exec "$JAVACMD" \ - $MAVEN_OPTS \ - -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ - ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/backend/mvnw.cmd b/backend/mvnw.cmd deleted file mode 100755 index e5cfb0ae9..000000000 --- a/backend/mvnw.cmd +++ /dev/null @@ -1,161 +0,0 @@ -@REM ---------------------------------------------------------------------------- -@REM Licensed to the Apache Software Foundation (ASF) under one -@REM or more contributor license agreements. See the NOTICE file -@REM distributed with this work for additional information -@REM regarding copyright ownership. The ASF licenses this file -@REM to you under the Apache License, Version 2.0 (the -@REM "License"); you may not use this file except in compliance -@REM with the License. You may obtain a copy of the License at -@REM -@REM http://www.apache.org/licenses/LICENSE-2.0 -@REM -@REM Unless required by applicable law or agreed to in writing, -@REM software distributed under the License is distributed on an -@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -@REM KIND, either express or implied. See the License for the -@REM specific language governing permissions and limitations -@REM under the License. -@REM ---------------------------------------------------------------------------- - -@REM ---------------------------------------------------------------------------- -@REM Maven2 Start Up Batch script -@REM -@REM Required ENV vars: -@REM JAVA_HOME - location of a JDK home dir -@REM -@REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir -@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending -@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven -@REM e.g. to debug Maven itself, use -@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files -@REM ---------------------------------------------------------------------------- - -@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' -@echo off -@REM set title of command window -title %0 -@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' -@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% - -@REM set %HOME% to equivalent of $HOME -if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") - -@REM Execute a user defined script before this one -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre -@REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" -:skipRcPre - -@setlocal - -set ERROR_CODE=0 - -@REM To isolate internal variables from possible post scripts, we use another setlocal -@setlocal - -@REM ==== START VALIDATION ==== -if not "%JAVA_HOME%" == "" goto OkJHome - -echo. -echo Error: JAVA_HOME not found in your environment. >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -:OkJHome -if exist "%JAVA_HOME%\bin\java.exe" goto init - -echo. -echo Error: JAVA_HOME is set to an invalid directory. >&2 -echo JAVA_HOME = "%JAVA_HOME%" >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -@REM ==== END VALIDATION ==== - -:init - -@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". -@REM Fallback to current working directory if not found. - -set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% -IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir - -set EXEC_DIR=%CD% -set WDIR=%EXEC_DIR% -:findBaseDir -IF EXIST "%WDIR%"\.mvn goto baseDirFound -cd .. -IF "%WDIR%"=="%CD%" goto baseDirNotFound -set WDIR=%CD% -goto findBaseDir - -:baseDirFound -set MAVEN_PROJECTBASEDIR=%WDIR% -cd "%EXEC_DIR%" -goto endDetectBaseDir - -:baseDirNotFound -set MAVEN_PROJECTBASEDIR=%EXEC_DIR% -cd "%EXEC_DIR%" - -:endDetectBaseDir - -IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig - -@setlocal EnableExtensions EnableDelayedExpansion -for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a -@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% - -:endReadAdditionalConfig - -SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" -set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" -set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" -FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO ( - IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B -) - -@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -@REM This allows using the maven wrapper in projects that prohibit checking in binary data. -if exist %WRAPPER_JAR% ( - echo Found %WRAPPER_JAR% -) else ( - echo Couldn't find %WRAPPER_JAR%, downloading it ... - echo Downloading from: %DOWNLOAD_URL% - powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')" - echo Finished downloading %WRAPPER_JAR% -) -@REM End of extension - -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* -if ERRORLEVEL 1 goto error -goto end - -:error -set ERROR_CODE=1 - -:end -@endlocal & set ERROR_CODE=%ERROR_CODE% - -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost -@REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" -:skipRcPost - -@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause - -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% - -exit /B %ERROR_CODE% From 01cce887d3226bd39a5ef999caeb94a4e407ad02 Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Tue, 28 Oct 2025 14:32:44 -0500 Subject: [PATCH 122/128] feat(api_keys): simplify API key listing endpoint and enhance expiration handling --- backend/mvnw | 286 +++++++++++++++++++++++++++++++++++++++++++++++ backend/mvnw.cmd | 161 ++++++++++++++++++++++++++ 2 files changed, 447 insertions(+) create mode 100755 backend/mvnw create mode 100755 backend/mvnw.cmd diff --git a/backend/mvnw b/backend/mvnw new file mode 100755 index 000000000..5551fde8e --- /dev/null +++ b/backend/mvnw @@ -0,0 +1,286 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven2 Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" + # TODO classpath? +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + wget "$jarUrl" -O "$wrapperJarPath" + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + curl -o "$wrapperJarPath" "$jarUrl" + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/backend/mvnw.cmd b/backend/mvnw.cmd new file mode 100755 index 000000000..e5cfb0ae9 --- /dev/null +++ b/backend/mvnw.cmd @@ -0,0 +1,161 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven2 Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" +FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + echo Found %WRAPPER_JAR% +) else ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')" + echo Finished downloading %WRAPPER_JAR% +) +@REM End of extension + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% From c7c284d55a94b93c162ce9a566cadf3d1bd7ab32 Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Tue, 28 Oct 2025 14:37:48 -0500 Subject: [PATCH 123/128] feat(api_keys): simplify API key listing endpoint and enhance expiration handling --- backend/mvnw | 286 ----------------------------------------------- backend/mvnw.cmd | 161 -------------------------- 2 files changed, 447 deletions(-) delete mode 100755 backend/mvnw delete mode 100755 backend/mvnw.cmd diff --git a/backend/mvnw b/backend/mvnw deleted file mode 100755 index 5551fde8e..000000000 --- a/backend/mvnw +++ /dev/null @@ -1,286 +0,0 @@ -#!/bin/sh -# ---------------------------------------------------------------------------- -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you 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. -# ---------------------------------------------------------------------------- - -# ---------------------------------------------------------------------------- -# Maven2 Start Up Batch script -# -# Required ENV vars: -# ------------------ -# JAVA_HOME - location of a JDK home dir -# -# Optional ENV vars -# ----------------- -# M2_HOME - location of maven2's installed home dir -# MAVEN_OPTS - parameters passed to the Java VM when running Maven -# e.g. to debug Maven itself, use -# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -# MAVEN_SKIP_RC - flag to disable loading of mavenrc files -# ---------------------------------------------------------------------------- - -if [ -z "$MAVEN_SKIP_RC" ] ; then - - if [ -f /etc/mavenrc ] ; then - . /etc/mavenrc - fi - - if [ -f "$HOME/.mavenrc" ] ; then - . "$HOME/.mavenrc" - fi - -fi - -# OS specific support. $var _must_ be set to either true or false. -cygwin=false; -darwin=false; -mingw=false -case "`uname`" in - CYGWIN*) cygwin=true ;; - MINGW*) mingw=true;; - Darwin*) darwin=true - # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home - # See https://developer.apple.com/library/mac/qa/qa1170/_index.html - if [ -z "$JAVA_HOME" ]; then - if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" - else - export JAVA_HOME="/Library/Java/Home" - fi - fi - ;; -esac - -if [ -z "$JAVA_HOME" ] ; then - if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` - fi -fi - -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - PRG="$0" - - # need this for relative symlinks - while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG="`dirname "$PRG"`/$link" - fi - done - - saveddir=`pwd` - - M2_HOME=`dirname "$PRG"`/.. - - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` - - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi - -# For Cygwin, ensure paths are in UNIX format before anything is touched -if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` -fi - -# For Mingw, ensure paths are in UNIX format before anything is touched -if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" - # TODO classpath? -fi - -if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then - # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then - if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" - else - javaExecutable="`readlink -f \"$javaExecutable\"`" - fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` - JAVA_HOME="$javaHome" - export JAVA_HOME - fi - fi -fi - -if [ -z "$JAVACMD" ] ; then - if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - else - JAVACMD="`which java`" - fi -fi - -if [ ! -x "$JAVACMD" ] ; then - echo "Error: JAVA_HOME is not defined correctly." >&2 - echo " We cannot execute $JAVACMD" >&2 - exit 1 -fi - -if [ -z "$JAVA_HOME" ] ; then - echo "Warning: JAVA_HOME environment variable is not set." -fi - -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher - -# traverses directory structure from process work directory to filesystem root -# first directory with .mvn subdirectory is considered project base directory -find_maven_basedir() { - - if [ -z "$1" ] - then - echo "Path not specified to find_maven_basedir" - return 1 - fi - - basedir="$1" - wdir="$1" - while [ "$wdir" != '/' ] ; do - if [ -d "$wdir"/.mvn ] ; then - basedir=$wdir - break - fi - # workaround for JBEAP-8937 (on Solaris 10/Sparc) - if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` - fi - # end of workaround - done - echo "${basedir}" -} - -# concatenates all lines of a file -concat_lines() { - if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" - fi -} - -BASE_DIR=`find_maven_basedir "$(pwd)"` -if [ -z "$BASE_DIR" ]; then - exit 1; -fi - -########################################################################################## -# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -# This allows using the maven wrapper in projects that prohibit checking in binary data. -########################################################################################## -if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found .mvn/wrapper/maven-wrapper.jar" - fi -else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." - fi - jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" - while IFS="=" read key value; do - case "$key" in (wrapperUrl) jarUrl="$value"; break ;; - esac - done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" - if [ "$MVNW_VERBOSE" = true ]; then - echo "Downloading from: $jarUrl" - fi - wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" - - if command -v wget > /dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found wget ... using wget" - fi - wget "$jarUrl" -O "$wrapperJarPath" - elif command -v curl > /dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found curl ... using curl" - fi - curl -o "$wrapperJarPath" "$jarUrl" - else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Falling back to using Java to download" - fi - javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" - if [ -e "$javaClass" ]; then - if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Compiling MavenWrapperDownloader.java ..." - fi - # Compiling the Java class - ("$JAVA_HOME/bin/javac" "$javaClass") - fi - if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - # Running the downloader - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Running MavenWrapperDownloader.java ..." - fi - ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") - fi - fi - fi -fi -########################################################################################## -# End of extension -########################################################################################## - -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -if [ "$MVNW_VERBOSE" = true ]; then - echo $MAVEN_PROJECTBASEDIR -fi -MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" - -# For Cygwin, switch paths to Windows format before running java -if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` - [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` -fi - -WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -exec "$JAVACMD" \ - $MAVEN_OPTS \ - -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ - ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/backend/mvnw.cmd b/backend/mvnw.cmd deleted file mode 100755 index e5cfb0ae9..000000000 --- a/backend/mvnw.cmd +++ /dev/null @@ -1,161 +0,0 @@ -@REM ---------------------------------------------------------------------------- -@REM Licensed to the Apache Software Foundation (ASF) under one -@REM or more contributor license agreements. See the NOTICE file -@REM distributed with this work for additional information -@REM regarding copyright ownership. The ASF licenses this file -@REM to you under the Apache License, Version 2.0 (the -@REM "License"); you may not use this file except in compliance -@REM with the License. You may obtain a copy of the License at -@REM -@REM http://www.apache.org/licenses/LICENSE-2.0 -@REM -@REM Unless required by applicable law or agreed to in writing, -@REM software distributed under the License is distributed on an -@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -@REM KIND, either express or implied. See the License for the -@REM specific language governing permissions and limitations -@REM under the License. -@REM ---------------------------------------------------------------------------- - -@REM ---------------------------------------------------------------------------- -@REM Maven2 Start Up Batch script -@REM -@REM Required ENV vars: -@REM JAVA_HOME - location of a JDK home dir -@REM -@REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir -@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending -@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven -@REM e.g. to debug Maven itself, use -@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files -@REM ---------------------------------------------------------------------------- - -@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' -@echo off -@REM set title of command window -title %0 -@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' -@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% - -@REM set %HOME% to equivalent of $HOME -if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") - -@REM Execute a user defined script before this one -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre -@REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" -:skipRcPre - -@setlocal - -set ERROR_CODE=0 - -@REM To isolate internal variables from possible post scripts, we use another setlocal -@setlocal - -@REM ==== START VALIDATION ==== -if not "%JAVA_HOME%" == "" goto OkJHome - -echo. -echo Error: JAVA_HOME not found in your environment. >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -:OkJHome -if exist "%JAVA_HOME%\bin\java.exe" goto init - -echo. -echo Error: JAVA_HOME is set to an invalid directory. >&2 -echo JAVA_HOME = "%JAVA_HOME%" >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -@REM ==== END VALIDATION ==== - -:init - -@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". -@REM Fallback to current working directory if not found. - -set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% -IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir - -set EXEC_DIR=%CD% -set WDIR=%EXEC_DIR% -:findBaseDir -IF EXIST "%WDIR%"\.mvn goto baseDirFound -cd .. -IF "%WDIR%"=="%CD%" goto baseDirNotFound -set WDIR=%CD% -goto findBaseDir - -:baseDirFound -set MAVEN_PROJECTBASEDIR=%WDIR% -cd "%EXEC_DIR%" -goto endDetectBaseDir - -:baseDirNotFound -set MAVEN_PROJECTBASEDIR=%EXEC_DIR% -cd "%EXEC_DIR%" - -:endDetectBaseDir - -IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig - -@setlocal EnableExtensions EnableDelayedExpansion -for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a -@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% - -:endReadAdditionalConfig - -SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" -set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" -set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" -FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO ( - IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B -) - -@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -@REM This allows using the maven wrapper in projects that prohibit checking in binary data. -if exist %WRAPPER_JAR% ( - echo Found %WRAPPER_JAR% -) else ( - echo Couldn't find %WRAPPER_JAR%, downloading it ... - echo Downloading from: %DOWNLOAD_URL% - powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')" - echo Finished downloading %WRAPPER_JAR% -) -@REM End of extension - -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* -if ERRORLEVEL 1 goto error -goto end - -:error -set ERROR_CODE=1 - -:end -@endlocal & set ERROR_CODE=%ERROR_CODE% - -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost -@REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" -:skipRcPost - -@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause - -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% - -exit /B %ERROR_CODE% From 2eb0f889bd8c829dbc072b3c14afcbbcdc4fa14c Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Tue, 28 Oct 2025 14:39:25 -0500 Subject: [PATCH 124/128] feat(api_keys): simplify API key listing endpoint and enhance expiration handling --- backend/.gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/.gitignore b/backend/.gitignore index 518edd788..8de108186 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -138,6 +138,8 @@ Desktop.ini # Maven Wrapper ###################### !.mvn/wrapper/maven-wrapper.jar +mvnw +mvn.cmd ###################### # ESLint From 62f49fe2a7259f6d7451b3eb1bdb596a02a716cd Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Tue, 28 Oct 2025 14:40:04 -0500 Subject: [PATCH 125/128] feat(api_keys): simplify API key listing endpoint and enhance expiration handling --- backend/.gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/.gitignore b/backend/.gitignore index 8de108186..1e4c485c4 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -139,7 +139,7 @@ Desktop.ini ###################### !.mvn/wrapper/maven-wrapper.jar mvnw -mvn.cmd +mvnw.cmd ###################### # ESLint From ebd9fb2d79576110d94d112f28195ff4c8070237 Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Tue, 28 Oct 2025 14:40:51 -0500 Subject: [PATCH 126/128] feat(api_keys): simplify API key listing endpoint and enhance expiration handling --- backend/.gitignore | 2 -- 1 file changed, 2 deletions(-) diff --git a/backend/.gitignore b/backend/.gitignore index 1e4c485c4..518edd788 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -138,8 +138,6 @@ Desktop.ini # Maven Wrapper ###################### !.mvn/wrapper/maven-wrapper.jar -mvnw -mvnw.cmd ###################### # ESLint From 5aa81b86b64088fed4f5391d5892c372b552d058 Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Tue, 28 Oct 2025 14:41:55 -0500 Subject: [PATCH 127/128] feat(api_keys): simplify API key listing endpoint and enhance expiration handling --- backend/mvnw | 286 +++++++++++++++++++++++++++++++++++++++++++++++ backend/mvnw.cmd | 161 ++++++++++++++++++++++++++ 2 files changed, 447 insertions(+) create mode 100644 backend/mvnw create mode 100644 backend/mvnw.cmd diff --git a/backend/mvnw b/backend/mvnw new file mode 100644 index 000000000..5551fde8e --- /dev/null +++ b/backend/mvnw @@ -0,0 +1,286 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven2 Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" + # TODO classpath? +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + wget "$jarUrl" -O "$wrapperJarPath" + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + curl -o "$wrapperJarPath" "$jarUrl" + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/backend/mvnw.cmd b/backend/mvnw.cmd new file mode 100644 index 000000000..e5cfb0ae9 --- /dev/null +++ b/backend/mvnw.cmd @@ -0,0 +1,161 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven2 Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" +FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + echo Found %WRAPPER_JAR% +) else ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')" + echo Finished downloading %WRAPPER_JAR% +) +@REM End of extension + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% From 735abfbce2e10199a4f50e85e66c1be6121cd1e5 Mon Sep 17 00:00:00 2001 From: Manuel Abascal Date: Tue, 28 Oct 2025 14:45:14 -0500 Subject: [PATCH 128/128] Update backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../java/com/park/utmstack/security/api_key/ApiKeyFilter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java b/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java index 352199301..4ab3f5a95 100644 --- a/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java +++ b/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java @@ -50,7 +50,7 @@ public class ApiKeyFilter extends OncePerRequestFilter { private final UserRepository userRepository; private final ApiKeyService apiKeyService; - private final ConcurrentMap invalidApiKeyBlackList = new ConcurrentHashMap<>();; + private final ConcurrentMap invalidApiKeyBlackList = new ConcurrentHashMap<>(); private final ConcurrentMap cidrCache = new ConcurrentHashMap<>(); private final ApplicationEventService applicationEventService; private final ApiKeyUsageLoggingService apiKeyUsageLoggingService;