diff --git a/README.md b/README.md index 6c27548..6c7252d 100644 --- a/README.md +++ b/README.md @@ -1,54 +1,3 @@ -# mstats - -This project uses Quarkus, the Supersonic Subatomic Java Framework. - -If you want to learn more about Quarkus, please visit its website: . - -## Running the application in dev mode - -You can run your application in dev mode that enables live coding using: - -```shell script -./mvnw compile quarkus:dev -``` - -> **_NOTE:_** Quarkus now ships with a Dev UI, which is available in dev mode only at . - -## Packaging and running the application - -The application can be packaged using: - -```shell script -./mvnw package -``` - -It produces the `quarkus-run.jar` file in the `target/quarkus-app/` directory. -Be aware that it’s not an _über-jar_ as the dependencies are copied into the `target/quarkus-app/lib/` directory. - -The application is now runnable using `java -jar target/quarkus-app/quarkus-run.jar`. - -If you want to build an _über-jar_, execute the following command: - -```shell script -./mvnw package -Dquarkus.package.jar.type=uber-jar -``` - -The application, packaged as an _über-jar_, is now runnable using `java -jar target/*-runner.jar`. - -## Creating a native executable - -You can create a native executable using: - -```shell script -./mvnw package -Dnative -``` - -Or, if you don't have GraalVM installed, you can run the native executable build in a container using: - -```shell script -./mvnw package -Dnative -Dquarkus.native.container-build=true -``` - -You can then execute your native executable with: `./target/mstats-0.0.1-SNAPSHOT-runner` - -If you want to learn more about building native executables, please consult . +- `/api/server/heartbeat` - server heartbeat +- `/api/plugin/{id}` - plugin info +- `/api/plugin/find/{name}` - find a plugin id by name \ No newline at end of file diff --git a/src/main/java/eu/m724/mstats/Startup.java b/src/main/java/eu/m724/mstats/Startup.java index a257d94..36b6ab3 100644 --- a/src/main/java/eu/m724/mstats/Startup.java +++ b/src/main/java/eu/m724/mstats/Startup.java @@ -1,7 +1,6 @@ package eu.m724.mstats; -import eu.m724.mstats.auth.AuthService; -import eu.m724.mstats.orm.Administrator; +import eu.m724.mstats.api.service.PluginService; import io.quarkus.runtime.StartupEvent; import jakarta.enterprise.event.Observes; import jakarta.inject.Inject; @@ -9,11 +8,10 @@ import jakarta.transaction.Transactional; public class Startup { @Inject - AuthService authService; + PluginService pluginService; @Transactional public void onStartup(@Observes StartupEvent event) { - Administrator administrator = Administrator.createAdministrator(); - System.out.println(administrator.getTokenEncoded()); + pluginService.createPlugin("ploogin"); } } diff --git a/src/main/java/eu/m724/mstats/api/resource/PluginApiResource.java b/src/main/java/eu/m724/mstats/api/resource/PluginApiResource.java new file mode 100644 index 0000000..1bf0c40 --- /dev/null +++ b/src/main/java/eu/m724/mstats/api/resource/PluginApiResource.java @@ -0,0 +1,95 @@ +package eu.m724.mstats.api.resource; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.ObjectMapper; +import eu.m724.mstats.api.service.PluginService; +import eu.m724.mstats.orm.Plugin; +import eu.m724.mstats.orm.Server; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Path("/api/plugin") +@Produces(MediaType.APPLICATION_JSON) +public class PluginApiResource { + @Inject + PluginService pluginService; + + @Path("/{id}") + @GET + public Response stats(Long id) { + Plugin plugin = pluginService.getPlugin(id); + if (plugin == null) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + + Map servers = new HashMap<>(); + int serversTotal = 0; + + Map versions = new HashMap<>(); + for (Server server : plugin.servers) { + String pluginVersion = server.plugins.stream().filter(p -> p.plugin.equals(plugin)).findFirst().get().version; + serversTotal++; + versions.put(pluginVersion, versions.getOrDefault(pluginVersion, 0) + 1); + servers.put(server.serverVersion, servers.getOrDefault(server.serverVersion, 0) + 1); + } + + StatsResponse statsResponse = new StatsResponse(); + statsResponse.id = id; + statsResponse.name = plugin.name; + statsResponse.servers = serversTotal; + statsResponse.serverVersions = servers.entrySet().stream().map(e -> new Version(e.getKey(), e.getValue())).toList(); + statsResponse.pluginVersions = versions.entrySet().stream().map(e -> new Version(e.getKey(), e.getValue())).toList(); + + return Response.ok(statsResponse).build(); + } + + + @Path("/find/{name}") + @GET + public Response find(String name) { + Plugin plugin = Plugin.find("name", name).firstResult(); + if (plugin == null) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + return Response.ok(new ObjectMapper().createObjectNode().put("id", plugin.id)).build(); + + } + + class StatsResponse { + @JsonProperty("id") + public Long id; + + @JsonProperty("name") + public String name; + + @JsonProperty("servers") + public int servers; + + @JsonProperty("serverVersions") + public List serverVersions; + + @JsonProperty("pluginVersions") + public List pluginVersions; + } + + class Version { + public Version(String version, int servers) { + this.version = version; + this.servers = servers; + } + + @JsonProperty("version") + public String version; + + @JsonProperty("servers") + public int servers; + } +} diff --git a/src/main/java/eu/m724/mstats/api/resource/ServerApiResource.java b/src/main/java/eu/m724/mstats/api/resource/ServerApiResource.java new file mode 100644 index 0000000..2400ae0 --- /dev/null +++ b/src/main/java/eu/m724/mstats/api/resource/ServerApiResource.java @@ -0,0 +1,100 @@ +package eu.m724.mstats.api.resource; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.Nulls; +import eu.m724.mstats.orm.Server; +import eu.m724.mstats.api.service.ServerService; +import io.quarkus.security.identity.SecurityIdentity; +import jakarta.inject.Inject; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +import java.util.ArrayList; +import java.util.List; + +@Path("/api/server") +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.APPLICATION_JSON) +public class ServerApiResource { + @Inject + ServerService serverService; + + @Inject + SecurityIdentity identity; + + @Path("/heartbeat") + @POST + public Response heartbeat(HeartbeatRequest heartbeatRequest) { + if (heartbeatRequest == null) + heartbeatRequest = new HeartbeatRequest(); + + HeartbeatResponse heartbeatResponse = new HeartbeatResponse(); + Server server; + + if (identity.isAnonymous()) { + server = serverService.createServer(); + heartbeatResponse.token = server.getTokenEncoded(); + } else { + server = identity.getAttribute("server"); + } + + if (heartbeatRequest.statsVersion != 1) + heartbeatResponse.version = 1; + + serverService.heartbeat(server, heartbeatRequest); + + return Response.ok(heartbeatResponse).build(); + } + + public static class HeartbeatRequest { + /** List of registered plugins, this is sent only on boot */ + @JsonProperty("plugins") + public List plugins = new ArrayList<>(); + + /** Server version like 1.21.1, this is sent only on boot */ + @JsonProperty("serverVersion") + public String serverVersion; + + /** Client version */ + @JsonProperty("statsVersion") + public int statsVersion; + + public HeartbeatRequest() {} + } + + @JsonInclude(JsonInclude.Include.NON_NULL) + public static class HeartbeatResponse { + /** New token assigned to the server, usually after first request */ + @JsonProperty("token") + @JsonSetter(nulls = Nulls.SKIP) + public String token = null; + + /** A message that will be displayed to server console */ + @JsonProperty("message") + @JsonSetter(nulls = Nulls.SKIP) + public String message = null; + + /** Server version, used only if it doesn't match the client version */ + @JsonProperty("version") + @JsonSetter(nulls = Nulls.SKIP) + public Integer version = null; + + public HeartbeatResponse() {} + } + + public static class RunningPlugin { + public RunningPlugin() {} + + @JsonProperty("id") + public Long id; + + @JsonProperty("version") + public String version; + } +} diff --git a/src/main/java/eu/m724/mstats/resource/api/PluginService.java b/src/main/java/eu/m724/mstats/api/service/PluginService.java similarity index 71% rename from src/main/java/eu/m724/mstats/resource/api/PluginService.java rename to src/main/java/eu/m724/mstats/api/service/PluginService.java index 25f0715..71dcb92 100644 --- a/src/main/java/eu/m724/mstats/resource/api/PluginService.java +++ b/src/main/java/eu/m724/mstats/api/service/PluginService.java @@ -1,4 +1,4 @@ -package eu.m724.mstats.resource.api; +package eu.m724.mstats.api.service; import eu.m724.mstats.orm.Plugin; import jakarta.enterprise.context.ApplicationScoped; @@ -7,8 +7,8 @@ import jakarta.transaction.Transactional; @ApplicationScoped public class PluginService { @Transactional - public Plugin createPlugin(String name, String description) { - return Plugin.createPlugin(name, description); + public Plugin createPlugin(String name) { + return Plugin.createPlugin(name); } @Transactional @@ -29,7 +29,7 @@ public class PluginService { } @Transactional - public Plugin editPlugin(long id, String name, String description) { + public Plugin editPlugin(long id, String name) { Plugin plugin = Plugin.findById(id); if (plugin == null) @@ -38,9 +38,6 @@ public class PluginService { if (name != null) plugin.name = name; - if (description != null) - plugin.description = description; - plugin.persistAndFlush(); return plugin; } diff --git a/src/main/java/eu/m724/mstats/api/service/ServerService.java b/src/main/java/eu/m724/mstats/api/service/ServerService.java new file mode 100644 index 0000000..1921ad2 --- /dev/null +++ b/src/main/java/eu/m724/mstats/api/service/ServerService.java @@ -0,0 +1,36 @@ +package eu.m724.mstats.api.service; + +import eu.m724.mstats.api.resource.ServerApiResource; +import eu.m724.mstats.orm.Server; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.transaction.Transactional; + +import java.time.Instant; + +@ApplicationScoped +public class ServerService { + @Transactional + public Server createServer() { + return Server.createServer(); + } + + @Transactional + public void modifyServer(Server server) { + server.persistAndFlush(); + } + + @Transactional + public void heartbeat(Server server, ServerApiResource.HeartbeatRequest heartbeatRequest) { + server = Server.findById(server.id); // this is necessary for some reason + + for (ServerApiResource.RunningPlugin plugin : heartbeatRequest.plugins) { + server.addPlugin(plugin.id, plugin.version); + } + + if (heartbeatRequest.serverVersion != null) + server.serverVersion = heartbeatRequest.serverVersion; + + server.heartbeat = Instant.now(); + server.persistAndFlush(); + } +} diff --git a/src/main/java/eu/m724/mstats/auth/AuthService.java b/src/main/java/eu/m724/mstats/auth/AuthService.java index 199775b..4e32458 100644 --- a/src/main/java/eu/m724/mstats/auth/AuthService.java +++ b/src/main/java/eu/m724/mstats/auth/AuthService.java @@ -1,6 +1,5 @@ package eu.m724.mstats.auth; -import eu.m724.mstats.orm.Administrator; import eu.m724.mstats.orm.Server; import jakarta.enterprise.context.ApplicationScoped; import jakarta.transaction.Transactional; @@ -9,17 +8,9 @@ import java.util.Base64; @ApplicationScoped public class AuthService { - public final Base64.Decoder decoder = Base64.getDecoder(); - - @Transactional - Administrator getAdministratorByToken(String encoded) { - byte[] token = decoder.decode(encoded); - return Administrator.find("token", (Object) token).firstResult(); - } - @Transactional Server getServerByToken(String encoded) { - byte[] token = decoder.decode(encoded); + byte[] token = Base64.getDecoder().decode(encoded); return Server.find("token", (Object) token).firstResult(); } } diff --git a/src/main/java/eu/m724/mstats/auth/MyHttpAuthenticationMechanism.java b/src/main/java/eu/m724/mstats/auth/MyHttpAuthenticationMechanism.java index a1a6d2e..20d026b 100644 --- a/src/main/java/eu/m724/mstats/auth/MyHttpAuthenticationMechanism.java +++ b/src/main/java/eu/m724/mstats/auth/MyHttpAuthenticationMechanism.java @@ -1,7 +1,5 @@ package eu.m724.mstats.auth; - -import eu.m724.mstats.orm.Administrator; import eu.m724.mstats.orm.Server; import io.quarkus.security.identity.IdentityProviderManager; import io.quarkus.security.identity.SecurityIdentity; @@ -16,7 +14,6 @@ import jakarta.annotation.Priority; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.inject.Alternative; import jakarta.inject.Inject; -import jakarta.ws.rs.core.Response; import java.util.function.Supplier; @@ -40,29 +37,6 @@ public class MyHttpAuthenticationMechanism implements HttpAuthenticationMechanis .addRole("server") .addAttribute("server", server) .build(); - } else { - context.response() - .setStatusCode(Response.Status.UNAUTHORIZED.getStatusCode()) - .setStatusMessage("{\"message\": \"X-Server-Token is invalid\"}"); - return null; - } - } else { - String adminTokenEncoded = context.request().getHeader("X-Administrator-Token"); - if (adminTokenEncoded != null) { - Administrator administrator = authService.getAdministratorByToken(adminTokenEncoded); - if (administrator != null) { - return QuarkusSecurityIdentity.builder() - .setPrincipal(new QuarkusPrincipal("Administrator " + administrator.id.toString())) - .addRole("administrator") - .addRoles(administrator.roles) - .addAttribute("administrator", administrator) - .build(); - } else { - context.response() - .setStatusCode(Response.Status.UNAUTHORIZED.getStatusCode()) - .setStatusMessage("{\"message\": \"X-Administrator-Token is invalid\"}"); - return null; - } } } diff --git a/src/main/java/eu/m724/mstats/orm/Administrator.java b/src/main/java/eu/m724/mstats/orm/Administrator.java deleted file mode 100644 index b4e509e..0000000 --- a/src/main/java/eu/m724/mstats/orm/Administrator.java +++ /dev/null @@ -1,39 +0,0 @@ -package eu.m724.mstats.orm; - -import io.quarkus.hibernate.orm.panache.PanacheEntity; -import jakarta.persistence.*; -import jakarta.transaction.Transactional; - -import java.util.Base64; -import java.util.Set; -import java.util.concurrent.ThreadLocalRandom; - -@Entity -@Table( - uniqueConstraints = @UniqueConstraint(columnNames = "token"), - indexes = @Index(columnList = "token") -) -public class Administrator extends PanacheEntity { - @Column(nullable = false) - public byte[] token; - - public Set roles; - - public String getTokenEncoded() { - return Base64.getEncoder().encodeToString(token); - } - - @Transactional - public static Administrator createAdministrator(String... roles) { - byte[] token = new byte[32]; - ThreadLocalRandom.current().nextBytes(token); - - Administrator administrator = new Administrator(); - administrator.token = token; - administrator.roles = Set.of(roles); - - administrator.persistAndFlush(); - - return administrator; - } -} diff --git a/src/main/java/eu/m724/mstats/orm/Plugin.java b/src/main/java/eu/m724/mstats/orm/Plugin.java index 125f329..961b405 100644 --- a/src/main/java/eu/m724/mstats/orm/Plugin.java +++ b/src/main/java/eu/m724/mstats/orm/Plugin.java @@ -1,37 +1,30 @@ package eu.m724.mstats.orm; import io.quarkus.hibernate.orm.panache.PanacheEntity; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.ManyToMany; +import jakarta.persistence.*; import jakarta.transaction.Transactional; import java.util.ArrayList; import java.util.List; @Entity +@Table( + indexes = @Index(columnList = "name") +) public class Plugin extends PanacheEntity { @Column(unique = true, nullable = false) public String name; - public String description; - - @ManyToMany + @ManyToMany(fetch = FetchType.EAGER) public List servers = new ArrayList<>(); @Transactional - public static Plugin createPlugin(String name, String description) { + public static Plugin createPlugin(String name) { Plugin plugin = new Plugin(); plugin.name = name; - plugin.description = description; plugin.persistAndFlush(); return plugin; } - - @Transactional - public static Plugin createPlugin(String name) { - return createPlugin(name, null); - } } diff --git a/src/main/java/eu/m724/mstats/orm/PluginWithVersion.java b/src/main/java/eu/m724/mstats/orm/PluginWithVersion.java new file mode 100644 index 0000000..cd893ce --- /dev/null +++ b/src/main/java/eu/m724/mstats/orm/PluginWithVersion.java @@ -0,0 +1,19 @@ +package eu.m724.mstats.orm; + +import io.quarkus.hibernate.orm.panache.PanacheEntity; +import jakarta.persistence.*; + +@Entity +public class PluginWithVersion extends PanacheEntity { + public PluginWithVersion() {} + + public PluginWithVersion(Plugin plugin, String version) { + this.plugin = plugin; + this.version = version; + } + + @ManyToOne + public Plugin plugin; + + public String version; +} diff --git a/src/main/java/eu/m724/mstats/orm/Server.java b/src/main/java/eu/m724/mstats/orm/Server.java index 1a2dc54..186a112 100644 --- a/src/main/java/eu/m724/mstats/orm/Server.java +++ b/src/main/java/eu/m724/mstats/orm/Server.java @@ -19,13 +19,10 @@ public class Server extends PanacheEntity { @Column(nullable = false) public byte[] token; - /** - * ISO 3166-1 numeric - */ - public int countryCode; + @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER) + public List plugins = new ArrayList<>(); - @ManyToMany(cascade = CascadeType.ALL) - public List plugins = new ArrayList<>(); + public String serverVersion; public Instant heartbeat = Instant.now(); @@ -34,13 +31,16 @@ public class Server extends PanacheEntity { } @Transactional - public Plugin addPlugin(long id) { + public Plugin addPlugin(long id, String version) { Plugin plugin = Plugin.findById(id); if (plugin == null) return null; - plugins.add(plugin); + if (plugins.stream().anyMatch(pwv -> pwv.plugin.id == id)) + return null; + + plugins.add(new PluginWithVersion(plugin, version)); plugin.servers.add(this); this.persistAndFlush(); diff --git a/src/main/java/eu/m724/mstats/resource/api/AdminApiResource.java b/src/main/java/eu/m724/mstats/resource/api/AdminApiResource.java deleted file mode 100644 index b364bdf..0000000 --- a/src/main/java/eu/m724/mstats/resource/api/AdminApiResource.java +++ /dev/null @@ -1,90 +0,0 @@ -package eu.m724.mstats.resource.api; - -import eu.m724.mstats.auth.AuthService; -import eu.m724.mstats.orm.Plugin; -import io.quarkus.security.identity.SecurityIdentity; -import io.vertx.core.json.JsonObject; -import jakarta.annotation.security.RolesAllowed; -import jakarta.inject.Inject; -import jakarta.ws.rs.*; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; - -@Path("/api/admin") -@Consumes(MediaType.APPLICATION_JSON) -@Produces(MediaType.APPLICATION_JSON) -@RolesAllowed("administrator") -public class AdminApiResource { - @Inject - AuthService authService; - - @Inject - PluginService pluginService; - - @Inject - SecurityIdentity identity; - - @Path("/plugin") - @GET - public Response getPlugin(@QueryParam("id") long id) { - Plugin plugin = pluginService.getPlugin(id); - - if (plugin == null) - return Response.ok(new JsonObject().put("id", id)).status(Response.Status.NOT_FOUND).build(); - - JsonObject response = new JsonObject() - .put("id", plugin.id) - .put("name", plugin.name) - .put("description", plugin.description); - - return Response.ok(response).build(); - } - - @Path("/plugin") - @PUT - public Response createPlugin(JsonObject data) { - String name = data.getString("name"); - String description = data.getString("description"); - - Plugin plugin = pluginService.createPlugin(name, description); - - JsonObject response = new JsonObject() - .put("id", plugin.id); - - return Response.ok(response).build(); - } - - @Path("/plugin") - @PATCH - public Response editPlugin(JsonObject data) { - long id = data.getLong("id"); - String name = data.getString("name"); - String description = data.getString("description"); - - Plugin plugin = pluginService.editPlugin(id, name, description); - - if (plugin == null) - return Response.ok(new JsonObject().put("id", id)).status(Response.Status.NOT_FOUND).build(); - - JsonObject response = new JsonObject() - .put("id", plugin.id); - - return Response.ok(response).build(); - } - - @Path("/plugin") - @DELETE - public Response deletePlugin(JsonObject data) { - long id = data.getLong("id"); - - Plugin plugin = pluginService.deletePlugin(id); - - if (plugin == null) - return Response.ok(new JsonObject().put("id", id)).status(Response.Status.NOT_FOUND).build(); - - JsonObject response = new JsonObject() - .put("id", plugin.id); - - return Response.ok(response).build(); - } -} diff --git a/src/main/java/eu/m724/mstats/resource/api/ServerApiResource.java b/src/main/java/eu/m724/mstats/resource/api/ServerApiResource.java deleted file mode 100644 index 0418914..0000000 --- a/src/main/java/eu/m724/mstats/resource/api/ServerApiResource.java +++ /dev/null @@ -1,82 +0,0 @@ -package eu.m724.mstats.resource.api; - -import com.fasterxml.jackson.annotation.JsonProperty; -import eu.m724.mstats.orm.Plugin; -import eu.m724.mstats.orm.Server; -import io.quarkus.security.identity.SecurityIdentity; -import jakarta.annotation.security.RolesAllowed; -import jakarta.inject.Inject; -import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.POST; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; - -import java.util.ArrayList; -import java.util.List; - -@Path("/api/server") -@Consumes(MediaType.APPLICATION_JSON) -@Produces(MediaType.APPLICATION_JSON) -public class ServerApiResource { - @Inject - ServerService serverService; - - @Inject - SecurityIdentity identity; - - @Path("/boot") - @POST - public Response boot(BootRequest bootRequest) { - BootResponse bootResponse = new BootResponse(); - Server server; - - if (identity.isAnonymous()) { - server = serverService.createServer(); - bootResponse.token = server.getTokenEncoded(); - } else { - server = identity.getAttribute("server"); - } - - List invalidIds = new ArrayList<>(); - - for (Long pluginId : bootRequest.plugins) { - Plugin plugin = server.addPlugin(pluginId); - if (plugin == null) - invalidIds.add(pluginId); - } - - if (!invalidIds.isEmpty()) { - bootResponse.invalidPlugins = invalidIds; - } - - serverService. - - return Response.ok(bootResponse).build(); - } - - @Path("/heartbeat") - @POST - @RolesAllowed("server") - public void heartbeat() { - - } - - public static class BootRequest { - @JsonProperty("plugins") - public List plugins; - - public BootRequest() {} - } - - public static class BootResponse { - @JsonProperty("token") - public String token; - - @JsonProperty("invalidPlugins") - public List invalidPlugins; - - public BootResponse() {} - } -} diff --git a/src/main/java/eu/m724/mstats/resource/api/ServerService.java b/src/main/java/eu/m724/mstats/resource/api/ServerService.java deleted file mode 100644 index 01b63ac..0000000 --- a/src/main/java/eu/m724/mstats/resource/api/ServerService.java +++ /dev/null @@ -1,26 +0,0 @@ -package eu.m724.mstats.resource.api; - -import eu.m724.mstats.orm.Server; -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.transaction.Transactional; - -import java.util.List; - -@ApplicationScoped -public class ServerService { - @Transactional - public Server createServer() { - return Server.createServer(); - } - - @Transactional - public void modifyServer(Server server) { - server.persistAndFlush(); - } - - @Transactional - public void heartbeat(Server server, List plugins) { - server.heartbeat = - server.persistAndFlush(); - } -}