diff options
| author | ilotterytea <iltsu@alright.party> | 2025-01-23 14:53:48 +0500 |
|---|---|---|
| committer | ilotterytea <iltsu@alright.party> | 2025-01-23 14:53:48 +0500 |
| commit | 3fd5917ef5333c4c9ee8c79ab360b654459626f2 (patch) | |
| tree | 84ec5c41950ecb59ed9e9fd0d3f38315ec1d038f | |
| parent | 35fafc313b8c9a7425af0d1fb930ed33c3c8413a (diff) | |
feat: client-side sign-in authorization system
7 files changed, 390 insertions, 35 deletions
diff --git a/core/src/main/java/kz/ilotterytea/frogartha/FrogarthaGame.java b/core/src/main/java/kz/ilotterytea/frogartha/FrogarthaGame.java index bf4159b..3d6d28f 100644 --- a/core/src/main/java/kz/ilotterytea/frogartha/FrogarthaGame.java +++ b/core/src/main/java/kz/ilotterytea/frogartha/FrogarthaGame.java @@ -1,6 +1,7 @@ package kz.ilotterytea.frogartha; import com.badlogic.gdx.Game; +import com.badlogic.gdx.Gdx; import com.badlogic.gdx.assets.AssetManager; import kz.ilotterytea.frogartha.assets.AssetUtils; import kz.ilotterytea.frogartha.screens.SplashScreen; @@ -24,7 +25,7 @@ public class FrogarthaGame extends Game { AssetUtils.setup(assetManager); AssetUtils.queue(assetManager); - identityClient = new IdentityClient(); + identityClient = new IdentityClient(Gdx.app.getPreferences("kz.ilotterytea.SigninIdentity")); sessionClient = new SessionClient(); setScreen(new SplashScreen()); diff --git a/core/src/main/java/kz/ilotterytea/frogartha/screens/MenuScreen.java b/core/src/main/java/kz/ilotterytea/frogartha/screens/MenuScreen.java index 13b69ea..f5948e2 100644 --- a/core/src/main/java/kz/ilotterytea/frogartha/screens/MenuScreen.java +++ b/core/src/main/java/kz/ilotterytea/frogartha/screens/MenuScreen.java @@ -14,21 +14,29 @@ import com.badlogic.gdx.scenes.scene2d.ui.*; import com.badlogic.gdx.scenes.scene2d.utils.ClickListener; import com.badlogic.gdx.utils.Align; import com.badlogic.gdx.utils.viewport.ScreenViewport; +import kz.ilotterytea.frogartha.FrogarthaConstants; import kz.ilotterytea.frogartha.FrogarthaGame; import kz.ilotterytea.frogartha.assets.Assets; import kz.ilotterytea.frogartha.domain.RoomTopic; +import kz.ilotterytea.frogartha.sessions.IdentityClient; +import kz.ilotterytea.frogartha.sessions.SessionClient; import kz.ilotterytea.frogartha.ui.menu.RoomTopicWidget; public class MenuScreen implements Screen { private FrogarthaGame game; + private SessionClient session; + private IdentityClient identity; + private Stage stage; - private Table gameTable, topicTable; + private Table gameTable, topicTable, credentialsTable; private Label authorizingLabel; @Override public void show() { game = FrogarthaGame.getInstance(); + session = game.getSessionClient(); + identity = game.getIdentityClient(); createStage(); @@ -44,17 +52,24 @@ public class MenuScreen implements Screen { stage.draw(); if ( - game.getIdentityClient().isAuthorized() && - !game.getSessionClient().isJoined() && + identity.isAuthorized() && + !session.isJoined() && authorizingLabel.hasParent() ) { gameTable.clear(); gameTable.add(topicTable).grow(); + } else if ( + !identity.isProcessing() && + !identity.isAuthorized() && + authorizingLabel.hasParent() + ) { + gameTable.clear(); + gameTable.add(credentialsTable).grow().maxWidth(384f); } - if (game.getSessionClient().hasThrown()) { + if (session.hasThrown()) { game.setScreen(new KickScreen()); - } else if (game.getSessionClient().isJoined()) { + } else if (session.isJoined()) { game.setScreen(new GameScreen()); } } @@ -126,6 +141,7 @@ public class MenuScreen implements Screen { // --- Authorizing label --- authorizingLabel = new Label("Authorizing", skin); authorizingLabel.setAlignment(Align.center); + gameTable.add(authorizingLabel).grow(); // --- Topic selector --- topicTable = new Table(); @@ -164,8 +180,7 @@ public class MenuScreen implements Screen { } // --- Credentials --- - Table credentialsTable = new Table(); - gameTable.add(credentialsTable).grow().maxWidth(384f); + credentialsTable = new Table(); // Username Label usernameLabel = new Label("Username", skin); @@ -177,13 +192,36 @@ public class MenuScreen implements Screen { usernameField.setMaxLength(25); credentialsTable.add(usernameField).padBottom(15f).grow().row(); + // Password + Label passwordLabel = new Label("Password", skin); + credentialsTable.add(passwordLabel).grow().row(); + + TextField passwordField = new TextField("", skin); + passwordField.setPasswordMode(true); + passwordField.setPasswordCharacter('*'); + passwordField.setMessageText("..."); + passwordField.setTextFieldFilter((tf, c) -> Character.toString(c).matches("^[a-zA-Z0-9]")); + passwordField.setMaxLength(25); + credentialsTable.add(passwordField).padBottom(15f).grow().row(); + + // Register button + TextButton registerButton = new TextButton("Need account?", skin, "link"); + registerButton.addListener(new ClickListener() { + @Override + public void clicked(InputEvent event, float x, float y) { + super.clicked(event, x, y); + Gdx.net.openURI(FrogarthaConstants.URLS.IDENTITY_REGISTRATION_URL); + } + }); + credentialsTable.add(registerButton).padTop(15f).padBottom(15f).grow().row(); + // Login button TextButton loginButton = new TextButton("Login", skin); loginButton.addListener(new ClickListener() { @Override public void clicked(InputEvent event, float x, float y) { super.clicked(event, x, y); - FrogarthaGame.getInstance().getIdentityClient().authorize(usernameField.getText()); + FrogarthaGame.getInstance().getIdentityClient().authorize(usernameField.getText(), passwordField.getText()); gameTable.removeActor(credentialsTable); gameTable.add(authorizingLabel).grow(); } diff --git a/core/src/main/java/kz/ilotterytea/frogartha/sessions/IdentityClient.java b/core/src/main/java/kz/ilotterytea/frogartha/sessions/IdentityClient.java index 64e7270..10af0e0 100644 --- a/core/src/main/java/kz/ilotterytea/frogartha/sessions/IdentityClient.java +++ b/core/src/main/java/kz/ilotterytea/frogartha/sessions/IdentityClient.java @@ -1,53 +1,331 @@ package kz.ilotterytea.frogartha.sessions; +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.Net; +import com.badlogic.gdx.Preferences; +import com.badlogic.gdx.net.HttpRequestBuilder; +import com.badlogic.gdx.net.HttpStatus; +import com.badlogic.gdx.utils.JsonReader; +import com.badlogic.gdx.utils.JsonValue; +import com.badlogic.gdx.utils.JsonWriter; +import com.badlogic.gdx.utils.Timer; +import kz.ilotterytea.frogartha.FrogarthaConstants; import kz.ilotterytea.frogartha.FrogarthaGame; import kz.ilotterytea.frogartha.utils.Logger; +import kz.ilotterytea.frogartha.utils.RandomUtils; public class IdentityClient { private final Logger log; - private final FrogarthaGame game; - private String username; - private boolean isProcessing, isAuthorized, inRoom; + private final Preferences sessionPreferences; + private final String clientToken; + private String accessToken; + private String userId, username, password; + private boolean isProcessing, isAuthorized; - public IdentityClient() { + public IdentityClient(Preferences sessionPreferences) { + startValidationThread(); this.log = new Logger(IdentityClient.class); - this.game = FrogarthaGame.getInstance(); - - this.username = null; + this.clientToken = RandomUtils.generateRandomString(); + this.sessionPreferences = sessionPreferences; this.isProcessing = false; this.isAuthorized = false; - this.inRoom = false; + + if (sessionPreferences.contains("username") && sessionPreferences.contains("password")) { + this.authorize(sessionPreferences.getString("username"), sessionPreferences.getString("password")); + } + } + + public void authorize(String username, String password) { + log.log("Authorizing..."); + this.isProcessing = true; + sessionPreferences.putString("username", username); + sessionPreferences.putString("password", password); + sessionPreferences.flush(); + + JsonValue agent = new JsonValue(JsonValue.ValueType.object); + agent.addChild("name", new JsonValue(String.valueOf(FrogarthaConstants.Game.APP_ID.charAt(0)).toUpperCase() + FrogarthaConstants.Game.APP_ID.substring(1))); + agent.addChild("version", new JsonValue(FrogarthaConstants.Game.APP_PROTOCOL)); + + JsonValue json = new JsonValue(JsonValue.ValueType.object); + json.addChild("agent", agent); + json.addChild("username", new JsonValue(username)); + json.addChild("password", new JsonValue(password)); + json.addChild("clientToken", new JsonValue(clientToken)); + + Net.HttpRequest request = + new HttpRequestBuilder() + .newRequest() + .method(Net.HttpMethods.POST) + .url(FrogarthaConstants.URLS.IDENTITY_AUTHENTICATION_URL) + .timeout(20000) + .header("Content-Type", "application/json") + .content(json.toJson(JsonWriter.OutputType.json)) + .build(); + + Gdx.net.sendHttpRequest(request, new Net.HttpResponseListener() { + @Override + public void handleHttpResponse(Net.HttpResponse httpResponse) { + IdentityClient.this.isProcessing = false; + + try { + JsonValue json = new JsonReader().parse(httpResponse.getResultAsString()); + + if (httpResponse.getStatus().getStatusCode() != HttpStatus.SC_OK) { + String type = json.get("error").getString("type"); + String error = json.get("error").getString("message"); + log.log("Failed to authorize: {} ({})", error, type); + + sessionPreferences.remove("username"); + sessionPreferences.remove("password"); + sessionPreferences.flush(); + + return; + } + + IdentityClient.this.username = username; + IdentityClient.this.password = password; + IdentityClient.this.accessToken = json.get("data").getString("accessToken"); + IdentityClient.this.userId = String.valueOf(json.get("data").get("user").getInt("id")); + IdentityClient.this.isAuthorized = true; + log.log("Successfully authorized! Welcome back, {}!", IdentityClient.this.username); + + FrogarthaGame.getInstance().getSessionClient().connect(); + } catch (Exception e) { + log.error("An exception was thrown while authorizing", e); + } + } + + @Override + public void failed(Throwable t) { + log.error("Failed to send an authorization request", t); + } + + @Override + public void cancelled() { + log.log("Authorization request was cancelled!"); + } + }); + } + + public void validateToken() { + if (clientToken == null || accessToken == null) { + return; + } + + log.log("Validating token..."); + + JsonValue json = new JsonValue(JsonValue.ValueType.object); + json.addChild("clientToken", new JsonValue(clientToken)); + json.addChild("accessToken", new JsonValue(accessToken)); + + Net.HttpRequest request = + new HttpRequestBuilder() + .newRequest() + .method(Net.HttpMethods.POST) + .url(FrogarthaConstants.URLS.IDENTITY_VALIDATE_URL) + .timeout(20000) + .header("Content-Type", "application/json") + .content(json.toJson(JsonWriter.OutputType.json)) + .build(); + + Gdx.net.sendHttpRequest(request, new Net.HttpResponseListener() { + @Override + public void handleHttpResponse(Net.HttpResponse httpResponse) { + try { + JsonValue json = new JsonReader().parse(httpResponse.getResultAsString()); + + if (httpResponse.getStatus().getStatusCode() != HttpStatus.SC_OK) { + String type = json.get("error").getString("type"); + String error = json.get("error").getString("message"); + log.log("Failed to validate: {} ({})", error, type); + accessToken = null; + userId = null; + isAuthorized = false; + FrogarthaGame.getInstance().getSessionClient().close(); + authorize(username, password); + return; + } + + int expiresInSeconds = json.get("data").getInt("expiresInSeconds"); + + if (expiresInSeconds < 1000) { + refreshToken(); + } + + log.log("Token validated!"); + } catch (Exception e) { + log.error("An exception was thrown while validating", e); + } + } + + @Override + public void failed(Throwable t) { + log.error("Failed to send a validation request", t); + } + + @Override + public void cancelled() { + log.log("Validation request was cancelled!"); + } + }); + } + + public void invalidateToken() { + if (clientToken == null || accessToken == null) { + return; + } + + log.log("Invalidating token..."); + + JsonValue json = new JsonValue(JsonValue.ValueType.object); + json.addChild("clientToken", new JsonValue(clientToken)); + json.addChild("accessToken", new JsonValue(accessToken)); + + Net.HttpRequest request = + new HttpRequestBuilder() + .newRequest() + .method(Net.HttpMethods.POST) + .url(FrogarthaConstants.URLS.IDENTITY_INVALIDATE_URL) + .timeout(20000) + .header("Content-Type", "application/json") + .content(json.toJson(JsonWriter.OutputType.json)) + .build(); + + Gdx.net.sendHttpRequest(request, new Net.HttpResponseListener() { + @Override + public void handleHttpResponse(Net.HttpResponse httpResponse) { + try { + JsonValue json = new JsonReader().parse(httpResponse.getResultAsString()); + + if (httpResponse.getStatus().getStatusCode() != HttpStatus.SC_OK) { + String type = json.get("error").getString("type"); + String error = json.get("error").getString("message"); + log.log("Failed to invalidate: {} ({})", error, type); + return; + } + + log.log("Invalidated! Bye, {}", username); + + accessToken = null; + userId = null; + username = null; + password = null; + isAuthorized = false; + sessionPreferences.remove("username"); + sessionPreferences.remove("password"); + sessionPreferences.flush(); + + FrogarthaGame.getInstance().getSessionClient().close(); + } catch (Exception ignored) { + } + } + + @Override + public void failed(Throwable t) { + } + + @Override + public void cancelled() { + } + }); } + public void refreshToken() { + if (clientToken == null || accessToken == null) { + return; + } + + log.log("Refreshing token..."); + + JsonValue json = new JsonValue(JsonValue.ValueType.object); + json.addChild("clientToken", new JsonValue(clientToken)); + json.addChild("accessToken", new JsonValue(accessToken)); + + Net.HttpRequest request = + new HttpRequestBuilder() + .newRequest() + .method(Net.HttpMethods.POST) + .url(FrogarthaConstants.URLS.IDENTITY_REFRESH_URL) + .timeout(20000) + .header("Content-Type", "application/json") + .content(json.toJson(JsonWriter.OutputType.json)) + .build(); + + Gdx.net.sendHttpRequest(request, new Net.HttpResponseListener() { + @Override + public void handleHttpResponse(Net.HttpResponse httpResponse) { + try { + JsonValue json = new JsonReader().parse(httpResponse.getResultAsString()); - public void authorize(String username) { - this.username = username; - game.getSessionClient().connect(); + if (httpResponse.getStatus().getStatusCode() != HttpStatus.SC_OK) { + String type = json.get("error").getString("type"); + String error = json.get("error").getString("message"); + log.log("Failed to refresh: {} ({})", error, type); + accessToken = null; + userId = null; + isAuthorized = false; + log.log(error); + FrogarthaGame.getInstance().getSessionClient().close(); + return; + } + + accessToken = json.get("data").get("accessToken").asString(); + log.log("Token has been refreshed!"); + + FrogarthaGame.getInstance().getSessionClient().updateIdentity(); + } catch (Exception e) { + log.error("An exception was thrown while refreshing", e); + } + } + + @Override + public void failed(Throwable t) { + log.error("Failed to send a refresh request", t); + } + + @Override + public void cancelled() { + log.log("Refresh request was cancelled!"); + } + }); + } + + public boolean isAuthorized() { + return this.isAuthorized; + } + + public boolean isProcessing() { + return isProcessing; } public String getUsername() { return username; } - public boolean isAuthorized() { - return isAuthorized; + public String getPassword() { + return password; } - public void setAuthorized(boolean authorized) { - isAuthorized = authorized; + public String getAccessToken() { + return accessToken; } - public boolean isInRoom() { - return inRoom; + public String getClientToken() { + return clientToken; } - public void setInRoom(boolean inRoom) { - this.inRoom = inRoom; + public String getUserId() { + return userId; } - public boolean isProcessing() { - return isProcessing; + private void startValidationThread() { + Timer.schedule(new Timer.Task() { + @Override + public void run() { + validateToken(); + } + }, 60000, 60000); } } diff --git a/core/src/main/java/kz/ilotterytea/frogartha/sessions/SessionClient.java b/core/src/main/java/kz/ilotterytea/frogartha/sessions/SessionClient.java index ba05f37..a696121 100644 --- a/core/src/main/java/kz/ilotterytea/frogartha/sessions/SessionClient.java +++ b/core/src/main/java/kz/ilotterytea/frogartha/sessions/SessionClient.java @@ -56,7 +56,7 @@ public class SessionClient implements WebSocketListener { @Override public boolean onClose(WebSocket webSocket, WebSocketCloseCode code, String reason) { log.log("Connection closed! Reason: {} {}", code.getCode(), reason); - game.getIdentityClient().setAuthorized(false); + setLastThrow(null); return true; } @@ -168,7 +168,9 @@ public class SessionClient implements WebSocketListener { private void setLastThrow(Throwable throwable) { this.lastThrow = throwable; - game.getIdentityClient().setInRoom(false); + joined = false; + isJoining = false; + topic = null; playerDataMap.clear(); if (connection.isOpen()) { connection.close(); diff --git a/core/src/main/java/kz/ilotterytea/frogartha/sessions/SessionHandlers.java b/core/src/main/java/kz/ilotterytea/frogartha/sessions/SessionHandlers.java index 55f3bdb..9ced32b 100644 --- a/core/src/main/java/kz/ilotterytea/frogartha/sessions/SessionHandlers.java +++ b/core/src/main/java/kz/ilotterytea/frogartha/sessions/SessionHandlers.java @@ -20,7 +20,6 @@ public class SessionHandlers { log.log("The server identified me!"); client.setConnectionId(event.getPlayerId()); - game.getIdentityClient().setAuthorized(true); } public static void handlePlayerJumpEvent(PlayerJumpEvent event) { @@ -96,7 +95,6 @@ public class SessionHandlers { } client.getPlayerDataMap().putAll(map); - game.getIdentityClient().setInRoom(true); } public static void handleSenderLeftRoomEvent(SenderLeftRoomEvent event) { @@ -106,7 +104,6 @@ public class SessionHandlers { } client.getPlayerDataMap().clear(); - game.getIdentityClient().setInRoom(false); } public static void handlePlayerJoinedRoomEvent(PlayerJoinedRoomEvent event) { diff --git a/shared/src/main/java/kz/ilotterytea/frogartha/FrogarthaConstants.java b/shared/src/main/java/kz/ilotterytea/frogartha/FrogarthaConstants.java index e205bf5..1e55d52 100644 --- a/shared/src/main/java/kz/ilotterytea/frogartha/FrogarthaConstants.java +++ b/shared/src/main/java/kz/ilotterytea/frogartha/FrogarthaConstants.java @@ -1,8 +1,19 @@ package kz.ilotterytea.frogartha; public class FrogarthaConstants { + public static class Game { + public static final String APP_ID = "frogartha"; + public static final int APP_PROTOCOL = 1; + } + public static class URLS { public static final String SESSION_WSS = "ws://localhost:20015"; + public static final String IDENTITY_BASE_URL = "https://id.ilotterytea.kz"; + public static final String IDENTITY_INVALIDATE_URL = IDENTITY_BASE_URL + "/invalidate"; + public static final String IDENTITY_VALIDATE_URL = IDENTITY_BASE_URL + "/validate"; + public static final String IDENTITY_REFRESH_URL = IDENTITY_BASE_URL + "/refresh"; + public static final String IDENTITY_AUTHENTICATION_URL = IDENTITY_BASE_URL + "/authenticate"; + public static final String IDENTITY_REGISTRATION_URL = IDENTITY_BASE_URL; } public static class Player { diff --git a/shared/src/main/java/kz/ilotterytea/frogartha/utils/RandomUtils.java b/shared/src/main/java/kz/ilotterytea/frogartha/utils/RandomUtils.java new file mode 100644 index 0000000..2933aac --- /dev/null +++ b/shared/src/main/java/kz/ilotterytea/frogartha/utils/RandomUtils.java @@ -0,0 +1,28 @@ +package kz.ilotterytea.frogartha.utils; + +import java.util.Random; + +public class RandomUtils { + public static final char[] CHARACTER_POOL = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".toCharArray(); + public static final int TOKEN_LENGTH = 32; + + public static String generateRandomString() { + return generateRandomString(CHARACTER_POOL, TOKEN_LENGTH); + } + + public static String generateRandomString(int length) { + return generateRandomString(CHARACTER_POOL, length); + } + + public static String generateRandomString(char[] characterPool, int length) { + StringBuilder output = new StringBuilder(); + Random random = new Random(); + + for (int i = 0; i < length; i++) { + char character = characterPool[random.nextInt(0, characterPool.length)]; + output.append(character); + } + + return output.toString(); + } +} |
