🔒 Аудит: безопасность, TypeScript, UI, BottomNav

Безопасность:
- proxy: белый список путей (только /sub/*), POST заблокирован
- console.log заменён на logger (утечки URL/данных)
- OnboardingFlow: убраны --tg-theme-* (не существуют в проекте)

TypeScript (0 ошибок):
- tsconfig target es5→es2017 (regex /u flag fix)
- layout.tsx: viewport перенесён в metadata (Next.js 13.5)
- telegram-webhook: fix text possibly undefined
- hooks/useTelegramWebApp: fix Object possibly undefined
- types/telegram: убрана дублирующая Window декларация

UI:
- BottomNav: новый компонент (Назад/Главная/Помощь)
- safe-area-bottom CSS класс добавлен в globals.css
- dashboard: spacer h-20, toast поднят над BottomNav
- OnboardingFlow: цены 149/249/350₽ (были 200/350/500₽)

Очистка:
- page_NEW.tsx удалён локально (не был в git)
This commit is contained in:
Umbrix Dev
2026-02-08 18:59:02 +03:00
parent 3dffb35eaa
commit 33b20b979f
21 changed files with 1446 additions and 266 deletions

304
app/dashboard/page.tsx Normal file
View File

@@ -0,0 +1,304 @@
// app/dashboard/page.tsx - Main dashboard for existing users
// Shows subscription status, QR code, referral stats, quick actions
'use client';
import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import QRCodeModal from '@/components/QRCodeModal';
import ReferralModal from '@/components/ReferralModal';
import { marzbanApi } from '@/lib/marzban-api';
import { getSubscriptionUrl } from '@/lib/constants';
import {
Shield,
Settings,
Gift,
HelpCircle,
QrCode,
Copy,
Share2,
CheckCircle,
AlertCircle,
Clock,
Users,
} from 'lucide-react';
export default function Dashboard() {
const router = useRouter();
const [isLoading, setIsLoading] = useState(true);
const [subscriptionToken, setSubscriptionToken] = useState<string | null>(null);
const [subscriptionStatus, setSubscriptionStatus] = useState<'active' | 'expired' | 'trial'>('active');
const [expiryDate, setExpiryDate] = useState<string>('');
const [daysRemaining, setDaysRemaining] = useState<number>(0);
const [username, setUsername] = useState<string>('');
const [isQROpen, setIsQROpen] = useState(false);
const [isReferralOpen, setIsReferralOpen] = useState(false);
const [showToast, setShowToast] = useState(false);
const [toastMessage, setToastMessage] = useState('');
const [referralCount, setReferralCount] = useState(0);
const [bonusDays, setBonusDays] = useState(0);
const subscriptionUrl = subscriptionToken ? getSubscriptionUrl(subscriptionToken) : '';
useEffect(() => {
loadUserData();
}, []);
const loadUserData = async () => {
try {
// Get Telegram data
const telegramWebApp = (window as any).Telegram?.WebApp;
const telegramId = telegramWebApp?.initDataUnsafe?.user?.id;
const telegramUsername = telegramWebApp?.initDataUnsafe?.user?.username;
if (!telegramId && !telegramUsername) {
console.log('❌ No Telegram data - redirecting to home');
router.push('/');
return;
}
// Load subscription
const params = new URLSearchParams();
if (telegramId) params.append('telegramId', telegramId.toString());
if (telegramUsername) params.append('telegramUsername', telegramUsername);
const response = await fetch(`/api/user-subscription?${params.toString()}`);
const data = await response.json();
if (!data.success || !data.hasSubscription) {
console.log('❌ No subscription - redirecting to home');
router.push('/');
return;
}
setSubscriptionToken(data.token);
setUsername(data.username);
setSubscriptionStatus(data.status === 'active' ? 'active' : 'expired');
if (data.expire) {
setExpiryDate(marzbanApi.formatExpireDate(data.expire));
// Calculate days remaining
const expireTimestamp = data.expire * 1000;
const now = Date.now();
const diff = expireTimestamp - now;
const days = Math.ceil(diff / (1000 * 60 * 60 * 24));
setDaysRemaining(days > 0 ? days : 0);
}
// Load referral stats
const referralResponse = await fetch(`/api/referral/stats?username=${data.username}`);
const referralData = await referralResponse.json();
if (referralData.success) {
setReferralCount(referralData.referral_count || 0);
setBonusDays(referralData.bonus_days_earned || 0);
}
} catch (error) {
console.error('Failed to load user data:', error);
router.push('/');
} finally {
setIsLoading(false);
}
};
const showToastNotification = (message: string) => {
setToastMessage(message);
setShowToast(true);
setTimeout(() => setShowToast(false), 3000);
};
const copyToClipboard = async (text: string) => {
try {
await navigator.clipboard.writeText(text);
showToastNotification('✅ Скопировано в буфер обмена!');
} catch (err) {
console.error('Failed to copy:', err);
showToastNotification('❌ Ошибка копирования');
}
};
const shareReferralLink = async () => {
const botUsername = process.env.NEXT_PUBLIC_TELEGRAM_BOT_USERNAME || 'Dorod_vps_bot';
const referralUrl = `https://t.me/${botUsername}?start=ref_${username}`;
const telegramWebApp = (window as any).Telegram?.WebApp;
if (telegramWebApp?.openTelegramLink) {
const shareText = encodeURIComponent(`🚀 Попробуй Umbrix VPN! Получи 7 дней бесплатно по моей ссылке:\n${referralUrl}`);
telegramWebApp.openTelegramLink(`https://t.me/share/url?url=${referralUrl}&text=${shareText}`);
} else {
copyToClipboard(referralUrl);
}
};
if (isLoading) {
return (
<div className="min-h-screen bg-gradient-to-b from-slate-900 via-slate-800 to-slate-900 text-white flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-16 w-16 border-b-4 border-blue-500 mx-auto mb-6"></div>
<p className="text-slate-400">Загрузка...</p>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-gradient-to-b from-slate-900 via-slate-800 to-slate-900 text-white">
{/* Header */}
<header className="border-b border-slate-700 bg-slate-900/50 backdrop-blur-sm sticky top-0 z-50">
<div className="max-w-7xl mx-auto px-4 py-4 flex items-center justify-between">
<h1 className="text-xl font-bold">🚀 Umbrix VPN</h1>
<Link href="/help">
<button className="p-2 hover:bg-slate-700 rounded-lg transition-colors">
<HelpCircle className="h-6 w-6" />
</button>
</Link>
</div>
</header>
{/* Main Content */}
<main className="max-w-7xl mx-auto px-4 py-8 space-y-6">
{/* Subscription Status Card */}
<div className="bg-slate-800/50 border border-slate-700 rounded-lg p-6">
<div className="flex items-start justify-between mb-4">
<div>
<h2 className="text-2xl font-bold mb-2">Ваша подписка</h2>
<p className="text-slate-400">@{username}</p>
</div>
{subscriptionStatus === 'active' ? (
<div className="flex items-center gap-2 px-3 py-1 bg-green-600/20 border border-green-500 rounded-full">
<CheckCircle className="h-4 w-4 text-green-400" />
<span className="text-sm font-medium">Активна</span>
</div>
) : (
<div className="flex items-center gap-2 px-3 py-1 bg-red-600/20 border border-red-500 rounded-full">
<AlertCircle className="h-4 w-4 text-red-400" />
<span className="text-sm font-medium">Истекла</span>
</div>
)}
</div>
<div className="grid grid-cols-2 gap-4">
<div className="bg-slate-900/50 rounded-lg p-4">
<div className="flex items-center gap-2 mb-2">
<Clock className="h-5 w-5 text-blue-400" />
<span className="text-sm text-slate-400">Осталось дней</span>
</div>
<p className="text-3xl font-bold">{daysRemaining}</p>
<p className="text-sm text-slate-400 mt-1">{expiryDate}</p>
</div>
<div className="bg-slate-900/50 rounded-lg p-4">
<div className="flex items-center gap-2 mb-2">
<Users className="h-5 w-5 text-purple-400" />
<span className="text-sm text-slate-400">Рефералы</span>
</div>
<p className="text-3xl font-bold">{referralCount}</p>
<p className="text-sm text-slate-400 mt-1">+{bonusDays} дней заработано</p>
</div>
</div>
</div>
{/* Quick Actions */}
<div className="grid grid-cols-2 gap-4">
<button
onClick={() => setIsQROpen(true)}
className="bg-slate-800/50 hover:bg-slate-700 border border-slate-700 rounded-lg p-6 flex flex-col items-center gap-3 transition-colors"
>
<QrCode className="h-8 w-8 text-blue-500" />
<span className="font-semibold">QR Код</span>
</button>
<button
onClick={() => copyToClipboard(subscriptionUrl)}
className="bg-slate-800/50 hover:bg-slate-700 border border-slate-700 rounded-lg p-6 flex flex-col items-center gap-3 transition-colors"
>
<Copy className="h-8 w-8 text-green-500" />
<span className="font-semibold">Копировать ссылку</span>
</button>
<button
onClick={shareReferralLink}
className="bg-slate-800/50 hover:bg-slate-700 border border-slate-700 rounded-lg p-6 flex flex-col items-center gap-3 transition-colors"
>
<Share2 className="h-8 w-8 text-purple-500" />
<span className="font-semibold">Пригласить друга</span>
</button>
<Link href="/referral">
<button className="w-full bg-slate-800/50 hover:bg-slate-700 border border-slate-700 rounded-lg p-6 flex flex-col items-center gap-3 transition-colors">
<Gift className="h-8 w-8 text-yellow-500" />
<span className="font-semibold">Реферальная программа</span>
</button>
</Link>
</div>
{/* Referral Progress (if has referrals) */}
{referralCount > 0 && (
<div className="bg-gradient-to-r from-purple-600/20 to-blue-600/20 border border-purple-500/50 rounded-lg p-6">
<h3 className="text-lg font-bold mb-4">🎉 Прогресс реферальной программы</h3>
<div className="space-y-3">
<div>
<div className="flex items-center justify-between mb-2">
<span className="text-sm text-slate-300">До следующего бонуса</span>
<span className="text-sm font-bold">{referralCount}/5</span>
</div>
<div className="h-3 bg-slate-900/50 rounded-full overflow-hidden">
<div
className="h-full bg-gradient-to-r from-purple-500 to-blue-500 transition-all"
style={{ width: `${(referralCount / 5) * 100}%` }}
/>
</div>
</div>
<p className="text-sm text-slate-300">
{referralCount >= 5
? '🎁 Вы получили месяц в подарок!'
: `Пригласите еще ${5 - referralCount} друзей и получите месяц бесплатно!`}
</p>
</div>
</div>
)}
{/* Setup Guide Link */}
<Link href="/setup">
<div className="bg-slate-800/50 border border-slate-700 rounded-lg p-6 flex items-center justify-between hover:bg-slate-700 transition-colors">
<div className="flex items-center gap-4">
<Shield className="h-8 w-8 text-blue-500" />
<div>
<h3 className="font-bold">Инструкция по настройке</h3>
<p className="text-sm text-slate-400">Как подключить VPN на вашем устройстве</p>
</div>
</div>
<Settings className="h-6 w-6 text-slate-400" />
</div>
</Link>
{/* Spacer для BottomNav */}
<div className="h-20" />
</main>
{/* Modals */}
{isQROpen && subscriptionUrl && (
<QRCodeModal isOpen={isQROpen} url={subscriptionUrl} onClose={() => setIsQROpen(false)} />
)}
{isReferralOpen && (
<ReferralModal
isOpen={isReferralOpen}
onClose={() => setIsReferralOpen(false)}
referralUrl={`https://t.me/${process.env.NEXT_PUBLIC_TELEGRAM_BOT_USERNAME || 'Dorod_vps_bot'}?start=ref_${username}`}
onShare={shareReferralLink}
onCopy={() => copyToClipboard(`https://t.me/${process.env.NEXT_PUBLIC_TELEGRAM_BOT_USERNAME || 'Dorod_vps_bot'}?start=ref_${username}`)}
/>
)}
{/* Toast */}
{showToast && (
<div className="fixed bottom-24 left-1/2 -translate-x-1/2 bg-green-600 text-white px-6 py-3 rounded-lg shadow-lg z-50 animate-fade-in-up">
{toastMessage}
</div>
)}
</div>
);
}