2025-10-23 12:11:41 +02:00

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