SHA256
UI и API: экран серверов доступа и фильтр SearchUsers
This commit is contained in:
@@ -154,6 +154,11 @@ public final class CurrentUsersDAO {
|
|||||||
|
|
||||||
/** Поиск по префиксу с внешним соединением. Соединение НЕ закрывает. */
|
/** Поиск по префиксу с внешним соединением. Соединение НЕ закрывает. */
|
||||||
public List<CurrentUserEntry> searchByLoginPrefix(Connection c, String prefix) throws SQLException {
|
public List<CurrentUserEntry> searchByLoginPrefix(Connection c, String prefix) throws SQLException {
|
||||||
|
return searchByLoginPrefix(c, prefix, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Поиск по префиксу с optional-фильтром server PDA. Соединение НЕ закрывает. */
|
||||||
|
public List<CurrentUserEntry> searchByLoginPrefix(Connection c, String prefix, Boolean isServer) throws SQLException {
|
||||||
String sql = """
|
String sql = """
|
||||||
SELECT
|
SELECT
|
||||||
login,
|
login,
|
||||||
@@ -163,6 +168,7 @@ public final class CurrentUsersDAO {
|
|||||||
client_key
|
client_key
|
||||||
FROM %s
|
FROM %s
|
||||||
WHERE LOWER(login) LIKE ?
|
WHERE LOWER(login) LIKE ?
|
||||||
|
AND (? IS NULL OR is_server = ?)
|
||||||
ORDER BY login
|
ORDER BY login
|
||||||
LIMIT 5
|
LIMIT 5
|
||||||
""".formatted(CurrentUsersSql.usersSubquery("su"));
|
""".formatted(CurrentUsersSql.usersSubquery("su"));
|
||||||
@@ -171,6 +177,13 @@ public final class CurrentUsersDAO {
|
|||||||
|
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
ps.setString(1, prefix.toLowerCase() + "%");
|
ps.setString(1, prefix.toLowerCase() + "%");
|
||||||
|
if (isServer == null) {
|
||||||
|
ps.setNull(2, Types.BOOLEAN);
|
||||||
|
ps.setNull(3, Types.BOOLEAN);
|
||||||
|
} else {
|
||||||
|
ps.setBoolean(2, isServer);
|
||||||
|
ps.setBoolean(3, isServer);
|
||||||
|
}
|
||||||
try (ResultSet rs = ps.executeQuery()) {
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
while (rs.next()) result.add(mapRow(rs));
|
while (rs.next()) result.add(mapRow(rs));
|
||||||
}
|
}
|
||||||
@@ -181,8 +194,13 @@ public final class CurrentUsersDAO {
|
|||||||
|
|
||||||
/** Поиск по префиксу без внешнего соединения. Сам открывает/закрывает. */
|
/** Поиск по префиксу без внешнего соединения. Сам открывает/закрывает. */
|
||||||
public List<CurrentUserEntry> searchByLoginPrefix(String prefix) throws SQLException {
|
public List<CurrentUserEntry> searchByLoginPrefix(String prefix) throws SQLException {
|
||||||
|
return searchByLoginPrefix(prefix, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Поиск по префиксу без внешнего соединения с optional-фильтром server PDA. */
|
||||||
|
public List<CurrentUserEntry> searchByLoginPrefix(String prefix, Boolean isServer) throws SQLException {
|
||||||
try (Connection c = db.getConnection()) {
|
try (Connection c = db.getConnection()) {
|
||||||
return searchByLoginPrefix(c, prefix);
|
return searchByLoginPrefix(c, prefix, isServer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ public final class CurrentUsersSql {
|
|||||||
current_users.blockchain_name AS blockchain_name,
|
current_users.blockchain_name AS blockchain_name,
|
||||||
current_users.client_key AS solana_key,
|
current_users.client_key AS solana_key,
|
||||||
current_users.blockchain_key AS blockchain_key,
|
current_users.blockchain_key AS blockchain_key,
|
||||||
current_users.client_key AS client_key
|
current_users.client_key AS client_key,
|
||||||
|
current_users.is_server AS is_server
|
||||||
FROM solana_user_pda_current current_users
|
FROM solana_user_pda_current current_users
|
||||||
) %s
|
) %s
|
||||||
""".formatted(alias);
|
""".formatted(alias);
|
||||||
|
|||||||
+3
-2
@@ -35,10 +35,11 @@ public class Net_SearchUsers_Handler implements JsonMessageHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String prefix = req.getPrefix().trim();
|
String prefix = req.getPrefix().trim();
|
||||||
|
Boolean isServer = req.getIsServer();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
CurrentUsersDAO dao = CurrentUsersDAO.getInstance();
|
CurrentUsersDAO dao = CurrentUsersDAO.getInstance();
|
||||||
List<CurrentUserEntry> users = dao.searchByLoginPrefix(prefix); // case-insensitive + LIMIT 5
|
List<CurrentUserEntry> users = dao.searchByLoginPrefix(prefix, isServer); // case-insensitive + LIMIT 5
|
||||||
|
|
||||||
List<String> logins = new ArrayList<>();
|
List<String> logins = new ArrayList<>();
|
||||||
for (CurrentUserEntry u : users) {
|
for (CurrentUserEntry u : users) {
|
||||||
@@ -53,7 +54,7 @@ public class Net_SearchUsers_Handler implements JsonMessageHandler {
|
|||||||
resp.setStatus(WireCodes.Status.OK);
|
resp.setStatus(WireCodes.Status.OK);
|
||||||
resp.setLogins(logins);
|
resp.setLogins(logins);
|
||||||
|
|
||||||
log.info("✅ SearchUsers ok: prefix='{}' -> {}", prefix, logins.size());
|
log.info("✅ SearchUsers ok: prefix='{}', isServer={} -> {}", prefix, isServer, logins.size());
|
||||||
return resp;
|
return resp;
|
||||||
|
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
|
|||||||
+3
@@ -18,7 +18,10 @@ import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
|||||||
public class Net_SearchUsers_Request extends Net_Request {
|
public class Net_SearchUsers_Request extends Net_Request {
|
||||||
|
|
||||||
private String prefix;
|
private String prefix;
|
||||||
|
private Boolean isServer;
|
||||||
|
|
||||||
public String getPrefix() { return prefix; }
|
public String getPrefix() { return prefix; }
|
||||||
public void setPrefix(String prefix) { this.prefix = prefix; }
|
public void setPrefix(String prefix) { this.prefix = prefix; }
|
||||||
|
public Boolean getIsServer() { return isServer; }
|
||||||
|
public void setIsServer(Boolean isServer) { this.isServer = isServer; }
|
||||||
}
|
}
|
||||||
+2
-2
@@ -1,2 +1,2 @@
|
|||||||
client.version=1.2.355
|
client.version=1.2.356
|
||||||
server.version=1.2.340
|
server.version=1.2.341
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
Сейчас здесь два метода:
|
Сейчас здесь два метода:
|
||||||
|
|
||||||
- `GetUser` — временная серверная проверка существования пользователя и чтение его базовых данных;
|
- `GetUser` — временная серверная проверка существования пользователя и чтение его базовых данных;
|
||||||
- `SearchUsers` — dev/test поиск логинов по префиксу.
|
- `SearchUsers` — dev/test поиск логинов по префиксу, при необходимости с фильтром только по server PDA.
|
||||||
|
|
||||||
Регистрация выполняется только через Solana.
|
Регистрация выполняется только через Solana.
|
||||||
|
|
||||||
@@ -111,6 +111,7 @@
|
|||||||
### Назначение
|
### Назначение
|
||||||
|
|
||||||
Поиск пользователей по префиксу логина. Операция зарегистрирована в серверном API и используется как вспомогательная dev/test операция.
|
Поиск пользователей по префиксу логина. Операция зарегистрирована в серверном API и используется как вспомогательная dev/test операция.
|
||||||
|
При необходимости можно включить фильтр только по server PDA.
|
||||||
|
|
||||||
### Запрос
|
### Запрос
|
||||||
|
|
||||||
@@ -119,11 +120,16 @@
|
|||||||
"op": "SearchUsers",
|
"op": "SearchUsers",
|
||||||
"requestId": "search-001",
|
"requestId": "search-001",
|
||||||
"payload": {
|
"payload": {
|
||||||
"prefix": "an"
|
"prefix": "an",
|
||||||
|
"isServer": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- `prefix` — обязательный префикс логина.
|
||||||
|
- `isServer` — необязательный boolean-флаг; если `true`, сервер вернёт только логины,
|
||||||
|
у которых в `solana_user_pda_current` стоит `is_server = true`.
|
||||||
|
|
||||||
### Успешный ответ
|
### Успешный ответ
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ import * as profileView from './pages/profile-view.js?v=202607150910';
|
|||||||
import * as profileEditView from './pages/profile-edit-view.js';
|
import * as profileEditView from './pages/profile-edit-view.js';
|
||||||
import * as walletView from './pages/wallet-view.js?v=202606281930';
|
import * as walletView from './pages/wallet-view.js?v=202606281930';
|
||||||
import * as settingsView from './pages/settings-view.js';
|
import * as settingsView from './pages/settings-view.js';
|
||||||
|
import * as accessServersView from './pages/access-servers-view.js';
|
||||||
import * as developerSettingsView from './pages/developer-settings-view.js';
|
import * as developerSettingsView from './pages/developer-settings-view.js';
|
||||||
import * as serverSettingsView from './pages/server-settings-view.js?v=202606161240';
|
import * as serverSettingsView from './pages/server-settings-view.js?v=202606161240';
|
||||||
import * as remoteAddBlockSessionView from './pages/remote-addblock-session-view.js?v=202606281300';
|
import * as remoteAddBlockSessionView from './pages/remote-addblock-session-view.js?v=202606281300';
|
||||||
@@ -116,6 +117,7 @@ const routes = {
|
|||||||
'profile-edit-view': profileEditView,
|
'profile-edit-view': profileEditView,
|
||||||
'wallet-view': walletView,
|
'wallet-view': walletView,
|
||||||
'settings-view': settingsView,
|
'settings-view': settingsView,
|
||||||
|
'access-servers-view': accessServersView,
|
||||||
'developer-settings-view': developerSettingsView,
|
'developer-settings-view': developerSettingsView,
|
||||||
'server-settings-view': serverSettingsView,
|
'server-settings-view': serverSettingsView,
|
||||||
'remote-addblock-session-view': remoteAddBlockSessionView,
|
'remote-addblock-session-view': remoteAddBlockSessionView,
|
||||||
|
|||||||
@@ -0,0 +1,380 @@
|
|||||||
|
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, '"')
|
||||||
|
.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 = `
|
||||||
|
<div class="modal-card stack" style="max-width:min(94vw,34rem);">
|
||||||
|
<h3 class="modal-title" id="access-servers-dialog-title">Подтверждение</h3>
|
||||||
|
<p class="meta-muted" id="access-servers-dialog-text"></p>
|
||||||
|
<p class="meta-muted" id="access-servers-dialog-note" hidden></p>
|
||||||
|
<div class="auth-footer-actions">
|
||||||
|
<button class="ghost-btn" type="button" id="access-servers-dialog-cancel">Нет</button>
|
||||||
|
<button class="primary-btn" type="button" id="access-servers-dialog-confirm">Да</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
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 = `
|
||||||
|
<p class="field-label">Где хранятся личные данные</p>
|
||||||
|
<p class="meta-muted">
|
||||||
|
Серверы доступа хранят зашифрованную личную переписку пользователя и участвуют в звонках.
|
||||||
|
Всё, что публикуется в блокчейне SHiNE, доступно через любой сервер Сияния,
|
||||||
|
а доступ пользователя и приватная переписка хранятся только на этих серверах.
|
||||||
|
</p>
|
||||||
|
`;
|
||||||
|
|
||||||
|
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 = `
|
||||||
|
<span style="display:block; text-align:left;">
|
||||||
|
<strong>@${escapeHtml(server.login)}</strong>
|
||||||
|
<span class="meta-muted" style="display:block; margin-top:0.2rem;">${escapeHtml(server.url || 'URL не указан')}</span>
|
||||||
|
</span>
|
||||||
|
`;
|
||||||
|
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) => (
|
||||||
|
`<button type="button" class="profile-relative-suggest-item" data-login="${escapeHtml(item.login)}" data-url="${escapeHtml(item.url || '')}">
|
||||||
|
@${escapeHtml(item.login)}
|
||||||
|
<span class="meta-muted" style="display:block; margin-top:0.2rem;">${escapeHtml(item.url || 'URL не указан')}</span>
|
||||||
|
</button>`
|
||||||
|
)).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;
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ import { renderHeader } from '../components/header.js';
|
|||||||
import { saveEntrySettings, state } from '../state.js';
|
import { saveEntrySettings, state } from '../state.js';
|
||||||
import { checkServerAvailabilityByKey, resolveAndCheckShineServerLogin } from '../services/server-health-service.js';
|
import { checkServerAvailabilityByKey, resolveAndCheckShineServerLogin } from '../services/server-health-service.js';
|
||||||
|
|
||||||
export const pageMeta = { id: 'server-settings-view', title: 'Настройки серверов' };
|
export const pageMeta = { id: 'server-settings-view', title: 'Серверы блокчейнов' };
|
||||||
|
|
||||||
const SERVER_FIELDS = [
|
const SERVER_FIELDS = [
|
||||||
{ key: 'solanaServer', label: 'Адрес Solana сервера' },
|
{ key: 'solanaServer', label: 'Адрес Solana сервера' },
|
||||||
@@ -26,6 +26,16 @@ export function render({ navigate }) {
|
|||||||
|
|
||||||
const timers = new Map();
|
const timers = new Map();
|
||||||
|
|
||||||
|
const introCard = document.createElement('div');
|
||||||
|
introCard.className = 'card stack';
|
||||||
|
introCard.innerHTML = `
|
||||||
|
<p class="field-label">Серверы блокчейнов и публичных данных</p>
|
||||||
|
<p class="meta-muted">
|
||||||
|
Здесь настраиваются Solana, SHiNE и Arweave для чтения публичных данных и доступа к блокчейну.
|
||||||
|
Эти настройки не меняют список личных серверов доступа пользователя.
|
||||||
|
</p>
|
||||||
|
`;
|
||||||
|
|
||||||
const body = document.createElement('div');
|
const body = document.createElement('div');
|
||||||
body.className = 'card stack';
|
body.className = 'card stack';
|
||||||
|
|
||||||
@@ -168,7 +178,7 @@ export function render({ navigate }) {
|
|||||||
await saveEntrySettings(draft);
|
await saveEntrySettings(draft);
|
||||||
navigate('settings-view');
|
navigate('settings-view');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
window.alert(error?.message || 'Не удалось сохранить настройки серверов.');
|
window.alert(error?.message || 'Не удалось сохранить настройки серверов блокчейнов.');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -186,9 +196,10 @@ export function render({ navigate }) {
|
|||||||
|
|
||||||
screen.append(
|
screen.append(
|
||||||
renderHeader({
|
renderHeader({
|
||||||
title: 'Настройки серверов',
|
title: 'Серверы блокчейнов',
|
||||||
leftAction: { label: '←', onClick: () => navigate('settings-view') },
|
leftAction: { label: '←', onClick: () => navigate('settings-view') },
|
||||||
}),
|
}),
|
||||||
|
introCard,
|
||||||
body,
|
body,
|
||||||
actions,
|
actions,
|
||||||
help,
|
help,
|
||||||
|
|||||||
@@ -42,14 +42,26 @@ export function render({ navigate }) {
|
|||||||
card.innerHTML = `
|
card.innerHTML = `
|
||||||
<button class="text-btn" type="button" id="settings-device">Устройства</button>
|
<button class="text-btn" type="button" id="settings-device">Устройства</button>
|
||||||
<button class="text-btn" type="button" id="settings-remote-addblock">Remote AddBlock через homeserver</button>
|
<button class="text-btn" type="button" id="settings-remote-addblock">Remote AddBlock через homeserver</button>
|
||||||
<button class="text-btn" type="button" id="settings-servers">Настройки серверов</button>
|
<button class="text-btn" type="button" id="settings-access-servers">
|
||||||
|
<span style="display:block; text-align:left;">
|
||||||
|
<strong>Серверы доступа</strong>
|
||||||
|
<span class="meta-muted" style="display:block; margin-top:0.2rem;">Личная переписка, звонки и зашифрованные данные</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button class="text-btn" type="button" id="settings-blockchain-servers">
|
||||||
|
<span style="display:block; text-align:left;">
|
||||||
|
<strong>Серверы блокчейнов</strong>
|
||||||
|
<span class="meta-muted" style="display:block; margin-top:0.2rem;">Solana, SHiNE и Arweave для публичных данных</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
<button class="text-btn" type="button" id="settings-language">Язык / Language</button>
|
<button class="text-btn" type="button" id="settings-language">Язык / Language</button>
|
||||||
<button class="text-btn" type="button" id="settings-signout">Завершить текущий сеанс</button>
|
<button class="text-btn" type="button" id="settings-signout">Завершить текущий сеанс</button>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
card.querySelector('#settings-device').addEventListener('click', () => navigate('device-view'));
|
card.querySelector('#settings-device').addEventListener('click', () => navigate('device-view'));
|
||||||
card.querySelector('#settings-remote-addblock').addEventListener('click', () => navigate('remote-addblock-session-view'));
|
card.querySelector('#settings-remote-addblock').addEventListener('click', () => navigate('remote-addblock-session-view'));
|
||||||
card.querySelector('#settings-servers').addEventListener('click', () => navigate('server-settings-view'));
|
card.querySelector('#settings-access-servers').addEventListener('click', () => navigate('access-servers-view'));
|
||||||
|
card.querySelector('#settings-blockchain-servers').addEventListener('click', () => navigate('server-settings-view'));
|
||||||
card.querySelector('#settings-language').addEventListener('click', () => navigate('language-view'));
|
card.querySelector('#settings-language').addEventListener('click', () => navigate('language-view'));
|
||||||
|
|
||||||
const signOutBtn = card.querySelector('#settings-signout');
|
const signOutBtn = card.querySelector('#settings-signout');
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ const PRETTY_PATHS = new Map([
|
|||||||
['network-view', 'network'],
|
['network-view', 'network'],
|
||||||
['notifications-view', 'notifications'],
|
['notifications-view', 'notifications'],
|
||||||
['settings-view', 'settings'],
|
['settings-view', 'settings'],
|
||||||
|
['access-servers-view', 'settings/access-servers'],
|
||||||
['server-settings-view', 'settings/servers'],
|
['server-settings-view', 'settings/servers'],
|
||||||
['developer-settings-view', 'settings/developer'],
|
['developer-settings-view', 'settings/developer'],
|
||||||
['trusted-device-login-settings-view', 'settings/device-login'],
|
['trusted-device-login-settings-view', 'settings/device-login'],
|
||||||
@@ -258,6 +259,7 @@ export function getRoute() {
|
|||||||
|
|
||||||
if (pageId === 'settings') {
|
if (pageId === 'settings') {
|
||||||
const sub = decodePart(segments[1] || '').toLowerCase();
|
const sub = decodePart(segments[1] || '').toLowerCase();
|
||||||
|
if (sub === 'access-servers') return { pageId: 'access-servers-view', params: {} };
|
||||||
if (sub === 'servers') return { pageId: 'server-settings-view', params: {} };
|
if (sub === 'servers') return { pageId: 'server-settings-view', params: {} };
|
||||||
if (sub === 'developer') return { pageId: 'developer-settings-view', params: {} };
|
if (sub === 'developer') return { pageId: 'developer-settings-view', params: {} };
|
||||||
if (sub === 'device-login') return { pageId: 'trusted-device-login-settings-view', params: {} };
|
if (sub === 'device-login') return { pageId: 'trusted-device-login-settings-view', params: {} };
|
||||||
@@ -368,6 +370,7 @@ export function resolveToolbarActive(pageId) {
|
|||||||
pageId === 'profile-edit-view' ||
|
pageId === 'profile-edit-view' ||
|
||||||
pageId === 'wallet-view' ||
|
pageId === 'wallet-view' ||
|
||||||
pageId === 'settings-view' ||
|
pageId === 'settings-view' ||
|
||||||
|
pageId === 'access-servers-view' ||
|
||||||
pageId === 'developer-settings-view' ||
|
pageId === 'developer-settings-view' ||
|
||||||
pageId === 'server-settings-view' ||
|
pageId === 'server-settings-view' ||
|
||||||
pageId === 'remote-addblock-session-view' ||
|
pageId === 'remote-addblock-session-view' ||
|
||||||
|
|||||||
@@ -2603,8 +2603,12 @@ export class AuthService {
|
|||||||
return response.payload || {};
|
return response.payload || {};
|
||||||
}
|
}
|
||||||
|
|
||||||
async searchUsers(prefix) {
|
async searchUsers(prefix, options = {}) {
|
||||||
const response = await this.ws.request('SearchUsers', { prefix });
|
const payload = { prefix };
|
||||||
|
if (typeof options?.isServer === 'boolean') {
|
||||||
|
payload.isServer = options.isServer;
|
||||||
|
}
|
||||||
|
const response = await this.ws.request('SearchUsers', payload);
|
||||||
if (response.status !== 200) throw opError('SearchUsers', response);
|
if (response.status !== 200) throw opError('SearchUsers', response);
|
||||||
return response.payload?.logins || [];
|
return response.payload?.logins || [];
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user