diff --git a/gradle.properties b/gradle.properties
index a75b6af..d05da32 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,6 +1,6 @@
org.gradle.jvmargs=-Xmx1024M
-quarkusPlatformVersion=3.14.2
-quarkusPluginVersion=3.14.2
+quarkusPlatformVersion=3.14.4
+quarkusPluginVersion=3.14.4
sentryVersion=7.14.0
twelveMonkeysVersion=3.11.0
spotlessPluginVersion=6.25.0
diff --git a/src/main/java/app/fyreplace/api/data/Color.java b/src/main/java/app/fyreplace/api/data/Color.java
new file mode 100644
index 0000000..6708dd3
--- /dev/null
+++ b/src/main/java/app/fyreplace/api/data/Color.java
@@ -0,0 +1,8 @@
+package app.fyreplace.api.data;
+
+import org.eclipse.microprofile.openapi.annotations.media.Schema;
+
+public record Color(
+ @Schema(required = true, minimum = "0", maximum = "255", format = "uint8") int r,
+ @Schema(required = true, minimum = "0", maximum = "255", format = "uint8") int g,
+ @Schema(required = true, minimum = "0", maximum = "255", format = "uint8") int b) {}
diff --git a/src/main/java/app/fyreplace/api/data/TokenCreation.java b/src/main/java/app/fyreplace/api/data/TokenCreation.java
index e9f7a8d..bf35719 100644
--- a/src/main/java/app/fyreplace/api/data/TokenCreation.java
+++ b/src/main/java/app/fyreplace/api/data/TokenCreation.java
@@ -6,4 +6,4 @@
public record TokenCreation(
@NotBlank @Length(max = Email.EMAIL_MAX_LENGTH) @Schema(maxLength = Email.EMAIL_MAX_LENGTH) String identifier,
- @NotBlank String secret) {}
+ @NotBlank @Schema(format = "password") String secret) {}
diff --git a/src/main/java/app/fyreplace/api/data/User.java b/src/main/java/app/fyreplace/api/data/User.java
index 07ec1f4..8a31c6c 100644
--- a/src/main/java/app/fyreplace/api/data/User.java
+++ b/src/main/java/app/fyreplace/api/data/User.java
@@ -14,7 +14,7 @@
import jakarta.persistence.Table;
import jakarta.ws.rs.NotAuthorizedException;
import jakarta.ws.rs.core.SecurityContext;
-import java.awt.Color;
+import java.security.MessageDigest;
import java.time.Duration;
import java.time.Instant;
import java.util.Arrays;
@@ -22,7 +22,7 @@
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
-import java.util.zip.CRC32;
+import lombok.SneakyThrows;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import org.hibernate.annotations.OnDelete;
import org.hibernate.annotations.OnDeleteAction;
@@ -154,16 +154,17 @@ public boolean getBlocked() {
return Block.count("source = ?1 and target = ?2", currentUser, this) > 0;
}
- @Schema(required = true, pattern = "^#[A-F0-9]{6}$")
- public String getTint() {
- final var crc = new CRC32();
- crc.update(username.getBytes());
- final var hue = (float) (crc.getValue() / Math.pow(2, 32));
+ @SneakyThrows
+ @Schema(required = true)
+ public Color getTint() {
+ final var md5 = MessageDigest.getInstance("MD5");
+ final var digest = md5.digest(username.getBytes());
+ final var hue = bytesToFloat(digest);
final var h = hue * 6;
- final var variance = (h - (float) Math.floor(h)) * 0.25f;
- final var brightness = (int) h % 2 == 0 ? 1f - variance : 0.75f + variance;
- final var color = Color.HSBtoRGB(hue, 0.5f, brightness);
- return "#%02X%02X%02X".formatted((color >> 16) & 0xFF, (color >> 8) & 0xFF, color & 0xFF);
+ final var variance = Math.abs(h - (float) Math.round(h)) * 0.15f;
+ final var brightness = Math.round(h) % 2 == 0 ? 0.75f - variance : 0.6f + variance;
+ final var color = java.awt.Color.HSBtoRGB(hue, 0.5f, brightness);
+ return new Color((color >> 2 * Byte.SIZE) & 0xFF, (color >> Byte.SIZE) & 0xFF, color & 0xFF);
}
@Override
@@ -261,6 +262,17 @@ public static User getFromSecurityContext(final SecurityContext context, @Nullab
return user;
}
+ private static float bytesToFloat(byte[] bytes) {
+ final var bytesToUse = Math.min(bytes.length, Long.BYTES);
+ var result = 0L;
+
+ for (var i = 0; i < bytesToUse; i++) {
+ result = (result << Byte.SIZE) | (bytes[i] & 0xFF);
+ }
+
+ return (float) Math.abs((double) result / Long.MAX_VALUE);
+ }
+
public enum Rank {
CITIZEN,
MODERATOR,
@@ -277,5 +289,5 @@ public record Profile(
@Schema(required = true) UUID id,
@Schema(required = true) String username,
@Schema(required = true) String avatar,
- @Schema(required = true) String tint) {}
+ @Schema(required = true) Color tint) {}
}
diff --git a/src/main/java/app/fyreplace/api/data/dev/DataSeeder.java b/src/main/java/app/fyreplace/api/data/dev/DataSeeder.java
index f23be18..04f3a32 100644
--- a/src/main/java/app/fyreplace/api/data/dev/DataSeeder.java
+++ b/src/main/java/app/fyreplace/api/data/dev/DataSeeder.java
@@ -1,5 +1,6 @@
package app.fyreplace.api.data.dev;
+import static java.util.Objects.requireNonNull;
import static java.util.stream.IntStream.range;
import app.fyreplace.api.data.Block;
@@ -19,6 +20,7 @@
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes;
import jakarta.transaction.Transactional;
+import java.io.IOException;
import org.eclipse.microprofile.config.inject.ConfigProperty;
@SuppressWarnings({"UnusedReturnValue", "unused"})
@@ -44,8 +46,8 @@ public void onShutdown(@Observes final ShutdownEvent event) {
@Transactional
public void insertData() {
- range(0, 20).forEach(i -> createUser("user_" + i, true));
- range(0, 10).forEach(i -> createUser("user_inactive_" + i, false));
+ range(0, 20).forEach(i -> createUser("user_" + i, true, i % 2 == 0));
+ range(0, 10).forEach(i -> createUser("user_inactive_" + i, false, false));
final var user = User.findByUsername("user_0");
range(0, 20).forEach(i -> createPost(user, "Post " + i, true, false));
range(0, 20).forEach(i -> createPost(user, "Draft " + i, false, false));
@@ -74,12 +76,24 @@ public void deleteData() {
}
@Transactional(Transactional.TxType.REQUIRES_NEW)
- public User createUser(final String username, final boolean active) {
+ public User createUser(final String username, final boolean active, final boolean hasAvatar) {
final var user = new User();
user.username = username;
user.active = active;
user.persist();
+ if (hasAvatar) {
+ try (final var stream = getClass().getResourceAsStream("/META-INF/resources/images/logo-maskable.png")) {
+ final var storedFile = new StoredFile(
+ "avatars", user.username, requireNonNull(stream).readAllBytes());
+ storedFile.persist();
+ user.avatar = storedFile;
+ user.persist();
+ } catch (final IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
final var email = new Email();
email.user = user;
email.email = username + "@example.org";
diff --git a/src/main/java/app/fyreplace/api/emails/EmailBase.java b/src/main/java/app/fyreplace/api/emails/EmailBase.java
index 0fb27a7..69f9e38 100644
--- a/src/main/java/app/fyreplace/api/emails/EmailBase.java
+++ b/src/main/java/app/fyreplace/api/emails/EmailBase.java
@@ -9,7 +9,9 @@
import io.quarkus.mailer.Mailer;
import io.quarkus.qute.TemplateInstance;
import jakarta.inject.Inject;
+import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.UriBuilder;
+import jakarta.ws.rs.core.UriInfo;
import java.net.URI;
import java.time.Instant;
import java.time.ZoneId;
@@ -24,9 +26,6 @@ public abstract class EmailBase extends Mail {
@ConfigProperty(name = "app.name")
String appName;
- @ConfigProperty(name = "app.url")
- URI appUrl;
-
@ConfigProperty(name = "app.front.url")
URI appFrontUrl;
@@ -45,6 +44,9 @@ public abstract class EmailBase extends Mail {
@Inject
LocaleService localeService;
+ @Context
+ UriInfo uriInfo;
+
private boolean customDeepLinks;
private String randomCodeClearText;
@@ -85,8 +87,9 @@ protected String getRandomCode() {
protected String getLink() {
return UriBuilder.fromUri(appFrontUrl.toString())
.scheme(customDeepLinks ? appFrontCustomScheme : appFrontUrl.getScheme())
+ .path(email.user.active ? "/login" : "/register")
.queryParam("action", action())
- .fragment(email.email + ':' + getRandomCode())
+ .fragment(getRandomCode())
.build()
.toString();
}
@@ -96,9 +99,11 @@ protected ResourceBundle getResourceBundle() {
}
protected TemplateCommonData getTemplateCommonData() {
- return new TemplateCommonData(getResourceBundle(), appName, appUrl, appWebsiteUrl, getRandomCode(), getLink());
+ return new TemplateCommonData(
+ getResourceBundle(), appName, uriInfo.getBaseUri(), appWebsiteUrl, getRandomCode(), getLink());
}
+ @SuppressWarnings("unused")
public static final class TemplateCommonData {
public final ResourceBundle res;
public final String appName;
@@ -117,5 +122,9 @@ private TemplateCommonData(
this.code = code;
this.link = link;
}
+
+ public URI makeAppUrl(String path) {
+ return UriBuilder.fromUri(appUrl).path(path).build();
+ }
}
}
diff --git a/src/main/java/app/fyreplace/api/endpoints/DevEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/DevEndpoint.java
index ecb3184..50f751f 100644
--- a/src/main/java/app/fyreplace/api/endpoints/DevEndpoint.java
+++ b/src/main/java/app/fyreplace/api/endpoints/DevEndpoint.java
@@ -1,10 +1,8 @@
package app.fyreplace.api.endpoints;
-import app.fyreplace.api.cache.DuplicateRequestKeyGenerator;
import app.fyreplace.api.data.User;
import app.fyreplace.api.services.JwtService;
import io.quarkus.arc.properties.IfBuildProperty;
-import io.quarkus.cache.CacheResult;
import io.quarkus.elytron.security.common.BcryptUtil;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
@@ -20,7 +18,6 @@ public final class DevEndpoint {
@GET
@Path("users/{username}/token")
- @CacheResult(cacheName = "requests", keyGenerator = DuplicateRequestKeyGenerator.class)
public String getUserToken(@PathParam("username") final String username) {
final var user = User.findByUsername(username);
diff --git a/src/main/java/app/fyreplace/api/endpoints/StoredFilesEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/StoredFilesEndpoint.java
index ad200ea..f719ec9 100644
--- a/src/main/java/app/fyreplace/api/endpoints/StoredFilesEndpoint.java
+++ b/src/main/java/app/fyreplace/api/endpoints/StoredFilesEndpoint.java
@@ -8,33 +8,33 @@
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.RedirectionException;
+import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
+import jakarta.ws.rs.core.UriInfo;
import java.io.IOException;
-import java.net.URI;
import java.net.URISyntaxException;
-import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.eclipse.microprofile.openapi.annotations.Operation;
@Path("stored-files")
public final class StoredFilesEndpoint {
- @ConfigProperty(name = "app.url")
- URI appUrl;
-
@Inject
StorageService storageService;
@Inject
MimeTypeService mimeTypeService;
+ @Context
+ UriInfo uriInfo;
+
@GET
@Path("{path:.*}")
@Operation(hidden = true)
public Response getStoredFile(@PathParam("path") final String path) throws URISyntaxException {
- final var requestUri = storageService.getUri(path);
+ final var pathUri = storageService.getUri(path);
- if (!appUrl.getHost().equals(requestUri.getHost())) {
- throw new RedirectionException(Status.SEE_OTHER, requestUri);
+ if (!uriInfo.getBaseUri().getHost().equals(pathUri.getHost())) {
+ throw new RedirectionException(Status.SEE_OTHER, pathUri);
}
try {
diff --git a/src/main/java/app/fyreplace/api/endpoints/TokensEndpoint.java b/src/main/java/app/fyreplace/api/endpoints/TokensEndpoint.java
index eceb603..8a1faf6 100644
--- a/src/main/java/app/fyreplace/api/endpoints/TokensEndpoint.java
+++ b/src/main/java/app/fyreplace/api/endpoints/TokensEndpoint.java
@@ -51,7 +51,10 @@ public final class TokensEndpoint {
@APIResponse(
responseCode = "201",
description = "Created",
- content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(implementation = String.class)))
+ content =
+ @Content(
+ mediaType = MediaType.TEXT_PLAIN,
+ schema = @Schema(implementation = String.class, format = "password")))
@APIResponse(responseCode = "400", description = "Bad Request")
@APIResponse(responseCode = "404", description = "Not Found")
@CacheResult(cacheName = "requests", keyGenerator = DuplicateRequestKeyGenerator.class)
@@ -85,7 +88,13 @@ public Response createToken(@NotNull @Valid final TokenCreation input) {
@GET
@Path("new")
@Authenticated
- @APIResponse(responseCode = "200", description = "OK")
+ @APIResponse(
+ responseCode = "200",
+ description = "OK",
+ content =
+ @Content(
+ mediaType = MediaType.TEXT_PLAIN,
+ schema = @Schema(implementation = String.class, format = "password")))
public String getNewToken() {
return jwtService.makeJwt(User.getFromSecurityContext(context));
}
diff --git a/src/main/java/app/fyreplace/api/services/MimeTypeService.java b/src/main/java/app/fyreplace/api/services/MimeTypeService.java
index c727b7d..31ef633 100644
--- a/src/main/java/app/fyreplace/api/services/MimeTypeService.java
+++ b/src/main/java/app/fyreplace/api/services/MimeTypeService.java
@@ -17,11 +17,11 @@ public String getMimeType(final byte[] data) throws IOException {
while (readers.hasNext()) {
final var reader = readers.next();
- final var format = reader.getFormatName().toLowerCase();
+ final var format = reader.getFormatName().toUpperCase();
try {
return Arrays.stream(KnownFileType.values())
- .filter(m -> m.name.equals(format))
+ .filter(m -> m.name().equals(format))
.findFirst()
.orElseThrow()
.mime;
@@ -39,7 +39,8 @@ public String getExtension(final byte[] data) {
.filter(m -> m.mime.equals(mime))
.findFirst()
.orElseThrow()
- .name();
+ .name()
+ .toLowerCase();
} catch (final IOException | NoSuchElementException e) {
return "unknown";
}
diff --git a/src/main/java/app/fyreplace/api/services/mimetype/KnownFileType.java b/src/main/java/app/fyreplace/api/services/mimetype/KnownFileType.java
index 0e4fa14..29522ef 100644
--- a/src/main/java/app/fyreplace/api/services/mimetype/KnownFileType.java
+++ b/src/main/java/app/fyreplace/api/services/mimetype/KnownFileType.java
@@ -1,15 +1,13 @@
package app.fyreplace.api.services.mimetype;
public enum KnownFileType {
- JPEG("jpeg", "image/jpeg"),
- PNG("png", "image/png"),
- WEBP("webp", "image/webp");
+ JPEG("image/jpeg"),
+ PNG("image/png"),
+ WEBP("image/webp");
- public final String name;
public final String mime;
- KnownFileType(String name, String mime) {
- this.name = name;
+ KnownFileType(String mime) {
this.mime = mime;
}
}
diff --git a/src/main/java/app/fyreplace/api/services/storage/LocalStorageServiceBase.java b/src/main/java/app/fyreplace/api/services/storage/LocalStorageServiceBase.java
index c24148c..cfd2817 100644
--- a/src/main/java/app/fyreplace/api/services/storage/LocalStorageServiceBase.java
+++ b/src/main/java/app/fyreplace/api/services/storage/LocalStorageServiceBase.java
@@ -3,17 +3,17 @@
import app.fyreplace.api.endpoints.StoredFilesEndpoint;
import app.fyreplace.api.services.StorageService;
import jakarta.ws.rs.Path;
-import jakarta.ws.rs.core.UriBuilder;
+import jakarta.ws.rs.core.Context;
+import jakarta.ws.rs.core.UriInfo;
import java.net.URI;
-import org.eclipse.microprofile.config.inject.ConfigProperty;
public abstract class LocalStorageServiceBase implements StorageService {
- @ConfigProperty(name = "app.url")
- URI appUrl;
+ @Context
+ UriInfo uriInfo;
@Override
public URI getUri(final String path) {
final var pathBase = StoredFilesEndpoint.class.getAnnotation(Path.class).value();
- return UriBuilder.fromUri(appUrl).path(pathBase).path(path).build();
+ return uriInfo.getBaseUriBuilder().path(pathBase).path(path).build();
}
}
diff --git a/src/main/resources/templates/_logo.mjml b/src/main/resources/templates/_logo.mjml
index 0cb5194..cb1a135 100644
--- a/src/main/resources/templates/_logo.mjml
+++ b/src/main/resources/templates/_logo.mjml
@@ -1,2 +1,2 @@
-
+
diff --git a/src/main/resources/templates/_schema.mjml b/src/main/resources/templates/_schema.mjml
index ad354ca..4f44710 100644
--- a/src/main/resources/templates/_schema.mjml
+++ b/src/main/resources/templates/_schema.mjml
@@ -8,7 +8,7 @@
"@context": "https://schema.org",
"@type": "Organization",
"url": "{d.websiteUrl}",
- "logo": "{d.appUrl}/images/logo-maskable.png",
+ "logo": "{d.makeAppUrl('/images/logo-maskable.png')}",
"name": "{d.appName}"
},
"potentialAction": {
diff --git a/src/test/java/app/fyreplace/api/testing/UserTestsBase.java b/src/test/java/app/fyreplace/api/testing/UserTestsBase.java
index a530ae2..9cfa11c 100644
--- a/src/test/java/app/fyreplace/api/testing/UserTestsBase.java
+++ b/src/test/java/app/fyreplace/api/testing/UserTestsBase.java
@@ -11,8 +11,8 @@ public class UserTestsBase extends TransactionalTestsBase {
@Override
public void beforeEach() {
super.beforeEach();
- range(0, getActiveUserCount()).forEach(i -> dataSeeder.createUser("user_" + i, true));
- range(0, getInactiveUserCount()).forEach(i -> dataSeeder.createUser("user_inactive_" + i, false));
+ range(0, getActiveUserCount()).forEach(i -> dataSeeder.createUser("user_" + i, true, false));
+ range(0, getInactiveUserCount()).forEach(i -> dataSeeder.createUser("user_inactive_" + i, false, false));
}
public int getActiveUserCount() {