Add seed/Telegram login button and traffic usage indicator

New non-blocking login panel on the Home screen (seed backup/restore +
Telegram magic-link via the bot deep-link, polled through the new backend
endpoints) — uses its own popover instead of a full-screen Dialog so the
burger menu stays clickable while it's open. VpnStats now shows a used/limit
usage bar backed by a new useUsageStore.

Also registers tauri-plugin-shell (needed to open the t.me deep-link) and
threads an optional auth token from the app down through TunnelConfig to the
proxy client (desktop + Android), for instances running --require-auth.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-07 00:31:27 +07:00
parent bf50824833
commit 0cb6ad2860
17 changed files with 596 additions and 26 deletions
+73
View File
@@ -140,3 +140,76 @@ export async function fetchNodes(): Promise<ServerNode[]> {
const data = (await res.json()) as RawVpnNode[];
return data.map(mapNode);
}
/** Текущий JWT, если уже есть (для передачи прокси на `--require-auth` инстансах). */
export function getCachedToken(): string | null {
const cached = localStorage.getItem(TOKEN_KEY);
return isTokenValid(cached) ? cached : null;
}
/** Гарантирует, что seed/токен существуют (Ghost Protocol), и возвращает seed для бэкапа. */
export async function getCurrentSeed(): Promise<string> {
await ensureToken();
const seed = localStorage.getItem(SEED_KEY);
if (!seed) {
// Юзер залогинен через Telegram (seed никогда не выдавался) — сгенерировать
// отдельный seed поверх текущей сессии сервер сейчас не умеет, только через
// новую анонимную регистрацию, что разлогинило бы текущий аккаунт.
throw new Error("NO_SEED_FOR_ACCOUNT");
}
return seed;
}
/** Вход по чужому/сохранённому seed (например, с другого устройства). */
export async function loginWithSeed(seed: string): Promise<void> {
await loginSeed(seed);
localStorage.setItem(SEED_KEY, seed);
}
export interface TelegramLoginSession {
authCode: string;
deepLink: string;
}
/** Создаёт ожидающую подтверждения сессию для входа через Telegram-бота (magic-link). */
export async function startTelegramLogin(): Promise<TelegramLoginSession> {
const res = await fetch(`${API_V1}/auth/telegram/session`, { method: "POST" });
if (!res.ok) throw new Error(`telegram/session failed: ${res.status}`);
const data = (await res.json()) as { auth_code: string; deep_link: string };
return { authCode: data.auth_code, deepLink: data.deep_link };
}
/** Опрашивает статус подтверждения; `true` — юзер подтвердил вход в боте. */
export async function pollTelegramLogin(authCode: string): Promise<boolean> {
const res = await fetch(`${API_V1}/auth/telegram/session/${authCode}`);
if (res.status === 401 || res.status === 410) {
throw new Error("TELEGRAM_LOGIN_EXPIRED");
}
if (!res.ok) throw new Error(`telegram/session poll failed: ${res.status}`);
const data = (await res.json()) as { status: string; token?: string };
if (data.status === "success" && data.token) {
localStorage.setItem(TOKEN_KEY, data.token);
localStorage.removeItem(SEED_KEY);
return true;
}
return false;
}
export interface UsageInfo {
usedBytes: number;
limitBytes: number | null;
}
/** Текущий расход/лимит трафика юзера — для индикатора на `VpnStats`. */
export async function fetchUsage(): Promise<UsageInfo> {
const token = await ensureToken();
const res = await fetch(`${API_V1}/users/me`, {
headers: { Authorization: `Bearer ${token}` },
});
if (!res.ok) throw new Error(`GET /users/me failed: ${res.status}`);
const data = (await res.json()) as {
data_used_bytes: number;
data_limit_bytes: number | null;
};
return { usedBytes: data.data_used_bytes, limitBytes: data.data_limit_bytes };
}