Gitea:
Push für Serveranwendung in Gitea implementiert
Dieser Commit ist enthalten in:
@ -370,6 +370,258 @@ class UserMenuHandler {
|
||||
}
|
||||
}
|
||||
|
||||
// Session Timer Handler
|
||||
class SessionTimerHandler {
|
||||
constructor(authManager) {
|
||||
this.auth = authManager;
|
||||
this.timerElement = null;
|
||||
this.countdownElement = null;
|
||||
this.intervalId = null;
|
||||
this.expiresAt = null;
|
||||
this.warningThreshold = 60; // Warnung bei 60 Sekunden verbleibend
|
||||
this.refreshDebounceTimer = null;
|
||||
this.refreshDebounceDelay = 1000; // 1 Sekunde Debounce
|
||||
this.isRefreshing = false;
|
||||
this.isActive = false; // Nur aktiv wenn eingeloggt und Timer läuft
|
||||
}
|
||||
|
||||
init() {
|
||||
this.timerElement = $('#session-timer');
|
||||
this.countdownElement = $('#session-countdown');
|
||||
|
||||
// Bei Login neu initialisieren
|
||||
window.addEventListener('auth:login', () => {
|
||||
// Kurze Verzögerung um sicherzustellen, dass Token gespeichert ist
|
||||
setTimeout(() => {
|
||||
this.updateFromToken();
|
||||
this.start();
|
||||
this.isActive = true;
|
||||
}, 100);
|
||||
});
|
||||
|
||||
// Bei Logout stoppen
|
||||
window.addEventListener('auth:logout', () => {
|
||||
this.isActive = false;
|
||||
this.stop();
|
||||
this.hide();
|
||||
});
|
||||
|
||||
// Bei Interaktionen Session refreshen (mit Debouncing)
|
||||
this.bindInteractionEvents();
|
||||
}
|
||||
|
||||
// Interaktions-Events binden für Session-Refresh
|
||||
bindInteractionEvents() {
|
||||
const refreshOnInteraction = (e) => {
|
||||
// Nicht refreshen wenn nicht aktiv (nicht eingeloggt oder Timer läuft nicht)
|
||||
if (!this.isActive) return;
|
||||
|
||||
// Nicht refreshen bei Klicks auf Login-Formular
|
||||
if (e.target.closest('#login-form') || e.target.closest('.login-container')) return;
|
||||
|
||||
// Nur refreshen wenn Token existiert
|
||||
if (!localStorage.getItem('auth_token')) return;
|
||||
|
||||
// Debounce: Nur alle X ms refreshen
|
||||
if (this.refreshDebounceTimer) {
|
||||
clearTimeout(this.refreshDebounceTimer);
|
||||
}
|
||||
|
||||
this.refreshDebounceTimer = setTimeout(() => {
|
||||
this.refreshSession();
|
||||
}, this.refreshDebounceDelay);
|
||||
};
|
||||
|
||||
// Click-Events auf dem gesamten Dokument
|
||||
document.addEventListener('click', refreshOnInteraction);
|
||||
|
||||
// Keyboard-Events
|
||||
document.addEventListener('keydown', refreshOnInteraction);
|
||||
}
|
||||
|
||||
// Session beim Server refreshen
|
||||
async refreshSession() {
|
||||
if (this.isRefreshing) return;
|
||||
|
||||
const token = localStorage.getItem('auth_token');
|
||||
if (!token) return;
|
||||
|
||||
this.isRefreshing = true;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/auth/refresh', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.token) {
|
||||
// Wichtig: api.setToken() verwenden, um den Cache zu aktualisieren
|
||||
api.setToken(data.token);
|
||||
this.expiresAt = this.parseToken(data.token);
|
||||
this.timerElement?.classList.remove('warning', 'critical');
|
||||
|
||||
// CSRF-Token auch aktualisieren
|
||||
if (data.csrfToken) {
|
||||
api.setCsrfToken(data.csrfToken);
|
||||
}
|
||||
}
|
||||
} else if (response.status === 401) {
|
||||
// Token ungültig - ausloggen
|
||||
this.auth.logout();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Session refresh error:', error);
|
||||
} finally {
|
||||
this.isRefreshing = false;
|
||||
}
|
||||
}
|
||||
|
||||
// JWT-Token parsen und Ablaufzeit extrahieren
|
||||
parseToken(token) {
|
||||
if (!token) return null;
|
||||
try {
|
||||
const payload = token.split('.')[1];
|
||||
const decoded = JSON.parse(atob(payload));
|
||||
return decoded.exp ? decoded.exp * 1000 : null; // exp ist in Sekunden, wir brauchen ms
|
||||
} catch (e) {
|
||||
console.error('Token parsing error:', e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
updateFromToken() {
|
||||
const token = localStorage.getItem('auth_token');
|
||||
this.expiresAt = this.parseToken(token);
|
||||
}
|
||||
|
||||
// Beim Seiten-Reload aufrufen
|
||||
async initFromExistingSession() {
|
||||
const token = localStorage.getItem('auth_token');
|
||||
if (!token) {
|
||||
this.hide();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Prüfen ob Token noch gültig ist
|
||||
const expiresAt = this.parseToken(token);
|
||||
if (!expiresAt || expiresAt <= Date.now()) {
|
||||
// Token abgelaufen
|
||||
this.hide();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Session refreshen um neues Token zu bekommen
|
||||
this.isActive = true; // Aktivieren damit refreshSession funktioniert
|
||||
await this.refreshSession();
|
||||
|
||||
// Timer mit neuem Token starten
|
||||
this.updateFromToken();
|
||||
if (this.expiresAt) {
|
||||
this.start();
|
||||
return true;
|
||||
}
|
||||
|
||||
this.isActive = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
start() {
|
||||
this.stop(); // Bestehenden Timer stoppen
|
||||
|
||||
if (!this.expiresAt) {
|
||||
this.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
this.show();
|
||||
this.update(); // Sofort updaten
|
||||
|
||||
// Jede Sekunde aktualisieren
|
||||
this.intervalId = setInterval(() => this.update(), 1000);
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (this.intervalId) {
|
||||
clearInterval(this.intervalId);
|
||||
this.intervalId = null;
|
||||
}
|
||||
}
|
||||
|
||||
update() {
|
||||
if (!this.expiresAt || !this.countdownElement) return;
|
||||
|
||||
const now = Date.now();
|
||||
const remaining = Math.max(0, this.expiresAt - now);
|
||||
const seconds = Math.floor(remaining / 1000);
|
||||
|
||||
// Zeit formatieren
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const secs = seconds % 60;
|
||||
const timeStr = `${String(minutes).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
|
||||
|
||||
this.countdownElement.textContent = timeStr;
|
||||
|
||||
// Warnung bei wenig Zeit
|
||||
if (seconds <= this.warningThreshold && seconds > 0) {
|
||||
this.timerElement?.classList.add('warning');
|
||||
} else {
|
||||
this.timerElement?.classList.remove('warning');
|
||||
}
|
||||
|
||||
// Kritisch bei < 30 Sekunden
|
||||
if (seconds <= 30 && seconds > 0) {
|
||||
this.timerElement?.classList.add('critical');
|
||||
} else {
|
||||
this.timerElement?.classList.remove('critical');
|
||||
}
|
||||
|
||||
// Session abgelaufen
|
||||
if (seconds <= 0) {
|
||||
this.stop();
|
||||
this.handleExpired();
|
||||
}
|
||||
}
|
||||
|
||||
handleExpired() {
|
||||
// Toast anzeigen
|
||||
window.dispatchEvent(new CustomEvent('toast:show', {
|
||||
detail: {
|
||||
message: 'Sitzung abgelaufen. Bitte erneut anmelden.',
|
||||
type: 'warning',
|
||||
duration: 5000
|
||||
}
|
||||
}));
|
||||
|
||||
// Automatisch ausloggen
|
||||
this.auth.logout();
|
||||
}
|
||||
|
||||
show() {
|
||||
this.timerElement?.classList.remove('hidden');
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.timerElement?.classList.add('hidden');
|
||||
if (this.countdownElement) {
|
||||
this.countdownElement.textContent = '--:--';
|
||||
}
|
||||
}
|
||||
|
||||
// Token wurde erneuert (z.B. durch API-Response mit X-New-Token)
|
||||
refreshToken(newToken) {
|
||||
if (newToken) {
|
||||
// api.setToken() wird bereits in api.js aufgerufen, hier nur Timer aktualisieren
|
||||
this.expiresAt = this.parseToken(newToken);
|
||||
this.timerElement?.classList.remove('warning', 'critical');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Change Password Modal Handler
|
||||
class ChangePasswordHandler {
|
||||
constructor(authManager) {
|
||||
@ -516,12 +768,21 @@ const authManager = new AuthManager();
|
||||
let loginFormHandler = null;
|
||||
let userMenuHandler = null;
|
||||
let changePasswordHandler = null;
|
||||
let sessionTimerHandler = null;
|
||||
|
||||
// Initialize handlers when DOM is ready
|
||||
function initAuthHandlers() {
|
||||
async function initAuthHandlers() {
|
||||
loginFormHandler = new LoginFormHandler(authManager);
|
||||
userMenuHandler = new UserMenuHandler(authManager);
|
||||
changePasswordHandler = new ChangePasswordHandler(authManager);
|
||||
sessionTimerHandler = new SessionTimerHandler(authManager);
|
||||
sessionTimerHandler.init();
|
||||
|
||||
// Bei bestehendem Token: Session refreshen und Timer starten
|
||||
const token = localStorage.getItem('auth_token');
|
||||
if (token) {
|
||||
await sessionTimerHandler.initFromExistingSession();
|
||||
}
|
||||
}
|
||||
|
||||
// Listen for DOM ready
|
||||
@ -536,11 +797,17 @@ window.addEventListener('auth:login', () => {
|
||||
userMenuHandler?.update();
|
||||
});
|
||||
|
||||
// Listen for token refresh event to update session timer
|
||||
window.addEventListener('auth:token-refreshed', (e) => {
|
||||
sessionTimerHandler?.refreshToken(e.detail?.token);
|
||||
});
|
||||
|
||||
export {
|
||||
authManager,
|
||||
loginFormHandler,
|
||||
userMenuHandler,
|
||||
changePasswordHandler
|
||||
changePasswordHandler,
|
||||
sessionTimerHandler
|
||||
};
|
||||
|
||||
export default authManager;
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren