Embrace simplicity

This commit is contained in:
kuroppoi 2023-02-05 19:28:28 +01:00
parent beb99d96da
commit 87a8c991bd
10 changed files with 208 additions and 273 deletions

View file

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

View file

@ -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<ZoneInfo> zones = (List<ZoneInfo>)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();
}

View file

@ -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<String, Object> news = new HashMap<>();
public NewsRequestHandler(Collection<NewsEntry> posts) {
news.put("posts", posts);
}
@Override
public void handle(Context ctx) throws Exception {
ctx.json(news);
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<ZoneInfo> zones = (List<ZoneInfo>)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));
}
}