// ============================================ // MariusVPN — api helper, mock data, icons // Exposes on window: api, MOCK, ICON, FMT // ============================================ const API_BASE = "/api/cabinet"; // Real backend (served by FastAPI). Flip to true only for standalone // design preview without a backend. const MOCK = false; // CSRF function getCsrf() { return document.cookie.match(/csrf=([^;]+)/)?.[1] || ''; } // ============================================ // MOCK DATA (mutable so writes feel realistic) // ============================================ const M = { me: { user: { id: 'u_47102', name: 'Алексей Кузнецов', telegram: '@alexei_k', initials: 'АК' }, subscription: { status: 'active', // 'active' | 'expired' | 'trial' plan_id: 'pro_12m', plan_name: 'PRO', period_label: '12 месяцев', price_per_month: 249, currency: '₽', started_at: '2026-01-17', expires_at: '2027-01-17', days_left: 247, total_days: 365, auto_renew: true, }, limits: { devices_used: 3, devices_max: 5, extra_device_price: 99, extra_device_currency: '₽', }, balance: { amount: 320, currency: '₽' }, }, traffic: { total: { up_gb: 28.4, down_gb: 142.7 }, bypass: { used_gb: 87.3, quota_gb: 150, base_cap_gb: 100, extra_gb: 50, price_per_gb: 19, currency: '₽' }, devices: [ { id: 'd1', name: 'iPhone 15 Pro', up_gb: 12.1, down_gb: 88.4 }, { id: 'd2', name: 'MacBook Pro 14"', up_gb: 14.2, down_gb: 48.6 }, { id: 'd3', name: 'iPad Air', up_gb: 2.1, down_gb: 5.7 }, ], }, plans: [ { id: 'pro_1m', name: '1 месяц', period_months: 1, price: 390, per_month: 390, currency: '₽', popular: false, discount_pct: 0 }, { id: 'pro_6m', name: '6 месяцев', period_months: 6, price: 1890, per_month: 315, currency: '₽', popular: false, discount_pct: 19 }, { id: 'pro_12m', name: '12 месяцев', period_months: 12, price: 2990, per_month: 249, currency: '₽', popular: true, discount_pct: 36 }, { id: 'pro_24m', name: '24 месяца', period_months: 24, price: 4790, per_month: 199, currency: '₽', popular: false, discount_pct: 49 }, ], payment_methods: [ { id: 'card', name: 'Банковская карта', description: 'Visa, Mastercard, МИР', enabled: true, icon: 'card' }, { id: 'sbp', name: 'СБП', description: 'Через любой банк РФ', enabled: true, icon: 'sbp' }, { id: 'crypto', name: 'Криптовалюта', description: 'USDT, BTC, ETH', enabled: true, icon: 'crypto' }, { id: 'apple', name: 'Apple Pay', description: 'Временно недоступно', enabled: false, icon: 'apple' }, ], payments_history: [ { id: 'p_2026_01', date: '2026-01-17', amount: 2990, currency: '₽', method: 'Карта •• 4421', status: 'paid', description: 'Подписка PRO · 12 месяцев' }, { id: 'p_2025_12', date: '2025-12-17', amount: 390, currency: '₽', method: 'СБП', status: 'paid', description: 'Подписка PRO · 1 месяц' }, { id: 'p_2025_11', date: '2025-11-22', amount: 950, currency: '₽', method: 'Карта •• 4421', status: 'pending', description: 'Докупка трафика · 50 ГБ' }, { id: 'p_2025_10', date: '2025-10-17', amount: 390, currency: '₽', method: 'Карта •• 4421', status: 'paid', description: 'Подписка PRO · 1 месяц' }, { id: 'p_2025_09', date: '2025-09-30', amount: 390, currency: '₽', method: 'Карта •• 4421', status: 'cancelled', description: 'Подписка PRO · 1 месяц' }, ], devices: [ { id: 'd1', name: 'iPhone 15 Pro', platform: 'ios', hwid: 'A7B3-4D2F-9E1C', online: true, last_activity: 'сейчас' }, { id: 'd2', name: 'MacBook Pro 14"', platform: 'macos', hwid: '8C4E-1F7A-3B2D', online: true, last_activity: '12 минут назад' }, { id: 'd3', name: 'iPad Air', platform: 'ios', hwid: '2D9F-6E1B-4A8C', online: false, last_activity: 'вчера, 21:14' }, ], referral: { code: 'ALEXEIK7M', link: 'https://mariusvpn.com/r/ALEXEIK7M', invited_count: 3, paid_count: 2, balance: 90, balance_unit: 'дней', rule_text: 'Пригласите друга по своей ссылке. Когда он оплатит первый месяц, вы получите +30 дней к подписке, а друг — первый месяц бесплатно. Бонус начисляется автоматически, ограничений по количеству приглашений нет.', }, help: { platforms: [ { id: 'ios', name: 'iPhone / iPad', steps: [ { n: 1, text: 'Скачайте Happ или v2RayTun в App Store — это бесплатно.' }, { n: 2, text: 'Откройте раздел «Устройства» в личном кабинете и нажмите «Добавить устройство».' }, { n: 3, text: 'Скопируйте ссылку-подписку или отсканируйте QR-код в приложении.' }, { n: 4, text: 'Выберите любой сервер из списка и нажмите «Подключить».' }, ]}, { id: 'android', name: 'Android', steps: [ { n: 1, text: 'Установите v2RayTun, Hiddify или Happ из Google Play.' }, { n: 2, text: 'Откройте раздел «Устройства» и нажмите «Добавить устройство».' }, { n: 3, text: 'Вставьте ссылку-подписку в приложение или импортируйте через QR.' }, { n: 4, text: 'Выберите сервер и нажмите «Подключить».' }, ]}, { id: 'windows', name: 'Windows', steps: [ { n: 1, text: 'Скачайте Hiddify-Next или Nekobox с официального сайта.' }, { n: 2, text: 'В личном кабинете создайте устройство и скопируйте ссылку-подписку.' }, { n: 3, text: 'Вставьте ссылку в Hiddify и нажмите «Добавить».' }, { n: 4, text: 'Выберите сервер и подключитесь.' }, ]}, { id: 'macos', name: 'macOS', steps: [ { n: 1, text: 'Установите Streisand или Hiddify-Next через App Store или официальный сайт.' }, { n: 2, text: 'Добавьте устройство в кабинете и получите ссылку-подписку.' }, { n: 3, text: 'Импортируйте подписку в клиент (Subscription → Add).' }, { n: 4, text: 'Выберите сервер и нажмите «Подключить».' }, ]}, ], faq: [ { q: 'Что делать, если VPN не подключается?', a: 'Проверьте, что устройство активно в кабинете и подписка не истекла. Попробуйте сменить сервер в клиенте, переустановить конфиг. Если не помогло — напишите в поддержку.' }, { q: 'Сколько устройств можно подключить?', a: 'На тарифе PRO — до 5 устройств. Каждое дополнительное устройство стоит 99 ₽/месяц.' }, { q: 'Как отвязать устройство?', a: 'В разделе «Устройства» нажмите на нужное устройство и выберите «Удалить». Конфиг будет аннулирован сразу.' }, { q: 'Можно ли вернуть деньги?', a: 'Возврат возможен в течение 7 дней после первой оплаты при условии, что использовано менее 5 ГБ трафика. Напишите в поддержку для оформления.' }, { q: 'Что такое «сервер обхода»?', a: 'Это специальная категория серверов для регионов с блокировками. У них отдельный лимит трафика; превышение можно докупить пакетами.' }, ], support_url: 'https://t.me/marius_support', }, }; // fake VLESS config + QR function fakeConfigFor(device) { const vless = `vless://${device.id}-uuid-mock@de-fra-01.example.com:443?encryption=none&security=reality&type=tcp&sni=cloudflare.com&pbk=fake_pub_key&fp=chrome#${encodeURIComponent('VPN — ' + device.name)}`; const sub_url = `https://mariusvpn.com/sub/${device.id}-${device.hwid.replace(/-/g, '').toLowerCase()}`; return { vless, sub_url, qr_data: sub_url }; } // ============================================ // MOCK RESOLVER // ============================================ function mockResolve(path, opts = {}) { const method = opts.method || 'GET'; const body = opts.body || {}; // GET endpoints if (method === 'GET') { if (path === '/me') return clone(M.me); if (path === '/traffic') return clone(M.traffic); if (path === '/plans') return clone(M.plans); if (path === '/payments/methods') return clone(M.payment_methods); if (path === '/payments/history') return clone(M.payments_history); if (path === '/devices') return clone(M.devices); if (path === '/referral') return clone(M.referral); if (path === '/help') return clone(M.help); // GET /devices/{id}/config const cfg = path.match(/^\/devices\/([^/]+)\/config$/); if (cfg) { const dev = M.devices.find(d => d.id === cfg[1]); if (!dev) throw new Error('device not found'); return fakeConfigFor(dev); } } // POST /login if (method === 'POST' && path === '/login') { return { ok: true, user: clone(M.me.user) }; } // POST /subscription/auto-renew if (method === 'POST' && path === '/subscription/auto-renew') { M.me.subscription.auto_renew = !!body.enabled; return { ok: true, auto_renew: M.me.subscription.auto_renew }; } // POST /subscription/renew if (method === 'POST' && path === '/subscription/renew') { return { ok: true, payment_url: 'https://pay.mariusvpn.com/mock?order=ord_' + Date.now() }; } // POST /payments/topup if (method === 'POST' && path === '/payments/topup') { return { ok: true, payment_url: 'https://pay.mariusvpn.com/mock?order=top_' + Date.now() }; } // POST /traffic/buy-gb if (method === 'POST' && path === '/traffic/buy-gb') { M.traffic.bypass.extra_gb += body.gb || 0; M.traffic.bypass.quota_gb = M.traffic.bypass.base_cap_gb + M.traffic.bypass.extra_gb; return { ok: true, quota_gb: M.traffic.bypass.quota_gb }; } // POST /devices if (method === 'POST' && path === '/devices') { const id = 'd' + (M.devices.length + 1 + Math.floor(Math.random() * 1000)); const hwid = Array.from({ length: 3 }, () => Math.random().toString(16).slice(2, 6).toUpperCase()).join('-'); const dev = { id, name: body.name || 'Новое устройство', platform: body.platform || 'ios', hwid, online: false, last_activity: 'только что' }; M.devices.push(dev); M.me.limits.devices_used = M.devices.length; return { ok: true, device: clone(dev), config: fakeConfigFor(dev) }; } // PATCH /devices/{id} const patchDev = path.match(/^\/devices\/([^/]+)$/); if (method === 'PATCH' && patchDev) { const dev = M.devices.find(d => d.id === patchDev[1]); if (!dev) throw new Error('device not found'); Object.assign(dev, body); return { ok: true, device: clone(dev) }; } // DELETE /devices/{id} if (method === 'DELETE' && patchDev) { M.devices = M.devices.filter(d => d.id !== patchDev[1]); M.me.limits.devices_used = M.devices.length; return { ok: true }; } console.warn('MOCK: no handler for', method, path); return { ok: false, error: 'unhandled' }; } function clone(x) { return JSON.parse(JSON.stringify(x)); } // ============================================ // API // ============================================ async function api(path, opts = {}) { if (MOCK) { await new Promise(r => setTimeout(r, 180 + Math.random() * 220)); return mockResolve(path, opts); } const headers = { 'Content-Type': 'application/json', 'X-CSRF': getCsrf(), ...(opts.headers || {}) }; const init = { method: opts.method || 'GET', credentials: 'include', headers }; if (opts.body) init.body = JSON.stringify(opts.body); const res = await fetch(API_BASE + path, init); if (!res.ok) throw new Error(`API ${path} → ${res.status}`); return res.json(); } // ============================================ // FORMATTERS // ============================================ const FMT = { money: (amount, currency = '₽') => `${new Intl.NumberFormat('ru-RU').format(amount)} ${currency}`, gb: (n) => `${(+n).toFixed(1).replace(/\.0$/, '')} ГБ`, date: (iso) => { const d = new Date(iso); return d.toLocaleDateString('ru-RU', { day: 'numeric', month: 'long', year: 'numeric' }); }, dateShort: (iso) => { const d = new Date(iso); return d.toLocaleDateString('ru-RU', { day: 'numeric', month: 'short', year: '2-digit' }); }, declOf: (n, forms) => { const a = Math.abs(n) % 100, b = a % 10; if (a > 10 && a < 20) return forms[2]; if (b > 1 && b < 5) return forms[1]; if (b === 1) return forms[0]; return forms[2]; }, daysWord: (n) => FMT.declOf(n, ['день', 'дня', 'дней']), }; // ============================================ // ICONS (lucide-style, 24×24, stroke 1.7) // ============================================ const Icon = ({ name, size = 20, className = '', strokeWidth = 1.7 }) => { const paths = ICON_PATHS[name]; if (!paths) return null; return ( ); }; const ICON_PATHS = { home: <>, shield: <>, shieldCheck: <>, crown: <>, device: <>, chart: <>, card: <>, users: <>, help: <>, menu: <>, more: <>, close: <>, chevR: <>, chevD: <>, chevU: <>, copy: <>, qr: <>, check: <>, plus: <>, download: <>, trash: <>, edit: <>, bell: <>, link: <>, telegram: <>, arrowUp: <>, arrowDown: <>, arrowRight: <>, zap: <>, gift: <>, globe: <>, settings: <>, logout: <>, refresh: <>, // platforms apple: <>, android: <>, windows: <>, linux: <>, }; // platform → human + icon name const PLATFORM_LABEL = { ios: 'iOS', android: 'Android', windows: 'Windows', macos: 'macOS', linux: 'Linux' }; const PLATFORM_ICON = { ios: 'apple', android: 'android', windows: 'windows', macos: 'apple', linux: 'linux' }; Object.assign(window, { api, MOCK, Icon, ICON_PATHS, FMT, PLATFORM_LABEL, PLATFORM_ICON, getCsrf });