Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Request to join events 6222 #6807

Merged
merged 21 commits into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from 7 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
97 changes: 97 additions & 0 deletions core/src/main/java/greencity/controller/EventController.java
Original file line number Diff line number Diff line change
Expand Up @@ -494,4 +494,101 @@ public ResponseEntity<Long> getAllAttendersCount(@RequestParam(name = "user-id")
public ResponseEntity<Long> getOrganizersCount(@RequestParam(name = "user-id") Long userId) {
return ResponseEntity.ok().body(eventService.getCountOfOrganizedEventsByUserId(userId));
}

/**
* Method for adding an event to requested by event id.
*
* @author Olha Pitsyk.
*/
@Operation(summary = "Add an event to requested")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = HttpStatuses.OK),
@ApiResponse(responseCode = "400", description = HttpStatuses.BAD_REQUEST),
@ApiResponse(responseCode = "401", description = HttpStatuses.UNAUTHORIZED),
@ApiResponse(responseCode = "404", description = HttpStatuses.NOT_FOUND)
})
@PostMapping("/addToRequested/{eventId}")
ChernenkoVitaliy marked this conversation as resolved.
Show resolved Hide resolved
public ResponseEntity<Object> addToRequested(@PathVariable Long eventId,
@Parameter(hidden = true) Principal principal) {
eventService.addToRequested(eventId, principal.getName());
return ResponseEntity.status(HttpStatus.OK).build();
}

/**
* Method for removing an event from requested by event id.
*
* @author Olha Pitsyk.
*/
@Operation(summary = "Remove an event from requested")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = HttpStatuses.OK),
@ApiResponse(responseCode = "400", description = HttpStatuses.BAD_REQUEST),
@ApiResponse(responseCode = "401", description = HttpStatuses.UNAUTHORIZED),
@ApiResponse(responseCode = "404", description = HttpStatuses.NOT_FOUND)
})
@DeleteMapping("/removeFromRequested/{eventId}")
ChernenkoVitaliy marked this conversation as resolved.
Show resolved Hide resolved
public ResponseEntity<Object> removeFromRequested(@PathVariable Long eventId,
@Parameter(hidden = true) Principal principal) {
eventService.removeFromRequested(eventId, principal.getName());
return ResponseEntity.status(HttpStatus.OK).build();
}

/**
* Method for getting all users who made request for joining the event.
*
* @author Olha Pitsyk.
*/
@Operation(summary = "List of requested users")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = HttpStatuses.OK),
@ApiResponse(responseCode = "400", description = HttpStatuses.BAD_REQUEST),
@ApiResponse(responseCode = "401", description = HttpStatuses.UNAUTHORIZED),
ChernenkoVitaliy marked this conversation as resolved.
Show resolved Hide resolved
@ApiResponse(responseCode = "404", description = HttpStatuses.NOT_FOUND)
})
@GetMapping("/{eventId}/requested-users")
public ResponseEntity<Object> getRequestedUsers(
@PathVariable Long eventId,
@Parameter(hidden = true) Principal principal,
@Parameter(hidden = true) Pageable pageable) {
return ResponseEntity.status(HttpStatus.OK)
.body(eventService.getRequestedUsers(eventId, principal.getName(), pageable));
}
holotsvan marked this conversation as resolved.
Show resolved Hide resolved

/**
* Method for approving join request.
*
* @author Olha Pitsyk.
*/
@Operation(summary = "Approve join request")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = HttpStatuses.OK),
@ApiResponse(responseCode = "400", description = HttpStatuses.BAD_REQUEST),
@ApiResponse(responseCode = "401", description = HttpStatuses.UNAUTHORIZED),
@ApiResponse(responseCode = "404", description = HttpStatuses.NOT_FOUND)
})
@PostMapping("/{eventId}/requested-users/{userId}/approve")
public ResponseEntity<Object> approveRequest(@PathVariable Long eventId, @PathVariable Long userId,
@Parameter(hidden = true) Principal principal) {
eventService.approveRequest(eventId, principal.getName(), userId);
return ResponseEntity.status(HttpStatus.OK).build();
}

/**
* Method for declining join request.
*
* @author Olha Pitsyk.
*/
@Operation(summary = "Decline join request")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = HttpStatuses.OK),
@ApiResponse(responseCode = "400", description = HttpStatuses.BAD_REQUEST),
@ApiResponse(responseCode = "401", description = HttpStatuses.UNAUTHORIZED),
@ApiResponse(responseCode = "404", description = HttpStatuses.NOT_FOUND)
})
@PostMapping("/{eventId}/requested-users/{userId}/decline")
public ResponseEntity<Object> declineRequest(@PathVariable Long eventId, @PathVariable Long userId,
@Parameter(hidden = true) Principal principal) {
eventService.declineRequest(eventId, principal.getName(), userId);
return ResponseEntity.status(HttpStatus.OK).build();
}
}
53 changes: 53 additions & 0 deletions core/src/test/java/greencity/controller/EventControllerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -703,4 +703,57 @@ private EventDto getEventDto() {
objectMapper.findAndRegisterModules();
return objectMapper.readValue(json, EventDto.class);
}

@Test
@SneakyThrows
void addToRequestedTest() {
Long eventId = 1L;
mockMvc.perform(post(EVENTS_CONTROLLER_LINK + "/addToRequested/{eventId}", eventId)
.principal(principal))
.andExpect(status().isOk());
verify(eventService).addToRequested(eventId, principal.getName());
}

@Test
@SneakyThrows
void removeFromRequestedTest() {
Long eventId = 1L;
mockMvc.perform(delete(EVENTS_CONTROLLER_LINK + "/removeFromRequested/{eventId}", eventId)
.principal(principal))
.andExpect(status().isOk());
verify(eventService).removeFromRequested(eventId, principal.getName());
}

@Test
@SneakyThrows
void getRequestedUsersTest() {
Long eventId = 1L;
Pageable pageable = PageRequest.of(0, 20);
mockMvc.perform(get(EVENTS_CONTROLLER_LINK + "/{eventId}/requested-users", eventId)
.principal(principal))
.andExpect(status().isOk());
verify(eventService).getRequestedUsers(eventId, principal.getName(), pageable);
}

@Test
@SneakyThrows
void approveRequest() {
Long eventId = 1L;
Long userId = 1L;
mockMvc.perform(post(EVENTS_CONTROLLER_LINK + "/{eventId}/requested-users/{userId}/approve", eventId, userId)
.principal(principal))
.andExpect(status().isOk());
verify(eventService).approveRequest(eventId, principal.getName(), userId);
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add security validation tests for approveRequest endpoint

Add test cases to verify:

  • Only event organizer can approve requests
  • Unauthorized access handling
  • Invalid user ID handling
 @Test
 @SneakyThrows
 void approveRequest() {
     Long eventId = 1L;
     Long userId = 1L;
     mockMvc.perform(post(EVENTS_CONTROLLER_LINK + "/{eventId}/requested-users/{userId}/approve", eventId, userId)
         .principal(principal))
         .andExpect(status().isOk());
     verify(eventService).approveRequest(eventId, principal.getName(), userId);
+    
+    // Test unauthorized access
+    doThrow(new UserHasNoPermissionToAccessException(ErrorMessage.USER_HAS_NO_PERMISSION))
+        .when(eventService).approveRequest(eventId, principal.getName(), userId);
+    mockMvc.perform(post(EVENTS_CONTROLLER_LINK + "/{eventId}/requested-users/{userId}/approve", eventId, userId)
+        .principal(principal))
+        .andExpect(status().isForbidden());
 }

Committable suggestion skipped: line range outside the PR's diff.


@Test
@SneakyThrows
void declineRequest() {
Long eventId = 1L;
Long userId = 1L;
mockMvc.perform(post(EVENTS_CONTROLLER_LINK + "/{eventId}/requested-users/{userId}/decline", eventId, userId)
.principal(principal))
.andExpect(status().isOk());
verify(eventService).declineRequest(eventId, principal.getName(), userId);
}
}
9 changes: 7 additions & 2 deletions dao/src/main/java/greencity/entity/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -190,10 +190,12 @@ u.id IN (:users)
@Table(name = "users")
@EqualsAndHashCode(
exclude = {"verifyEmail", "ownSecurity", "ecoNewsLiked", "refreshTokenKey", "estimates", "restorePasswordEmail",
"customToDoListItems", "eventOrganizerRating", "favoriteEcoNews", "favoriteEvents", "subscribedEvents"})
"customToDoListItems", "eventOrganizerRating", "favoriteEcoNews", "favoriteEvents", "requestedEvents",
"subscribedEvents"})
@ToString(
exclude = {"verifyEmail", "ownSecurity", "refreshTokenKey", "ecoNewsLiked", "estimates", "restorePasswordEmail",
"customToDoListItems", "eventOrganizerRating", "favoriteEcoNews", "favoriteEvents", "subscribedEvents"})
"customToDoListItems", "eventOrganizerRating", "favoriteEcoNews", "favoriteEvents", "requestedEvents",
"subscribedEvents"})
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Expand Down Expand Up @@ -317,4 +319,7 @@ public class User {
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
@Builder.Default
private Set<UserNotificationPreference> emailPreference = new HashSet<>();

@ManyToMany(mappedBy = "requesters", fetch = FetchType.LAZY)
private Set<Event> requestedEvents;
}
11 changes: 8 additions & 3 deletions dao/src/main/java/greencity/entity/event/Event.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@

@Entity
@Table(name = "events")
@EqualsAndHashCode(exclude = {"attenders", "followers", "dates"})
@EqualsAndHashCode(exclude = {"attenders", "followers", "requesters", "dates"})
@AllArgsConstructor
@NoArgsConstructor
@Getter
Expand Down Expand Up @@ -72,6 +72,12 @@ public class Event {
inverseJoinColumns = @JoinColumn(name = "user_id"))
private Set<User> followers = new HashSet<>();

@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(name = "events_requesters",
joinColumns = @JoinColumn(name = "event_id"),
inverseJoinColumns = @JoinColumn(name = "user_id"))
private Set<User> requesters = new HashSet<>();

@NonNull
@OrderBy("finishDate ASC")
@OneToMany(mappedBy = "event", cascade = CascadeType.ALL)
Expand All @@ -96,8 +102,7 @@ public class Event {
private List<EventGrade> eventGrades = new ArrayList<>();

@ManyToMany
@JoinTable(
name = "events_users_likes",
@JoinTable(name = "events_users_likes",
joinColumns = @JoinColumn(name = "event_id"),
inverseJoinColumns = @JoinColumn(name = "users_id"))
private Set<User> usersLikedEvents = new HashSet<>();
Expand Down
5 changes: 4 additions & 1 deletion dao/src/main/java/greencity/enums/NotificationType.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ public enum NotificationType {
HABIT_COMMENT_USER_TAG,
HABIT_LAST_DAY_OF_PRIMARY_DURATION,
PLACE_STATUS,
PLACE_ADDED;
PLACE_ADDED,
EVENT_REQUEST_ACCEPTED,
EVENT_REQUEST_DECLINED,
EVENT_INVITE;

private static final EnumSet<NotificationType> COMMENT_LIKE_TYPES = EnumSet.of(
ECONEWS_COMMENT_LIKE, EVENT_COMMENT_LIKE, HABIT_COMMENT_LIKE);
Expand Down
12 changes: 12 additions & 0 deletions dao/src/main/java/greencity/repository/UserRepo.java
Original file line number Diff line number Diff line change
Expand Up @@ -900,4 +900,16 @@ uep.emailPreference, uep.periodicity, COUNT(uep.id)
*/
@Query("SELECT COUNT(u) FROM User u WHERE u.userStatus IN (greencity.enums.UserStatus.ACTIVATED) ")
Long countActiveUsers();

/**
* Method for getting all users who made request for joining the event.
*
* @param eventId - id of the event
* @param pageable
*
*/
@Query(nativeQuery = true, value = "SELECT users.* FROM users "
+ "JOIN events_requesters ON users.id = events_requesters.user_id "
+ "WHERE events_requesters.event_id = :eventId")
Page<User> findUsersByRequestedEvents(Long eventId, Pageable pageable);
}
Original file line number Diff line number Diff line change
Expand Up @@ -260,4 +260,5 @@
<include file="/db/changelog/logs/ch-habits-add-created-at-column.xml"/>
<include file="db/changelog/logs/ch-change-user-profile-columns-type-Haliara.xml"/>
<include file="db/changelog/logs/ch-add-default-value-userprofile-confidentiality-Haliara.xml"/>
<include file="db/changelog/logs/ch-add-table-events-requesters-Pitsyk.xml"/>
</databaseChangeLog>
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">

<changeSet id="Pitsyk-1" author="Olha Pitsyk">
<createTable tableName="events_requesters">
<column name="event_id" type="BIGINT" >
<constraints nullable="false"/>
</column>
<column name="user_id" type="BIGINT">
<constraints nullable="false"/>
</column>
</createTable>

<addPrimaryKey tableName="events_requesters"
columnNames="event_id, user_id"/>
<addForeignKeyConstraint baseTableName="events_requesters"
baseColumnNames="event_id"
constraintName="fk_events_requesters_event_id"
referencedTableName="events"
referencedColumnNames="id"/>
<addForeignKeyConstraint baseTableName="events_requesters"
baseColumnNames="user_id"
constraintName="fk_events_requesters_user_id"
referencedTableName="users"
referencedColumnNames="id"/>
</changeSet>
</databaseChangeLog>
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,12 @@ public class EmailNotificationMessagesConstants {
public static final String FRIEND_REQUEST_RECEIVED_MESSAGE = "%s sent you a friend request";
public static final String FRIEND_REQUEST_ACCEPTED_SUBJECT = "Your friend request was accepted";
public static final String FRIEND_REQUEST_ACCEPTED_MESSAGE = "Now you are friends with %s";
public static final String JOIN_REQUEST_APPROVED_SUBJECT = "You have successfully joined the event";
public static final String JOIN_REQUEST_APPROVED_MESSAGE = "You have successfully joined %s";
public static final String JOIN_REQUEST_DECLINED_SUBJECT = "The organizer didn't approve your request";
public static final String JOIN_REQUEST_DECLINED_MESSAGE =
"While we can't confirm your attendance this time, there's another way to stay connected! Join our "
+ "organizer's friend list for future updates and opportunities.";
public static final String NEW_JOIN_REQUEST_SUBJECT = "New people want to join your event";
public static final String NEW_JOIN_REQUEST_MESSAGE = "%s wants to join your event";
}
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,11 @@ public class ErrorMessage {
public static final String USER_HAS_ALREADY_ADDED_EVENT_TO_FAVORITES =
"User has already added this event to favorites.";
public static final String EVENT_IS_NOT_IN_FAVORITES = "This event is not in favorites.";
public static final String USER_HAS_ALREADY_ADDED_EVENT_TO_REQUESTED =
"User has already added this event to requested.";
public static final String EVENT_IS_NOT_IN_REQUESTED = "This event is not in requested.";
public static final String USER_DID_NOT_REQUEST_FOR_EVENT = "User with this id did not request to join event: ";
public static final String EVENT_COMMENT_NOT_FOUND_BY_ID = "Event comment doesn't exist by this id: ";
public static final String EVENT_IS_FINISHED = "Finished event cannot be modified";
public static final String USER_HAS_NO_FRIEND_WITH_ID = "User has no friend with this id: ";
public static final String INVALID_DURATION = "The duration for such habit is lower than previously set";
Expand Down
45 changes: 45 additions & 0 deletions service-api/src/main/java/greencity/dto/user/UserForListDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package greencity.dto.user;

import greencity.constant.ServiceValidationConstants;
import greencity.enums.Role;
import greencity.enums.UserStatus;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.time.LocalDateTime;

@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
@EqualsAndHashCode
public class UserForListDto {
@NotNull
private Long id;

@NotBlank
@Size(
min = ServiceValidationConstants.USERNAME_MIN_LENGTH,
max = ServiceValidationConstants.USERNAME_MAX_LENGTH)
private String name;

private LocalDateTime dateOfRegistration;

@Email(message = ServiceValidationConstants.INVALID_EMAIL)
@NotBlank
private String email;

@NotNull
private UserStatus userStatus;

@NotNull
private Role role;

Comment on lines +38 to +43
Copy link

Choose a reason for hiding this comment

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

💡 Codebase verification

⚠️ Architectural and Consistency Issues in Enum Definitions

While the @NotNull validation is appropriate, I've identified some concerning issues in the codebase:

  • Duplicate enum definitions exist in both dao and service-api modules for UserStatus and Role
  • The Role enum values are inconsistent between modules:
    • service-api: includes ROLE_UBS_EMPLOYEE
    • dao: missing ROLE_UBS_EMPLOYEE

Consider:

  1. Consolidating enum definitions into a shared module to prevent duplication
  2. Ensuring consistent enum values across all modules to prevent potential runtime issues
🔗 Analysis chain

Verify enum completeness for UserStatus and Role

The validation looks good. Let's verify that all necessary status and role values are covered in the enums.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for UserStatus and Role enum definitions
echo "=== UserStatus enum values ==="
ast-grep --pattern 'enum UserStatus {
  $$$
}'

echo "=== Role enum values ==="
ast-grep --pattern 'enum Role {
  $$$
}'

Length of output: 221


Script:

#!/bin/bash
# First find relevant enum files
echo "=== Finding enum files ==="
fd -e java "UserStatus|Role"

echo -e "\n=== Searching for UserStatus enum ==="
rg "enum\s+UserStatus" -A 10

echo -e "\n=== Searching for Role enum ==="
rg "enum\s+Role" -A 10

Length of output: 2210


Script:

#!/bin/bash
# Check which Role enum is imported in UserForListDto
echo "=== Checking Role import in UserForListDto ==="
rg "import.*Role" service-api/src/main/java/greencity/dto/user/UserForListDto.java

Length of output: 216

private String userCredo;
}
Loading
Loading