Skip to content

Commit 7cb82db

Browse files
committed
Tntroduce the Lag realtime game
1 parent 4b9378c commit 7cb82db

33 files changed

+501
-130
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
11
package eu.solven.kumite.game;
22

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

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

811
// https://en.wikipedia.org/wiki/Perfect_information
912
// https://www.reddit.com/r/boardgames/comments/bdi78u/what_are_some_simple_games_with_no_hidden/
1013
String TAG_PERFECT_INFORMATION = "perfect_information";
14+
15+
// A turn-based game expects a move from a single player for any state
16+
String TAG_TURNBASED = "turned-based";
17+
18+
// A real-time game allows all players to move concurrently.
19+
String TAG_REALTIME = "real-time";
1120
}

server/src/main/java/eu/solven/kumite/account/login/FakePlayerTokens.java

+5-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public static void main(String[] args) {
4747

4848
@Bean
4949
public Void generateFakePlayerToken(KumiteTokenService tokenService) {
50-
String accessToken = tokenService.generateAccessToken(fakeUser());
50+
String accessToken = tokenService.generateAccessToken(fakeUser(), KumitePlayer.FAKE_PLAYER_ID);
5151

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

@@ -69,4 +69,8 @@ public static KumiteUser fakeUser() {
6969
.build();
7070
}
7171

72+
public static KumitePlayer fakePlayer() {
73+
return KumitePlayer.builder().playerId(KumitePlayer.FAKE_PLAYER_ID).build();
74+
}
75+
7276
}

server/src/main/java/eu/solven/kumite/account/login/KumiteOAuth2UserService.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
public class KumiteOAuth2UserService extends DefaultReactiveOAuth2UserService {
3535

3636
// private final AccountsStore accountsStore;
37-
private final KumiteUsersRegistry usersRegistry;
37+
final KumiteUsersRegistry usersRegistry;
3838

3939
@Override
4040
@SneakyThrows(OAuth2AuthenticationException.class)

server/src/main/java/eu/solven/kumite/account/login/KumiteSecurity.java

+10-1
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,20 @@
33
import org.springframework.context.annotation.Bean;
44
import org.springframework.context.annotation.Import;
55
import org.springframework.web.bind.annotation.RestController;
6+
import org.springframework.web.server.WebExceptionHandler;
67
import org.springframework.web.server.WebFilter;
78

89
import eu.solven.kumite.app.controllers.KumiteLoginController;
910
import eu.solven.kumite.app.controllers.KumitePublicController;
1011
import eu.solven.kumite.app.controllers.MetadataController;
1112
import eu.solven.kumite.app.webflux.KumiteExceptionRoutingWebFilter;
13+
import eu.solven.kumite.app.webflux.KumiteWebExceptionHandler;
1214

1315
// https://docs.spring.io/spring-security/reference/reactive/oauth2/login/advanced.html#webflux-oauth2-login-advanced-userinfo-endpoint
1416
@RestController
15-
@Import({ SocialWebFluxSecurity.class,
17+
@Import({
18+
19+
SocialWebFluxSecurity.class,
1620

1721
KumitePublicController.class,
1822
KumiteLoginController.class,
@@ -27,4 +31,9 @@ public class KumiteSecurity {
2731
WebFilter kumiteExceptionRoutingWebFilter() {
2832
return new KumiteExceptionRoutingWebFilter();
2933
}
34+
35+
@Bean
36+
WebExceptionHandler kumiteWebExceptionHandler() {
37+
return new KumiteWebExceptionHandler();
38+
}
3039
}

server/src/main/java/eu/solven/kumite/account/login/KumiteTokenService.java

+10-8
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import java.time.Instant;
66
import java.util.Date;
77
import java.util.Map;
8+
import java.util.UUID;
89
import java.util.function.Supplier;
910

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

3637
final Environment env;
37-
final IUuidGenerator uuidgenerator;
38+
final IUuidGenerator uuidGenerator;
3839
final Supplier<OctetSequenceKey> supplierSymetricKey;
3940

4041
public KumiteTokenService(Environment env, IUuidGenerator uuidgenerator) {
4142
this.env = env;
42-
this.uuidgenerator = uuidgenerator;
43+
this.uuidGenerator = uuidgenerator;
4344
this.supplierSymetricKey = () -> loadSigningJwk();
4445
}
4546

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

51-
public Map<String, ?> wrapInJwtToken(KumiteUser user) {
52-
String accessToken = generateAccessToken(user);
53-
return Map.of("access_token", accessToken);
52+
public Map<String, ?> wrapInJwtToken(KumiteUser user, UUID playerId) {
53+
String accessToken = generateAccessToken(user, playerId);
54+
return Map.of("access_token", accessToken, "player_id", playerId);
5455
}
5556

5657
public static void main(String[] args) {
@@ -82,13 +83,14 @@ static JWK generateSignatureSecret(IUuidGenerator uuidgenerator) {
8283
*
8384
* @param user
8485
* The user for whom to generate an access token.
86+
* @param playerId
8587
* @throws IllegalArgumentException
8688
* if provided argument is <code>null</code>.
8789
* @return The generated JWT access token.
8890
* @throws IllegalStateException
8991
*/
9092
@SneakyThrows({ JOSEException.class })
91-
public String generateAccessToken(KumiteUser user) {
93+
public String generateAccessToken(KumiteUser user, UUID playerId) {
9294
Duration accessTokenValidity = Duration.parse(env.getProperty(KEY_ACCESSTOKEN_EXP, "PT1H"));
9395

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

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

server/src/main/java/eu/solven/kumite/account/login/KumiteUsersRegistry.java

+11-7
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,24 @@
88
import eu.solven.kumite.account.KumiteUser.KumiteUserBuilder;
99
import eu.solven.kumite.account.KumiteUserRaw;
1010
import eu.solven.kumite.account.KumiteUserRawRaw;
11+
import eu.solven.kumite.player.AccountPlayersRegistry;
1112
import eu.solven.kumite.player.KumitePlayer;
1213
import eu.solven.kumite.tools.IUuidGenerator;
13-
import lombok.Value;
14+
import lombok.RequiredArgsConstructor;
1415

15-
@Value
16+
@RequiredArgsConstructor
1617
public class KumiteUsersRegistry {
17-
IUuidGenerator uuidGenerator;
18+
final IUuidGenerator uuidGenerator;
19+
20+
final AccountPlayersRegistry playersRegistry;
1821

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

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

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

6467
UUID playerId = uuidGenerator.randomUUID();
6568
kumiteUserBuilder.playerId(playerId);
69+
70+
playersRegistry.registerPlayer(accountId, KumitePlayer.builder().playerId(playerId).build());
71+
accountIdToUser.putIfAbsent(accountId, rawRaw);
6672
} else {
6773
kumiteUserBuilder.accountId(alreadyIn.getAccountId()).playerId(alreadyIn.getPlayerId());
6874
}
6975

7076
return kumiteUserBuilder.build();
7177
});
72-
73-
accountIdToUser.putIfAbsent(kumiteUser.getAccountId(), rawRaw);
7478
}
7579

7680
return kumiteUser;

server/src/main/java/eu/solven/kumite/app/controllers/KumiteHandlerHelper.java

+4
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ public static Optional<UUID> optUuid(ServerRequest request, String idKey) {
4040
return optUuid.map(rawUuid -> uuid(rawUuid));
4141
}
4242

43+
public static Optional<UUID> optUuid(Optional<String> optRaw) {
44+
return optRaw.map(raw -> uuid(raw));
45+
}
46+
4347
public static Optional<Boolean> optBoolean(ServerRequest request, String idKey) {
4448
Optional<String> optBoolean = request.queryParam(idKey);
4549

server/src/main/java/eu/solven/kumite/app/controllers/KumiteLoginController.java

+21-8
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package eu.solven.kumite.app.controllers;
22

33
import java.util.Map;
4+
import java.util.Optional;
45
import java.util.TreeMap;
6+
import java.util.UUID;
57
import java.util.stream.StreamSupport;
68

79
import org.springframework.core.env.Environment;
@@ -12,6 +14,7 @@
1214
import org.springframework.security.oauth2.core.user.OAuth2User;
1315
import org.springframework.web.bind.annotation.GetMapping;
1416
import org.springframework.web.bind.annotation.RequestMapping;
17+
import org.springframework.web.bind.annotation.RequestParam;
1518
import org.springframework.web.bind.annotation.RestController;
1619

1720
import eu.solven.kumite.account.KumiteUser;
@@ -21,6 +24,7 @@
2124
import eu.solven.kumite.account.login.KumiteUsersRegistry;
2225
import eu.solven.kumite.app.IKumiteSpringProfiles;
2326
import eu.solven.kumite.app.webflux.LoginRouteButNotAuthenticatedException;
27+
import eu.solven.kumite.player.AccountPlayersRegistry;
2428
import lombok.AllArgsConstructor;
2529
import reactor.core.publisher.Mono;
2630

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

3337
final KumiteUsersRegistry usersRegistry;
38+
final AccountPlayersRegistry playersRegistry;
3439
final Environment env;
3540

3641
final KumiteTokenService kumiteTokenService;
3742

38-
// Redirect to the UI route showing the User how to login
39-
// @GetMapping
40-
// public ResponseEntity<?> login() {
41-
// return ResponseEntity.status(HttpStatus.MOVED_PERMANENTLY).header(HttpHeaders.LOCATION, "/login").build();
42-
// }
43-
4443
@GetMapping("/providers")
4544
public Map<String, ?> loginProviders() {
4645
Map<String, Object> registrationIdToDetails = new TreeMap<>();
@@ -89,8 +88,22 @@ private String guessProviderId(OAuth2User o) {
8988
}
9089

9190
@GetMapping("/token")
92-
public Mono<Map<String, ?>> token(@AuthenticationPrincipal Mono<OAuth2User> oauth2User) {
93-
return user(oauth2User).map(user -> kumiteTokenService.wrapInJwtToken(user));
91+
public Mono<Map<String, ?>> token(@AuthenticationPrincipal Mono<OAuth2User> oauth2User,
92+
@RequestParam(name = "player_id", required = false) String rawPlayerId) {
93+
return user(oauth2User).map(user -> {
94+
UUID playerId = KumiteHandlerHelper.optUuid(Optional.ofNullable(rawPlayerId)).orElse(user.getPlayerId());
95+
96+
checkValidPlayerId(user, playerId);
97+
98+
return kumiteTokenService.wrapInJwtToken(user, playerId);
99+
});
100+
}
101+
102+
void checkValidPlayerId(KumiteUser user, UUID playerId) {
103+
UUID accountId = user.getAccountId();
104+
if (!playersRegistry.makeDynamicHasPlayers(accountId).hasPlayerId(playerId)) {
105+
throw new IllegalArgumentException("player_id=" + playerId + " is not managed by accountId=" + accountId);
106+
}
94107
}
95108

96109
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package eu.solven.kumite.app.webflux;
2+
3+
import java.nio.charset.StandardCharsets;
4+
import java.util.LinkedHashMap;
5+
import java.util.Map;
6+
7+
import org.springframework.core.annotation.Order;
8+
import org.springframework.core.io.buffer.DataBuffer;
9+
import org.springframework.http.HttpStatus;
10+
import org.springframework.stereotype.Component;
11+
import org.springframework.web.reactive.resource.NoResourceFoundException;
12+
import org.springframework.web.server.ServerWebExchange;
13+
import org.springframework.web.server.WebExceptionHandler;
14+
15+
import com.fasterxml.jackson.core.JsonProcessingException;
16+
import com.fasterxml.jackson.databind.ObjectMapper;
17+
18+
import lombok.extern.slf4j.Slf4j;
19+
import reactor.core.publisher.Flux;
20+
import reactor.core.publisher.Mono;
21+
22+
@Component
23+
// '-2' to have higher priority than the default WebExceptionHandler
24+
@Order(-2)
25+
@Slf4j
26+
public class KumiteWebExceptionHandler implements WebExceptionHandler {
27+
28+
@Override
29+
public Mono<Void> handle(ServerWebExchange exchange, Throwable e) {
30+
if (e instanceof NoResourceFoundException) {
31+
// Let the default WebExceptionHandler manage 404
32+
return Mono.error(e);
33+
} else if (e instanceof IllegalArgumentException) {
34+
exchange.getResponse().setStatusCode(HttpStatus.BAD_REQUEST);
35+
} else if (e instanceof LoginRouteButNotAuthenticatedException) {
36+
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
37+
} else {
38+
exchange.getResponse().setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
39+
}
40+
41+
Map<String, Object> responseBody = new LinkedHashMap<>();
42+
43+
if (e.getMessage() == null) {
44+
responseBody.put("error_message", "");
45+
} else {
46+
responseBody.put("error_message", e.getMessage());
47+
}
48+
49+
String respondyBodyAsString;
50+
try {
51+
respondyBodyAsString = new ObjectMapper().writeValueAsString(responseBody);
52+
} catch (JsonProcessingException ee) {
53+
log.error("Issue producing responseBody given {}", responseBody, ee);
54+
respondyBodyAsString = "{\"error_message\":\"something_went_very_wrong\"}";
55+
}
56+
57+
byte[] bytes = respondyBodyAsString.getBytes(StandardCharsets.UTF_8);
58+
DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes);
59+
return exchange.getResponse().writeWith(Flux.just(buffer));
60+
}
61+
62+
}

server/src/main/java/eu/solven/kumite/board/BoardHandler.java

+2
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ public Mono<ServerResponse> getBoard(ServerRequest request) {
5050
ContestSearchParametersBuilder parameters = ContestSearchParameters.builder();
5151
List<Contest> contest = contestsRegistry.searchContests(parameters.contestId(Optional.of(contestId)).build());
5252
if (contest.isEmpty()) {
53+
// https://stackoverflow.com/questions/5604816/whats-the-most-appropriate-http-status-code-for-an-item-not-found-error-page
54+
// We may want a specific exception + httpStatusCode
5355
throw new IllegalArgumentException("No contest for contestId=" + contestId);
5456
} else if (contest.size() >= 2) {
5557
throw new IllegalStateException("Multiple contests for contestId=" + contestId + " contests=" + contest);

server/src/main/java/eu/solven/kumite/game/opposition/tictactoe/TicTacToe.java

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public class TicTacToe implements IGame {
2626
.title("Tic-Tac-Toe")
2727
.tag(IGameMetadataConstants.TAG_1V1)
2828
.tag(IGameMetadataConstants.TAG_PERFECT_INFORMATION)
29+
.tag(IGameMetadataConstants.TAG_TURNBASED)
2930
.minPlayers(2)
3031
.maxPlayers(2)
3132
.shortDescription(

0 commit comments

Comments
 (0)