Added session management with periodic keepalive:

- **Backend**: Introduced `SessionController` with `/keepalive` endpoint to keep sessions active.
- **Frontend**: Enhanced session handling with `startSessionRefresh` and activity tracking in `backend.js`. Integrated session refresh initiation in `main.js`. Adjusted logic in `CalculationAssistant.vue` for input validation.
- **Other Changes**: Updated `BulkOperationRepository` cleanup interval to 60 minutes for improved timeout handling.
This commit is contained in:
Jan 2025-11-10 14:19:01 +01:00
parent 982b637d47
commit af7b51578b
5 changed files with 89 additions and 3 deletions

View file

@ -10,6 +10,55 @@ const getCsrfToken = () => {
return null; return null;
} }
let sessionRefreshInterval = null;
let lastActivity = Date.now();
const refreshSession = async () => {
try {
// Ein simpler authenticated GET request hält die Session am Leben
await fetch('/api/session/keepalive', {
method: 'GET',
credentials: 'include'
});
console.log('Session refreshed');
} catch (e) {
console.error('Session refresh failed', e);
}
}
const trackActivity = () => {
lastActivity = Date.now();
}
export const startSessionRefresh = () => {
['mousedown', 'keypress', 'scroll', 'touchstart'].forEach(event => {
document.addEventListener(event, trackActivity);
});
if (sessionRefreshInterval) {
clearInterval(sessionRefreshInterval);
}
sessionRefreshInterval = setInterval(() => {
const timeSinceActivity = Date.now() - lastActivity;
if (timeSinceActivity < (12 * 60 * 60 * 1000)) {
refreshSession();
}
}, 10 * 60 * 1000);
}
export const stopSessionRefresh = () => {
if (sessionRefreshInterval) {
clearInterval(sessionRefreshInterval);
}
}
const performRequest = async (requestingStore, method, url, body, expectResponse = true, expectedException = null) => { const performRequest = async (requestingStore, method, url, body, expectResponse = true, expectedException = null) => {
const params = { const params = {
@ -124,6 +173,12 @@ const executeRequest = async (requestingStore, request) => {
throw e; throw e;
}); });
if (response.status === 401) {
logger.warn('Session expired, redirecting to login...');
window.location.href = '/oauth2/authorization/azure';
return Promise.reject(new Error('Session expired'));
}
let data = null; let data = null;
if (request.expectResponse) { if (request.expectResponse) {
try { try {
@ -173,4 +228,4 @@ const executeRequest = async (requestingStore, request) => {
} }
export default performRequest; export default performRequest;
export {performUpload, performDownload, getCsrfToken}; export {performUpload, performDownload, getCsrfToken, startSessionRefresh, stopSessionRefresh};

View file

@ -3,6 +3,7 @@ import router from './router.js';
import { setupErrorBuffer } from './store/error.js' import { setupErrorBuffer } from './store/error.js'
import {createApp} from 'vue' import {createApp} from 'vue'
import {createPinia} from 'pinia'; import {createPinia} from 'pinia';
import {startSessionRefresh} from "@/backend.js";
import App from './App.vue' import App from './App.vue'
import { import {
@ -31,6 +32,9 @@ import {
PhCloudX, PhDesktop, PhHardDrives PhCloudX, PhDesktop, PhHardDrives
} from "@phosphor-icons/vue"; } from "@phosphor-icons/vue";
const app = createApp(App); const app = createApp(App);
const pinia = createPinia(); const pinia = createPinia();
app.use(pinia); app.use(pinia);
@ -72,5 +76,6 @@ app.use(router);
//app.component('base-button', () => import('./components/UI/BasicButton.vue')); //app.component('base-button', () => import('./components/UI/BasicButton.vue'));
//app.component('base-badge', () => import('./components/UI/BasicBadge.vue')); //app.component('base-badge', () => import('./components/UI/BasicBadge.vue'));
setupErrorBuffer() setupErrorBuffer()
startSessionRefresh();
app.mount('#app'); app.mount('#app');

View file

@ -150,7 +150,10 @@ export default {
}, },
parsePartNumbers() { parsePartNumbers() {
this.closeModal('partNumber'); this.closeModal('partNumber');
this.assistantStore.getMaterialsAndSuppliers(this.partNumberField);
if(this.partNumberField.trim().length !== 0)
this.assistantStore.getMaterialsAndSuppliers(this.partNumberField);
this.partNumberField = ''; this.partNumberField = '';
}, },
addCreatedNode(supplier) { addCreatedNode(supplier) {

View file

@ -0,0 +1,23 @@
package de.avatic.lcc.controller;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@RestController
@RequestMapping("/api/session")
public class SessionController {
@GetMapping("/keepalive")
public Map<String, Object> keepalive(Authentication authentication) {
return Map.of(
"status", "ok",
"user", authentication.getName(),
"timestamp", System.currentTimeMillis()
);
}
}

View file

@ -135,7 +135,7 @@ public class BulkOperationRepository {
private void cleanupTimeouts(Integer userId) { private void cleanupTimeouts(Integer userId) {
String sql = """ String sql = """
UPDATE bulk_operation SET state = 'EXCEPTION' WHERE user_id = ? AND (state = 'PROCESSING' OR state = 'SCHEDULED') AND created_at < NOW() - INTERVAL 30 MINUTE UPDATE bulk_operation SET state = 'EXCEPTION' WHERE user_id = ? AND (state = 'PROCESSING' OR state = 'SCHEDULED') AND created_at < NOW() - INTERVAL 60 MINUTE
"""; """;
jdbcTemplate.update(sql, userId); jdbcTemplate.update(sql, userId);