178 lines
5.5 KiB
TypeScript
178 lines
5.5 KiB
TypeScript
// UserGreeting.tsx
|
|
'use client';
|
|
|
|
import { useEffect, useState, useRef, useCallback } from 'react';
|
|
import { Button } from './Button';
|
|
import { useCurrentUser } from './AuthContext';
|
|
import { useFade } from '@/app/providers/FadeContext';
|
|
import { writeLogoutNotice } from '@/lib/logoutNotice';
|
|
|
|
function capitalize(name: string) {
|
|
return name.charAt(0).toUpperCase() + name.slice(1);
|
|
}
|
|
|
|
function getGreeting() {
|
|
const hour = new Date().getHours();
|
|
if (hour < 11) return 'Morgen';
|
|
if (hour < 18) return 'Tag';
|
|
return 'Abend';
|
|
}
|
|
|
|
function formatTimeLeft(seconds: number): string {
|
|
const m = Math.floor(seconds / 60);
|
|
const s = seconds % 60;
|
|
const paddedSeconds = s.toString().padStart(2, '0');
|
|
return `${m}m ${paddedSeconds}s`;
|
|
}
|
|
|
|
// Texte zentral halten, falls mehrfach benötigt
|
|
const LOGOUT_REASON_MANUAL = 'Du hast dich abgemeldet.';
|
|
const LOGOUT_REASON_INACTIVE = 'Du wurdest wegen Inaktivität automatisch abgemeldet.';
|
|
const LOGOUT_REASON_EXPIRED = 'Deine Sitzung ist abgelaufen. Bitte melde dich erneut an.';
|
|
|
|
export default function UserGreeting() {
|
|
const { user, logout, tokenExpiresAt } = useCurrentUser();
|
|
const INACTIVITY_LIMIT = 5 * 60; // 5 Minuten in Sekunden
|
|
const [timeLeft, setTimeLeft] = useState<number>(INACTIVITY_LIMIT);
|
|
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
|
const refreshedRef = useRef(false);
|
|
const cachedUsername =
|
|
typeof window !== 'undefined' ? sessionStorage.getItem('username') : null;
|
|
const [username, setUsername] = useState(cachedUsername ?? '');
|
|
const [isClient, setIsClient] = useState(false);
|
|
const fade = useFade();
|
|
|
|
const doLogoutWithNotice = useCallback(
|
|
(noticeReason: 'manual' | 'timeout' | 'expired', message: string) => {
|
|
// 1) persistent Notice schreiben
|
|
writeLogoutNotice({ reason: noticeReason, message });
|
|
|
|
// 2) Context-Logout (räumt User + Token)
|
|
logout(message);
|
|
|
|
// 3) Navigation
|
|
fade('/login');
|
|
},
|
|
[logout, fade]
|
|
);
|
|
|
|
const handleLogout = async () => {
|
|
doLogoutWithNotice('manual', LOGOUT_REASON_MANUAL);
|
|
};
|
|
|
|
const refreshToken = useCallback(async () => {
|
|
try {
|
|
const res = await fetch(`/api/refresh-token`, {
|
|
method: 'POST',
|
|
credentials: 'include',
|
|
});
|
|
|
|
if (res.ok) {
|
|
setTimeLeft(INACTIVITY_LIMIT); // Reset auf volle Zeit
|
|
sessionStorage.setItem('tokenIssuedAt', `${Date.now()}`);
|
|
refreshedRef.current = false;
|
|
} else {
|
|
console.warn('Token refresh failed');
|
|
doLogoutWithNotice('expired', LOGOUT_REASON_EXPIRED);
|
|
}
|
|
} catch (err) {
|
|
console.error('Error refreshing token', err);
|
|
// Netzwerkfehler beim Refresh -> wir loggen besser aus, damit User nicht "hängen" bleibt
|
|
doLogoutWithNotice('expired', LOGOUT_REASON_EXPIRED);
|
|
}
|
|
}, [INACTIVITY_LIMIT, doLogoutWithNotice]);
|
|
|
|
// ⏳ Countdown
|
|
useEffect(() => {
|
|
intervalRef.current = setInterval(() => {
|
|
setTimeLeft(prev => {
|
|
if (prev <= 1) {
|
|
doLogoutWithNotice('timeout', LOGOUT_REASON_INACTIVE);
|
|
return 0;
|
|
}
|
|
|
|
// Token bald ablaufend? Refresh
|
|
const now = Date.now();
|
|
if (
|
|
tokenExpiresAt &&
|
|
tokenExpiresAt - now <= 30_000 &&
|
|
!refreshedRef.current
|
|
) {
|
|
refreshedRef.current = true;
|
|
refreshToken();
|
|
}
|
|
|
|
return prev - 1;
|
|
});
|
|
}, 1000);
|
|
|
|
return () => {
|
|
if (intervalRef.current) clearInterval(intervalRef.current);
|
|
};
|
|
}, [refreshToken, tokenExpiresAt, doLogoutWithNotice]);
|
|
|
|
// 🖱 Aktivität zurücksetzen
|
|
useEffect(() => {
|
|
const reset = () => setTimeLeft(INACTIVITY_LIMIT);
|
|
|
|
const events = ['mousemove', 'keydown', 'scroll', 'click', 'touchstart'];
|
|
events.forEach(e => window.addEventListener(e, reset, { passive: true }));
|
|
|
|
return () => {
|
|
events.forEach(e => window.removeEventListener(e, reset));
|
|
};
|
|
}, [INACTIVITY_LIMIT]);
|
|
|
|
useEffect(() => {
|
|
if (user?.username) {
|
|
setUsername(user.username);
|
|
sessionStorage.setItem('username', user.username);
|
|
}
|
|
}, [user?.username]);
|
|
|
|
useEffect(() => {
|
|
setIsClient(true);
|
|
}, []);
|
|
|
|
const percent = (timeLeft / INACTIVITY_LIMIT) * 100;
|
|
|
|
if (!isClient) return null;
|
|
|
|
return (
|
|
<div className="flex flex-row sm:items-center gap-2 w-full">
|
|
<span className="flex-grow text-lg font-medium text-neutral-700 dark:text-white">
|
|
{username ? `Guten ${getGreeting()}, ${capitalize(username)}!` : null}
|
|
</span>
|
|
<Button
|
|
onClick={handleLogout}
|
|
size="small"
|
|
color="red"
|
|
variant="outline"
|
|
className="flex-shrink-0 min-w-[120px] sm:min-w-[180px] relative overflow-hidden text-sm font-medium px-3 py-1 rounded bg-red-400"
|
|
>
|
|
<span className="relative z-10 text-black dark:text-white flex items-center gap-1">
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
className="h-4 w-4"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
strokeWidth={2}
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
d="M15.75 9V5.25A2.25 2.25 0 0013.5 3H6.75A2.25 2.25 0 004.5 5.25v13.5A2.25 2.25 0 006.75 21h6.75a2.25 2.25 0 002.25-2.25V15M18 12H9m0 0l3-3m-3 3l3 3"
|
|
/>
|
|
</svg>
|
|
Abmelden ({formatTimeLeft(timeLeft)})
|
|
</span>
|
|
<div
|
|
className="absolute top-0 left-0 h-full bg-red-600 z-0 transition-all duration-100 ease-linear"
|
|
style={{ width: `${percent}%` }}
|
|
/>
|
|
</Button>
|
|
</div>
|
|
);
|
|
}
|