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
244 changes: 244 additions & 0 deletions logs/tech-cup.log

Large diffs are not rendered by default.

Empty file modified mvnw
100644 → 100755
Empty file.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package edu.eci.dosw.tech_cup.controller;

import edu.eci.dosw.tech_cup.exception.NotFoundException;
import edu.eci.dosw.tech_cup.model.TournamentModel;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
Expand Down Expand Up @@ -82,7 +83,7 @@ public ResponseEntity<List<TournamentModel>> getAllTournaments() {
public ResponseEntity<?> getTournament(@PathVariable Long id) {
try {
return ResponseEntity.ok(tournamentService.getTournament(id));
} catch (RuntimeException e) {
} catch (NotFoundException e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage());
}
}
Expand All @@ -92,7 +93,7 @@ public ResponseEntity<?> getTournament(@PathVariable Long id) {
*
* @param id unique tournament identifier
* @param tournament payload containing updated tournament data
* @return 200 with the updated tournament; 400 with an error message when the update is invalid
* @return 200 with the updated tournament; 404 when not found; 400 when the update is invalid
*/
@PutMapping("/{id}")
@Operation(summary = "Update tournament", description = "Updates information for an existing tournament")
Expand All @@ -101,6 +102,8 @@ public ResponseEntity<?> updateTournament(@PathVariable Long id,
try {
TournamentModel updated = tournamentService.updateTournament(id, tournament);
return ResponseEntity.ok(updated);
} catch (NotFoundException e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage());
} catch (RuntimeException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
}
Expand All @@ -110,14 +113,16 @@ public ResponseEntity<?> updateTournament(@PathVariable Long id,
* Cancels a tournament.
*
* @param id unique tournament identifier
* @return 204 No Content when cancellation succeeds; 400 with an error message when it fails
* @return 204 No Content when cancellation succeeds; 404 when not found; 400 when it cannot be cancelled
*/
@DeleteMapping("/{id}")
@Operation(summary = "Cancel tournament", description = "Cancels an existing tournament")
public ResponseEntity<?> cancelTournament(@PathVariable Long id) {
try {
tournamentService.cancelTournament(id);
return ResponseEntity.noContent().build();
} catch (NotFoundException e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage());
} catch (RuntimeException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
}
Expand All @@ -127,14 +132,16 @@ public ResponseEntity<?> cancelTournament(@PathVariable Long id) {
* Transitions a tournament to the started state.
*
* @param id unique tournament identifier
* @return 200 with the updated tournament when successful; 400 with an error message when it fails
* @return 200 with the updated tournament when successful; 404 when not found; 400 when it fails
*/
@PutMapping("/{id}/start")
@Operation(summary = "Start tournament", description = "Transitions the tournament to the started state")
public ResponseEntity<?> startTournament(@PathVariable Long id) {
try {
tournamentService.startTournament(id);
return ResponseEntity.ok("Tournament started");
} catch (NotFoundException e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage());
} catch (RuntimeException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
}
Expand All @@ -144,14 +151,16 @@ public ResponseEntity<?> startTournament(@PathVariable Long id) {
* Transitions a tournament to the finished state.
*
* @param id unique tournament identifier
* @return 200 with the updated tournament when successful; 400 with an error message when it fails
* @return 200 with the updated tournament when successful; 404 when not found; 400 when it fails
*/
@PutMapping("/{id}/finish")
@Operation(summary = "Finish tournament", description = "Transitions the tournament to the finished state")
public ResponseEntity<?> finishTournament(@PathVariable Long id) {
try {
tournamentService.finishTournament(id);
return ResponseEntity.ok("Tournament finished");
} catch (NotFoundException e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage());
} catch (RuntimeException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
}
Expand Down
31 changes: 29 additions & 2 deletions src/main/java/edu/eci/dosw/tech_cup/controller/UserController.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package edu.eci.dosw.tech_cup.controller;

import edu.eci.dosw.tech_cup.exception.NotFoundException;
import edu.eci.dosw.tech_cup.model.PlayerModel;
import edu.eci.dosw.tech_cup.model.UserRoleModel;
import edu.eci.dosw.tech_cup.service.IUserService;
Expand All @@ -13,6 +14,7 @@
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;
Expand Down Expand Up @@ -88,14 +90,16 @@
*
* @param id unique user identifier
* @param user payload containing updated user data
* @return 200 with the updated user; 400 with an error message when the update is invalid
* @return 200 with the updated user; 404 when user not found; 400 when update data is invalid
*/
@PutMapping("/{id}")
@Operation(summary = "Actualizar usuario", description = "Actualiza la informacion de un usuario existente")
public ResponseEntity<?> updateUser(@PathVariable Long id, @RequestBody PlayerModel user) {
try {
PlayerModel updated = userService.updateUser(id, user);
return ResponseEntity.ok(updated);
} catch (NotFoundException e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage());
} catch (RuntimeException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
}
Expand All @@ -113,8 +117,31 @@
try {
userService.deactivateUser(id);
return ResponseEntity.ok("User deactivated");
} catch (RuntimeException e) {
} catch (NotFoundException e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage());
}
}

/**
* Assigns a role to a user. Only administrators can call this endpoint.
*
* @param id unique identifier of the target user
* @param role new role to assign (PLAYER, ADMIN, ORGANIZER, REFEREE)
* @param adminUserId identifier of the administrator performing the operation
* @return 200 on success; 404 when a user is not found; 400 when the caller is not an admin
*/
@PutMapping("/{id}/role")
@Operation(summary = "Asignar rol", description = "Asigna un rol a un usuario. Solo los administradores pueden realizar esta operación.")
public ResponseEntity<?> assignRole(@PathVariable Long id,

Check failure on line 135 in src/main/java/edu/eci/dosw/tech_cup/controller/UserController.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove usage of generic wildcard type.

See more on https://sonarcloud.io/project/issues?id=CodeForge-DOSW_TECHCUP-FUTBOL-BackEnd-SpringBoot&issues=AZ3MHrkMP2HSqMZhG8WN&open=AZ3MHrkMP2HSqMZhG8WN&pullRequest=41
@RequestParam String role,
@RequestParam Long adminUserId) {
try {
userService.assignRole(id, role, adminUserId);
return ResponseEntity.ok("Role assigned successfully");
} catch (NotFoundException e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage());
} catch (RuntimeException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
}
}
}
20 changes: 20 additions & 0 deletions src/main/java/edu/eci/dosw/tech_cup/entity/UserEntity.java
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,12 @@ public class UserEntity {
@Column(name = "status", nullable = false)
private Boolean status = true;

/**
* Role of the user in the system (e.g. PLAYER, ADMIN, ORGANIZER, REFEREE).
*/
@Column(name = "role", length = 20)
private String role = "PLAYER";

/**
* Creates an empty user entity required by JPA.
*/
Expand Down Expand Up @@ -235,4 +241,18 @@ public UserEntity(String firstName, String lastName, String email,
* @param status new active flag
*/
public void setStatus(Boolean status) { this.status = status; }

/**
* Returns the user's role.
*
* @return role value
*/
public String getRole() { return role; }

/**
* Updates the user's role.
*
* @param role new role value
*/
public void setRole(String role) { this.role = role; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package edu.eci.dosw.tech_cup.exception;

/**
* Exception thrown when a requested resource does not exist in the system.
*
* <p>Controllers catch this exception and return {@code 404 Not Found} to the client,
* distinguishing resource-not-found errors from validation/business-rule errors
* ({@code 400 Bad Request}).</p>
*/
public class NotFoundException extends RuntimeException {

public NotFoundException(String message) {
super(message);
}
}
2 changes: 2 additions & 0 deletions src/main/java/edu/eci/dosw/tech_cup/mapper/UserMapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public interface UserMapper {

@Mapping(source = "userId", target = "id")
@Mapping(source = "passwordUser", target = "password")
@Mapping(source = "role", target = "role")
@Mapping(target = "name", ignore = true)
@Mapping(target = "available", ignore = true)
@Mapping(target = "preferredPositions", ignore = true)
Expand All @@ -35,5 +36,6 @@ public interface UserMapper {

@Mapping(source = "id", target = "userId")
@Mapping(source = "password", target = "passwordUser")
@Mapping(source = "role", target = "role")
UserEntity toEntity(PlayerModel model);
}
6 changes: 6 additions & 0 deletions src/main/java/edu/eci/dosw/tech_cup/model/UserRoleModel.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ public abstract class UserRoleModel {
/** Género */
protected String gender;

/** Rol del usuario en el sistema (e.g. PLAYER, ADMIN, ORGANIZER, REFEREE) */
protected String role;

// ===================== GETTERS & SETTERS =====================

public Long getId() { return id; }
Expand Down Expand Up @@ -77,6 +80,9 @@ public abstract class UserRoleModel {
public String getGender() { return gender; }
public void setGender(String gender) { this.gender = gender; }

public String getRole() { return role; }
public void setRole(String role) { this.role = role; }

// ===================== MÉTODOS DE DOMINIO =====================

/** Desactiva el usuario cambiando su estado a inactivo. */
Expand Down
11 changes: 11 additions & 0 deletions src/main/java/edu/eci/dosw/tech_cup/service/IUserService.java
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,15 @@ public interface IUserService {
* @throws RuntimeException si las credenciales son inválidas o la cuenta está inactiva
*/
void authenticate(String email, String password);

/**
* Asigna un rol a un usuario. Solo puede ser ejecutado por un administrador.
*
* @param targetUserId identificador del usuario al que se asignará el rol
* @param newRole nuevo rol a asignar (e.g. "PLAYER", "ADMIN", "ORGANIZER", "REFEREE")
* @param adminUserId identificador del usuario administrador que realiza la operación
* @throws edu.eci.dosw.tech_cup.exception.NotFoundException si alguno de los usuarios no existe
* @throws RuntimeException si el usuario solicitante no tiene rol de administrador
*/
void assignRole(Long targetUserId, String newRole, Long adminUserId);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package edu.eci.dosw.tech_cup.service;

import edu.eci.dosw.tech_cup.entity.TournamentEntity;
import edu.eci.dosw.tech_cup.exception.NotFoundException;
import edu.eci.dosw.tech_cup.mapper.TournamentMapper;
import edu.eci.dosw.tech_cup.model.TournamentModel;
import edu.eci.dosw.tech_cup.model.TournamentStatusModel;
Expand Down Expand Up @@ -75,7 +76,7 @@
@Override
public TournamentModel getTournament(Long id) {
TournamentEntity entity = tournamentRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Tournament not found"));
.orElseThrow(() -> new NotFoundException("Tournament not found"));

Check failure on line 79 in src/main/java/edu/eci/dosw/tech_cup/service/TournamentService.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "Tournament not found" 5 times.

See more on https://sonarcloud.io/project/issues?id=CodeForge-DOSW_TECHCUP-FUTBOL-BackEnd-SpringBoot&issues=AZ3MHrgMP2HSqMZhG8WH&open=AZ3MHrgMP2HSqMZhG8WH&pullRequest=41
return tournamentMapper.toModel(entity);
}

Expand All @@ -94,7 +95,7 @@
}

TournamentEntity existing = tournamentRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Tournament not found"));
.orElseThrow(() -> new NotFoundException("Tournament not found"));

TournamentStatusModel currentStatus =
TournamentStatusModel.valueOf(existing.getStatus().toUpperCase());
Expand Down Expand Up @@ -138,7 +139,7 @@
@Override
public void cancelTournament(Long id) {
TournamentEntity entity = tournamentRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Tournament not found"));
.orElseThrow(() -> new NotFoundException("Tournament not found"));

TournamentStatusModel status =
TournamentStatusModel.valueOf(entity.getStatus().toUpperCase());
Expand All @@ -153,7 +154,7 @@
@Override
public void startTournament(Long id) {
TournamentEntity entity = tournamentRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Tournament not found"));
.orElseThrow(() -> new NotFoundException("Tournament not found"));

TournamentStatusModel status =
TournamentStatusModel.valueOf(entity.getStatus().toUpperCase());
Expand All @@ -173,7 +174,7 @@
@Override
public void finishTournament(Long id) {
TournamentEntity entity = tournamentRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Tournament not found"));
.orElseThrow(() -> new NotFoundException("Tournament not found"));

TournamentStatusModel status =
TournamentStatusModel.valueOf(entity.getStatus().toUpperCase());
Expand Down
35 changes: 32 additions & 3 deletions src/main/java/edu/eci/dosw/tech_cup/service/UserService.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package edu.eci.dosw.tech_cup.service;

import edu.eci.dosw.tech_cup.entity.UserEntity;
import edu.eci.dosw.tech_cup.exception.NotFoundException;
import edu.eci.dosw.tech_cup.mapper.UserMapper;
import edu.eci.dosw.tech_cup.model.PlayerModel;
import edu.eci.dosw.tech_cup.model.UserRoleModel;
Expand Down Expand Up @@ -51,6 +52,8 @@
throw new RuntimeException("Email already exists");
}

user.setRole("PLAYER");

UserEntity entity = userMapper.toEntity(user);
entity.setStatus(true);

Expand All @@ -62,7 +65,7 @@
@Override
public PlayerModel getUser(Long id) {
UserEntity entity = userRepository.findById(id)
.orElseThrow(() -> new RuntimeException("User not found"));
.orElseThrow(() -> new NotFoundException("User not found"));

Check failure on line 68 in src/main/java/edu/eci/dosw/tech_cup/service/UserService.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "User not found" 3 times.

See more on https://sonarcloud.io/project/issues?id=CodeForge-DOSW_TECHCUP-FUTBOL-BackEnd-SpringBoot&issues=AZ3MHriqP2HSqMZhG8WL&open=AZ3MHriqP2HSqMZhG8WL&pullRequest=41
return userMapper.toModel(entity);
}

Expand All @@ -81,7 +84,7 @@
}

UserEntity existing = userRepository.findById(id)
.orElseThrow(() -> new RuntimeException("User not found"));
.orElseThrow(() -> new NotFoundException("User not found"));

if (updatedUser.getEmail() != null) {
if (updatedUser.getEmail().trim().isEmpty()) {
Expand Down Expand Up @@ -115,7 +118,7 @@
@Override
public void deactivateUser(Long id) {
UserEntity user = userRepository.findById(id)
.orElseThrow(() -> new RuntimeException("User not found"));
.orElseThrow(() -> new NotFoundException("User not found"));
user.setStatus(false);
userRepository.save(user);
log.info("User {} deactivated", id);
Expand All @@ -140,6 +143,32 @@
throw new RuntimeException("User account is inactive");
}

log.info("User {} authenticated successfully", email);

Check warning on line 146 in src/main/java/edu/eci/dosw/tech_cup/service/UserService.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Change this code to not log user-controlled data.

See more on https://sonarcloud.io/project/issues?id=CodeForge-DOSW_TECHCUP-FUTBOL-BackEnd-SpringBoot&issues=AZ3MHriqP2HSqMZhG8WM&open=AZ3MHriqP2HSqMZhG8WM&pullRequest=41
}

@Override
public void assignRole(Long targetUserId, String newRole, Long adminUserId) {
if (newRole == null || newRole.trim().isEmpty()) {
throw new RuntimeException("Role cannot be empty");

Check warning on line 152 in src/main/java/edu/eci/dosw/tech_cup/service/UserService.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace generic exceptions with specific library exceptions or a custom exception.

See more on https://sonarcloud.io/project/issues?id=CodeForge-DOSW_TECHCUP-FUTBOL-BackEnd-SpringBoot&issues=AZ3MHriqP2HSqMZhG8WI&open=AZ3MHriqP2HSqMZhG8WI&pullRequest=41
}
String normalizedRole = newRole.trim().toUpperCase();
if (!normalizedRole.equals("PLAYER") && !normalizedRole.equals("ADMIN")
&& !normalizedRole.equals("ORGANIZER") && !normalizedRole.equals("REFEREE")) {
throw new RuntimeException("Invalid role. Allowed values: PLAYER, ADMIN, ORGANIZER, REFEREE");

Check warning on line 157 in src/main/java/edu/eci/dosw/tech_cup/service/UserService.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace generic exceptions with specific library exceptions or a custom exception.

See more on https://sonarcloud.io/project/issues?id=CodeForge-DOSW_TECHCUP-FUTBOL-BackEnd-SpringBoot&issues=AZ3MHriqP2HSqMZhG8WJ&open=AZ3MHriqP2HSqMZhG8WJ&pullRequest=41
}

UserEntity admin = userRepository.findById(adminUserId)
.orElseThrow(() -> new NotFoundException("Admin user not found"));

if (!"ADMIN".equalsIgnoreCase(admin.getRole())) {
throw new RuntimeException("Only administrators can assign roles");

Check warning on line 164 in src/main/java/edu/eci/dosw/tech_cup/service/UserService.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace generic exceptions with specific library exceptions or a custom exception.

See more on https://sonarcloud.io/project/issues?id=CodeForge-DOSW_TECHCUP-FUTBOL-BackEnd-SpringBoot&issues=AZ3MHriqP2HSqMZhG8WK&open=AZ3MHriqP2HSqMZhG8WK&pullRequest=41
}

UserEntity target = userRepository.findById(targetUserId)
.orElseThrow(() -> new NotFoundException("Target user not found"));

target.setRole(normalizedRole);
userRepository.save(target);
log.info("Role {} assigned to user {} by admin {}", normalizedRole, targetUserId, adminUserId);
}
}
Loading