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
@@ -0,0 +1,10 @@
package io.backend.lined.common.exception;

import org.springframework.http.HttpStatus;

public class BadRequestException extends BaseAppException {

public BadRequestException(String message) {
super(HttpStatus.BAD_REQUEST, "common.bad_request", message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package io.backend.lined.common.exception;

import org.springframework.http.HttpStatus;

public class ForbiddenException extends BaseAppException {

public ForbiddenException(String message) {
super(HttpStatus.FORBIDDEN, "common.forbidden", message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public ResponseEntity<ProblemDetail> handleBase(BaseAppException ex) {
case NOT_FOUND -> "Resource not found";
case CONFLICT -> "Conflict";
case BAD_REQUEST -> "Bad request";
case FORBIDDEN -> "Forbidden";
default -> ex.getStatus().getReasonPhrase();
});
pd.setType(URI.create("https://errors.lined.app/" + ex.getCode()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,6 @@ void onCreate() {
if (createdAt == null) {
createdAt = OffsetDateTime.now();
}
if (startAt != null && endAt != null && !startAt.isBefore(endAt)) {
throw new IllegalArgumentException("startAt must be before endAt");
}
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package io.backend.lined.event.service;

import io.backend.lined.common.exception.BadRequestException;
import io.backend.lined.common.exception.ForbiddenException;
import io.backend.lined.common.exception.NotFoundException;
import io.backend.lined.event.api.EventConflictDto;
import io.backend.lined.event.api.EventCreateDto;
import io.backend.lined.event.api.EventDto;
Expand All @@ -21,9 +24,9 @@ public interface EventService {
* @param dto the event creation data
* @param currentUserId the ID of the user creating the event
* @return the created event as a DTO
* @throws IllegalArgumentException if startAt is not before endAt
* @throws SecurityException if the user is not a lobby member
* @throws NoSuchElementException if the lobby or user is not found
* @throws BadRequestException if startAt is not before endAt
* @throws ForbiddenException if the user is not a lobby member
* @throws NotFoundException if the lobby or user is not found
*/
EventDto create(EventCreateDto dto, Long currentUserId);

Expand All @@ -35,9 +38,9 @@ public interface EventService {
* @param dto the update data
* @param currentUserId the ID of the requesting user
* @return the updated event as a DTO
* @throws IllegalArgumentException if the updated dates are invalid
* @throws SecurityException if the user is not a lobby member
* @throws NoSuchElementException if the event is not found
* @throws BadRequestException if the updated dates are invalid
* @throws ForbiddenException if the user is not a lobby member
* @throws NotFoundException if the event is not found
*/
EventDto update(Long id, EventUpdateDto dto, Long currentUserId);

Expand All @@ -46,8 +49,8 @@ public interface EventService {
*
* @param id the event ID to delete
* @param currentUserId the ID of the requesting user
* @throws SecurityException if the user is not a lobby member
* @throws NoSuchElementException if the event is not found
* @throws ForbiddenException if the user is not a lobby member
* @throws NotFoundException if the event is not found
*/
void delete(Long id, Long currentUserId);

Expand All @@ -59,8 +62,8 @@ public interface EventService {
* @param to the end of the time window (exclusive)
* @param currentUserId the ID of the requesting user
* @return a list of events ordered by start time
* @throws IllegalArgumentException if the time window is invalid
* @throws SecurityException if the user is not a lobby member
* @throws BadRequestException if the time window is invalid
* @throws ForbiddenException if the user is not a lobby member
*/
List<EventDto> list(Long lobbyId, OffsetDateTime from, OffsetDateTime to, Long currentUserId);

Expand All @@ -72,8 +75,8 @@ public interface EventService {
* @param end the end of the time window
* @param requesterId the ID of the requesting user
* @return a list of conflicting event pairs with overlap bounds
* @throws IllegalArgumentException if start is not before end
* @throws SecurityException if the user is not a lobby member
* @throws BadRequestException if start is not before end
* @throws ForbiddenException if the user is not a lobby member
*/
List<EventConflictDto> findConflicts(Long lobbyId, OffsetDateTime start,
OffsetDateTime end, Long requesterId);
Expand All @@ -86,7 +89,7 @@ List<EventConflictDto> findConflicts(Long lobbyId, OffsetDateTime start,
* @param end the end of the time window
* @param requesterId the ID of the requesting user
* @return a conflict result with whether a conflict exists and which event causes it
* @throws IllegalArgumentException if start is not before end
* @throws BadRequestException if start is not before end
*/
UserConflictDto hasConflict(Long userId, OffsetDateTime start,
OffsetDateTime end, Long requesterId);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package io.backend.lined.event.service;

import io.backend.lined.common.EntityFinder;
import io.backend.lined.common.exception.BadRequestException;
import io.backend.lined.common.exception.NotFoundException;
import io.backend.lined.event.api.EventConflictDto;
import io.backend.lined.event.api.EventCreateDto;
import io.backend.lined.event.api.EventDto;
Expand All @@ -17,7 +20,6 @@
import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.NoSuchElementException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

Expand All @@ -39,7 +41,7 @@ public EventDto create(EventCreateDto dto, Long currentUserId) {
accessPolicy.ensureMember(lobby, currentUserId);

if (!dto.startAt().isBefore(dto.endAt())) {
throw new IllegalArgumentException("startAt must be before endAt");
throw new BadRequestException("startAt must be before endAt");
}

var entity = EventEntity.builder()
Expand Down Expand Up @@ -77,7 +79,7 @@ public EventDto update(Long id, EventUpdateDto dto, Long currentUserId) {
}

if (!e.getStartAt().isBefore(e.getEndAt())) {
throw new IllegalArgumentException("startAt must be before endAt");
throw new BadRequestException("startAt must be before endAt");
}

return mapper.toDto(e);
Expand All @@ -97,7 +99,7 @@ public List<EventDto> list(Long lobbyId, OffsetDateTime from, OffsetDateTime to,
accessPolicy.ensureMember(lobby, currentUserId);

if (from == null || to == null || !from.isBefore(to)) {
throw new IllegalArgumentException("Invalid time window: from < to is required");
throw new BadRequestException("Invalid time window: from < to is required");
}

return repo.findOverlapping(lobbyId, from, to).stream().map(mapper::toDto).toList();
Expand All @@ -110,7 +112,7 @@ public List<EventConflictDto> findConflicts(Long lobbyId,
var lobby = mustLobby(lobbyId);
accessPolicy.ensureMember(lobby, requesterId);
if (!start.isBefore(end)) {
throw new IllegalArgumentException("start must be before end");
throw new BadRequestException("start must be before end");
}
var events = repo.findOverlapping(lobbyId, start, end);
List<EventConflictDto> conflicts = new ArrayList<>();
Expand All @@ -135,7 +137,7 @@ public List<EventConflictDto> findConflicts(Long lobbyId,
public UserConflictDto hasConflict(Long userId, OffsetDateTime start,
OffsetDateTime end, Long requesterId) {
if (!start.isBefore(end)) {
throw new IllegalArgumentException("start must be before end");
throw new BadRequestException("start must be before end");
}
var overlapping = repo.findOverlappingByUser(userId, start, end);
if (overlapping.isEmpty()) {
Expand All @@ -145,18 +147,18 @@ public UserConflictDto hasConflict(Long userId, OffsetDateTime start,
}

private UserEntity mustUser(Long id) {
return userRepo.findById(id)
.orElseThrow(() -> new NoSuchElementException("User %d not found".formatted(id)));
return EntityFinder.findOrThrow(userRepo.findById(id),
() -> new NotFoundException("User %d not found".formatted(id)));
}

private LobbyEntity mustLobby(Long id) {
return lobbyRepo.findById(id)
.orElseThrow(() -> new NoSuchElementException("Lobby %d not found".formatted(id)));
return EntityFinder.findOrThrow(lobbyRepo.findById(id),
() -> new NotFoundException("Lobby %d not found".formatted(id)));
}

private EventEntity mustEvent(Long id) {
return repo.findById(id)
.orElseThrow(() -> new NoSuchElementException("Event %d not found".formatted(id)));
return EntityFinder.findOrThrow(repo.findById(id),
() -> new NotFoundException("Event %d not found".formatted(id)));
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.backend.lined.lobby.service;

import io.backend.lined.common.exception.ForbiddenException;
import io.backend.lined.lobby.domain.LobbyEntity;
import java.util.Objects;
import org.springframework.stereotype.Component;
Expand All @@ -13,14 +14,15 @@ public class LobbyAccessPolicy {
* are initialized (i.e. called within an active transaction).
*/
public void ensureMember(LobbyEntity lobby, Long userId) {
Objects.requireNonNull(lobby, "lobby must not be null");
Objects.requireNonNull(userId, "userId must not be null");
if (lobby.getOwner().getId().equals(userId)) {
return;
}
boolean isMember = lobby.getMembers().stream()
.anyMatch(u -> u.getId().equals(userId));
if (!isMember) {
throw new SecurityException("User is not a member of the lobby");
throw new ForbiddenException("User is not a member of the lobby");
}
}

Expand All @@ -29,9 +31,10 @@ public void ensureMember(LobbyEntity lobby, Long userId) {
* Callers must ensure {@code lobby.getOwner()} is initialized.
*/
public void ensureOwner(LobbyEntity lobby, Long requesterId) {
Objects.requireNonNull(lobby, "lobby must not be null");
Objects.requireNonNull(requesterId, "requesterId must not be null");
if (!lobby.getOwner().getId().equals(requesterId)) {
throw new SecurityException("Only lobby owner can perform this action");
throw new ForbiddenException("Only lobby owner can perform this action");
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package io.backend.lined.lobby.service;

import io.backend.lined.common.EntityFinder;
import io.backend.lined.common.exception.BadRequestException;
import io.backend.lined.common.exception.NotFoundException;
import io.backend.lined.lobby.api.LobbyCreateDto;
import io.backend.lined.lobby.api.LobbyDto;
import io.backend.lined.lobby.api.LobbyMapper;
Expand All @@ -9,7 +11,6 @@
import io.backend.lined.user.domain.UserRepository;
import jakarta.transaction.Transactional;
import java.util.List;
import java.util.NoSuchElementException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

Expand All @@ -25,8 +26,8 @@ public class LobbyServiceImpl implements LobbyService {

@Override
public LobbyDto create(LobbyCreateDto dto, Long ownerId) {
var owner = userRepo.findById(ownerId)
.orElseThrow(() -> new NoSuchElementException("Owner %d not found".formatted(ownerId)));
var owner = EntityFinder.findOrThrow(userRepo.findById(ownerId),
() -> new NotFoundException("Owner %d not found".formatted(ownerId)));

var entity = LobbyEntity.builder()
.name(dto.name())
Expand Down Expand Up @@ -58,8 +59,8 @@ public LobbyDto addMember(Long lobbyId, Long userIdToAdd, Long requesterId) {

accessPolicy.ensureOwner(lobby, requesterId);

var user = userRepo.findById(userIdToAdd)
.orElseThrow(() -> new NoSuchElementException("User %d not found".formatted(userIdToAdd)));
var user = EntityFinder.findOrThrow(userRepo.findById(userIdToAdd),
() -> new NotFoundException("User %d not found".formatted(userIdToAdd)));

lobby.getMembers().add(user);
return mapper.toDto(lobby);
Expand All @@ -72,7 +73,7 @@ public LobbyDto removeMember(Long lobbyId, Long userIdToRemove, Long requesterId
accessPolicy.ensureOwner(lobby, requesterId);

if (lobby.getOwner().getId().equals(userIdToRemove)) {
throw new IllegalArgumentException("Owner cannot be removed from lobby");
throw new BadRequestException("Owner cannot be removed from lobby");
}

lobby.getMembers().removeIf(u -> u.getId().equals(userIdToRemove));
Expand All @@ -89,7 +90,7 @@ public void delete(Long lobbyId, Long requesterId) {
private LobbyEntity mustLobby(Long id) {
return EntityFinder.findOrThrow(
lobbyRepo.findById(id),
() -> new NoSuchElementException("Lobby %d not found".formatted(id)));
() -> new NotFoundException("Lobby %d not found".formatted(id)));
}

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package io.backend.lined.task.service;


import io.backend.lined.common.exception.BadRequestException;
import io.backend.lined.common.exception.ForbiddenException;
import io.backend.lined.common.exception.NotFoundException;
import io.backend.lined.task.api.TaskCreateDto;
import io.backend.lined.task.api.TaskDto;
import io.backend.lined.task.api.TaskUpdateDto;
Expand All @@ -18,8 +20,8 @@
* @param dto the task creation data
* @param currentUserId the ID of the user creating the task
* @return the created task as a DTO
* @throws SecurityException if the user is not a lobby member
* @throws NoSuchElementException if the lobby or user is not found
* @throws ForbiddenException if the user is not a lobby member
* @throws NotFoundException if the lobby or user is not found
*/
TaskDto create(TaskCreateDto dto, Long currentUserId);

Expand All @@ -31,8 +33,8 @@
* @param dto the update data
* @param currentUserId the ID of the requesting user
* @return the updated task as a DTO
* @throws SecurityException if the user is not a lobby member
* @throws NoSuchElementException if the task is not found
* @throws ForbiddenException if the user is not a lobby member
* @throws NotFoundException if the task is not found
*/
TaskDto update(Long id, TaskUpdateDto dto, Long currentUserId);

Expand All @@ -41,8 +43,8 @@
*
* @param id the task ID to delete
* @param currentUserId the ID of the requesting user
* @throws SecurityException if the user is not a lobby member
* @throws NoSuchElementException if the task is not found
* @throws ForbiddenException if the user is not a lobby member
* @throws NotFoundException if the task is not found
*/
void delete(Long id, Long currentUserId);

Expand All @@ -52,8 +54,9 @@
*
* @param lobbyId the lobby ID to filter by, or null for all lobbies
* @param assigneeId the assignee user ID to filter by, or null for all assignees
* @param status the task status to filter by (e.g. "TO DO", "DONE"), or null for all statuses
* @param status the task status string to filter by (e.g. "TODO", "DONE"), or null for all

Check warning on line 57 in backend/lined/src/main/java/io/backend/lined/task/service/TaskService.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Complete the task associated to this TODO comment.

See more on https://sonarcloud.io/project/issues?id=Pan14ek_lined&issues=AZ6CVKUTxhoxqMM9Ke3I&open=AZ6CVKUTxhoxqMM9Ke3I&pullRequest=42
* @return a list of matching tasks
* @throws BadRequestException if {@code status} is not a valid {@link io.backend.lined.task.domain.TaskStatus} value
*/
List<TaskDto> list(Long lobbyId, Long assigneeId, String status);

Expand Down
Loading
Loading