validation work

This commit is contained in:
Jake Potrebic 2022-11-20 13:29:04 -08:00
parent 4e7a66d929
commit 31fe7bb53e
No known key found for this signature in database
GPG Key ID: 27CC63F7CBC866C7
21 changed files with 256 additions and 187 deletions

View File

@ -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) {

View File

@ -126,7 +126,7 @@ public class WebConfig extends WebMvcConfigurationSupport {
@Override
public void configureMessageConverters(@NotNull List<HttpMessageConverter<?>> 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()

View File

@ -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);
}
}

View File

@ -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());
}
}

View File

@ -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");
}
}
}

View File

@ -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);
}
}

View File

@ -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<String> staffRoles
) {
public Validation userTagline() {
return Validation.max(this.maxTaglineLen());
}
}

View File

@ -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> gitProperties;
@Autowired
public BackendDataController(ObjectMapper mapper, HangarConfig config, PlatformService platformService, Optional<GitProperties> gitProperties) {
public BackendDataController(final ObjectMapper mapper, final HangarConfig config, final PlatformService platformService, final Optional<GitProperties> 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 extends Annotation> A _findAnnotation(Annotated annotated, Class<A> annoClass) {
if (!annotated.hasAnnotation(JsonValue.class)) {
return super._findAnnotation(annotated, annoClass);
protected <A extends Annotation> A _findAnnotation(final Annotated annotated, final Class<A> 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<ArrayNode> getCategories() {
return ResponseEntity.ok(noJsonValueMapper.valueToTree(Category.getValues()));
return ResponseEntity.ok(this.objectMapper.valueToTree(Category.getValues()));
}
@GetMapping("/permissions")
@Cacheable("permissions")
public ResponseEntity<ArrayNode> 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<ArrayNode> 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<ArrayNode> 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<ArrayNode> 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<HangarConfig.Sponsor> 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<List<Announcement>> getAnnouncements() {
return ResponseEntity.ok(config.getAnnouncements());
@ResponseBody
public List<Announcement> getAnnouncements() {
return this.config.getAnnouncements();
}
@GetMapping("/projectRoles")
@Cacheable("projectRoles")
public ResponseEntity<List<ProjectRole>> getAssignableProjectRoles() {
return ResponseEntity.ok(ProjectRole.getAssignableRoles());
@ResponseBody
public List<ProjectRole> getAssignableProjectRoles() {
return ProjectRole.getAssignableRoles();
}
@GetMapping("/globalRoles")
@Cacheable("globalRoles")
public ResponseEntity<List<GlobalRole>> getGlobalRoles() {
return ResponseEntity.ok(Arrays.stream(GlobalRole.values()).toList());
@ResponseBody
public GlobalRole[] getGlobalRoles() {
return GlobalRole.getValues();
}
@GetMapping("/orgRoles")
@Cacheable("orgRoles")
public ResponseEntity<List<OrganizationRole>> getAssignableOrganizationRoles() {
return ResponseEntity.ok(OrganizationRole.getAssignableRoles());
@ResponseBody
public List<OrganizationRole> getAssignableOrganizationRoles() {
return OrganizationRole.getAssignableRoles();
}
@GetMapping("/licenses")
@Cacheable("licenses")
public ResponseEntity<List<String>> getLicenses() {
return ResponseEntity.ok(config.getLicenses());
@ResponseBody
public List<String> getLicenses() {
return this.config.getLicenses();
}
@GetMapping("/visibilities")
@Cacheable("visibilities")
public ResponseEntity<ArrayNode> 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<ObjectNode> 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);
}
}

View File

@ -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<UserTable, Permission> 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);
}

View File

@ -11,8 +11,8 @@ import java.sql.SQLException;
public class LogActionColumnMapper implements ColumnMapper<LogAction<?>> {
@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");
}

View File

@ -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<Permission> {
@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));
}
}

View File

@ -12,11 +12,10 @@ import java.util.Optional;
public class RoleColumnMapperFactory implements ColumnMapperFactory {
@Override
public Optional<ColumnMapper<?>> build(Type type, ConfigRegistry config) {
if (!(type instanceof Class) || !Role.class.isAssignableFrom((Class<?>) type) || !((Class<?>) type).isEnum()) {
public Optional<ColumnMapper<?>> 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 {

View File

@ -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"),

View File

@ -67,19 +67,13 @@ public class Permission implements Comparable<Permission>, 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<Permission>, Argument {
public boolean isNone() {
return value == 0;
}
@JsonValue
public String toBinString() {
return Long.toBinaryString(value);

View File

@ -24,6 +24,8 @@ public enum GlobalRole implements Role<GlobalRoleTable> {
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<GlobalRoleTable> {
}
throw new IllegalArgumentException("No GlobalRole '" + apiValue + "'");
}
public static GlobalRole[] getValues() {
return VALUES;
}
}

View File

@ -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);
}
}

View File

@ -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()
);
}
}

View File

@ -43,7 +43,7 @@ spring:
# Fake User #
#############
fake-user:
enabled: false
enabled: true
username: paper
email: paper@papermc.io
# id: -3

View File

@ -3,4 +3,5 @@
cd frontend
pnpm prettier --write src/types/generated/**.*
git add src/types/generated/**.*
pnpm lint-staged

@ -1 +1 @@
Subproject commit 2b34cb6eafc8ad015d349e05a5420acd539e75ff
Subproject commit 03f449ccfbbf9c813f4675ec6505ddc818910944

View File

@ -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"];