GithubTeamService.java
package edu.ucsb.cs156.frontiers.services;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import edu.ucsb.cs156.frontiers.entities.Course;
import edu.ucsb.cs156.frontiers.entities.Team;
import edu.ucsb.cs156.frontiers.enums.TeamStatus;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.util.HashMap;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestTemplate;
@Slf4j
@Service
public class GithubTeamService {
private final JwtService jwtService;
private final ObjectMapper objectMapper;
private final RestTemplate restTemplate;
public GithubTeamService(
JwtService jwtService, ObjectMapper objectMapper, RestTemplateBuilder builder) {
this.jwtService = jwtService;
this.objectMapper = objectMapper;
this.objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
this.restTemplate = builder.build();
}
/**
* Creates a team on GitHub if it doesn't exist, or returns the existing team ID.
*
* @param team The team to create
* @param course The course containing the organization
* @return The GitHub team ID
* @throws JsonProcessingException if there is an error processing JSON
* @throws NoSuchAlgorithmException if there is an algorithm error
* @throws InvalidKeySpecException if there is a key specification error
*/
public Integer createOrGetTeamId(Team team, Course course)
throws JsonProcessingException, NoSuchAlgorithmException, InvalidKeySpecException {
// First check if team already exists by getting team info
Integer existingTeamId = getTeamId(team.getName(), course);
if (existingTeamId != null) {
return existingTeamId;
}
// Create the team if it doesn't exist
return createTeam(team.getName(), course);
}
/**
* Get the org id, given the org name.
*
* <p>Note: in the future, it would be better to cache this value in the Course row in the
* database at the time the Github App is linked to the org, since it doesn't change.
*
* @param orgName
* @param course
* @return
* @throws JsonProcessingException
* @throws NoSuchAlgorithmException
* @throws InvalidKeySpecException
*/
public Integer getOrgId(String orgName, Course course)
throws JsonProcessingException, NoSuchAlgorithmException, InvalidKeySpecException {
String endpoint = "https://api.github.com/orgs/" + orgName;
HttpHeaders headers = new HttpHeaders();
String token = jwtService.getInstallationToken(course);
headers.add("Authorization", "Bearer " + token);
headers.add("Accept", "application/vnd.github+json");
headers.add("X-GitHub-Api-Version", "2022-11-28");
HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<String> response =
restTemplate.exchange(endpoint, HttpMethod.GET, entity, String.class);
JsonNode responseJson = objectMapper.readTree(response.getBody());
return responseJson.get("id").asInt();
}
/**
* Gets the team ID for a team name, returns null if team doesn't exist.
*
* @param teamName The name of the team
* @param course The course containing the organization
* @return The GitHub team ID or null if not found
* @throws JsonProcessingException if there is an error processing JSON
* @throws NoSuchAlgorithmException if there is an algorithm error
* @throws InvalidKeySpecException if there is a key specification error
*/
public Integer getTeamId(String teamName, Course course)
throws JsonProcessingException, NoSuchAlgorithmException, InvalidKeySpecException {
String endpoint = "https://api.github.com/orgs/" + course.getOrgName() + "/teams/" + teamName;
HttpHeaders headers = new HttpHeaders();
String token = jwtService.getInstallationToken(course);
headers.add("Authorization", "Bearer " + token);
headers.add("Accept", "application/vnd.github+json");
headers.add("X-GitHub-Api-Version", "2022-11-28");
HttpEntity<String> entity = new HttpEntity<>(headers);
try {
ResponseEntity<String> response =
restTemplate.exchange(endpoint, HttpMethod.GET, entity, String.class);
JsonNode responseJson = objectMapper.readTree(response.getBody());
return responseJson.get("id").asInt();
} catch (HttpClientErrorException e) {
if (e.getStatusCode().value() == 404) {
return null; // Team doesn't exist
}
throw e;
}
}
/**
* Creates a new team on GitHub.
*
* @param teamName The name of the team to create
* @param course The course containing the organization
* @return The GitHub team ID
* @throws JsonProcessingException if there is an error processing JSON
* @throws NoSuchAlgorithmException if there is an algorithm error
* @throws InvalidKeySpecException if there is a key specification error
*/
private Integer createTeam(String teamName, Course course)
throws JsonProcessingException, NoSuchAlgorithmException, InvalidKeySpecException {
String endpoint = "https://api.github.com/orgs/" + course.getOrgName() + "/teams";
HttpHeaders headers = new HttpHeaders();
String token = jwtService.getInstallationToken(course);
headers.add("Authorization", "Bearer " + token);
headers.add("Accept", "application/vnd.github+json");
headers.add("X-GitHub-Api-Version", "2022-11-28");
Map<String, Object> body = new HashMap<>();
body.put("name", teamName);
body.put("privacy", "closed"); // Teams are private by default
String bodyAsJson = objectMapper.writeValueAsString(body);
HttpEntity<String> entity = new HttpEntity<>(bodyAsJson, headers);
ResponseEntity<String> response =
restTemplate.exchange(endpoint, HttpMethod.POST, entity, String.class);
JsonNode responseJson = objectMapper.readTree(response.getBody());
Integer teamId = responseJson.get("id").asInt();
log.info(
"Created team '{}' with ID {} in organization {}", teamName, teamId, course.getOrgName());
return teamId;
}
/**
* Gets the current team membership status for a user.
*
* @param githubLogin The GitHub login of the user
* @param teamId The GitHub team ID
* @param course The course containing the organization
* @return The team status of the user
* @throws JsonProcessingException if there is an error processing JSON
* @throws NoSuchAlgorithmException if there is an algorithm error
* @throws InvalidKeySpecException if there is a key specification error
*/
public TeamStatus getTeamMembershipStatus(String githubLogin, Integer teamId, Course course)
throws JsonProcessingException, NoSuchAlgorithmException, InvalidKeySpecException {
if (githubLogin == null) {
return TeamStatus.NO_GITHUB_ID;
}
String endpoint =
"https://api.github.com/orgs/"
+ course.getOrgName()
+ "/teams/"
+ teamId
+ "/memberships/"
+ githubLogin;
HttpHeaders headers = new HttpHeaders();
String token = jwtService.getInstallationToken(course);
headers.add("Authorization", "Bearer " + token);
headers.add("Accept", "application/vnd.github+json");
headers.add("X-GitHub-Api-Version", "2022-11-28");
HttpEntity<String> entity = new HttpEntity<>(headers);
try {
ResponseEntity<String> response =
restTemplate.exchange(endpoint, HttpMethod.GET, entity, String.class);
JsonNode responseJson = objectMapper.readTree(response.getBody());
String role = responseJson.get("role").asText();
return "maintainer".equalsIgnoreCase(role)
? TeamStatus.TEAM_MAINTAINER
: TeamStatus.TEAM_MEMBER;
} catch (HttpClientErrorException e) {
if (e.getStatusCode().value() == 404) {
return TeamStatus.NOT_ORG_MEMBER; // User is not a member of the team
}
throw e;
}
}
/**
* Adds a member to a GitHub team.
*
* @param githubLogin The GitHub login of the user to add
* @param teamSlug The GitHub team slug (name)
* @param role The role to assign ("member" or "maintainer")
* @param course The course containing the organization
* @return The resulting team status
* @throws JsonProcessingException if there is an error processing JSON
* @throws NoSuchAlgorithmException if there is an algorithm error
* @throws InvalidKeySpecException if there is a key specification error
*/
public TeamStatus addMemberToGithubTeam(
String githubLogin, Integer teamId, String role, Course course, Integer orgId)
throws JsonProcessingException, NoSuchAlgorithmException, InvalidKeySpecException {
String endpoint =
"https://api.github.com/organizations/"
+ orgId
+ "/team/"
+ teamId
+ "/memberships/"
+ githubLogin;
HttpHeaders headers = new HttpHeaders();
String token = jwtService.getInstallationToken(course);
headers.add("Authorization", "Bearer " + token);
headers.add("Accept", "application/vnd.github+json");
headers.add("X-GitHub-Api-Version", "2022-11-28");
Map<String, Object> body = new HashMap<>();
body.put("role", role);
String bodyAsJson = objectMapper.writeValueAsString(body);
HttpEntity<String> entity = new HttpEntity<>(bodyAsJson, headers);
ResponseEntity<String> response =
restTemplate.exchange(endpoint, HttpMethod.PUT, entity, String.class);
JsonNode responseJson = objectMapper.readTree(response.getBody());
String resultRole = responseJson.get("role").asText();
log.info("Added user '{}' to team ID {} with role '{}'", githubLogin, teamId, resultRole);
return "maintainer".equalsIgnoreCase(resultRole)
? TeamStatus.TEAM_MAINTAINER
: TeamStatus.TEAM_MEMBER;
}
}