From 31fe7bb53ed65cd7cc3d9ecb4ab77cd26fd5847e Mon Sep 17 00:00:00 2001 From: Jake Potrebic Date: Sun, 20 Nov 2022 13:29:04 -0800 Subject: [PATCH] validation work --- .../io/papermc/hangar/HangarApplication.java | 3 +- .../io/papermc/hangar/config/WebConfig.java | 2 +- .../hangar/config/hangar/ChannelsConfig.java | 12 +- .../config/hangar/OrganizationsConfig.java | 12 +- .../hangar/config/hangar/PagesConfig.java | 33 ++-- .../hangar/config/hangar/ProjectsConfig.java | 19 ++- .../hangar/config/hangar/UserConfig.java | 5 + .../internal/BackendDataController.java | 145 +++++++----------- .../papermc/hangar/db/dao/PermissionsDAO.java | 90 +++++------ .../db/mappers/LogActionColumnMapper.java | 4 +- .../hangar/db/mappers/PermissionMapper.java | 4 +- .../factories/RoleColumnMapperFactory.java | 5 +- .../hangar/model/common/NamedPermission.java | 2 + .../hangar/model/common/Permission.java | 9 +- .../hangar/model/common/roles/GlobalRole.java | 6 + .../internal/api/responses/Validation.java | 25 +++ .../internal/api/responses/Validations.java | 49 ++++++ backend/src/main/resources/application.yml | 2 +- frontend/.husky/pre-commit | 1 + frontend/src/lib | 2 +- frontend/src/types/generated/icons.d.ts | 13 -- 21 files changed, 256 insertions(+), 187 deletions(-) create mode 100644 backend/src/main/java/io/papermc/hangar/model/internal/api/responses/Validation.java create mode 100644 backend/src/main/java/io/papermc/hangar/model/internal/api/responses/Validations.java diff --git a/backend/src/main/java/io/papermc/hangar/HangarApplication.java b/backend/src/main/java/io/papermc/hangar/HangarApplication.java index b7d547bdc..ee60e5d47 100644 --- a/backend/src/main/java/io/papermc/hangar/HangarApplication.java +++ b/backend/src/main/java/io/papermc/hangar/HangarApplication.java @@ -1,5 +1,6 @@ package io.papermc.hangar; +import io.papermc.hangar.config.hangar.PagesConfig; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.properties.ConfigurationPropertiesScan; @@ -9,7 +10,7 @@ import org.springframework.scheduling.annotation.EnableScheduling; @EnableScheduling @SpringBootApplication @Import(JdbiBeanFactoryPostProcessor.class) -@ConfigurationPropertiesScan("io.papermc.hangar.config.hangar") +@ConfigurationPropertiesScan(value = "io.papermc.hangar.config.hangar", basePackageClasses = PagesConfig.class) public class HangarApplication { public static void main(String[] args) { diff --git a/backend/src/main/java/io/papermc/hangar/config/WebConfig.java b/backend/src/main/java/io/papermc/hangar/config/WebConfig.java index 75bb84a39..4b07a41e1 100644 --- a/backend/src/main/java/io/papermc/hangar/config/WebConfig.java +++ b/backend/src/main/java/io/papermc/hangar/config/WebConfig.java @@ -126,7 +126,7 @@ public class WebConfig extends WebMvcConfigurationSupport { @Override public void configureMessageConverters(@NotNull List> converters) { // TODO kinda wack, but idk a better way rn - ParameterNamesAnnotationIntrospector sAnnotationIntrospector = (ParameterNamesAnnotationIntrospector) mapper.getSerializationConfig().getAnnotationIntrospector().allIntrospectors().stream().filter(ParameterNamesAnnotationIntrospector.class::isInstance).findFirst().get(); + ParameterNamesAnnotationIntrospector sAnnotationIntrospector = (ParameterNamesAnnotationIntrospector) mapper.getSerializationConfig().getAnnotationIntrospector().allIntrospectors().stream().filter(ParameterNamesAnnotationIntrospector.class::isInstance).findFirst().orElseThrow(); mapper.setAnnotationIntrospectors( AnnotationIntrospector.pair(sAnnotationIntrospector, new HangarAnnotationIntrospector()), mapper.getDeserializationConfig().getAnnotationIntrospector() diff --git a/backend/src/main/java/io/papermc/hangar/config/hangar/ChannelsConfig.java b/backend/src/main/java/io/papermc/hangar/config/hangar/ChannelsConfig.java index 3c2cf7d27..a8988ca05 100644 --- a/backend/src/main/java/io/papermc/hangar/config/hangar/ChannelsConfig.java +++ b/backend/src/main/java/io/papermc/hangar/config/hangar/ChannelsConfig.java @@ -1,6 +1,8 @@ package io.papermc.hangar.config.hangar; import io.papermc.hangar.model.common.Color; +import io.papermc.hangar.model.internal.api.responses.Validation; +import io.papermc.hangar.util.PatternWrapper; import javax.validation.constraints.Min; import javax.validation.constraints.Size; import org.springframework.boot.context.properties.ConfigurationProperties; @@ -9,12 +11,16 @@ import org.springframework.boot.context.properties.bind.DefaultValue; @ConfigurationProperties(prefix = "hangar.channels") public record ChannelsConfig( @Min(1) @DefaultValue("15") int maxNameLen, - @DefaultValue("^[a-zA-Z0-9]+$") String nameRegex, + @DefaultValue("^[a-zA-Z0-9]+$") PatternWrapper nameRegex, @DefaultValue("cyan") Color colorDefault, @Size(min = 1, max = 15) @DefaultValue("Release") String nameDefault ) { - public boolean isValidChannelName(String name) { - return name.length() >= 1 && name.length() <= maxNameLen && name.matches(nameRegex); + public boolean isValidChannelName(final String name) { + return name.length() >= 1 && name.length() <= this.maxNameLen() && this.nameRegex().test(name); + } + + public Validation channelName() { + return new Validation(this.nameRegex(), this.maxNameLen(), null); } } diff --git a/backend/src/main/java/io/papermc/hangar/config/hangar/OrganizationsConfig.java b/backend/src/main/java/io/papermc/hangar/config/hangar/OrganizationsConfig.java index 6c5f853e0..b6e27c0b6 100644 --- a/backend/src/main/java/io/papermc/hangar/config/hangar/OrganizationsConfig.java +++ b/backend/src/main/java/io/papermc/hangar/config/hangar/OrganizationsConfig.java @@ -1,11 +1,9 @@ package io.papermc.hangar.config.hangar; +import io.papermc.hangar.model.internal.api.responses.Validation; +import io.papermc.hangar.util.PatternWrapper; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.bind.DefaultValue; -import org.springframework.stereotype.Component; - -import java.util.function.Predicate; -import java.util.regex.Pattern; @ConfigurationProperties(prefix = "hangar.orgs") public record OrganizationsConfig( @@ -14,6 +12,10 @@ public record OrganizationsConfig( @DefaultValue("5") int createLimit, @DefaultValue("3") int minNameLen, @DefaultValue("20") int maxNameLen, - @DefaultValue("[a-zA-Z0-9-_]*") String nameRegex + @DefaultValue("[a-zA-Z0-9-_]*") PatternWrapper nameRegex ) { + + public Validation orgName() { + return new Validation(this.nameRegex(), this.maxNameLen(), this.minNameLen()); + } } diff --git a/backend/src/main/java/io/papermc/hangar/config/hangar/PagesConfig.java b/backend/src/main/java/io/papermc/hangar/config/hangar/PagesConfig.java index 971f38d1f..8316f6651 100644 --- a/backend/src/main/java/io/papermc/hangar/config/hangar/PagesConfig.java +++ b/backend/src/main/java/io/papermc/hangar/config/hangar/PagesConfig.java @@ -1,7 +1,8 @@ package io.papermc.hangar.config.hangar; import io.papermc.hangar.exceptions.HangarApiException; -import java.util.regex.Pattern; +import io.papermc.hangar.model.internal.api.responses.Validation; +import io.papermc.hangar.util.PatternWrapper; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.NestedConfigurationProperty; import org.springframework.boot.context.properties.bind.DefaultValue; @@ -10,27 +11,35 @@ import org.springframework.http.HttpStatus; @ConfigurationProperties(prefix = "hangar.pages") public record PagesConfig( @NestedConfigurationProperty Home home, - @DefaultValue("^[a-zA-Z0-9-_ ]+$") String nameRegex, + @DefaultValue("^[a-zA-Z0-9-_ ]+$") PatternWrapper nameRegex, @DefaultValue("3") int minNameLen, @DefaultValue("25") int maxNameLen, @DefaultValue("15") int minLen, @DefaultValue("32000") int maxLen ) { + public void testPageName(final String name) { + if (name.length() > this.maxNameLen) { + throw new HangarApiException(HttpStatus.BAD_REQUEST, "page.new.error.name.maxLength"); + } else if (name.length() < this.minNameLen) { + throw new HangarApiException(HttpStatus.BAD_REQUEST, "page.new.error.name.minLength"); + } else if (!this.nameRegex().test(name)) { + throw new HangarApiException(HttpStatus.BAD_REQUEST, "page.new.error.name.invalidChars"); + } + } + + public Validation pageName() { + return new Validation(this.nameRegex(), this.maxNameLen(), this.minNameLen()); + } + + public Validation pageContent() { + return Validation.size(this.maxLen(), this.minLen()); + } + @ConfigurationProperties(prefix = "hangar.pages.home") public record Home( @DefaultValue("Home") String name, @DefaultValue("Welcome to your new project!") String message ) { } - - public void testPageName(String name) { - if (name.length() > maxNameLen) { - throw new HangarApiException(HttpStatus.BAD_REQUEST, "page.new.error.name.maxLength"); - } else if (name.length() < minNameLen) { - throw new HangarApiException(HttpStatus.BAD_REQUEST, "page.new.error.name.minLength"); - } else if (!Pattern.compile(nameRegex).matcher(name).matches()) { - throw new HangarApiException(HttpStatus.BAD_REQUEST, "page.new.error.name.invalidChars"); - } - } } diff --git a/backend/src/main/java/io/papermc/hangar/config/hangar/ProjectsConfig.java b/backend/src/main/java/io/papermc/hangar/config/hangar/ProjectsConfig.java index b5cd4673a..fea77d1d6 100644 --- a/backend/src/main/java/io/papermc/hangar/config/hangar/ProjectsConfig.java +++ b/backend/src/main/java/io/papermc/hangar/config/hangar/ProjectsConfig.java @@ -1,10 +1,9 @@ package io.papermc.hangar.config.hangar; +import io.papermc.hangar.model.internal.api.responses.Validation; import io.papermc.hangar.util.PatternWrapper; import java.time.Duration; import java.time.temporal.ChronoUnit; -import java.util.function.Predicate; -import java.util.regex.Pattern; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.bind.DefaultValue; import org.springframework.boot.convert.DurationUnit; @@ -33,4 +32,20 @@ public record ProjectsConfig( // TODO split into ProjectsConfig and VersionsConf @DefaultValue("10") @DurationUnit(ChronoUnit.MINUTES) Duration unsafeDownloadMaxAge, @DefaultValue("false") boolean showUnreviewedDownloadWarning ) { + + public Validation projectName() { + return new Validation(this.nameRegex(), this.maxNameLen(), null); + } + + public Validation projectDescription() { + return Validation.max(this.maxDescLen()); + } + + public Validation projectKeywords() { + return Validation.max(this.maxKeywords()); + } + + public Validation versionName() { + return new Validation(this.versionNameRegex(), this.maxVersionNameLen(), null); + } } diff --git a/backend/src/main/java/io/papermc/hangar/config/hangar/UserConfig.java b/backend/src/main/java/io/papermc/hangar/config/hangar/UserConfig.java index b65919215..43fd189b5 100644 --- a/backend/src/main/java/io/papermc/hangar/config/hangar/UserConfig.java +++ b/backend/src/main/java/io/papermc/hangar/config/hangar/UserConfig.java @@ -1,5 +1,6 @@ package io.papermc.hangar.config.hangar; +import io.papermc.hangar.model.internal.api.responses.Validation; import java.util.List; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.bind.DefaultValue; @@ -9,4 +10,8 @@ public record UserConfig( @DefaultValue("100") int maxTaglineLen, @DefaultValue({"Hangar_Admin", "Hangar_Mod"}) List staffRoles ) { + + public Validation userTagline() { + return Validation.max(this.maxTaglineLen()); + } } diff --git a/backend/src/main/java/io/papermc/hangar/controller/internal/BackendDataController.java b/backend/src/main/java/io/papermc/hangar/controller/internal/BackendDataController.java index 2bb77a7bd..d4492b3fd 100644 --- a/backend/src/main/java/io/papermc/hangar/controller/internal/BackendDataController.java +++ b/backend/src/main/java/io/papermc/hangar/controller/internal/BackendDataController.java @@ -1,7 +1,5 @@ package io.papermc.hangar.controller.internal; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.annotation.JsonValue; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.introspect.Annotated; @@ -21,11 +19,11 @@ import io.papermc.hangar.model.common.projects.Visibility; import io.papermc.hangar.model.common.roles.GlobalRole; import io.papermc.hangar.model.common.roles.OrganizationRole; import io.papermc.hangar.model.common.roles.ProjectRole; +import io.papermc.hangar.model.internal.api.responses.Validations; import io.papermc.hangar.security.annotations.Anyone; import io.papermc.hangar.security.annotations.ratelimit.RateLimit; import io.papermc.hangar.service.internal.PlatformService; import java.lang.annotation.Annotation; -import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Optional; @@ -48,24 +46,24 @@ import org.springframework.web.bind.annotation.ResponseBody; @RequestMapping(path = "/api/internal/data", produces = MediaType.APPLICATION_JSON_VALUE, method = RequestMethod.GET) public class BackendDataController { - private final ObjectMapper noJsonValueMapper; + private final ObjectMapper objectMapper; // ignores JsonValue annotations private final HangarConfig config; private final PlatformService platformService; private final Optional gitProperties; @Autowired - public BackendDataController(ObjectMapper mapper, HangarConfig config, PlatformService platformService, Optional gitProperties) { + public BackendDataController(final ObjectMapper mapper, final HangarConfig config, final PlatformService platformService, final Optional gitProperties) { this.config = config; - this.noJsonValueMapper = mapper.copy(); + this.objectMapper = mapper.copy(); this.platformService = platformService; this.gitProperties = gitProperties; - this.noJsonValueMapper.setAnnotationIntrospector(new JacksonAnnotationIntrospector() { + this.objectMapper.setAnnotationIntrospector(new JacksonAnnotationIntrospector() { @Override - protected A _findAnnotation(Annotated annotated, Class annoClass) { - if (!annotated.hasAnnotation(JsonValue.class)) { - return super._findAnnotation(annotated, annoClass); + protected A _findAnnotation(final Annotated annotated, final Class annoClass) { + if (annoClass == JsonValue.class) { + return null; } - return null; + return super._findAnnotation(annotated, annoClass); } }); } @@ -73,15 +71,15 @@ public class BackendDataController { @GetMapping("/categories") @Cacheable("categories") public ResponseEntity getCategories() { - return ResponseEntity.ok(noJsonValueMapper.valueToTree(Category.getValues())); + return ResponseEntity.ok(this.objectMapper.valueToTree(Category.getValues())); } @GetMapping("/permissions") @Cacheable("permissions") public ResponseEntity getPermissions() { - ArrayNode arrayNode = noJsonValueMapper.createArrayNode(); - for (NamedPermission namedPermission : NamedPermission.getValues()) { - ObjectNode namedPermissionObject = noJsonValueMapper.createObjectNode(); + final ArrayNode arrayNode = this.objectMapper.createArrayNode(); + for (final NamedPermission namedPermission : NamedPermission.getValues()) { + final ObjectNode namedPermissionObject = this.objectMapper.createObjectNode(); namedPermissionObject.put("value", namedPermission.getValue()); namedPermissionObject.put("frontendName", namedPermission.getFrontendName()); namedPermissionObject.put("permission", namedPermission.getPermission().toBinString()); @@ -93,10 +91,10 @@ public class BackendDataController { @GetMapping("/platforms") @Cacheable(CacheConfig.PLATFORMS) public ResponseEntity getPlatforms() { - ArrayNode arrayNode = noJsonValueMapper.createArrayNode(); - for (Platform platform : Platform.getValues()) { - ObjectNode objectNode = noJsonValueMapper.valueToTree(platform); - objectNode.set("possibleVersions", noJsonValueMapper.valueToTree(platformService.getVersionsForPlatform(platform))); + final ArrayNode arrayNode = this.objectMapper.createArrayNode(); + for (final Platform platform : Platform.getValues()) { + final ObjectNode objectNode = this.objectMapper.valueToTree(platform); + objectNode.set("possibleVersions", this.objectMapper.valueToTree(this.platformService.getVersionsForPlatform(platform))); arrayNode.add(objectNode); } return ResponseEntity.ok(arrayNode); @@ -105,11 +103,11 @@ public class BackendDataController { @GetMapping("/channelColors") @Cacheable("channelColors") public ResponseEntity getColors() { - ArrayNode arrayNode = noJsonValueMapper.createArrayNode(); - for (Color color : Color.getNonTransparentValues()) { - ObjectNode objectNode = noJsonValueMapper.createObjectNode() - .put("name", color.name()) - .put("hex", color.getHex()); + final ArrayNode arrayNode = this.objectMapper.createArrayNode(); + for (final Color color : Color.getNonTransparentValues()) { + final ObjectNode objectNode = this.objectMapper.createObjectNode() + .put("name", color.name()) + .put("hex", color.getHex()); arrayNode.add(objectNode); } return ResponseEntity.ok(arrayNode); @@ -119,11 +117,11 @@ public class BackendDataController { @GetMapping("/flagReasons") @Cacheable("flagReasons") public ResponseEntity getFlagReasons() { - ArrayNode arrayNode = noJsonValueMapper.createArrayNode(); - for (FlagReason flagReason : FlagReason.getValues()) { - ObjectNode objectNode = noJsonValueMapper.createObjectNode() - .put("type", flagReason.name()) - .put("title", flagReason.getTitle()); + final ArrayNode arrayNode = this.objectMapper.createArrayNode(); + for (final FlagReason flagReason : FlagReason.getValues()) { + final ObjectNode objectNode = this.objectMapper.createObjectNode() + .put("type", flagReason.name()) + .put("title", flagReason.getTitle()); arrayNode.add(objectNode); } return ResponseEntity.ok(arrayNode); @@ -131,49 +129,55 @@ public class BackendDataController { @GetMapping("/sponsor") @Cacheable("sponsor") - public ResponseEntity getSponsor() { - return ResponseEntity.ok(config.getSponsors().get(ThreadLocalRandom.current().nextInt(config.getSponsors().size()))); + @ResponseBody + public HangarConfig.Sponsor getSponsor() { + return this.config.getSponsors().get(ThreadLocalRandom.current().nextInt(this.config.getSponsors().size())); } @GetMapping("/announcements") - public ResponseEntity> getAnnouncements() { - return ResponseEntity.ok(config.getAnnouncements()); + @ResponseBody + public List getAnnouncements() { + return this.config.getAnnouncements(); } @GetMapping("/projectRoles") @Cacheable("projectRoles") - public ResponseEntity> getAssignableProjectRoles() { - return ResponseEntity.ok(ProjectRole.getAssignableRoles()); + @ResponseBody + public List getAssignableProjectRoles() { + return ProjectRole.getAssignableRoles(); } @GetMapping("/globalRoles") @Cacheable("globalRoles") - public ResponseEntity> getGlobalRoles() { - return ResponseEntity.ok(Arrays.stream(GlobalRole.values()).toList()); + @ResponseBody + public GlobalRole[] getGlobalRoles() { + return GlobalRole.getValues(); } @GetMapping("/orgRoles") @Cacheable("orgRoles") - public ResponseEntity> getAssignableOrganizationRoles() { - return ResponseEntity.ok(OrganizationRole.getAssignableRoles()); + @ResponseBody + public List getAssignableOrganizationRoles() { + return OrganizationRole.getAssignableRoles(); } @GetMapping("/licenses") @Cacheable("licenses") - public ResponseEntity> getLicenses() { - return ResponseEntity.ok(config.getLicenses()); + @ResponseBody + public List getLicenses() { + return this.config.getLicenses(); } @GetMapping("/visibilities") @Cacheable("visibilities") public ResponseEntity getVisibilities() { - ArrayNode arrayNode = noJsonValueMapper.createArrayNode(); - for (Visibility value : Visibility.getValues()) { - ObjectNode objectNode = noJsonValueMapper.createObjectNode(); + final ArrayNode arrayNode = this.objectMapper.createArrayNode(); + for (final Visibility value : Visibility.getValues()) { + final ObjectNode objectNode = this.objectMapper.createObjectNode(); objectNode.put("name", value.getName()) - .put("showModal", value.getShowModal()) - .put("cssClass", value.getCssClass()) - .put("title", value.getTitle()); + .put("showModal", value.getShowModal()) + .put("cssClass", value.getCssClass()) + .put("title", value.getTitle()); arrayNode.add(objectNode); } return ResponseEntity.ok(arrayNode); @@ -207,50 +211,9 @@ public class BackendDataController { @GetMapping("/validations") @Cacheable("validations") - public ResponseEntity getValidations() { - ObjectNode validations = noJsonValueMapper.createObjectNode(); - ObjectNode projectValidations = noJsonValueMapper.createObjectNode(); - projectValidations.set("name", noJsonValueMapper.valueToTree(new Validation(config.projects.nameRegex().strPattern(), config.projects.maxNameLen(), null))); - projectValidations.set("desc", noJsonValueMapper.valueToTree(new Validation(null, config.projects.maxDescLen(), null))); - projectValidations.set("keywords", noJsonValueMapper.valueToTree(new Validation(null, config.projects.maxKeywords(), null))); - projectValidations.set("channels", noJsonValueMapper.valueToTree(new Validation(config.channels.nameRegex(), config.channels.maxNameLen(), null))); - projectValidations.set("pageName", noJsonValueMapper.valueToTree(new Validation(config.pages.nameRegex(), config.pages.maxNameLen(), config.pages.minNameLen()))); - projectValidations.set("pageContent", noJsonValueMapper.valueToTree(new Validation(null, config.pages.maxLen(), config.pages.minLen()))); - projectValidations.put("maxPageCount", config.projects.maxPages()); - projectValidations.put("maxChannelCount", config.projects.maxChannels()); - validations.set("project", projectValidations); - validations.set("userTagline", noJsonValueMapper.valueToTree(new Validation(null, config.user.maxTaglineLen(), null))); - validations.set("version", noJsonValueMapper.valueToTree(new Validation(config.projects.versionNameRegex().strPattern(), config.projects.maxVersionNameLen(), null))); - validations.set("org", noJsonValueMapper.valueToTree(new Validation(config.org.nameRegex(), config.org.maxNameLen(), config.org.minNameLen()))); - validations.put("maxOrgCount", config.org.createLimit()); - validations.put("urlRegex", config.getUrlRegex()); - return ResponseEntity.ok(validations); - } - - @JsonInclude(Include.NON_NULL) - private static class Validation { - - private final String regex; - private final Integer max; - private final Integer min; - - public Validation(String regex, Integer max, Integer min) { - this.regex = regex; - this.max = max; - this.min = min; - } - - public String getRegex() { - return regex; - } - - public Integer getMax() { - return max; - } - - public Integer getMin() { - return min; - } + @ResponseBody + public Validations getValidations() { + return Validations.create(this.config); } } diff --git a/backend/src/main/java/io/papermc/hangar/db/dao/PermissionsDAO.java b/backend/src/main/java/io/papermc/hangar/db/dao/PermissionsDAO.java index 706311e1c..769ccdc72 100644 --- a/backend/src/main/java/io/papermc/hangar/db/dao/PermissionsDAO.java +++ b/backend/src/main/java/io/papermc/hangar/db/dao/PermissionsDAO.java @@ -2,7 +2,6 @@ package io.papermc.hangar.db.dao; import io.papermc.hangar.model.common.Permission; import io.papermc.hangar.model.db.UserTable; -import org.jdbi.v3.sqlobject.config.RegisterBeanMapper; import org.jdbi.v3.sqlobject.config.RegisterConstructorMapper; import org.jdbi.v3.sqlobject.config.ValueColumn; import org.jdbi.v3.sqlobject.statement.SqlQuery; @@ -12,64 +11,69 @@ import org.springframework.stereotype.Repository; import java.util.Map; @Repository -@RegisterBeanMapper(value = Permission.class, prefix = "perm") public interface PermissionsDAO { - @SqlQuery("SELECT coalesce(gt.permission, B'0'::BIT(64))::BIGINT perm_value" + - " FROM users u " + - " LEFT JOIN global_trust gt ON u.id = gt.user_id" + - " WHERE u.id = :userId OR u.name = :userName") + @SqlQuery("SELECT COALESCE(gt.permission, B'0'::bit(64))::bigint perm_value" + + " FROM users u " + + " LEFT JOIN global_trust gt ON u.id = gt.user_id" + + " WHERE u.id = :userId OR u.name = :userName") Permission _getGlobalPermission(Long userId, String userName); - default Permission getGlobalPermission(long userId) { - return _getGlobalPermission(userId, null); - } - default Permission getGlobalPermission(@NotNull String userName) { - return _getGlobalPermission(null, userName); + + default Permission getGlobalPermission(final long userId) { + return this._getGlobalPermission(userId, null); } - @SqlQuery("SELECT (coalesce(gt.permission, B'0'::BIT(64)) | coalesce(pt.permission, B'0'::BIT(64)) | coalesce(ot.permission, B'0'::BIT(64)))::BIGINT AS perm_value" + - " FROM users u " + - " LEFT JOIN global_trust gt ON u.id = gt.user_id" + - " LEFT JOIN projects p ON (lower(p.owner_name) = lower(:author) AND p.slug = :slug) OR p.id = :projectId" + - " LEFT JOIN project_trust pt ON u.id = pt.user_id AND pt.project_id = p.id" + - " LEFT JOIN organization_trust ot ON u.id = ot.user_id AND ot.organization_id = p.owner_id" + - " WHERE u.id = :userId") - Permission _getProjectPermission(long userId, Long projectId, String author, String slug); - default Permission getProjectPermission(long userId, long projectId) { - return _getProjectPermission(userId, projectId, null, null); + default Permission getGlobalPermission(final @NotNull String userName) { + return this._getGlobalPermission(null, userName); } - default Permission getProjectPermission(long userId, String author, String slug) { - return _getProjectPermission(userId, null, author, slug); + + @SqlQuery("SELECT (COALESCE(gt.permission, B'0'::bit(64)) | COALESCE(pt.permission, B'0'::bit(64)) | COALESCE(ot.permission, B'0'::bit(64)))::bigint AS perm_value" + + " FROM users u " + + " LEFT JOIN global_trust gt ON u.id = gt.user_id" + + " LEFT JOIN projects p ON (LOWER(p.owner_name) = LOWER(:author) AND p.slug = :slug) OR p.id = :projectId" + + " LEFT JOIN project_trust pt ON u.id = pt.user_id AND pt.project_id = p.id" + + " LEFT JOIN organization_trust ot ON u.id = ot.user_id AND ot.organization_id = p.owner_id" + + " WHERE u.id = :userId") + Permission _getProjectPermission(long userId, Long projectId, String author, String slug); + + default Permission getProjectPermission(final long userId, final long projectId) { + return this._getProjectPermission(userId, projectId, null, null); + } + + default Permission getProjectPermission(final long userId, final String author, final String slug) { + return this._getProjectPermission(userId, null, author, slug); } @ValueColumn("permission") @RegisterConstructorMapper(UserTable.class) - @SqlQuery("SELECT u.*, (coalesce(gt.permission, B'0'::bit(64)) | coalesce(pt.permission, B'0'::bit(64)) | coalesce(ot.permission, B'0'::bit(64)))::bigint AS permission" + - " FROM users u" + - " JOIN project_trust pt ON u.id = pt.user_id" + - " JOIN projects p ON pt.project_id = p.id" + - " LEFT JOIN global_trust gt ON u.id = gt.user_id" + - " LEFT JOIN organization_trust ot ON u.id = ot.user_id AND ot.organization_id = p.owner_id" + - " WHERE pt.project_id = :projectId") + @SqlQuery("SELECT u.*, (COALESCE(gt.permission, B'0'::bit(64)) | COALESCE(pt.permission, B'0'::bit(64)) | COALESCE(ot.permission, B'0'::bit(64)))::bigint AS permission" + + " FROM users u" + + " JOIN project_trust pt ON u.id = pt.user_id" + + " JOIN projects p ON pt.project_id = p.id" + + " LEFT JOIN global_trust gt ON u.id = gt.user_id" + + " LEFT JOIN organization_trust ot ON u.id = ot.user_id AND ot.organization_id = p.owner_id" + + " WHERE pt.project_id = :projectId") Map getProjectMemberPermissions(long projectId); - @SqlQuery("SELECT (coalesce(gt.permission, B'0'::BIT(64)) | coalesce(ot.permission, B'0'::BIT(64)))::BIGINT AS perm_value" + - " FROM users u " + - " LEFT JOIN organizations o ON o.name = :orgName OR o.id = :orgId" + - " LEFT JOIN global_trust gt ON u.id = gt.user_id" + - " LEFT JOIN organization_trust ot ON o.id = ot.organization_id AND ot.user_id = u.id" + - " WHERE u.id = :userId") + @SqlQuery("SELECT (COALESCE(gt.permission, B'0'::bit(64)) | COALESCE(ot.permission, B'0'::bit(64)))::bigint AS perm_value" + + " FROM users u " + + " LEFT JOIN organizations o ON o.name = :orgName OR o.id = :orgId" + + " LEFT JOIN global_trust gt ON u.id = gt.user_id" + + " LEFT JOIN organization_trust ot ON o.id = ot.organization_id AND ot.user_id = u.id" + + " WHERE u.id = :userId") Permission _getOrganizationPermission(long userId, String orgName, Long orgId); - default Permission getOrganizationPermission(long userId, String orgName) { - return _getOrganizationPermission(userId, orgName, null); - } - default Permission getOrganizationPermission(long userId, long orgId) { - return _getOrganizationPermission(userId, null, orgId); + + default Permission getOrganizationPermission(final long userId, final String orgName) { + return this._getOrganizationPermission(userId, orgName, null); } - @SqlQuery("SELECT coalesce(bit_or(r.permission), B'0'::BIT(64))::BIGINT perm_value FROM user_project_roles upr JOIN roles r ON upr.role_type = r.name WHERE upr.user_id = :userId") + default Permission getOrganizationPermission(final long userId, final long orgId) { + return this._getOrganizationPermission(userId, null, orgId); + } + + @SqlQuery("SELECT COALESCE(BIT_OR(r.permission), B'0'::bit(64))::bigint perm_value FROM user_project_roles upr JOIN roles r ON upr.role_type = r.name WHERE upr.user_id = :userId") Permission getPossibleProjectPermissions(long userId); - @SqlQuery("SELECT coalesce(bit_or(r.permission), B'0'::BIT(64))::BIGINT perm_value FROM user_organization_roles uor JOIN roles r ON uor.role_type = r.name WHERE uor.user_id = :userId") + @SqlQuery("SELECT COALESCE(BIT_OR(r.permission), B'0'::bit(64))::bigint perm_value FROM user_organization_roles uor JOIN roles r ON uor.role_type = r.name WHERE uor.user_id = :userId") Permission getPossibleOrganizationPermissions(long userId); } diff --git a/backend/src/main/java/io/papermc/hangar/db/mappers/LogActionColumnMapper.java b/backend/src/main/java/io/papermc/hangar/db/mappers/LogActionColumnMapper.java index a3cc738aa..523ebb8e1 100644 --- a/backend/src/main/java/io/papermc/hangar/db/mappers/LogActionColumnMapper.java +++ b/backend/src/main/java/io/papermc/hangar/db/mappers/LogActionColumnMapper.java @@ -11,8 +11,8 @@ import java.sql.SQLException; public class LogActionColumnMapper implements ColumnMapper> { @Override - public LogAction map(ResultSet r, int columnNumber, StatementContext ctx) throws SQLException { - String action = r.getString(columnNumber); + public LogAction map(final ResultSet r, final int columnNumber, final StatementContext ctx) throws SQLException { + final String action = r.getString(columnNumber); if (!LogAction.LOG_REGISTRY.containsKey(action)) { throw new SQLDataException(action + " is not a valid LogAction"); } diff --git a/backend/src/main/java/io/papermc/hangar/db/mappers/PermissionMapper.java b/backend/src/main/java/io/papermc/hangar/db/mappers/PermissionMapper.java index ff6785eb2..265c735f9 100644 --- a/backend/src/main/java/io/papermc/hangar/db/mappers/PermissionMapper.java +++ b/backend/src/main/java/io/papermc/hangar/db/mappers/PermissionMapper.java @@ -9,13 +9,13 @@ import java.sql.ResultSet; import java.sql.SQLException; /** - * {@link Permission} should have it's own mapper just since its essentially a wrapper for a long. + * {@link Permission} should have its own mapper just since it's essentially a wrapper for a long. */ @Component public class PermissionMapper implements ColumnMapper { @Override - public Permission map(ResultSet r, int columnNumber, StatementContext ctx) throws SQLException { + public Permission map(final ResultSet r, final int columnNumber, final StatementContext ctx) throws SQLException { return Permission.fromLong(r.getLong(columnNumber)); } } diff --git a/backend/src/main/java/io/papermc/hangar/db/mappers/factories/RoleColumnMapperFactory.java b/backend/src/main/java/io/papermc/hangar/db/mappers/factories/RoleColumnMapperFactory.java index 0276556fd..0661fd7c8 100644 --- a/backend/src/main/java/io/papermc/hangar/db/mappers/factories/RoleColumnMapperFactory.java +++ b/backend/src/main/java/io/papermc/hangar/db/mappers/factories/RoleColumnMapperFactory.java @@ -12,11 +12,10 @@ import java.util.Optional; public class RoleColumnMapperFactory implements ColumnMapperFactory { @Override - public Optional> build(Type type, ConfigRegistry config) { - if (!(type instanceof Class) || !Role.class.isAssignableFrom((Class) type) || !((Class) type).isEnum()) { + public Optional> build(final Type type, final ConfigRegistry config) { + if (!(type instanceof final Class clazz) || !Role.class.isAssignableFrom((Class) type) || !((Class) type).isEnum()) { return Optional.empty(); } - Class clazz = (Class) type; if (clazz == GlobalRole.class) { return Optional.of((r, columnNumber, ctx) -> Role.ID_ROLES.get(r.getLong(columnNumber))); } else { diff --git a/backend/src/main/java/io/papermc/hangar/model/common/NamedPermission.java b/backend/src/main/java/io/papermc/hangar/model/common/NamedPermission.java index 2c93b4875..49bf75404 100644 --- a/backend/src/main/java/io/papermc/hangar/model/common/NamedPermission.java +++ b/backend/src/main/java/io/papermc/hangar/model/common/NamedPermission.java @@ -1,11 +1,13 @@ package io.papermc.hangar.model.common; import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonValue; import java.util.List; import java.util.stream.Collectors; +@JsonFormat(shape = JsonFormat.Shape.OBJECT) public enum NamedPermission { VIEW_PUBLIC_INFO("view_public_info", Permission.ViewPublicInfo, "ViewPublicInfo"), EDIT_OWN_USER_SETTINGS("edit_own_user_settings", Permission.EditOwnUserSettings, "EditOwnUserSettings"), diff --git a/backend/src/main/java/io/papermc/hangar/model/common/Permission.java b/backend/src/main/java/io/papermc/hangar/model/common/Permission.java index 6057b0dfa..83e7fdca8 100644 --- a/backend/src/main/java/io/papermc/hangar/model/common/Permission.java +++ b/backend/src/main/java/io/papermc/hangar/model/common/Permission.java @@ -67,19 +67,13 @@ public class Permission implements Comparable, Argument { public static final Permission RestoreVersion = new Permission(1L << 44); public static final Permission RestoreProject = new Permission(1L << 45); - private long value; + private final long value; @JdbiConstructor public Permission(long value) { this.value = value; } - public Permission() { } - - public void setValue(long value) { - this.value = value; - } - public Permission add(Permission other) { return new Permission(value | other.value); } @@ -117,6 +111,7 @@ public class Permission implements Comparable, Argument { public boolean isNone() { return value == 0; } + @JsonValue public String toBinString() { return Long.toBinaryString(value); diff --git a/backend/src/main/java/io/papermc/hangar/model/common/roles/GlobalRole.java b/backend/src/main/java/io/papermc/hangar/model/common/roles/GlobalRole.java index 79773192f..98aa1e7b0 100644 --- a/backend/src/main/java/io/papermc/hangar/model/common/roles/GlobalRole.java +++ b/backend/src/main/java/io/papermc/hangar/model/common/roles/GlobalRole.java @@ -24,6 +24,8 @@ public enum GlobalRole implements Role { ORGANIZATION("Organization", 100, OrganizationRole.ORGANIZATION_OWNER.getPermissions(), "Organization", Color.PURPLE); + private static final GlobalRole[] VALUES = GlobalRole.values(); + private final String value; private final long roleId; private final Permission permissions; @@ -104,4 +106,8 @@ public enum GlobalRole implements Role { } throw new IllegalArgumentException("No GlobalRole '" + apiValue + "'"); } + + public static GlobalRole[] getValues() { + return VALUES; + } } diff --git a/backend/src/main/java/io/papermc/hangar/model/internal/api/responses/Validation.java b/backend/src/main/java/io/papermc/hangar/model/internal/api/responses/Validation.java new file mode 100644 index 000000000..c167da5ce --- /dev/null +++ b/backend/src/main/java/io/papermc/hangar/model/internal/api/responses/Validation.java @@ -0,0 +1,25 @@ +package io.papermc.hangar.model.internal.api.responses; + +import com.fasterxml.jackson.annotation.JsonInclude; +import io.papermc.hangar.util.PatternWrapper; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.framework.qual.DefaultQualifier; + +@DefaultQualifier(NonNull.class) +@JsonInclude(JsonInclude.Include.NON_NULL) +public record Validation(@Nullable String regex, @Nullable Integer max, @Nullable Integer min) { + + public Validation(final PatternWrapper wrapper, final @Nullable Integer max, final @Nullable Integer min) { + this(wrapper.strPattern(), max, min); + } + + public static Validation max(final int max) { + return new Validation((String) null, max, null); + } + + public static Validation size(final int max, final int min) { + return new Validation((String) null, max, min); + } +} + diff --git a/backend/src/main/java/io/papermc/hangar/model/internal/api/responses/Validations.java b/backend/src/main/java/io/papermc/hangar/model/internal/api/responses/Validations.java new file mode 100644 index 000000000..1e557b865 --- /dev/null +++ b/backend/src/main/java/io/papermc/hangar/model/internal/api/responses/Validations.java @@ -0,0 +1,49 @@ +package io.papermc.hangar.model.internal.api.responses; + +import io.papermc.hangar.config.hangar.HangarConfig; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.framework.qual.DefaultQualifier; + +@DefaultQualifier(NonNull.class) +public record Validations( + Project project, + Validation userTagline, + Validation version, + Validation org, + int maxOrgCount, + String urlRegex +) { + + public record Project( + Validation name, + Validation desc, + Validation keywords, + Validation channels, + Validation pageName, + Validation pageContent, + int maxPageCount, + int maxChannelCount + ) { + } + + public static Validations create(final HangarConfig config) { + final Project project = new Project( + config.projects.projectName(), + config.projects.projectDescription(), + config.projects.projectKeywords(), + config.channels.channelName(), + config.pages.pageName(), + config.pages.pageContent(), + config.projects.maxPages(), + config.projects.maxChannels() + ); + return new Validations( + project, + config.user.userTagline(), + config.projects.versionName(), + config.org.orgName(), + config.org.createLimit(), + config.getUrlRegex() + ); + } +} diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 56a2207bd..2e3784c02 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -43,7 +43,7 @@ spring: # Fake User # ############# fake-user: - enabled: false + enabled: true username: paper email: paper@papermc.io # id: -3 diff --git a/frontend/.husky/pre-commit b/frontend/.husky/pre-commit index 55ee6881f..176420aa7 100755 --- a/frontend/.husky/pre-commit +++ b/frontend/.husky/pre-commit @@ -3,4 +3,5 @@ cd frontend pnpm prettier --write src/types/generated/**.* +git add src/types/generated/**.* pnpm lint-staged diff --git a/frontend/src/lib b/frontend/src/lib index 2b34cb6ea..03f449ccf 160000 --- a/frontend/src/lib +++ b/frontend/src/lib @@ -1 +1 @@ -Subproject commit 2b34cb6eafc8ad015d349e05a5420acd539e75ff +Subproject commit 03f449ccfbbf9c813f4675ec6505ddc818910944 diff --git a/frontend/src/types/generated/icons.d.ts b/frontend/src/types/generated/icons.d.ts index f60bde9cd..9577e865c 100644 --- a/frontend/src/types/generated/icons.d.ts +++ b/frontend/src/types/generated/icons.d.ts @@ -7,13 +7,9 @@ export {}; declare module "@vue/runtime-core" { export interface GlobalComponents { - IconMdiAccountPlus: typeof import("~icons/mdi/account-plus")["default"]; IconMdiAlert: typeof import("~icons/mdi/alert")["default"]; IconMdiAlertBox: typeof import("~icons/mdi/alert-box")["default"]; - IconMdiAlertCircleOutline: typeof import("~icons/mdi/alert-circle-outline")["default"]; IconMdiAlertOutline: typeof import("~icons/mdi/alert-outline")["default"]; - IconMdiBell: typeof import("~icons/mdi/bell")["default"]; - IconMdiBellOutline: typeof import("~icons/mdi/bell-outline")["default"]; IconMdiCalendar: typeof import("~icons/mdi/calendar")["default"]; IconMdiCancel: typeof import("~icons/mdi/cancel")["default"]; IconMdiCashMultiple: typeof import("~icons/mdi/cash-multiple")["default"]; @@ -24,33 +20,24 @@ declare module "@vue/runtime-core" { IconMdiClipboardOutline: typeof import("~icons/mdi/clipboard-outline")["default"]; IconMdiClose: typeof import("~icons/mdi/close")["default"]; IconMdiCodeBracesBox: typeof import("~icons/mdi/code-braces-box")["default"]; - IconMdiContentSave: typeof import("~icons/mdi/content-save")["default"]; IconMdiController: typeof import("~icons/mdi/controller")["default"]; - IconMdiDelete: typeof import("~icons/mdi/delete")["default"]; IconMdiDownload: typeof import("~icons/mdi/download")["default"]; - IconMdiDownloadOutline: typeof import("~icons/mdi/download-outline")["default"]; IconMdiEarth: typeof import("~icons/mdi/earth")["default"]; IconMdiEye: typeof import("~icons/mdi/eye")["default"]; IconMdiEyeOff: typeof import("~icons/mdi/eye-off")["default"]; IconMdiFeather: typeof import("~icons/mdi/feather")["default"]; - IconMdiFlag: typeof import("~icons/mdi/flag")["default"]; IconMdiGamepadRoundLeft: typeof import("~icons/mdi/gamepad-round-left")["default"]; IconMdiGavel: typeof import("~icons/mdi/gavel")["default"]; IconMdiHelp: typeof import("~icons/mdi/help")["default"]; - IconMdiHome: typeof import("~icons/mdi/home")["default"]; IconMdiHorseVariant: typeof import("~icons/mdi/horse-variant")["default"]; IconMdiInformation: typeof import("~icons/mdi/information")["default"]; IconMdiKeyOutline: typeof import("~icons/mdi/key-outline")["default"]; IconMdiMenu: typeof import("~icons/mdi/menu")["default"]; - IconMdiMenuDown: typeof import("~icons/mdi/menu-down")["default"]; IconMdiOpenInNew: typeof import("~icons/mdi/open-in-new")["default"]; - IconMdiPencil: typeof import("~icons/mdi/pencil")["default"]; - IconMdiPlus: typeof import("~icons/mdi/plus")["default"]; IconMdiShape: typeof import("~icons/mdi/shape")["default"]; IconMdiShieldSun: typeof import("~icons/mdi/shield-sun")["default"]; IconMdiSortVariant: typeof import("~icons/mdi/sort-variant")["default"]; IconMdiStar: typeof import("~icons/mdi/star")["default"]; - IconMdiStarOutline: typeof import("~icons/mdi/star-outline")["default"]; IconMdiTools: typeof import("~icons/mdi/tools")["default"]; IconMdiTrophy: typeof import("~icons/mdi/trophy")["default"]; IconMdiWeatherNight: typeof import("~icons/mdi/weather-night")["default"];