import { renderHeader } from '../components/header.js'; import { authService, state } from '../state.js'; import { readShineUserPda } from '../services/shine-user-pda-service.js'; import { resolveShineServerByServerLogin } from '../services/shine-server-resolver.js'; export const pageMeta = { id: 'access-servers-view', title: 'Серверы доступа' }; function escapeHtml(value) { return String(value || '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function normalizeLogin(value) { return String(value || '').trim().toLowerCase(); } function createConfirmModal() { const root = document.getElementById('modal-root'); if (!(root instanceof HTMLElement)) return null; const modal = document.createElement('div'); modal.className = 'modal'; modal.hidden = true; modal.innerHTML = ` `; root.append(modal); const titleEl = modal.querySelector('#access-servers-dialog-title'); const textEl = modal.querySelector('#access-servers-dialog-text'); const noteEl = modal.querySelector('#access-servers-dialog-note'); const cancelBtn = modal.querySelector('#access-servers-dialog-cancel'); const confirmBtn = modal.querySelector('#access-servers-dialog-confirm'); let onConfirm = null; let onCancel = null; const close = () => { modal.hidden = true; onConfirm = null; onCancel = null; }; modal.addEventListener('click', (event) => { if (event.target === modal) close(); }); cancelBtn?.addEventListener('click', async () => { const handler = onCancel; close(); if (typeof handler === 'function') await handler(); }); confirmBtn?.addEventListener('click', async () => { const handler = onConfirm; close(); if (typeof handler === 'function') await handler(); }); return { open({ title, text, note = '', confirmLabel = 'Да', cancelLabel = 'Нет', onConfirm: confirmHandler, onCancel: cancelHandler, }) { titleEl.textContent = String(title || 'Подтверждение'); textEl.textContent = String(text || ''); if (note) { noteEl.hidden = false; noteEl.textContent = String(note); } else { noteEl.hidden = true; noteEl.textContent = ''; } confirmBtn.textContent = String(confirmLabel || 'Да'); cancelBtn.textContent = String(cancelLabel || 'Нет'); onConfirm = confirmHandler || null; onCancel = cancelHandler || null; modal.hidden = false; }, destroy() { modal.remove(); }, }; } export function render({ navigate }) { const screen = document.createElement('section'); screen.className = 'stack'; const sessionLogin = normalizeLogin(state.session.login); const solanaEndpoint = String(state.entrySettings.solanaServer || '').trim(); let disposed = false; let currentAccessServers = []; let selectedCandidate = null; let suggestionsLoading = false; const confirmModal = createConfirmModal(); const introCard = document.createElement('div'); introCard.className = 'card stack'; introCard.innerHTML = `

Где хранятся личные данные

Серверы доступа хранят зашифрованную личную переписку пользователя и участвуют в звонках. Всё, что публикуется в блокчейне SHiNE, доступно через любой сервер Сияния, а доступ пользователя и приватная переписка хранятся только на этих серверах.

`; const listCard = document.createElement('div'); listCard.className = 'card stack'; const listTitle = document.createElement('p'); listTitle.className = 'field-label'; listTitle.textContent = 'Текущий список серверов доступа'; const listHint = document.createElement('p'); listHint.className = 'meta-muted'; listHint.textContent = sessionLogin ? `Пользователь: @${sessionLogin}` : 'В текущей сессии не найден логин пользователя.'; const listBody = document.createElement('div'); listBody.className = 'stack'; const listStatus = document.createElement('p'); listStatus.className = 'meta-muted'; listStatus.textContent = 'Загрузка данных из PDA...'; listCard.append(listTitle, listHint, listBody, listStatus); const addCard = document.createElement('div'); addCard.className = 'card stack'; const addTitle = document.createElement('p'); addTitle.className = 'field-label'; addTitle.textContent = 'Добавить сервер доступа'; const addHint = document.createElement('p'); addHint.className = 'meta-muted'; addHint.textContent = 'Введите логин сервера или несколько первых букв, затем выберите сервер из подсказок.'; const addInput = document.createElement('input'); addInput.className = 'input'; addInput.type = 'text'; addInput.autocomplete = 'off'; addInput.placeholder = 'Например: shineup'; const suggestEl = document.createElement('div'); suggestEl.className = 'profile-relative-search-suggest'; suggestEl.hidden = true; const addStatus = document.createElement('p'); addStatus.className = 'meta-muted'; addStatus.textContent = 'Добавление сервера пока работает как подтверждающая заглушка.'; const addButton = document.createElement('button'); addButton.className = 'primary-btn'; addButton.type = 'button'; addButton.textContent = 'Добавить сервер'; addButton.disabled = true; addCard.append(addTitle, addHint, addInput, suggestEl, addStatus, addButton); const setSelectedCandidate = (candidate) => { selectedCandidate = candidate; addButton.disabled = !candidate; if (candidate) { addInput.value = candidate.login; addStatus.textContent = `Выбран сервер @${candidate.login}${candidate.url ? ` (${candidate.url})` : ''}`; } else { addStatus.textContent = 'Добавление сервера пока работает как подтверждающая заглушка.'; } }; const renderServerList = () => { listBody.innerHTML = ''; if (!currentAccessServers.length) { const empty = document.createElement('p'); empty.className = 'meta-muted'; empty.textContent = 'Список серверов доступа пока пуст.'; listBody.append(empty); return; } currentAccessServers.forEach((server, index) => { const button = document.createElement('button'); button.className = 'text-btn'; button.type = 'button'; button.innerHTML = ` @${escapeHtml(server.login)} ${escapeHtml(server.url || 'URL не указан')} `; button.addEventListener('click', () => { const isLast = currentAccessServers.length <= 1; const openStub = (noteText = '') => { confirmModal?.open({ title: isLast ? 'Последний сервер доступа' : 'Отключить сервер доступа?', text: isLast ? 'Это последний сервер доступа пользователя. Если его отключить, личная переписка пользователя на серверах доступа будет удалена.' : `Хотите изменить запись в блокчейне Solana и отключить сервер доступа @${server.login}?`, note: noteText, confirmLabel: isLast ? 'Понимаю' : 'Да', cancelLabel: 'Нет', onConfirm: () => window.alert('Отключение сервера доступа пока не реализовано.'), }); }; if (isLast) { confirmModal?.open({ title: 'Отключить последний сервер?', text: `У пользователя остался только один сервер доступа: @${server.login}.`, note: 'После отключения последнего сервера доступа личная переписка пользователя на серверах доступа будет удалена.', confirmLabel: 'Продолжить', cancelLabel: 'Отмена', onConfirm: () => openStub('Это повторное предупреждение. Сейчас действие остаётся заглушкой.'), }); return; } openStub(); }); listBody.append(button); if (index < currentAccessServers.length - 1) { const divider = document.createElement('div'); divider.style.height = '1px'; divider.style.background = 'rgba(255,255,255,0.08)'; listBody.append(divider); } }); }; const loadCurrentServers = async () => { if (!sessionLogin) { currentAccessServers = []; renderServerList(); listStatus.textContent = 'Нет активной пользовательской сессии.'; return; } if (!solanaEndpoint) { currentAccessServers = []; renderServerList(); listStatus.textContent = 'Не задан Solana RPC endpoint.'; return; } listStatus.textContent = 'Читаем серверы доступа из Solana PDA...'; try { const parsed = await readShineUserPda({ login: sessionLogin, solanaEndpoint }); const logins = Array.isArray(parsed?.accessServers) ? [...new Set(parsed.accessServers.map(normalizeLogin).filter(Boolean))] : []; const rows = []; for (const login of logins) { try { const resolved = await resolveShineServerByServerLogin({ serverLogin: login, solanaEndpoint }); rows.push({ login: resolved.serverLogin, url: resolved.httpBase }); } catch { rows.push({ login, url: '' }); } } currentAccessServers = rows; renderServerList(); listStatus.textContent = rows.length ? `Найдено серверов доступа: ${rows.length}` : 'В PDA пользователя пока нет серверов доступа.'; } catch (error) { currentAccessServers = []; renderServerList(); listStatus.textContent = error?.message || 'Не удалось прочитать список серверов доступа.'; } }; const renderSuggestions = (items) => { suggestEl.innerHTML = ''; if (!items.length) { suggestEl.hidden = true; return; } suggestEl.hidden = false; suggestEl.innerHTML = items.map((item) => ( `` )).join(''); }; const loadSuggestions = async () => { const prefix = normalizeLogin(addInput.value); if (suggestionsLoading || prefix.length < 2) { suggestEl.hidden = true; suggestEl.innerHTML = ''; return; } suggestionsLoading = true; try { const logins = await authService.searchUsers(prefix, { isServer: true }); const items = []; for (const login of logins.slice(0, 6)) { try { const resolved = await resolveShineServerByServerLogin({ serverLogin: login, solanaEndpoint }); items.push({ login: resolved.serverLogin, url: resolved.httpBase }); } catch { // если server PDA битая, пропускаем подсказку } } if (!disposed) renderSuggestions(items); } catch (error) { if (!disposed) { suggestEl.hidden = true; suggestEl.innerHTML = ''; addStatus.textContent = error?.message || 'Не удалось получить список серверов.'; } } finally { suggestionsLoading = false; } }; addInput.addEventListener('input', () => { setSelectedCandidate(null); void loadSuggestions(); }); addInput.addEventListener('focus', () => { void loadSuggestions(); }); suggestEl.addEventListener('click', (event) => { const target = event.target instanceof HTMLElement ? event.target.closest('[data-login]') : null; if (!(target instanceof HTMLElement)) return; setSelectedCandidate({ login: String(target.dataset.login || ''), url: String(target.dataset.url || ''), }); suggestEl.hidden = true; suggestEl.innerHTML = ''; }); addButton.addEventListener('click', async () => { const login = normalizeLogin(selectedCandidate?.login || addInput.value); if (!login) { setSelectedCandidate(null); addStatus.textContent = 'Сначала укажите логин сервера доступа.'; return; } try { const resolved = await resolveShineServerByServerLogin({ serverLogin: login, solanaEndpoint }); confirmModal?.open({ title: 'Добавить сервер доступа?', text: `Вы хотите добавить сервер доступа @${resolved.serverLogin}?`, note: resolved.httpBase ? `Адрес сервера: ${resolved.httpBase}` : '', onConfirm: () => window.alert('Добавление сервера доступа пока не реализовано.'), }); } catch (error) { addStatus.textContent = error?.message || 'Не удалось проверить выбранный сервер.'; } }); screen.append( renderHeader({ title: 'Серверы доступа', leftAction: { label: '←', onClick: () => navigate('settings-view') }, }), introCard, listCard, addCard, ); void loadCurrentServers(); screen.cleanup = () => { disposed = true; confirmModal?.destroy(); }; return screen; }