From 87a8c991bd8f664a7b2a126fb62c781a4945e453 Mon Sep 17 00:00:00 2001 From: kuroppoi <68156848+kuroppoi@users.noreply.github.com> Date: Sun, 5 Feb 2023 19:28:28 +0100 Subject: [PATCH] Embrace simplicity --- .../java/brainwine/api/GatewayService.java | 141 +++++++++++++++--- .../java/brainwine/api/PortalService.java | 90 ++++++++++- .../api/handlers/NewsRequestHandler.java | 23 --- .../api/handlers/PasswordForgotHandler.java | 14 -- .../api/handlers/PasswordResetHandler.java | 14 -- .../api/handlers/PlayerLoginHandler.java | 47 ------ .../handlers/PlayerRegistrationHandler.java | 39 ----- .../api/handlers/RwcPurchaseHandler.java | 14 -- .../api/handlers/SimpleExceptionHandler.java | 21 --- .../api/handlers/ZoneSearchHandler.java | 78 ---------- 10 files changed, 208 insertions(+), 273 deletions(-) delete mode 100644 api/src/main/java/brainwine/api/handlers/NewsRequestHandler.java delete mode 100644 api/src/main/java/brainwine/api/handlers/PasswordForgotHandler.java delete mode 100644 api/src/main/java/brainwine/api/handlers/PasswordResetHandler.java delete mode 100644 api/src/main/java/brainwine/api/handlers/PlayerLoginHandler.java delete mode 100644 api/src/main/java/brainwine/api/handlers/PlayerRegistrationHandler.java delete mode 100644 api/src/main/java/brainwine/api/handlers/RwcPurchaseHandler.java delete mode 100644 api/src/main/java/brainwine/api/handlers/SimpleExceptionHandler.java delete mode 100644 api/src/main/java/brainwine/api/handlers/ZoneSearchHandler.java diff --git a/api/src/main/java/brainwine/api/GatewayService.java b/api/src/main/java/brainwine/api/GatewayService.java index e908575..2ff42eb 100644 --- a/api/src/main/java/brainwine/api/GatewayService.java +++ b/api/src/main/java/brainwine/api/GatewayService.java @@ -1,40 +1,147 @@ package brainwine.api; +import static brainwine.api.util.ContextUtils.error; import static brainwine.shared.LogMarkers.SERVER_MARKER; +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Pattern; + import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import brainwine.api.handlers.NewsRequestHandler; -import brainwine.api.handlers.PasswordForgotHandler; -import brainwine.api.handlers.PasswordResetHandler; -import brainwine.api.handlers.PlayerLoginHandler; -import brainwine.api.handlers.PlayerRegistrationHandler; -import brainwine.api.handlers.RwcPurchaseHandler; -import brainwine.api.handlers.SimpleExceptionHandler; +import brainwine.api.models.PlayersRequest; +import brainwine.api.models.ServerConnectInfo; +import brainwine.api.models.SessionsRequest; import brainwine.shared.JsonHelper; import io.javalin.Javalin; +import io.javalin.http.Context; import io.javalin.plugin.json.JavalinJackson; public class GatewayService { + private static final Pattern namePattern = Pattern.compile("^[a-zA-Z0-9_.-]{4,20}$"); private static final Logger logger = LogManager.getLogger(); + private final Api api; + private final DataFetcher dataFetcher; private final Javalin gateway; public GatewayService(Api api, int port) { + this.api = api; + this.dataFetcher = api.getDataFetcher(); logger.info(SERVER_MARKER, "Starting GatewayService @ port {} ...", port); - DataFetcher dataFetcher = api.getDataFetcher(); - String gameServerHost = api.getGameServerHost(); - gateway = Javalin.create(config -> config.jsonMapper(new JavalinJackson(JsonHelper.MAPPER))).start(port); - gateway.exception(Exception.class, new SimpleExceptionHandler()); - gateway.get("/clients", new NewsRequestHandler(api.getNews())); - gateway.post("/players", new PlayerRegistrationHandler(dataFetcher, gameServerHost)); - gateway.post("/sessions", new PlayerLoginHandler(dataFetcher, gameServerHost)); - gateway.post("/passwords/request", new PasswordForgotHandler()); - gateway.post("/passwords/reset", new PasswordResetHandler()); - gateway.post("/purchases", new RwcPurchaseHandler()); + gateway = Javalin.create(config -> config.jsonMapper(new JavalinJackson(JsonHelper.MAPPER))) + .exception(Exception.class, this::handleException) + .get("/clients", this::handleNewsRequest) + .post("/players", this::handlePlayerRegistration) + .post("/sessions", this::handlePlayerLogin) + .post("/passwords/request", this::handlePasswordForget) + .post("/passwords/reset", this::handlePasswordReset) + .post("/purchases", this::handleInAppPurchase) + .start(port); } + /** + * Exception handler function. + */ + private void handleException(Exception exception, Context ctx) { + logger.error(SERVER_MARKER, "Exception caught", exception); + error(ctx, "%s", exception); + } + + /** + * Handler function for news requests. (main menu) + */ + private void handleNewsRequest(Context ctx) { + Map news = new HashMap<>(); + news.put("posts", api.getNews()); + ctx.json(news); + } + + /** + * Handler function for registering a new account. + */ + private void handlePlayerRegistration(Context ctx) { + PlayersRequest request = ctx.bodyValidator(PlayersRequest.class).get(); + String name = request.getName(); + + // Check if name is too short, too long or contains illegal characters + if(!namePattern.matcher(name).matches()) { + error(ctx, "Please enter a valid username."); + return; + } + + // Check if a player with this name already exists + if(dataFetcher.isPlayerNameTaken(name)) { + error(ctx, "Sorry, this username has already been taken."); + return; + } + + String token = dataFetcher.registerPlayer(name); + ctx.json(new ServerConnectInfo(api.getGameServerHost(), name, token)); + } + + /** + * Handler function for logging into an existing account with username & password/auth token. + * If the user logs in using a password, a new auth token is generated. + */ + private void handlePlayerLogin(Context ctx) { + SessionsRequest request = ctx.bodyValidator(SessionsRequest.class).get(); + String name = request.getName(); + String password = request.getPassword(); + String token = request.getToken(); + + // If a password is present, try to log in and generate an auth token. + // Null auth token = incorrect username/password combination. + // Otherwise, if an auth token is present, try to verify that instead. + if(password != null) { + token = dataFetcher.login(name, password); + + if(token == null) { + error(ctx, "Username or password is incorrect. Please check your credentials."); + return; + } + } else if(token != null) { + if(!dataFetcher.verifyAuthToken(name, token)) { + error(ctx, "The provided session token is invalid or has expired. Please try relogging."); + return; + } + } else { + error(ctx, "No credentials provided."); + return; + } + + ctx.json(new ServerConnectInfo(api.getGameServerHost(), name, token)); + } + + /** + * Handler function for initiating password resets. + * TODO wip + */ + private void handlePasswordForget(Context ctx) { + error(ctx, "Sorry, it is currently not possible to reset your password."); + } + + /** + * Handler function for processing password resets. + * TODO wip + */ + private void handlePasswordReset(Context ctx) { + error(ctx, "Sorry, it is currently not possible to reset your password."); + } + + /** + * Handler function for in-app purchases. + * Permanently doomed to err, as it will never be implemented. + */ + private void handleInAppPurchase(Context ctx) { + error(ctx, "Sorry, in-app purchases are not supported."); + } + + /** + * Stops the gateway service. + * @see Javalin#stop() + */ public void stop() { gateway.stop(); } diff --git a/api/src/main/java/brainwine/api/PortalService.java b/api/src/main/java/brainwine/api/PortalService.java index 30907d1..46d0d00 100644 --- a/api/src/main/java/brainwine/api/PortalService.java +++ b/api/src/main/java/brainwine/api/PortalService.java @@ -1,14 +1,18 @@ package brainwine.api; +import static brainwine.api.util.ContextUtils.error; +import static brainwine.api.util.ContextUtils.handleQueryParam; import static brainwine.shared.LogMarkers.SERVER_MARKER; +import java.util.List; + import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import brainwine.api.handlers.SimpleExceptionHandler; -import brainwine.api.handlers.ZoneSearchHandler; +import brainwine.api.models.ZoneInfo; import brainwine.shared.JsonHelper; import io.javalin.Javalin; +import io.javalin.http.Context; import io.javalin.plugin.json.JavalinJackson; /** @@ -16,17 +20,91 @@ import io.javalin.plugin.json.JavalinJackson; */ public class PortalService { + private static final int zoneSearchPageSize = 6; private static final Logger logger = LogManager.getLogger(); + private final DataFetcher dataFetcher; private final Javalin portal; public PortalService(Api api, int port) { + this.dataFetcher = api.getDataFetcher(); logger.info(SERVER_MARKER, "Starting PortalService @ port {} ...", port); - DataFetcher dataFetcher = api.getDataFetcher(); - portal = Javalin.create(config -> config.jsonMapper(new JavalinJackson(JsonHelper.MAPPER))).start(port); - portal.exception(Exception.class, new SimpleExceptionHandler()); - portal.get("/v1/worlds", new ZoneSearchHandler(dataFetcher)); + portal = Javalin.create(config -> config.jsonMapper(new JavalinJackson(JsonHelper.MAPPER))) + .exception(Exception.class, this::handleException) + .get("/v1/worlds", this::handleZoneSearch) + .start(port); } + /** + * Exception handler function. + */ + private void handleException(Exception exception, Context ctx) { + logger.error(SERVER_MARKER, "Exception caught", exception); + error(ctx, "%s", exception); + } + + /** + * Handler function for zone search requests. + * TODO could use some work. + */ + private void handleZoneSearch(Context ctx) { + final List zones = (List)dataFetcher.fetchZoneInfo(); + String apiToken = ctx.queryParam("api_token"); + + if(apiToken == null || !dataFetcher.verifyApiToken(apiToken)) { + error(ctx, "A valid api token is required for this request."); + return; + } + + handleQueryParam(ctx, "name", String.class, name -> { + zones.removeIf(zone -> !zone.getName().toLowerCase().contains(name.toLowerCase())); + }); + + handleQueryParam(ctx, "activity", String.class, activity -> { + zones.removeIf(zone -> zone.getActivity() == null || !zone.getActivity().equalsIgnoreCase(activity)); + }); + + handleQueryParam(ctx, "biome", String.class, biome -> { + zones.removeIf(zone -> !zone.getBiome().equalsIgnoreCase(biome)); + }); + + handleQueryParam(ctx, "pvp", boolean.class, pvp -> { + zones.removeIf(zone -> zone.isPvp() != pvp); + }); + + handleQueryParam(ctx, "protected", boolean.class, locked -> { + zones.removeIf(zone -> zone.isLocked() != locked); + }); + + handleQueryParam(ctx, "residency", String.class, residency -> { + zones.clear(); // not supported yet + }); + + handleQueryParam(ctx, "account", String.class, account -> { + zones.clear(); // not supported yet + }); + + handleQueryParam(ctx, "sort", String.class, sort -> { + switch(sort) { + case "popularity": + zones.removeIf(zone -> zone.getPlayerCount() == 0); + zones.sort((a, b) -> Integer.compare(b.getPlayerCount(), a.getPlayerCount())); + break; + case "created": + break; + } + }); + + // Page + int page = ctx.queryParamAsClass("page", Integer.class).getOrDefault(1); + int fromIndex = (page - 1) * zoneSearchPageSize; + int toIndex = page * zoneSearchPageSize; + ctx.json(zones.subList(fromIndex < 0 ? 0 : fromIndex > zones.size() ? zones.size() : fromIndex, toIndex > zones.size() ? zones.size() : toIndex)); + } + + /** + * Stops the portal service. + * @see Javalin#stop() + */ public void stop() { portal.stop(); } diff --git a/api/src/main/java/brainwine/api/handlers/NewsRequestHandler.java b/api/src/main/java/brainwine/api/handlers/NewsRequestHandler.java deleted file mode 100644 index 4fe56c7..0000000 --- a/api/src/main/java/brainwine/api/handlers/NewsRequestHandler.java +++ /dev/null @@ -1,23 +0,0 @@ -package brainwine.api.handlers; - -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; - -import brainwine.api.config.NewsEntry; -import io.javalin.http.Context; -import io.javalin.http.Handler; - -public class NewsRequestHandler implements Handler { - - private final Map news = new HashMap<>(); - - public NewsRequestHandler(Collection posts) { - news.put("posts", posts); - } - - @Override - public void handle(Context ctx) throws Exception { - ctx.json(news); - } -} diff --git a/api/src/main/java/brainwine/api/handlers/PasswordForgotHandler.java b/api/src/main/java/brainwine/api/handlers/PasswordForgotHandler.java deleted file mode 100644 index a7c1784..0000000 --- a/api/src/main/java/brainwine/api/handlers/PasswordForgotHandler.java +++ /dev/null @@ -1,14 +0,0 @@ -package brainwine.api.handlers; - -import static brainwine.api.util.ContextUtils.error; - -import io.javalin.http.Context; -import io.javalin.http.Handler; - -public class PasswordForgotHandler implements Handler { - - @Override - public void handle(Context ctx) throws Exception { - error(ctx, "Sorry, it is currently not possible to reset your password."); - } -} diff --git a/api/src/main/java/brainwine/api/handlers/PasswordResetHandler.java b/api/src/main/java/brainwine/api/handlers/PasswordResetHandler.java deleted file mode 100644 index 073caf3..0000000 --- a/api/src/main/java/brainwine/api/handlers/PasswordResetHandler.java +++ /dev/null @@ -1,14 +0,0 @@ -package brainwine.api.handlers; - -import static brainwine.api.util.ContextUtils.error; - -import io.javalin.http.Context; -import io.javalin.http.Handler; - -public class PasswordResetHandler implements Handler { - - @Override - public void handle(Context ctx) throws Exception { - error(ctx, "Sorry, it is currently not possible to reset your password."); - } -} diff --git a/api/src/main/java/brainwine/api/handlers/PlayerLoginHandler.java b/api/src/main/java/brainwine/api/handlers/PlayerLoginHandler.java deleted file mode 100644 index 1d6ab0d..0000000 --- a/api/src/main/java/brainwine/api/handlers/PlayerLoginHandler.java +++ /dev/null @@ -1,47 +0,0 @@ -package brainwine.api.handlers; - -import static brainwine.api.util.ContextUtils.error; - -import brainwine.api.DataFetcher; -import brainwine.api.models.ServerConnectInfo; -import brainwine.api.models.SessionsRequest; -import io.javalin.http.Context; -import io.javalin.http.Handler; - -public class PlayerLoginHandler implements Handler { - - private final DataFetcher dataFetcher; - private final String gameServerHost; - - public PlayerLoginHandler(DataFetcher dataFetcher, String gameServerHost) { - this.dataFetcher = dataFetcher; - this.gameServerHost = gameServerHost; - } - - @Override - public void handle(Context ctx) throws Exception { - SessionsRequest request = ctx.bodyValidator(SessionsRequest.class).get(); - String name = request.getName(); - String password = request.getPassword(); - String token = request.getToken(); - - if(password != null) { - token = dataFetcher.login(name, password); - - if(token == null) { - error(ctx, "Username or password is incorrect. Please check your credentials."); - return; - } - } else if(token != null) { - if(!dataFetcher.verifyAuthToken(name, token)) { - error(ctx, "The provided session token is invalid or has expired. Please try relogging."); - return; - } - } else { - error(ctx, "No credentials provided."); - return; - } - - ctx.json(new ServerConnectInfo(gameServerHost, name, token)); - } -} diff --git a/api/src/main/java/brainwine/api/handlers/PlayerRegistrationHandler.java b/api/src/main/java/brainwine/api/handlers/PlayerRegistrationHandler.java deleted file mode 100644 index 3f941e1..0000000 --- a/api/src/main/java/brainwine/api/handlers/PlayerRegistrationHandler.java +++ /dev/null @@ -1,39 +0,0 @@ -package brainwine.api.handlers; - -import static brainwine.api.util.ContextUtils.error; - -import brainwine.api.DataFetcher; -import brainwine.api.models.PlayersRequest; -import brainwine.api.models.ServerConnectInfo; -import io.javalin.http.Context; -import io.javalin.http.Handler; - -public class PlayerRegistrationHandler implements Handler { - - private final DataFetcher dataFetcher; - private final String gameServerHost; - - public PlayerRegistrationHandler(DataFetcher dataFetcher, String gameServerHost) { - this.dataFetcher = dataFetcher; - this.gameServerHost = gameServerHost; - } - - @Override - public void handle(Context ctx) throws Exception { - PlayersRequest request = ctx.bodyValidator(PlayersRequest.class).get(); - String name = request.getName(); - - if(!name.matches("^[a-zA-Z0-9_.-]{4,20}$")) { - error(ctx, "Please enter a valid username."); - return; - } - - if(dataFetcher.isPlayerNameTaken(name)) { - error(ctx, "Sorry, this username has already been taken."); - return; - } - - String token = dataFetcher.registerPlayer(name); - ctx.json(new ServerConnectInfo(gameServerHost, name, token)); - } -} diff --git a/api/src/main/java/brainwine/api/handlers/RwcPurchaseHandler.java b/api/src/main/java/brainwine/api/handlers/RwcPurchaseHandler.java deleted file mode 100644 index 6873102..0000000 --- a/api/src/main/java/brainwine/api/handlers/RwcPurchaseHandler.java +++ /dev/null @@ -1,14 +0,0 @@ -package brainwine.api.handlers; - -import static brainwine.api.util.ContextUtils.error; - -import io.javalin.http.Context; -import io.javalin.http.Handler; - -public class RwcPurchaseHandler implements Handler { - - @Override - public void handle(Context ctx) throws Exception { - error(ctx, "Sorry, RWC purchases are disabled."); - } -} diff --git a/api/src/main/java/brainwine/api/handlers/SimpleExceptionHandler.java b/api/src/main/java/brainwine/api/handlers/SimpleExceptionHandler.java deleted file mode 100644 index 2f7ee4f..0000000 --- a/api/src/main/java/brainwine/api/handlers/SimpleExceptionHandler.java +++ /dev/null @@ -1,21 +0,0 @@ -package brainwine.api.handlers; - -import static brainwine.api.util.ContextUtils.error; -import static brainwine.shared.LogMarkers.SERVER_MARKER; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import io.javalin.http.Context; -import io.javalin.http.ExceptionHandler; - -public class SimpleExceptionHandler implements ExceptionHandler { - - private static final Logger logger = LogManager.getLogger(); - - @Override - public void handle(Exception exception, Context ctx) { - logger.error(SERVER_MARKER, "Exception caught", exception); - error(ctx, "%s", exception); - } -} diff --git a/api/src/main/java/brainwine/api/handlers/ZoneSearchHandler.java b/api/src/main/java/brainwine/api/handlers/ZoneSearchHandler.java deleted file mode 100644 index f5caeb8..0000000 --- a/api/src/main/java/brainwine/api/handlers/ZoneSearchHandler.java +++ /dev/null @@ -1,78 +0,0 @@ -package brainwine.api.handlers; - -import static brainwine.api.util.ContextUtils.error; -import static brainwine.api.util.ContextUtils.handleQueryParam; - -import java.util.List; - -import brainwine.api.DataFetcher; -import brainwine.api.models.ZoneInfo; -import io.javalin.http.Context; -import io.javalin.http.Handler; - -public class ZoneSearchHandler implements Handler { - - public static final int PAGE_SIZE = 6; - private final DataFetcher dataFetcher; - - public ZoneSearchHandler(DataFetcher dataFetcher) { - this.dataFetcher = dataFetcher; - } - - @Override - public void handle(Context ctx) throws Exception { - final List zones = (List)dataFetcher.fetchZoneInfo(); - String apiToken = ctx.queryParam("api_token"); - - if(apiToken == null || !dataFetcher.verifyApiToken(apiToken)) { - error(ctx, "A valid api token is required for this request."); - return; - } - - handleQueryParam(ctx, "name", String.class, name -> { - zones.removeIf(zone -> !zone.getName().toLowerCase().contains(name.toLowerCase())); - }); - - handleQueryParam(ctx, "activity", String.class, activity -> { - zones.removeIf(zone -> zone.getActivity() == null || !zone.getActivity().equalsIgnoreCase(activity)); - }); - - handleQueryParam(ctx, "biome", String.class, biome -> { - zones.removeIf(zone -> !zone.getBiome().equalsIgnoreCase(biome)); - }); - - handleQueryParam(ctx, "pvp", boolean.class, pvp -> { - zones.removeIf(zone -> zone.isPvp() != pvp); - }); - - handleQueryParam(ctx, "protected", boolean.class, locked -> { - zones.removeIf(zone -> zone.isLocked() != locked); - }); - - handleQueryParam(ctx, "residency", String.class, residency -> { - zones.clear(); // not supported yet - }); - - handleQueryParam(ctx, "account", String.class, account -> { - zones.clear(); // not supported yet - }); - - handleQueryParam(ctx, "sort", String.class, sort -> { - switch(sort) { - case "popularity": - zones.removeIf(zone -> zone.getPlayerCount() == 0); - zones.sort((a, b) -> Integer.compare(b.getPlayerCount(), a.getPlayerCount())); - break; - case "created": - break; - } - }); - - // Page - int page = ctx.queryParamAsClass("page", Integer.class).getOrDefault(1); - int fromIndex = (page - 1) * PAGE_SIZE; - int toIndex = page * PAGE_SIZE; - ctx.json(zones.subList(fromIndex < 0 ? 0 : fromIndex > zones.size() ? zones.size() : fromIndex, toIndex > zones.size() ? zones.size() : toIndex)); - } - -}