Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@EnableMethodSecurity(proxyTargetClass = true)
public class SecurityConfig {

@Bean
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package tum.devoops.financeservice.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.web.bind.annotation.RestController;
import tum.devoops.financeservice.api.FinanceApi;
import tum.devoops.financeservice.model.Balance;
import tum.devoops.financeservice.model.Transaction;
import tum.devoops.financeservice.model.TransactionCreate;
import tum.devoops.financeservice.model.TransactionPartialUpdate;
import tum.devoops.financeservice.service.TransactionService;

import java.util.List;
import java.util.UUID;

@RestController
@PreAuthorize("hasAnyRole('admin', 'member')")
public class FinanceController implements FinanceApi {

@Autowired
TransactionService transactionService;

@Override
public ResponseEntity<Transaction> createTransaction(TransactionCreate transactionCreate) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
UUID requesterId = extractRequesterId(auth);
boolean isAdmin = extractIsAdmin(auth);
Transaction created = transactionService.createTransaction(transactionCreate, requesterId, isAdmin);
return ResponseEntity.status(HttpStatus.CREATED).body(created);
}

@Override
public ResponseEntity<Void> deleteTransaction(UUID transactionId) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
UUID requesterId = extractRequesterId(auth);
boolean isAdmin = extractIsAdmin(auth);
transactionService.deleteTransaction(transactionId, requesterId, isAdmin);
return ResponseEntity.noContent().build();
}

@Override
public ResponseEntity<List<Transaction>> getAllTransactions() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
UUID requesterId = extractRequesterId(auth);
boolean isAdmin = extractIsAdmin(auth);
return ResponseEntity.ok(transactionService.getAllTransactions(requesterId, isAdmin));
}

@Override
public ResponseEntity<Transaction> getTransaction(UUID transactionId) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
UUID requesterId = extractRequesterId(auth);
boolean isAdmin = extractIsAdmin(auth);
return ResponseEntity.ok(transactionService.getTransaction(transactionId, requesterId, isAdmin));
}

@Override
public ResponseEntity<Transaction> updateTransaction(UUID transactionId, TransactionPartialUpdate transactionPartialUpdate) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
UUID requesterId = extractRequesterId(auth);
boolean isAdmin = extractIsAdmin(auth);
return ResponseEntity.ok(transactionService.updateTransaction(transactionId, transactionPartialUpdate, requesterId, isAdmin));
}

@Override
public ResponseEntity<List<Balance>> getAllBalances() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
UUID requesterId = extractRequesterId(auth);
boolean isAdmin = extractIsAdmin(auth);
return ResponseEntity.ok(transactionService.getAllBalances(requesterId, isAdmin));
}

@Override
public ResponseEntity<Balance> getMemberBalance(UUID memberId) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
UUID requesterId = extractRequesterId(auth);
boolean isAdmin = extractIsAdmin(auth);
return ResponseEntity.ok(transactionService.getMemberBalance(memberId, requesterId, isAdmin));
}

private UUID extractRequesterId(Authentication auth) {
Jwt jwt = (Jwt) auth.getPrincipal();
return UUID.fromString(jwt.getSubject());
}

private boolean extractIsAdmin(Authentication auth) {
return auth.getAuthorities().stream()
.anyMatch(a -> "ROLE_admin".equals(a.getAuthority()));
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package tum.devoops.financeservice;
package tum.devoops.financeservice.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package tum.devoops.financeservice.converter;

import tum.devoops.financeservice.entity.TransactionEntity;
import tum.devoops.financeservice.model.Transaction;
import tum.devoops.financeservice.model.TransactionCreate;

import java.time.Instant;
import java.time.ZoneOffset;
import java.util.UUID;

public class TransactionConverter {

public static Transaction toTransaction(TransactionEntity entity) {
return new Transaction(
entity.getId(),
entity.getMemberId().toString(),
entity.getCreatorId().toString(),
entity.getAmountCents(),
entity.getCreatedAt().atOffset(ZoneOffset.UTC),
entity.getTitle(),
entity.getDescription()
);
}

public static TransactionEntity toEntity(TransactionCreate create, UUID memberId, UUID creatorId) {
TransactionEntity entity = new TransactionEntity();
entity.setMemberId(memberId);
entity.setCreatorId(creatorId);
entity.setAmountCents(create.getAmountCents());
entity.setCreatedAt(Instant.now());
entity.setTitle(create.getTitle());
entity.setDescription(create.getDescription() != null ? create.getDescription() : "");
return entity;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package tum.devoops.financeservice.entity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import jakarta.persistence.Embeddable;
import jakarta.persistence.EmbeddedId;
import lombok.Getter;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;
import java.util.UUID;

@Entity
@Table(schema = "organization", name = "directors")
@Getter @NoArgsConstructor
public class DirectorEntity {
@EmbeddedId
private Id id;

@Embeddable
@Data @NoArgsConstructor @AllArgsConstructor
public static class Id implements Serializable {
@Column(name = "sport_name", nullable = false)
private String sportName;

@Column(name = "member_id", nullable = false)
private UUID memberId;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package tum.devoops.financeservice.entity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.util.UUID;

@Entity
@Table(schema = "member", name="members")
@Getter
@NoArgsConstructor
public class MemberEntity {
@Id
@Column(name = "id", nullable = false, updatable = false)
private UUID id;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package tum.devoops.financeservice.entity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.persistence.OneToMany;
import jakarta.persistence.JoinColumn;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.util.List;
import java.util.UUID;

@Entity
@Table(schema = "organization", name = "teams")
@Getter
@NoArgsConstructor
public class TeamEntity {

@Id
@Column(name = "id", nullable = false)
UUID id;

@Column(name = "sport_name", nullable = false)
private String sportName;

@OneToMany
@JoinColumn(name = "team_id", referencedColumnName = "id", insertable = false, updatable = false)
private List<TrainerEntity> trainers;

@OneToMany
@JoinColumn(name = "team_id", referencedColumnName = "id", insertable = false, updatable = false)
private List<TraineeEntity> trainees;
Comment on lines +32 to +34

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does JPQL allow selecting a collection-valued path expression like SELECT t.trainees FROM TeamEntity t, or must it use JOIN t.trainees trainee and select the joined alias?

💡 Result:

JPQL does not allow selecting a collection-valued path expression directly in the SELECT clause [1][2][3]. A query such as SELECT t.trainees FROM TeamEntity t is invalid because the SELECT clause must contain only single-valued expressions [2][3][4]. To achieve this, you must use a JOIN in the FROM clause to define an identification variable for the elements within the collection, and then select that alias [1][2][4]. The correct approach is: SELECT trainee FROM TeamEntity t JOIN t.trainees trainee Alternatively, using the IN syntax: SELECT trainee FROM TeamEntity t, IN (t.trainees) trainee In both cases, you are selecting the single-valued alias (trainee) that represents an individual element of the collection, which is permitted by the JPQL specification [2][4][5].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Files ==\n'
git ls-files 'services/spring-finance/src/main/java/tum/devoops/financeservice/entity/TeamEntity.java' \
             'services/spring-finance/src/main/java/tum/devoops/financeservice/repository/TeamRepository.java' \
             'services/spring-finance/src/main/java/tum/devoops/financeservice/service/TransactionService.java'

printf '\n== TeamEntity outline ==\n'
ast-grep outline services/spring-finance/src/main/java/tum/devoops/financeservice/entity/TeamEntity.java --view expanded

printf '\n== TeamRepository outline ==\n'
ast-grep outline services/spring-finance/src/main/java/tum/devoops/financeservice/repository/TeamRepository.java --view expanded

printf '\n== TransactionService outline (relevant symbols) ==\n'
ast-grep outline services/spring-finance/src/main/java/tum/devoops/financeservice/service/TransactionService.java --view expanded | sed -n '1,220p'

Repository: AET-DevOps26/team-devoops

Length of output: 2036


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== TeamRepository query lines ==\n'
nl -ba services/spring-finance/src/main/java/tum/devoops/financeservice/repository/TeamRepository.java | sed -n '1,120p'

printf '\n== TransactionService relevant lines ==\n'
nl -ba services/spring-finance/src/main/java/tum/devoops/financeservice/service/TransactionService.java | sed -n '180,260p'

printf '\n== TeamEntity relevant lines ==\n'
nl -ba services/spring-finance/src/main/java/tum/devoops/financeservice/entity/TeamEntity.java | sed -n '1,120p'

Repository: AET-DevOps26/team-devoops

Length of output: 237


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== TeamRepository relevant lines ==\n'
awk 'NR>=1 && NR<=80 {printf "%4d: %s\n", NR, $0}' \
  services/spring-finance/src/main/java/tum/devoops/financeservice/repository/TeamRepository.java

printf '\n== TransactionService relevant lines ==\n'
awk 'NR>=190 && NR<=245 {printf "%4d: %s\n", NR, $0}' \
  services/spring-finance/src/main/java/tum/devoops/financeservice/service/TransactionService.java

printf '\n== TeamEntity relevant lines ==\n'
awk 'NR>=1 && NR<=80 {printf "%4d: %s\n", NR, $0}' \
  services/spring-finance/src/main/java/tum/devoops/financeservice/entity/TeamEntity.java

Repository: AET-DevOps26/team-devoops

Length of output: 4622


Use a JOIN in the trainee repository queries

SELECT t.trainees is not valid JPQL for a collection-valued association. Both findTraineesBySportName and findTraineesByTeamId should join t.trainees and select the joined alias, otherwise the authorization flow in TransactionService can fail when these queries are parsed or executed.

🤖 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
`@services/spring-finance/src/main/java/tum/devoops/financeservice/entity/TeamEntity.java`
around lines 32 - 34, The JPQL in the trainee repository methods is selecting
the collection-valued association directly, which is invalid. Update both
findTraineesBySportName and findTraineesByTeamId to join TeamEntity.trainees
with an alias and select that joined TraineeEntity alias instead of using SELECT
t.trainees, so the queries parse and execute correctly for the
TransactionService flow.

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package tum.devoops.financeservice.entity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import jakarta.persistence.EmbeddedId;
import jakarta.persistence.Embeddable;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.io.Serializable;
import java.util.UUID;

@Entity
@Table(schema = "organization", name = "trainees")
@Getter
@NoArgsConstructor @AllArgsConstructor
public class TraineeEntity {
@EmbeddedId
private Id id;

@Embeddable
@Data @NoArgsConstructor @AllArgsConstructor
public static class Id implements Serializable {
@Column(name = "team_id", nullable = false)
private UUID teamId;

@Column(name = "member_id", nullable = false)
private UUID memberId;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package tum.devoops.financeservice.entity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import jakarta.persistence.EmbeddedId;
import jakarta.persistence.Embeddable;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.io.Serializable;
import java.util.UUID;

@Entity
@Table(schema = "organization", name = "trainers")
@Getter
public class TrainerEntity {
@EmbeddedId
private Id id;

@Embeddable
@Data @NoArgsConstructor @AllArgsConstructor
public static class Id implements Serializable {
@Column(name = "team_id", nullable = false)
private UUID teamId;

@Column(name = "member_id", nullable = false)
private UUID memberId;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ public class TransactionEntity {
@Column(name = "id", nullable = false, updatable = false)
private UUID id;

// FK to member.member(id) added in V3 migration.
// FK to member.members(id), added in the V2 migration.
@Column(name = "member_id", nullable = false)
private UUID memberId;

// UUID of the member who created this transaction.
// FK to member.member(id) added in V3 migration.
// FK to member.members(id), added in the V2 migration.
@Column(name = "creator_id", nullable = false)
private UUID creatorId;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package tum.devoops.financeservice.exception;

public class BadRequestException extends RuntimeException {
public BadRequestException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package tum.devoops.financeservice.exception;

public class ForbiddenException extends RuntimeException {
public ForbiddenException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package tum.devoops.financeservice.exception;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import tum.devoops.financeservice.model.BadRequestResponse;
import tum.devoops.financeservice.model.ErrorResponse;

@RestControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(NotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(NotFoundException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse().message(ex.getMessage()));
}

@ExceptionHandler(ForbiddenException.class)
public ResponseEntity<ErrorResponse> handleForbidden(ForbiddenException ex) {
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(new ErrorResponse().message(ex.getMessage()));
}

@ExceptionHandler(BadRequestException.class)
public ResponseEntity<BadRequestResponse> handleBadRequest(BadRequestException ex) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new BadRequestResponse().message(ex.getMessage()));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package tum.devoops.financeservice.exception;

public class NotFoundException extends RuntimeException {
public NotFoundException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package tum.devoops.financeservice.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import tum.devoops.financeservice.entity.DirectorEntity;

import java.util.List;
import java.util.UUID;

public interface DirectorRepository extends JpaRepository<DirectorEntity, DirectorEntity.Id> {
@Query("SELECT d.id.sportName FROM DirectorEntity d WHERE d.id.memberId = :memberId")
List<String> findSportNamesByMemberId(@Param("memberId") UUID memberId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package tum.devoops.financeservice.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import tum.devoops.financeservice.entity.MemberEntity;

import java.util.UUID;

public interface MemberRepository extends JpaRepository<MemberEntity, UUID> {
}
Loading
Loading