Files
netrunner-app/src/features/VpnControl.tsx
T
nineap 79c9cb48aa Экран входа как гейт перед подключением, фикс matrix-engine и Android-иконки
Home теперь рендерится только когда isLoggedIn — до этого юзер видит
полноэкранный Login (анонимно/seed/Telegram), а не экран подключения; кнопка
VPN дополнительно блокируется на уровне VpnControl как defense-in-depth.
isLoggedIn — отдельный персистентный флаг, не совпадает с фактом наличия
фонового Ghost-токена (тот может тихо появиться до явного логина).

Заодно: matrix-engine в package.json указывал на путь другого пользователя
(/home/kirill/...) — поправил под текущую машину; прогнал `tauri icon`,
чтобы Android больше не показывал дефолтный логотип Tauri (иконка никогда не
генерировалась для этой платформы); dev-узел Singapore (DEV_IP) переведён на
порт 8443 вслед за прокси.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 18:21:40 +07:00

130 lines
4.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import * as React from "react";
import { motion, AnimatePresence } from "framer-motion";
import { cn } from "@/lib/utils";
import { useTranslation } from "react-i18next";
import { TunnelConfig, startVpn, stopVpn } from "vpn-plugin";
import { useVpnStore } from "@/store/useVpnStore";
import { useEffect } from "react";
import { listen } from "@tauri-apps/api/event";
import VpnControlButton from "@/components/shared/VpnControlButton";
import { useTheme } from "@/ThemeProvider";
import { useSettingsStore } from "@/store/useSettingsStore";
import { useNodesStore } from "@/store/useNodesStore";
import { useAuthStore } from "@/store/useAuthStore";
import { getCachedToken } from "@/lib/api";
export function VpnControl() {
const { status, setStatus } = useVpnStore();
const { t } = useTranslation();
const { killSwitch, excludedApps, excludedDomains } = useSettingsStore();
const { getSelectedNode } = useNodesStore();
const isLoggedIn = useAuthStore((s) => s.isLoggedIn);
// Подключаем тему
const { isDark } = useTheme();
const currentTheme = isDark ? "dark" : "light";
const handleToggle = async () => {
// Защита на всякий случай — основной гейт на уровне роута ("/" рендерит
// Login, а не Home, пока не вошли), но кнопка не должна работать даже
// если это состояние как-то обошли.
if (!isLoggedIn) return;
try {
if (status === "idle") {
setStatus("connecting");
// Параметры подключения берём из выбранного на бэкенде узла.
const node = getSelectedNode();
const config: TunnelConfig = {
address: node.tunnelAddress,
prefix: node.tunnelPrefix,
dns: node.tunnelDns,
mtu: node.tunnelMtu,
killswitchEnabled: killSwitch,
excludedApps: excludedApps,
excludedDomains: excludedDomains,
// Есть только если сервер требует `--require-auth`; на инстансах без
// этого флага прокси значение просто игнорирует.
authToken: getCachedToken() ?? undefined,
};
await startVpn(config, node.ipAddress, node.port, node.sni);
} else {
await stopVpn();
}
} catch (error) {
console.error("VPN Toggle Error:", error);
setStatus("idle");
}
};
useEffect(() => {
let unlisten: (() => void) | null = null;
async function setupListener() {
unlisten = await listen<{ status: string }>("status-change", (event) => {
const newStatus = event.payload.status as
| "idle"
| "connecting"
| "connected";
if (newStatus) {
setStatus(newStatus);
}
});
}
setupListener();
return () => {
if (unlisten) unlisten();
};
}, [setStatus]);
const isActive = status !== "idle";
const statusMap = {
idle: t("disconnected"),
connecting: t("connecting"),
connected: t("connected"),
};
return (
<div className="flex flex-col items-center justify-center gap-6 p-10">
<div className="relative flex items-center justify-center size-75">
<VpnControlButton
// Теперь прокидываем реальную тему
theme={currentTheme}
isOn={isActive}
onClick={handleToggle}
style={{
pointerEvents: status === "connecting" || !isLoggedIn ? "none" : "auto",
opacity: !isLoggedIn ? 0.4 : status === "connecting" ? 0.8 : 1,
cursor: !isLoggedIn ? "not-allowed" : undefined,
}}
/>
</div>
<div className="text-center space-y-1">
<p className="text-sm uppercase tracking-widest text-muted-foreground font-semibold">
{t("status_label")}
</p>
<AnimatePresence mode="wait">
<motion.p
key={status}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
className={cn(
"text-xl font-bold tracking-tight",
status === "connected" ? "text-primary" : "text-foreground",
status === "connecting" && "animate-pulse opacity-70",
)}
>
{statusMap[status]}
</motion.p>
</AnimatePresence>
</div>
</div>
);
}