From 4b0a934e5149ecb66270247430879205588d6d6c6d4d86d2c80c41c4296452cb Mon Sep 17 00:00:00 2001 From: AidarKC Date: Tue, 11 Aug 2026 17:21:07 +0400 Subject: [PATCH] =?UTF-8?q?=D0=9E=D0=B3=D1=80=D0=B0=D0=BD=D0=B8=D1=87?= =?UTF-8?q?=D0=B8=D0=BB=20access=20servers=20=D0=B4=D0=BE=20=D0=B4=D0=B2?= =?UTF-8?q?=D1=83=D1=85=20=D0=B2=20UI=20=D0=B8=20=D1=81=D0=B5=D1=80=D0=B2?= =?UTF-8?q?=D0=B5=D1=80=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/resources/postgres/migration_v2.sql | 32 ++++++++++++------- .../src/main/resources/postgres/schema_v1.sql | 32 ++++++++++++------- .../auth/SolanaUserPdaImportService.java | 10 +++++- .../postgres/PostgresStorageRepository.java | 32 ++++++++++++------- VERSION.properties | 4 +-- shine-UI/js/pages/access-servers-view.js | 23 ++++++++++--- shine-UI/js/services/shine-server-resolver.js | 2 +- .../js/services/shine-user-pda-service.js | 21 ++++++++++-- 8 files changed, 109 insertions(+), 47 deletions(-) diff --git a/SHiNE-server/shine-server-db/src/main/resources/postgres/migration_v2.sql b/SHiNE-server/shine-server-db/src/main/resources/postgres/migration_v2.sql index 172ea9f3..d0934849 100644 --- a/SHiNE-server/shine-server-db/src/main/resources/postgres/migration_v2.sql +++ b/SHiNE-server/shine-server-db/src/main/resources/postgres/migration_v2.sql @@ -53,12 +53,16 @@ BEGIN s.updated_at_ms, CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT) FROM solana_user_pda_current u - CROSS JOIN LATERAL jsonb_array_elements_text( - CASE - WHEN btrim(COALESCE(u.access_servers_json, '')) = '' THEN '[]'::jsonb - ELSE u.access_servers_json::jsonb - END - ) AS access_server(login_value) + CROSS JOIN LATERAL ( + SELECT login_value + FROM jsonb_array_elements_text( + CASE + WHEN btrim(COALESCE(u.access_servers_json, '')) = '' THEN '[]'::jsonb + ELSE u.access_servers_json::jsonb + END + ) WITH ORDINALITY AS access_server(login_value, ord) + WHERE ord <= 2 + ) AS access_server JOIN solana_user_pda_current s ON LOWER(s.login) = LOWER(btrim(access_server.login_value)) AND s.is_server = TRUE @@ -92,12 +96,16 @@ BEGIN FOR affected_user IN SELECT u.login FROM solana_user_pda_current u - CROSS JOIN LATERAL jsonb_array_elements_text( - CASE - WHEN btrim(COALESCE(u.access_servers_json, '')) = '' THEN '[]'::jsonb - ELSE u.access_servers_json::jsonb - END - ) AS access_server(login_value) + CROSS JOIN LATERAL ( + SELECT login_value + FROM jsonb_array_elements_text( + CASE + WHEN btrim(COALESCE(u.access_servers_json, '')) = '' THEN '[]'::jsonb + ELSE u.access_servers_json::jsonb + END + ) WITH ORDINALITY AS access_server(login_value, ord) + WHERE ord <= 2 + ) AS access_server WHERE LOWER(btrim(access_server.login_value)) = LOWER(p_server_login) LOOP PERFORM shine_refresh_user_access_servers_for_user(affected_user.login); diff --git a/SHiNE-server/shine-server-db/src/main/resources/postgres/schema_v1.sql b/SHiNE-server/shine-server-db/src/main/resources/postgres/schema_v1.sql index e514899d..5778d7fe 100644 --- a/SHiNE-server/shine-server-db/src/main/resources/postgres/schema_v1.sql +++ b/SHiNE-server/shine-server-db/src/main/resources/postgres/schema_v1.sql @@ -162,12 +162,16 @@ BEGIN s.updated_at_ms, CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT) FROM solana_user_pda_current u - CROSS JOIN LATERAL jsonb_array_elements_text( - CASE - WHEN btrim(COALESCE(u.access_servers_json, '')) = '' THEN '[]'::jsonb - ELSE u.access_servers_json::jsonb - END - ) AS access_server(login_value) + CROSS JOIN LATERAL ( + SELECT login_value + FROM jsonb_array_elements_text( + CASE + WHEN btrim(COALESCE(u.access_servers_json, '')) = '' THEN '[]'::jsonb + ELSE u.access_servers_json::jsonb + END + ) WITH ORDINALITY AS access_server(login_value, ord) + WHERE ord <= 2 + ) AS access_server JOIN solana_user_pda_current s ON LOWER(s.login) = LOWER(btrim(access_server.login_value)) AND s.is_server = TRUE @@ -201,12 +205,16 @@ BEGIN FOR affected_user IN SELECT u.login FROM solana_user_pda_current u - CROSS JOIN LATERAL jsonb_array_elements_text( - CASE - WHEN btrim(COALESCE(u.access_servers_json, '')) = '' THEN '[]'::jsonb - ELSE u.access_servers_json::jsonb - END - ) AS access_server(login_value) + CROSS JOIN LATERAL ( + SELECT login_value + FROM jsonb_array_elements_text( + CASE + WHEN btrim(COALESCE(u.access_servers_json, '')) = '' THEN '[]'::jsonb + ELSE u.access_servers_json::jsonb + END + ) WITH ORDINALITY AS access_server(login_value, ord) + WHERE ord <= 2 + ) AS access_server WHERE LOWER(btrim(access_server.login_value)) = LOWER(p_server_login) LOOP PERFORM shine_refresh_user_access_servers_for_user(affected_user.login); diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/SolanaUserPdaImportService.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/SolanaUserPdaImportService.java index 4f58614a..e97fa70f 100644 --- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/SolanaUserPdaImportService.java +++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/SolanaUserPdaImportService.java @@ -27,6 +27,7 @@ public final class SolanaUserPdaImportService { private static final ObjectMapper MAPPER = new ObjectMapper(); private static final HttpClient HTTP = HttpClient.newHttpClient(); private static final String MAGIC = "SHiNE"; + private static final int MAX_EFFECTIVE_ACCESS_SERVERS = 2; private SolanaUserPdaImportService() {} @@ -87,6 +88,7 @@ public final class SolanaUserPdaImportService { String serverAddress = safe(serverProfile.serverAddress()); if (serverAddress.isBlank()) continue; routes.putIfAbsent(normalized, new ParsedServerRoute(normalized, serverAddress)); + if (routes.size() >= MAX_EFFECTIVE_ACCESS_SERVERS) break; } return new ArrayList<>(routes.values()); } @@ -234,7 +236,13 @@ public final class SolanaUserPdaImportService { int n = u8(raw, c++); String accessServerLogin = new String(raw, c, n, StandardCharsets.UTF_8); c += n; - accessServers.add(normalizeLogin(accessServerLogin)); + String normalizedAccessServerLogin = normalizeLogin(accessServerLogin); + if (normalizedAccessServerLogin == null || accessServers.contains(normalizedAccessServerLogin)) { + continue; + } + if (accessServers.size() < MAX_EFFECTIVE_ACCESS_SERVERS) { + accessServers.add(normalizedAccessServerLogin); + } } } else if (blockType == 50) { int sessionsMode = u8(raw, c++); diff --git a/SHiNE-server/shine-server-solana-users-sync/src/main/java/sync/storage/postgres/PostgresStorageRepository.java b/SHiNE-server/shine-server-solana-users-sync/src/main/java/sync/storage/postgres/PostgresStorageRepository.java index cdbe7d27..b1a2c145 100644 --- a/SHiNE-server/shine-server-solana-users-sync/src/main/java/sync/storage/postgres/PostgresStorageRepository.java +++ b/SHiNE-server/shine-server-solana-users-sync/src/main/java/sync/storage/postgres/PostgresStorageRepository.java @@ -1057,12 +1057,16 @@ public final class PostgresStorageRepository s.updated_at_ms, CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT) FROM solana_user_pda_current u - CROSS JOIN LATERAL jsonb_array_elements_text( - CASE - WHEN btrim(COALESCE(u.access_servers_json, '')) = '' THEN '[]'::jsonb - ELSE u.access_servers_json::jsonb - END - ) AS access_server(login_value) + CROSS JOIN LATERAL ( + SELECT login_value + FROM jsonb_array_elements_text( + CASE + WHEN btrim(COALESCE(u.access_servers_json, '')) = '' THEN '[]'::jsonb + ELSE u.access_servers_json::jsonb + END + ) WITH ORDINALITY AS access_server(login_value, ord) + WHERE ord <= 2 + ) AS access_server JOIN solana_user_pda_current s ON LOWER(s.login) = LOWER(btrim(access_server.login_value)) AND s.is_server = TRUE @@ -1100,12 +1104,16 @@ public final class PostgresStorageRepository FOR affected_user IN SELECT u.login FROM solana_user_pda_current u - CROSS JOIN LATERAL jsonb_array_elements_text( - CASE - WHEN btrim(COALESCE(u.access_servers_json, '')) = '' THEN '[]'::jsonb - ELSE u.access_servers_json::jsonb - END - ) AS access_server(login_value) + CROSS JOIN LATERAL ( + SELECT login_value + FROM jsonb_array_elements_text( + CASE + WHEN btrim(COALESCE(u.access_servers_json, '')) = '' THEN '[]'::jsonb + ELSE u.access_servers_json::jsonb + END + ) WITH ORDINALITY AS access_server(login_value, ord) + WHERE ord <= 2 + ) AS access_server WHERE LOWER(btrim(access_server.login_value)) = LOWER(p_server_login) LOOP PERFORM shine_refresh_user_access_servers_for_user(affected_user.login); diff --git a/VERSION.properties b/VERSION.properties index 4e7b8be6..628d8941 100644 --- a/VERSION.properties +++ b/VERSION.properties @@ -1,2 +1,2 @@ -client.version=1.5.34 -server.version=1.4.10 +client.version=1.5.35 +server.version=1.4.11 diff --git a/shine-UI/js/pages/access-servers-view.js b/shine-UI/js/pages/access-servers-view.js index ce7d7277..b7a68d91 100644 --- a/shine-UI/js/pages/access-servers-view.js +++ b/shine-UI/js/pages/access-servers-view.js @@ -7,6 +7,7 @@ import { readShineUserPda, updateShineUserPdaOnSolana } from '../services/shine- import { getTopupSiteUrl } from '../services/solana-wallet-service.js'; export const pageMeta = { id: 'access-servers-view', title: 'Серверы доступа' }; +const MAX_ACCESS_SERVERS = 2; function escapeHtml(value) { return String(value || '') @@ -39,8 +40,9 @@ function uniqueLogins(values) { if (!login || seen.has(login)) return; seen.add(login); result.push(login); + if (result.length >= MAX_ACCESS_SERVERS) return; }); - return result; + return result.slice(0, MAX_ACCESS_SERVERS); } function shortenSignature(value) { @@ -309,7 +311,7 @@ export function render({ navigate }) { addTitle.textContent = 'Добавить сервер доступа'; const addHint = document.createElement('p'); addHint.className = 'meta-muted'; - addHint.textContent = 'Введите логин сервера или несколько первых букв, затем выберите сервер из подсказок.'; + addHint.textContent = 'Можно использовать максимум два сервера доступа. Введите логин сервера или несколько первых букв, затем выберите сервер из подсказок.'; const addInput = document.createElement('input'); addInput.className = 'input'; addInput.type = 'text'; @@ -329,7 +331,9 @@ export function render({ navigate }) { addCard.append(addTitle, addHint, addInput, suggestEl, addStatus, addButton); const refreshAddButton = () => { - addButton.disabled = operationBusy || (!selectedCandidate && !normalizeLogin(addInput.value)); + addButton.disabled = operationBusy + || currentAccessServers.length >= MAX_ACCESS_SERVERS + || (!selectedCandidate && !normalizeLogin(addInput.value)); }; const setOperationBusy = (busy) => { @@ -348,7 +352,9 @@ export function render({ navigate }) { addInput.value = candidate.login; addStatus.textContent = `Выбран сервер @${candidate.login}${candidate.url ? ` (${candidate.url})` : ''}`; } else { - addStatus.textContent = 'Для изменения списка понадобится подпись root key.'; + addStatus.textContent = currentAccessServers.length >= MAX_ACCESS_SERVERS + ? 'У пользователя уже выбраны два сервера доступа. Сначала отключите один из них.' + : 'Для изменения списка понадобится подпись root key.'; } }; @@ -457,12 +463,17 @@ export function render({ navigate }) { } currentAccessServers = rows; renderServerList(); + refreshAddButton(); listStatus.textContent = rows.length ? `Найдено серверов доступа: ${rows.length}` : 'В PDA пользователя пока нет серверов доступа.'; + if (rows.length >= MAX_ACCESS_SERVERS) { + addStatus.textContent = 'У пользователя уже выбраны два сервера доступа. Сначала отключите один из них.'; + } } catch (error) { currentAccessServers = []; renderServerList(); + refreshAddButton(); listStatus.textContent = error?.message || 'Не удалось прочитать список серверов доступа.'; } }; @@ -654,6 +665,10 @@ export function render({ navigate }) { addStatus.textContent = `Сервер @${login} уже есть в списке.`; return; } + if (currentAccessServers.length >= MAX_ACCESS_SERVERS) { + addStatus.textContent = 'Нельзя добавить больше двух серверов доступа. Сначала отключите один из текущих.'; + return; + } try { const resolved = await resolveShineServerByServerLogin({ serverLogin: login, solanaEndpoint }); diff --git a/shine-UI/js/services/shine-server-resolver.js b/shine-UI/js/services/shine-server-resolver.js index e42cc816..c4031780 100644 --- a/shine-UI/js/services/shine-server-resolver.js +++ b/shine-UI/js/services/shine-server-resolver.js @@ -69,7 +69,7 @@ export async function resolveShineServerForUserLogin({ login, solanaEndpoint, fa solanaEndpoint, }); const accessServers = Array.isArray(parsedUser?.accessServers) - ? parsedUser.accessServers.map((value) => normalizeShineServerLogin(value)).filter(Boolean) + ? parsedUser.accessServers.map((value) => normalizeShineServerLogin(value)).filter(Boolean).slice(0, 2) : []; const pickedServerLogin = accessServers[0] || normalizeShineServerLogin(fallbackServerLogin); if (!pickedServerLogin) { diff --git a/shine-UI/js/services/shine-user-pda-service.js b/shine-UI/js/services/shine-user-pda-service.js index 843c6b5d..fd5fed2d 100644 --- a/shine-UI/js/services/shine-user-pda-service.js +++ b/shine-UI/js/services/shine-user-pda-service.js @@ -30,6 +30,7 @@ const BLOCK_TYPE_TRUSTED_STATE = 70; const SESSIONS_MODE_MIXED = 1; const SESSION_TYPE_USER = 1; const SESSION_TYPE_HOMESERVER = 100; +const MAX_EFFECTIVE_ACCESS_SERVERS = 2; let solanaLibPromise = null; function loadSolanaLib() { @@ -41,6 +42,19 @@ function normalizeLogin(login) { return String(login || '').trim().toLowerCase(); } +function normalizeAccessServers(values) { + const seen = new Set(); + const result = []; + for (const value of Array.isArray(values) ? values : []) { + const login = normalizeLogin(value); + if (!login || seen.has(login)) continue; + seen.add(login); + result.push(login); + if (result.length >= MAX_EFFECTIVE_ACCESS_SERVERS) break; + } + return result; +} + function pushU32LE(buf, value) { const n = Number(value) >>> 0; buf.push(n & 0xff, (n >>> 8) & 0xff, (n >>> 16) & 0xff, (n >>> 24) & 0xff); @@ -202,7 +216,7 @@ function serializeCreateUserPdaArgs(args) { buf.push(syncServers.length & 0xff); for (const value of syncServers) pushStrU8(buf, value); } - const accessServers = Array.isArray(args.accessServers) ? args.accessServers : []; + const accessServers = normalizeAccessServers(args.accessServers); buf.push(accessServers.length & 0xff); for (const value of accessServers) pushStrU8(buf, value); buf.push(Number(args.sessionsMode || SESSIONS_MODE_MIXED) & 0xff); @@ -252,7 +266,7 @@ function serializeUpdateUserPdaArgs(args) { buf.push(syncServers.length & 0xff); for (const value of syncServers) pushStrU8(buf, value); } - const accessServers = Array.isArray(args.accessServers) ? args.accessServers : []; + const accessServers = normalizeAccessServers(args.accessServers); buf.push(accessServers.length & 0xff); for (const value of accessServers) pushStrU8(buf, value); buf.push(Number(args.sessionsMode || SESSIONS_MODE_MIXED) & 0xff); @@ -337,7 +351,7 @@ function createPdaState({ addressFormatVersion: serverProfile?.addressFormatVersion ?? 0, serverAddress: serverProfile?.serverAddress ?? '', syncServers: serverProfile?.syncServers ? [...serverProfile.syncServers] : [], - accessServers: Array.isArray(accessServers) ? [...accessServers] : [], + accessServers: normalizeAccessServers(accessServers), sessionsMode: Number(sessionsMode || SESSIONS_MODE_MIXED) & 0xff, sessions: Array.isArray(sessions) ? sessions.map((x) => ({ sessionType: Number(x?.sessionType || 0) & 0xff, @@ -493,6 +507,7 @@ export function parseShineUserPda(dataBytes) { const accessCount = reader.readU8(); accessServers = []; for (let j = 0; j < accessCount; j += 1) accessServers.push(reader.readStrU8()); + accessServers = normalizeAccessServers(accessServers); continue; } if (blockType === BLOCK_TYPE_SESSIONS) {