diff --git a/.idea/data_source_mapping.xml b/.idea/data_source_mapping.xml deleted file mode 100644 index bb91f04..0000000 --- a/.idea/data_source_mapping.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/pom.xml b/pom.xml index 0bfbb1f..2e2e12c 100644 --- a/pom.xml +++ b/pom.xml @@ -98,7 +98,7 @@ io.github.og4dev og4dev-spring-response - 1.4.0 + 1.5.0 diff --git a/src/main/java/dev/pasinduog/eventsphere/config/ExceptionConfig.java b/src/main/java/dev/pasinduog/eventsphere/config/ExceptionConfig.java new file mode 100644 index 0000000..5d535bd --- /dev/null +++ b/src/main/java/dev/pasinduog/eventsphere/config/ExceptionConfig.java @@ -0,0 +1,20 @@ +package dev.pasinduog.eventsphere.config; + +import io.github.og4dev.exception.ApiExceptionRegistry; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.AuthenticationException; + +import java.sql.SQLException; + +@Configuration +public class ExceptionConfig { + + @Bean + public ApiExceptionRegistry apiExceptionRegistry() { + return new ApiExceptionRegistry() + .register(SQLException.class, HttpStatus.INTERNAL_SERVER_ERROR, "A Database error occurred") + .register(AuthenticationException.class, HttpStatus.UNAUTHORIZED, "Authentication required"); + } +} diff --git a/src/main/java/dev/pasinduog/eventsphere/controller/AuthController.java b/src/main/java/dev/pasinduog/eventsphere/controller/AuthController.java index 3e1fc41..7618a53 100644 --- a/src/main/java/dev/pasinduog/eventsphere/controller/AuthController.java +++ b/src/main/java/dev/pasinduog/eventsphere/controller/AuthController.java @@ -4,13 +4,13 @@ import dev.pasinduog.eventsphere.dto.LoginResponse; import dev.pasinduog.eventsphere.dto.OAuth2CallbackRequest; import dev.pasinduog.eventsphere.exception.InvalidAuthCodeException; +import dev.pasinduog.eventsphere.exception.InvalidLoginException; import dev.pasinduog.eventsphere.exception.UserNotFoundException; import dev.pasinduog.eventsphere.model.User; import dev.pasinduog.eventsphere.repository.UserRepository; import dev.pasinduog.eventsphere.service.JwtService; import dev.pasinduog.eventsphere.service.OAuth2CodeService; import lombok.RequiredArgsConstructor; -import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.web.bind.annotation.*; @@ -27,10 +27,10 @@ public class AuthController { @PostMapping("/login") public LoginResponse login(@RequestBody LoginRequest request) { User user = userRepository.findByEmail(request.email()) - .orElseThrow(() -> new UserNotFoundException("User not found")); + .orElseThrow(() -> new UserNotFoundException("We couldn't find an account with that email.")); if (!passwordEncoder.matches(request.password(), user.getPasswordHash())) { - throw new BadCredentialsException("Invalid credentials"); + throw new InvalidLoginException("The password you entered is incorrect. Please try again."); } String token = jwtService.generateToken(user); return new LoginResponse(token); diff --git a/src/main/java/dev/pasinduog/eventsphere/controller/EventController.java b/src/main/java/dev/pasinduog/eventsphere/controller/EventController.java index 386ab38..a4670fe 100644 --- a/src/main/java/dev/pasinduog/eventsphere/controller/EventController.java +++ b/src/main/java/dev/pasinduog/eventsphere/controller/EventController.java @@ -3,12 +3,15 @@ import dev.pasinduog.eventsphere.dto.AiMatchResult; import dev.pasinduog.eventsphere.dto.MatchSuggestionResponse; import dev.pasinduog.eventsphere.model.Event; +import dev.pasinduog.eventsphere.model.User; import dev.pasinduog.eventsphere.service.AiMatchmakingService; import dev.pasinduog.eventsphere.service.EventService; +import dev.pasinduog.eventsphere.service.UserService; import lombok.RequiredArgsConstructor; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; +import java.security.Principal; import java.util.List; @RestController @@ -17,6 +20,7 @@ public class EventController { private final EventService eventService; private final AiMatchmakingService aiMatchmakingService; + private final UserService userService; @GetMapping("/upcoming") List getUpcomingEvents() { @@ -25,25 +29,67 @@ List getUpcomingEvents() { @GetMapping("/{eventId}/matches") @PreAuthorize("isAuthenticated()") - List getMatchSuggestions(@PathVariable String eventId, @RequestParam String userId) { - return aiMatchmakingService.getMatchSuggestions(eventId, userId); + List getMatchSuggestions(@PathVariable String eventId, Principal principal) { + User currentUser = userService.getUserEntityByEmail(principal.getName()); + return aiMatchmakingService.getMatchSuggestions(eventId, currentUser.getId()); } - @PostMapping - @PreAuthorize("hasAuthority('ORGANIZER') or hasAuthority('ADMIN')") - boolean createEvent(@RequestBody Event event){ - return eventService.createEvent(event); - } @PostMapping("/{eventId}/register") @PreAuthorize("isAuthenticated()") - boolean registerEvent(@PathVariable String eventId, @RequestParam String userId){ - return eventService.registerUserForEvent(eventId, userId); + boolean registerForEvent(@PathVariable String eventId, Principal principal) { + User currentUser = userService.getUserEntityByEmail(principal.getName()); + return eventService.registerUserForEvent(eventId, currentUser.getId()); } @PostMapping("/{eventId}/matchmaking") @PreAuthorize("isAuthenticated()") - AiMatchResult generateNetworkingMatches(@PathVariable String eventId, @RequestParam String userId){ - return aiMatchmakingService.generateMatchesForUser(eventId, userId); + AiMatchResult generateNetworkingMatches(@PathVariable String eventId, Principal principal) { + User currentUser = userService.getUserEntityByEmail(principal.getName()); + return aiMatchmakingService.generateMatchesForUser(eventId, currentUser.getId()); + } + + @PostMapping + @PreAuthorize("hasAuthority('ORGANIZER') or hasAuthority('ADMIN')") + boolean createEvent(@RequestBody Event event, Principal principal) { + User currentUser = userService.getUserEntityByEmail(principal.getName()); + event.setOrganizerId(currentUser.getId()); + return eventService.createEvent(event); + } + + @PutMapping("/{eventId}") + @PreAuthorize("hasAuthority('ORGANIZER') or hasAuthority('ADMIN')") + boolean updateEvent(@PathVariable String eventId, @RequestBody Event event, Principal principal) { + return eventService.updateEvent(event, eventId, principal.getName()); + } + + @PutMapping("/{eventId}/cancel") + @PreAuthorize("hasAuthority('ADMIN')") + boolean cancelEvent(@PathVariable String eventId) { + return eventService.cancelEvent(eventId); + } + + @DeleteMapping("/{eventId}/delete") + @PreAuthorize("hasAuthority('ADMIN')") + boolean softDeleteEvent(@PathVariable String eventId) { + return eventService.softDelete(eventId); + } + + @DeleteMapping("/{eventId}") + @PreAuthorize("hasAuthority('ADMIN')") + boolean deleteEvent(@PathVariable String eventId) { + return eventService.delete(eventId); + } + + @GetMapping("/by-organizer-email") + @PreAuthorize("hasAuthority('ADMIN')") + List findEventsByOrganizerEmail(@RequestParam String email) { + return eventService.getEventsByOrganizerEmail(email); + } + + @GetMapping("/my-events") + @PreAuthorize("hasAuthority('ORGANIZER') or hasAuthority('ADMIN')") + List getMyEvents(Principal principal) { + return eventService.getEventsByOrganizerEmail(principal.getName()); } } diff --git a/src/main/java/dev/pasinduog/eventsphere/dto/RegisterRequest.java b/src/main/java/dev/pasinduog/eventsphere/dto/RegisterRequest.java index 8abc0db..dab7fbd 100644 --- a/src/main/java/dev/pasinduog/eventsphere/dto/RegisterRequest.java +++ b/src/main/java/dev/pasinduog/eventsphere/dto/RegisterRequest.java @@ -1,5 +1,10 @@ package dev.pasinduog.eventsphere.dto; +import io.github.og4dev.annotation.AutoTrim; +import io.github.og4dev.annotation.XssCheck; + +@AutoTrim +@XssCheck public record RegisterRequest( String fullName, String email, diff --git a/src/main/java/dev/pasinduog/eventsphere/exception/InvalidLoginException.java b/src/main/java/dev/pasinduog/eventsphere/exception/InvalidLoginException.java new file mode 100644 index 0000000..7594d5c --- /dev/null +++ b/src/main/java/dev/pasinduog/eventsphere/exception/InvalidLoginException.java @@ -0,0 +1,10 @@ +package dev.pasinduog.eventsphere.exception; +import io.github.og4dev.exception.ApiException; +import org.springframework.http.HttpStatus; + +public class InvalidLoginException extends ApiException { + + public InvalidLoginException(String customMessage) { + super(customMessage, HttpStatus.UNAUTHORIZED); + } +} \ No newline at end of file diff --git a/src/main/java/dev/pasinduog/eventsphere/exception/UserAlreadyExistsException.java b/src/main/java/dev/pasinduog/eventsphere/exception/UserAlreadyExistsException.java index a033407..4c3dd9b 100644 --- a/src/main/java/dev/pasinduog/eventsphere/exception/UserAlreadyExistsException.java +++ b/src/main/java/dev/pasinduog/eventsphere/exception/UserAlreadyExistsException.java @@ -4,7 +4,7 @@ import org.springframework.http.HttpStatus; public class UserAlreadyExistsException extends ApiException { - public UserAlreadyExistsException(String id) { - super("A user with ID '" + id + "' already exists", HttpStatus.CONFLICT); + public UserAlreadyExistsException(String message) { + super(message, HttpStatus.CONFLICT); } } diff --git a/src/main/java/dev/pasinduog/eventsphere/repository/EventRepository.java b/src/main/java/dev/pasinduog/eventsphere/repository/EventRepository.java index c03dd05..6bf6d6d 100644 --- a/src/main/java/dev/pasinduog/eventsphere/repository/EventRepository.java +++ b/src/main/java/dev/pasinduog/eventsphere/repository/EventRepository.java @@ -7,6 +7,11 @@ public interface EventRepository { boolean save(Event event); + boolean update(Event event); + boolean cancelEvent(String eventId); + boolean softDelete(String eventId); + boolean delete(String eventId); Optional findById(String id); + List findByOrganizerEmail(String email); List findUpcomingEvents(); } diff --git a/src/main/java/dev/pasinduog/eventsphere/repository/impl/EventRepositoryImpl.java b/src/main/java/dev/pasinduog/eventsphere/repository/impl/EventRepositoryImpl.java index fc5a531..126be8e 100644 --- a/src/main/java/dev/pasinduog/eventsphere/repository/impl/EventRepositoryImpl.java +++ b/src/main/java/dev/pasinduog/eventsphere/repository/impl/EventRepositoryImpl.java @@ -34,6 +34,7 @@ private RowMapper rowMapper() { @Override public boolean save(Event event) { try { + if (event.getOrganizerId() == null) return false; String sql = "INSERT INTO events (id, organizer_id, title, description, start_time, end_time, max_attendees, status) VALUES (?,?,?,?,?,?,?,?)"; return jdbcTemplate.update(sql, event.getId(), @@ -49,12 +50,58 @@ public boolean save(Event event) { } } + @Override + public boolean update(Event event) { + try { + if (event.getOrganizerId() == null) return false; + String sql = "UPDATE events SET title = ?, description = ?, start_time = ?, end_time = ?, " + + "max_attendees = ?, status = ? WHERE id = ?"; + return jdbcTemplate.update(sql, + event.getTitle(), + event.getDescription(), + event.getStartTime(), + event.getEndTime(), + event.getMaxAttendees(), + event.getStatus(), + event.getId()) > 0; + } catch (DuplicateKeyException e) { + throw new EventAlreadyExistsException(event.getTitle()); + } + } + + @Override + public boolean cancelEvent(String eventId) { + String sql = "UPDATE events SET status = 'CANCELLED' WHERE id = ?"; + return jdbcTemplate.update(sql, eventId) > 0; + } + + @Override + public boolean softDelete(String eventId) { + String sql = "UPDATE events SET status = 'UNAVAILABLE' WHERE id = ?"; + return jdbcTemplate.update(sql, eventId) > 0; + } + + @Override + public boolean delete(String eventId) { + String sql = "DELETE FROM events WHERE id = ?"; + return jdbcTemplate.update(sql, eventId) > 0; + } + @Override public Optional findById(String id) { - String sql = "SELECT id, organizer_id, title, description, start_time, end_time, max_attendees, status, created_at FROM events WHERE id = ?"; + String sql = "SELECT id, organizer_id, title, description, start_time, end_time, max_attendees, status, " + + "created_at FROM events WHERE id = ?"; return jdbcTemplate.query(sql, rowMapper(), id).stream().findFirst(); } + @Override + public List findByOrganizerEmail(String email) { + String sql = "SELECT e.* FROM events e " + + "INNER JOIN users u ON e.organizer_id = u.id " + + "WHERE u.email = ? AND e.status NOT IN ('CANCELLED', 'UNAVAILABLE')"; + return jdbcTemplate.query(sql, rowMapper(), email); + } + @Override public List findUpcomingEvents() { String sql = "SELECT id, organizer_id, title, description, start_time, end_time, max_attendees, status, created_at" + diff --git a/src/main/java/dev/pasinduog/eventsphere/service/EventService.java b/src/main/java/dev/pasinduog/eventsphere/service/EventService.java index 3d38a00..3025a3f 100644 --- a/src/main/java/dev/pasinduog/eventsphere/service/EventService.java +++ b/src/main/java/dev/pasinduog/eventsphere/service/EventService.java @@ -6,6 +6,11 @@ public interface EventService { boolean createEvent(Event event); + boolean updateEvent(Event event, String eventId, String email); + boolean cancelEvent(String eventId); + boolean softDelete(String eventId); + boolean delete(String eventId); boolean registerUserForEvent(String eventId, String userId); List getUpcomingEvents(); + List getEventsByOrganizerEmail(String email); } diff --git a/src/main/java/dev/pasinduog/eventsphere/service/impl/EventServiceImpl.java b/src/main/java/dev/pasinduog/eventsphere/service/impl/EventServiceImpl.java index 85df9c6..27eb773 100644 --- a/src/main/java/dev/pasinduog/eventsphere/service/impl/EventServiceImpl.java +++ b/src/main/java/dev/pasinduog/eventsphere/service/impl/EventServiceImpl.java @@ -2,23 +2,28 @@ import dev.pasinduog.eventsphere.exception.EventNotFoundException; import dev.pasinduog.eventsphere.exception.OutOfReachException; +import dev.pasinduog.eventsphere.exception.UserNotFoundException; import dev.pasinduog.eventsphere.model.Event; +import dev.pasinduog.eventsphere.model.User; import dev.pasinduog.eventsphere.repository.EventRegistrationRepository; import dev.pasinduog.eventsphere.repository.EventRepository; +import dev.pasinduog.eventsphere.repository.UserRepository; import dev.pasinduog.eventsphere.service.EventService; import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Repository; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Isolation; import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.UUID; -@Repository +@Service @RequiredArgsConstructor public class EventServiceImpl implements EventService { private final EventRepository eventRepository; private final EventRegistrationRepository eventRegistrationRepository; + private final UserRepository userRepository; @Override public boolean createEvent(Event event) { @@ -28,6 +33,41 @@ public boolean createEvent(Event event) { return eventRepository.save(event); } + @Override + public boolean updateEvent(Event event, String eventId, String email) { + Event existingEvent = eventRepository.findById(eventId) + .orElseThrow(() -> new EventNotFoundException("Update failed. Event not found.")); + User currentUser = userRepository.findByEmail(email) + .orElseThrow(() -> new UserNotFoundException("Update failed. User not found.")); + if (!existingEvent.getOrganizerId().equals(currentUser.getId()) && !currentUser.getRole().equals("ADMIN")) { + throw new AccessDeniedException("You are not allowed to update this event"); + } + event.setId(eventId); + event.setOrganizerId(existingEvent.getOrganizerId()); + return eventRepository.update(event); + } + + @Override + public boolean cancelEvent(String eventId) { + if (eventRepository.findById(eventId).isEmpty()) + throw new EventNotFoundException("Cancel failed. Event not found"); + return eventRepository.cancelEvent(eventId); + } + + @Override + public boolean softDelete(String eventId) { + if (eventRepository.findById(eventId).isEmpty()) + throw new EventNotFoundException("Remove failed. Event not found"); + return eventRepository.softDelete(eventId); + } + + @Override + public boolean delete(String eventId) { + if (eventRepository.findById(eventId).isEmpty()) + throw new EventNotFoundException("Delete failed. Event not found"); + return eventRepository.delete(eventId); + } + @Override @Transactional(isolation = Isolation.SERIALIZABLE) public boolean registerUserForEvent(String eventId, String userId) { @@ -44,4 +84,9 @@ public boolean registerUserForEvent(String eventId, String userId) { public List getUpcomingEvents() { return eventRepository.findUpcomingEvents(); } + + @Override + public List getEventsByOrganizerEmail(String email) { + return eventRepository.findByOrganizerEmail(email); + } } diff --git a/src/main/java/dev/pasinduog/eventsphere/service/impl/UserServiceImpl.java b/src/main/java/dev/pasinduog/eventsphere/service/impl/UserServiceImpl.java index 4b60aa9..5c8db28 100644 --- a/src/main/java/dev/pasinduog/eventsphere/service/impl/UserServiceImpl.java +++ b/src/main/java/dev/pasinduog/eventsphere/service/impl/UserServiceImpl.java @@ -23,7 +23,7 @@ public class UserServiceImpl implements UserService { @Override public boolean registerUser(RegisterRequest request) { if (userRepository.findByEmail(request.email()).isPresent()) { - throw new UserAlreadyExistsException("Email already exists"); + throw new UserAlreadyExistsException("A user with email '" + request.email() + "' already exists"); } User user = new User(); user.setId(UUID.randomUUID().toString()); diff --git a/src/main/resources/db/migration/V3__insert_sample_data.sql b/src/main/resources/db/migration/V3__insert_sample_data.sql index f1e7e15..32f01e5 100644 --- a/src/main/resources/db/migration/V3__insert_sample_data.sql +++ b/src/main/resources/db/migration/V3__insert_sample_data.sql @@ -2,12 +2,12 @@ -- Insert Sample Data for Testing -- 0.1. ROLE (3 Records) -INSERT INTO roles (name) VALUES ('ADMIN'), ('ATTENDEE'), ('SPEAKER'); +INSERT INTO roles (name) VALUES ('ADMIN'), ('ORGANIZER'), ('ATTENDEE'), ('SPEAKER'); -- 1. USERS (10 Records - Admins, Speakers, Attendees) INSERT INTO users (id, full_name, email, password_hash, role, skills_and_interests) VALUES ('886a8539-5426-4c95-b50c-c54d72e55d28', 'Pasindu Owa Gamage', 'admin1@gmail.com', '$2a$10$.g3Buxksd/EBNCE8UGoPA.1jCjrJJBspKoh3dfg7e5Tq2OIRF2emi', 'ADMIN', 'System Architecture, Spring Boot, Angular'), - ('eb334f13-7461-4d28-9e0e-7b933a5e5e4e', 'Kasun Perera', 'admin2@gmail.com', '$2a$10$.g3Buxksd/EBNCE8UGoPA.1jCjrJJBspKoh3dfg7e5Tq2OIRF2emi', 'ADMIN', 'Event Management, Marketing'), + ('eb334f13-7461-4d28-9e0e-7b933a5e5e4e', 'Kasun Perera', 'organizer2@gmail.com', '$2a$10$.g3Buxksd/EBNCE8UGoPA.1jCjrJJBspKoh3dfg7e5Tq2OIRF2emi', 'ORGANIZER', 'Event Management, Organizing'), ('1ea3c9fd-ad68-4105-8220-3a15694bc083', 'Dr. Ruwan Kumara', 'speaker1@gmail.com', '$2a$10$.g3Buxksd/EBNCE8UGoPA.1jCjrJJBspKoh3dfg7e5Tq2OIRF2emi', 'SPEAKER', 'AI, Machine Learning, Python'), ('94d5ca84-a17c-45ed-8506-cf4220f5fa80', 'Sarah Jenkins', 'speaker2@gmail.com', '$2a$10$.g3Buxksd/EBNCE8UGoPA.1jCjrJJBspKoh3dfg7e5Tq2OIRF2emi', 'SPEAKER', 'Cloud Computing, AWS, DevOps'), ('7743f6b1-d520-4a8f-b7a5-18e1d384e0f4', 'Nimali Silva', 'nimali@gmail.com', '$2a$10$.g3Buxksd/EBNCE8UGoPA.1jCjrJJBspKoh3dfg7e5Tq2OIRF2emi', 'ATTENDEE', 'Java, Spring Boot, Looking for Internships'), @@ -18,7 +18,7 @@ INSERT INTO users (id, full_name, email, password_hash, role, skills_and_interes ('5ff6e1d8-fa5c-4ff3-8e69-8d41ae1f728d', 'Tharindu Bandara', 'tharindu@gmail.com', '$2a$10$.g3Buxksd/EBNCE8UGoPA.1jCjrJJBspKoh3dfg7e5Tq2OIRF2emi', 'ATTENDEE', 'Spring Boot, Backend Development'); -- 0.2. EVENT STATUS (4 Records) -INSERT INTO event_statuses (name) VALUES ('UPCOMING'), ('LIVE'), ('COMPLETED'), ('UNAVAILABLE'); +INSERT INTO event_statuses (name) VALUES ('UPCOMING'), ('LIVE'), ('COMPLETED'), ('CANCELLED'), ('UNAVAILABLE'); -- 2. EVENTS (10 Records) INSERT INTO events (id, organizer_id, title, description, start_time, end_time, max_attendees, status) VALUES