Skip to content

Commit d07d71d

Browse files
authored
Merge pull request #390 from bounswe/feat/256-implement-mentor-endpoints
Feat/256 implement mentor endpoints
2 parents 734dae2 + 64ddc27 commit d07d71d

38 files changed

+1613
-3
lines changed

apps/jobboard-backend/pom.xml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,10 @@
5050
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
5151
<version>2.8.13</version>
5252
</dependency>
53-
53+
<dependency>
54+
<groupId>org.springframework.boot</groupId>
55+
<artifactId>spring-boot-starter-websocket</artifactId>
56+
</dependency>
5457
<dependency>
5558
<groupId>org.springframework.boot</groupId>
5659
<artifactId>spring-boot-starter-test</artifactId>

apps/jobboard-backend/src/main/java/org/bounswe/jobboardbackend/auth/security/WebSecurityConfig.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
7474
)
7575
.authorizeHttpRequests(auth -> auth
7676
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
77-
.requestMatchers("/api/auth/login/verify","/api/auth/login", "/api/auth/register", "/api/auth/verify-email", "/api/auth/password-reset/**", "/v3/api-docs/**", "/swagger-ui/**", "/error").permitAll()
77+
.requestMatchers("/api/auth/login/verify","/api/auth/login", "/api/auth/register", "/api/auth/verify-email", "/api/auth/password-reset/**", "/v3/api-docs/**", "/swagger-ui/**", "/error","/ws-chat/**").permitAll()
7878
.anyRequest().permitAll()
7979
)
8080

apps/jobboard-backend/src/main/java/org/bounswe/jobboardbackend/exception/ErrorCode.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,22 @@ public enum ErrorCode {
6868

6969
JOB_POST_NOT_FOUND(HttpStatus.NOT_FOUND),
7070
JOB_POST_FORBIDDEN(HttpStatus.FORBIDDEN),
71-
JOB_POST_CORRUPT(HttpStatus.INTERNAL_SERVER_ERROR);
71+
JOB_POST_CORRUPT(HttpStatus.INTERNAL_SERVER_ERROR),
72+
73+
CHAT_NOT_LINKED_TO_REVIEW(HttpStatus.CONFLICT),
74+
CONVERSATION_NOT_FOUND(HttpStatus.NOT_FOUND),
75+
MENTORSHIP_NOT_ACTIVE(HttpStatus.CONFLICT),
76+
RESUME_REVIEW_NOT_FOUND(HttpStatus.NOT_FOUND),
77+
REQUEST_NOT_FOUND(HttpStatus.NOT_FOUND),
78+
REVIEW_NOT_COMPLETED(HttpStatus.CONFLICT),
79+
UNAUTHORIZED_REVIEW_ACCESS(HttpStatus.FORBIDDEN),
80+
REVIEW_NOT_FOUND(HttpStatus.NOT_FOUND),
81+
REQUEST_ALREADY_PROCESSED(HttpStatus.CONFLICT),
82+
MENTOR_UNAVAILABLE(HttpStatus.CONFLICT),
83+
ACTIVE_MENTORSHIP_EXIST(HttpStatus.CONFLICT),
84+
MENTEE_CAPACITY_CONFLICT(HttpStatus.CONFLICT),
85+
MENTOR_PROFILE_NOT_FOUND(HttpStatus.NOT_FOUND),
86+
MENTOR_PROFILE_ALREADY_EXISTS(HttpStatus.CONFLICT);
7287

7388

7489
public final HttpStatus status;
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package org.bounswe.jobboardbackend.mentorship.config;
2+
3+
import lombok.RequiredArgsConstructor;
4+
import org.bounswe.jobboardbackend.auth.security.JwtUtils;
5+
import org.springframework.messaging.Message;
6+
import org.springframework.messaging.MessageChannel;
7+
import org.springframework.messaging.simp.stomp.StompCommand;
8+
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
9+
import org.springframework.messaging.support.ChannelInterceptor;
10+
import org.springframework.messaging.support.MessageHeaderAccessor;
11+
import org.springframework.security.core.Authentication;
12+
import org.springframework.security.core.userdetails.UserDetailsService;
13+
import org.springframework.stereotype.Component;
14+
15+
import java.util.List;
16+
17+
@Component
18+
@RequiredArgsConstructor
19+
public class JwtChannelInterceptor implements ChannelInterceptor {
20+
21+
private final UserDetailsService uds;
22+
private final JwtUtils jwtUtils;
23+
24+
25+
26+
@Override
27+
public Message<?> preSend(Message<?> message, MessageChannel channel) {
28+
StompHeaderAccessor accessor =
29+
MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
30+
31+
if (accessor != null && StompCommand.CONNECT.equals(accessor.getCommand())) {
32+
List<String> auth = accessor.getNativeHeader("Authorization");
33+
if (auth != null && !auth.isEmpty()) {
34+
String raw = auth.get(0);
35+
if (raw.startsWith("Bearer ")) {
36+
String jwt = raw.substring(7);
37+
38+
Authentication authentication =
39+
jwtUtils.buildAuthenticationFromAccessToken(jwt, uds);
40+
41+
accessor.setUser(authentication);
42+
}
43+
}
44+
}
45+
return message;
46+
}
47+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package org.bounswe.jobboardbackend.mentorship.config;
2+
3+
import lombok.RequiredArgsConstructor;
4+
import org.springframework.context.annotation.Configuration;
5+
import org.springframework.messaging.simp.config.ChannelRegistration;
6+
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
7+
import org.springframework.messaging.support.ChannelInterceptor;
8+
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
9+
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
10+
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
11+
12+
@Configuration
13+
@EnableWebSocketMessageBroker
14+
@RequiredArgsConstructor
15+
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
16+
17+
private final ChannelInterceptor jwtChannelInterceptor;
18+
19+
@Override
20+
public void configureClientInboundChannel(ChannelRegistration registration) {
21+
registration.interceptors(jwtChannelInterceptor);
22+
}
23+
24+
@Override
25+
public void configureMessageBroker(MessageBrokerRegistry registry) {
26+
registry.enableSimpleBroker("/topic");
27+
registry.setApplicationDestinationPrefixes("/app");
28+
}
29+
30+
@Override
31+
public void registerStompEndpoints(StompEndpointRegistry registry) {
32+
registry.addEndpoint("/ws-chat")
33+
.setAllowedOriginPatterns("*")
34+
.withSockJS();
35+
}
36+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package org.bounswe.jobboardbackend.mentorship.controller;
2+
3+
import lombok.RequiredArgsConstructor;
4+
import org.bounswe.jobboardbackend.auth.service.UserDetailsImpl;
5+
import org.bounswe.jobboardbackend.mentorship.dto.ChatMessageDTO;
6+
import org.bounswe.jobboardbackend.mentorship.dto.CreateMessageDTO;
7+
import org.bounswe.jobboardbackend.mentorship.service.ChatService;
8+
import org.springframework.http.ResponseEntity;
9+
import org.springframework.messaging.handler.annotation.DestinationVariable;
10+
import org.springframework.messaging.handler.annotation.MessageMapping;
11+
import org.springframework.messaging.handler.annotation.Payload;
12+
import org.springframework.messaging.handler.annotation.SendTo;
13+
import org.springframework.security.core.Authentication;
14+
import org.springframework.stereotype.Controller;
15+
import org.springframework.web.bind.annotation.GetMapping;
16+
import org.springframework.web.bind.annotation.PathVariable;
17+
import org.springframework.web.bind.annotation.RestController;
18+
19+
import java.util.List;
20+
21+
@Controller
22+
@RestController
23+
@RequiredArgsConstructor
24+
public class ChatController {
25+
26+
private final ChatService chatService;
27+
28+
@MessageMapping("/chat.sendMessage/{conversationId}")
29+
@SendTo("/topic/conversation/{conversationId}")
30+
public ChatMessageDTO sendMessage(
31+
@DestinationVariable Long conversationId,
32+
@Payload CreateMessageDTO createMessageDTO,
33+
Authentication auth
34+
) {
35+
UserDetailsImpl userDetails = (UserDetailsImpl) auth.getPrincipal();
36+
return chatService.saveAndBroadcastMessage(conversationId, createMessageDTO, userDetails.getId());
37+
}
38+
39+
@GetMapping("/api/chat/history/{conversationId}")
40+
public ResponseEntity<List<ChatMessageDTO>> getChatHistory(
41+
@PathVariable Long conversationId,
42+
Authentication auth
43+
) {
44+
45+
UserDetailsImpl userDetails = (UserDetailsImpl) auth.getPrincipal();
46+
47+
List<ChatMessageDTO> history = chatService.getMessageHistory(conversationId, userDetails.getId());
48+
return ResponseEntity.ok(history);
49+
}
50+
}
51+
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
package org.bounswe.jobboardbackend.mentorship.controller;
2+
3+
import lombok.RequiredArgsConstructor;
4+
import org.bounswe.jobboardbackend.auth.service.UserDetailsImpl;
5+
import org.bounswe.jobboardbackend.mentorship.dto.*;
6+
import org.bounswe.jobboardbackend.mentorship.service.MentorshipService;
7+
import jakarta.validation.Valid;
8+
import org.springframework.http.HttpStatus;
9+
import org.springframework.http.ResponseEntity;
10+
import org.springframework.security.access.prepost.PreAuthorize;
11+
import org.springframework.security.core.Authentication;
12+
import org.springframework.web.bind.annotation.*;
13+
14+
import java.util.List;
15+
16+
@RequiredArgsConstructor
17+
@RestController
18+
@RequestMapping("/api/mentorship")
19+
@PreAuthorize("isAuthenticated()")
20+
public class MentorshipController {
21+
22+
private final MentorshipService mentorshipService;
23+
24+
@GetMapping
25+
public ResponseEntity<List<MentorProfileDetailDTO>> searchMentors() {
26+
List<MentorProfileDetailDTO> mentors = mentorshipService.searchMentors();
27+
return ResponseEntity.ok(mentors);
28+
}
29+
30+
@PostMapping("/mentor")
31+
public ResponseEntity<MentorProfileDTO> createMentorProfile(
32+
@Valid @RequestBody CreateMentorProfileDTO createDTO,
33+
Authentication auth
34+
) {
35+
UserDetailsImpl userDetails = (UserDetailsImpl) auth.getPrincipal();
36+
MentorProfileDTO newProfile = mentorshipService.createMentorProfile(userDetails.getId(), createDTO);
37+
return new ResponseEntity<>(newProfile, HttpStatus.CREATED);
38+
}
39+
40+
@PutMapping("/mentor/{userId}")
41+
public ResponseEntity<MentorProfileDTO> updateMentorProfile(
42+
@Valid @RequestBody UpdateMentorProfileDTO updateDTO,
43+
@PathVariable Long userId
44+
) {
45+
MentorProfileDTO newProfile = mentorshipService.updateMentorProfile(userId, updateDTO);
46+
return new ResponseEntity<>(newProfile, HttpStatus.OK);
47+
}
48+
49+
@GetMapping("/mentor/{userId}")
50+
public ResponseEntity<MentorProfileDetailDTO> getMentorProfile(
51+
@PathVariable Long userId
52+
) {
53+
MentorProfileDetailDTO profile = mentorshipService.getMentorProfile(userId);
54+
return new ResponseEntity<>(profile, HttpStatus.OK);
55+
}
56+
57+
@DeleteMapping("/mentor/{userId}")
58+
public ResponseEntity<MentorProfileDTO> deleteMentorProfile(
59+
@PathVariable Long userId
60+
) {
61+
mentorshipService.deleteMentorProfile(userId);
62+
return ResponseEntity.ok().build();
63+
}
64+
65+
@PatchMapping("/review/{resumeReviewId}/complete")
66+
public ResponseEntity<Void> completeMentorship(
67+
@PathVariable Long resumeReviewId,
68+
Authentication auth
69+
) {
70+
mentorshipService.completeMentorship(resumeReviewId, auth);
71+
return ResponseEntity.ok().build();
72+
}
73+
74+
75+
@PatchMapping("/review/{resumeReviewId}/close")
76+
public ResponseEntity<Void> closeMentorship(
77+
@PathVariable Long resumeReviewId,
78+
Authentication auth
79+
) {
80+
mentorshipService.closeMentorship(resumeReviewId, auth);
81+
return ResponseEntity.ok().build();
82+
}
83+
84+
85+
@PostMapping("/requests")
86+
public ResponseEntity<MentorshipRequestDTO> createMentorshipRequest(
87+
@Valid @RequestBody CreateMentorshipRequestDTO requestDTO,
88+
Authentication auth
89+
) {
90+
UserDetailsImpl userDetails = (UserDetailsImpl) auth.getPrincipal();
91+
MentorshipRequestDTO newRequest = mentorshipService.createMentorshipRequest(requestDTO, userDetails.getId());
92+
return new ResponseEntity<>(newRequest, HttpStatus.CREATED);
93+
}
94+
95+
@GetMapping("mentor/{mentorId}/requests")
96+
public ResponseEntity<List<MentorshipRequestDTO>> getMentorshipRequestOfMentor(
97+
@PathVariable Long mentorId,
98+
Authentication auth
99+
) {
100+
UserDetailsImpl userDetails = (UserDetailsImpl) auth.getPrincipal();
101+
List<MentorshipRequestDTO> requests = mentorshipService.getMentorshipRequestsOfMentor(mentorId, userDetails.getId());
102+
return ResponseEntity.ok(requests);
103+
}
104+
105+
@GetMapping("/mentee/{menteeId}/requests")
106+
public ResponseEntity<List<MentorshipDetailsDTO>> getMyMentorshipDetails(
107+
@PathVariable Long menteeId
108+
) {
109+
List<MentorshipDetailsDTO> details = mentorshipService.getMentorshipDetailsForMentee(menteeId, menteeId);
110+
return ResponseEntity.ok(details);
111+
}
112+
113+
114+
@GetMapping("/requests/{requestId}")
115+
public ResponseEntity<MentorshipRequestDTO> getMentorshipRequest(
116+
@PathVariable Long requestId,
117+
Authentication auth
118+
) {
119+
UserDetailsImpl userDetails = (UserDetailsImpl) auth.getPrincipal();
120+
MentorshipRequestDTO request = mentorshipService.getMentorshipRequest(requestId, userDetails.getId());
121+
return new ResponseEntity<>(request, HttpStatus.OK);
122+
}
123+
124+
125+
@PatchMapping("/requests/{requestId}/respond")
126+
public ResponseEntity<MentorshipRequestDTO> respondToMentorshipRequest(
127+
@PathVariable Long requestId,
128+
@Valid @RequestBody RespondToRequestDTO responseDTO,
129+
Authentication auth
130+
) {
131+
UserDetailsImpl userDetails = (UserDetailsImpl) auth.getPrincipal();
132+
MentorshipRequestDTO updatedRequest = mentorshipService.respondToMentorshipRequest(
133+
requestId,
134+
responseDTO.accept(),
135+
userDetails.getId()
136+
);
137+
return new ResponseEntity<>(updatedRequest, HttpStatus.OK);
138+
}
139+
140+
141+
@PostMapping("/ratings")
142+
public ResponseEntity<Void> rateMentor(
143+
@Valid @RequestBody CreateRatingDTO ratingDTO,
144+
Authentication auth
145+
) {
146+
UserDetailsImpl jobSeeker = (UserDetailsImpl) auth.getPrincipal();
147+
mentorshipService.rateMentor(ratingDTO, jobSeeker.getId());
148+
149+
return new ResponseEntity<>(HttpStatus.CREATED);
150+
}
151+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package org.bounswe.jobboardbackend.mentorship.dto;
2+
3+
import java.time.LocalDateTime;
4+
5+
6+
public record ChatMessageDTO(
7+
String id,
8+
String conversationId,
9+
String senderId,
10+
String senderUsername,
11+
String content,
12+
LocalDateTime timestamp
13+
) {}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package org.bounswe.jobboardbackend.mentorship.dto;
2+
3+
4+
import jakarta.validation.constraints.Min;
5+
import jakarta.validation.constraints.NotEmpty;
6+
import java.util.List;
7+
8+
public record CreateMentorProfileDTO(
9+
@NotEmpty(message = "Expertise list cannot be empty")
10+
List<String> expertise,
11+
12+
@Min(value = 1, message = "You must be willing to mentor at least 1 person")
13+
int maxMentees
14+
) {}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package org.bounswe.jobboardbackend.mentorship.dto;
2+
3+
4+
5+
import jakarta.validation.constraints.NotNull;
6+
7+
public record CreateMentorshipRequestDTO(
8+
@NotNull Long mentorId
9+
) {}

0 commit comments

Comments
 (0)