import React, { useState, useEffect } from "react"; import { invoke } from "@tauri-apps/api/core"; import logoUrl from "./assets/logo.png"; import { MailIcon, LockIcon, EyeIcon, EyeOffIcon, ArrowRightLoginIcon, AlertCircleIcon, CheckCircleIcon, SpinnerIcon, ChevronLeftIcon, UserIcon, } from "./components/Icons"; import { AuthResponse, DbStatus, ResetRequestResponse, SafeDbInfo, UserDto } from "./types"; import { OtpInput } from "./components/OtpInput"; import { UpdaterBanner } from "./components/UpdaterBanner"; import "./App.css"; import Dashboard from "./pages/dashboard/Dashboard"; import { TvProfileSelector } from "./components/TvProfileSelector"; const isTauri = () => { return typeof window !== "undefined" && "__TAURI_INTERNALS__" in window; }; // Helper for Smart TV / Android TV detection const detectTvEnvironment = (): boolean => { if (typeof window === "undefined") return false; const saved = localStorage.getItem("has_tv_mode"); if (saved !== null) return saved === "true"; const ua = (navigator.userAgent || "").toLowerCase(); const isTvUa = ua.includes("android tv") || ua.includes("smarttv") || ua.includes("googletv") || ua.includes("tizen") || ua.includes("webos") || ua.includes("appletv") || ua.includes("crkey") || ua.includes("bravia") || ua.includes("aftt") || ua.includes("aftm") || ua.includes("mibox") || ua.includes("mi box") || ua.includes("shield") || ua.includes("dtv") || ua.includes("smart-tv") || ua.includes("hbbtv") || ua.includes("netcast") || ua.includes("viera") || ua.includes("roku") || ua.includes("firetv"); if (isTvUa) return true; const isAndroid = ua.includes("android"); const noTouch = !("ontouchstart" in window) && navigator.maxTouchPoints === 0; if (isAndroid && noTouch) return true; return false; }; export default function App() { const [isTvEnvironment, setIsTvEnvironment] = useState(detectTvEnvironment); const [mode, setMode] = useState<"login" | "register" | "reset">("login"); const [username, setUsername] = useState(""); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [confirmPassword, setConfirmPassword] = useState(""); const [showPassword, setShowPassword] = useState(false); const [rememberMe, setRememberMe] = useState(true); // Registration verification code state const [regStage, setRegStage] = useState<1 | 2>(1); const [regCode, setRegCode] = useState(""); const [regCooldown, setRegCooldown] = useState(0); const [emailCheckLoading, setEmailCheckLoading] = useState(false); const [emailCheckStatus, setEmailCheckStatus] = useState<"idle" | "checking" | "valid" | "invalid">("idle"); const [emailCheckMsg, setEmailCheckMsg] = useState(null); // Password reset state const [resetStage, setResetStage] = useState<1 | 2 | 3>(1); const [resetEmail, setResetEmail] = useState(""); const [resetCode, setResetCode] = useState(""); const [countdown, setCountdown] = useState(30); const [newPassword, setNewPassword] = useState(""); const [confirmNewPassword, setConfirmNewPassword] = useState(""); const [showNewPassword, setShowNewPassword] = useState(false); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [success, setSuccess] = useState(null); const [currentUser, setCurrentUser] = useState(null); const [dbStatus, setDbStatus] = useState(null); const [dbInfo, setDbInfo] = useState(null); const [showForgotModal, setShowForgotModal] = useState(false); useEffect(() => { if (regCooldown > 0) { const timer = setTimeout(() => setRegCooldown(regCooldown - 1), 1000); return () => clearTimeout(timer); } }, [regCooldown]); // Check saved session on mount useEffect(() => { const isTv = isTvEnvironment; // On TV, the user explicitly wants the profile selection screen at startup! if (!isTv) { const savedUser = localStorage.getItem("has_remembered_user"); if (savedUser) { try { const parsed = JSON.parse(savedUser); if (parsed?.email && parsed.email.toLowerCase() === "ne60201@gmail.com") { parsed.role = "admin"; } setCurrentUser(parsed); } catch { localStorage.removeItem("has_remembered_user"); } } } else { // On TV, ensure remembered user is saved into has_tv_saved_profiles const savedUser = localStorage.getItem("has_remembered_user"); if (savedUser) { try { const parsed = JSON.parse(savedUser); const savedProfilesStr = localStorage.getItem("has_tv_saved_profiles"); const existingProfiles = savedProfilesStr ? JSON.parse(savedProfilesStr) : []; if (!existingProfiles.some((p: any) => p.email?.toLowerCase() === parsed.email?.toLowerCase())) { existingProfiles.unshift(parsed); localStorage.setItem("has_tv_saved_profiles", JSON.stringify(existingProfiles)); } } catch {} } } const savedEmail = localStorage.getItem("has_remembered_email"); if (savedEmail) { setEmail(savedEmail); } // Check DB status on startup checkDatabase(); }, [isTvEnvironment]); // Activate TV mode immediately upon any remote control key press useEffect(() => { const handleRemoteKey = (e: KeyboardEvent) => { const isRemoteKey = [19, 20, 21, 22, 23, 4, 82, 85, 179].includes(e.keyCode) || ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(e.key); if (isRemoteKey && !isTvEnvironment) { setIsTvEnvironment(true); localStorage.setItem("has_tv_mode", "true"); } }; window.addEventListener("keydown", handleRemoteKey); return () => window.removeEventListener("keydown", handleRemoteKey); }, [isTvEnvironment]); // Countdown timer for OTP re-send in resetStage 2 useEffect(() => { if (resetStage !== 2) return; setCountdown(30); const timer = setInterval(() => { setCountdown((prev) => { if (prev <= 1) { clearInterval(timer); return 0; } return prev - 1; }); }, 1000); return () => clearInterval(timer); }, [resetStage]); const checkDatabase = async () => { if (!isTauri()) { setDbStatus({ connected: false, host: "localhost", dbname: "postgres", message: "Приложение запущено в браузере. Запустите через Tauri для подключения к Postgres.", }); return; } try { const info = await invoke("get_db_info"); setDbInfo(info); const status = await invoke("check_db"); setDbStatus(status); } catch (err: any) { setDbStatus({ connected: false, host: "unknown", dbname: "unknown", message: String(err?.message || err), }); } }; const handleLogin = async (e: React.FormEvent) => { e.preventDefault(); setError(null); setSuccess(null); if (!email.trim() || !password) { setError("Пожалуйста, заполните email и пароль"); return; } if (!isTauri()) { setError("Для реального подключения к PostgreSQL запустите десктоп-приложение ('npm run tauri dev')"); return; } setLoading(true); try { const res = await invoke("login", { email: email.trim(), password, }); if (res.success && res.user) { setCurrentUser(res.user); try { const savedProfilesStr = localStorage.getItem("has_tv_saved_profiles"); const existingProfiles = savedProfilesStr ? JSON.parse(savedProfilesStr) : []; const updated = [ res.user, ...existingProfiles.filter((p: any) => p.email?.toLowerCase() !== res.user!.email.toLowerCase()), ]; localStorage.setItem("has_tv_saved_profiles", JSON.stringify(updated)); } catch {} if (rememberMe) { localStorage.setItem("has_remembered_user", JSON.stringify(res.user)); localStorage.setItem("has_remembered_email", email.trim()); } else { localStorage.removeItem("has_remembered_user"); localStorage.removeItem("has_remembered_email"); } } else { setError(res.message || "Ошибка входа. Проверьте данные."); } } catch (err: any) { setError(`Ошибка: ${err?.message || err}`); } finally { setLoading(false); } }; const isValidEmailFormat = (val: string) => { return /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(val.trim()); }; const handleEmailBlur = async () => { const clean = email.trim(); if (!clean || mode !== "register" || regStage !== 1) return; if (!isValidEmailFormat(clean)) { setEmailCheckStatus("invalid"); setEmailCheckMsg("Некорректный формат адреса электронной почты"); return; } if (!isTauri()) return; setEmailCheckLoading(true); setEmailCheckStatus("checking"); setEmailCheckMsg(null); try { await invoke("validate_email", { email: clean }); setEmailCheckStatus("valid"); setEmailCheckMsg("Почта подтверждена (существует)"); setError(null); } catch (err: any) { setEmailCheckStatus("invalid"); const msg = err?.message || String(err); setEmailCheckMsg(msg); setError(msg); } finally { setEmailCheckLoading(false); } }; const handleRequestRegCode = async (e?: React.FormEvent) => { if (e) e.preventDefault(); setError(null); setSuccess(null); const cleanEmail = email.trim(); if (!cleanEmail || !password) { setError("Пожалуйста, укажите email и пароль"); return; } if (!isValidEmailFormat(cleanEmail)) { setError("Пожалуйста, введите корректный адрес электронной почты (например, name@mail.ru)"); return; } if (emailCheckStatus === "invalid") { setError(emailCheckMsg || "Указанный адрес электронной почты не существует. Пожалуйста, укажите действительный почтовый ящик."); return; } if (!username.trim()) { setError("Пожалуйста, укажите имя пользователя"); return; } if (password !== confirmPassword) { setError("Пароли не совпадают"); return; } if (password.length < 6) { setError("Пароль должен содержать минимум 6 символов"); return; } if (!isTauri()) { setError("Для регистрации пользователя запустите приложение в десктопном режиме"); return; } setLoading(true); try { const res = await invoke("request_registration_code", { email: cleanEmail, username: username.trim(), }); if (res.success) { setRegStage(2); setRegCooldown(60); setSuccess(res.message); if (res.dev_code) { setRegCode(res.dev_code); } } else { setError(res.message || "Ошибка отправки кода подтверждения на указанную почту."); } } catch (err: any) { setError(`Ошибка: ${err?.message || err}`); } finally { setLoading(false); } }; const handleCompleteRegistration = async (e: React.FormEvent) => { e.preventDefault(); setError(null); setSuccess(null); if (!regCode.trim()) { setError("Пожалуйста, введите проверочный код из письма"); return; } if (!isTauri()) { setError("Для регистрации пользователя запустите десктопное приложение"); return; } setLoading(true); try { const res = await invoke("register_with_code", { email: email.trim(), code: regCode.trim(), password, username: username.trim(), }); if (res.success) { setSuccess("Аккаунт успешно создан и подтвержден! Теперь вы можете войти."); setRegStage(1); setRegCode(""); setMode("login"); } else { setError(res.message || "Ошибка подтверждения кода или создания аккаунта."); } } catch (err: any) { setError(`Ошибка: ${err?.message || err}`); } finally { setLoading(false); } }; const handleRequestResetCode = async (e: React.FormEvent) => { e.preventDefault(); setError(null); setSuccess(null); if (!resetEmail.trim()) { setError("Пожалуйста, введите адрес электронной почты"); return; } if (!isTauri()) { setError("Для работы сброса пароля запустите десктоп-приложение ('npm run tauri dev')"); return; } setLoading(true); try { const res = await invoke("request_reset_code", { email: resetEmail.trim(), }); if (res.success) { setResetStage(2); setSuccess(res.message); } else { setError(res.message || "Ошибка отправки проверочного кода"); } } catch (err: any) { setError(`Ошибка: ${err?.message || err}`); } finally { setLoading(false); } }; const handleVerifyResetCode = async (e: React.FormEvent) => { e.preventDefault(); setError(null); setSuccess(null); if (!resetCode.trim()) { setError("Пожалуйста, введите проверочный код"); return; } if (!isTauri()) { setError("Для проверки кода запустите десктоп-приложение"); return; } setLoading(true); try { const res = await invoke("verify_reset_code", { email: resetEmail.trim(), code: resetCode.trim(), }); if (res.success) { setResetStage(3); setSuccess("Код подтвержден! Теперь задайте новый пароль."); } else { setError(res.message || "Неверный код подтверждения"); } } catch (err: any) { setError(`Ошибка: ${err?.message || err}`); } finally { setLoading(false); } }; const handleResetPassword = async (e: React.FormEvent) => { e.preventDefault(); setError(null); setSuccess(null); if (!newPassword) { setError("Пожалуйста, введите новый пароль"); return; } if (newPassword !== confirmNewPassword) { setError("Пароли не совпадают"); return; } if (!isTauri()) { setError("Для смены пароля запустите десктоп-приложение"); return; } setLoading(true); try { const res = await invoke("reset_password", { email: resetEmail.trim(), code: resetCode.trim(), newPassword, }); if (res.success) { setMode("login"); setEmail(resetEmail.trim()); setPassword(""); setResetEmail(""); setResetCode(""); setNewPassword(""); setConfirmNewPassword(""); setResetStage(1); setSuccess("Пароль успешно обновлен! Войдите с новым паролем."); } else { setError(res.message || "Не удалось изменить пароль"); } } catch (err: any) { setError(`Ошибка: ${err?.message || err}`); } finally { setLoading(false); } }; const handleLogout = () => { setCurrentUser(null); localStorage.removeItem("has_remembered_user"); setPassword(""); setConfirmPassword(""); setSuccess(null); setError(null); }; if (currentUser) { return ( <> ); } if (isTvEnvironment) { return ( <> { setCurrentUser(user); localStorage.setItem("has_remembered_user", JSON.stringify(user)); }} onSwitchToDesktop={() => { setIsTvEnvironment(false); localStorage.setItem("has_tv_mode", "false"); }} /> ); } return (
{/* Auto-updater banner */} {/* Main Content Area */} {mode === "reset" ? ( /* Password Reset Flow */
{/* Project Branding */}
Home Autli Server Home Autli Server
{/* Title & Subtitle */}

{resetStage === 1 ? "Сброс пароля" : resetStage === 2 ? "Сброс пароля" : "Новый пароль"}

{resetStage === 1 ? "Забыли пароль? Введите email для получения проверочного кода." : resetStage === 2 ? "Введите 6-значный код подтверждения из письма." : "Код подтвержден. Введите новый пароль для вашей учетной записи:"}

{/* Notification Messages */} {error && (
{error}
)} {success && (
{success}
)} {/* Step 1: Request code by email */} {resetStage === 1 && (
setResetEmail(e.target.value)} required autoComplete="email" />
{ setMode("login"); setError(null); setSuccess(null); }} > Назад ко входу
)} {/* Step 2: Verify code (6 separate OTP boxes matching mockup) */} {resetStage === 2 && (
setResetCode(code)} disabled={loading} />
Не получили код?{" "} {countdown > 0 ? ( Отправить повторно через {countdown}с ) : ( { handleRequestResetCode(e); setCountdown(30); }} > Отправить код повторно )}
{ setMode("login"); setError(null); setSuccess(null); }} > Назад ко входу
)} {/* Step 3: Enter new password and confirmation */} {resetStage === 3 && (
setNewPassword(e.target.value)} required />
setConfirmNewPassword(e.target.value)} required />
{ setMode("login"); setError(null); setSuccess(null); }} > Назад ко входу
)}
) : ( /* Login / Register Card with actual logo & branding */
{/* Actual Project Logo and Name */}
Home Autli Server Home Autli Server
{/* Title & Subtitle */}

{mode === "login" ? "Вход в аккаунт" : "Создать аккаунт"}

{mode === "login" ? "Авторизация в системе Home Autli Server" : "Регистрация нового пользователя на сервере"}

{/* Notification Messages */} {error && (
{error}
)} {success && (
{success}
)} {/* Form */} {mode === "register" && regStage === 2 ? (
На почту {email.trim()} отправлен 6-значный проверочный код. Введите его для подтверждения адреса:
setRegCode(code)} disabled={loading} />
) : (
{/* Username (Register mode only) */} {mode === "register" && (
setUsername(e.target.value)} required autoComplete="name" />
)} {/* Email Address */}
{mode === "register" && emailCheckStatus === "valid" && ( ✓ Почта существует )} {mode === "register" && emailCheckStatus === "invalid" && ( {emailCheckMsg?.includes("зарегистрирован") ? "✕ Уже зарегистрирован" : "✕ Недействительна"} )}
{ setEmail(e.target.value); if (emailCheckStatus !== "idle") { setEmailCheckStatus("idle"); setEmailCheckMsg(null); } }} onBlur={handleEmailBlur} required autoComplete="email" /> {mode === "register" && emailCheckLoading && (
)}
{mode === "register" && emailCheckMsg && emailCheckStatus === "invalid" && (
{emailCheckMsg}
)}
{/* Password */}
setPassword(e.target.value)} required autoComplete={mode === "login" ? "current-password" : "new-password"} />
{/* Confirm Password (Register mode) */} {mode === "register" && (
setConfirmPassword(e.target.value)} required />
)} {/* Remember Me & Forgot Password (Login mode) */} {mode === "login" && (
{ setMode("reset"); setResetStage(1); setResetEmail(email.trim()); setError(null); setSuccess(null); }} > Забыли пароль?
)} {/* Submit Button */} {/* Switch Mode Link */}
{mode === "login" ? ( <> Нет аккаунта?{" "} { setMode("register"); setRegStage(1); setRegCode(""); setUsername(""); setEmailCheckStatus("idle"); setEmailCheckMsg(null); setError(null); setSuccess(null); }} > Зарегистрироваться ) : ( <> Уже есть аккаунт?{" "} { setMode("login"); setRegStage(1); setRegCode(""); setUsername(""); setEmailCheckStatus("idle"); setEmailCheckMsg(null); setError(null); setSuccess(null); }} > Войти )}
)}
)} {/* Footer */}
Copyright © 2026 Home Autli Server
{/*
Политика конфиденциальности Условия использования
*/}
{/* Forgot Password Modal */} {showForgotModal && (
setShowForgotModal(false)}>
e.stopPropagation()}>

Восстановление доступа

Для смены или сброса пароля обратитесь к системному администратору сервера либо воспользуйтесь функцией создания новой учетной записи.

)}
); }