convert almost all custom config objects to records

This commit is contained in:
Jake Potrebic 2022-11-06 19:04:30 -08:00
parent 03352e47cb
commit 1ec34b350b
No known key found for this signature in database
GPG Key ID: ECE0B3C133C016C5
45 changed files with 267 additions and 850 deletions

View File

@ -1,49 +1,19 @@
package io.papermc.hangar.config.hangar;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
import org.springframework.boot.convert.DurationUnit;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
import org.springframework.boot.context.properties.bind.DefaultValue;
import org.springframework.boot.convert.DurationUnit;
@Component
@ConfigurationProperties(prefix = "hangar.api")
public class ApiConfig {
public record ApiConfig(@NestedConfigurationProperty Session session) { // TODO is this used anywhere now?
@NestedConfigurationProperty
public Session session;
@Autowired
public ApiConfig(Session session) {
this.session = session;
}
@Component
@ConfigurationProperties(prefix = "hangar.api.session")
public static class Session {
@DurationUnit(ChronoUnit.HOURS)
private Duration publicExpiration = Duration.ofHours(3);
@DurationUnit(ChronoUnit.DAYS)
private Duration expiration = Duration.ofDays(14);
public Duration getPublicExpiration() {
return publicExpiration;
}
public void setPublicExpiration(Duration publicExpiration) {
this.publicExpiration = publicExpiration;
}
public Duration getExpiration() {
return expiration;
}
public void setExpiration(Duration expiration) {
this.expiration = expiration;
}
public record Session( // TODO is this used anywhere now?
@DurationUnit(ChronoUnit.HOURS) @DefaultValue("3") Duration publicExpiration,
@DurationUnit(ChronoUnit.DAYS) @DefaultValue("14") Duration expiration
) {
}
}

View File

@ -1,52 +1,18 @@
package io.papermc.hangar.config.hangar;
import io.papermc.hangar.model.common.Color;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import javax.validation.constraints.Min;
import javax.validation.constraints.Size;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.DefaultValue;
@Component
@ConfigurationProperties(prefix = "hangar.channels")
public class ChannelsConfig {
@Size(min = 1)
private int maxNameLen = 15;
private String nameRegex = "^[a-zA-Z0-9]+$";
private Color colorDefault = Color.CYAN;
@Size(min = 1, max = 15)
private String nameDefault = "Release";
public int getMaxNameLen() {
return maxNameLen;
}
public void setMaxNameLen(int maxNameLen) {
this.maxNameLen = maxNameLen;
}
public String getNameRegex() {
return nameRegex;
}
public void setNameRegex(String nameRegex) {
this.nameRegex = nameRegex;
}
public Color getColorDefault() {
return colorDefault;
}
public void setColorDefault(Color colorDefault) {
this.colorDefault = colorDefault;
}
public String getNameDefault() {
return nameDefault;
}
public void setNameDefault(String nameDefault) {
this.nameDefault = nameDefault;
}
public record ChannelsConfig(
@Min(1) @DefaultValue("15") int maxNameLen,
@DefaultValue("^[a-zA-Z0-9]+$") String 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);

View File

@ -1,64 +1,15 @@
package io.papermc.hangar.config.hangar;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import org.springframework.boot.context.properties.bind.DefaultValue;
@Component
@ConfigurationProperties(prefix = "hangar.discourse")
public class DiscourseConfig {
private boolean enabled = false;
private String url = "https://papermc.io/forums/";
private String adminUser;
private String apiKey;
private int category;
private int categoryDeleted;
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getAdminUser() {
return adminUser;
}
public void setAdminUser(String adminUser) {
this.adminUser = adminUser;
}
public String getApiKey() {
return apiKey;
}
public void setApiKey(String apiKey) {
this.apiKey = apiKey;
}
public int getCategory() {
return category;
}
public void setCategory(int category) {
this.category = category;
}
public int getCategoryDeleted() {
return categoryDeleted;
}
public void setCategoryDeleted(int categoryDeleted) {
this.categoryDeleted = categoryDeleted;
}
public record DiscourseConfig(
@DefaultValue("false") boolean enabled,
@DefaultValue("https://papermc.io/forums/") String url,
String adminUser,
String apiKey,
int category,
int categoryDeleted
) {
}

View File

@ -1,37 +1,12 @@
package io.papermc.hangar.config.hangar;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import org.springframework.boot.context.properties.bind.DefaultValue;
@Component
@ConfigurationProperties(prefix = "fake-user")
public class FakeUserConfig {
private boolean enabled = true;
private String username = "paper";
private String email = "paper@papermc.io";
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public record FakeUserConfig(
@DefaultValue("true") boolean enabled,
@DefaultValue("paper") String username,
@DefaultValue("paper@papermc.io") String email
) {
}

View File

@ -8,138 +8,32 @@ import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
import org.springframework.boot.context.properties.bind.DefaultValue;
import org.springframework.boot.convert.DurationUnit;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "hangar.security")
public class HangarSecurityConfig {
private boolean secure = false;
private long unsafeDownloadMaxAge = 600000;
private List<String> safeDownloadHosts = List.of();
private String imageProxyUrl = "http://localhost:3001/image/%s";
private String tokenIssuer;
private String tokenSecret;
@DurationUnit(ChronoUnit.SECONDS)
private Duration tokenExpiry;
@DurationUnit(ChronoUnit.DAYS)
private Duration refreshTokenExpiry;
@NestedConfigurationProperty
public SecurityApiConfig api;
@Autowired
public HangarSecurityConfig(SecurityApiConfig api) {
this.api = api;
}
@Component
public record HangarSecurityConfig(
@DefaultValue("true") boolean secure,
@DefaultValue("600000") long unsafeDownloadMaxAge, // TODO implement or remove
@DefaultValue List<String> safeDownloadHosts,
@DefaultValue("http://localhost:3001/image/%s") String imageProxyUrl,
String tokenIssuer,
String tokenSecret,
@DurationUnit(ChronoUnit.SECONDS) Duration tokenExpiry,
@DurationUnit(ChronoUnit.DAYS) Duration refreshTokenExpiry,
@NestedConfigurationProperty SecurityApiConfig api
) {
@ConfigurationProperties(prefix = "hangar.security.api")
public static class SecurityApiConfig {
private String url = "http://localhost:8081";
private String avatarUrl = "http://localhost:8081/avatar/%s";
private long timeout = 10000;
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getAvatarUrl() {
return avatarUrl;
}
public void setAvatarUrl(String avatarUrl) {
this.avatarUrl = avatarUrl;
}
public long getTimeout() {
return timeout;
}
public void setTimeout(long timeout) {
this.timeout = timeout;
}
public record SecurityApiConfig(
@DefaultValue("http://localhost:8081") String url,
@DefaultValue("http://localhost:8081/avatar/%s") String avatarUrl,
@DefaultValue("10000") long timeout // TODO implement or remove
) {
}
public Duration getTokenExpiry() {
return tokenExpiry;
}
public void setTokenExpiry(Duration tokenExpiry) {
this.tokenExpiry = tokenExpiry;
}
public Duration getRefreshTokenExpiry() {
return refreshTokenExpiry;
}
public void setRefreshTokenExpiry(Duration refreshTokenExpiry) {
this.refreshTokenExpiry = refreshTokenExpiry;
}
public String getTokenIssuer() {
return tokenIssuer;
}
public void setTokenIssuer(String tokenIssuer) {
this.tokenIssuer = tokenIssuer;
}
public String getTokenSecret() {
return tokenSecret;
}
public void setTokenSecret(String tokenSecret) {
this.tokenSecret = tokenSecret;
}
public boolean isSecure() {
return secure;
}
public void setSecure(boolean secure) {
this.secure = secure;
}
public long getUnsafeDownloadMaxAge() {
return unsafeDownloadMaxAge;
}
public void setUnsafeDownloadMaxAge(long unsafeDownloadMaxAge) {
this.unsafeDownloadMaxAge = unsafeDownloadMaxAge;
}
public List<String> getSafeDownloadHosts() {
return safeDownloadHosts;
}
public void setSafeDownloadHosts(List<String> safeDownloadHosts) {
this.safeDownloadHosts = safeDownloadHosts;
}
public String getImageProxyUrl() {
return imageProxyUrl;
}
public void setImageProxyUrl(String imageProxyUrl) {
this.imageProxyUrl = imageProxyUrl;
}
public SecurityApiConfig getApi() {
return api;
}
public void setApi(SecurityApiConfig api) {
this.api = api;
}
public boolean checkSafe(String url) {
return safeDownloadHosts.contains(URI.create(url).getHost());
public boolean checkSafe(final String url) {
return this.safeDownloadHosts.contains(URI.create(url).getHost());
}
public boolean isSafeHost(final String host) {
@ -160,25 +54,25 @@ public class HangarSecurityConfig {
if (uri.getScheme().equals("mailto")) {
return true;
}
} else if (host == null || isSafeHost(host)) {
} else if (host == null || this.isSafeHost(host)) {
return true;
}
} catch (URISyntaxException ignored) {
} catch (final URISyntaxException ignored) {
}
return false;
}
public String makeSafe(final String urlString) {
if (isSafe(urlString)) {
if (this.isSafe(urlString)) {
return urlString;
}
return "/linkout?remoteUrl=" + urlString;
}
public String proxyImage(String urlString) {
if (isSafe(urlString)) {
public String proxyImage(final String urlString) {
if (this.isSafe(urlString)) {
return urlString;
}
return String.format(imageProxyUrl, urlString);
return String.format(this.imageProxyUrl(), urlString);
}
}

View File

@ -1,24 +1,11 @@
package io.papermc.hangar.config.hangar;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.convert.DurationUnit;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.DefaultValue;
import org.springframework.boot.convert.DurationUnit;
@Component
@ConfigurationProperties(prefix = "hangar.homepage")
public class HomepageConfig {
@DurationUnit(ChronoUnit.MINUTES)
private Duration updateInterval = Duration.ofMinutes(10);
public Duration getUpdateInterval() {
return updateInterval;
}
public void setUpdateInterval(Duration updateInterval) {
this.updateInterval = updateInterval;
}
public record HomepageConfig(@DurationUnit(ChronoUnit.MINUTES) @DefaultValue("10") Duration updateInterval) {
}

View File

@ -1,67 +1,17 @@
package io.papermc.hangar.config.hangar;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.convert.DurationUnit;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.DefaultValue;
import org.springframework.boot.convert.DurationUnit;
@Component
@ConfigurationProperties(prefix = "hangar.jobs")
public class JobsConfig {
@DurationUnit(ChronoUnit.MINUTES)
private Duration checkInterval = Duration.ofMinutes(1);
@DurationUnit(ChronoUnit.MINUTES)
private Duration unknownErrorTimeout = Duration.ofMinutes(15);
@DurationUnit(ChronoUnit.MINUTES)
private Duration statusErrorTimeout = Duration.ofMinutes(5);
@DurationUnit(ChronoUnit.MINUTES)
private Duration notAvailableTimeout = Duration.ofMinutes(2);
private int maxConcurrentJobs = 32;
public Duration getCheckInterval() {
return checkInterval;
}
public void setCheckInterval(Duration checkInterval) {
this.checkInterval = checkInterval;
}
public Duration getUnknownErrorTimeout() {
return unknownErrorTimeout;
}
public void setUnknownErrorTimeout(Duration unknownErrorTimeout) {
this.unknownErrorTimeout = unknownErrorTimeout;
}
public Duration getStatusErrorTimeout() {
return statusErrorTimeout;
}
public void setStatusErrorTimeout(Duration statusErrorTimeout) {
this.statusErrorTimeout = statusErrorTimeout;
}
public Duration getNotAvailableTimeout() {
return notAvailableTimeout;
}
public void setNotAvailableTimeout(Duration notAvailableTimeout) {
this.notAvailableTimeout = notAvailableTimeout;
}
public int getMaxConcurrentJobs() {
return maxConcurrentJobs;
}
public void setMaxConcurrentJobs(int maxConcurrentJobs) {
this.maxConcurrentJobs = maxConcurrentJobs;
}
public record JobsConfig(
@DurationUnit(ChronoUnit.MINUTES) @DefaultValue("1") Duration checkInterval,
@DurationUnit(ChronoUnit.MINUTES) @DefaultValue("15") Duration unknownErrorTimeout,
@DurationUnit(ChronoUnit.MINUTES) @DefaultValue("5") Duration statusErrorTimeout,
@DurationUnit(ChronoUnit.MINUTES) @DefaultValue("2") Duration notAvailableTimeout,
@DefaultValue("32") int maxConcurrentJobs
) {
}

View File

@ -1,71 +1,19 @@
package io.papermc.hangar.config.hangar;
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;
@Component
@ConfigurationProperties(prefix = "hangar.orgs")
public class OrganizationsConfig {
private boolean enabled = true;
private String dummyEmailDomain = "org.papermc.io";
private int createLimit = 5;
private int minNameLen = 3;
private int maxNameLen = 20;
private String nameRegex = "[a-zA-Z0-9-_]*";
private final Predicate<String> namePredicate = Pattern.compile(nameRegex).asMatchPredicate();
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public String getDummyEmailDomain() {
return dummyEmailDomain;
}
public void setDummyEmailDomain(String dummyEmailDomain) {
this.dummyEmailDomain = dummyEmailDomain;
}
public int getCreateLimit() {
return createLimit;
}
public void setCreateLimit(int createLimit) {
this.createLimit = createLimit;
}
public int getMinNameLen() {
return minNameLen;
}
public void setMinNameLen(int minNameLen) {
this.minNameLen = minNameLen;
}
public String getNameRegex() {
return nameRegex;
}
public void setNameRegex(String nameRegex) {
this.nameRegex = nameRegex;
}
public int getMaxNameLen() {
return maxNameLen;
}
public void setMaxNameLen(int maxNameLen) {
this.maxNameLen = maxNameLen;
}
public boolean testName(String name) {
return namePredicate.test(name);
}
public record OrganizationsConfig(
@DefaultValue("true") boolean enabled,
@DefaultValue("org.papermc.io") String dummyEmailDomain,
@DefaultValue("5") int createLimit,
@DefaultValue("3") int minNameLen,
@DefaultValue("20") int maxNameLen,
@DefaultValue("[a-zA-Z0-9-_]*") String nameRegex
) {
}

View File

@ -1,242 +1,36 @@
package io.papermc.hangar.config.hangar;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.convert.DurationUnit;
import org.springframework.stereotype.Component;
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;
@Component
@ConfigurationProperties(prefix = "hangar.projects")
public class ProjectsConfig {
private String nameRegex = "^[a-zA-Z0-9-_]{3,}$";
private String versionNameRegex = "^[a-zA-Z0-9-_.+]+$";
private String pageNameRegex = "^[a-zA-Z0-9-_.]+$";
private Pattern namePattern = Pattern.compile(this.nameRegex);
private Pattern versionNamePattern = Pattern.compile(this.versionNameRegex);
private Pattern pageNamePattern = Pattern.compile(this.versionNameRegex);
private int maxNameLen = 25;
private int maxVersionNameLen = 30;
private int maxDependencies = 100;
private int maxPageNameLen = 25;
private int maxPages = 50;
private int maxChannels = 5;
private int maxBBCodeLen = 30_000;
private int initLoad = 25;
private int initVersionLoad = 10;
private int maxDescLen = 120;
private int maxSponsorsLen = 500;
private int maxKeywords = 5;
private int contentMaxLen = 1_000_000;
private boolean fileValidate = true;
@DurationUnit(ChronoUnit.DAYS)
private Duration staleAge = Duration.ofDays(28);
private String checkInterval = "1h";
private String draftExpire = "1d";
private int userGridPageSize = 30;
@DurationUnit(ChronoUnit.MINUTES)
private Duration unsafeDownloadMaxAge = Duration.ofMinutes(10);
private boolean showUnreviewedDownloadWarning;
public String getNameRegex() {
return nameRegex;
}
public Predicate<String> getNameMatcher() {
return namePattern.asMatchPredicate();
}
public Predicate<String> getVersionNameMatcher() {
return versionNamePattern.asMatchPredicate();
}
public Predicate<String> getPageNameMatcher() {
return pageNamePattern.asMatchPredicate();
}
public void setNameRegex(String nameRegex) {
this.nameRegex = nameRegex;
this.namePattern = Pattern.compile(nameRegex);
}
public String getVersionNameRegex() {
return versionNameRegex;
}
public void setVersionNameRegex(String versionNameRegex) {
this.versionNameRegex = versionNameRegex;
this.versionNamePattern = Pattern.compile(versionNameRegex);
}
public String getPageNameRegex() {
return pageNameRegex;
}
public void setPageNameRegex(String pageNameRegex) {
this.pageNameRegex = pageNameRegex;
}
public int getMaxNameLen() {
return maxNameLen;
}
public void setMaxNameLen(int maxNameLen) {
this.maxNameLen = maxNameLen;
}
public int getMaxVersionNameLen() {
return maxVersionNameLen;
}
public int getMaxDependencies() {
return maxDependencies;
}
public void setMaxDependencies(final int maxDependencies) {
this.maxDependencies = maxDependencies;
}
public void setMaxVersionNameLen(int maxVersionNameLen) {
this.maxVersionNameLen = maxVersionNameLen;
}
public int getMaxPageNameLen() {
return maxPageNameLen;
}
public void setMaxPageNameLen(int maxPageNameLen) {
this.maxPageNameLen = maxPageNameLen;
}
public int getMaxPages() {
return maxPages;
}
public void setMaxPages(int maxPages) {
this.maxPages = maxPages;
}
public int getMaxChannels() {
return maxChannels;
}
public void setMaxChannels(int maxChannels) {
this.maxChannels = maxChannels;
}
public void setMaxBBCodeLen(int maxBBCodeLen) {
this.maxBBCodeLen = maxBBCodeLen;
}
public int getMaxBBCodeLen() {
return maxBBCodeLen;
}
public int getMaxSponsorsLen() {
return maxSponsorsLen;
}
public void setMaxSponsorsLen(final int maxSponsorsLen) {
this.maxSponsorsLen = maxSponsorsLen;
}
public int getInitLoad() {
return initLoad;
}
public void setInitLoad(int initLoad) {
this.initLoad = initLoad;
}
public int getInitVersionLoad() {
return initVersionLoad;
}
public void setInitVersionLoad(int initVersionLoad) {
this.initVersionLoad = initVersionLoad;
}
public int getMaxDescLen() {
return maxDescLen;
}
public void setMaxDescLen(int maxDescLen) {
this.maxDescLen = maxDescLen;
}
public boolean isFileValidate() {
return fileValidate;
}
public void setFileValidate(boolean fileValidate) {
this.fileValidate = fileValidate;
}
public Duration getStaleAge() {
return staleAge;
}
public void setStaleAge(Duration staleAge) {
this.staleAge = staleAge;
}
public String getCheckInterval() {
return checkInterval;
}
public void setCheckInterval(String checkInterval) {
this.checkInterval = checkInterval;
}
public String getDraftExpire() {
return draftExpire;
}
public void setDraftExpire(String draftExpire) {
this.draftExpire = draftExpire;
}
public int getUserGridPageSize() {
return userGridPageSize;
}
public void setUserGridPageSize(int userGridPageSize) {
this.userGridPageSize = userGridPageSize;
}
public int getMaxKeywords() {
return maxKeywords;
}
public void setMaxKeywords(int maxKeywords) {
this.maxKeywords = maxKeywords;
}
public Duration getUnsafeDownloadMaxAge() {
return unsafeDownloadMaxAge;
}
public void setUnsafeDownloadMaxAge(Duration unsafeDownloadMaxAage) {
this.unsafeDownloadMaxAge = unsafeDownloadMaxAage;
}
public boolean showUnreviewedDownloadWarning() {
return showUnreviewedDownloadWarning;
}
public void setShowUnreviewedDownloadWarning(boolean showUnreviewedDownloadWarning) {
this.showUnreviewedDownloadWarning = showUnreviewedDownloadWarning;
}
public int getContentMaxLen() {
return contentMaxLen;
}
public void setContentMaxLen(int contentMaxLen) {
this.contentMaxLen = contentMaxLen;
}
public record ProjectsConfig( // TODO split into ProjectsConfig and VersionsConfig
@DefaultValue("^[a-zA-Z0-9-_]{3,}$") PatternWrapper nameRegex,
@DefaultValue("^[a-zA-Z0-9-_.+]+$") PatternWrapper versionNameRegex,
@DefaultValue("25") int maxNameLen,
@DefaultValue("30") int maxVersionNameLen,
@DefaultValue("100") int maxDependencies,
@DefaultValue("50") int maxPages,
@DefaultValue("5") int maxChannels,
@DefaultValue("30000") int maxBBCodeLen,
@DefaultValue("25") int initLoad,
@DefaultValue("10") int initVersionLoad, // TODO implement (see @ConfigurePagination)
@DefaultValue("120") int maxDescLen,
@DefaultValue("500") int maxSponsorsLen,
@DefaultValue("5") int maxKeywords,
@DefaultValue("1000000") int contentMaxLen,
@DefaultValue("true") boolean fileValidate, // TODO implement or remove
@DefaultValue("28") @DurationUnit(ChronoUnit.DAYS) Duration staleAge,
@DefaultValue("1h") String checkInterval, // TODO implement or remove
@DefaultValue("1d") String draftExpire, // TODO implement or remove
@DefaultValue("30") int userGridPageSize, // TODO implement or remove
@DefaultValue("10") @DurationUnit(ChronoUnit.MINUTES) Duration unsafeDownloadMaxAge,
@DefaultValue("false") boolean showUnreviewedDownloadWarning
) {
}

View File

@ -2,6 +2,8 @@ package io.papermc.hangar.config.hangar;
import io.awspring.cloud.autoconfigure.core.AwsProperties;
import io.papermc.hangar.HangarApplication;
import org.springframework.boot.context.properties.bind.DefaultValue;
import org.springframework.context.annotation.Configuration;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.regions.Region;
@ -13,100 +15,37 @@ import org.springframework.stereotype.Component;
import java.net.URI;
import java.net.URISyntaxException;
@Component
@ConfigurationProperties(prefix = "hangar.storage")
public class StorageConfig {
public record StorageConfig(
@DefaultValue("local") String type,
@DefaultValue("backend/work") String workDir,
String accessKey,
String secretKey,
String bucket,
String objectStorageEndpoint,
String cdnEndpoint,
@DefaultValue("true") boolean cdnIncludeBucket
) {
// type = local or object
private String type = "local";
// local
private String pluginUploadDir = new ApplicationHome(HangarApplication.class).getDir().toPath().resolve("work").toString();
// object
private String accessKey;
private String secretKey;
private String bucket;
private String objectStorageEndpoint;
private String cdnEndpoint;
private boolean cdnIncludeBucket = true;
@Component
public record AWSConfig(StorageConfig storageConfig) {
@Bean
public StaticCredentialsProvider credProvider() {
return StaticCredentialsProvider.create(AwsBasicCredentials.create(getAccessKey(), getSecretKey()));
}
@Bean
public StaticCredentialsProvider credProvider() {
return StaticCredentialsProvider.create(AwsBasicCredentials.create(this.storageConfig.accessKey(), this.storageConfig.secretKey()));
}
@Bean
public AwsRegionProvider regionProvider() {
return () -> Region.of("hangar");
}
@Bean
public AwsRegionProvider regionProvider() {
return () -> Region.of("hangar");
}
@Bean
public AwsProperties awsProperties() throws URISyntaxException {
AwsProperties awsProperties = new AwsProperties();
awsProperties.setEndpoint(new URI(objectStorageEndpoint));
return awsProperties;
}
@Bean
public AwsProperties awsProperties() throws URISyntaxException {
final AwsProperties awsProperties = new AwsProperties();
awsProperties.setEndpoint(new URI(this.storageConfig.objectStorageEndpoint()));
return awsProperties;
}
public String getPluginUploadDir() {
return pluginUploadDir;
}
public void setPluginUploadDir(String pluginUploadDir) {
this.pluginUploadDir = pluginUploadDir;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getAccessKey() {
return accessKey;
}
public void setAccessKey(String accessKey) {
this.accessKey = accessKey;
}
public String getSecretKey() {
return secretKey;
}
public void setSecretKey(String secretKey) {
this.secretKey = secretKey;
}
public String getObjectStorageEndpoint() {
return objectStorageEndpoint;
}
public void setObjectStorageEndpoint(String objectStorageEndpoint) {
this.objectStorageEndpoint = objectStorageEndpoint;
}
public String getBucket() {
return bucket;
}
public void setBucket(String bucket) {
this.bucket = bucket;
}
public String getCdnEndpoint() {
return cdnEndpoint;
}
public void setCdnEndpoint(String cdnEndpoint) {
this.cdnEndpoint = cdnEndpoint;
}
public boolean isCdnIncludeBucket() {
return cdnIncludeBucket;
}
public void setCdnIncludeBucket(boolean cdnIncludeBucket) {
this.cdnIncludeBucket = cdnIncludeBucket;
}
}

View File

@ -0,0 +1,21 @@
package io.papermc.hangar.config.hangar.converters;
import io.papermc.hangar.util.PatternWrapper;
import java.util.regex.Pattern;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.framework.qual.DefaultQualifier;
import org.springframework.boot.context.properties.ConfigurationPropertiesBinding;
import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;
@DefaultQualifier(NonNull.class)
@Component
@ConfigurationPropertiesBinding
public class StringToPatternWrapperConverter implements Converter<String, PatternWrapper> {
@Override
public @Nullable PatternWrapper convert(final String source) {
return new PatternWrapper(Pattern.compile(source));
}
}

View File

@ -50,7 +50,7 @@ public class LoginController extends HangarComponent {
@GetMapping(path = "/login", params = "returnUrl")
public RedirectView loginFromFrontend(@RequestParam(defaultValue = "/") String returnUrl) {
if (config.fakeUser.isEnabled()) {
if (config.fakeUser.enabled()) {
config.checkDev();
UserTable fakeUser = authenticationService.loginAsFakeUser();
@ -95,7 +95,7 @@ public class LoginController extends HangarComponent {
@GetMapping(path = "/logout", params = "returnUrl")
public RedirectView logout(@RequestParam(defaultValue = "/logged-out") String returnUrl) {
if (config.fakeUser.isEnabled()) {
if (config.fakeUser.enabled()) {
response.addCookie(new Cookie("url", returnUrl));
return new RedirectView("/fake-logout");
} else {
@ -151,7 +151,7 @@ public class LoginController extends HangarComponent {
@GetMapping("/signup")
public RedirectView signUp(@RequestParam(defaultValue = "") String returnUrl) {
if (config.fakeUser.isEnabled()) {
if (config.fakeUser.enabled()) {
throw new HangarApiException("nav.user.error.fakeUserEnabled", "Signup");
}
return new RedirectView(ssoService.getSignupUrl(returnUrl));

View File

@ -18,7 +18,7 @@ public class ApiUtils {
* @return actual limit
*/
public static long limitOrDefault(@Nullable Long limit) {
return limitOrDefault(limit, hangarConfig.projects.getInitLoad());
return limitOrDefault(limit, hangarConfig.projects.initLoad());
}
public static long limitOrDefault(@Nullable Long limit, long maxLimit) {

View File

@ -18,4 +18,6 @@ public @interface ConfigurePagination {
* -1 means fallback to default configured value
*/
long maxLimit();
// TODO add String SpEL param to use configurable values for action log amounts and version amounts
}

View File

@ -27,6 +27,7 @@ import org.springframework.web.bind.annotation.ResponseStatus;
import javax.validation.Valid;
import java.util.List;
// @el(user: io.papermc.hangar.model.db.UserTable)
@LoggedIn
@Controller
@RateLimit(path = "apikey")

View File

@ -210,19 +210,19 @@ public class BackendDataController {
public ResponseEntity<ObjectNode> getValidations() {
ObjectNode validations = noJsonValueMapper.createObjectNode();
ObjectNode projectValidations = noJsonValueMapper.createObjectNode();
projectValidations.set("name", noJsonValueMapper.valueToTree(new Validation(config.projects.getNameRegex(), config.projects.getMaxNameLen(), null)));
projectValidations.set("desc", noJsonValueMapper.valueToTree(new Validation(null, config.projects.getMaxDescLen(), null)));
projectValidations.set("keywords", noJsonValueMapper.valueToTree(new Validation(null, config.projects.getMaxKeywords(), null)));
projectValidations.set("channels", noJsonValueMapper.valueToTree(new Validation(config.channels.getNameRegex(), config.channels.getMaxNameLen(), null)));
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.getMaxPages());
projectValidations.put("maxChannelCount", config.projects.getMaxChannels());
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.getVersionNameRegex(), config.projects.getMaxVersionNameLen(), null)));
validations.set("org", noJsonValueMapper.valueToTree(new Validation(config.org.getNameRegex(), config.org.getMaxNameLen(), config.org.getMinNameLen())));
validations.put("maxOrgCount", config.org.getCreateLimit());
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);
}

View File

@ -36,7 +36,7 @@ public class DiscourseController extends HangarComponent {
@RateLimit(overdraft = 5, refillTokens = 1, refillSeconds = 30)
@VisibilityRequired(type = Type.PROJECT, args = "{#projectId}")
public String createPost(@PathVariable long projectId, @RequestBody Map<String, String> content) {
if (!config.discourse.isEnabled()) {
if (!config.discourse.enabled()) {
throw new HangarApiException("Discourse is NOT enabled!");
}
jobService.save(new PostDiscourseReplyJob(projectId, getHangarPrincipal().getName(), content.get("content")));

View File

@ -126,7 +126,7 @@ public class ProjectController extends HangarComponent {
@PermissionRequired(type = PermissionType.PROJECT, perms = NamedPermission.EDIT_SUBJECT_SETTINGS, args = "{#author, #slug}")
@PostMapping(path = "/project/{author}/{slug}/sponsors", consumes = MediaType.APPLICATION_JSON_VALUE)
public void saveProjectSettings(@PathVariable String author, @PathVariable String slug, @RequestBody @Valid StringContent content) {
if (content.getContent().length() > config.projects.getMaxSponsorsLen()) {
if (content.getContent().length() > config.projects.maxSponsorsLen()) {
throw new HangarApiException("page.new.error.name.maxLength");
}
projectService.saveSponsors(author, slug, content);

View File

@ -56,7 +56,7 @@ public class ProjectPageController extends HangarComponent {
@RateLimit(overdraft = 10, refillTokens = 3, refillSeconds = 5, greedy = true)
@PostMapping(path = "/render", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<String> renderMarkdown(@RequestBody @Valid StringContent content) {
if (content.getContent().length() > config.projects.getContentMaxLen()) {
if (content.getContent().length() > config.projects.contentMaxLen()) {
throw new HangarApiException("page.new.error.name.maxLength");
}
return ResponseEntity.ok(markdownService.render(content.getContent()));
@ -67,7 +67,7 @@ public class ProjectPageController extends HangarComponent {
@ResponseBody
@PostMapping(path = "/convert-bbcode", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.TEXT_PLAIN_VALUE)
public String convertBBCode(@RequestBody @Valid StringContent bbCodeContent) {
if (bbCodeContent.getContent().length() > config.projects.getMaxBBCodeLen()) {
if (bbCodeContent.getContent().length() > config.projects.maxBBCodeLen()) {
throw new HangarApiException("page.new.error.name.maxLength");
}
BBCodeConverter bbCodeConverter = new BBCodeConverter();

View File

@ -12,19 +12,19 @@ public class CreateOrganizationForm extends EditMembersForm<OrganizationRole> {
@Validate(SpEL = "@validate.min(#root, @hangarConfig.org.minNameLen)", message = "organization.new.error.invalidName")
private final String name;
public CreateOrganizationForm(List<Member<OrganizationRole>> members, String name) {
public CreateOrganizationForm(final List<Member<OrganizationRole>> members, final String name) {
super(members);
this.name = name;
}
public String getName() {
return name;
return this.name;
}
@Override
public String toString() {
return "CreateOrganizationForm{" +
"name='" + name + '\'' +
"name='" + this.name + '\'' +
"} " + super.toString();
}
}

View File

@ -57,7 +57,7 @@ public class APIKeyService extends HangarComponent {
String tokenIdentifier = UUID.randomUUID().toString();
String token = UUID.randomUUID().toString();
String hashedToken = CryptoUtils.hmacSha256(config.security.getTokenSecret(), token.getBytes(StandardCharsets.UTF_8));
String hashedToken = CryptoUtils.hmacSha256(config.security.tokenSecret(), token.getBytes(StandardCharsets.UTF_8));
apiKeyDAO.insert(new ApiKeyTable(apiKeyForm.getName(), userIdentified.getUserId(), tokenIdentifier, hashedToken, keyPermission));
actionLogger.user(LogAction.USER_APIKEY_CREATED.create(UserContext.of(userIdentified.getUserId()), "Key Name: " + apiKeyForm.getName() + "<br>" + apiKeyForm.getPermissions().stream().map(NamedPermission::getFrontendName).collect(Collectors.joining(",<br>")), ""));
return tokenIdentifier + "." + token;

View File

@ -37,14 +37,14 @@ public class AuthenticationService extends HangarComponent {
}
public UserTable loginAsFakeUser() {
String userName = config.fakeUser.getUsername();
String userName = config.fakeUser.username();
UserTable userTable = userService.getUserTable(userName);
if (userTable == null) {
userTable = new UserTable(
-1, // we can pass -1 here since it's not actually inserted in the DB in the DAO
UUID.randomUUID(),
userName,
config.fakeUser.getEmail(),
config.fakeUser.email(),
List.of(),
false,
Locale.ENGLISH.toLanguageTag(),
@ -68,7 +68,7 @@ public class AuthenticationService extends HangarComponent {
HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body, headers);
try {
ResponseEntity<Void> response = restTemplate.postForEntity(config.security.api.getUrl() + "/avatar/org/" + org + "?apiKey=" + config.sso.apiKey(), requestEntity, Void.class);
ResponseEntity<Void> response = restTemplate.postForEntity(config.security.api().url() + "/avatar/org/" + org + "?apiKey=" + config.sso.apiKey(), requestEntity, Void.class);
if (!response.getStatusCode().is2xxSuccessful()) {
throw new ResponseStatusException(response.getStatusCode(), "Error from auth api");
}

View File

@ -54,14 +54,14 @@ public class TokenService extends HangarComponent {
public void issueRefreshAndAccessToken(UserTable userTable) {
UserRefreshToken userRefreshToken = userRefreshTokenDAO.insert(new UserRefreshToken(userTable.getId(), UUID.randomUUID(), UUID.randomUUID()));
addCookie(SecurityConfig.REFRESH_COOKIE_NAME, userRefreshToken.getToken().toString(), config.security.getRefreshTokenExpiry().toSeconds(), true);
addCookie(SecurityConfig.REFRESH_COOKIE_NAME, userRefreshToken.getToken().toString(), config.security.refreshTokenExpiry().toSeconds(), true);
String accessToken = newToken0(userTable);
// let the access token cookie be around for longer, so we can more nicely detect expired tokens via the response code
addCookie(SecurityConfig.AUTH_NAME, accessToken, config.security.getTokenExpiry().toSeconds() * 2, false);
addCookie(SecurityConfig.AUTH_NAME, accessToken, config.security.tokenExpiry().toSeconds() * 2, false);
}
private void addCookie(String name, String value, long maxAge, boolean httpOnly) {
response.addHeader(HttpHeaders.SET_COOKIE, ResponseCookie.from(name, value).path("/").secure(config.security.isSecure()).maxAge(maxAge).sameSite("Lax").httpOnly(httpOnly).build().toString());
response.addHeader(HttpHeaders.SET_COOKIE, ResponseCookie.from(name, value).path("/").secure(config.security.secure()).maxAge(maxAge).sameSite("Lax").httpOnly(httpOnly).build().toString());
}
public void refreshAccessToken(String refreshToken) {
@ -78,7 +78,7 @@ public class TokenService extends HangarComponent {
if (userRefreshToken == null) {
throw new HangarApiException(HttpStatus.UNAUTHORIZED, "Unrecognized refresh token " + uuid);
}
if (userRefreshToken.getLastUpdated().isBefore(OffsetDateTime.now().minus(config.security.getRefreshTokenExpiry()))) {
if (userRefreshToken.getLastUpdated().isBefore(OffsetDateTime.now().minus(config.security.refreshTokenExpiry()))) {
throw new HangarApiException(HttpStatus.UNAUTHORIZED, "Expired refresh token" + uuid);
}
UserTable userTable = userService.getUserTable(userRefreshToken.getUserId());
@ -88,10 +88,10 @@ public class TokenService extends HangarComponent {
// we gotta update the refresh token
userRefreshToken.setToken(UUID.randomUUID());
userRefreshToken = userRefreshTokenDAO.update(userRefreshToken);
addCookie(SecurityConfig.REFRESH_COOKIE_NAME, userRefreshToken.getToken().toString(), config.security.getRefreshTokenExpiry().toSeconds(), true);
addCookie(SecurityConfig.REFRESH_COOKIE_NAME, userRefreshToken.getToken().toString(), config.security.refreshTokenExpiry().toSeconds(), true);
// then issue a new access token
String accessToken = newToken0(userTable);
addCookie(SecurityConfig.AUTH_NAME, accessToken, config.security.getTokenExpiry().toSeconds(), false);
addCookie(SecurityConfig.AUTH_NAME, accessToken, config.security.tokenExpiry().toSeconds(), false);
}
public void invalidateToken(String refreshToken) {
@ -109,8 +109,8 @@ public class TokenService extends HangarComponent {
public String expiring(UserTable userTable, Permission globalPermission, @Nullable String apiKeyIdentifier) {
return JWT.create()
.withIssuer(config.security.getTokenIssuer())
.withExpiresAt(new Date(Instant.now().plus(config.security.getTokenExpiry()).toEpochMilli()))
.withIssuer(config.security.tokenIssuer())
.withExpiresAt(new Date(Instant.now().plus(config.security.tokenExpiry()).toEpochMilli()))
.withSubject(userTable.getName())
.withClaim("id", userTable.getId())
.withClaim("permissions", globalPermission.toBinString())
@ -121,8 +121,8 @@ public class TokenService extends HangarComponent {
public String simple(String username) {
return JWT.create()
.withIssuer(config.security.getTokenIssuer())
.withExpiresAt(new Date(Instant.now().plus(config.security.getTokenExpiry()).toEpochMilli()))
.withIssuer(config.security.tokenIssuer())
.withExpiresAt(new Date(Instant.now().plus(config.security.tokenExpiry()).toEpochMilli()))
.withSubject(username)
.sign(getAlgo());
}
@ -151,7 +151,7 @@ public class TokenService extends HangarComponent {
if (verifier == null) {
verifier = JWT.require(getAlgo())
.acceptLeeway(10)
.withIssuer(config.security.getTokenIssuer())
.withIssuer(config.security.tokenIssuer())
.build();
}
return verifier;
@ -159,7 +159,7 @@ public class TokenService extends HangarComponent {
private Algorithm getAlgo() {
if (algo == null) {
algo = Algorithm.HMAC256(config.security.getTokenSecret());
algo = Algorithm.HMAC256(config.security.tokenSecret());
}
return algo;
}

View File

@ -39,9 +39,9 @@ public class ValidationService {
error = "invalidName";
} else if (name.length() < 3) {
error = "tooShortName";
} else if (name.length() > config.projects.getMaxNameLen()) {
} else if (name.length() > config.projects.maxNameLen()) {
error = "tooLongName";
} else if (name.contains(ProjectFactory.SOFT_DELETION_SUFFIX) || !config.projects.getNameMatcher().test(name)) {
} else if (name.contains(ProjectFactory.SOFT_DELETION_SUFFIX) || !config.projects.nameRegex().test(name)) {
error = "invalidName";
}
return error != null ? "project.new.error." + error : null;
@ -52,7 +52,7 @@ public class ValidationService {
if (bannedRoutes.contains(name) || name.contains(ProjectFactory.SOFT_DELETION_SUFFIX)) {
return false;
}
if (name.length() < 1 || name.length() > config.projects.getMaxVersionNameLen() || !config.projects.getVersionNameMatcher().test(name)) {
if (name.length() < 1 || name.length() > config.projects.maxVersionNameLen() || !config.projects.versionNameRegex().test(name)) {
return false;
}
return true;

View File

@ -39,13 +39,13 @@ public class APIAuthenticationService extends HangarComponent {
}
String identifier = apiKey.split("\\.")[0];
String token = apiKey.split("\\.")[1];
String hashedToken = CryptoUtils.hmacSha256(config.security.getTokenSecret(), token.getBytes(StandardCharsets.UTF_8));
String hashedToken = CryptoUtils.hmacSha256(config.security.tokenSecret(), token.getBytes(StandardCharsets.UTF_8));
ApiKeyTable apiKeyTable = apiKeyDAO.findApiKey(identifier, hashedToken);
if (apiKeyTable == null) {
throw new HangarApiException("No valid API Key found");
}
UserTable userTable = userDAO.getUserTable(apiKeyTable.getOwnerId());
String jwt = tokenService.expiring(userTable, apiKeyTable.getPermissions(), identifier);
return new ApiSession(jwt, config.security.getRefreshTokenExpiry().toSeconds());
return new ApiSession(jwt, config.security.refreshTokenExpiry().toSeconds());
}
}

View File

@ -51,15 +51,15 @@ public class JobService extends HangarComponent {
@PostConstruct
public void initThreadPool() {
this.executorService = new ThreadPoolExecutor(1, config.jobs.getMaxConcurrentJobs(), 60, TimeUnit.SECONDS, new SynchronousQueue<>());
this.executorService = new ThreadPoolExecutor(1, config.jobs.maxConcurrentJobs(), 60, TimeUnit.SECONDS, new SynchronousQueue<>());
}
public void checkAndProcess() {
if (!config.discourse.isEnabled()) { return; }
if (!config.discourse.enabled()) { return; }
long awaitingJobs = jobsDAO.countAwaitingJobs();
logger.debug("Found {} awaiting jobs", awaitingJobs);
if (awaitingJobs > 0) {
long numberToProcess = Math.max(1, Math.min(awaitingJobs, config.jobs.getMaxConcurrentJobs()));
long numberToProcess = Math.max(1, Math.min(awaitingJobs, config.jobs.maxConcurrentJobs()));
for (long i = 0; i < numberToProcess; i++) {
executorService.submit(this::process);
}
@ -72,7 +72,7 @@ public class JobService extends HangarComponent {
@Transactional
public void save(Job job) {
if (!config.discourse.isEnabled()) { return; }
if (!config.discourse.enabled()) { return; }
jobsDAO.save(job.toTable());
}
@ -97,15 +97,15 @@ public class JobService extends HangarComponent {
toJobString(jobTable) +
"Status Code: " + statusError.getStatus() + "\n" +
toMessageString(statusError);
jobsDAO.retryIn(jobTable.getId(), OffsetDateTime.now().plus(config.jobs.getStatusErrorTimeout()).plusSeconds(5), error, "status_error_" + statusError.getStatus().value());
jobsDAO.retryIn(jobTable.getId(), OffsetDateTime.now().plus(config.jobs.statusErrorTimeout()).plusSeconds(5), error, "status_error_" + statusError.getStatus().value());
} catch (DiscourseError.UnknownError unknownError) {
String error = "Encountered error when executing Discourse request\n" +
toJobString(jobTable) +
"Type: " + unknownError.getDescriptor() + "\n" +
toMessageString(unknownError);
jobsDAO.retryIn(jobTable.getId(), OffsetDateTime.now().plus(config.jobs.getUnknownErrorTimeout()).plusSeconds(5), error, "unknown_error" + unknownError.getDescriptor());
jobsDAO.retryIn(jobTable.getId(), OffsetDateTime.now().plus(config.jobs.unknownErrorTimeout()).plusSeconds(5), error, "unknown_error" + unknownError.getDescriptor());
} catch (DiscourseError.NotAvailableError notAvailableError) {
jobsDAO.retryIn(jobTable.getId(), OffsetDateTime.now().plus(config.jobs.getNotAvailableTimeout()).plusSeconds(5), "Not Available", "not_available");
jobsDAO.retryIn(jobTable.getId(), OffsetDateTime.now().plus(config.jobs.notAvailableTimeout()).plusSeconds(5), "Not Available", "not_available");
} catch (DiscourseError.NotProcessable notProcessable) {
logger.debug("job failed to process discourse job: {} {}", notProcessable.getMessage(), jobTable);
String error = "Encountered error when processing discourse job\n" +

View File

@ -30,7 +30,7 @@ public class HealthService extends HangarComponent {
}
public List<UnhealthyProject> getStaleProjects() {
return healthDAO.getStaleProjects("'" + config.projects.getStaleAge().toSeconds() + " SECONDS'");
return healthDAO.getStaleProjects("'" + config.projects.staleAge().toSeconds() + " SECONDS'");
}
public List<UnhealthyProject> getNonPublicProjects() {

View File

@ -66,7 +66,7 @@ public class StatService extends HangarComponent {
private void setCookie(String cookieValue) {
response.addHeader(HttpHeaders.SET_COOKIE,
ResponseCookie.from(STAT_TRACKING_COOKIE, cookieValue)
.secure(config.security.isSecure())
.secure(config.security.secure())
.path("/")
.maxAge((long) (60 * 60 * 24 * 356.24 * 1000))
.sameSite("Strict")

View File

@ -39,8 +39,8 @@ public class DiscourseApi {
private HttpHeaders header(String poster) {
HttpHeaders headers = new HttpHeaders();
headers.set("Api-Key", config.getApiKey());
headers.set("Api-Username", poster == null ? config.getAdminUser() : poster);
headers.set("Api-Key", config.apiKey());
headers.set("Api-Username", poster == null ? config.adminUser() : poster);
return headers;
}
@ -48,7 +48,7 @@ public class DiscourseApi {
Map<String, Object> args = new HashMap<>();
args.put("topic_id", topicId);
args.put("raw", content);
return execute(args, config.getUrl() + "/posts.json", header(poster), HttpMethod.POST, DiscoursePost.class);
return execute(args, config.url() + "/posts.json", header(poster), HttpMethod.POST, DiscoursePost.class);
}
public DiscoursePost createTopic(String poster, String title, String content, @Nullable Integer categoryId) {
@ -56,7 +56,7 @@ public class DiscourseApi {
args.put("title", title);
args.put("raw", content);
args.put("category", categoryId);
return execute(args, config.getUrl() + "/posts.json", header(poster), HttpMethod.POST, DiscoursePost.class);
return execute(args, config.url() + "/posts.json", header(poster), HttpMethod.POST, DiscoursePost.class);
}
public void updateTopic(String poster, long topicId, @Nullable String title, @Nullable Integer categoryId) {
@ -64,17 +64,17 @@ public class DiscourseApi {
args.put("topic_id", topicId);
args.put("title", title);
args.put("category", categoryId);
execute(args, config.getUrl() + "/t/-/" + topicId + ".json", header(poster), HttpMethod.PUT);
execute(args, config.url() + "/t/-/" + topicId + ".json", header(poster), HttpMethod.PUT);
}
public void updatePost(String poster, long postId, String content) {
Map<String, String> args = new HashMap<>();
args.put("raw", content);
execute(args, config.getUrl() + "/posts/" + postId + ".json", header(poster), HttpMethod.PUT);
execute(args, config.url() + "/posts/" + postId + ".json", header(poster), HttpMethod.PUT);
}
public void deleteTopic(String poster, long topicId) {
execute(null, config.getUrl() + "/t/" + topicId + ".json", header(poster), HttpMethod.DELETE);
execute(null, config.url() + "/t/" + topicId + ".json", header(poster), HttpMethod.DELETE);
}
private void execute(Object args, String url, HttpHeaders headers, HttpMethod method) {

View File

@ -69,7 +69,7 @@ public class DiscourseService {
String title = discourseFormatter.formatProjectTitle(project);
String content = discourseFormatter.formatProjectTopic(project, getHomepageContent(project));
DiscoursePost post = api.createTopic(project.getOwnerName(), title, content, config.getCategory());
DiscoursePost post = api.createTopic(project.getOwnerName(), title, content, config.category());
if (post == null) {
throw new JobException("project post wasn't created " + project.getProjectId(), "sanity_check");
}
@ -87,7 +87,7 @@ public class DiscourseService {
String title = discourseFormatter.formatProjectTitle(project);
String content = discourseFormatter.formatProjectTopic(project, getHomepageContent(project));
api.updateTopic(project.getOwnerName(), project.getTopicId(), title, project.getVisibility() == Visibility.PUBLIC ? config.getCategory() : config.getCategoryDeleted());
api.updateTopic(project.getOwnerName(), project.getTopicId(), title, project.getVisibility() == Visibility.PUBLIC ? config.category() : config.categoryDeleted());
api.updatePost(project.getOwnerName(), project.getPostId(), content);
}
@ -124,6 +124,6 @@ public class DiscourseService {
}
public void deleteTopic(long topicId) {
api.deleteTopic(config.getAdminUser(), topicId);
api.deleteTopic(config.adminUser(), topicId);
}
}

View File

@ -91,7 +91,7 @@ public class LocalStorageFileService implements FileService {
@Override
public String getRoot() {
return config.getPluginUploadDir();
return config.workDir();
}
@Override

View File

@ -40,7 +40,7 @@ public class S3FileService implements FileService {
@Override
public void deleteDirectory(String dir) {
this.s3Template.deleteObject(config.getBucket(), dir);
this.s3Template.deleteObject(config.bucket(), dir);
}
@Override
@ -86,11 +86,11 @@ public class S3FileService implements FileService {
@Override
public String getRoot() {
return "s3://" + config.getBucket() + "/";
return "s3://" + config.bucket() + "/";
}
@Override
public String getDownloadUrl(String user, String project, String version, Platform platform, String fileName) {
return config.getCdnEndpoint() + (config.isCdnIncludeBucket() ? "/" + config.getBucket() : "") + "/plugins/" + user + "/" + project + "/versions/" + version + "/" + platform.name() + "/" + fileName;
return config.cdnEndpoint() + (config.cdnIncludeBucket() ? "/" + config.bucket() : "") + "/plugins/" + user + "/" + project + "/versions/" + version + "/" + platform.name() + "/" + fileName;
}
}

View File

@ -48,14 +48,14 @@ public class OrganizationFactory extends HangarComponent {
@Transactional
public void createOrganization(String name) {
if (!config.org.isEnabled()) {
if (!config.org.enabled()) {
throw new HangarApiException(HttpStatus.BAD_REQUEST, "organization.new.error.notEnabled");
}
if (organizationService.getOrganizationsOwnedBy(getHangarPrincipal().getId()).size() >= config.org.getCreateLimit()) {
throw new HangarApiException(HttpStatus.BAD_REQUEST, "organization.new.error.tooManyOrgs", config.org.getCreateLimit());
if (organizationService.getOrganizationsOwnedBy(getHangarPrincipal().getId()).size() >= config.org.createLimit()) {
throw new HangarApiException(HttpStatus.BAD_REQUEST, "organization.new.error.tooManyOrgs", config.org.createLimit());
}
String dummyEmail = name.replaceAll("[^a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]", "") + '@' + config.org.getDummyEmailDomain();
String dummyEmail = name.replaceAll("[^a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]", "") + '@' + config.org.dummyEmailDomain();
UserTable userTable = userDAO.create(UUID.randomUUID(), name, dummyEmail, "", "", List.of(), false, null);
OrganizationTable organizationTable = organizationDAO.insert(new OrganizationTable(userTable.getId(), name, getHangarPrincipal().getId(), userTable.getId()));
globalRoleService.addRole(GlobalRole.ORGANIZATION.create(null, userTable.getId(), false));

View File

@ -56,8 +56,8 @@ public class ChannelService extends HangarComponent {
throw new HangarApiException(HttpStatus.BAD_REQUEST, "channel.modal.error.invalidName");
}
if (existingChannels.size() >= this.config.projects.getMaxChannels()) {
throw new HangarApiException(HttpStatus.BAD_REQUEST, "channel.modal.error.maxChannels", this.config.projects.getMaxChannels());
if (existingChannels.size() >= this.config.projects.maxChannels()) {
throw new HangarApiException(HttpStatus.BAD_REQUEST, "channel.modal.error.maxChannels", this.config.projects.maxChannels());
}
this.checkName(projectId, name, null, ignored -> existingChannels);

View File

@ -69,7 +69,7 @@ public class ProjectFactory extends HangarComponent {
ProjectTable projectTable = null;
try {
projectTable = this.projectsDAO.insert(new ProjectTable(projectOwner, newProject));
this.channelService.createProjectChannel(this.config.channels.getNameDefault(), this.config.channels.getColorDefault(), projectTable.getId(), Set.of(ChannelFlag.FROZEN, ChannelFlag.PINNED));
this.channelService.createProjectChannel(this.config.channels.nameDefault(), this.config.channels.colorDefault(), projectTable.getId(), Set.of(ChannelFlag.FROZEN, ChannelFlag.PINNED));
this.projectMemberService.addNewAcceptedByDefaultMember(ProjectRole.PROJECT_OWNER.create(projectTable.getId(), projectOwner.getUserId(), true));
String newPageContent = newProject.getPageContent();
if (newPageContent == null) {

View File

@ -138,7 +138,7 @@ public class ProjectService extends HangarComponent {
final Map<Platform, HangarVersion> mainChannelVersions = new EnumMap<>(Platform.class);
for (final Platform platform : Platform.getValues()) {
final HangarVersion version = getLastVersion(author, slug, platform, config.channels.getNameDefault());
final HangarVersion version = getLastVersion(author, slug, platform, config.channels.nameDefault());
if (version != null) {
if (version.getPlatformDependencies().isEmpty()) {
final Map<Platform, SortedSet<String>> platformDependencies = versionsApiDAO.getPlatformDependencies(version.getId());
@ -246,7 +246,7 @@ public class ProjectService extends HangarComponent {
private void evictIconCache(String author, String slug) {
String url = config.getBaseUrl() + "/api/internal/projects/project/" + author + "/" + slug + "/icon";
restTemplate.delete(config.security.api.getUrl() + "/image/" + url + "?apiKey=" + config.sso.apiKey());
restTemplate.delete(config.security.api().url() + "/image/" + url + "?apiKey=" + config.sso.apiKey());
}
private String getBase64(String author, String slug, String old, String path) {

View File

@ -42,6 +42,6 @@ public class ImageService extends HangarComponent {
}
public String getUserIcon(String author) {
return String.format(config.security.api.getAvatarUrl(), author);
return String.format(config.security.api().avatarUrl(), author);
}
}

View File

@ -25,7 +25,7 @@ public class ProjectFiles {
@Autowired
public ProjectFiles(StorageConfig storageConfig, FileService fileService) {
this.fileService = fileService;
Path uploadsDir = Path.of(storageConfig.getPluginUploadDir());
Path uploadsDir = Path.of(storageConfig.workDir());
pluginsDir = fileService.resolve(fileService.getRoot(), "plugins");
tmpDir = uploadsDir.resolve("tmp");
if (Files.exists(tmpDir)) {

View File

@ -113,7 +113,7 @@ public class UserService extends HangarComponent {
HttpEntity<Traits> requestEntity = new HttpEntity<>(traits, headers);
try {
ResponseEntity<Void> response = restTemplate.postForEntity(config.security.api.getUrl() + "/sync/user/" + uuid.toString() + "?apiKey=" + config.sso.apiKey(), requestEntity, Void.class);
ResponseEntity<Void> response = restTemplate.postForEntity(config.security.api().url() + "/sync/user/" + uuid.toString() + "?apiKey=" + config.sso.apiKey(), requestEntity, Void.class);
if (!response.getStatusCode().is2xxSuccessful()) {
throw new ResponseStatusException(response.getStatusCode(), "Error from auth api");
}

View File

@ -79,7 +79,7 @@ public class DownloadService extends HangarComponent {
// create new token
UUID token = UUID.randomUUID();
OffsetDateTime expiresAt = OffsetDateTime.now().plus(config.projects.getUnsafeDownloadMaxAge().toMillis(), ChronoUnit.MILLIS);
OffsetDateTime expiresAt = OffsetDateTime.now().plus(config.projects.unsafeDownloadMaxAge().toMillis(), ChronoUnit.MILLIS);
projectVersionDownloadWarningsDAO.insert(new ProjectVersionDownloadWarningTable(
expiresAt,
token,

View File

@ -116,7 +116,7 @@ public class VersionDependencyService extends HangarComponent {
@Transactional
public void updateVersionPluginDependencies(long projectId, long versionId, UpdatePluginDependencies form) {
if (form.getPluginDependencies().size() > config.projects.getMaxDependencies()) {
if (form.getPluginDependencies().size() > config.projects.maxDependencies()) {
throw new HangarApiException(HttpStatus.BAD_REQUEST, "version.new.error.tooManyDependencies");
}

View File

@ -246,7 +246,7 @@ public class VersionFactory extends HangarComponent {
if (!validationService.isValidVersionName(pendingVersion.getVersionString())) {
throw new HangarApiException(HttpStatus.BAD_REQUEST, "version.new.error.invalidName");
}
if (pendingVersion.getPluginDependencies().values().stream().anyMatch(pluginDependencies -> pluginDependencies.size() > config.projects.getMaxDependencies())) {
if (pendingVersion.getPluginDependencies().values().stream().anyMatch(pluginDependencies -> pluginDependencies.size() > config.projects.maxDependencies())) {
throw new HangarApiException(HttpStatus.BAD_REQUEST, "version.new.error.tooManyDependencies");
}
if (exists(projectId, pendingVersion.getVersionString())) {

View File

@ -1,11 +1,10 @@
package io.papermc.hangar.tasks;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import io.papermc.hangar.service.internal.admin.StatService;
import io.papermc.hangar.service.internal.projects.ProjectService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Component
public class DbUpdateTask {
@ -14,19 +13,19 @@ public class DbUpdateTask {
private final StatService statService;
@Autowired
public DbUpdateTask(ProjectService projectService, StatService statService) {
public DbUpdateTask(final ProjectService projectService, final StatService statService) {
this.projectService = projectService;
this.statService = statService;
}
@Scheduled(fixedRateString = "#{@hangarConfig.homepage.updateInterval.toMillis()}")
public void refreshHomePage() {
projectService.refreshHomeProjects();
this.projectService.refreshHomeProjects();
}
@Scheduled(fixedRateString = "#{@hangarConfig.homepage.updateInterval.toMillis()}", initialDelay = 1000)
public void updateStats() {
statService.processProjectViews();
statService.processVersionDownloads();
this.statService.processProjectViews();
this.statService.processVersionDownloads();
}
}

View File

@ -0,0 +1,20 @@
package io.papermc.hangar.util;
import java.util.function.Predicate;
import java.util.regex.Pattern;
public record PatternWrapper(Pattern pattern, Predicate<String> matcherPredicate) implements Predicate<String> {
public PatternWrapper(final Pattern pattern) {
this(pattern, pattern.asPredicate());
}
public String strPattern() {
return this.pattern.pattern();
}
@Override
public boolean test(final String s) {
return this.matcherPredicate.test(s);
}
}

View File

@ -7,7 +7,7 @@ import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.validation.constraints.NotNull;
public class StringUtils {
public final class StringUtils {
private StringUtils() {
}