1 | package edu.ucsb.cs156.frontiers.controllers; | |
2 | ||
3 | import com.fasterxml.jackson.core.JsonProcessingException; | |
4 | import com.fasterxml.jackson.databind.JsonNode; | |
5 | import edu.ucsb.cs156.frontiers.entities.Course; | |
6 | import edu.ucsb.cs156.frontiers.entities.CourseStaff; | |
7 | import edu.ucsb.cs156.frontiers.entities.RosterStudent; | |
8 | import edu.ucsb.cs156.frontiers.enums.OrgStatus; | |
9 | import edu.ucsb.cs156.frontiers.repositories.CourseRepository; | |
10 | import edu.ucsb.cs156.frontiers.repositories.CourseStaffRepository; | |
11 | import edu.ucsb.cs156.frontiers.repositories.RosterStudentRepository; | |
12 | import edu.ucsb.cs156.frontiers.utilities.WebhookSecurityUtils; | |
13 | import io.swagger.v3.oas.annotations.tags.Tag; | |
14 | import java.security.InvalidKeyException; | |
15 | import java.security.NoSuchAlgorithmException; | |
16 | import java.util.Optional; | |
17 | import lombok.extern.slf4j.Slf4j; | |
18 | import org.springframework.beans.factory.annotation.Value; | |
19 | import org.springframework.http.ResponseEntity; | |
20 | import org.springframework.web.bind.annotation.PostMapping; | |
21 | import org.springframework.web.bind.annotation.RequestBody; | |
22 | import org.springframework.web.bind.annotation.RequestHeader; | |
23 | import org.springframework.web.bind.annotation.RequestMapping; | |
24 | import org.springframework.web.bind.annotation.RestController; | |
25 | ||
26 | @Tag(name = "Webhooks Controller") | |
27 | @RestController | |
28 | @RequestMapping("/api/webhooks") | |
29 | @Slf4j | |
30 | public class WebhookController { | |
31 | ||
32 | private final CourseRepository courseRepository; | |
33 | private final RosterStudentRepository rosterStudentRepository; | |
34 | private final CourseStaffRepository courseStaffRepository; | |
35 | ||
36 | @Value("${app.webhook.secret}") | |
37 | private String webhookSecret; | |
38 | ||
39 | public WebhookController( | |
40 | CourseRepository courseRepository, | |
41 | RosterStudentRepository rosterStudentRepository, | |
42 | CourseStaffRepository courseStaffRepository) { | |
43 | this.courseRepository = courseRepository; | |
44 | this.rosterStudentRepository = rosterStudentRepository; | |
45 | this.courseStaffRepository = courseStaffRepository; | |
46 | } | |
47 | ||
48 | /** | |
49 | * Accepts webhooks from GitHub, currently to update the membership status of a RosterStudent. | |
50 | * | |
51 | * @param jsonBody body of the webhook. The description of the currently used webhook is available | |
52 | * in docs/webhooks.md | |
53 | * @param signature the GitHub webhook signature header for security validation | |
54 | * @return either the word success so GitHub will not flag the webhook as a failure, or the | |
55 | * updated RosterStudent | |
56 | */ | |
57 | @PostMapping("/github") | |
58 | public ResponseEntity<String> createGitHubWebhook( | |
59 | @RequestBody String requestBody, | |
60 | @RequestHeader(value = "X-Hub-Signature-256", required = false) String signature) | |
61 | throws JsonProcessingException, NoSuchAlgorithmException, InvalidKeyException { | |
62 | ||
63 | // Validate webhook signature | |
64 |
1
1. createGitHubWebhook : negated conditional → KILLED |
if (!WebhookSecurityUtils.validateGitHubSignature(requestBody, signature, webhookSecret)) { |
65 | log.error("Webhook signature validation failed"); | |
66 |
1
1. createGitHubWebhook : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/WebhookController::createGitHubWebhook → KILLED |
return ResponseEntity.status(401).body("Unauthorized: Invalid signature"); |
67 | } | |
68 | ||
69 | // Parse JSON after signature validation | |
70 | JsonNode jsonBody; | |
71 | try { | |
72 | jsonBody = new com.fasterxml.jackson.databind.ObjectMapper().readTree(requestBody); | |
73 | } catch (JsonProcessingException e) { | |
74 | log.error("Failed to parse webhook JSON body", e); | |
75 |
1
1. createGitHubWebhook : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/WebhookController::createGitHubWebhook → KILLED |
return ResponseEntity.badRequest().body("Invalid JSON"); |
76 | } | |
77 | ||
78 | log.info("Received GitHub webhook: {}", jsonBody.toString()); | |
79 | ||
80 |
1
1. createGitHubWebhook : negated conditional → KILLED |
if (!jsonBody.has("action")) { |
81 |
1
1. createGitHubWebhook : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/WebhookController::createGitHubWebhook → KILLED |
return ResponseEntity.ok().body("success"); |
82 | } | |
83 | ||
84 | String action = jsonBody.get("action").asText(); | |
85 | log.info("Webhook action: {}", action); | |
86 | ||
87 | // Early return if not an action we care about | |
88 |
2
1. createGitHubWebhook : negated conditional → KILLED 2. createGitHubWebhook : negated conditional → KILLED |
if (!action.equals("member_added") && !action.equals("member_invited")) { |
89 |
1
1. createGitHubWebhook : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/WebhookController::createGitHubWebhook → KILLED |
return ResponseEntity.ok().body("success"); |
90 | } | |
91 | ||
92 | // Extract GitHub login based on payload structure | |
93 | String githubLogin = null; | |
94 | String installationId = null; | |
95 | OrgStatus role = null; | |
96 | ||
97 | // For member_added events, the structure is different | |
98 |
1
1. createGitHubWebhook : negated conditional → KILLED |
if (action.equals("member_added")) { |
99 |
1
1. createGitHubWebhook : negated conditional → KILLED |
if (!jsonBody.has("membership") |
100 |
1
1. createGitHubWebhook : negated conditional → KILLED |
|| !jsonBody.get("membership").has("user") |
101 |
1
1. createGitHubWebhook : negated conditional → KILLED |
|| !jsonBody.get("membership").has("role") |
102 |
1
1. createGitHubWebhook : negated conditional → KILLED |
|| !jsonBody.get("membership").get("user").has("login") |
103 |
1
1. createGitHubWebhook : negated conditional → KILLED |
|| !jsonBody.has("installation") |
104 |
1
1. createGitHubWebhook : negated conditional → KILLED |
|| !jsonBody.get("installation").has("id")) { |
105 |
1
1. createGitHubWebhook : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/WebhookController::createGitHubWebhook → KILLED |
return ResponseEntity.ok().body("success"); |
106 | } | |
107 | ||
108 | githubLogin = jsonBody.get("membership").get("user").get("login").asText(); | |
109 | installationId = jsonBody.get("installation").get("id").asText(); | |
110 | String textRole = jsonBody.get("membership").get("role").asText(); | |
111 |
1
1. createGitHubWebhook : negated conditional → KILLED |
if (textRole.equals("admin")) { |
112 | role = OrgStatus.OWNER; | |
113 | } else { | |
114 | role = OrgStatus.MEMBER; | |
115 | } | |
116 | } | |
117 | // For member_invited events, use the original structure | |
118 | else { // must be "member_invited" based on earlier check | |
119 |
1
1. createGitHubWebhook : negated conditional → KILLED |
if (!jsonBody.has("user") |
120 |
1
1. createGitHubWebhook : negated conditional → KILLED |
|| !jsonBody.get("user").has("login") |
121 |
1
1. createGitHubWebhook : negated conditional → KILLED |
|| !jsonBody.has("installation") |
122 |
1
1. createGitHubWebhook : negated conditional → KILLED |
|| !jsonBody.get("installation").has("id")) { |
123 |
1
1. createGitHubWebhook : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/WebhookController::createGitHubWebhook → KILLED |
return ResponseEntity.ok().body("success"); |
124 | } | |
125 | ||
126 | githubLogin = jsonBody.get("user").get("login").asText(); | |
127 | installationId = jsonBody.get("installation").get("id").asText(); | |
128 | } | |
129 | ||
130 | log.info("GitHub login: {}, Installation ID: {}", githubLogin, installationId); | |
131 | ||
132 | Optional<Course> course = courseRepository.findByInstallationId(installationId); | |
133 | log.info("Course found: {}", course.isPresent()); | |
134 | ||
135 |
1
1. createGitHubWebhook : negated conditional → KILLED |
if (!course.isPresent()) { |
136 | log.warn("No course found with installation ID: {}", installationId); | |
137 |
1
1. createGitHubWebhook : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/WebhookController::createGitHubWebhook → KILLED |
return ResponseEntity.ok().body("success"); |
138 | } | |
139 | ||
140 | Optional<RosterStudent> student = | |
141 | rosterStudentRepository.findByCourseAndGithubLogin(course.get(), githubLogin); | |
142 | Optional<CourseStaff> staff = | |
143 | courseStaffRepository.findByCourseAndGithubLogin(course.get(), githubLogin); | |
144 | log.info("Student found: {}", student.isPresent()); | |
145 | log.info("Staff found: {}", staff.isPresent()); | |
146 | ||
147 |
2
1. createGitHubWebhook : negated conditional → KILLED 2. createGitHubWebhook : negated conditional → KILLED |
if (!student.isPresent() && !staff.isPresent()) { |
148 | log.warn( | |
149 | "No student or staff found with GitHub login: {} in course: {}", | |
150 | githubLogin, | |
151 | course.get().getCourseName()); | |
152 |
1
1. createGitHubWebhook : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/WebhookController::createGitHubWebhook → KILLED |
return ResponseEntity.ok().body("success"); |
153 | } | |
154 | StringBuilder response = new StringBuilder(); | |
155 |
1
1. createGitHubWebhook : negated conditional → KILLED |
if (student.isPresent()) { |
156 | RosterStudent updatedStudent = student.get(); | |
157 | log.info("Current student org status: {}", updatedStudent.getOrgStatus()); | |
158 | ||
159 | // Update status based on action | |
160 |
1
1. createGitHubWebhook : negated conditional → KILLED |
if (action.equals("member_added")) { |
161 |
1
1. createGitHubWebhook : removed call to edu/ucsb/cs156/frontiers/entities/RosterStudent::setOrgStatus → KILLED |
updatedStudent.setOrgStatus(role); |
162 | log.info("Setting status to {}", role.toString()); | |
163 | } else { // must be "member_invited" based on earlier check | |
164 |
1
1. createGitHubWebhook : removed call to edu/ucsb/cs156/frontiers/entities/RosterStudent::setOrgStatus → KILLED |
updatedStudent.setOrgStatus(OrgStatus.INVITED); |
165 | log.info("Setting status to INVITED"); | |
166 | } | |
167 | ||
168 | rosterStudentRepository.save(updatedStudent); | |
169 | log.info("Student saved with new org status: {}", updatedStudent.getOrgStatus()); | |
170 | response.append(updatedStudent); | |
171 | } | |
172 |
1
1. createGitHubWebhook : negated conditional → KILLED |
if (staff.isPresent()) { |
173 | CourseStaff updatedStaff = staff.get(); | |
174 | log.info("Current course staff member org status: {}", updatedStaff.getOrgStatus()); | |
175 | ||
176 | // Update status based on action | |
177 |
1
1. createGitHubWebhook : negated conditional → KILLED |
if (action.equals("member_added")) { |
178 |
1
1. createGitHubWebhook : removed call to edu/ucsb/cs156/frontiers/entities/CourseStaff::setOrgStatus → KILLED |
updatedStaff.setOrgStatus(role); |
179 | log.info("Setting status to {}", role.toString()); | |
180 | } else { // must be "member_invited" based on earlier check | |
181 |
1
1. createGitHubWebhook : removed call to edu/ucsb/cs156/frontiers/entities/CourseStaff::setOrgStatus → KILLED |
updatedStaff.setOrgStatus(OrgStatus.INVITED); |
182 | log.info("Setting status to INVITED"); | |
183 | } | |
184 | ||
185 | courseStaffRepository.save(updatedStaff); | |
186 | log.info("Course staff member saved with new org status: {}", updatedStaff.getOrgStatus()); | |
187 | response.append(updatedStaff); | |
188 | } | |
189 |
1
1. createGitHubWebhook : replaced return value with null for edu/ucsb/cs156/frontiers/controllers/WebhookController::createGitHubWebhook → KILLED |
return ResponseEntity.ok().body(response.toString()); |
190 | } | |
191 | } | |
Mutations | ||
64 |
1.1 |
|
66 |
1.1 |
|
75 |
1.1 |
|
80 |
1.1 |
|
81 |
1.1 |
|
88 |
1.1 2.2 |
|
89 |
1.1 |
|
98 |
1.1 |
|
99 |
1.1 |
|
100 |
1.1 |
|
101 |
1.1 |
|
102 |
1.1 |
|
103 |
1.1 |
|
104 |
1.1 |
|
105 |
1.1 |
|
111 |
1.1 |
|
119 |
1.1 |
|
120 |
1.1 |
|
121 |
1.1 |
|
122 |
1.1 |
|
123 |
1.1 |
|
135 |
1.1 |
|
137 |
1.1 |
|
147 |
1.1 2.2 |
|
152 |
1.1 |
|
155 |
1.1 |
|
160 |
1.1 |
|
161 |
1.1 |
|
164 |
1.1 |
|
172 |
1.1 |
|
177 |
1.1 |
|
178 |
1.1 |
|
181 |
1.1 |
|
189 |
1.1 |