import React, { useEffect, useRef, useState } from "react"; import { storage, isCloudBackend } from "../storage/adapter.js"; const isValidEmail = (s) => /.+@.+\..+/.test(s); // Simple per-tab rate limit: after MAX_ATTEMPTS failed tries, lock for LOCK_MS. const MAX_ATTEMPTS = 5; const LOCK_MS = 60_000; const ATTEMPT_KEY = "rapport_login_attempts"; function readAttempts() { try { return JSON.parse(sessionStorage.getItem(ATTEMPT_KEY)) || { count: 0, lockedUntil: 0 }; } catch { return { count: 0, lockedUntil: 0 }; } } function writeAttempts(state) { try { sessionStorage.setItem(ATTEMPT_KEY, JSON.stringify(state)); } catch {} } export default function Login({ verifyLogin, settings, version, cloudUnreachable = false }) { // Backend-Modus aus localStorage (per-Device, ähnlich Dark Mode). // Beim Wechsel wird die App neu geladen, damit der Storage-Adapter neu initialisiert. const [backend, setBackend] = useState(() => localStorage.getItem("rapport_backend") || "local"); const [cloudUrl, setCloudUrl] = useState(() => localStorage.getItem("rapport_cloud_url") || ""); const [editUrl, setEditUrl] = useState(() => { const stored = localStorage.getItem("rapport_cloud_url") || ""; return (localStorage.getItem("rapport_backend") === "cloud") && !stored; }); const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); const [error, setError] = useState(false); const [errorMsg, setErrorMsg] = useState(""); const [shake, setShake] = useState(false); const [lockedUntil, setLockedUntil] = useState(() => readAttempts().lockedUntil); const [now, setNow] = useState(Date.now()); const submittingRef = useRef(false); // Cloud: Studios der Instanz für Multi-Studio-Dropdown const [studios, setStudios] = useState([]); const [selectedStudioId, setSelectedStudioId] = useState(""); // Passwort-Vergessen-Inline-Modus const [forgotOpen, setForgotOpen] = useState(false); const [forgotSent, setForgotSent] = useState(false); const [forgotErr, setForgotErr] = useState(""); const isCloud = backend === "cloud"; useEffect(() => { if (!isCloudBackend || !cloudUrl) return; let cancelled = false; (async () => { const list = await storage.listStudios?.(); if (cancelled) return; setStudios(list || []); if (list?.length === 1) setSelectedStudioId(list[0].id); })(); return () => { cancelled = true; }; }, [cloudUrl]); // Tick once a second while locked, so the countdown updates and unlocks automatically useEffect(() => { if (!lockedUntil || lockedUntil <= now) return; const id = setInterval(() => setNow(Date.now()), 1000); return () => clearInterval(id); }, [lockedUntil, now]); const isLocked = lockedUntil > now; const remainingSec = isLocked ? Math.ceil((lockedUntil - now) / 1000) : 0; const switchBackend = (next) => { if (next === backend) return; localStorage.setItem("rapport_backend", next); window.location.reload(); }; const handleSubmit = async (e) => { e.preventDefault(); if (isLocked || submittingRef.current) return; submittingRef.current = true; try { // Cloud: URL muss gesetzt sein, sonst kann der Adapter nicht initialisiert worden sein if (isCloud) { const trimmed = (cloudUrl || "").trim().replace(/\/+$/, ""); if (!trimmed) { setError(true); setErrorMsg("Server-Adresse eingeben."); return; } const currentUrl = localStorage.getItem("rapport_cloud_url") || ""; if (trimmed !== currentUrl) { localStorage.setItem("rapport_cloud_url", trimmed); window.location.reload(); return; } } const user = await Promise.resolve(verifyLogin(username, password, { studioId: selectedStudioId || null })); if (user) { writeAttempts({ count: 0, lockedUntil: 0 }); } else { const state = readAttempts(); const count = state.count + 1; const next = count >= MAX_ATTEMPTS ? { count: 0, lockedUntil: Date.now() + LOCK_MS } : { count, lockedUntil: 0 }; writeAttempts(next); setLockedUntil(next.lockedUntil); setNow(Date.now()); setError(true); setErrorMsg(isCloud ? "Anmeldung fehlgeschlagen." : "Falscher Benutzername oder Passwort"); setShake(true); setTimeout(() => setShake(false), 500); } } finally { submittingRef.current = false; } }; const studioHeader = settings?.name || "Studio"; const userLabel = isCloud ? "EMAIL" : "BENUTZER"; const userPlaceholder = isCloud ? "name@studio.ch" : "admin"; const userInputType = isCloud ? "email" : "text"; const showUrlField = isCloud && editUrl; const showUrlBadge = isCloud && !editUrl && cloudUrl; // Hostname zur Anzeige (ohne Protokoll, ohne Port falls Standard) let urlDisplay = cloudUrl; try { const u = new URL(cloudUrl); urlDisplay = u.host; } catch {} return (
RAPPORT
{studioHeader.toUpperCase()}
{cloudUnreachable && (
Server nicht erreichbar. Bitte Verbindung prüfen und neu laden.
)}
{isCloud && studios.length > 1 && (
)}
{ setUsername(e.target.value); setError(false); }} placeholder={userPlaceholder} />
{ setPassword(e.target.value); setError(false); }} placeholder="••••••" />
{showUrlField && (
{ setCloudUrl(e.target.value); setError(false); }} placeholder="http://mac-mini.local:54321" />
)} {isLocked ? (
Zu viele Fehlversuche. Bitte {remainingSec}s warten.
) : error && (
{errorMsg || (isCloud ? "Anmeldung fehlgeschlagen." : "Falscher Benutzername oder Passwort")}
)}
{/* Passwort vergessen — nur Cloud */} {isCloud && !isLocked && !forgotOpen && !forgotSent && (
)} {isCloud && forgotOpen && !forgotSent && (
Wir senden Ihnen eine Email mit einem Link zum Zurücksetzen des Passworts.
setUsername(e.target.value)} style={{ marginBottom: 8 }} /> {forgotErr &&
{forgotErr}
}
)} {isCloud && forgotSent && (
Email gesendet — bitte Ihren Posteingang prüfen.
)} {/* Verbindung-Switch + Server-Anzeige (dezent darunter) */}
Verbindung
{showUrlBadge && (
{urlDisplay}
)}
{version ? `V${version}` : ""}
); }