Skip to content

Commit

Permalink
Tntroduce the Lag realtime game
Browse files Browse the repository at this point in the history
  • Loading branch information
blacelle committed Sep 13, 2024
1 parent 4b9378c commit 7cb82db
Show file tree
Hide file tree
Showing 33 changed files with 501 additions and 130 deletions.
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
package eu.solven.kumite.game;

public interface IGameMetadataConstants {
// An optimization game consists in proposing the best solution to a given problem. They can be played independently
// by any players.
String TAG_OPTIMIZATION = "optimization";

// Many games are `1v1` as the oppose 2 players on a given board.
String TAG_1V1 = "1v1";

// https://en.wikipedia.org/wiki/Perfect_information
// https://www.reddit.com/r/boardgames/comments/bdi78u/what_are_some_simple_games_with_no_hidden/
String TAG_PERFECT_INFORMATION = "perfect_information";

// A turn-based game expects a move from a single player for any state
String TAG_TURNBASED = "turned-based";

// A real-time game allows all players to move concurrently.
String TAG_REALTIME = "real-time";
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public static void main(String[] args) {

@Bean
public Void generateFakePlayerToken(KumiteTokenService tokenService) {
String accessToken = tokenService.generateAccessToken(fakeUser());
String accessToken = tokenService.generateAccessToken(fakeUser(), KumitePlayer.FAKE_PLAYER_ID);

log.info("access_token for fakeUser: {}", accessToken);

Expand All @@ -69,4 +69,8 @@ public static KumiteUser fakeUser() {
.build();
}

public static KumitePlayer fakePlayer() {
return KumitePlayer.builder().playerId(KumitePlayer.FAKE_PLAYER_ID).build();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
public class KumiteOAuth2UserService extends DefaultReactiveOAuth2UserService {

// private final AccountsStore accountsStore;
private final KumiteUsersRegistry usersRegistry;
final KumiteUsersRegistry usersRegistry;

@Override
@SneakyThrows(OAuth2AuthenticationException.class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,20 @@
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.WebExceptionHandler;
import org.springframework.web.server.WebFilter;

import eu.solven.kumite.app.controllers.KumiteLoginController;
import eu.solven.kumite.app.controllers.KumitePublicController;
import eu.solven.kumite.app.controllers.MetadataController;
import eu.solven.kumite.app.webflux.KumiteExceptionRoutingWebFilter;
import eu.solven.kumite.app.webflux.KumiteWebExceptionHandler;

// https://docs.spring.io/spring-security/reference/reactive/oauth2/login/advanced.html#webflux-oauth2-login-advanced-userinfo-endpoint
@RestController
@Import({ SocialWebFluxSecurity.class,
@Import({

SocialWebFluxSecurity.class,

KumitePublicController.class,
KumiteLoginController.class,
Expand All @@ -27,4 +31,9 @@ public class KumiteSecurity {
WebFilter kumiteExceptionRoutingWebFilter() {
return new KumiteExceptionRoutingWebFilter();
}

@Bean
WebExceptionHandler kumiteWebExceptionHandler() {
return new KumiteWebExceptionHandler();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import java.time.Instant;
import java.util.Date;
import java.util.Map;
import java.util.UUID;
import java.util.function.Supplier;

import org.springframework.core.env.Environment;
Expand Down Expand Up @@ -34,12 +35,12 @@ public class KumiteTokenService {
public static final String KEY_ACCESSTOKEN_EXP = "kumite.login.oauth2_exp";

final Environment env;
final IUuidGenerator uuidgenerator;
final IUuidGenerator uuidGenerator;
final Supplier<OctetSequenceKey> supplierSymetricKey;

public KumiteTokenService(Environment env, IUuidGenerator uuidgenerator) {
this.env = env;
this.uuidgenerator = uuidgenerator;
this.uuidGenerator = uuidgenerator;
this.supplierSymetricKey = () -> loadSigningJwk();
}

Expand All @@ -48,9 +49,9 @@ private OctetSequenceKey loadSigningJwk() {
return OctetSequenceKey.parse(env.getRequiredProperty(KEY_JWT_SIGNINGKEY));
}

public Map<String, ?> wrapInJwtToken(KumiteUser user) {
String accessToken = generateAccessToken(user);
return Map.of("access_token", accessToken);
public Map<String, ?> wrapInJwtToken(KumiteUser user, UUID playerId) {
String accessToken = generateAccessToken(user, playerId);
return Map.of("access_token", accessToken, "player_id", playerId);
}

public static void main(String[] args) {
Expand Down Expand Up @@ -82,13 +83,14 @@ static JWK generateSignatureSecret(IUuidGenerator uuidgenerator) {
*
* @param user
* The user for whom to generate an access token.
* @param playerId
* @throws IllegalArgumentException
* if provided argument is <code>null</code>.
* @return The generated JWT access token.
* @throws IllegalStateException
*/
@SneakyThrows({ JOSEException.class })
public String generateAccessToken(KumiteUser user) {
public String generateAccessToken(KumiteUser user, UUID playerId) {
Duration accessTokenValidity = Duration.parse(env.getProperty(KEY_ACCESSTOKEN_EXP, "PT1H"));

if (accessTokenValidity.compareTo(Duration.parse("PT1H")) > 0) {
Expand All @@ -109,11 +111,11 @@ public String generateAccessToken(KumiteUser user) {
JWTClaimsSet.Builder claimsSetBuilder = new JWTClaimsSet.Builder().subject(user.getAccountId().toString())
.audience("Kumite-Server")
.issuer("https://kumite.com")
.jwtID(uuidgenerator.randomUUID().toString())
.jwtID(uuidGenerator.randomUUID().toString())
.issueTime(curDate)
.notBeforeTime(Date.from(Instant.now()))
.expirationTime(Date.from(Instant.now().plusMillis(expirationMs)))
.claim("mainPlayerId", user.getPlayerId().toString());
.claim("playerId", playerId.toString());

SignedJWT signedJWT = new SignedJWT(headerBuilder.build(), claimsSetBuilder.build());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,24 @@
import eu.solven.kumite.account.KumiteUser.KumiteUserBuilder;
import eu.solven.kumite.account.KumiteUserRaw;
import eu.solven.kumite.account.KumiteUserRawRaw;
import eu.solven.kumite.player.AccountPlayersRegistry;
import eu.solven.kumite.player.KumitePlayer;
import eu.solven.kumite.tools.IUuidGenerator;
import lombok.Value;
import lombok.RequiredArgsConstructor;

@Value
@RequiredArgsConstructor
public class KumiteUsersRegistry {
IUuidGenerator uuidGenerator;
final IUuidGenerator uuidGenerator;

final AccountPlayersRegistry playersRegistry;

// This is a cache of the external information about a user
// This is useful to enrich some data about other players (e.g. a Leaderboard)
Map<KumiteUserRawRaw, KumiteUser> userIdToUser = new ConcurrentHashMap<>();
final Map<KumiteUserRawRaw, KumiteUser> userIdToUser = new ConcurrentHashMap<>();

// We may have multiple users for a single account
// This maps to the latest/main one
Map<UUID, KumiteUserRawRaw> accountIdToUser = new ConcurrentHashMap<>();
final Map<UUID, KumiteUserRawRaw> accountIdToUser = new ConcurrentHashMap<>();

public KumiteUser getUser(UUID accountId) {
KumiteUserRawRaw rawUser = accountIdToUser.get(accountId);
Expand Down Expand Up @@ -63,14 +66,15 @@ public KumiteUser registerOrUpdate(KumiteUserRaw kumiteUserRaw) {

UUID playerId = uuidGenerator.randomUUID();
kumiteUserBuilder.playerId(playerId);

playersRegistry.registerPlayer(accountId, KumitePlayer.builder().playerId(playerId).build());
accountIdToUser.putIfAbsent(accountId, rawRaw);
} else {
kumiteUserBuilder.accountId(alreadyIn.getAccountId()).playerId(alreadyIn.getPlayerId());
}

return kumiteUserBuilder.build();
});

accountIdToUser.putIfAbsent(kumiteUser.getAccountId(), rawRaw);
}

return kumiteUser;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ public static Optional<UUID> optUuid(ServerRequest request, String idKey) {
return optUuid.map(rawUuid -> uuid(rawUuid));
}

public static Optional<UUID> optUuid(Optional<String> optRaw) {
return optRaw.map(raw -> uuid(raw));
}

public static Optional<Boolean> optBoolean(ServerRequest request, String idKey) {
Optional<String> optBoolean = request.queryParam(idKey);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package eu.solven.kumite.app.controllers;

import java.util.Map;
import java.util.Optional;
import java.util.TreeMap;
import java.util.UUID;
import java.util.stream.StreamSupport;

import org.springframework.core.env.Environment;
Expand All @@ -12,6 +14,7 @@
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import eu.solven.kumite.account.KumiteUser;
Expand All @@ -21,6 +24,7 @@
import eu.solven.kumite.account.login.KumiteUsersRegistry;
import eu.solven.kumite.app.IKumiteSpringProfiles;
import eu.solven.kumite.app.webflux.LoginRouteButNotAuthenticatedException;
import eu.solven.kumite.player.AccountPlayersRegistry;
import lombok.AllArgsConstructor;
import reactor.core.publisher.Mono;

Expand All @@ -31,16 +35,11 @@ public class KumiteLoginController {
final InMemoryReactiveClientRegistrationRepository clientRegistrationRepository;

final KumiteUsersRegistry usersRegistry;
final AccountPlayersRegistry playersRegistry;
final Environment env;

final KumiteTokenService kumiteTokenService;

// Redirect to the UI route showing the User how to login
// @GetMapping
// public ResponseEntity<?> login() {
// return ResponseEntity.status(HttpStatus.MOVED_PERMANENTLY).header(HttpHeaders.LOCATION, "/login").build();
// }

@GetMapping("/providers")
public Map<String, ?> loginProviders() {
Map<String, Object> registrationIdToDetails = new TreeMap<>();
Expand Down Expand Up @@ -89,8 +88,22 @@ private String guessProviderId(OAuth2User o) {
}

@GetMapping("/token")
public Mono<Map<String, ?>> token(@AuthenticationPrincipal Mono<OAuth2User> oauth2User) {
return user(oauth2User).map(user -> kumiteTokenService.wrapInJwtToken(user));
public Mono<Map<String, ?>> token(@AuthenticationPrincipal Mono<OAuth2User> oauth2User,
@RequestParam(name = "player_id", required = false) String rawPlayerId) {
return user(oauth2User).map(user -> {
UUID playerId = KumiteHandlerHelper.optUuid(Optional.ofNullable(rawPlayerId)).orElse(user.getPlayerId());

checkValidPlayerId(user, playerId);

return kumiteTokenService.wrapInJwtToken(user, playerId);
});
}

void checkValidPlayerId(KumiteUser user, UUID playerId) {
UUID accountId = user.getAccountId();
if (!playersRegistry.makeDynamicHasPlayers(accountId).hasPlayerId(playerId)) {
throw new IllegalArgumentException("player_id=" + playerId + " is not managed by accountId=" + accountId);
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package eu.solven.kumite.app.webflux;

import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap;
import java.util.Map;

import org.springframework.core.annotation.Order;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.resource.NoResourceFoundException;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebExceptionHandler;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import lombok.extern.slf4j.Slf4j;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@Component
// '-2' to have higher priority than the default WebExceptionHandler
@Order(-2)
@Slf4j
public class KumiteWebExceptionHandler implements WebExceptionHandler {

@Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable e) {
if (e instanceof NoResourceFoundException) {
// Let the default WebExceptionHandler manage 404
return Mono.error(e);
} else if (e instanceof IllegalArgumentException) {
exchange.getResponse().setStatusCode(HttpStatus.BAD_REQUEST);
} else if (e instanceof LoginRouteButNotAuthenticatedException) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
} else {
exchange.getResponse().setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
}

Map<String, Object> responseBody = new LinkedHashMap<>();

if (e.getMessage() == null) {
responseBody.put("error_message", "");
} else {
responseBody.put("error_message", e.getMessage());
}

String respondyBodyAsString;
try {
respondyBodyAsString = new ObjectMapper().writeValueAsString(responseBody);
} catch (JsonProcessingException ee) {
log.error("Issue producing responseBody given {}", responseBody, ee);
respondyBodyAsString = "{\"error_message\":\"something_went_very_wrong\"}";
}

byte[] bytes = respondyBodyAsString.getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes);
return exchange.getResponse().writeWith(Flux.just(buffer));
}

}
2 changes: 2 additions & 0 deletions server/src/main/java/eu/solven/kumite/board/BoardHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ public Mono<ServerResponse> getBoard(ServerRequest request) {
ContestSearchParametersBuilder parameters = ContestSearchParameters.builder();
List<Contest> contest = contestsRegistry.searchContests(parameters.contestId(Optional.of(contestId)).build());
if (contest.isEmpty()) {
// https://stackoverflow.com/questions/5604816/whats-the-most-appropriate-http-status-code-for-an-item-not-found-error-page
// We may want a specific exception + httpStatusCode
throw new IllegalArgumentException("No contest for contestId=" + contestId);
} else if (contest.size() >= 2) {
throw new IllegalStateException("Multiple contests for contestId=" + contestId + " contests=" + contest);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public class TicTacToe implements IGame {
.title("Tic-Tac-Toe")
.tag(IGameMetadataConstants.TAG_1V1)
.tag(IGameMetadataConstants.TAG_PERFECT_INFORMATION)
.tag(IGameMetadataConstants.TAG_TURNBASED)
.minPlayers(2)
.maxPlayers(2)
.shortDescription(
Expand Down
Loading

0 comments on commit 7cb82db

Please sign in to comment.