From 0ff2ab77475bd780a9a810891f54f9e716979f84 Mon Sep 17 00:00:00 2001 From: arzunusretova12-debug Date: Wed, 24 Jun 2026 01:46:04 +0400 Subject: [PATCH 1/5] feat: implement DTOs for dashboard entities and update documentation --- backend/README.md | 18 + .../dev/cleat/api/CleatApiApplication.java | 22 +- .../api/controller/DashboardController.java | 25 + backend/libs/common/build.gradle.kts | 2 +- .../dev/cleat/common/dto/BreakdownItem.java | 39 + .../dev/cleat/common/dto/UsagePointDto.java | 47 ++ .../common/dto/request/AccountRequestDto.java | 48 ++ .../dto/request/ActivityEventRequestDto.java | 97 +++ .../common/dto/request/MemberRequestDto.java | 68 ++ .../common/dto/request/RepoRequestDto.java | 68 ++ .../dto/request/ScorecardCheckRequestDto.java | 42 + .../dto/request/SecretFindingRequestDto.java | 134 ++++ .../dto/request/VulnerabilityRequestDto.java | 184 +++++ .../dto/response/AccountResponseDto.java | 109 +++ .../response/ActivityEventResponseDto.java | 109 +++ .../cleat/common/dto/response/DatasetDto.java | 96 +++ .../dto/response/MemberResponseDto.java | 99 +++ .../common/dto/response/RepoResponseDto.java | 249 ++++++ .../response/ScorecardCheckResponseDto.java | 46 ++ .../response/SecretFindingResponseDto.java | 139 ++++ .../common/dto/response/UsageResponseDto.java | 111 +++ .../response/VulnerabilityResponseDto.java | 180 +++++ .../dev/cleat/common/enums/AccountType.java | 19 + .../dev/cleat/common/enums/EventCategory.java | 21 + .../java/dev/cleat/common/enums/Plan.java | 20 + .../dev/cleat/common/enums/Reachable.java | 20 + .../java/dev/cleat/common/enums/Role.java | 20 + .../java/dev/cleat/common/enums/Severity.java | 21 + .../java/dev/cleat/common/enums/Validity.java | 20 + .../dev/cleat/common/enums/Visibility.java | 20 + .../common/exception/NotFoundException.java | 10 + .../dev/cleat/common/mapper/CleatMapper.java | 268 +++++++ backend/libs/domain/build.gradle.kts | 3 + .../dev/cleat/domain/DashboardService.java | 74 ++ .../dev/cleat/persistence/AccountType.java | 6 - .../main/java/dev/cleat/persistence/Plan.java | 6 - .../dev/cleat/persistence/Visibility.java | 7 - .../{ => entity}/AccountEntity.java | 402 +++++----- .../entity/ActivityEventEntity.java | 172 ++++ .../persistence/entity/MemberEntity.java | 157 ++++ .../persistence/{ => entity}/RepoEntity.java | 741 +++++++++--------- .../entity/ScorecardCheckEntity.java | 65 ++ .../entity/SecretFindingEntity.java | 214 +++++ .../cleat/persistence/entity/UsageEntity.java | 145 ++++ .../persistence/entity/UsagePointEntity.java | 96 +++ .../entity/VulnerabilityEntity.java | 274 +++++++ .../{ => repository}/AccountRepository.java | 13 +- .../repository/ActivityEventRepository.java | 10 + .../repository/MemberRepository.java | 10 + .../repository/RepoRepository.java | 10 + .../repository/ScorecardCheckRepository.java | 7 + .../repository/SecretFindingRepository.java | 10 + .../repository/UsagePointRepository.java | 7 + .../repository/UsageRepository.java | 10 + .../repository/VulnerabilityRepository.java | 10 + .../V2__update_repo_and_add_tables.sql | 151 ++++ .../persistence/AccountRepositoryTest.java | 116 +-- 57 files changed, 4442 insertions(+), 645 deletions(-) create mode 100644 backend/apps/api/src/main/java/dev/cleat/api/controller/DashboardController.java create mode 100644 backend/libs/common/src/main/java/dev/cleat/common/dto/BreakdownItem.java create mode 100644 backend/libs/common/src/main/java/dev/cleat/common/dto/UsagePointDto.java create mode 100644 backend/libs/common/src/main/java/dev/cleat/common/dto/request/AccountRequestDto.java create mode 100644 backend/libs/common/src/main/java/dev/cleat/common/dto/request/ActivityEventRequestDto.java create mode 100644 backend/libs/common/src/main/java/dev/cleat/common/dto/request/MemberRequestDto.java create mode 100644 backend/libs/common/src/main/java/dev/cleat/common/dto/request/RepoRequestDto.java create mode 100644 backend/libs/common/src/main/java/dev/cleat/common/dto/request/ScorecardCheckRequestDto.java create mode 100644 backend/libs/common/src/main/java/dev/cleat/common/dto/request/SecretFindingRequestDto.java create mode 100644 backend/libs/common/src/main/java/dev/cleat/common/dto/request/VulnerabilityRequestDto.java create mode 100644 backend/libs/common/src/main/java/dev/cleat/common/dto/response/AccountResponseDto.java create mode 100644 backend/libs/common/src/main/java/dev/cleat/common/dto/response/ActivityEventResponseDto.java create mode 100644 backend/libs/common/src/main/java/dev/cleat/common/dto/response/DatasetDto.java create mode 100644 backend/libs/common/src/main/java/dev/cleat/common/dto/response/MemberResponseDto.java create mode 100644 backend/libs/common/src/main/java/dev/cleat/common/dto/response/RepoResponseDto.java create mode 100644 backend/libs/common/src/main/java/dev/cleat/common/dto/response/ScorecardCheckResponseDto.java create mode 100644 backend/libs/common/src/main/java/dev/cleat/common/dto/response/SecretFindingResponseDto.java create mode 100644 backend/libs/common/src/main/java/dev/cleat/common/dto/response/UsageResponseDto.java create mode 100644 backend/libs/common/src/main/java/dev/cleat/common/dto/response/VulnerabilityResponseDto.java create mode 100644 backend/libs/common/src/main/java/dev/cleat/common/enums/AccountType.java create mode 100644 backend/libs/common/src/main/java/dev/cleat/common/enums/EventCategory.java create mode 100644 backend/libs/common/src/main/java/dev/cleat/common/enums/Plan.java create mode 100644 backend/libs/common/src/main/java/dev/cleat/common/enums/Reachable.java create mode 100644 backend/libs/common/src/main/java/dev/cleat/common/enums/Role.java create mode 100644 backend/libs/common/src/main/java/dev/cleat/common/enums/Severity.java create mode 100644 backend/libs/common/src/main/java/dev/cleat/common/enums/Validity.java create mode 100644 backend/libs/common/src/main/java/dev/cleat/common/enums/Visibility.java create mode 100644 backend/libs/common/src/main/java/dev/cleat/common/exception/NotFoundException.java create mode 100644 backend/libs/common/src/main/java/dev/cleat/common/mapper/CleatMapper.java create mode 100644 backend/libs/domain/src/main/java/dev/cleat/domain/DashboardService.java delete mode 100644 backend/libs/persistence/src/main/java/dev/cleat/persistence/AccountType.java delete mode 100644 backend/libs/persistence/src/main/java/dev/cleat/persistence/Plan.java delete mode 100644 backend/libs/persistence/src/main/java/dev/cleat/persistence/Visibility.java rename backend/libs/persistence/src/main/java/dev/cleat/persistence/{ => entity}/AccountEntity.java (91%) create mode 100644 backend/libs/persistence/src/main/java/dev/cleat/persistence/entity/ActivityEventEntity.java create mode 100644 backend/libs/persistence/src/main/java/dev/cleat/persistence/entity/MemberEntity.java rename backend/libs/persistence/src/main/java/dev/cleat/persistence/{ => entity}/RepoEntity.java (84%) create mode 100644 backend/libs/persistence/src/main/java/dev/cleat/persistence/entity/ScorecardCheckEntity.java create mode 100644 backend/libs/persistence/src/main/java/dev/cleat/persistence/entity/SecretFindingEntity.java create mode 100644 backend/libs/persistence/src/main/java/dev/cleat/persistence/entity/UsageEntity.java create mode 100644 backend/libs/persistence/src/main/java/dev/cleat/persistence/entity/UsagePointEntity.java create mode 100644 backend/libs/persistence/src/main/java/dev/cleat/persistence/entity/VulnerabilityEntity.java rename backend/libs/persistence/src/main/java/dev/cleat/persistence/{ => repository}/AccountRepository.java (62%) create mode 100644 backend/libs/persistence/src/main/java/dev/cleat/persistence/repository/ActivityEventRepository.java create mode 100644 backend/libs/persistence/src/main/java/dev/cleat/persistence/repository/MemberRepository.java create mode 100644 backend/libs/persistence/src/main/java/dev/cleat/persistence/repository/RepoRepository.java create mode 100644 backend/libs/persistence/src/main/java/dev/cleat/persistence/repository/ScorecardCheckRepository.java create mode 100644 backend/libs/persistence/src/main/java/dev/cleat/persistence/repository/SecretFindingRepository.java create mode 100644 backend/libs/persistence/src/main/java/dev/cleat/persistence/repository/UsagePointRepository.java create mode 100644 backend/libs/persistence/src/main/java/dev/cleat/persistence/repository/UsageRepository.java create mode 100644 backend/libs/persistence/src/main/java/dev/cleat/persistence/repository/VulnerabilityRepository.java create mode 100644 backend/libs/persistence/src/main/resources/db/migration/V2__update_repo_and_add_tables.sql diff --git a/backend/README.md b/backend/README.md index 267e8eb..59078ff 100644 --- a/backend/README.md +++ b/backend/README.md @@ -59,3 +59,21 @@ for example `dev.cleat.domain` and `dev.cleat.githubclient`. Local dependencies (Postgres, Redis) are expected to run via Docker Compose. The two services build into one container image each. + + +## Frontend–Backend Data Contract + +The following rules apply to data exchange between the Frontend (`apps/web/src/data/types.ts`) and the +Backend: + +1. Field Mapping: All fields in Backend DTOs (e.g., `repoCount`, `postureScore`) follow the same camelCase + naming convention as the Frontend interfaces. + +2. Enum Synchronization: Enums such as `AccountType`, `Plan`, and `Severity` are serialized using + `@JsonValue` to the lowercase values expected by the Frontend (e.g., `user`, `critical`). + +3. ISO Dates: Date fields (e.g., `lastPushedAt`, `detectedAt`) use the `OffsetDateTime` type and are + serialized to JSON as ISO-8601 formatted strings. + +4. Data Container: The Frontend `Dataset` type is backed by `DatasetDto`. To switch from mock data + to real data, requests should be sent to the `/api/dashboard/dataset` endpoint. diff --git a/backend/apps/api/src/main/java/dev/cleat/api/CleatApiApplication.java b/backend/apps/api/src/main/java/dev/cleat/api/CleatApiApplication.java index 0ed835e..2a344e1 100644 --- a/backend/apps/api/src/main/java/dev/cleat/api/CleatApiApplication.java +++ b/backend/apps/api/src/main/java/dev/cleat/api/CleatApiApplication.java @@ -1,11 +1,11 @@ -package dev.cleat.api; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -@SpringBootApplication -public class CleatApiApplication { - public static void main(String[] args) { - SpringApplication.run(CleatApiApplication.class); - } -} +package dev.cleat.api; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication(scanBasePackages = "dev.cleat") +public class CleatApiApplication { + public static void main(String[] args) { + SpringApplication.run(CleatApiApplication.class); + } +} diff --git a/backend/apps/api/src/main/java/dev/cleat/api/controller/DashboardController.java b/backend/apps/api/src/main/java/dev/cleat/api/controller/DashboardController.java new file mode 100644 index 0000000..975a6e1 --- /dev/null +++ b/backend/apps/api/src/main/java/dev/cleat/api/controller/DashboardController.java @@ -0,0 +1,25 @@ +package dev.cleat.api.controller; + +import dev.cleat.common.dto.response.DatasetDto; +import dev.cleat.domain.DashboardService; +import java.util.UUID; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/dashboard") +public class DashboardController { + private final DashboardService dashboardService; + + public DashboardController(DashboardService dashboardService) { + this.dashboardService = dashboardService; + } + + @GetMapping("/dataset") + public ResponseEntity getDashboardData(@RequestHeader("X-Account-Id") UUID accountId) { + return ResponseEntity.ok(dashboardService.getDataset(accountId)); + } +} diff --git a/backend/libs/common/build.gradle.kts b/backend/libs/common/build.gradle.kts index 2e01556..c554ab6 100644 --- a/backend/libs/common/build.gradle.kts +++ b/backend/libs/common/build.gradle.kts @@ -5,6 +5,6 @@ plugins { dependencies { api("org.springframework.boot:spring-boot-starter") api("org.springframework.boot:spring-boot-starter-validation") - + implementation(project(":libs:persistence")) testImplementation("org.springframework.boot:spring-boot-starter-test") } diff --git a/backend/libs/common/src/main/java/dev/cleat/common/dto/BreakdownItem.java b/backend/libs/common/src/main/java/dev/cleat/common/dto/BreakdownItem.java new file mode 100644 index 0000000..2dbcb6b --- /dev/null +++ b/backend/libs/common/src/main/java/dev/cleat/common/dto/BreakdownItem.java @@ -0,0 +1,39 @@ +package dev.cleat.common.dto; + +import jakarta.persistence.Embeddable; +import java.math.BigDecimal; + +@Embeddable +public class BreakdownItem { + + private String label; + private BigDecimal cost; + private String hex; + + public String getLabel() { + return label; + } + + public BreakdownItem setLabel(String label) { + this.label = label; + return this; + } + + public BigDecimal getCost() { + return cost; + } + + public BreakdownItem setCost(BigDecimal cost) { + this.cost = cost; + return this; + } + + public String getHex() { + return hex; + } + + public BreakdownItem setHex(String hex) { + this.hex = hex; + return this; + } +} diff --git a/backend/libs/common/src/main/java/dev/cleat/common/dto/UsagePointDto.java b/backend/libs/common/src/main/java/dev/cleat/common/dto/UsagePointDto.java new file mode 100644 index 0000000..bf477bb --- /dev/null +++ b/backend/libs/common/src/main/java/dev/cleat/common/dto/UsagePointDto.java @@ -0,0 +1,47 @@ +package dev.cleat.common.dto; + +import java.math.BigDecimal; + +public class UsagePointDto { + + private String label; + private Integer minutes; + private Double storageGb; + private BigDecimal cost; + + public String getLabel() { + return label; + } + + public UsagePointDto setLabel(String label) { + this.label = label; + return this; + } + + public Integer getMinutes() { + return minutes; + } + + public UsagePointDto setMinutes(Integer minutes) { + this.minutes = minutes; + return this; + } + + public Double getStorageGb() { + return storageGb; + } + + public UsagePointDto setStorageGb(Double storageGb) { + this.storageGb = storageGb; + return this; + } + + public BigDecimal getCost() { + return cost; + } + + public UsagePointDto setCost(BigDecimal cost) { + this.cost = cost; + return this; + } +} diff --git a/backend/libs/common/src/main/java/dev/cleat/common/dto/request/AccountRequestDto.java b/backend/libs/common/src/main/java/dev/cleat/common/dto/request/AccountRequestDto.java new file mode 100644 index 0000000..59f045c --- /dev/null +++ b/backend/libs/common/src/main/java/dev/cleat/common/dto/request/AccountRequestDto.java @@ -0,0 +1,48 @@ +package dev.cleat.common.dto.request; + +import dev.cleat.common.enums.AccountType; +import dev.cleat.common.enums.Plan; + +public class AccountRequestDto { + + private String login; + private String name; + private AccountType type; + private Plan plan; + + public String getLogin() { + return login; + } + + public AccountRequestDto setLogin(String login) { + this.login = login; + return this; + } + + public String getName() { + return name; + } + + public AccountRequestDto setName(String name) { + this.name = name; + return this; + } + + public AccountType getType() { + return type; + } + + public AccountRequestDto setType(AccountType type) { + this.type = type; + return this; + } + + public Plan getPlan() { + return plan; + } + + public AccountRequestDto setPlan(Plan plan) { + this.plan = plan; + return this; + } +} diff --git a/backend/libs/common/src/main/java/dev/cleat/common/dto/request/ActivityEventRequestDto.java b/backend/libs/common/src/main/java/dev/cleat/common/dto/request/ActivityEventRequestDto.java new file mode 100644 index 0000000..214856c --- /dev/null +++ b/backend/libs/common/src/main/java/dev/cleat/common/dto/request/ActivityEventRequestDto.java @@ -0,0 +1,97 @@ +package dev.cleat.common.dto.request; + +import dev.cleat.common.enums.EventCategory; +import dev.cleat.common.enums.Severity; + +public class ActivityEventRequestDto { + + private String type; + private Severity severity; + private String actor; + private String target; + private String message; + private EventCategory eventCategory; + + public ActivityEventRequestDto() {} + + public ActivityEventRequestDto( + String type, + Severity severity, + String actor, + String target, + String repo, + String message, + EventCategory eventCategory) { + + this.type = type; + this.severity = severity; + this.actor = actor; + this.target = target; + this.repo = repo; + this.message = message; + this.eventCategory = eventCategory; + } + + public String getType() { + return type; + } + + public ActivityEventRequestDto setType(String type) { + this.type = type; + return this; + } + + public Severity getSeverity() { + return severity; + } + + public ActivityEventRequestDto setSeverity(Severity severity) { + this.severity = severity; + return this; + } + + public String getActor() { + return actor; + } + + public ActivityEventRequestDto setActor(String actor) { + this.actor = actor; + return this; + } + + public String getTarget() { + return target; + } + + public ActivityEventRequestDto setTarget(String target) { + this.target = target; + return this; + } + + public String getRepo() { + return repo; + } + + public ActivityEventRequestDto setRepo(String repo) { + this.repo = repo; + return this; + } + + public String getMessage() { + return message; + } + + public ActivityEventRequestDto setMessage(String message) { + this.message = message; + return this; + } + + public EventCategory getEventCategory() { + return eventCategory; + } + + public ActivityEventRequestDto setEventCategory(EventCategory eventCategory) { + this.eventCategory = eventCategory; + return this; + } +} diff --git a/backend/libs/common/src/main/java/dev/cleat/common/dto/request/MemberRequestDto.java b/backend/libs/common/src/main/java/dev/cleat/common/dto/request/MemberRequestDto.java new file mode 100644 index 0000000..1a5badb --- /dev/null +++ b/backend/libs/common/src/main/java/dev/cleat/common/dto/request/MemberRequestDto.java @@ -0,0 +1,68 @@ +package dev.cleat.common.dto.request; + +import dev.cleat.common.enums.Role; +import java.util.List; + +public class MemberRequestDto { + + private String login; + private String name; + private Role role; + private Boolean twoFactor; + private List teams; + + public MemberRequestDto() {} + + public MemberRequestDto(String login, String name, Role role, Boolean twoFactor, List team) { + this.login = login; + this.name = name; + this.role = role; + this.twoFactor = twoFactor; + this.teams = team; + } + + public String getLogin() { + return login; + } + + public MemberRequestDto setLogin(String login) { + this.login = login; + return this; + } + + public String getName() { + return name; + } + + public MemberRequestDto setName(String name) { + this.name = name; + return this; + } + + public Role getRole() { + return role; + } + + public MemberRequestDto setRole(Role role) { + this.role = role; + return this; + } + + public Boolean getTwoFactor() { + return twoFactor; + } + + public MemberRequestDto setTwoFactor(Boolean twoFactor) { + this.twoFactor = twoFactor; + return this; + } + + public List getTeams() { + return teams; + } + + public MemberRequestDto setTeams(List teams) { + this.teams = teams; + return this; + } +} diff --git a/backend/libs/common/src/main/java/dev/cleat/common/dto/request/RepoRequestDto.java b/backend/libs/common/src/main/java/dev/cleat/common/dto/request/RepoRequestDto.java new file mode 100644 index 0000000..5daccdd --- /dev/null +++ b/backend/libs/common/src/main/java/dev/cleat/common/dto/request/RepoRequestDto.java @@ -0,0 +1,68 @@ +package dev.cleat.common.dto.request; + +import dev.cleat.common.enums.Visibility; +import java.util.List; + +public class RepoRequestDto { + private String name; + private Visibility visibility; + private String language; + private String defaultBranch; + private List topics; + + public RepoRequestDto() {} + + public RepoRequestDto( + String name, Visibility visibility, String language, String defaultBranch, List topics) { + this.name = name; + this.visibility = visibility; + this.language = language; + this.defaultBranch = defaultBranch; + this.topics = topics; + } + + public String getName() { + return name; + } + + public RepoRequestDto setName(String name) { + this.name = name; + return this; + } + + public Visibility getVisibility() { + return visibility; + } + + public RepoRequestDto setVisibility(Visibility visibility) { + this.visibility = visibility; + return this; + } + + public String getLanguage() { + return language; + } + + public RepoRequestDto setLanguage(String language) { + this.language = language; + return this; + } + + public String getDefaultBranch() { + return defaultBranch; + } + + public RepoRequestDto setDefaultBranch(String defaultBranch) { + this.defaultBranch = defaultBranch; + return this; + } + + public List getTopics() { + return topics; + } + + public RepoRequestDto setTopics(List topics) { + this.topics = topics; + return this; + } +} diff --git a/backend/libs/common/src/main/java/dev/cleat/common/dto/request/ScorecardCheckRequestDto.java b/backend/libs/common/src/main/java/dev/cleat/common/dto/request/ScorecardCheckRequestDto.java new file mode 100644 index 0000000..8de698b --- /dev/null +++ b/backend/libs/common/src/main/java/dev/cleat/common/dto/request/ScorecardCheckRequestDto.java @@ -0,0 +1,42 @@ +package dev.cleat.common.dto.request; + +public class ScorecardCheckRequestDto { + private String name; + private Integer score; + private String reason; + + public ScorecardCheckRequestDto() {} + + public ScorecardCheckRequestDto(String name, Integer score, String reason) { + this.name = name; + this.score = score; + this.reason = reason; + } + + public String getName() { + return name; + } + + public ScorecardCheckRequestDto setName(String name) { + this.name = name; + return this; + } + + public Integer getScore() { + return score; + } + + public ScorecardCheckRequestDto setScore(Integer score) { + this.score = score; + return this; + } + + public String getReason() { + return reason; + } + + public ScorecardCheckRequestDto setReason(String reason) { + this.reason = reason; + return this; + } +} diff --git a/backend/libs/common/src/main/java/dev/cleat/common/dto/request/SecretFindingRequestDto.java b/backend/libs/common/src/main/java/dev/cleat/common/dto/request/SecretFindingRequestDto.java new file mode 100644 index 0000000..4a8f3c3 --- /dev/null +++ b/backend/libs/common/src/main/java/dev/cleat/common/dto/request/SecretFindingRequestDto.java @@ -0,0 +1,134 @@ +package dev.cleat.common.dto.request; + +import dev.cleat.common.enums.Severity; +import dev.cleat.common.enums.Validity; +import java.time.OffsetDateTime; + +public class SecretFindingRequestDto { + private String provider; + private String secretType; + private String file; + private Integer line; + private String commit; + private String author; + private OffsetDateTime detectedAt; + private Validity validity; + private Severity severity; + private Boolean pushProtectionBlocked; + + public SecretFindingRequestDto() {} + + public SecretFindingRequestDto( + String provider, + String secretType, + String file, + Integer line, + String commit, + String author, + OffsetDateTime detectedAt, + Validity validity, + Severity severity, + Boolean pushProtectionBlocked) { + + this.provider = provider; + this.secretType = secretType; + this.file = file; + this.line = line; + this.commit = commit; + this.author = author; + this.detectedAt = detectedAt; + this.validity = validity; + this.severity = severity; + this.pushProtectionBlocked = pushProtectionBlocked; + } + + public String getProvider() { + return provider; + } + + public SecretFindingRequestDto setProvider(String provider) { + this.provider = provider; + return this; + } + + public String getSecretType() { + return secretType; + } + + public SecretFindingRequestDto setSecretType(String secretType) { + this.secretType = secretType; + return this; + } + + public String getFile() { + return file; + } + + public SecretFindingRequestDto setFile(String file) { + this.file = file; + return this; + } + + public Integer getLine() { + return line; + } + + public SecretFindingRequestDto setLine(Integer line) { + this.line = line; + return this; + } + + public String getCommit() { + return commit; + } + + public SecretFindingRequestDto setCommit(String commit) { + this.commit = commit; + return this; + } + + public String getAuthor() { + return author; + } + + public SecretFindingRequestDto setAuthor(String author) { + this.author = author; + return this; + } + + public OffsetDateTime getDetectedAt() { + return detectedAt; + } + + public SecretFindingRequestDto setDetectedAt(OffsetDateTime detectedAt) { + this.detectedAt = detectedAt; + return this; + } + + public Validity getValidity() { + return validity; + } + + public SecretFindingRequestDto setValidity(Validity validity) { + this.validity = validity; + return this; + } + + public Severity getSeverity() { + return severity; + } + + public SecretFindingRequestDto setSeverity(Severity severity) { + this.severity = severity; + return this; + } + + public Boolean getPushProtectionBlocked() { + return pushProtectionBlocked; + } + + public SecretFindingRequestDto setPushProtectionBlocked(Boolean pushProtectionBlocked) { + this.pushProtectionBlocked = pushProtectionBlocked; + return this; + } +} diff --git a/backend/libs/common/src/main/java/dev/cleat/common/dto/request/VulnerabilityRequestDto.java b/backend/libs/common/src/main/java/dev/cleat/common/dto/request/VulnerabilityRequestDto.java new file mode 100644 index 0000000..93b61b7 --- /dev/null +++ b/backend/libs/common/src/main/java/dev/cleat/common/dto/request/VulnerabilityRequestDto.java @@ -0,0 +1,184 @@ +package dev.cleat.common.dto.request; + +import dev.cleat.common.enums.Reachable; +import dev.cleat.common.enums.Severity; +import java.util.List; +import java.util.UUID; + +public class VulnerabilityRequestDto { + + private String packageName; + private String ecosystem; + private String currentVersion; + private String fixedVersion; + private Double cvss; + private Severity severity; + private Double epss; + private Boolean kev; + private Reachable reachable; + private UUID advisoryId; + private String cwe; + private String title; + private List affectedRepos; + private Boolean hasFixPr; + + public VulnerabilityRequestDto() {} + + public VulnerabilityRequestDto( + String packageName, + String ecosystem, + String currentVersion, + String fixedVersion, + Double cvss, + Severity severity, + Double epss, + Boolean kev, + Reachable reachable, + UUID advisoryId, + String cwe, + String title, + List affectedRepos, + Boolean hasFixPr) { + + this.packageName = packageName; + this.ecosystem = ecosystem; + this.currentVersion = currentVersion; + this.fixedVersion = fixedVersion; + this.cvss = cvss; + this.severity = severity; + this.epss = epss; + this.kev = kev; + this.reachable = reachable; + this.advisoryId = advisoryId; + this.cwe = cwe; + this.title = title; + this.affectedRepos = affectedRepos; + this.hasFixPr = hasFixPr; + } + + public String getPackageName() { + return packageName; + } + + public VulnerabilityRequestDto setPackageName(String packageName) { + this.packageName = packageName; + return this; + } + + public String getEcosystem() { + return ecosystem; + } + + public VulnerabilityRequestDto setEcosystem(String ecosystem) { + this.ecosystem = ecosystem; + return this; + } + + public String getCurrentVersion() { + return currentVersion; + } + + public VulnerabilityRequestDto setCurrentVersion(String currentVersion) { + this.currentVersion = currentVersion; + return this; + } + + public String getFixedVersion() { + return fixedVersion; + } + + public VulnerabilityRequestDto setFixedVersion(String fixedVersion) { + this.fixedVersion = fixedVersion; + return this; + } + + public Double getCvss() { + return cvss; + } + + public VulnerabilityRequestDto setCvss(Double cvss) { + this.cvss = cvss; + return this; + } + + public Severity getSeverity() { + return severity; + } + + public VulnerabilityRequestDto setSeverity(Severity severity) { + this.severity = severity; + return this; + } + + public Double getEpss() { + return epss; + } + + public VulnerabilityRequestDto setEpss(Double epss) { + this.epss = epss; + return this; + } + + public Boolean getKev() { + return kev; + } + + public VulnerabilityRequestDto setKev(Boolean kev) { + this.kev = kev; + return this; + } + + public Reachable getReachable() { + return reachable; + } + + public VulnerabilityRequestDto setReachable(Reachable reachable) { + this.reachable = reachable; + return this; + } + + public UUID getAdvisoryId() { + return advisoryId; + } + + public VulnerabilityRequestDto setAdvisoryId(UUID advisoryId) { + this.advisoryId = advisoryId; + return this; + } + + public String getCwe() { + return cwe; + } + + public VulnerabilityRequestDto setCwe(String cwe) { + this.cwe = cwe; + return this; + } + + public String getTitle() { + return title; + } + + public VulnerabilityRequestDto setTitle(String title) { + this.title = title; + return this; + } + + public List getAffectedRepos() { + return affectedRepos; + } + + public VulnerabilityRequestDto setAffectedRepos(List affectedRepos) { + this.affectedRepos = affectedRepos; + return this; + } + + public Boolean getHasFixPr() { + return hasFixPr; + } + + public VulnerabilityRequestDto setHasFixPr(Boolean hasFixPr) { + this.hasFixPr = hasFixPr; + return this; + } +} diff --git a/backend/libs/common/src/main/java/dev/cleat/common/dto/response/AccountResponseDto.java b/backend/libs/common/src/main/java/dev/cleat/common/dto/response/AccountResponseDto.java new file mode 100644 index 0000000..36e20e1 --- /dev/null +++ b/backend/libs/common/src/main/java/dev/cleat/common/dto/response/AccountResponseDto.java @@ -0,0 +1,109 @@ +package dev.cleat.common.dto.response; + +import dev.cleat.common.enums.AccountType; +import dev.cleat.common.enums.Plan; +import java.math.BigDecimal; +import java.util.UUID; + +public class AccountResponseDto { + private UUID id; + private String login; + private String name; + private AccountType type; + private Plan plan; + private Integer repoCount; + private Integer memberCount; + private Integer postureScore; + private BigDecimal monthlySpend; + private BigDecimal reclaimable; + + public UUID getId() { + return id; + } + + public AccountResponseDto setId(UUID id) { + this.id = id; + return this; + } + + public String getLogin() { + return login; + } + + public AccountResponseDto setLogin(String login) { + this.login = login; + return this; + } + + public String getName() { + return name; + } + + public AccountResponseDto setName(String name) { + this.name = name; + return this; + } + + public AccountType getType() { + return type; + } + + public AccountResponseDto setType(AccountType type) { + this.type = type; + return this; + } + + public Plan getPlan() { + return plan; + } + + public AccountResponseDto setPlan(Plan plan) { + this.plan = plan; + return this; + } + + public Integer getRepoCount() { + return repoCount; + } + + public AccountResponseDto setRepoCount(Integer repoCount) { + this.repoCount = repoCount; + return this; + } + + public Integer getMemberCount() { + return memberCount; + } + + public AccountResponseDto setMemberCount(Integer memberCount) { + this.memberCount = memberCount; + return this; + } + + public Integer getPostureScore() { + return postureScore; + } + + public AccountResponseDto setPostureScore(Integer postureScore) { + this.postureScore = postureScore; + return this; + } + + public BigDecimal getMonthlySpend() { + return monthlySpend; + } + + public AccountResponseDto setMonthlySpend(BigDecimal monthlySpend) { + this.monthlySpend = monthlySpend; + return this; + } + + public BigDecimal getReclaimable() { + return reclaimable; + } + + public AccountResponseDto setReclaimable(BigDecimal reclaimable) { + this.reclaimable = reclaimable; + return this; + } +} diff --git a/backend/libs/common/src/main/java/dev/cleat/common/dto/response/ActivityEventResponseDto.java b/backend/libs/common/src/main/java/dev/cleat/common/dto/response/ActivityEventResponseDto.java new file mode 100644 index 0000000..981eb8f --- /dev/null +++ b/backend/libs/common/src/main/java/dev/cleat/common/dto/response/ActivityEventResponseDto.java @@ -0,0 +1,109 @@ +package dev.cleat.common.dto.response; + +import dev.cleat.common.enums.EventCategory; +import dev.cleat.common.enums.Severity; +import java.time.OffsetDateTime; +import java.util.UUID; + +public class ActivityEventResponseDto { + private UUID id; + private UUID accountId; + private String type; + private Severity severity; + private String actor; + private String target; + private String repo; + private String message; + private OffsetDateTime createdAt; + private EventCategory eventCategory; + + public UUID getId() { + return id; + } + + public ActivityEventResponseDto setId(UUID id) { + this.id = id; + return this; + } + + public UUID getAccountId() { + return accountId; + } + + public ActivityEventResponseDto setAccountId(UUID accountId) { + this.accountId = accountId; + return this; + } + + public String getType() { + return type; + } + + public ActivityEventResponseDto setType(String type) { + this.type = type; + return this; + } + + public Severity getSeverity() { + return severity; + } + + public ActivityEventResponseDto setSeverity(Severity severity) { + this.severity = severity; + return this; + } + + public String getActor() { + return actor; + } + + public ActivityEventResponseDto setActor(String actor) { + this.actor = actor; + return this; + } + + public String getTarget() { + return target; + } + + public ActivityEventResponseDto setTarget(String target) { + this.target = target; + return this; + } + + public String getRepo() { + return repo; + } + + public ActivityEventResponseDto setRepo(String repo) { + this.repo = repo; + return this; + } + + public String getMessage() { + return message; + } + + public ActivityEventResponseDto setMessage(String message) { + this.message = message; + return this; + } + + public OffsetDateTime getCreatedAt() { + return createdAt; + } + + public ActivityEventResponseDto setCreatedAt(OffsetDateTime createdAt) { + this.createdAt = createdAt; + return this; + } + + public EventCategory getEventCategory() { + return eventCategory; + } + + public ActivityEventResponseDto setEventCategory(EventCategory eventCategory) { + this.eventCategory = eventCategory; + return this; + } +} diff --git a/backend/libs/common/src/main/java/dev/cleat/common/dto/response/DatasetDto.java b/backend/libs/common/src/main/java/dev/cleat/common/dto/response/DatasetDto.java new file mode 100644 index 0000000..09d2ae2 --- /dev/null +++ b/backend/libs/common/src/main/java/dev/cleat/common/dto/response/DatasetDto.java @@ -0,0 +1,96 @@ +package dev.cleat.common.dto.response; + +import java.util.List; + +public class DatasetDto { + + private AccountResponseDto account; + private List repos; + private List secrets; + private List vulnerabilities; + private UsageResponseDto usage; + private List members; + private List events; + + public DatasetDto() {} + + public DatasetDto( + AccountResponseDto account, + List repos, + List secrets, + List vulnerabilities, + UsageResponseDto usage, + List members, + List events) { + this.account = account; + this.repos = repos; + this.secrets = secrets; + this.vulnerabilities = vulnerabilities; + this.usage = usage; + this.members = members; + this.events = events; + } + + public AccountResponseDto getAccount() { + return account; + } + + public DatasetDto setAccount(AccountResponseDto account) { + this.account = account; + return this; + } + + public List getRepos() { + return repos; + } + + public DatasetDto setRepos(List repos) { + this.repos = repos; + return this; + } + + public List getSecrets() { + return secrets; + } + + public DatasetDto setSecrets(List secrets) { + this.secrets = secrets; + return this; + } + + public List getVulnerabilities() { + return vulnerabilities; + } + + public DatasetDto setVulnerabilities(List vulnerabilities) { + this.vulnerabilities = vulnerabilities; + return this; + } + + public UsageResponseDto getUsage() { + return usage; + } + + public DatasetDto setUsage(UsageResponseDto usage) { + this.usage = usage; + return this; + } + + public List getMembers() { + return members; + } + + public DatasetDto setMembers(List members) { + this.members = members; + return this; + } + + public List getEvents() { + return events; + } + + public DatasetDto setEvents(List events) { + this.events = events; + return this; + } +} diff --git a/backend/libs/common/src/main/java/dev/cleat/common/dto/response/MemberResponseDto.java b/backend/libs/common/src/main/java/dev/cleat/common/dto/response/MemberResponseDto.java new file mode 100644 index 0000000..7280cc8 --- /dev/null +++ b/backend/libs/common/src/main/java/dev/cleat/common/dto/response/MemberResponseDto.java @@ -0,0 +1,99 @@ +package dev.cleat.common.dto.response; + +import dev.cleat.common.enums.Role; +import java.time.OffsetDateTime; +import java.util.List; +import java.util.UUID; + +public class MemberResponseDto { + private UUID id; + private String login; + private String name; + private Role role; + private Boolean twoFactor; + private OffsetDateTime lastActiveAt; + private List teams; + private Boolean outsideCollaborator; + private Integer repoAccess; + + public UUID getId() { + return id; + } + + public MemberResponseDto setId(UUID id) { + this.id = id; + return this; + } + + public String getLogin() { + return login; + } + + public MemberResponseDto setLogin(String login) { + this.login = login; + return this; + } + + public String getName() { + return name; + } + + public MemberResponseDto setName(String name) { + this.name = name; + return this; + } + + public Role getRole() { + return role; + } + + public MemberResponseDto setRole(Role role) { + this.role = role; + return this; + } + + public Boolean getTwoFactor() { + return twoFactor; + } + + public MemberResponseDto setTwoFactor(Boolean twoFactor) { + this.twoFactor = twoFactor; + return this; + } + + public OffsetDateTime getLastActiveAt() { + return lastActiveAt; + } + + public MemberResponseDto setLastActiveAt(OffsetDateTime lastActiveAt) { + this.lastActiveAt = lastActiveAt; + return this; + } + + public List getTeams() { + return teams; + } + + public MemberResponseDto setTeams(List teams) { + this.teams = teams; + return this; + } + + public Boolean getOutsideCollaborator() { + return outsideCollaborator; + } + + public MemberResponseDto setOutsideCollaborator(Boolean outsideCollaborator) { + this.outsideCollaborator = outsideCollaborator; + return this; + } + + public Integer getRepoAccess() { + return repoAccess; + } + + public MemberResponseDto setRepoAccess(Integer repoAccess) { + this.repoAccess = repoAccess; + return this; + } +} diff --git a/backend/libs/common/src/main/java/dev/cleat/common/dto/response/RepoResponseDto.java b/backend/libs/common/src/main/java/dev/cleat/common/dto/response/RepoResponseDto.java new file mode 100644 index 0000000..ad96cc1 --- /dev/null +++ b/backend/libs/common/src/main/java/dev/cleat/common/dto/response/RepoResponseDto.java @@ -0,0 +1,249 @@ +package dev.cleat.common.dto.response; + +import dev.cleat.common.enums.Visibility; +import java.time.OffsetDateTime; +import java.util.List; +import java.util.UUID; + +public class RepoResponseDto { + private UUID id; + private String name; + private UUID accountId; + private Visibility visibility; + private String language; + private Integer stars; + private String defaultBranch; + private Boolean branchProtected; + private Boolean hasReadme; + private Boolean hasLicense; + private Boolean hasContributing; + private Boolean hasCodeowners; + private Boolean hasCI; + private Double sizeMb; + private OffsetDateTime lastPushedAt; + private Boolean archived; + private Integer openVulns; + private Integer openSecrets; + private Integer openCodeAlerts; + private Integer staleBranches; + private Integer openPRs; + private Integer hygieneScore; + private List scorecard; + private List topics; + + public UUID getId() { + return id; + } + + public RepoResponseDto setId(UUID id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public RepoResponseDto setName(String name) { + this.name = name; + return this; + } + + public UUID getAccountId() { + return accountId; + } + + public RepoResponseDto setAccountId(UUID accountId) { + this.accountId = accountId; + return this; + } + + public Visibility getVisibility() { + return visibility; + } + + public RepoResponseDto setVisibility(Visibility visibility) { + this.visibility = visibility; + return this; + } + + public String getLanguage() { + return language; + } + + public RepoResponseDto setLanguage(String language) { + this.language = language; + return this; + } + + public Integer getStars() { + return stars; + } + + public RepoResponseDto setStars(Integer stars) { + this.stars = stars; + return this; + } + + public String getDefaultBranch() { + return defaultBranch; + } + + public RepoResponseDto setDefaultBranch(String defaultBranch) { + this.defaultBranch = defaultBranch; + return this; + } + + public Boolean getBranchProtected() { + return branchProtected; + } + + public RepoResponseDto setBranchProtected(Boolean branchProtected) { + this.branchProtected = branchProtected; + return this; + } + + public Boolean getHasReadme() { + return hasReadme; + } + + public RepoResponseDto setHasReadme(Boolean hasReadme) { + this.hasReadme = hasReadme; + return this; + } + + public Boolean getHasLicense() { + return hasLicense; + } + + public RepoResponseDto setHasLicense(Boolean hasLicense) { + this.hasLicense = hasLicense; + return this; + } + + public Boolean getHasContributing() { + return hasContributing; + } + + public RepoResponseDto setHasContributing(Boolean hasContributing) { + this.hasContributing = hasContributing; + return this; + } + + public Boolean getHasCodeowners() { + return hasCodeowners; + } + + public RepoResponseDto setHasCodeowners(Boolean hasCodeowners) { + this.hasCodeowners = hasCodeowners; + return this; + } + + public Boolean getHasCI() { + return hasCI; + } + + public RepoResponseDto setHasCI(Boolean hasCI) { + this.hasCI = hasCI; + return this; + } + + public Double getSizeMb() { + return sizeMb; + } + + public RepoResponseDto setSizeMb(Double sizeMb) { + this.sizeMb = sizeMb; + return this; + } + + public OffsetDateTime getLastPushedAt() { + return lastPushedAt; + } + + public RepoResponseDto setLastPushedAt(OffsetDateTime lastPushedAt) { + this.lastPushedAt = lastPushedAt; + return this; + } + + public Boolean getArchived() { + return archived; + } + + public RepoResponseDto setArchived(Boolean archived) { + this.archived = archived; + return this; + } + + public Integer getOpenVulns() { + return openVulns; + } + + public RepoResponseDto setOpenVulns(Integer openVulns) { + this.openVulns = openVulns; + return this; + } + + public Integer getOpenSecrets() { + return openSecrets; + } + + public RepoResponseDto setOpenSecrets(Integer openSecrets) { + this.openSecrets = openSecrets; + return this; + } + + public Integer getOpenCodeAlerts() { + return openCodeAlerts; + } + + public RepoResponseDto setOpenCodeAlerts(Integer openCodeAlerts) { + this.openCodeAlerts = openCodeAlerts; + return this; + } + + public Integer getStaleBranches() { + return staleBranches; + } + + public RepoResponseDto setStaleBranches(Integer staleBranches) { + this.staleBranches = staleBranches; + return this; + } + + public Integer getOpenPRs() { + return openPRs; + } + + public RepoResponseDto setOpenPRs(Integer openPRs) { + this.openPRs = openPRs; + return this; + } + + public Integer getHygieneScore() { + return hygieneScore; + } + + public RepoResponseDto setHygieneScore(Integer hygieneScore) { + this.hygieneScore = hygieneScore; + return this; + } + + public List getScorecard() { + return scorecard; + } + + public RepoResponseDto setScorecard(List scorecard) { + this.scorecard = scorecard; + return this; + } + + public List getTopics() { + return topics; + } + + public RepoResponseDto setTopics(List topics) { + this.topics = topics; + return this; + } +} diff --git a/backend/libs/common/src/main/java/dev/cleat/common/dto/response/ScorecardCheckResponseDto.java b/backend/libs/common/src/main/java/dev/cleat/common/dto/response/ScorecardCheckResponseDto.java new file mode 100644 index 0000000..07e7681 --- /dev/null +++ b/backend/libs/common/src/main/java/dev/cleat/common/dto/response/ScorecardCheckResponseDto.java @@ -0,0 +1,46 @@ +package dev.cleat.common.dto.response; + +import java.util.UUID; + +public class ScorecardCheckResponseDto { + private UUID id; + private String name; + private Integer score; + private String reason; + + public UUID getId() { + return id; + } + + public ScorecardCheckResponseDto setId(UUID id) { + this.id = id; + return this; + } + + public String getReason() { + return reason; + } + + public ScorecardCheckResponseDto setReason(String reason) { + this.reason = reason; + return this; + } + + public Integer getScore() { + return score; + } + + public ScorecardCheckResponseDto setScore(Integer score) { + this.score = score; + return this; + } + + public String getName() { + return name; + } + + public ScorecardCheckResponseDto setName(String name) { + this.name = name; + return this; + } +} diff --git a/backend/libs/common/src/main/java/dev/cleat/common/dto/response/SecretFindingResponseDto.java b/backend/libs/common/src/main/java/dev/cleat/common/dto/response/SecretFindingResponseDto.java new file mode 100644 index 0000000..cae4fb9 --- /dev/null +++ b/backend/libs/common/src/main/java/dev/cleat/common/dto/response/SecretFindingResponseDto.java @@ -0,0 +1,139 @@ +package dev.cleat.common.dto.response; + +import dev.cleat.common.enums.Severity; +import dev.cleat.common.enums.Validity; +import java.time.OffsetDateTime; +import java.util.UUID; + +public class SecretFindingResponseDto { + private UUID id; + private UUID accountId; + private String repo; + private String provider; + private String secretType; + private String file; + private Integer line; + private String commit; + private String author; + private OffsetDateTime detectedAt; + private Validity validity; + private Severity severity; + private Boolean pushProtectionBlocked; + + public UUID getId() { + return id; + } + + public SecretFindingResponseDto setId(UUID id) { + this.id = id; + return this; + } + + public UUID getAccountId() { + return accountId; + } + + public SecretFindingResponseDto setAccountId(UUID accountId) { + this.accountId = accountId; + return this; + } + + public String getRepo() { + return repo; + } + + public SecretFindingResponseDto setRepo(String repo) { + this.repo = repo; + return this; + } + + public String getProvider() { + return provider; + } + + public SecretFindingResponseDto setProvider(String provider) { + this.provider = provider; + return this; + } + + public String getSecretType() { + return secretType; + } + + public SecretFindingResponseDto setSecretType(String secretType) { + this.secretType = secretType; + return this; + } + + public String getFile() { + return file; + } + + public SecretFindingResponseDto setFile(String file) { + this.file = file; + return this; + } + + public Integer getLine() { + return line; + } + + public SecretFindingResponseDto setLine(Integer line) { + this.line = line; + return this; + } + + public String getCommit() { + return commit; + } + + public SecretFindingResponseDto setCommit(String commit) { + this.commit = commit; + return this; + } + + public String getAuthor() { + return author; + } + + public SecretFindingResponseDto setAuthor(String author) { + this.author = author; + return this; + } + + public OffsetDateTime getDetectedAt() { + return detectedAt; + } + + public SecretFindingResponseDto setDetectedAt(OffsetDateTime detectedAt) { + this.detectedAt = detectedAt; + return this; + } + + public Validity getValidity() { + return validity; + } + + public SecretFindingResponseDto setValidity(Validity validity) { + this.validity = validity; + return this; + } + + public Severity getSeverity() { + return severity; + } + + public SecretFindingResponseDto setSeverity(Severity severity) { + this.severity = severity; + return this; + } + + public Boolean getPushProtectionBlocked() { + return pushProtectionBlocked; + } + + public SecretFindingResponseDto setPushProtectionBlocked(Boolean pushProtectionBlocked) { + this.pushProtectionBlocked = pushProtectionBlocked; + return this; + } +} diff --git a/backend/libs/common/src/main/java/dev/cleat/common/dto/response/UsageResponseDto.java b/backend/libs/common/src/main/java/dev/cleat/common/dto/response/UsageResponseDto.java new file mode 100644 index 0000000..e7144bd --- /dev/null +++ b/backend/libs/common/src/main/java/dev/cleat/common/dto/response/UsageResponseDto.java @@ -0,0 +1,111 @@ +package dev.cleat.common.dto.response; + +import dev.cleat.common.dto.BreakdownItem; +import dev.cleat.common.dto.UsagePointDto; +import java.math.BigDecimal; +import java.util.List; + +public class UsageResponseDto { + + private Integer actionsMinutes; + private Integer minutesIncluded; + private Double storageGb; + private Double storageIncludedGb; + private BigDecimal monthlyCost; + private BigDecimal reclaimable; + private List breakdown; + private List series; + + public UsageResponseDto() {} + + public UsageResponseDto( + Integer actionsMinutes, + Integer minutesIncluded, + Double storageGb, + Double storageIncludedGb, + BigDecimal monthlyCost, + BigDecimal reclaimable, + List breakdown, + List series) { + this.actionsMinutes = actionsMinutes; + this.minutesIncluded = minutesIncluded; + this.storageGb = storageGb; + this.storageIncludedGb = storageIncludedGb; + this.monthlyCost = monthlyCost; + this.reclaimable = reclaimable; + this.breakdown = breakdown; + this.series = series; + } + + public Integer getActionsMinutes() { + return actionsMinutes; + } + + public UsageResponseDto setActionsMinutes(Integer actionsMinutes) { + this.actionsMinutes = actionsMinutes; + return this; + } + + public Integer getMinutesIncluded() { + return minutesIncluded; + } + + public UsageResponseDto setMinutesIncluded(Integer minutesIncluded) { + this.minutesIncluded = minutesIncluded; + return this; + } + + public Double getStorageGb() { + return storageGb; + } + + public UsageResponseDto setStorageGb(Double storageGb) { + this.storageGb = storageGb; + return this; + } + + public Double getStorageIncludedGb() { + return storageIncludedGb; + } + + public UsageResponseDto setStorageIncludedGb(Double storageIncludedGb) { + this.storageIncludedGb = storageIncludedGb; + return this; + } + + public BigDecimal getMonthlyCost() { + return monthlyCost; + } + + public UsageResponseDto setMonthlyCost(BigDecimal monthlyCost) { + this.monthlyCost = monthlyCost; + return this; + } + + public BigDecimal getReclaimable() { + return reclaimable; + } + + public UsageResponseDto setReclaimable(BigDecimal reclaimable) { + this.reclaimable = reclaimable; + return this; + } + + public List getBreakdown() { + return breakdown; + } + + public UsageResponseDto setBreakdown(List breakdown) { + this.breakdown = breakdown; + return this; + } + + public List getSeries() { + return series; + } + + public UsageResponseDto setSeries(List series) { + this.series = series; + return this; + } +} diff --git a/backend/libs/common/src/main/java/dev/cleat/common/dto/response/VulnerabilityResponseDto.java b/backend/libs/common/src/main/java/dev/cleat/common/dto/response/VulnerabilityResponseDto.java new file mode 100644 index 0000000..93d11fb --- /dev/null +++ b/backend/libs/common/src/main/java/dev/cleat/common/dto/response/VulnerabilityResponseDto.java @@ -0,0 +1,180 @@ +package dev.cleat.common.dto.response; + +import dev.cleat.common.enums.Reachable; +import dev.cleat.common.enums.Severity; +import java.time.OffsetDateTime; +import java.util.List; +import java.util.UUID; + +public class VulnerabilityResponseDto { + private UUID id; + private UUID accountId; + private String packageName; + private String ecosystem; + private String currentVersion; + private String fixedVersion; + private Double cvss; + private Severity severity; + private Double epss; + private Boolean kev; + private Reachable reachable; + private UUID advisoryId; + private String cwe; + private String title; + private List affectedRepos; + private Boolean hasFixPr; + private OffsetDateTime publishedAt; + + public UUID getId() { + return id; + } + + public VulnerabilityResponseDto setId(UUID id) { + this.id = id; + return this; + } + + public UUID getAccountId() { + return accountId; + } + + public VulnerabilityResponseDto setAccountId(UUID accountId) { + this.accountId = accountId; + return this; + } + + public String getPackageName() { + return packageName; + } + + public VulnerabilityResponseDto setPackageName(String packageName) { + this.packageName = packageName; + return this; + } + + public String getEcosystem() { + return ecosystem; + } + + public VulnerabilityResponseDto setEcosystem(String ecosystem) { + this.ecosystem = ecosystem; + return this; + } + + public String getCurrentVersion() { + return currentVersion; + } + + public VulnerabilityResponseDto setCurrentVersion(String currentVersion) { + this.currentVersion = currentVersion; + return this; + } + + public String getFixedVersion() { + return fixedVersion; + } + + public VulnerabilityResponseDto setFixedVersion(String fixedVersion) { + this.fixedVersion = fixedVersion; + return this; + } + + public Double getCvss() { + return cvss; + } + + public VulnerabilityResponseDto setCvss(Double cvss) { + this.cvss = cvss; + return this; + } + + public Severity getSeverity() { + return severity; + } + + public VulnerabilityResponseDto setSeverity(Severity severity) { + this.severity = severity; + return this; + } + + public Double getEpss() { + return epss; + } + + public VulnerabilityResponseDto setEpss(Double epss) { + this.epss = epss; + return this; + } + + public Boolean getKev() { + return kev; + } + + public VulnerabilityResponseDto setKev(Boolean kev) { + this.kev = kev; + return this; + } + + public Reachable getReachable() { + return reachable; + } + + public VulnerabilityResponseDto setReachable(Reachable reachable) { + this.reachable = reachable; + return this; + } + + public UUID getAdvisoryId() { + return advisoryId; + } + + public VulnerabilityResponseDto setAdvisoryId(UUID advisoryId) { + this.advisoryId = advisoryId; + return this; + } + + public String getCwe() { + return cwe; + } + + public VulnerabilityResponseDto setCwe(String cwe) { + this.cwe = cwe; + return this; + } + + public String getTitle() { + return title; + } + + public VulnerabilityResponseDto setTitle(String title) { + this.title = title; + return this; + } + + public List getAffectedRepos() { + return affectedRepos; + } + + public VulnerabilityResponseDto setAffectedRepos(List affectedRepos) { + this.affectedRepos = affectedRepos; + return this; + } + + public Boolean getHasFixPr() { + return hasFixPr; + } + + public VulnerabilityResponseDto setHasFixPr(Boolean hasFixPr) { + this.hasFixPr = hasFixPr; + return this; + } + + public OffsetDateTime getPublishedAt() { + return publishedAt; + } + + public VulnerabilityResponseDto setPublishedAt(OffsetDateTime publishedAt) { + this.publishedAt = publishedAt; + return this; + } +} diff --git a/backend/libs/common/src/main/java/dev/cleat/common/enums/AccountType.java b/backend/libs/common/src/main/java/dev/cleat/common/enums/AccountType.java new file mode 100644 index 0000000..f3409f2 --- /dev/null +++ b/backend/libs/common/src/main/java/dev/cleat/common/enums/AccountType.java @@ -0,0 +1,19 @@ +package dev.cleat.common.enums; + +import com.fasterxml.jackson.annotation.JsonValue; + +public enum AccountType { + USER("user"), + ORG("org"); + + private final String value; + + AccountType(String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return value; + } +} diff --git a/backend/libs/common/src/main/java/dev/cleat/common/enums/EventCategory.java b/backend/libs/common/src/main/java/dev/cleat/common/enums/EventCategory.java new file mode 100644 index 0000000..f3ab153 --- /dev/null +++ b/backend/libs/common/src/main/java/dev/cleat/common/enums/EventCategory.java @@ -0,0 +1,21 @@ +package dev.cleat.common.enums; + +import com.fasterxml.jackson.annotation.JsonValue; + +public enum EventCategory { + SECURITY("security"), + ACCESS("access"), + SUPPLY_CHAIN("supply-chain"), + MAINTENANCE("maintenance"); + + private final String value; + + EventCategory(String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return value; + } +} diff --git a/backend/libs/common/src/main/java/dev/cleat/common/enums/Plan.java b/backend/libs/common/src/main/java/dev/cleat/common/enums/Plan.java new file mode 100644 index 0000000..3a1f80d --- /dev/null +++ b/backend/libs/common/src/main/java/dev/cleat/common/enums/Plan.java @@ -0,0 +1,20 @@ +package dev.cleat.common.enums; + +import com.fasterxml.jackson.annotation.JsonValue; + +public enum Plan { + FREE("Free"), + TEAM("Team"), + ENTERPRISE("Enterprise"); + + private final String value; + + Plan(String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return value; + } +} diff --git a/backend/libs/common/src/main/java/dev/cleat/common/enums/Reachable.java b/backend/libs/common/src/main/java/dev/cleat/common/enums/Reachable.java new file mode 100644 index 0000000..4c59626 --- /dev/null +++ b/backend/libs/common/src/main/java/dev/cleat/common/enums/Reachable.java @@ -0,0 +1,20 @@ +package dev.cleat.common.enums; + +import com.fasterxml.jackson.annotation.JsonValue; + +public enum Reachable { + REACHABLE("reachable"), + NOT_REACHABLE("not-reachable"), + UNKNOWN("unknown"); + + private final String value; + + Reachable(String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return value; + } +} diff --git a/backend/libs/common/src/main/java/dev/cleat/common/enums/Role.java b/backend/libs/common/src/main/java/dev/cleat/common/enums/Role.java new file mode 100644 index 0000000..ddcccd8 --- /dev/null +++ b/backend/libs/common/src/main/java/dev/cleat/common/enums/Role.java @@ -0,0 +1,20 @@ +package dev.cleat.common.enums; + +import com.fasterxml.jackson.annotation.JsonValue; + +public enum Role { + OWNER("owner"), + ADMIN("admin"), + MEMBER("member"); + + private final String value; + + Role(String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return value; + } +} diff --git a/backend/libs/common/src/main/java/dev/cleat/common/enums/Severity.java b/backend/libs/common/src/main/java/dev/cleat/common/enums/Severity.java new file mode 100644 index 0000000..f0fe12b --- /dev/null +++ b/backend/libs/common/src/main/java/dev/cleat/common/enums/Severity.java @@ -0,0 +1,21 @@ +package dev.cleat.common.enums; + +import com.fasterxml.jackson.annotation.JsonValue; + +public enum Severity { + CRITICAL("critical"), + HIGH("high"), + MEDIUM("medium"), + LOW("low"); + + private final String value; + + Severity(String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return value; + } +} diff --git a/backend/libs/common/src/main/java/dev/cleat/common/enums/Validity.java b/backend/libs/common/src/main/java/dev/cleat/common/enums/Validity.java new file mode 100644 index 0000000..d0c1de0 --- /dev/null +++ b/backend/libs/common/src/main/java/dev/cleat/common/enums/Validity.java @@ -0,0 +1,20 @@ +package dev.cleat.common.enums; + +import com.fasterxml.jackson.annotation.JsonValue; + +public enum Validity { + ACTIVE("active"), + REVOKED("revoked"), + UNKNOWN("unknown"); + + private final String value; + + Validity(String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return value; + } +} diff --git a/backend/libs/common/src/main/java/dev/cleat/common/enums/Visibility.java b/backend/libs/common/src/main/java/dev/cleat/common/enums/Visibility.java new file mode 100644 index 0000000..45b3db5 --- /dev/null +++ b/backend/libs/common/src/main/java/dev/cleat/common/enums/Visibility.java @@ -0,0 +1,20 @@ +package dev.cleat.common.enums; + +import com.fasterxml.jackson.annotation.JsonValue; + +public enum Visibility { + PUBLIC("public"), + PRIVATE("private"), + INTERNAL("internal"); + + private final String value; + + Visibility(String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return value; + } +} diff --git a/backend/libs/common/src/main/java/dev/cleat/common/exception/NotFoundException.java b/backend/libs/common/src/main/java/dev/cleat/common/exception/NotFoundException.java new file mode 100644 index 0000000..781ba69 --- /dev/null +++ b/backend/libs/common/src/main/java/dev/cleat/common/exception/NotFoundException.java @@ -0,0 +1,10 @@ +package dev.cleat.common.exception; + +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus +public class NotFoundException extends RuntimeException { + public NotFoundException(String message) { + super(message); + } +} diff --git a/backend/libs/common/src/main/java/dev/cleat/common/mapper/CleatMapper.java b/backend/libs/common/src/main/java/dev/cleat/common/mapper/CleatMapper.java new file mode 100644 index 0000000..b4110b6 --- /dev/null +++ b/backend/libs/common/src/main/java/dev/cleat/common/mapper/CleatMapper.java @@ -0,0 +1,268 @@ +package dev.cleat.common.mapper; + +import dev.cleat.common.dto.UsagePointDto; +import dev.cleat.common.dto.request.AccountRequestDto; +import dev.cleat.common.dto.request.ActivityEventRequestDto; +import dev.cleat.common.dto.request.MemberRequestDto; +import dev.cleat.common.dto.request.RepoRequestDto; +import dev.cleat.common.dto.request.SecretFindingRequestDto; +import dev.cleat.common.dto.request.VulnerabilityRequestDto; +import dev.cleat.common.dto.response.AccountResponseDto; +import dev.cleat.common.dto.response.ActivityEventResponseDto; +import dev.cleat.common.dto.response.DatasetDto; +import dev.cleat.common.dto.response.MemberResponseDto; +import dev.cleat.common.dto.response.RepoResponseDto; +import dev.cleat.common.dto.response.ScorecardCheckResponseDto; +import dev.cleat.common.dto.response.SecretFindingResponseDto; +import dev.cleat.common.dto.response.UsageResponseDto; +import dev.cleat.common.dto.response.VulnerabilityResponseDto; +import dev.cleat.persistence.entity.AccountEntity; +import dev.cleat.persistence.entity.ActivityEventEntity; +import dev.cleat.persistence.entity.MemberEntity; +import dev.cleat.persistence.entity.RepoEntity; +import dev.cleat.persistence.entity.ScorecardCheckEntity; +import dev.cleat.persistence.entity.SecretFindingEntity; +import dev.cleat.persistence.entity.UsageEntity; +import dev.cleat.persistence.entity.UsagePointEntity; +import dev.cleat.persistence.entity.VulnerabilityEntity; +import java.util.List; +import org.springframework.stereotype.Component; + +@Component +public class CleatMapper { + + public AccountResponseDto toAccountDto(AccountEntity accountEntity) { + if (accountEntity == null) return null; + + return new AccountResponseDto() + .setId(accountEntity.getId()) + .setLogin(accountEntity.getLogin()) + .setName(accountEntity.getName()) + .setType(accountEntity.getType()) + .setPlan(accountEntity.getPlan()) + .setRepoCount(accountEntity.getRepoCount()) + .setMemberCount(accountEntity.getMemberCount()) + .setPostureScore(accountEntity.getPostureScore()) + .setMonthlySpend(accountEntity.getMonthlySpend()) + .setReclaimable(accountEntity.getReclaimable()); + } + + public AccountEntity toAccountEntity(AccountRequestDto accountRequestDto) { + if (accountRequestDto == null) return null; + return new AccountEntity() + .setLogin(accountRequestDto.getLogin()) + .setName(accountRequestDto.getName()) + .setType(accountRequestDto.getType()) + .setPlan(accountRequestDto.getPlan()); + } + + public RepoResponseDto toRepoDto(RepoEntity repoEntity) { + if (repoEntity == null) return null; + return new RepoResponseDto() + .setId(repoEntity.getId()) + .setName(repoEntity.getName()) + .setAccountId(repoEntity.getAccount().getId()) + .setVisibility(repoEntity.getVisibility()) + .setLanguage(repoEntity.getLanguage()) + .setStars(repoEntity.getStars()) + .setDefaultBranch(repoEntity.getDefaultBranch()) + .setBranchProtected(repoEntity.getBranchProtected()) + .setHasReadme(repoEntity.getHasReadme()) + .setHasLicense(repoEntity.getHasLicense()) + .setHasContributing(repoEntity.getHasContributing()) + .setHasCodeowners(repoEntity.getHasCodeowners()) + .setHasCI(repoEntity.getHasCi()) + .setSizeMb(repoEntity.getSizeMb()) + .setLastPushedAt(repoEntity.getLastPushedAt()) + .setArchived(repoEntity.getArchived()) + .setOpenVulns(repoEntity.getOpenVulns()) + .setOpenSecrets(repoEntity.getOpenSecrets()) + .setOpenCodeAlerts(repoEntity.getOpenCodeAlerts()) + .setStaleBranches(repoEntity.getStaleBranches()) + .setOpenPRs(repoEntity.getOpenPRs()) + .setHygieneScore(repoEntity.getHygieneScore()) + .setScorecard((repoEntity.getScorecard()) + .stream().map(this::toScorecardCheckResponseDto).toList()) + .setTopics(repoEntity.getTopics()); + } + + public RepoEntity toRepoEntity(RepoRequestDto repoRequestDto) { + if (repoRequestDto == null) return null; + return new RepoEntity() + .setName(repoRequestDto.getName()) + .setVisibility(repoRequestDto.getVisibility()) + .setLanguage(repoRequestDto.getLanguage()) + .setDefaultBranch(repoRequestDto.getDefaultBranch()) + .setTopics(repoRequestDto.getTopics()); + } + + public ScorecardCheckResponseDto toScorecardCheckResponseDto(ScorecardCheckEntity scorecardCheckEntity) { + if (scorecardCheckEntity == null) return null; + return new ScorecardCheckResponseDto() + .setId(scorecardCheckEntity.getId()) + .setName(scorecardCheckEntity.getName()) + .setReason(scorecardCheckEntity.getReason()) + .setScore(scorecardCheckEntity.getScore()); + } + + public ActivityEventResponseDto toActivityEventDto(ActivityEventEntity activityEventEntity) { + if (activityEventEntity == null) return null; + return new ActivityEventResponseDto() + .setId(activityEventEntity.getId()) + .setAccountId(activityEventEntity.getAccount().getId()) + .setType(activityEventEntity.getType()) + .setSeverity(activityEventEntity.getSeverity()) + .setActor(activityEventEntity.getActor()) + .setTarget(activityEventEntity.getTarget()) + .setRepo(activityEventEntity.getRepo().getName()) + .setMessage(activityEventEntity.getMessage()) + .setCreatedAt(activityEventEntity.getCreatedAt()); + } + + public ActivityEventEntity toActiveEventEntity(ActivityEventRequestDto activityEventRequestDto) { + if (activityEventRequestDto == null) return null; + return new ActivityEventEntity() + .setType(activityEventRequestDto.getType()) + .setSeverity(activityEventRequestDto.getSeverity()) + .setActor(activityEventRequestDto.getActor()) + .setTarget(activityEventRequestDto.getTarget()) + .setMessage(activityEventRequestDto.getMessage()); + } + + public DatasetDto toDatasetDto( + AccountResponseDto account, + List repos, + List secrets, + List vulnerabilities, + UsageResponseDto usage, + List members, + List events) { + return new DatasetDto() + .setAccount(account) + .setRepos(repos) + .setSecrets(secrets) + .setVulnerabilities(vulnerabilities) + .setUsage(usage) + .setMembers(members) + .setEvents(events); + } + + public MemberResponseDto toMemberDto(MemberEntity memberEntity) { + return new MemberResponseDto() + .setId(memberEntity.getId()) + .setLogin(memberEntity.getLogin()) + .setName(memberEntity.getName()) + .setRole(memberEntity.getRole()) + .setTwoFactor(memberEntity.getTwoFactor()) + .setLastActiveAt(memberEntity.getLastActiveAt()) + .setTeams(memberEntity.getTeams()) + .setOutsideCollaborator(memberEntity.getOutsideCollaborator()) + .setRepoAccess(memberEntity.getRepoAccess()); + } + + public MemberEntity toMemberEntity(MemberRequestDto memberRequestDto) { + if (memberRequestDto == null) return null; + return new MemberEntity() + .setLogin(memberRequestDto.getLogin()) + .setName(memberRequestDto.getName()) + .setRole(memberRequestDto.getRole()) + .setTwoFactor(memberRequestDto.getTwoFactor()) + .setTeams(memberRequestDto.getTeams()); + } + + public SecretFindingResponseDto toSecretFindingDto(SecretFindingEntity secretFindingEntity) { + return new SecretFindingResponseDto() + .setId(secretFindingEntity.getId()) + .setAccountId(secretFindingEntity.getAccount().getId()) + .setRepo(secretFindingEntity.getRepo().getName()) + .setProvider(secretFindingEntity.getProvider()) + .setSecretType(secretFindingEntity.getSecretType()) + .setFile(secretFindingEntity.getFile()) + .setLine(secretFindingEntity.getLine()) + .setCommit(secretFindingEntity.getCommit()) + .setAuthor(secretFindingEntity.getAuthor()) + .setDetectedAt(secretFindingEntity.getDetectedAt()) + .setValidity(secretFindingEntity.getValidity()) + .setSeverity(secretFindingEntity.getSeverity()) + .setPushProtectionBlocked(secretFindingEntity.getPushProtectionBlocked()); + } + + public SecretFindingEntity toSecretFindingEntity(SecretFindingRequestDto secretFindingRequestDto) { + if (secretFindingRequestDto == null) return null; + return new SecretFindingEntity() + .setProvider(secretFindingRequestDto.getProvider()) + .setSecretType(secretFindingRequestDto.getSecretType()) + .setFile(secretFindingRequestDto.getFile()) + .setLine(secretFindingRequestDto.getLine()) + .setCommit(secretFindingRequestDto.getCommit()) + .setAuthor(secretFindingRequestDto.getAuthor()) + .setDetectedAt(secretFindingRequestDto.getDetectedAt()) + .setValidity(secretFindingRequestDto.getValidity()) + .setSeverity(secretFindingRequestDto.getSeverity()) + .setPushProtectionBlocked(secretFindingRequestDto.getPushProtectionBlocked()); + } + + public VulnerabilityResponseDto toVulnerabilityDto(VulnerabilityEntity vulnerabilityEntity) { + return new VulnerabilityResponseDto() + .setId(vulnerabilityEntity.getId()) + .setAccountId(vulnerabilityEntity.getAccount().getId()) + .setPackageName(vulnerabilityEntity.getPackageName()) + .setEcosystem(vulnerabilityEntity.getEcosystem()) + .setCurrentVersion(vulnerabilityEntity.getCurrentVersion()) + .setFixedVersion(vulnerabilityEntity.getFixedVersion()) + .setCvss(vulnerabilityEntity.getCvss()) + .setSeverity(vulnerabilityEntity.getSeverity()) + .setEpss(vulnerabilityEntity.getEpss()) + .setKev(vulnerabilityEntity.getKev()) + .setReachable(vulnerabilityEntity.getReachable()) + .setAdvisoryId(vulnerabilityEntity.getAdvisoryId()) + .setCwe(vulnerabilityEntity.getCwe()) + .setTitle(vulnerabilityEntity.getTitle()) + .setAffectedRepos(vulnerabilityEntity.getAffectedRepos()) + .setHasFixPr(vulnerabilityEntity.getHasFixPr()) + .setPublishedAt(vulnerabilityEntity.getPublishedAt()); + } + + public VulnerabilityEntity toVulnerabilityEntity(VulnerabilityRequestDto vulnerabilityRequestDto) { + if (vulnerabilityRequestDto == null) return null; + return new VulnerabilityEntity() + .setPackageName(vulnerabilityRequestDto.getPackageName()) + .setEcosystem(vulnerabilityRequestDto.getEcosystem()) + .setCurrentVersion(vulnerabilityRequestDto.getCurrentVersion()) + .setFixedVersion(vulnerabilityRequestDto.getFixedVersion()) + .setCvss(vulnerabilityRequestDto.getCvss()) + .setSeverity(vulnerabilityRequestDto.getSeverity()) + .setEpss(vulnerabilityRequestDto.getEpss()) + .setKev(vulnerabilityRequestDto.getKev()) + .setReachable(vulnerabilityRequestDto.getReachable()) + .setAdvisoryId(vulnerabilityRequestDto.getAdvisoryId()) + .setCwe(vulnerabilityRequestDto.getCwe()) + .setTitle(vulnerabilityRequestDto.getTitle()) + .setAffectedRepos(vulnerabilityRequestDto.getAffectedRepos()) + .setHasFixPr(vulnerabilityRequestDto.getHasFixPr()); + } + + public UsageResponseDto toUsageDto(UsageEntity usageEntity) { + if (usageEntity == null) return null; + return new UsageResponseDto() + .setActionsMinutes(usageEntity.getActionsMinutes()) + .setMinutesIncluded(usageEntity.getMinutesIncluded()) + .setStorageGb(usageEntity.getStorageGb()) + .setStorageIncludedGb(usageEntity.getStorageIncludedGb()) + .setMonthlyCost(usageEntity.getMonthlyCost()) + .setReclaimable(usageEntity.getReclaimable()) + .setBreakdown(usageEntity.getBreakdown()) + .setSeries(usageEntity.getSeries().stream() + .map(this::toUsagePointDto) + .toList()); + } + + public UsagePointDto toUsagePointDto(UsagePointEntity usagePointEntity) { + if (usagePointEntity == null) return null; + return new UsagePointDto() + .setLabel(usagePointEntity.getLabel()) + .setMinutes(usagePointEntity.getMinutes()) + .setStorageGb(usagePointEntity.getStorageGb()) + .setCost(usagePointEntity.getCost()); + } +} diff --git a/backend/libs/domain/build.gradle.kts b/backend/libs/domain/build.gradle.kts index 025c934..c6800f2 100644 --- a/backend/libs/domain/build.gradle.kts +++ b/backend/libs/domain/build.gradle.kts @@ -3,5 +3,8 @@ plugins { } dependencies { + implementation("org.springframework.boot:spring-boot-starter-web") testImplementation("org.springframework.boot:spring-boot-starter-test") + implementation(project(":libs:persistence")) + implementation(project(":libs:common")) } diff --git a/backend/libs/domain/src/main/java/dev/cleat/domain/DashboardService.java b/backend/libs/domain/src/main/java/dev/cleat/domain/DashboardService.java new file mode 100644 index 0000000..032997f --- /dev/null +++ b/backend/libs/domain/src/main/java/dev/cleat/domain/DashboardService.java @@ -0,0 +1,74 @@ +package dev.cleat.domain; + +import dev.cleat.common.dto.response.DatasetDto; +import dev.cleat.common.exception.NotFoundException; +import dev.cleat.common.mapper.CleatMapper; +import dev.cleat.persistence.entity.AccountEntity; +import dev.cleat.persistence.entity.UsageEntity; +import dev.cleat.persistence.repository.AccountRepository; +import dev.cleat.persistence.repository.ActivityEventRepository; +import dev.cleat.persistence.repository.MemberRepository; +import dev.cleat.persistence.repository.RepoRepository; +import dev.cleat.persistence.repository.SecretFindingRepository; +import dev.cleat.persistence.repository.UsageRepository; +import dev.cleat.persistence.repository.VulnerabilityRepository; +import java.util.UUID; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class DashboardService { + private final AccountRepository accountRepository; + private final RepoRepository repoRepository; + private final SecretFindingRepository secretFindingRepository; + private final VulnerabilityRepository vulnerabilityRepository; + private final UsageRepository usageRepository; + private final MemberRepository memberRepository; + private final ActivityEventRepository activityEventRepository; + private final CleatMapper cleatMapper; + + public DashboardService( + AccountRepository accountRepository, + RepoRepository repoRepository, + SecretFindingRepository secretFindingRepository, + VulnerabilityRepository vulnerabilityRepository, + UsageRepository usageRepository, + MemberRepository memberRepository, + ActivityEventRepository activityEventRepository, + CleatMapper cleatMapper) { + this.accountRepository = accountRepository; + this.repoRepository = repoRepository; + this.secretFindingRepository = secretFindingRepository; + this.vulnerabilityRepository = vulnerabilityRepository; + this.usageRepository = usageRepository; + this.memberRepository = memberRepository; + this.activityEventRepository = activityEventRepository; + this.cleatMapper = cleatMapper; + } + + @Transactional(readOnly = true) + public DatasetDto getDataset(UUID accountId) { + + AccountEntity account = + accountRepository.findById(accountId).orElseThrow(() -> new NotFoundException("Account not found")); + return new DatasetDto( + cleatMapper.toAccountDto(account), + repoRepository.findAllByAccountId(accountId).stream() + .map(cleatMapper::toRepoDto) + .toList(), + secretFindingRepository.findAllByAccountId(accountId).stream() + .map(cleatMapper::toSecretFindingDto) + .toList(), + vulnerabilityRepository.findAllByAccountId(accountId).stream() + .map(cleatMapper::toVulnerabilityDto) + .toList(), + cleatMapper.toUsageDto( + usageRepository.findByAccountId(accountId).orElse(new UsageEntity())), + memberRepository.findAllByAccountId(accountId).stream() + .map(cleatMapper::toMemberDto) + .toList(), + activityEventRepository.findAllByAccountId(accountId).stream() + .map(cleatMapper::toActivityEventDto) + .toList()); + } +} diff --git a/backend/libs/persistence/src/main/java/dev/cleat/persistence/AccountType.java b/backend/libs/persistence/src/main/java/dev/cleat/persistence/AccountType.java deleted file mode 100644 index d792f6a..0000000 --- a/backend/libs/persistence/src/main/java/dev/cleat/persistence/AccountType.java +++ /dev/null @@ -1,6 +0,0 @@ -package dev.cleat.persistence; - -public enum AccountType { - USER, - ORG -} diff --git a/backend/libs/persistence/src/main/java/dev/cleat/persistence/Plan.java b/backend/libs/persistence/src/main/java/dev/cleat/persistence/Plan.java deleted file mode 100644 index 752828f..0000000 --- a/backend/libs/persistence/src/main/java/dev/cleat/persistence/Plan.java +++ /dev/null @@ -1,6 +0,0 @@ -package dev.cleat.persistence; - -public enum Plan { - FREE, - TEAM -} diff --git a/backend/libs/persistence/src/main/java/dev/cleat/persistence/Visibility.java b/backend/libs/persistence/src/main/java/dev/cleat/persistence/Visibility.java deleted file mode 100644 index d5920b6..0000000 --- a/backend/libs/persistence/src/main/java/dev/cleat/persistence/Visibility.java +++ /dev/null @@ -1,7 +0,0 @@ -package dev.cleat.persistence; - -public enum Visibility { - PUBLIC, - PRIVATE, - INTERNAL -} diff --git a/backend/libs/persistence/src/main/java/dev/cleat/persistence/AccountEntity.java b/backend/libs/persistence/src/main/java/dev/cleat/persistence/entity/AccountEntity.java similarity index 91% rename from backend/libs/persistence/src/main/java/dev/cleat/persistence/AccountEntity.java rename to backend/libs/persistence/src/main/java/dev/cleat/persistence/entity/AccountEntity.java index 33cb645..c06c4c9 100644 --- a/backend/libs/persistence/src/main/java/dev/cleat/persistence/AccountEntity.java +++ b/backend/libs/persistence/src/main/java/dev/cleat/persistence/entity/AccountEntity.java @@ -1,200 +1,202 @@ -package dev.cleat.persistence; - -import jakarta.persistence.CascadeType; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.OneToMany; -import jakarta.persistence.Table; -import java.math.BigDecimal; -import java.time.OffsetDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; -import org.hibernate.annotations.CreationTimestamp; - -@Entity -@Table(name = "account") -public class AccountEntity { - - @Id - @GeneratedValue(strategy = GenerationType.UUID) - private UUID id; - - @Column(name = "login", nullable = false) - private String login; - - @Column(name = "name", nullable = false) - private String name; - - @Column(name = "type", nullable = false) - @Enumerated(EnumType.STRING) - private AccountType type; - - @Column(name = "plan") - @Enumerated(EnumType.STRING) - private Plan plan; - - @Column(name = "repo_count") - private Integer repoCount; - - @Column(name = "member_count") - private Integer memberCount; - - @Column(name = "posture_score") - private Integer postureScore; - - @Column(name = "monthly_spend") - private BigDecimal monthlySpend; - - @Column(name = "reclaimable") - private BigDecimal reclaimable; - - @CreationTimestamp - @Column(name = "created_at", nullable = false, updatable = false) - private OffsetDateTime createdAt; - - @OneToMany(mappedBy = "account", cascade = CascadeType.ALL, orphanRemoval = true) - private List repos = new ArrayList<>(); - - public AccountEntity( - UUID id, - String login, - String name, - AccountType type, - Plan plan, - Integer repoCount, - Integer memberCount, - Integer postureScore, - BigDecimal monthlySpend, - BigDecimal reclaimable, - OffsetDateTime createdAt, - List repos) { - this.id = id; - this.login = login; - this.name = name; - this.type = type; - this.plan = plan; - this.repoCount = repoCount; - this.memberCount = memberCount; - this.postureScore = postureScore; - this.monthlySpend = monthlySpend; - this.reclaimable = reclaimable; - this.createdAt = createdAt; - this.repos = repos; - } - - public AccountEntity() {} - - public UUID getId() { - return id; - } - - public AccountEntity setId(UUID id) { - this.id = id; - return this; - } - - public String getLogin() { - return login; - } - - public AccountEntity setLogin(String login) { - this.login = login; - return this; - } - - public String getName() { - return name; - } - - public AccountEntity setName(String name) { - this.name = name; - return this; - } - - public AccountType getType() { - return type; - } - - public AccountEntity setType(AccountType type) { - this.type = type; - return this; - } - - public Plan getPlan() { - return plan; - } - - public AccountEntity setPlan(Plan plan) { - this.plan = plan; - return this; - } - - public Integer getRepoCount() { - return repoCount; - } - - public AccountEntity setRepoCount(Integer repoCount) { - this.repoCount = repoCount; - return this; - } - - public Integer getMemberCount() { - return memberCount; - } - - public AccountEntity setMemberCount(Integer memberCount) { - this.memberCount = memberCount; - return this; - } - - public Integer getPostureScore() { - return postureScore; - } - - public AccountEntity setPostureScore(Integer postureScore) { - this.postureScore = postureScore; - return this; - } - - public BigDecimal getMonthlySpend() { - return monthlySpend; - } - - public AccountEntity setMonthlySpend(BigDecimal monthlySpend) { - this.monthlySpend = monthlySpend; - return this; - } - - public BigDecimal getReclaimable() { - return reclaimable; - } - - public AccountEntity setReclaimable(BigDecimal reclaimable) { - this.reclaimable = reclaimable; - return this; - } - - public OffsetDateTime getCreatedAt() { - return createdAt; - } - - public AccountEntity setCreatedAt(OffsetDateTime createdAt) { - this.createdAt = createdAt; - return this; - } - - public List getRepos() { - return repos; - } - - public AccountEntity setRepos(List repos) { - this.repos = repos; - return this; - } -} +package dev.cleat.persistence.entity; + +import dev.cleat.common.enums.AccountType; +import dev.cleat.common.enums.Plan; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import java.math.BigDecimal; +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import org.hibernate.annotations.CreationTimestamp; + +@Entity +@Table(name = "account") +public class AccountEntity { + + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private UUID id; + + @Column(name = "login", nullable = false) + private String login; + + @Column(name = "name", nullable = false) + private String name; + + @Column(name = "type", nullable = false) + @Enumerated(EnumType.STRING) + private AccountType type; + + @Column(name = "plan") + @Enumerated(EnumType.STRING) + private Plan plan; + + @Column(name = "repo_count") + private Integer repoCount; + + @Column(name = "member_count") + private Integer memberCount; + + @Column(name = "posture_score") + private Integer postureScore; + + @Column(name = "monthly_spend") + private BigDecimal monthlySpend; + + @Column(name = "reclaimable") + private BigDecimal reclaimable; + + @CreationTimestamp + @Column(name = "created_at", nullable = false, updatable = false) + private OffsetDateTime createdAt; + + @OneToMany(mappedBy = "account", cascade = CascadeType.ALL, orphanRemoval = true) + private List repos = new ArrayList<>(); + + public AccountEntity( + UUID id, + String login, + String name, + AccountType type, + Plan plan, + Integer repoCount, + Integer memberCount, + Integer postureScore, + BigDecimal monthlySpend, + BigDecimal reclaimable, + OffsetDateTime createdAt, + List repos) { + this.id = id; + this.login = login; + this.name = name; + this.type = type; + this.plan = plan; + this.repoCount = repoCount; + this.memberCount = memberCount; + this.postureScore = postureScore; + this.monthlySpend = monthlySpend; + this.reclaimable = reclaimable; + this.createdAt = createdAt; + this.repos = repos; + } + + public AccountEntity() {} + + public UUID getId() { + return id; + } + + public AccountEntity setId(UUID id) { + this.id = id; + return this; + } + + public String getLogin() { + return login; + } + + public AccountEntity setLogin(String login) { + this.login = login; + return this; + } + + public String getName() { + return name; + } + + public AccountEntity setName(String name) { + this.name = name; + return this; + } + + public dev.cleat.common.enums.AccountType getType() { + return type; + } + + public AccountEntity setType(AccountType type) { + this.type = type; + return this; + } + + public dev.cleat.common.enums.Plan getPlan() { + return plan; + } + + public AccountEntity setPlan(Plan plan) { + this.plan = plan; + return this; + } + + public Integer getRepoCount() { + return repoCount; + } + + public AccountEntity setRepoCount(Integer repoCount) { + this.repoCount = repoCount; + return this; + } + + public Integer getMemberCount() { + return memberCount; + } + + public AccountEntity setMemberCount(Integer memberCount) { + this.memberCount = memberCount; + return this; + } + + public Integer getPostureScore() { + return postureScore; + } + + public AccountEntity setPostureScore(Integer postureScore) { + this.postureScore = postureScore; + return this; + } + + public BigDecimal getMonthlySpend() { + return monthlySpend; + } + + public AccountEntity setMonthlySpend(BigDecimal monthlySpend) { + this.monthlySpend = monthlySpend; + return this; + } + + public BigDecimal getReclaimable() { + return reclaimable; + } + + public AccountEntity setReclaimable(BigDecimal reclaimable) { + this.reclaimable = reclaimable; + return this; + } + + public OffsetDateTime getCreatedAt() { + return createdAt; + } + + public AccountEntity setCreatedAt(OffsetDateTime createdAt) { + this.createdAt = createdAt; + return this; + } + + public List getRepos() { + return repos; + } + + public AccountEntity setRepos(List repos) { + this.repos = repos; + return this; + } +} diff --git a/backend/libs/persistence/src/main/java/dev/cleat/persistence/entity/ActivityEventEntity.java b/backend/libs/persistence/src/main/java/dev/cleat/persistence/entity/ActivityEventEntity.java new file mode 100644 index 0000000..665eb45 --- /dev/null +++ b/backend/libs/persistence/src/main/java/dev/cleat/persistence/entity/ActivityEventEntity.java @@ -0,0 +1,172 @@ +package dev.cleat.persistence.entity; + +import dev.cleat.common.enums.EventCategory; +import dev.cleat.common.enums.Severity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import java.time.OffsetDateTime; +import java.util.UUID; + +@Entity +@Table(name = "activity-entity") +public class ActivityEventEntity { + + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private UUID id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "account_id") + private AccountEntity account; + + @Column(name = "type") + private String type; + + @Enumerated(EnumType.STRING) + @Column(name = "severity") + private Severity severity; + + @Column(name = "actor") + private String actor; + + @Column(name = "target") + private String target; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "repo") + private RepoEntity repo; + + @Column(name = "message") + private String message; + + @Column(name = "created_at") + private OffsetDateTime createdAt; + + @Enumerated(EnumType.STRING) + @Column(name = "category") + private EventCategory category; + + public ActivityEventEntity() {} + + public ActivityEventEntity( + UUID id, + AccountEntity account, + String type, + Severity severity, + String actor, + String target, + RepoEntity repo, + String message, + OffsetDateTime createdAt, + EventCategory category) { + this.id = id; + this.account = account; + this.type = type; + this.severity = severity; + this.actor = actor; + this.target = target; + this.repo = repo; + this.message = message; + this.createdAt = createdAt; + this.category = category; + } + + public UUID getId() { + return id; + } + + public ActivityEventEntity setId(UUID id) { + this.id = id; + return this; + } + + public AccountEntity getAccount() { + return account; + } + + public ActivityEventEntity setAccount(AccountEntity account) { + this.account = account; + return this; + } + + public String getType() { + return type; + } + + public ActivityEventEntity setType(String type) { + this.type = type; + return this; + } + + public Severity getSeverity() { + return severity; + } + + public ActivityEventEntity setSeverity(Severity severity) { + this.severity = severity; + return this; + } + + public String getActor() { + return actor; + } + + public ActivityEventEntity setActor(String actor) { + this.actor = actor; + return this; + } + + public String getTarget() { + return target; + } + + public ActivityEventEntity setTarget(String target) { + this.target = target; + return this; + } + + public RepoEntity getRepo() { + return repo; + } + + public ActivityEventEntity setRepo(RepoEntity repo) { + this.repo = repo; + return this; + } + + public String getMessage() { + return message; + } + + public ActivityEventEntity setMessage(String message) { + this.message = message; + return this; + } + + public OffsetDateTime getCreatedAt() { + return createdAt; + } + + public ActivityEventEntity setCreatedAt(OffsetDateTime createdAt) { + this.createdAt = createdAt; + return this; + } + + public EventCategory getCategory() { + return category; + } + + public ActivityEventEntity setCategory(EventCategory category) { + this.category = category; + return this; + } +} diff --git a/backend/libs/persistence/src/main/java/dev/cleat/persistence/entity/MemberEntity.java b/backend/libs/persistence/src/main/java/dev/cleat/persistence/entity/MemberEntity.java new file mode 100644 index 0000000..14ea0bc --- /dev/null +++ b/backend/libs/persistence/src/main/java/dev/cleat/persistence/entity/MemberEntity.java @@ -0,0 +1,157 @@ +package dev.cleat.persistence.entity; + +import dev.cleat.common.enums.Role; +import jakarta.persistence.CollectionTable; +import jakarta.persistence.Column; +import jakarta.persistence.ElementCollection; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.Table; +import java.time.OffsetDateTime; +import java.util.List; +import java.util.UUID; + +@Entity +@Table(name = "member") +public class MemberEntity { + + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private UUID id; + + @Column(name = "login") + private String login; + + @Column(name = "name") + private String name; + + @Enumerated(EnumType.STRING) + @Column(name = "role") + private Role role; + + @Column(name = "two_factor") + private Boolean twoFactor; + + @Column(name = "last_active_at") + private OffsetDateTime lastActiveAt; + + @ElementCollection + @CollectionTable(name = "member_teams", joinColumns = @JoinColumn(name = "member_id")) + @Column(name = "teams") + private List teams; + + @Column(name = "outside_collaborator") + private Boolean outsideCollaborator; + + @Column(name = "repo_access") + private Integer repoAccess; + + public MemberEntity() {} + + public MemberEntity( + UUID id, + String login, + String name, + Role role, + Boolean twoFactor, + OffsetDateTime lastActiveAt, + List teams, + Boolean outsideCollaborator, + Integer repoAccess) { + this.id = id; + this.login = login; + this.name = name; + this.role = role; + this.twoFactor = twoFactor; + this.lastActiveAt = lastActiveAt; + this.teams = teams; + this.outsideCollaborator = outsideCollaborator; + this.repoAccess = repoAccess; + } + + public UUID getId() { + return id; + } + + public MemberEntity setId(UUID id) { + this.id = id; + return this; + } + + public String getLogin() { + return login; + } + + public MemberEntity setLogin(String login) { + this.login = login; + return this; + } + + public String getName() { + return name; + } + + public MemberEntity setName(String name) { + this.name = name; + return this; + } + + public Role getRole() { + return role; + } + + public MemberEntity setRole(Role role) { + this.role = role; + return this; + } + + public Boolean getTwoFactor() { + return twoFactor; + } + + public MemberEntity setTwoFactor(Boolean twoFactor) { + this.twoFactor = twoFactor; + return this; + } + + public OffsetDateTime getLastActiveAt() { + return lastActiveAt; + } + + public MemberEntity setLastActiveAt(OffsetDateTime lastActiveAt) { + this.lastActiveAt = lastActiveAt; + return this; + } + + public List getTeams() { + return teams; + } + + public MemberEntity setTeams(List teams) { + this.teams = teams; + return this; + } + + public Boolean getOutsideCollaborator() { + return outsideCollaborator; + } + + public MemberEntity setOutsideCollaborator(Boolean outsideCollaborator) { + this.outsideCollaborator = outsideCollaborator; + return this; + } + + public Integer getRepoAccess() { + return repoAccess; + } + + public MemberEntity setRepoAccess(Integer repoAccess) { + this.repoAccess = repoAccess; + return this; + } +} diff --git a/backend/libs/persistence/src/main/java/dev/cleat/persistence/RepoEntity.java b/backend/libs/persistence/src/main/java/dev/cleat/persistence/entity/RepoEntity.java similarity index 84% rename from backend/libs/persistence/src/main/java/dev/cleat/persistence/RepoEntity.java rename to backend/libs/persistence/src/main/java/dev/cleat/persistence/entity/RepoEntity.java index 1cebae4..b45b9e2 100644 --- a/backend/libs/persistence/src/main/java/dev/cleat/persistence/RepoEntity.java +++ b/backend/libs/persistence/src/main/java/dev/cleat/persistence/entity/RepoEntity.java @@ -1,352 +1,389 @@ -package dev.cleat.persistence; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.Table; -import java.time.OffsetDateTime; -import java.util.UUID; -import org.hibernate.annotations.CreationTimestamp; - -@Entity -@Table(name = "repo") -public class RepoEntity { - - @Id - @GeneratedValue(strategy = GenerationType.UUID) - private UUID id; - - @Column(name = "name", nullable = false) - private String name; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "account_id", nullable = false) - private AccountEntity account; - - @Column(name = "visibility") - @Enumerated(EnumType.STRING) - private Visibility visibility; - - @Column(name = "language") - private String language; - - @Column(name = "stars") - private Integer stars; - - @Column(name = "default_branch") - private String defaultBranch; - - @Column(name = "branch_protected") - private Boolean branchProtected; - - @Column(name = "has_readme") - private Boolean hasReadme; - - @Column(name = "has_license") - private Boolean hasLicense; - - @Column(name = "has_contributing") - private Boolean hasContributing; - - @Column(name = "has_codeowners") - private Boolean hasCodeowners; - - @Column(name = "has_ci") - private Boolean hasCi; - - @Column(name = "size_mb") - private Double sizeMb; - - @Column(name = "last_pushed_at") - private OffsetDateTime lastPushedAt; - - @Column(name = "archived") - private Boolean archived; - - @Column(name = "open_vulns") - private Integer openVulns; - - @Column(name = "open_secrets") - private Integer openSecrets; - - @Column(name = "open_code_alerts") - private Integer openCodeAlerts; - - @Column(name = "stale_branches") - private Integer staleBranches; - - @Column(name = "open_prs") - private Integer openPRs; - - @Column(name = "hygiene_score") - private Integer hygieneScore; - - @CreationTimestamp - @Column(name = "created_at", nullable = false, updatable = false) - private OffsetDateTime createdAt; - - public RepoEntity( - UUID id, - String name, - AccountEntity account, - Visibility visibility, - String language, - Integer stars, - String defaultBranch, - Boolean branchProtected, - Boolean hasReadme, - Boolean hasLicense, - Boolean hasContributing, - Boolean hasCodeowners, - Boolean hasCi, - Double sizeMb, - OffsetDateTime lastPushedAt, - Boolean archived, - Integer openVulns, - Integer openSecrets, - Integer openCodeAlerts, - Integer staleBranches, - Integer openPRs, - Integer hygieneScore, - OffsetDateTime createdAt) { - this.id = id; - this.name = name; - this.account = account; - this.visibility = visibility; - this.language = language; - this.stars = stars; - this.defaultBranch = defaultBranch; - this.branchProtected = branchProtected; - this.hasReadme = hasReadme; - this.hasLicense = hasLicense; - this.hasContributing = hasContributing; - this.hasCodeowners = hasCodeowners; - this.hasCi = hasCi; - this.sizeMb = sizeMb; - this.lastPushedAt = lastPushedAt; - this.archived = archived; - this.openVulns = openVulns; - this.openSecrets = openSecrets; - this.openCodeAlerts = openCodeAlerts; - this.staleBranches = staleBranches; - this.openPRs = openPRs; - this.hygieneScore = hygieneScore; - this.createdAt = createdAt; - } - - public RepoEntity() {} - - public UUID getId() { - return id; - } - - public RepoEntity setId(UUID id) { - this.id = id; - return this; - } - - public String getName() { - return name; - } - - public RepoEntity setName(String name) { - this.name = name; - return this; - } - - public AccountEntity getAccount() { - return account; - } - - public RepoEntity setAccount(AccountEntity account) { - this.account = account; - return this; - } - - public Visibility getVisibility() { - return visibility; - } - - public RepoEntity setVisibility(Visibility visibility) { - this.visibility = visibility; - return this; - } - - public String getLanguage() { - return language; - } - - public RepoEntity setLanguage(String language) { - this.language = language; - return this; - } - - public Integer getStars() { - return stars; - } - - public RepoEntity setStars(Integer stars) { - this.stars = stars; - return this; - } - - public String getDefaultBranch() { - return defaultBranch; - } - - public RepoEntity setDefaultBranch(String defaultBranch) { - this.defaultBranch = defaultBranch; - return this; - } - - public Boolean getBranchProtected() { - return branchProtected; - } - - public RepoEntity setBranchProtected(Boolean branchProtected) { - this.branchProtected = branchProtected; - return this; - } - - public Boolean getHasReadme() { - return hasReadme; - } - - public RepoEntity setHasReadme(Boolean hasReadme) { - this.hasReadme = hasReadme; - return this; - } - - public Boolean getHasLicense() { - return hasLicense; - } - - public RepoEntity setHasLicense(Boolean hasLicense) { - this.hasLicense = hasLicense; - return this; - } - - public Boolean getHasContributing() { - return hasContributing; - } - - public RepoEntity setHasContributing(Boolean hasContributing) { - this.hasContributing = hasContributing; - return this; - } - - public Boolean getHasCodeowners() { - return hasCodeowners; - } - - public RepoEntity setHasCodeowners(Boolean hasCodeowners) { - this.hasCodeowners = hasCodeowners; - return this; - } - - public Boolean getHasCi() { - return hasCi; - } - - public RepoEntity setHasCi(Boolean hasCi) { - this.hasCi = hasCi; - return this; - } - - public Double getSizeMb() { - return sizeMb; - } - - public RepoEntity setSizeMb(Double sizeMb) { - this.sizeMb = sizeMb; - return this; - } - - public OffsetDateTime getLastPushedAt() { - return lastPushedAt; - } - - public RepoEntity setLastPushedAt(OffsetDateTime lastPushedAt) { - this.lastPushedAt = lastPushedAt; - return this; - } - - public Boolean getArchived() { - return archived; - } - - public RepoEntity setArchived(Boolean archived) { - this.archived = archived; - return this; - } - - public Integer getOpenVulns() { - return openVulns; - } - - public RepoEntity setOpenVulns(Integer openVulns) { - this.openVulns = openVulns; - return this; - } - - public Integer getOpenSecrets() { - return openSecrets; - } - - public RepoEntity setOpenSecrets(Integer openSecrets) { - this.openSecrets = openSecrets; - return this; - } - - public Integer getOpenCodeAlerts() { - return openCodeAlerts; - } - - public RepoEntity setOpenCodeAlerts(Integer openCodeAlerts) { - this.openCodeAlerts = openCodeAlerts; - return this; - } - - public Integer getStaleBranches() { - return staleBranches; - } - - public RepoEntity setStaleBranches(Integer staleBranches) { - this.staleBranches = staleBranches; - return this; - } - - public Integer getOpenPRs() { - return openPRs; - } - - public RepoEntity setOpenPRs(Integer openPRs) { - this.openPRs = openPRs; - return this; - } - - public Integer getHygieneScore() { - return hygieneScore; - } - - public RepoEntity setHygieneScore(Integer hygieneScore) { - this.hygieneScore = hygieneScore; - return this; - } - - public OffsetDateTime getCreatedAt() { - return createdAt; - } - - public RepoEntity setCreatedAt(OffsetDateTime createdAt) { - this.createdAt = createdAt; - return this; - } -} +package dev.cleat.persistence.entity; + +import dev.cleat.common.enums.Visibility; +import jakarta.persistence.CascadeType; +import jakarta.persistence.CollectionTable; +import jakarta.persistence.Column; +import jakarta.persistence.ElementCollection; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import java.time.OffsetDateTime; +import java.util.List; +import java.util.UUID; +import org.hibernate.annotations.CreationTimestamp; + +@Entity +@Table(name = "repo") +public class RepoEntity { + + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private UUID id; + + @Column(name = "name", nullable = false) + private String name; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "account_id", nullable = false) + private AccountEntity account; + + @Column(name = "visibility") + @Enumerated(EnumType.STRING) + private Visibility visibility; + + @Column(name = "language") + private String language; + + @Column(name = "stars") + private Integer stars; + + @Column(name = "default_branch") + private String defaultBranch; + + @Column(name = "branch_protected") + private Boolean branchProtected; + + @Column(name = "has_readme") + private Boolean hasReadme; + + @Column(name = "has_license") + private Boolean hasLicense; + + @Column(name = "has_contributing") + private Boolean hasContributing; + + @Column(name = "has_codeowners") + private Boolean hasCodeowners; + + @Column(name = "has_ci") + private Boolean hasCi; + + @Column(name = "size_mb") + private Double sizeMb; + + @Column(name = "last_pushed_at") + private OffsetDateTime lastPushedAt; + + @Column(name = "archived") + private Boolean archived; + + @Column(name = "open_vulns") + private Integer openVulns; + + @Column(name = "open_secrets") + private Integer openSecrets; + + @Column(name = "open_code_alerts") + private Integer openCodeAlerts; + + @Column(name = "stale_branches") + private Integer staleBranches; + + @Column(name = "open_prs") + private Integer openPRs; + + @Column(name = "hygiene_score") + private Integer hygieneScore; + + @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) + @JoinColumn(name = "repo_id") + private List scorecard; + + @ElementCollection + @CollectionTable(name = "repo_topics", joinColumns = @JoinColumn(name = "repo_id")) + @Column(name = "topics") + private List topics; + + @CreationTimestamp + @Column(name = "created_at", nullable = false, updatable = false) + private OffsetDateTime createdAt; + + public RepoEntity() {} + + public RepoEntity( + UUID id, + String name, + AccountEntity account, + Visibility visibility, + String language, + Integer stars, + String defaultBranch, + Boolean branchProtected, + Boolean hasReadme, + Boolean hasLicense, + Boolean hasContributing, + Boolean hasCodeowners, + Boolean hasCi, + Double sizeMb, + OffsetDateTime lastPushedAt, + Boolean archived, + Integer openVulns, + Integer openSecrets, + Integer openCodeAlerts, + Integer staleBranches, + Integer openPRs, + Integer hygieneScore, + List scorecard, + List topics, + OffsetDateTime createdAt) { + this.id = id; + this.name = name; + this.account = account; + this.visibility = visibility; + this.language = language; + this.stars = stars; + this.defaultBranch = defaultBranch; + this.branchProtected = branchProtected; + this.hasReadme = hasReadme; + this.hasLicense = hasLicense; + this.hasContributing = hasContributing; + this.hasCodeowners = hasCodeowners; + this.hasCi = hasCi; + this.sizeMb = sizeMb; + this.lastPushedAt = lastPushedAt; + this.archived = archived; + this.openVulns = openVulns; + this.openSecrets = openSecrets; + this.openCodeAlerts = openCodeAlerts; + this.staleBranches = staleBranches; + this.openPRs = openPRs; + this.hygieneScore = hygieneScore; + this.scorecard = scorecard; + this.topics = topics; + this.createdAt = createdAt; + } + + public UUID getId() { + return id; + } + + public RepoEntity setId(UUID id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public RepoEntity setName(String name) { + this.name = name; + return this; + } + + public AccountEntity getAccount() { + return account; + } + + public RepoEntity setAccount(AccountEntity account) { + this.account = account; + return this; + } + + public Visibility getVisibility() { + return visibility; + } + + public RepoEntity setVisibility(Visibility visibility) { + this.visibility = visibility; + return this; + } + + public String getLanguage() { + return language; + } + + public RepoEntity setLanguage(String language) { + this.language = language; + return this; + } + + public Integer getStars() { + return stars; + } + + public RepoEntity setStars(Integer stars) { + this.stars = stars; + return this; + } + + public String getDefaultBranch() { + return defaultBranch; + } + + public RepoEntity setDefaultBranch(String defaultBranch) { + this.defaultBranch = defaultBranch; + return this; + } + + public Boolean getBranchProtected() { + return branchProtected; + } + + public RepoEntity setBranchProtected(Boolean branchProtected) { + this.branchProtected = branchProtected; + return this; + } + + public Boolean getHasReadme() { + return hasReadme; + } + + public RepoEntity setHasReadme(Boolean hasReadme) { + this.hasReadme = hasReadme; + return this; + } + + public Boolean getHasLicense() { + return hasLicense; + } + + public RepoEntity setHasLicense(Boolean hasLicense) { + this.hasLicense = hasLicense; + return this; + } + + public Boolean getHasContributing() { + return hasContributing; + } + + public RepoEntity setHasContributing(Boolean hasContributing) { + this.hasContributing = hasContributing; + return this; + } + + public Boolean getHasCodeowners() { + return hasCodeowners; + } + + public RepoEntity setHasCodeowners(Boolean hasCodeowners) { + this.hasCodeowners = hasCodeowners; + return this; + } + + public Boolean getHasCi() { + return hasCi; + } + + public RepoEntity setHasCi(Boolean hasCi) { + this.hasCi = hasCi; + return this; + } + + public Double getSizeMb() { + return sizeMb; + } + + public RepoEntity setSizeMb(Double sizeMb) { + this.sizeMb = sizeMb; + return this; + } + + public OffsetDateTime getLastPushedAt() { + return lastPushedAt; + } + + public RepoEntity setLastPushedAt(OffsetDateTime lastPushedAt) { + this.lastPushedAt = lastPushedAt; + return this; + } + + public Boolean getArchived() { + return archived; + } + + public RepoEntity setArchived(Boolean archived) { + this.archived = archived; + return this; + } + + public Integer getOpenVulns() { + return openVulns; + } + + public RepoEntity setOpenVulns(Integer openVulns) { + this.openVulns = openVulns; + return this; + } + + public Integer getOpenSecrets() { + return openSecrets; + } + + public RepoEntity setOpenSecrets(Integer openSecrets) { + this.openSecrets = openSecrets; + return this; + } + + public Integer getOpenCodeAlerts() { + return openCodeAlerts; + } + + public RepoEntity setOpenCodeAlerts(Integer openCodeAlerts) { + this.openCodeAlerts = openCodeAlerts; + return this; + } + + public Integer getStaleBranches() { + return staleBranches; + } + + public RepoEntity setStaleBranches(Integer staleBranches) { + this.staleBranches = staleBranches; + return this; + } + + public Integer getOpenPRs() { + return openPRs; + } + + public RepoEntity setOpenPRs(Integer openPRs) { + this.openPRs = openPRs; + return this; + } + + public Integer getHygieneScore() { + return hygieneScore; + } + + public RepoEntity setHygieneScore(Integer hygieneScore) { + this.hygieneScore = hygieneScore; + return this; + } + + public List getScorecard() { + return scorecard; + } + + public RepoEntity setScorecard(List scorecard) { + this.scorecard = scorecard; + return this; + } + + public List getTopics() { + return topics; + } + + public RepoEntity setTopics(List topics) { + this.topics = topics; + return this; + } + + public OffsetDateTime getCreatedAt() { + return createdAt; + } + + public RepoEntity setCreatedAt(OffsetDateTime createdAt) { + this.createdAt = createdAt; + return this; + } +} diff --git a/backend/libs/persistence/src/main/java/dev/cleat/persistence/entity/ScorecardCheckEntity.java b/backend/libs/persistence/src/main/java/dev/cleat/persistence/entity/ScorecardCheckEntity.java new file mode 100644 index 0000000..0c82163 --- /dev/null +++ b/backend/libs/persistence/src/main/java/dev/cleat/persistence/entity/ScorecardCheckEntity.java @@ -0,0 +1,65 @@ +package dev.cleat.persistence.entity; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.util.UUID; + +@Entity +@Table(name = "scorecard-check") +public class ScorecardCheckEntity { + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private UUID id; + + private String name; + private Integer score; + private String reason; + + public ScorecardCheckEntity() {} + + public ScorecardCheckEntity(UUID id, String name, Integer score, String reason) { + this.id = id; + this.name = name; + this.score = score; + this.reason = reason; + } + + public UUID getId() { + return id; + } + + public ScorecardCheckEntity setId(UUID id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public ScorecardCheckEntity setName(String name) { + this.name = name; + return this; + } + + public Integer getScore() { + return score; + } + + public ScorecardCheckEntity setScore(Integer score) { + this.score = score; + return this; + } + + public String getReason() { + return reason; + } + + public ScorecardCheckEntity setReason(String reason) { + this.reason = reason; + return this; + } +} diff --git a/backend/libs/persistence/src/main/java/dev/cleat/persistence/entity/SecretFindingEntity.java b/backend/libs/persistence/src/main/java/dev/cleat/persistence/entity/SecretFindingEntity.java new file mode 100644 index 0000000..2c855c3 --- /dev/null +++ b/backend/libs/persistence/src/main/java/dev/cleat/persistence/entity/SecretFindingEntity.java @@ -0,0 +1,214 @@ +package dev.cleat.persistence.entity; + +import dev.cleat.common.enums.Severity; +import dev.cleat.common.enums.Validity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import java.time.OffsetDateTime; +import java.util.UUID; + +@Entity +@Table(name = "secret-finding") +public class SecretFindingEntity { + + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private UUID id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "account_id") + private AccountEntity account; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "repo") + private RepoEntity repo; + + @Column(name = "provider") + private String provider; + + @Column(name = "secret_type") + private String secretType; + + @Column(name = "file") + private String file; + + @Column(name = "line") + private Integer line; + + @Column(name = "commit") + private String commit; + + @Column(name = "author") + private String author; + + @Column(name = "detected_at") + private OffsetDateTime detectedAt; + + @Enumerated(EnumType.STRING) + @Column(name = "validity") + private Validity validity; + + @Enumerated(EnumType.STRING) + @Column(name = "severity") + private Severity severity; + + @Column(name = "push_protection_blocked") + private Boolean pushProtectionBlocked; + + public SecretFindingEntity() {} + + public SecretFindingEntity( + UUID id, + AccountEntity account, + RepoEntity repo, + String provider, + String secretType, + String file, + Integer line, + String commit, + String author, + OffsetDateTime detectedAt, + Validity validity, + Severity severity, + Boolean pushProtectionBlocked) { + this.id = id; + this.account = account; + this.repo = repo; + this.provider = provider; + this.secretType = secretType; + this.file = file; + this.line = line; + this.commit = commit; + this.author = author; + this.detectedAt = detectedAt; + this.validity = validity; + this.severity = severity; + this.pushProtectionBlocked = pushProtectionBlocked; + } + + public UUID getId() { + return id; + } + + public SecretFindingEntity setId(UUID id) { + this.id = id; + return this; + } + + public AccountEntity getAccount() { + return account; + } + + public SecretFindingEntity setAccount(AccountEntity account) { + this.account = account; + return this; + } + + public RepoEntity getRepo() { + return repo; + } + + public SecretFindingEntity setRepo(RepoEntity repo) { + this.repo = repo; + return this; + } + + public String getProvider() { + return provider; + } + + public SecretFindingEntity setProvider(String provider) { + this.provider = provider; + return this; + } + + public String getSecretType() { + return secretType; + } + + public SecretFindingEntity setSecretType(String secretType) { + this.secretType = secretType; + return this; + } + + public String getFile() { + return file; + } + + public SecretFindingEntity setFile(String file) { + this.file = file; + return this; + } + + public Integer getLine() { + return line; + } + + public SecretFindingEntity setLine(Integer line) { + this.line = line; + return this; + } + + public String getCommit() { + return commit; + } + + public SecretFindingEntity setCommit(String commit) { + this.commit = commit; + return this; + } + + public String getAuthor() { + return author; + } + + public SecretFindingEntity setAuthor(String author) { + this.author = author; + return this; + } + + public OffsetDateTime getDetectedAt() { + return detectedAt; + } + + public SecretFindingEntity setDetectedAt(OffsetDateTime detectedAt) { + this.detectedAt = detectedAt; + return this; + } + + public Validity getValidity() { + return validity; + } + + public SecretFindingEntity setValidity(Validity validity) { + this.validity = validity; + return this; + } + + public Severity getSeverity() { + return severity; + } + + public SecretFindingEntity setSeverity(Severity severity) { + this.severity = severity; + return this; + } + + public Boolean getPushProtectionBlocked() { + return pushProtectionBlocked; + } + + public SecretFindingEntity setPushProtectionBlocked(Boolean pushProtectionBlocked) { + this.pushProtectionBlocked = pushProtectionBlocked; + return this; + } +} diff --git a/backend/libs/persistence/src/main/java/dev/cleat/persistence/entity/UsageEntity.java b/backend/libs/persistence/src/main/java/dev/cleat/persistence/entity/UsageEntity.java new file mode 100644 index 0000000..481632a --- /dev/null +++ b/backend/libs/persistence/src/main/java/dev/cleat/persistence/entity/UsageEntity.java @@ -0,0 +1,145 @@ +package dev.cleat.persistence.entity; + +import dev.cleat.common.dto.BreakdownItem; +import jakarta.persistence.CascadeType; +import jakarta.persistence.CollectionTable; +import jakarta.persistence.Column; +import jakarta.persistence.ElementCollection; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import java.math.BigDecimal; +import java.util.List; +import java.util.UUID; + +@Entity +@Table(name = "usage") +public class UsageEntity { + + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private UUID id; + + private Integer actionsMinutes; + private Integer minutesIncluded; + private Double storageGb; + private Double storageIncludedGb; + private BigDecimal monthlyCost; + private BigDecimal reclaimable; + + @ElementCollection + @CollectionTable(name = "usage_breakdown", joinColumns = @JoinColumn(name = "usage_id")) + @Column(name = "breakdown") + private List breakdown; + + @OneToMany(mappedBy = "usage", cascade = CascadeType.ALL) + private List series; + + public UsageEntity() {} + + public UsageEntity( + UUID id, + Integer actionsMinutes, + Integer minutesIncluded, + Double storageGb, + Double storageIncludedGb, + BigDecimal monthlyCost, + BigDecimal reclaimable, + List breakdown, + List series) { + this.id = id; + this.actionsMinutes = actionsMinutes; + this.minutesIncluded = minutesIncluded; + this.storageGb = storageGb; + this.storageIncludedGb = storageIncludedGb; + this.monthlyCost = monthlyCost; + this.reclaimable = reclaimable; + this.breakdown = breakdown; + this.series = series; + } + + public UUID getId() { + return id; + } + + public UsageEntity setId(UUID id) { + this.id = id; + return this; + } + + public Integer getActionsMinutes() { + return actionsMinutes; + } + + public UsageEntity setActionsMinutes(Integer actionsMinutes) { + this.actionsMinutes = actionsMinutes; + return this; + } + + public Integer getMinutesIncluded() { + return minutesIncluded; + } + + public UsageEntity setMinutesIncluded(Integer minutesIncluded) { + this.minutesIncluded = minutesIncluded; + return this; + } + + public Double getStorageGb() { + return storageGb; + } + + public UsageEntity setStorageGb(Double storageGb) { + this.storageGb = storageGb; + return this; + } + + public Double getStorageIncludedGb() { + return storageIncludedGb; + } + + public UsageEntity setStorageIncludedGb(Double storageIncludedGb) { + this.storageIncludedGb = storageIncludedGb; + return this; + } + + public BigDecimal getMonthlyCost() { + return monthlyCost; + } + + public UsageEntity setMonthlyCost(BigDecimal monthlyCost) { + this.monthlyCost = monthlyCost; + return this; + } + + public BigDecimal getReclaimable() { + return reclaimable; + } + + public UsageEntity setReclaimable(BigDecimal reclaimable) { + this.reclaimable = reclaimable; + return this; + } + + public List getBreakdown() { + return breakdown; + } + + public UsageEntity setBreakdown(List breakdown) { + this.breakdown = breakdown; + return this; + } + + public List getSeries() { + return series; + } + + public UsageEntity setSeries(List series) { + this.series = series; + return this; + } +} diff --git a/backend/libs/persistence/src/main/java/dev/cleat/persistence/entity/UsagePointEntity.java b/backend/libs/persistence/src/main/java/dev/cleat/persistence/entity/UsagePointEntity.java new file mode 100644 index 0000000..f5ce6df --- /dev/null +++ b/backend/libs/persistence/src/main/java/dev/cleat/persistence/entity/UsagePointEntity.java @@ -0,0 +1,96 @@ +package dev.cleat.persistence.entity; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import java.math.BigDecimal; +import java.util.UUID; + +@Entity +@Table(name = "usage-point") +public class UsagePointEntity { + + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private UUID id; + + private String label; + private Integer minutes; + private Double storageGb; + private BigDecimal cost; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "usage_id") + private UsageEntity usage; + + public UsagePointEntity() {} + + public UsagePointEntity( + UUID id, String label, Integer minutes, Double storageGb, BigDecimal cost, UsageEntity usage) { + this.id = id; + this.label = label; + this.minutes = minutes; + this.storageGb = storageGb; + this.cost = cost; + this.usage = usage; + } + + public UUID getId() { + return id; + } + + public UsagePointEntity setId(UUID id) { + this.id = id; + return this; + } + + public String getLabel() { + return label; + } + + public UsagePointEntity setLabel(String label) { + this.label = label; + return this; + } + + public Integer getMinutes() { + return minutes; + } + + public UsagePointEntity setMinutes(Integer minutes) { + this.minutes = minutes; + return this; + } + + public Double getStorageGb() { + return storageGb; + } + + public UsagePointEntity setStorageGb(Double storageGb) { + this.storageGb = storageGb; + return this; + } + + public BigDecimal getCost() { + return cost; + } + + public UsagePointEntity setCost(BigDecimal cost) { + this.cost = cost; + return this; + } + + public UsageEntity getUsage() { + return usage; + } + + public UsagePointEntity setUsage(UsageEntity usage) { + this.usage = usage; + return this; + } +} diff --git a/backend/libs/persistence/src/main/java/dev/cleat/persistence/entity/VulnerabilityEntity.java b/backend/libs/persistence/src/main/java/dev/cleat/persistence/entity/VulnerabilityEntity.java new file mode 100644 index 0000000..5b9d340 --- /dev/null +++ b/backend/libs/persistence/src/main/java/dev/cleat/persistence/entity/VulnerabilityEntity.java @@ -0,0 +1,274 @@ +package dev.cleat.persistence.entity; + +import dev.cleat.common.enums.Reachable; +import dev.cleat.common.enums.Severity; +import jakarta.persistence.CollectionTable; +import jakarta.persistence.Column; +import jakarta.persistence.ElementCollection; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import java.time.OffsetDateTime; +import java.util.List; +import java.util.UUID; + +@Entity +@Table(name = "vulnerability") +public class VulnerabilityEntity { + + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private UUID id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "account_id") + private AccountEntity account; + + @Column(name = "package_name") + private String packageName; + + @Column(name = "ecosystem") + private String ecosystem; + + @Column(name = "current_version") + private String currentVersion; + + @Column(name = "fixed_version") + private String fixedVersion; + + @Column(name = "cvss") + private Double cvss; + + @Enumerated(EnumType.STRING) + @Column(name = "severity") + private Severity severity; + + @Column(name = "epss") + private Double epss; + + @Column(name = "kev") + private Boolean kev; + + @Enumerated(EnumType.STRING) + @Column(name = "reachable") + private Reachable reachable; + + @Column(name = "advisory_id") + private UUID advisoryId; + + @Column(name = "cwe") + private String cwe; + + @Column(name = "title") + private String title; + + @ElementCollection + @CollectionTable(name = "vulnerability_affected_repos", joinColumns = @JoinColumn(name = "vulnerability_id")) + @Column(name = "affected_repos") + private List affectedRepos; + + @Column(name = "has_fix_pr") + private Boolean hasFixPr; + + @Column(name = "published_at") + private OffsetDateTime publishedAt; + + public VulnerabilityEntity() {} + + public VulnerabilityEntity( + UUID id, + AccountEntity account, + String packageName, + String ecosystem, + String currentVersion, + String fixedVersion, + Double cvss, + Severity severity, + Double epss, + Boolean kev, + Reachable reachable, + UUID advisoryId, + String cwe, + String title, + List affectedRepos, + Boolean hasFixPr, + OffsetDateTime publishedAt) { + this.id = id; + this.account = account; + this.packageName = packageName; + this.ecosystem = ecosystem; + this.currentVersion = currentVersion; + this.fixedVersion = fixedVersion; + this.cvss = cvss; + this.severity = severity; + this.epss = epss; + this.kev = kev; + this.reachable = reachable; + this.advisoryId = advisoryId; + this.cwe = cwe; + this.title = title; + this.affectedRepos = affectedRepos; + this.hasFixPr = hasFixPr; + this.publishedAt = publishedAt; + } + + public UUID getId() { + return id; + } + + public VulnerabilityEntity setId(UUID id) { + this.id = id; + return this; + } + + public AccountEntity getAccount() { + return account; + } + + public VulnerabilityEntity setAccount(AccountEntity account) { + this.account = account; + return this; + } + + public String getPackageName() { + return packageName; + } + + public VulnerabilityEntity setPackageName(String packageName) { + this.packageName = packageName; + return this; + } + + public String getEcosystem() { + return ecosystem; + } + + public VulnerabilityEntity setEcosystem(String ecosystem) { + this.ecosystem = ecosystem; + return this; + } + + public String getCurrentVersion() { + return currentVersion; + } + + public VulnerabilityEntity setCurrentVersion(String currentVersion) { + this.currentVersion = currentVersion; + return this; + } + + public String getFixedVersion() { + return fixedVersion; + } + + public VulnerabilityEntity setFixedVersion(String fixedVersion) { + this.fixedVersion = fixedVersion; + return this; + } + + public Double getCvss() { + return cvss; + } + + public VulnerabilityEntity setCvss(Double cvss) { + this.cvss = cvss; + return this; + } + + public Severity getSeverity() { + return severity; + } + + public VulnerabilityEntity setSeverity(Severity severity) { + this.severity = severity; + return this; + } + + public Double getEpss() { + return epss; + } + + public VulnerabilityEntity setEpss(Double epss) { + this.epss = epss; + return this; + } + + public Boolean getKev() { + return kev; + } + + public VulnerabilityEntity setKev(Boolean kev) { + this.kev = kev; + return this; + } + + public Reachable getReachable() { + return reachable; + } + + public VulnerabilityEntity setReachable(Reachable reachable) { + this.reachable = reachable; + return this; + } + + public UUID getAdvisoryId() { + return advisoryId; + } + + public VulnerabilityEntity setAdvisoryId(UUID advisoryId) { + this.advisoryId = advisoryId; + return this; + } + + public String getCwe() { + return cwe; + } + + public VulnerabilityEntity setCwe(String cwe) { + this.cwe = cwe; + return this; + } + + public String getTitle() { + return title; + } + + public VulnerabilityEntity setTitle(String title) { + this.title = title; + return this; + } + + public List getAffectedRepos() { + return affectedRepos; + } + + public VulnerabilityEntity setAffectedRepos(List affectedRepos) { + this.affectedRepos = affectedRepos; + return this; + } + + public Boolean getHasFixPr() { + return hasFixPr; + } + + public VulnerabilityEntity setHasFixPr(Boolean hasFixPr) { + this.hasFixPr = hasFixPr; + return this; + } + + public OffsetDateTime getPublishedAt() { + return publishedAt; + } + + public VulnerabilityEntity setPublishedAt(OffsetDateTime publishedAt) { + this.publishedAt = publishedAt; + return this; + } +} diff --git a/backend/libs/persistence/src/main/java/dev/cleat/persistence/AccountRepository.java b/backend/libs/persistence/src/main/java/dev/cleat/persistence/repository/AccountRepository.java similarity index 62% rename from backend/libs/persistence/src/main/java/dev/cleat/persistence/AccountRepository.java rename to backend/libs/persistence/src/main/java/dev/cleat/persistence/repository/AccountRepository.java index 822d78a..c7b4ba7 100644 --- a/backend/libs/persistence/src/main/java/dev/cleat/persistence/AccountRepository.java +++ b/backend/libs/persistence/src/main/java/dev/cleat/persistence/repository/AccountRepository.java @@ -1,6 +1,7 @@ -package dev.cleat.persistence; - -import java.util.UUID; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface AccountRepository extends JpaRepository {} +package dev.cleat.persistence.repository; + +import dev.cleat.persistence.entity.AccountEntity; +import java.util.UUID; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface AccountRepository extends JpaRepository {} diff --git a/backend/libs/persistence/src/main/java/dev/cleat/persistence/repository/ActivityEventRepository.java b/backend/libs/persistence/src/main/java/dev/cleat/persistence/repository/ActivityEventRepository.java new file mode 100644 index 0000000..19b78e4 --- /dev/null +++ b/backend/libs/persistence/src/main/java/dev/cleat/persistence/repository/ActivityEventRepository.java @@ -0,0 +1,10 @@ +package dev.cleat.persistence.repository; + +import dev.cleat.persistence.entity.ActivityEventEntity; +import java.util.List; +import java.util.UUID; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ActivityEventRepository extends JpaRepository { + List findAllByAccountId(UUID accountId); +} diff --git a/backend/libs/persistence/src/main/java/dev/cleat/persistence/repository/MemberRepository.java b/backend/libs/persistence/src/main/java/dev/cleat/persistence/repository/MemberRepository.java new file mode 100644 index 0000000..1684081 --- /dev/null +++ b/backend/libs/persistence/src/main/java/dev/cleat/persistence/repository/MemberRepository.java @@ -0,0 +1,10 @@ +package dev.cleat.persistence.repository; + +import dev.cleat.persistence.entity.MemberEntity; +import java.util.List; +import java.util.UUID; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface MemberRepository extends JpaRepository { + List findAllByAccountId(UUID accountId); +} diff --git a/backend/libs/persistence/src/main/java/dev/cleat/persistence/repository/RepoRepository.java b/backend/libs/persistence/src/main/java/dev/cleat/persistence/repository/RepoRepository.java new file mode 100644 index 0000000..644331d --- /dev/null +++ b/backend/libs/persistence/src/main/java/dev/cleat/persistence/repository/RepoRepository.java @@ -0,0 +1,10 @@ +package dev.cleat.persistence.repository; + +import dev.cleat.persistence.entity.RepoEntity; +import java.util.List; +import java.util.UUID; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface RepoRepository extends JpaRepository { + List findAllByAccountId(UUID accountId); +} diff --git a/backend/libs/persistence/src/main/java/dev/cleat/persistence/repository/ScorecardCheckRepository.java b/backend/libs/persistence/src/main/java/dev/cleat/persistence/repository/ScorecardCheckRepository.java new file mode 100644 index 0000000..64b82c2 --- /dev/null +++ b/backend/libs/persistence/src/main/java/dev/cleat/persistence/repository/ScorecardCheckRepository.java @@ -0,0 +1,7 @@ +package dev.cleat.persistence.repository; + +import dev.cleat.persistence.entity.ScorecardCheckEntity; +import java.util.UUID; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ScorecardCheckRepository extends JpaRepository {} diff --git a/backend/libs/persistence/src/main/java/dev/cleat/persistence/repository/SecretFindingRepository.java b/backend/libs/persistence/src/main/java/dev/cleat/persistence/repository/SecretFindingRepository.java new file mode 100644 index 0000000..9e6a6d1 --- /dev/null +++ b/backend/libs/persistence/src/main/java/dev/cleat/persistence/repository/SecretFindingRepository.java @@ -0,0 +1,10 @@ +package dev.cleat.persistence.repository; + +import dev.cleat.persistence.entity.SecretFindingEntity; +import java.util.List; +import java.util.UUID; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface SecretFindingRepository extends JpaRepository { + List findAllByAccountId(UUID accountId); +} diff --git a/backend/libs/persistence/src/main/java/dev/cleat/persistence/repository/UsagePointRepository.java b/backend/libs/persistence/src/main/java/dev/cleat/persistence/repository/UsagePointRepository.java new file mode 100644 index 0000000..772fc91 --- /dev/null +++ b/backend/libs/persistence/src/main/java/dev/cleat/persistence/repository/UsagePointRepository.java @@ -0,0 +1,7 @@ +package dev.cleat.persistence.repository; + +import dev.cleat.persistence.entity.UsagePointEntity; +import java.util.UUID; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UsagePointRepository extends JpaRepository {} diff --git a/backend/libs/persistence/src/main/java/dev/cleat/persistence/repository/UsageRepository.java b/backend/libs/persistence/src/main/java/dev/cleat/persistence/repository/UsageRepository.java new file mode 100644 index 0000000..a95b309 --- /dev/null +++ b/backend/libs/persistence/src/main/java/dev/cleat/persistence/repository/UsageRepository.java @@ -0,0 +1,10 @@ +package dev.cleat.persistence.repository; + +import dev.cleat.persistence.entity.UsageEntity; +import java.util.Optional; +import java.util.UUID; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UsageRepository extends JpaRepository { + Optional findByAccountId(UUID accountId); +} diff --git a/backend/libs/persistence/src/main/java/dev/cleat/persistence/repository/VulnerabilityRepository.java b/backend/libs/persistence/src/main/java/dev/cleat/persistence/repository/VulnerabilityRepository.java new file mode 100644 index 0000000..6195e35 --- /dev/null +++ b/backend/libs/persistence/src/main/java/dev/cleat/persistence/repository/VulnerabilityRepository.java @@ -0,0 +1,10 @@ +package dev.cleat.persistence.repository; + +import dev.cleat.persistence.entity.VulnerabilityEntity; +import java.util.List; +import java.util.UUID; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface VulnerabilityRepository extends JpaRepository { + List findAllByAccountId(UUID accountId); +} diff --git a/backend/libs/persistence/src/main/resources/db/migration/V2__update_repo_and_add_tables.sql b/backend/libs/persistence/src/main/resources/db/migration/V2__update_repo_and_add_tables.sql new file mode 100644 index 0000000..641b700 --- /dev/null +++ b/backend/libs/persistence/src/main/resources/db/migration/V2__update_repo_and_add_tables.sql @@ -0,0 +1,151 @@ +CREATE TABLE scorecard_check( + id UUID PRIMARY KEY, + name VARCHAR(255), + score INTEGER, + reason TEXT, + repo_id UUID, + + CONSTRAINT fk_scorecard_repo + FOREIGN KEY (repo_id) + REFERENCES repo(id) + ON DELETE CASCADE); +); + +CREATE TABLE repo_topics( + repo_id UUID NOT NULL, + topics VARCHAR(255), + + CONSTRAINT fk_repo_topics + FOREIGN KEY (repo_id) + REFERENCES repo(id) + ON DELETE CASCADE); +); + +CREATE TABLE activity_event( + id UUID PRIMARY KEY, + account_id UUID, + type VARCHAR(255), + severity VARCHAR(50), + actor VARCHAR(255), + target VARCHAR(255), + repo UUID, + message TEXT, + created_at TIMESTAMP WITH TIME ZONE, + category VARCHAR(50), + + CONSTRAINT fk_activity_account + FOREIGN KEY (account_id) + REFERENCES account(id), + + CONSTRAINT fk_activity_repo + FOREIGN KEY (repo_id) + REFERENCES repo(id) +); + +CREATE TABLE member ( + id UUID PRIMARY KEY, + login VARCHAR(255), + name VARCHAR(255), + role VARCHAR(50), + two_factor BOOLEAN, + last_active_at TIMESTAMP WITH TIME ZONE, + outside_collaborator BOOLEAN, + repo_access INTEGER, + +); + +CREATE TABLE member_teams( + member_id UUID PRIMARY KEY, + teams VARCHAR(255), + + CONSTRAINT fk_member_teams + FOREIGN KEY (member_id) + REFERENCES member(id) +); + +CREATE TABLE secret_finding( + id UUID PRIMARY KEY, + account_id UUID, + repo UUID, + provider VARCHAR(255), + secret_type VARCHAR(255), + file VARCHAR(255), + line INTEGER, + commit VARCHAR(255), + author VARCHAR(255), + detected_at TIMESTAMP WITH TIME ZONE, + validity VARCHAR(50), + severity VARCHAR(50), + push_protection_blocked BOOLEAN, + + CONSTRAINT fk_secret_account + FOREIGN KEY(account_id) + REFERENCES account(id), + + CONSTRAINT fk_secret_repo + FOREIGN KEY (repo) + REFERENCES repo(id); +); + +CREATE TABLE usage( + id UUID PRIMARY KEY, + actions_minutes INTEGER, + minutes_included INTEGER, + storage_gb DOUBLE PRECISION, + monthly_cost DECIMAL(19,4) DEFAULT 0.0, + reclaimable DECIMAL(19,4) DEFAULT 0.0 +); + +CREATE TABLE breakdown( + usage_id UUID NOT NULL, + breakdown VARCHAR(255), + + CONSTRAINT fk_usage_breakdown + FOREIGN KEY(usage_id) + REFERENCES usage(id) +); + +CREATE TABLE usage_point( + id UUID PRIMARY KEY, + label VARCHAR(255), + minutes INTEGER, + storage_gb DOUBLE PRECISION, + cost DECIMAL(19, 4) DEFAULT 0.0 + usage_id UUID, + + CONSTRAINT fk_usage_point_usage + FOREIGN KEY(usage_id) + REFERENCES usage(id) + +); + +CREATE TABLE vulnerability( + id UUID PRIMARY KEY, + account_id UUID, + packageName VARCHAR(255), + ecosystem VARCHAR(255), + current_version VARCHAR(255), + fixed_version VARCHAR(255), + cvss DOUBLE PRECISION, + severity VARCHAR(50), + epss DOUBLE PRECISION, + kev BOOLEAN, + reachable VARCHAR(50), + advisory_id UUID, + cwe VARCHAR(255), + title VARCHAR(255), + has_fix_pr BOOLEAN, + published_at TIMESTAMP WITH TIME ZONE + +); + +CREATE TABLE vulnerability_affected_repos( + vulnerability_id UUID NOT NULL, + affected_repos VARCHAR(255), + + CONSTRAINT fk_vulnerability_affected_repos + FOREIGN KEY(vulnerability_id) + REFERENCES vulnerability(id) + +); + diff --git a/backend/libs/persistence/src/test/java/dev/cleat/persistence/AccountRepositoryTest.java b/backend/libs/persistence/src/test/java/dev/cleat/persistence/AccountRepositoryTest.java index 2cf33ca..d24a0f6 100644 --- a/backend/libs/persistence/src/test/java/dev/cleat/persistence/AccountRepositoryTest.java +++ b/backend/libs/persistence/src/test/java/dev/cleat/persistence/AccountRepositoryTest.java @@ -1,56 +1,60 @@ -package dev.cleat.persistence; - -import java.util.List; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.DynamicPropertyRegistry; -import org.springframework.test.context.DynamicPropertySource; -import org.testcontainers.containers.PostgreSQLContainer; -import org.testcontainers.junit.jupiter.Container; -import org.testcontainers.junit.jupiter.Testcontainers; - -@DataJpaTest -@Testcontainers -@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) -@ContextConfiguration(classes = TestPersistenceConfig.class) -public class AccountRepositoryTest { - - @Container - static PostgreSQLContainer postgres = new PostgreSQLContainer<>("postgres:16"); - - @Autowired - private TestEntityManager testEntityManager; - - @DynamicPropertySource - static void props(DynamicPropertyRegistry registry) { - registry.add("spring.datasource.url", postgres::getJdbcUrl); - registry.add("spring.datasource.username", postgres::getUsername); - registry.add("spring.datasource.password", postgres::getPassword); - } - - @Autowired - AccountRepository accountRepository; - - @Test - void shouldSaveAccountWithRepos() { - AccountEntity account = - new AccountEntity().setLogin("test-user").setName("Test User").setType(AccountType.USER); - - RepoEntity repo = new RepoEntity().setName("test-repo").setAccount(account); - - account.setRepos(List.of(repo)); - repo.setAccount(account); - accountRepository.saveAndFlush(account); - testEntityManager.clear(); - AccountEntity found = accountRepository.findById(account.getId()).orElseThrow(); - - Assertions.assertNotNull(found.getId()); - Assertions.assertEquals(1, found.getRepos().size()); - Assertions.assertEquals("test-repo", found.getRepos().getFirst().getName()); - } -} +package dev.cleat.persistence; + +import dev.cleat.common.enums.AccountType; +import dev.cleat.persistence.entity.AccountEntity; +import dev.cleat.persistence.entity.RepoEntity; +import dev.cleat.persistence.repository.AccountRepository; +import java.util.List; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +@DataJpaTest +@Testcontainers +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@ContextConfiguration(classes = TestPersistenceConfig.class) +public class AccountRepositoryTest { + + @Container + static PostgreSQLContainer postgres = new PostgreSQLContainer<>("postgres:16"); + + @Autowired + private TestEntityManager testEntityManager; + + @DynamicPropertySource + static void props(DynamicPropertyRegistry registry) { + registry.add("spring.datasource.url", postgres::getJdbcUrl); + registry.add("spring.datasource.username", postgres::getUsername); + registry.add("spring.datasource.password", postgres::getPassword); + } + + @Autowired + AccountRepository accountRepository; + + @Test + void shouldSaveAccountWithRepos() { + AccountEntity account = + new AccountEntity().setLogin("test-user").setName("Test User").setType(AccountType.USER); + + RepoEntity repo = new RepoEntity().setName("test-repo").setAccount(account); + + account.setRepos(List.of(repo)); + repo.setAccount(account); + accountRepository.saveAndFlush(account); + testEntityManager.clear(); + AccountEntity found = accountRepository.findById(account.getId()).orElseThrow(); + + Assertions.assertNotNull(found.getId()); + Assertions.assertEquals(1, found.getRepos().size()); + Assertions.assertEquals("test-repo", found.getRepos().getFirst().getName()); + } +} From 7c1deb625e5307cf17ae153dd45d96c3f0293c21 Mon Sep 17 00:00:00 2001 From: arzunusretova12-debug Date: Wed, 24 Jun 2026 01:54:22 +0400 Subject: [PATCH 2/5] feat: implement DTOs for dashboard entities and update documentation --- backend/gradlew | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 backend/gradlew diff --git a/backend/gradlew b/backend/gradlew old mode 100644 new mode 100755 From de35904c949820f84a9868a230c13e677ff3b851 Mon Sep 17 00:00:00 2001 From: arzunusretova12-debug Date: Wed, 24 Jun 2026 02:32:37 +0400 Subject: [PATCH 3/5] fix: fix circular dependency inn multi-module project --- backend/gradlew | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 backend/gradlew diff --git a/backend/gradlew b/backend/gradlew old mode 100755 new mode 100644 From c15bf83774a10f1dc534474e67c565108ddd82c3 Mon Sep 17 00:00:00 2001 From: arzunusretova12-debug Date: Wed, 24 Jun 2026 02:24:01 +0400 Subject: [PATCH 4/5] fix: fix circular dependency inn multi-module project --- backend/libs/common/build.gradle.kts | 1 - .../domain/src/main/java/dev/cleat/domain/DashboardService.java | 2 +- .../main/java/dev/cleat/persistence}/mapper/CleatMapper.java | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) rename backend/libs/{common/src/main/java/dev/cleat/common => persistence/src/main/java/dev/cleat/persistence}/mapper/CleatMapper.java (97%) diff --git a/backend/libs/common/build.gradle.kts b/backend/libs/common/build.gradle.kts index c554ab6..22d6cc2 100644 --- a/backend/libs/common/build.gradle.kts +++ b/backend/libs/common/build.gradle.kts @@ -5,6 +5,5 @@ plugins { dependencies { api("org.springframework.boot:spring-boot-starter") api("org.springframework.boot:spring-boot-starter-validation") - implementation(project(":libs:persistence")) testImplementation("org.springframework.boot:spring-boot-starter-test") } diff --git a/backend/libs/domain/src/main/java/dev/cleat/domain/DashboardService.java b/backend/libs/domain/src/main/java/dev/cleat/domain/DashboardService.java index 032997f..d95d1b3 100644 --- a/backend/libs/domain/src/main/java/dev/cleat/domain/DashboardService.java +++ b/backend/libs/domain/src/main/java/dev/cleat/domain/DashboardService.java @@ -2,9 +2,9 @@ import dev.cleat.common.dto.response.DatasetDto; import dev.cleat.common.exception.NotFoundException; -import dev.cleat.common.mapper.CleatMapper; import dev.cleat.persistence.entity.AccountEntity; import dev.cleat.persistence.entity.UsageEntity; +import dev.cleat.persistence.mapper.CleatMapper; import dev.cleat.persistence.repository.AccountRepository; import dev.cleat.persistence.repository.ActivityEventRepository; import dev.cleat.persistence.repository.MemberRepository; diff --git a/backend/libs/common/src/main/java/dev/cleat/common/mapper/CleatMapper.java b/backend/libs/persistence/src/main/java/dev/cleat/persistence/mapper/CleatMapper.java similarity index 97% rename from backend/libs/common/src/main/java/dev/cleat/common/mapper/CleatMapper.java rename to backend/libs/persistence/src/main/java/dev/cleat/persistence/mapper/CleatMapper.java index b4110b6..cb55620 100644 --- a/backend/libs/common/src/main/java/dev/cleat/common/mapper/CleatMapper.java +++ b/backend/libs/persistence/src/main/java/dev/cleat/persistence/mapper/CleatMapper.java @@ -1,4 +1,4 @@ -package dev.cleat.common.mapper; +package dev.cleat.persistence.mapper; import dev.cleat.common.dto.UsagePointDto; import dev.cleat.common.dto.request.AccountRequestDto; From bb1849eff11ba321815d618d81f3f81075fbf5de Mon Sep 17 00:00:00 2001 From: arzunusretova12-debug Date: Wed, 24 Jun 2026 02:43:33 +0400 Subject: [PATCH 5/5] feat: move DashboardService to persistence layer to fix circular dependency --- .../api/controller/DashboardController.java | 2 +- .../cleat/api/AbstractIntegrationTest.java | 42 ++-- .../java/dev/cleat/api/CleatApiTests.java | 22 +- .../cleat/worker/CleatWorkerApplication.java | 28 +-- .../cleat/worker/AbstractIntegrationTest.java | 42 ++-- .../worker/CleatWorkerApplicationTests.java | 22 +- backend/libs/domain/build.gradle.kts | 2 - .../githubclient/config/GitHubConfig.java | 56 ++--- .../githubclient/dto/GitHubTokenResponse.java | 58 ++--- .../githubclient/service/GitHubClient.java | 92 ++++---- .../service/RateLimiterService.java | 76 +++---- .../githubclient/service/TokenManager.java | 208 +++++++++--------- .../githubclient/config/TestGitHubConfig.java | 32 +-- .../service/GitHubClientTest.java | 128 +++++------ .../service/RateLimiterServiceTest.java | 130 +++++------ .../service/TokenManagerTest.java | 194 ++++++++-------- .../cleat/persistence}/DashboardService.java | 2 +- 17 files changed, 567 insertions(+), 569 deletions(-) rename backend/libs/{domain/src/main/java/dev/cleat/domain => persistence/src/main/java/dev/cleat/persistence}/DashboardService.java (97%) diff --git a/backend/apps/api/src/main/java/dev/cleat/api/controller/DashboardController.java b/backend/apps/api/src/main/java/dev/cleat/api/controller/DashboardController.java index 975a6e1..7390e3a 100644 --- a/backend/apps/api/src/main/java/dev/cleat/api/controller/DashboardController.java +++ b/backend/apps/api/src/main/java/dev/cleat/api/controller/DashboardController.java @@ -1,7 +1,7 @@ package dev.cleat.api.controller; import dev.cleat.common.dto.response.DatasetDto; -import dev.cleat.domain.DashboardService; +import dev.cleat.persistence.DashboardService; import java.util.UUID; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; diff --git a/backend/apps/api/src/test/java/dev/cleat/api/AbstractIntegrationTest.java b/backend/apps/api/src/test/java/dev/cleat/api/AbstractIntegrationTest.java index 7af426b..31dd971 100644 --- a/backend/apps/api/src/test/java/dev/cleat/api/AbstractIntegrationTest.java +++ b/backend/apps/api/src/test/java/dev/cleat/api/AbstractIntegrationTest.java @@ -1,21 +1,21 @@ -package dev.cleat.api; - -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.testcontainers.service.connection.ServiceConnection; -import org.testcontainers.containers.GenericContainer; -import org.testcontainers.containers.PostgreSQLContainer; -import org.testcontainers.junit.jupiter.Container; -import org.testcontainers.junit.jupiter.Testcontainers; - -@Testcontainers -@SpringBootTest -public abstract class AbstractIntegrationTest { - - @Container - @ServiceConnection - static PostgreSQLContainer postgres = new PostgreSQLContainer<>("postgres:16-alpine"); - - @Container - @ServiceConnection - static GenericContainer redis = new GenericContainer<>("redis:7-alpine").withExposedPorts(6379); -} +package dev.cleat.api; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +@Testcontainers +@SpringBootTest +public abstract class AbstractIntegrationTest { + + @Container + @ServiceConnection + static PostgreSQLContainer postgres = new PostgreSQLContainer<>("postgres:16-alpine"); + + @Container + @ServiceConnection + static GenericContainer redis = new GenericContainer<>("redis:7-alpine").withExposedPorts(6379); +} diff --git a/backend/apps/api/src/test/java/dev/cleat/api/CleatApiTests.java b/backend/apps/api/src/test/java/dev/cleat/api/CleatApiTests.java index eb41e9f..71f9100 100644 --- a/backend/apps/api/src/test/java/dev/cleat/api/CleatApiTests.java +++ b/backend/apps/api/src/test/java/dev/cleat/api/CleatApiTests.java @@ -1,11 +1,11 @@ -package dev.cleat.api; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -public class CleatApiTests extends AbstractIntegrationTest { - - @Test - void contextLoad() {} -} +package dev.cleat.api; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +public class CleatApiTests extends AbstractIntegrationTest { + + @Test + void contextLoad() {} +} diff --git a/backend/apps/worker/src/main/java/dev/cleat/worker/CleatWorkerApplication.java b/backend/apps/worker/src/main/java/dev/cleat/worker/CleatWorkerApplication.java index c28eff7..5134ef9 100644 --- a/backend/apps/worker/src/main/java/dev/cleat/worker/CleatWorkerApplication.java +++ b/backend/apps/worker/src/main/java/dev/cleat/worker/CleatWorkerApplication.java @@ -1,14 +1,14 @@ -package dev.cleat.worker; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.scheduling.annotation.EnableScheduling; - -@SpringBootApplication -@EnableScheduling -public class CleatWorkerApplication { - - public static void main(String[] args) { - SpringApplication.run(CleatWorkerApplication.class, args); - } -} +package dev.cleat.worker; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableScheduling; + +@SpringBootApplication +@EnableScheduling +public class CleatWorkerApplication { + + public static void main(String[] args) { + SpringApplication.run(CleatWorkerApplication.class, args); + } +} diff --git a/backend/apps/worker/src/test/java/dev/cleat/worker/AbstractIntegrationTest.java b/backend/apps/worker/src/test/java/dev/cleat/worker/AbstractIntegrationTest.java index 63842d6..60c6e6d 100644 --- a/backend/apps/worker/src/test/java/dev/cleat/worker/AbstractIntegrationTest.java +++ b/backend/apps/worker/src/test/java/dev/cleat/worker/AbstractIntegrationTest.java @@ -1,21 +1,21 @@ -package dev.cleat.worker; - -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.testcontainers.service.connection.ServiceConnection; -import org.testcontainers.containers.GenericContainer; -import org.testcontainers.containers.PostgreSQLContainer; -import org.testcontainers.junit.jupiter.Container; -import org.testcontainers.junit.jupiter.Testcontainers; - -@Testcontainers -@SpringBootTest -public abstract class AbstractIntegrationTest { - - @Container - @ServiceConnection - static PostgreSQLContainer postgres = new PostgreSQLContainer<>("postgres:16-alpine"); - - @Container - @ServiceConnection - static GenericContainer redis = new GenericContainer<>("redis:7-alpine").withExposedPorts(6379); -} +package dev.cleat.worker; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +@Testcontainers +@SpringBootTest +public abstract class AbstractIntegrationTest { + + @Container + @ServiceConnection + static PostgreSQLContainer postgres = new PostgreSQLContainer<>("postgres:16-alpine"); + + @Container + @ServiceConnection + static GenericContainer redis = new GenericContainer<>("redis:7-alpine").withExposedPorts(6379); +} diff --git a/backend/apps/worker/src/test/java/dev/cleat/worker/CleatWorkerApplicationTests.java b/backend/apps/worker/src/test/java/dev/cleat/worker/CleatWorkerApplicationTests.java index 7a6f1f4..be47ba0 100644 --- a/backend/apps/worker/src/test/java/dev/cleat/worker/CleatWorkerApplicationTests.java +++ b/backend/apps/worker/src/test/java/dev/cleat/worker/CleatWorkerApplicationTests.java @@ -1,11 +1,11 @@ -package dev.cleat.worker; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -public class CleatWorkerApplicationTests extends AbstractIntegrationTest { - - @Test - void contextLoads() {} -} +package dev.cleat.worker; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +public class CleatWorkerApplicationTests extends AbstractIntegrationTest { + + @Test + void contextLoads() {} +} diff --git a/backend/libs/domain/build.gradle.kts b/backend/libs/domain/build.gradle.kts index c6800f2..7f5e077 100644 --- a/backend/libs/domain/build.gradle.kts +++ b/backend/libs/domain/build.gradle.kts @@ -5,6 +5,4 @@ plugins { dependencies { implementation("org.springframework.boot:spring-boot-starter-web") testImplementation("org.springframework.boot:spring-boot-starter-test") - implementation(project(":libs:persistence")) - implementation(project(":libs:common")) } diff --git a/backend/libs/github-client/src/main/java/dev/cleat/githubclient/config/GitHubConfig.java b/backend/libs/github-client/src/main/java/dev/cleat/githubclient/config/GitHubConfig.java index e18c52c..100d8e9 100644 --- a/backend/libs/github-client/src/main/java/dev/cleat/githubclient/config/GitHubConfig.java +++ b/backend/libs/github-client/src/main/java/dev/cleat/githubclient/config/GitHubConfig.java @@ -1,28 +1,28 @@ -package dev.cleat.githubclient.config; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.data.redis.connection.RedisConnectionFactory; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.data.redis.serializer.StringRedisSerializer; -import org.springframework.web.reactive.function.client.WebClient; - -@Configuration -public class GitHubConfig { - - @Bean - public WebClient gitHubWebClient(WebClient.Builder builder) { - return builder.baseUrl("https://api.github.com").build(); - } - - @Bean - public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) { - RedisTemplate template = new RedisTemplate<>(); - template.setConnectionFactory(connectionFactory); - - template.setKeySerializer(new StringRedisSerializer()); - template.setValueSerializer(new StringRedisSerializer()); - - return template; - } -} +package dev.cleat.githubclient.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.StringRedisSerializer; +import org.springframework.web.reactive.function.client.WebClient; + +@Configuration +public class GitHubConfig { + + @Bean + public WebClient gitHubWebClient(WebClient.Builder builder) { + return builder.baseUrl("https://api.github.com").build(); + } + + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(connectionFactory); + + template.setKeySerializer(new StringRedisSerializer()); + template.setValueSerializer(new StringRedisSerializer()); + + return template; + } +} diff --git a/backend/libs/github-client/src/main/java/dev/cleat/githubclient/dto/GitHubTokenResponse.java b/backend/libs/github-client/src/main/java/dev/cleat/githubclient/dto/GitHubTokenResponse.java index 7019cf6..89e41ec 100644 --- a/backend/libs/github-client/src/main/java/dev/cleat/githubclient/dto/GitHubTokenResponse.java +++ b/backend/libs/github-client/src/main/java/dev/cleat/githubclient/dto/GitHubTokenResponse.java @@ -1,29 +1,29 @@ -package dev.cleat.githubclient.dto; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; - -@JsonIgnoreProperties(ignoreUnknown = true) -public class GitHubTokenResponse { - - private String token; - - @JsonProperty("expires_at") - private String expiresAt; - - public String getToken() { - return token; - } - - public void setToken(String token) { - this.token = token; - } - - public String getExpiresAt() { - return expiresAt; - } - - public void setExpiresAt(String expiresAt) { - this.expiresAt = expiresAt; - } -} +package dev.cleat.githubclient.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class GitHubTokenResponse { + + private String token; + + @JsonProperty("expires_at") + private String expiresAt; + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + public String getExpiresAt() { + return expiresAt; + } + + public void setExpiresAt(String expiresAt) { + this.expiresAt = expiresAt; + } +} diff --git a/backend/libs/github-client/src/main/java/dev/cleat/githubclient/service/GitHubClient.java b/backend/libs/github-client/src/main/java/dev/cleat/githubclient/service/GitHubClient.java index 8b04abf..ac1b244 100644 --- a/backend/libs/github-client/src/main/java/dev/cleat/githubclient/service/GitHubClient.java +++ b/backend/libs/github-client/src/main/java/dev/cleat/githubclient/service/GitHubClient.java @@ -1,46 +1,46 @@ -package dev.cleat.githubclient.service; - -import java.util.List; -import org.springframework.stereotype.Service; -import org.springframework.web.reactive.function.client.WebClient; -import reactor.core.publisher.Mono; - -@Service -public class GitHubClient { - private final WebClient webClient; - private final TokenManager tokenManager; - private final RateLimiterService rateLimiterService; - - public GitHubClient(WebClient gitHubWebClient, TokenManager tokenManager, RateLimiterService rateLimiterService) { - this.webClient = gitHubWebClient; - this.tokenManager = tokenManager; - this.rateLimiterService = rateLimiterService; - } - - public T get(String uri, String installationId, Class responseType) { - rateLimiterService.checkLimit(installationId); - - String token = tokenManager.getInstallationToken(installationId); - - return webClient - .get() - .uri(builder -> builder.path(uri).build()) - .header("Authorization", "Bearer " + token) - .retrieve() - .toEntity(responseType) - .flatMap(entity -> { - List remainingHeaders = entity.getHeaders().get("X-RateLimit-Remaining"); - List resetHeaders = entity.getHeaders().get("X-RateLimit-Reset"); - - if (remainingHeaders != null - && !remainingHeaders.isEmpty() - && resetHeaders != null - && !resetHeaders.isEmpty()) { - rateLimiterService.updateLimit(installationId, remainingHeaders.get(0), resetHeaders.get(0)); - } - return Mono.justOrEmpty(entity.getBody()); - }) - .timeout(java.time.Duration.ofSeconds(10)) - .block(); - } -} +package dev.cleat.githubclient.service; + +import java.util.List; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +@Service +public class GitHubClient { + private final WebClient webClient; + private final TokenManager tokenManager; + private final RateLimiterService rateLimiterService; + + public GitHubClient(WebClient gitHubWebClient, TokenManager tokenManager, RateLimiterService rateLimiterService) { + this.webClient = gitHubWebClient; + this.tokenManager = tokenManager; + this.rateLimiterService = rateLimiterService; + } + + public T get(String uri, String installationId, Class responseType) { + rateLimiterService.checkLimit(installationId); + + String token = tokenManager.getInstallationToken(installationId); + + return webClient + .get() + .uri(builder -> builder.path(uri).build()) + .header("Authorization", "Bearer " + token) + .retrieve() + .toEntity(responseType) + .flatMap(entity -> { + List remainingHeaders = entity.getHeaders().get("X-RateLimit-Remaining"); + List resetHeaders = entity.getHeaders().get("X-RateLimit-Reset"); + + if (remainingHeaders != null + && !remainingHeaders.isEmpty() + && resetHeaders != null + && !resetHeaders.isEmpty()) { + rateLimiterService.updateLimit(installationId, remainingHeaders.get(0), resetHeaders.get(0)); + } + return Mono.justOrEmpty(entity.getBody()); + }) + .timeout(java.time.Duration.ofSeconds(10)) + .block(); + } +} diff --git a/backend/libs/github-client/src/main/java/dev/cleat/githubclient/service/RateLimiterService.java b/backend/libs/github-client/src/main/java/dev/cleat/githubclient/service/RateLimiterService.java index 417d9ba..2cf7a40 100644 --- a/backend/libs/github-client/src/main/java/dev/cleat/githubclient/service/RateLimiterService.java +++ b/backend/libs/github-client/src/main/java/dev/cleat/githubclient/service/RateLimiterService.java @@ -1,38 +1,38 @@ -package dev.cleat.githubclient.service; - -import java.time.Duration; -import java.time.Instant; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.stereotype.Service; - -@Service -public class RateLimiterService { - private final RedisTemplate redisTemplate; - - public RateLimiterService(RedisTemplate redisTemplate) { - this.redisTemplate = redisTemplate; - } - - public void checkLimit(String installationId) { - String remaining = redisTemplate.opsForValue().get("rate_limit:" + installationId); - - if (remaining != null && Integer.parseInt(remaining) <= 0) { - throw new RuntimeException("Rate limit exceeded for installation: " + installationId); - } - } - - public void updateLimit(String installationId, String remaining, String resetTimestamp) { - try { - int limit = Integer.parseInt(remaining); - long resetTime = Long.parseLong(resetTimestamp); - - long ttl = resetTime - Instant.now().getEpochSecond(); - - redisTemplate - .opsForValue() - .set("rate_limit:" + installationId, String.valueOf(limit), Duration.ofSeconds(Math.max(ttl, 1))); - } catch (NumberFormatException e) { - redisTemplate.delete("rate_limit:" + installationId); - } - } -} +package dev.cleat.githubclient.service; + +import java.time.Duration; +import java.time.Instant; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +@Service +public class RateLimiterService { + private final RedisTemplate redisTemplate; + + public RateLimiterService(RedisTemplate redisTemplate) { + this.redisTemplate = redisTemplate; + } + + public void checkLimit(String installationId) { + String remaining = redisTemplate.opsForValue().get("rate_limit:" + installationId); + + if (remaining != null && Integer.parseInt(remaining) <= 0) { + throw new RuntimeException("Rate limit exceeded for installation: " + installationId); + } + } + + public void updateLimit(String installationId, String remaining, String resetTimestamp) { + try { + int limit = Integer.parseInt(remaining); + long resetTime = Long.parseLong(resetTimestamp); + + long ttl = resetTime - Instant.now().getEpochSecond(); + + redisTemplate + .opsForValue() + .set("rate_limit:" + installationId, String.valueOf(limit), Duration.ofSeconds(Math.max(ttl, 1))); + } catch (NumberFormatException e) { + redisTemplate.delete("rate_limit:" + installationId); + } + } +} diff --git a/backend/libs/github-client/src/main/java/dev/cleat/githubclient/service/TokenManager.java b/backend/libs/github-client/src/main/java/dev/cleat/githubclient/service/TokenManager.java index 9c219d9..a7adadf 100644 --- a/backend/libs/github-client/src/main/java/dev/cleat/githubclient/service/TokenManager.java +++ b/backend/libs/github-client/src/main/java/dev/cleat/githubclient/service/TokenManager.java @@ -1,104 +1,104 @@ -package dev.cleat.githubclient.service; - -import dev.cleat.githubclient.dto.GitHubTokenResponse; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.SignatureAlgorithm; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.security.KeyFactory; -import java.security.PrivateKey; -import java.security.spec.PKCS8EncodedKeySpec; -import java.time.Duration; -import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.Base64; -import java.util.Date; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.stereotype.Service; -import org.springframework.web.reactive.function.client.WebClient; - -@Service -public class TokenManager { - - private final RedisTemplate redisTemplate; - private final WebClient webClient; - - public TokenManager(RedisTemplate redisTemplate, WebClient gitHubWebClient) { - this.redisTemplate = redisTemplate; - this.webClient = gitHubWebClient; - } - - @Value("${github.app-id}") - private String appId; - - @Value("${github.private-key-path}") - private String privateKeyPath; - - public String getInstallationToken(String installationId) { - String cachedToken = redisTemplate.opsForValue().get("token:" + installationId); - - if (cachedToken != null) { - return cachedToken; - } - - return mintNewToken(installationId); - } - - private String mintNewToken(String installationId) { - String jwt = generateJwt(); - - GitHubTokenResponse response = webClient - .post() - .uri("/app/installations/" + installationId + "/access_tokens") - .header("Authorization", "Bearer " + jwt) - .retrieve() - .bodyToMono(GitHubTokenResponse.class) - .block(); - - if (response == null || response.getToken() == null || response.getExpiresAt() == null) { - throw new RuntimeException("Failed to retrieve a valid token from GitHub"); - } - - String newToken = response.getToken(); - - Instant expiresAt = Instant.parse(response.getExpiresAt()); - long safetyMarginSeconds = 300; - long ttlSeconds = ChronoUnit.SECONDS.between(Instant.now(), expiresAt) - safetyMarginSeconds; - - long finalTtl = Math.max(0, ttlSeconds); - - redisTemplate.opsForValue().set("token:" + installationId, newToken, Duration.ofSeconds(finalTtl)); - - return newToken; - } - - protected String generateJwt() { - Path path = Paths.get(privateKeyPath); - if (!Files.exists(path)) { - throw new IllegalStateException("Private key file not found at path: " + privateKeyPath); - } - - try { - String key = Files.readString(path); - - String privateKeyPEM = key.replace("-----BEGIN PRIVATE KEY-----", "") - .replaceAll("\\s+", "") - .replace("-----END PRIVATE KEY-----", ""); - - byte[] encoded = Base64.getDecoder().decode(privateKeyPEM); - KeyFactory keyFactory = KeyFactory.getInstance("RSA"); - PrivateKey privateKey = keyFactory.generatePrivate(new PKCS8EncodedKeySpec(encoded)); - - return Jwts.builder() - .setIssuer(appId) - .setIssuedAt(new Date()) - .setExpiration(new Date(System.currentTimeMillis() + 600000)) - .signWith(privateKey, SignatureAlgorithm.RS256) - .compact(); - } catch (Exception e) { - throw new RuntimeException("Error occurred while generating JWT", e); - } - } -} +package dev.cleat.githubclient.service; + +import dev.cleat.githubclient.dto.GitHubTokenResponse; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.KeyFactory; +import java.security.PrivateKey; +import java.security.spec.PKCS8EncodedKeySpec; +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Base64; +import java.util.Date; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClient; + +@Service +public class TokenManager { + + private final RedisTemplate redisTemplate; + private final WebClient webClient; + + public TokenManager(RedisTemplate redisTemplate, WebClient gitHubWebClient) { + this.redisTemplate = redisTemplate; + this.webClient = gitHubWebClient; + } + + @Value("${github.app-id}") + private String appId; + + @Value("${github.private-key-path}") + private String privateKeyPath; + + public String getInstallationToken(String installationId) { + String cachedToken = redisTemplate.opsForValue().get("token:" + installationId); + + if (cachedToken != null) { + return cachedToken; + } + + return mintNewToken(installationId); + } + + private String mintNewToken(String installationId) { + String jwt = generateJwt(); + + GitHubTokenResponse response = webClient + .post() + .uri("/app/installations/" + installationId + "/access_tokens") + .header("Authorization", "Bearer " + jwt) + .retrieve() + .bodyToMono(GitHubTokenResponse.class) + .block(); + + if (response == null || response.getToken() == null || response.getExpiresAt() == null) { + throw new RuntimeException("Failed to retrieve a valid token from GitHub"); + } + + String newToken = response.getToken(); + + Instant expiresAt = Instant.parse(response.getExpiresAt()); + long safetyMarginSeconds = 300; + long ttlSeconds = ChronoUnit.SECONDS.between(Instant.now(), expiresAt) - safetyMarginSeconds; + + long finalTtl = Math.max(0, ttlSeconds); + + redisTemplate.opsForValue().set("token:" + installationId, newToken, Duration.ofSeconds(finalTtl)); + + return newToken; + } + + protected String generateJwt() { + Path path = Paths.get(privateKeyPath); + if (!Files.exists(path)) { + throw new IllegalStateException("Private key file not found at path: " + privateKeyPath); + } + + try { + String key = Files.readString(path); + + String privateKeyPEM = key.replace("-----BEGIN PRIVATE KEY-----", "") + .replaceAll("\\s+", "") + .replace("-----END PRIVATE KEY-----", ""); + + byte[] encoded = Base64.getDecoder().decode(privateKeyPEM); + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + PrivateKey privateKey = keyFactory.generatePrivate(new PKCS8EncodedKeySpec(encoded)); + + return Jwts.builder() + .setIssuer(appId) + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() + 600000)) + .signWith(privateKey, SignatureAlgorithm.RS256) + .compact(); + } catch (Exception e) { + throw new RuntimeException("Error occurred while generating JWT", e); + } + } +} diff --git a/backend/libs/github-client/src/test/java/dev/cleat/githubclient/config/TestGitHubConfig.java b/backend/libs/github-client/src/test/java/dev/cleat/githubclient/config/TestGitHubConfig.java index 3d42c7c..7358044 100644 --- a/backend/libs/github-client/src/test/java/dev/cleat/githubclient/config/TestGitHubConfig.java +++ b/backend/libs/github-client/src/test/java/dev/cleat/githubclient/config/TestGitHubConfig.java @@ -1,16 +1,16 @@ -package dev.cleat.githubclient.config; - -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.web.reactive.function.client.WebClient; - -@SpringBootApplication -@ComponentScan(basePackages = "dev.cleat.githubclient") -public class TestGitHubConfig { - - @Bean - public WebClient webClient() { - return WebClient.builder().build(); - } -} +package dev.cleat.githubclient.config; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.web.reactive.function.client.WebClient; + +@SpringBootApplication +@ComponentScan(basePackages = "dev.cleat.githubclient") +public class TestGitHubConfig { + + @Bean + public WebClient webClient() { + return WebClient.builder().build(); + } +} diff --git a/backend/libs/github-client/src/test/java/dev/cleat/githubclient/service/GitHubClientTest.java b/backend/libs/github-client/src/test/java/dev/cleat/githubclient/service/GitHubClientTest.java index 080a9d7..7f87234 100644 --- a/backend/libs/github-client/src/test/java/dev/cleat/githubclient/service/GitHubClientTest.java +++ b/backend/libs/github-client/src/test/java/dev/cleat/githubclient/service/GitHubClientTest.java @@ -1,64 +1,64 @@ -package dev.cleat.githubclient.service; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.http.HttpStatus; -import org.springframework.web.reactive.function.client.ClientResponse; -import org.springframework.web.reactive.function.client.ExchangeFunction; -import org.springframework.web.reactive.function.client.WebClient; -import reactor.core.publisher.Mono; - -@ExtendWith(MockitoExtension.class) -class GitHubClientTest { - - @Mock - private TokenManager tokenManager; - - @Mock - private RateLimiterService rateLimiterService; - - private GitHubClient gitHubClient; - - @BeforeEach - void setUp() { - ExchangeFunction exchangeFunction = request -> { - if (request.headers().getFirst("Authorization") == null - || !request.headers().getFirst("Authorization").startsWith("Bearer ")) { - return Mono.just(ClientResponse.create(HttpStatus.UNAUTHORIZED).build()); - } - - return Mono.just(ClientResponse.create(HttpStatus.OK) - .header("X-RateLimit-Remaining", "4999") - .header("X-RateLimit-Reset", "1767272800") - .body("{\"name\": \"test\"}") - .build()); - }; - - WebClient webClient = - WebClient.builder().exchangeFunction(exchangeFunction).build(); - - gitHubClient = new GitHubClient(webClient, tokenManager, rateLimiterService); - } - - @Test - void getSendsBearerTokenAndUpdatesRateLimit() { - String installationId = "123"; - String fakeToken = "fake-token"; - - when(tokenManager.getInstallationToken(installationId)).thenReturn(fakeToken); - - String response = gitHubClient.get("/test", installationId, String.class); - - verify(rateLimiterService).checkLimit(installationId); - assertEquals("{\"name\": \"test\"}", response); - verify(rateLimiterService).updateLimit(eq(installationId), eq("4999"), eq("1767272800")); - } -} +package dev.cleat.githubclient.service; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpStatus; +import org.springframework.web.reactive.function.client.ClientResponse; +import org.springframework.web.reactive.function.client.ExchangeFunction; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +@ExtendWith(MockitoExtension.class) +class GitHubClientTest { + + @Mock + private TokenManager tokenManager; + + @Mock + private RateLimiterService rateLimiterService; + + private GitHubClient gitHubClient; + + @BeforeEach + void setUp() { + ExchangeFunction exchangeFunction = request -> { + if (request.headers().getFirst("Authorization") == null + || !request.headers().getFirst("Authorization").startsWith("Bearer ")) { + return Mono.just(ClientResponse.create(HttpStatus.UNAUTHORIZED).build()); + } + + return Mono.just(ClientResponse.create(HttpStatus.OK) + .header("X-RateLimit-Remaining", "4999") + .header("X-RateLimit-Reset", "1767272800") + .body("{\"name\": \"test\"}") + .build()); + }; + + WebClient webClient = + WebClient.builder().exchangeFunction(exchangeFunction).build(); + + gitHubClient = new GitHubClient(webClient, tokenManager, rateLimiterService); + } + + @Test + void getSendsBearerTokenAndUpdatesRateLimit() { + String installationId = "123"; + String fakeToken = "fake-token"; + + when(tokenManager.getInstallationToken(installationId)).thenReturn(fakeToken); + + String response = gitHubClient.get("/test", installationId, String.class); + + verify(rateLimiterService).checkLimit(installationId); + assertEquals("{\"name\": \"test\"}", response); + verify(rateLimiterService).updateLimit(eq(installationId), eq("4999"), eq("1767272800")); + } +} diff --git a/backend/libs/github-client/src/test/java/dev/cleat/githubclient/service/RateLimiterServiceTest.java b/backend/libs/github-client/src/test/java/dev/cleat/githubclient/service/RateLimiterServiceTest.java index bc3837b..17a66c9 100644 --- a/backend/libs/github-client/src/test/java/dev/cleat/githubclient/service/RateLimiterServiceTest.java +++ b/backend/libs/github-client/src/test/java/dev/cleat/githubclient/service/RateLimiterServiceTest.java @@ -1,65 +1,65 @@ -package dev.cleat.githubclient.service; - -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.time.Instant; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.data.redis.core.ValueOperations; - -@ExtendWith(MockitoExtension.class) -class RateLimiterServiceTest { - - @Mock - private RedisTemplate redisTemplate; - - @Mock - private ValueOperations valueOperations; - - @InjectMocks - private RateLimiterService rateLimiterService; - - @Test - void checkLimitThrowsExceptionWhenLimitIsZero() { - String id = "123"; - when(redisTemplate.opsForValue()).thenReturn(valueOperations); - when(valueOperations.get("rate_limit:" + id)).thenReturn("0"); - - assertThrows(RuntimeException.class, () -> rateLimiterService.checkLimit(id)); - } - - @Test - void updateLimitShouldCalculateCorrectTtlAndCallRedis() { - String installationId = "123"; - String remaining = "4999"; - long resetTimestamp = Instant.now().getEpochSecond() + 3600; - String resetStr = String.valueOf(resetTimestamp); - - when(redisTemplate.opsForValue()).thenReturn(valueOperations); - - rateLimiterService.updateLimit(installationId, remaining, resetStr); - - verify(valueOperations) - .set( - eq("rate_limit:" + installationId), - eq(remaining), - argThat(duration -> duration.getSeconds() >= 3590 && duration.getSeconds() <= 3600)); - } - - @Test - void updateLimitShouldClearCacheOnNumberFormatException() { - String installationId = "123"; - - rateLimiterService.updateLimit(installationId, "invalid", "invalid"); - - verify(redisTemplate).delete("rate_limit:" + installationId); - } -} +package dev.cleat.githubclient.service; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.time.Instant; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ValueOperations; + +@ExtendWith(MockitoExtension.class) +class RateLimiterServiceTest { + + @Mock + private RedisTemplate redisTemplate; + + @Mock + private ValueOperations valueOperations; + + @InjectMocks + private RateLimiterService rateLimiterService; + + @Test + void checkLimitThrowsExceptionWhenLimitIsZero() { + String id = "123"; + when(redisTemplate.opsForValue()).thenReturn(valueOperations); + when(valueOperations.get("rate_limit:" + id)).thenReturn("0"); + + assertThrows(RuntimeException.class, () -> rateLimiterService.checkLimit(id)); + } + + @Test + void updateLimitShouldCalculateCorrectTtlAndCallRedis() { + String installationId = "123"; + String remaining = "4999"; + long resetTimestamp = Instant.now().getEpochSecond() + 3600; + String resetStr = String.valueOf(resetTimestamp); + + when(redisTemplate.opsForValue()).thenReturn(valueOperations); + + rateLimiterService.updateLimit(installationId, remaining, resetStr); + + verify(valueOperations) + .set( + eq("rate_limit:" + installationId), + eq(remaining), + argThat(duration -> duration.getSeconds() >= 3590 && duration.getSeconds() <= 3600)); + } + + @Test + void updateLimitShouldClearCacheOnNumberFormatException() { + String installationId = "123"; + + rateLimiterService.updateLimit(installationId, "invalid", "invalid"); + + verify(redisTemplate).delete("rate_limit:" + installationId); + } +} diff --git a/backend/libs/github-client/src/test/java/dev/cleat/githubclient/service/TokenManagerTest.java b/backend/libs/github-client/src/test/java/dev/cleat/githubclient/service/TokenManagerTest.java index 29d253d..262b11c 100644 --- a/backend/libs/github-client/src/test/java/dev/cleat/githubclient/service/TokenManagerTest.java +++ b/backend/libs/github-client/src/test/java/dev/cleat/githubclient/service/TokenManagerTest.java @@ -1,97 +1,97 @@ -package dev.cleat.githubclient.service; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import dev.cleat.githubclient.dto.GitHubTokenResponse; -import java.time.Duration; -import java.time.Instant; -import java.time.temporal.ChronoUnit; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.data.redis.core.ValueOperations; -import org.springframework.test.util.ReflectionTestUtils; -import org.springframework.web.reactive.function.client.WebClient; -import reactor.core.publisher.Mono; - -@ExtendWith(MockitoExtension.class) -class TokenManagerTest { - - @Mock - private RedisTemplate redisTemplate; - - @Mock - private ValueOperations valueOperations; - - @Mock - private WebClient webClient; - - @Mock - private WebClient.RequestBodyUriSpec requestBodyUriSpec; - - @Mock - private WebClient.RequestBodySpec requestBodySpec; - - @Mock - private WebClient.ResponseSpec responseSpec; - - private TokenManager tokenManager; - - @BeforeEach - void setUp() { - TokenManager realTokenManager = new TokenManager(redisTemplate, webClient); - tokenManager = spy(realTokenManager); - - ReflectionTestUtils.setField(tokenManager, "appId", "12345"); - ReflectionTestUtils.setField(tokenManager, "privateKeyPath", "src/test/resources/test-key.pem"); - } - - @Test - void getInstallationTokenReturnsCachedTokenWhenExistsInRedis() { - when(redisTemplate.opsForValue()).thenReturn(valueOperations); - - String installationId = "123"; - String expectedToken = "valid-token"; - when(valueOperations.get("token:" + installationId)).thenReturn(expectedToken); - - String actualToken = tokenManager.getInstallationToken(installationId); - - assertEquals(expectedToken, actualToken); - } - - @Test - void getInstallationTokenMintNewTokenAndCacheItWhenCacheMiss() { - when(redisTemplate.opsForValue()).thenReturn(valueOperations); - doReturn("fake-jwt-token").when(tokenManager).generateJwt(); - - String installationId = "123"; - String newToken = "new-minted-token"; - - GitHubTokenResponse response = new GitHubTokenResponse(); - response.setToken(newToken); - response.setExpiresAt(Instant.now().plus(1, ChronoUnit.HOURS).toString()); - - when(valueOperations.get("token:" + installationId)).thenReturn(null); - - // WebClient chain - when(webClient.post()).thenReturn(requestBodyUriSpec); - when(requestBodyUriSpec.uri(any(String.class))).thenReturn(requestBodySpec); - when(requestBodySpec.header(any(), any())).thenReturn(requestBodySpec); - when(requestBodySpec.retrieve()).thenReturn(responseSpec); - when(responseSpec.bodyToMono(GitHubTokenResponse.class)).thenReturn(Mono.just(response)); - - String actualToken = tokenManager.getInstallationToken(installationId); - - assertEquals(newToken, actualToken); - verify(valueOperations).set(eq("token:" + installationId), eq(newToken), any(Duration.class)); - } -} +package dev.cleat.githubclient.service; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import dev.cleat.githubclient.dto.GitHubTokenResponse; +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ValueOperations; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +@ExtendWith(MockitoExtension.class) +class TokenManagerTest { + + @Mock + private RedisTemplate redisTemplate; + + @Mock + private ValueOperations valueOperations; + + @Mock + private WebClient webClient; + + @Mock + private WebClient.RequestBodyUriSpec requestBodyUriSpec; + + @Mock + private WebClient.RequestBodySpec requestBodySpec; + + @Mock + private WebClient.ResponseSpec responseSpec; + + private TokenManager tokenManager; + + @BeforeEach + void setUp() { + TokenManager realTokenManager = new TokenManager(redisTemplate, webClient); + tokenManager = spy(realTokenManager); + + ReflectionTestUtils.setField(tokenManager, "appId", "12345"); + ReflectionTestUtils.setField(tokenManager, "privateKeyPath", "src/test/resources/test-key.pem"); + } + + @Test + void getInstallationTokenReturnsCachedTokenWhenExistsInRedis() { + when(redisTemplate.opsForValue()).thenReturn(valueOperations); + + String installationId = "123"; + String expectedToken = "valid-token"; + when(valueOperations.get("token:" + installationId)).thenReturn(expectedToken); + + String actualToken = tokenManager.getInstallationToken(installationId); + + assertEquals(expectedToken, actualToken); + } + + @Test + void getInstallationTokenMintNewTokenAndCacheItWhenCacheMiss() { + when(redisTemplate.opsForValue()).thenReturn(valueOperations); + doReturn("fake-jwt-token").when(tokenManager).generateJwt(); + + String installationId = "123"; + String newToken = "new-minted-token"; + + GitHubTokenResponse response = new GitHubTokenResponse(); + response.setToken(newToken); + response.setExpiresAt(Instant.now().plus(1, ChronoUnit.HOURS).toString()); + + when(valueOperations.get("token:" + installationId)).thenReturn(null); + + // WebClient chain + when(webClient.post()).thenReturn(requestBodyUriSpec); + when(requestBodyUriSpec.uri(any(String.class))).thenReturn(requestBodySpec); + when(requestBodySpec.header(any(), any())).thenReturn(requestBodySpec); + when(requestBodySpec.retrieve()).thenReturn(responseSpec); + when(responseSpec.bodyToMono(GitHubTokenResponse.class)).thenReturn(Mono.just(response)); + + String actualToken = tokenManager.getInstallationToken(installationId); + + assertEquals(newToken, actualToken); + verify(valueOperations).set(eq("token:" + installationId), eq(newToken), any(Duration.class)); + } +} diff --git a/backend/libs/domain/src/main/java/dev/cleat/domain/DashboardService.java b/backend/libs/persistence/src/main/java/dev/cleat/persistence/DashboardService.java similarity index 97% rename from backend/libs/domain/src/main/java/dev/cleat/domain/DashboardService.java rename to backend/libs/persistence/src/main/java/dev/cleat/persistence/DashboardService.java index d95d1b3..a4e2220 100644 --- a/backend/libs/domain/src/main/java/dev/cleat/domain/DashboardService.java +++ b/backend/libs/persistence/src/main/java/dev/cleat/persistence/DashboardService.java @@ -1,4 +1,4 @@ -package dev.cleat.domain; +package dev.cleat.persistence; import dev.cleat.common.dto.response.DatasetDto; import dev.cleat.common.exception.NotFoundException;