From 0eb96ddf445884865bdc41d78749849ef78e83d0 Mon Sep 17 00:00:00 2001 From: Janis Date: Wed, 17 Jun 2026 09:09:04 +0200 Subject: [PATCH 1/2] fix(official-bots): configure JWT verification The official-bots service enabled smallrye-jwt but never set mp.jwt.verify.publickey.location or issuer, so it could not validate any incoming token and rejected every authenticated request with 401. Add the verify public key (issuer nowchess) mirroring tournament/core, and ship keys/public.pem from the shared keypair. Co-Authored-By: Claude Opus 4.8 --- modules/official-bots/src/main/resources/application.yml | 6 ++++++ modules/official-bots/src/main/resources/keys/public.pem | 9 +++++++++ 2 files changed, 15 insertions(+) create mode 100644 modules/official-bots/src/main/resources/keys/public.pem diff --git a/modules/official-bots/src/main/resources/application.yml b/modules/official-bots/src/main/resources/application.yml index cf77b52..1d642d2 100644 --- a/modules/official-bots/src/main/resources/application.yml +++ b/modules/official-bots/src/main/resources/application.yml @@ -12,6 +12,12 @@ quarkus: enabled: true log: level: INFO + mp: + jwt: + verify: + publickey: + location: ${JWT_PUBLIC_KEY_PATH:keys/public.pem} + issuer: nowchess nowchess: redis: diff --git a/modules/official-bots/src/main/resources/keys/public.pem b/modules/official-bots/src/main/resources/keys/public.pem new file mode 100644 index 0000000..6b6b842 --- /dev/null +++ b/modules/official-bots/src/main/resources/keys/public.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxDsnsCAl0vQx7Vu9CLDZ +g0SG05NgUzu9T+3DTEaHGq60T2uriO8BenwyvsF3BnDqTbKf4voohZ1DNfzdbT1J +Fj8B62FrDmxcO+sp1/b5HUCJP6y2uSRCmzOHe5k7Pk1IEi72FgBpKXSRkFibRlVf +634g7mgsPZAQ9PJEsv4Qvm05T9L6+Gmq6N3bMVLKRXs4RhDhaFbYH9GtUg1eI0yH +YjGyRfqzW/nqVMstOLHt8CuPouq4p7eMzeDH3YHkxPm4GG5foCXMOd2DZrW0SCcr +7dhFeNVWzQ2m53eOhBzNQX+v3pgjVStsePhBRt2LyGfwkNzmqDgqWsMzSHRMY+cn +WQIDAQAB +-----END PUBLIC KEY----- -- 2.52.0 From 2548bf6f94080df7b8bcbc274eadbe7525b9cb6c Mon Sep 17 00:00:00 2001 From: Janis Date: Wed, 17 Jun 2026 10:18:31 +0200 Subject: [PATCH 2/2] feat(official-bots): park expert bot on tournament server at startup Park the expert bot on the configured tournament server (default http://141.37.123.132:8086) on startup, reusing a fixed TOURNAMENT_BOT_TOKEN when present instead of minting a new identity. Co-Authored-By: Claude Opus 4.8 --- .../bot/service/TournamentBotConfig.scala | 2 +- .../bot/service/TournamentBotGamePlayer.scala | 37 +++++++++++++++++-- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/modules/official-bots/src/main/scala/de/nowchess/bot/service/TournamentBotConfig.scala b/modules/official-bots/src/main/scala/de/nowchess/bot/service/TournamentBotConfig.scala index 002c436..213129f 100644 --- a/modules/official-bots/src/main/scala/de/nowchess/bot/service/TournamentBotConfig.scala +++ b/modules/official-bots/src/main/scala/de/nowchess/bot/service/TournamentBotConfig.scala @@ -20,7 +20,7 @@ object TournamentBotConfig: tournamentId <- env.get("TOURNAMENT_ID").filter(_.nonEmpty) token <- env.get("TOURNAMENT_BOT_TOKEN").filter(_.nonEmpty) botId <- jwtSubject(token) - serverUrl = env.getOrElse("TOURNAMENT_SERVER_URL", "http://localhost:8089") + serverUrl = env.getOrElse("TOURNAMENT_SERVER_URL", "http://141.37.123.132:8086") difficulty = env.getOrElse("TOURNAMENT_BOT_DIFFICULTY", "medium") yield TournamentBotConfig(serverUrl, tournamentId, token, botId, difficulty) diff --git a/modules/official-bots/src/main/scala/de/nowchess/bot/service/TournamentBotGamePlayer.scala b/modules/official-bots/src/main/scala/de/nowchess/bot/service/TournamentBotGamePlayer.scala index 117d74a..93d5e9d 100644 --- a/modules/official-bots/src/main/scala/de/nowchess/bot/service/TournamentBotGamePlayer.scala +++ b/modules/official-bots/src/main/scala/de/nowchess/bot/service/TournamentBotGamePlayer.scala @@ -39,10 +39,11 @@ class TournamentBotGamePlayer: // scalafix:on DisableSyntax.var val defaultServerUrl: String = - System.getenv().asScala.getOrElse("TOURNAMENT_SERVER_URL", "http://localhost:8089") + System.getenv().asScala.getOrElse("TOURNAMENT_SERVER_URL", "http://141.37.123.132:8086") @PostConstruct def initialize(): Unit = + parkOnStartup() config match case None => log.info("Tournament bot disabled — set TOURNAMENT_ID and TOURNAMENT_BOT_TOKEN to enable") @@ -50,6 +51,35 @@ class TournamentBotGamePlayer: log.infof("Tournament bot enabled — server=%s tournament=%s bot=%s", cfg.serverUrl, cfg.tournamentId, cfg.botId) startAsync(cfg) + private def parkOnStartup(): Unit = + park(defaultServerUrl, "expert") match + case Some(id) => log.infof("Parked expert bot on %s as id %s", defaultServerUrl, id) + case None => log.warnf("Failed to park expert bot on %s", defaultServerUrl) + + private def parkToken(serverUrl: String, difficulty: String): Option[String] = + System.getenv().asScala.get("TOURNAMENT_BOT_TOKEN").filter(_.nonEmpty) + .orElse(registerBot(serverUrl, difficulty).map(_._2)) + + private def park(serverUrl: String, difficulty: String): Option[String] = + parkToken(serverUrl, difficulty).flatMap { token => + Try { + val body = s"""{"name":"${botName(difficulty)}"}""" + val response = client + .target(serverUrl) + .path("api") + .path("bots") + .request(MediaType.APPLICATION_JSON) + .header("Authorization", s"Bearer $token") + .post(Entity.entity(body, MediaType.APPLICATION_JSON)) + if response.getStatus == 201 || response.getStatus == 200 then + val id = objectMapper.readTree(response.readEntity(classOf[String])).path("id").asText() + response.close() + log.infof("Parked official bot %s as id %s", botName(difficulty), id) + Option(id).filter(_.nonEmpty) + else { log.warnf("Parking bot %s returned status %d", botName(difficulty), response.getStatus); response.close(); None } + }.getOrElse(None) + } + def joinTournament(tournamentId: String, difficulty: String, serverUrl: String): Either[String, String] = registerBot(serverUrl, difficulty) match case None => Left("Failed to register bot with tournament server") @@ -65,10 +95,11 @@ class TournamentBotGamePlayer: thread.setDaemon(true) thread.start() + private def botName(difficulty: String): String = s"NowChess ${difficulty.capitalize}" + private def registerBot(serverUrl: String, difficulty: String): Option[(String, String)] = Try { - val name = s"NowChess ${difficulty.capitalize}" - val body = s"""{"name":"$name","isBot":true}""" + val body = s"""{"name":"${botName(difficulty)}","isBot":true}""" val response = client .target(serverUrl) .path("api") -- 2.52.0