added all v2 api project routes

This commit is contained in:
Jake Potrebic 2020-09-02 16:20:57 -07:00
parent 7252f47069
commit d0544f8a31
No known key found for this signature in database
GPG Key ID: 7C58557EC9C421F8
6 changed files with 114 additions and 58 deletions

View File

@ -1,6 +1,5 @@
package io.papermc.hangar.controller.api;
import io.papermc.hangar.model.ApiAuthInfo;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
@ -45,8 +44,7 @@ public interface ProjectsApi {
, @ApiParam(value = "How to sort the projects") @Valid @RequestParam(value = "sort", required = false, defaultValue = "updated") ProjectSortingStrategy sort
, @ApiParam(value = "If how relevant the project is to the given query should be used when sorting the projects") @RequestParam(value = "relevance", required = false, defaultValue = "true") boolean relevance
, @ApiParam(value = "The maximum amount of projects to return") @Valid @RequestParam(value = "limit", required = false) Long limit
, @ApiParam(value = "Where to start searching", defaultValue = "0") @Valid @RequestParam(value = "offset", required = false, defaultValue = "0") Long offset,
ApiAuthInfo apiAuthInfo);
, @ApiParam(value = "Where to start searching", defaultValue = "0") @Valid @RequestParam(value = "offset", required = false, defaultValue = "0") Long offset);
@ApiOperation(value = "Returns the members of a project", nickname = "showMembers", notes = "Returns the members of a project. Requires the `view_public_info` permission.", response = ProjectMember.class, authorizations = {
@Authorization(value = "Session")}, tags = "Projects")
@ -56,7 +54,7 @@ public interface ProjectsApi {
@ApiResponse(code = 403, message = "Not enough permissions to use this endpoint")})
@GetMapping(value = "/projects/{pluginId}/members",
produces = MediaType.APPLICATION_JSON_VALUE)
ResponseEntity<ProjectMember> showMembers(@ApiParam(value = "The plugin id of the project to return members for", required = true) @PathVariable("pluginId") String pluginId
ResponseEntity<List<ProjectMember>> showMembers(@ApiParam(value = "The plugin id of the project to return members for", required = true) @PathVariable("pluginId") String pluginId
, @ApiParam(value = "The maximum amount of members to return") @Valid @RequestParam(value = "limit", required = false) Long limit
, @ApiParam(value = "Where to start returning", defaultValue = "0") @Valid @RequestParam(value = "offset", required = false, defaultValue = "0") Long offset
);
@ -81,7 +79,7 @@ public interface ProjectsApi {
@GetMapping(value = "/projects/{pluginId}/stats",
produces = MediaType.APPLICATION_JSON_VALUE)
ResponseEntity<Map<String, ProjectStatsDay>> showProjectStats(@ApiParam(value = "The plugin id of the project to return the stats for", required = true) @PathVariable("pluginId") String pluginId
, @NotNull @ApiParam(value = "The first date to include in the result", required = true) @Valid @RequestParam(value = "fromDate", required = true) LocalDate fromDate
, @NotNull @ApiParam(value = "The last date to include in the result", required = true) @Valid @RequestParam(value = "toDate", required = true) LocalDate toDate
, @NotNull @ApiParam(value = "The first date to include in the result", required = true) @Valid @RequestParam(value = "fromDate", required = true) String fromDate
, @NotNull @ApiParam(value = "The last date to include in the result", required = true) @Valid @RequestParam(value = "toDate", required = true) String toDate
);
}

View File

@ -1,12 +1,7 @@
package io.papermc.hangar.controller.api;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.papermc.hangar.config.hangar.HangarConfig;
import io.papermc.hangar.db.model.UsersTable;
import io.papermc.hangar.model.ApiAuthInfo;
import io.papermc.hangar.service.UserService;
import io.papermc.hangar.util.ApiUtil;
import io.papermc.hangar.model.Category;
import io.papermc.hangar.model.Permission;
import io.papermc.hangar.model.generated.PaginatedProjectResult;
@ -16,8 +11,9 @@ import io.papermc.hangar.model.generated.ProjectMember;
import io.papermc.hangar.model.generated.ProjectSortingStrategy;
import io.papermc.hangar.model.generated.ProjectStatsDay;
import io.papermc.hangar.model.generated.Tag;
import io.papermc.hangar.service.PermissionService;
import io.papermc.hangar.service.api.ProjectApiService;
import io.papermc.hangar.service.project.ProjectService;
import io.papermc.hangar.util.ApiUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@ -25,38 +21,36 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.web.server.ResponseStatusException;
import java.io.IOException;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Controller
public class ProjectsApiController implements ProjectsApi {
private static final Logger log = LoggerFactory.getLogger(ProjectsApiController.class);
private final ObjectMapper objectMapper;
private final ProjectService projectService;
private final HangarConfig hangarConfig;
private final UserService userService;
private final PermissionService permissionService;
private final ApiAuthInfo apiAuthInfo;
private final ProjectService projectService;
private final ProjectApiService projectApiService;
@Autowired
public ProjectsApiController(ObjectMapper objectMapper, ProjectService projectService, HangarConfig hangarConfig, UserService userService, PermissionService permissionService) {
this.objectMapper = objectMapper;
this.projectService = projectService;
public ProjectsApiController(HangarConfig hangarConfig, ApiAuthInfo apiAuthInfo, ProjectService projectService, ProjectApiService projectApiService) {
this.hangarConfig = hangarConfig;
this.userService = userService;
this.permissionService = permissionService;
this.apiAuthInfo = apiAuthInfo;
this.projectService = projectService;
this.projectApiService = projectApiService;
}
@Override
@PreAuthorize("@authenticationService.authApiRequest(T(io.papermc.hangar.model.Permission).ViewPublicInfo, T(io.papermc.hangar.controller.util.ApiScope).forGlobal())")
public ResponseEntity<PaginatedProjectResult> listProjects(String q, List<Category> categories, List<String> tags, String owner, ProjectSortingStrategy sort, boolean orderWithRelevance, Long inLimit, Long inOffset, ApiAuthInfo apiAuthInfo) {
public ResponseEntity<PaginatedProjectResult> listProjects(String q, List<Category> categories, List<String> tags, String owner, ProjectSortingStrategy sort, boolean orderWithRelevance, Long inLimit, Long inOffset) {
// handle input
long limit = ApiUtil.limitOrDefault(inLimit, hangarConfig.getProjects().getInitLoad());
long offset = ApiUtil.offsetOrZero(inOffset);
@ -71,15 +65,11 @@ public class ProjectsApiController implements ProjectsApi {
parsedTags.add(new Tag().name(split[0]).data(split.length > 1 ? split[1] : null));
}
UsersTable currentUser = userService.getCurrentUser();
// TODO this is really meh, we want to save global permissions somewhere
boolean seeHidden = currentUser != null && permissionService.getGlobalPermissions(currentUser.getId()).has(Permission.SeeHidden);
Long requesterId = currentUser == null ? null : currentUser.getId();
String pluginId = null;
boolean seeHidden = apiAuthInfo.getGlobalPerms().has(Permission.SeeHidden);
Long requesterId = apiAuthInfo.getUser() == null ? null : apiAuthInfo.getUser().getId();
List<Project> projects = projectService.getProjects(
pluginId,
null,
categories,
parsedTags,
q,
@ -93,7 +83,7 @@ public class ProjectsApiController implements ProjectsApi {
);
long count = projectService.countProjects(
pluginId,
null,
categories,
parsedTags,
q,
@ -110,38 +100,44 @@ public class ProjectsApiController implements ProjectsApi {
}
@Override
public ResponseEntity<ProjectMember> showMembers(String pluginId, Long inLimit, Long inOffset) {
@PreAuthorize("@authenticationService.authApiRequest(T(io.papermc.hangar.model.Permission).ViewPublicInfo, T(io.papermc.hangar.controller.util.ApiScope).forProject(#pluginId))")
public ResponseEntity<List<ProjectMember>> showMembers(String pluginId, Long inLimit, Long inOffset) {
long limit = ApiUtil.limitOrDefault(inLimit, hangarConfig.getProjects().getInitLoad());
long offset = ApiUtil.offsetOrZero(inOffset);
try {
return new ResponseEntity<>(objectMapper.readValue("{\n \"roles\" : [ {\n \"color\" : \"color\",\n \"name\" : \"name\",\n \"title\" : \"title\"\n }, {\n \"color\" : \"color\",\n \"name\" : \"name\",\n \"title\" : \"title\"\n } ],\n \"user\" : \"user\"\n}", ProjectMember.class), HttpStatus.OK); // TODO Implement me
} catch (IOException e) {
log.error("Couldn't serialize response for content type application/json", e);
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
List<ProjectMember> projectMembers = projectApiService.getProjectMembers(pluginId, limit, offset);
if (projectMembers == null || projectMembers.isEmpty()) { // TODO this will also happen when the offset is too high
log.error("Couldn't find a project for that pluginId");
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
return ResponseEntity.ok(projectMembers);
}
@Override
@PreAuthorize("@authenticationService.authApiRequest(T(io.papermc.hangar.model.Permission).ViewPublicInfo, T(io.papermc.hangar.controller.util.ApiScope).forProject(#pluginId))")
public ResponseEntity<Project> showProject(String pluginId) {
Project project = projectService.getProjectApi(pluginId);
if (project == null) {
log.error("Couldn't find a project for that pluginId");
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
return new ResponseEntity<>(project, HttpStatus.OK);
return ResponseEntity.ok(project);
}
@Override
public ResponseEntity<Map<String, ProjectStatsDay>> showProjectStats(String pluginId, LocalDate fromDate, LocalDate toDate) {
try {
return new ResponseEntity<Map<String, ProjectStatsDay>>(objectMapper.readValue("{\n \"key\" : {\n \"downloads\" : 0,\n \"views\" : 6\n }\n}", Map.class), HttpStatus.OK); // TODO Implement me
} catch (IOException e) {
log.error("Couldn't serialize response for content type application/json", e);
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
@PreAuthorize("@authenticationService.authApiRequest(T(io.papermc.hangar.model.Permission).IsProjectMember, T(io.papermc.hangar.controller.util.ApiScope).forProject(#pluginId))")
public ResponseEntity<Map<String, ProjectStatsDay>> showProjectStats(String pluginId, @NotNull @Valid String fromDate, @NotNull @Valid String toDate) {
LocalDate from = ApiUtil.parseDate(fromDate);
LocalDate to = ApiUtil.parseDate(toDate);
if (from.isAfter(to)) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "From date is after to date");
}
Map<String, ProjectStatsDay> projectStats = projectApiService.getProjectStats(pluginId, from, to);
if (projectStats == null || projectStats.size() == 0) {
log.error("Couldn't find a project for that pluginId");
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
return ResponseEntity.ok(projectStats);
}
}

View File

@ -85,8 +85,8 @@ public class VersionsApiController implements VersionsApi {
@Override
@PreAuthorize("@authenticationService.authApiRequest(T(io.papermc.hangar.model.Permission).IsProjectMember, T(io.papermc.hangar.controller.util.ApiScope).forProject(#pluginId))")
public ResponseEntity<Map<String, VersionStatsDay>> showVersionStats(String pluginId, String version, @NotNull @Valid String fromDate, @NotNull @Valid String toDate) {
LocalDate from = parseDate(fromDate);
LocalDate to = parseDate(toDate);
LocalDate from = ApiUtil.parseDate(fromDate);
LocalDate to = ApiUtil.parseDate(toDate);
if (from.isAfter(to)) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "From date is after to date");
}
@ -96,12 +96,4 @@ public class VersionsApiController implements VersionsApi {
}
return ResponseEntity.ok(versionStats);
}
private LocalDate parseDate(String date) {
try {
return LocalDate.parse(date);
} catch (DateTimeParseException e) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Badly formatted date " + date);
}
}
}

View File

@ -2,6 +2,9 @@ package io.papermc.hangar.db.dao.api;
import io.papermc.hangar.db.mappers.PromotedVersionMapper;
import io.papermc.hangar.model.generated.Project;
import io.papermc.hangar.model.generated.ProjectMember;
import io.papermc.hangar.model.generated.ProjectStatsDay;
import org.jdbi.v3.sqlobject.config.KeyColumn;
import org.jdbi.v3.sqlobject.config.RegisterBeanMapper;
import org.jdbi.v3.sqlobject.config.RegisterColumnMapper;
import org.jdbi.v3.sqlobject.customizer.BindList;
@ -11,7 +14,9 @@ import org.jdbi.v3.sqlobject.statement.SqlQuery;
import org.jdbi.v3.stringtemplate4.UseStringTemplateEngine;
import org.springframework.stereotype.Repository;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
@Repository
@RegisterBeanMapper(Project.class)
@ -109,4 +114,26 @@ public interface ProjectApiDao {
@BindList(onEmpty = BindList.EmptyHandling.NULL_VALUE) List<Integer> categories,
@BindList(onEmpty = BindList.EmptyHandling.NULL_VALUE) List<String> tags, //TODO: implement tags with mc_version('data')
String query, @Define String queryStatement);
@RegisterBeanMapper(ProjectMember.class)
@SqlQuery("SELECT u.name AS user, array_agg(r.name) roles " +
" FROM projects p " +
" JOIN user_project_roles upr ON p.id = upr.project_id " +
" JOIN users u ON upr.user_id = u.id " +
" JOIN roles r ON upr.role_type = r.name " +
" WHERE p.plugin_id = :pluginId" +
" GROUP BY u.name ORDER BY max(r.permission::BIGINT) DESC " +
" LIMIT :limit OFFSET :offset")
List<ProjectMember> projectMembers(String pluginId, long limit, long offset);
@KeyColumn("dateKey")
@RegisterBeanMapper(ProjectStatsDay.class)
@SqlQuery("SELECT CAST(dates.day AS date) dateKey, coalesce(sum(pvd.downloads), 0) AS downloads, coalesce(pv.views, 0) AS views" +
" FROM projects p," +
" (SELECT generate_series(:startDate::DATE, :endDate::DATE, INTERVAL '1 DAY') AS day) dates" +
" LEFT JOIN project_versions_downloads pvd ON dates.day = pvd.day" +
" LEFT JOIN project_views pv ON dates.day = pv.day AND pvd.project_id = pv.project_id" +
" WHERE p.plugin_id = :pluginId AND (pvd IS NULL OR pvd.project_id = p.id)" +
" GROUP BY pv.views, dates.day")
Map<String, ProjectStatsDay> projectStats(String pluginId, LocalDate startDate, LocalDate endDate);
}

View File

@ -0,0 +1,29 @@
package io.papermc.hangar.service.api;
import io.papermc.hangar.db.dao.HangarDao;
import io.papermc.hangar.db.dao.api.ProjectApiDao;
import io.papermc.hangar.model.generated.ProjectMember;
import io.papermc.hangar.model.generated.ProjectStatsDay;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
@Service
public class ProjectApiService {
private final HangarDao<ProjectApiDao> projectApiDao;
public ProjectApiService(HangarDao<ProjectApiDao> projectApiDao) {
this.projectApiDao = projectApiDao;
}
public List<ProjectMember> getProjectMembers(String pluginId, long limit, long offset) {
return projectApiDao.get().projectMembers(pluginId, limit, offset);
}
public Map<String, ProjectStatsDay> getProjectStats(String pluginId, LocalDate fromDate, LocalDate toDate) {
return projectApiDao.get().projectStats(pluginId, fromDate, toDate);
}
}

View File

@ -1,12 +1,18 @@
package io.papermc.hangar.util;
import io.papermc.hangar.db.model.UsersTable;
import org.springframework.http.HttpStatus;
import org.springframework.web.server.ResponseStatusException;
import java.time.LocalDate;
import java.time.format.DateTimeParseException;
public class ApiUtil {
private ApiUtil() { }
public static long limitOrDefault(Long limit, long defaultValue) {
if (limit < 1) throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Limit should be greater than 0");
return Math.min(limit == null ? defaultValue : limit, defaultValue);
}
@ -17,4 +23,12 @@ public class ApiUtil {
public static Long userIdOrNull(UsersTable usersTable) {
return usersTable == null ? null : usersTable.getId();
}
public static LocalDate parseDate(String date) {
try {
return LocalDate.parse(date);
} catch (DateTimeParseException e) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Badly formatted date " + date);
}
}
}