From 92a5578e0606fe091b13a214609bd07e3006e7d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Kr=C3=BChlmann?= Date: Sun, 25 Jan 2026 11:06:26 +0100 Subject: [PATCH] Add keepalive --- ...uth2AuthorizedClientPersistenceConfig.java | 21 +++++++ .../controller/SessionApiController.java | 55 +++++++++++++++++++ .../resources/static/js/session-refresh.js | 38 +++++++++++++ src/main/resources/templates/admin.html | 1 + src/main/resources/templates/audit-log.html | 1 + src/main/resources/templates/dashboard.html | 1 + src/main/resources/templates/settings.html | 1 + 7 files changed, 118 insertions(+) create mode 100644 src/main/java/dev/kruhlmann/imgfloat/controller/SessionApiController.java create mode 100644 src/main/resources/static/js/session-refresh.js diff --git a/src/main/java/dev/kruhlmann/imgfloat/config/OAuth2AuthorizedClientPersistenceConfig.java b/src/main/java/dev/kruhlmann/imgfloat/config/OAuth2AuthorizedClientPersistenceConfig.java index 296a52c..029b45d 100644 --- a/src/main/java/dev/kruhlmann/imgfloat/config/OAuth2AuthorizedClientPersistenceConfig.java +++ b/src/main/java/dev/kruhlmann/imgfloat/config/OAuth2AuthorizedClientPersistenceConfig.java @@ -4,8 +4,12 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; import org.springframework.jdbc.core.JdbcOperations; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProvider; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProviderBuilder; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager; import org.springframework.security.oauth2.client.web.AuthenticatedPrincipalOAuth2AuthorizedClientRepository; import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; @@ -29,4 +33,21 @@ public class OAuth2AuthorizedClientPersistenceConfig { OAuth2AuthorizedClientRepository authorizedClientRepository(OAuth2AuthorizedClientService authorizedClientService) { return new AuthenticatedPrincipalOAuth2AuthorizedClientRepository(authorizedClientService); } + + @Bean + OAuth2AuthorizedClientManager authorizedClientManager( + ClientRegistrationRepository clientRegistrationRepository, + OAuth2AuthorizedClientRepository authorizedClientRepository + ) { + OAuth2AuthorizedClientProvider authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder() + .authorizationCode() + .refreshToken() + .build(); + DefaultOAuth2AuthorizedClientManager manager = new DefaultOAuth2AuthorizedClientManager( + clientRegistrationRepository, + authorizedClientRepository + ); + manager.setAuthorizedClientProvider(authorizedClientProvider); + return manager; + } } diff --git a/src/main/java/dev/kruhlmann/imgfloat/controller/SessionApiController.java b/src/main/java/dev/kruhlmann/imgfloat/controller/SessionApiController.java new file mode 100644 index 0000000..d2c7681 --- /dev/null +++ b/src/main/java/dev/kruhlmann/imgfloat/controller/SessionApiController.java @@ -0,0 +1,55 @@ +package dev.kruhlmann.imgfloat.controller; + +import dev.kruhlmann.imgfloat.util.LogSanitizer; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.CacheControl; +import org.springframework.http.ResponseEntity; +import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/session") +@SecurityRequirement(name = "twitchOAuth") +public class SessionApiController { + + private static final Logger LOG = LoggerFactory.getLogger(SessionApiController.class); + private final OAuth2AuthorizedClientManager authorizedClientManager; + + public SessionApiController(OAuth2AuthorizedClientManager authorizedClientManager) { + this.authorizedClientManager = authorizedClientManager; + } + + @GetMapping("/refresh") + public ResponseEntity refreshSession( + OAuth2AuthenticationToken oauthToken, + HttpServletRequest request, + HttpServletResponse response + ) { + if (oauthToken == null) { + return ResponseEntity.ok().cacheControl(CacheControl.noStore()).build(); + } + OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest + .withClientRegistrationId(oauthToken.getAuthorizedClientRegistrationId()) + .principal(oauthToken) + .attribute(HttpServletRequest.class.getName(), request) + .attribute(HttpServletResponse.class.getName(), response) + .build(); + OAuth2AuthorizedClient authorizedClient = authorizedClientManager.authorize(authorizeRequest); + if (authorizedClient == null) { + LOG.warn( + "Failed to refresh session for {}", + LogSanitizer.sanitize(oauthToken.getName()) + ); + } + return ResponseEntity.ok().cacheControl(CacheControl.noStore()).build(); + } +} diff --git a/src/main/resources/static/js/session-refresh.js b/src/main/resources/static/js/session-refresh.js new file mode 100644 index 0000000..6162dfb --- /dev/null +++ b/src/main/resources/static/js/session-refresh.js @@ -0,0 +1,38 @@ +(() => { + const refreshDelayMs = 4 * 60 * 1000; + let refreshInterval; + + const refresh = () => { + fetch("/api/session/refresh", { cache: "no-store" }).catch(() => null); + }; + + const start = () => { + if (refreshInterval) { + return; + } + refresh(); + refreshInterval = globalThis.setInterval(refresh, refreshDelayMs); + }; + + const stop = () => { + if (refreshInterval) { + clearInterval(refreshInterval); + refreshInterval = null; + } + }; + + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", start, { once: true }); + } else { + start(); + } + + globalThis.addEventListener("beforeunload", stop); + document.addEventListener("visibilitychange", () => { + if (document.hidden) { + stop(); + } else { + start(); + } + }); +})(); diff --git a/src/main/resources/templates/admin.html b/src/main/resources/templates/admin.html index 14d8375..8f17ecf 100644 --- a/src/main/resources/templates/admin.html +++ b/src/main/resources/templates/admin.html @@ -525,6 +525,7 @@ const SETTINGS = JSON.parse(/*[[${settingsJson}]]*/); const ADMIN_CHANNELS = /*[[${adminChannels}]]*/ []; + diff --git a/src/main/resources/templates/audit-log.html b/src/main/resources/templates/audit-log.html index 518dcc8..b6baedd 100644 --- a/src/main/resources/templates/audit-log.html +++ b/src/main/resources/templates/audit-log.html @@ -81,6 +81,7 @@ + diff --git a/src/main/resources/templates/dashboard.html b/src/main/resources/templates/dashboard.html index faefdc1..174ff90 100644 --- a/src/main/resources/templates/dashboard.html +++ b/src/main/resources/templates/dashboard.html @@ -178,6 +178,7 @@ + diff --git a/src/main/resources/templates/settings.html b/src/main/resources/templates/settings.html index 7221a76..0c75ff3 100644 --- a/src/main/resources/templates/settings.html +++ b/src/main/resources/templates/settings.html @@ -253,6 +253,7 @@ const serverRenderedInitialSysadmin = /*[[${initialSysadmin}]]*/; +