Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,26 @@ for example `dev.cleat.domain` and `dev.cleat.githubclient`.
./gradlew build # build and test everything
```

Local dependencies (Postgres, Redis) are expected to run via Docker Compose. The
two services build into one container image each.
Comment on lines +60 to +61

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove duplicated “Local dependencies…” statement.

The sentence appears twice with different endings, which makes the setup guidance inconsistent.

Also applies to: 80-80

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/README.md` around lines 60 - 61, The "Local dependencies (Postgres,
Redis) are expected to run via Docker Compose" statement appears twice in the
README.md file, once around line 60-61 and again around line 80, with different
endings that create inconsistency. Identify both instances of this duplicated
statement and remove one of the duplicate occurrences, keeping the version that
provides the most complete and useful information for setup guidance. This will
eliminate the redundancy and ensure consistent documentation.



## 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.
Local dependencies (Postgres, Redis) are expected to run via Docker Compose, while the API and Worker services are intended to be run locally via Gradle


Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package dev.cleat.api.controller;

import dev.cleat.common.dto.response.DatasetDto;
import dev.cleat.persistence.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<DatasetDto> getDashboardData(@RequestHeader("X-Account-Id") UUID accountId) {
return ResponseEntity.ok(dashboardService.getDataset(accountId));
Comment on lines +22 to +23

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Do not trust tenant scope from client-controlled header alone.

Using X-Account-Id directly as data scope enables cross-account access unless a trusted gateway overwrites it and server-side auth enforces membership.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/apps/api/src/main/java/dev/cleat/api/controller/DashboardController.java`
around lines 22 - 23, The getDashboardData method in DashboardController accepts
the accountId directly from the client-controlled X-Account-Id header without
validating that the authenticated user has access to that account. To fix this,
extract the authenticated user's identity from the security context (such as
through SecurityContextHolder or a Spring Security Principal parameter),
retrieve the list of accounts the user has access to, verify that the requested
accountId is in that list, and only then proceed with the
dashboardService.getDataset call. If the user does not have access to the
requested accountId, return an appropriate error response (such as 403
Forbidden).

}
}
Original file line number Diff line number Diff line change
@@ -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);
}
22 changes: 11 additions & 11 deletions backend/apps/api/src/test/java/dev/cleat/api/CleatApiTests.java
Original file line number Diff line number Diff line change
@@ -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() {}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
@@ -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() {}
}
1 change: 0 additions & 1 deletion backend/libs/common/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,5 @@ plugins {
dependencies {
api("org.springframework.boot:spring-boot-starter")
api("org.springframework.boot:spring-boot-starter-validation")

testImplementation("org.springframework.boot:spring-boot-starter-test")
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading
Loading