Add keepalive

This commit is contained in:
2026-01-25 11:06:26 +01:00
parent c96918340a
commit 92a5578e06
7 changed files with 118 additions and 0 deletions

View File

@@ -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;
}
}

View File

@@ -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<Void> 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();
}
}

View File

@@ -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();
}
});
})();

View File

@@ -525,6 +525,7 @@
const SETTINGS = JSON.parse(/*[[${settingsJson}]]*/);
const ADMIN_CHANNELS = /*[[${adminChannels}]]*/ [];
</script>
<script src="/js/session-refresh.js"></script>
<script src="/js/cookie-consent.js"></script>
<script src="/js/toast.js"></script>
<script type="module" src="/js/admin.js"></script>

View File

@@ -81,6 +81,7 @@
<script th:inline="javascript">
const broadcaster = /*[[${broadcaster}]]*/ "";
</script>
<script src="/js/session-refresh.js"></script>
<script type="module" src="/js/audit-log.js"></script>
</body>
</html>

View File

@@ -178,6 +178,7 @@
</div>
</div>
<script src="/js/cookie-consent.js"></script>
<script src="/js/session-refresh.js"></script>
<script src="/js/csrf.js"></script>
<script src="/js/toast.js"></script>
<script src="/js/downloads.js"></script>

View File

@@ -253,6 +253,7 @@
const serverRenderedInitialSysadmin = /*[[${initialSysadmin}]]*/;
</script>
<script src="/js/cookie-consent.js"></script>
<script src="/js/session-refresh.js"></script>
<script src="/js/settings.js"></script>
<script src="/js/toast.js"></script>
</body>