Ограничил access servers до двух в UI и сервере

This commit is contained in:
AidarKC
2026-08-11 17:21:07 +04:00
parent ee5d86003b
commit 4b0a934e51
8 changed files with 109 additions and 47 deletions
@@ -53,12 +53,16 @@ BEGIN
s.updated_at_ms, s.updated_at_ms,
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT) CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
FROM solana_user_pda_current u FROM solana_user_pda_current u
CROSS JOIN LATERAL jsonb_array_elements_text( CROSS JOIN LATERAL (
SELECT login_value
FROM jsonb_array_elements_text(
CASE CASE
WHEN btrim(COALESCE(u.access_servers_json, '')) = '' THEN '[]'::jsonb WHEN btrim(COALESCE(u.access_servers_json, '')) = '' THEN '[]'::jsonb
ELSE u.access_servers_json::jsonb ELSE u.access_servers_json::jsonb
END END
) AS access_server(login_value) ) WITH ORDINALITY AS access_server(login_value, ord)
WHERE ord <= 2
) AS access_server
JOIN solana_user_pda_current s JOIN solana_user_pda_current s
ON LOWER(s.login) = LOWER(btrim(access_server.login_value)) ON LOWER(s.login) = LOWER(btrim(access_server.login_value))
AND s.is_server = TRUE AND s.is_server = TRUE
@@ -92,12 +96,16 @@ BEGIN
FOR affected_user IN FOR affected_user IN
SELECT u.login SELECT u.login
FROM solana_user_pda_current u FROM solana_user_pda_current u
CROSS JOIN LATERAL jsonb_array_elements_text( CROSS JOIN LATERAL (
SELECT login_value
FROM jsonb_array_elements_text(
CASE CASE
WHEN btrim(COALESCE(u.access_servers_json, '')) = '' THEN '[]'::jsonb WHEN btrim(COALESCE(u.access_servers_json, '')) = '' THEN '[]'::jsonb
ELSE u.access_servers_json::jsonb ELSE u.access_servers_json::jsonb
END END
) AS access_server(login_value) ) 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) WHERE LOWER(btrim(access_server.login_value)) = LOWER(p_server_login)
LOOP LOOP
PERFORM shine_refresh_user_access_servers_for_user(affected_user.login); PERFORM shine_refresh_user_access_servers_for_user(affected_user.login);
@@ -162,12 +162,16 @@ BEGIN
s.updated_at_ms, s.updated_at_ms,
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT) CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
FROM solana_user_pda_current u FROM solana_user_pda_current u
CROSS JOIN LATERAL jsonb_array_elements_text( CROSS JOIN LATERAL (
SELECT login_value
FROM jsonb_array_elements_text(
CASE CASE
WHEN btrim(COALESCE(u.access_servers_json, '')) = '' THEN '[]'::jsonb WHEN btrim(COALESCE(u.access_servers_json, '')) = '' THEN '[]'::jsonb
ELSE u.access_servers_json::jsonb ELSE u.access_servers_json::jsonb
END END
) AS access_server(login_value) ) WITH ORDINALITY AS access_server(login_value, ord)
WHERE ord <= 2
) AS access_server
JOIN solana_user_pda_current s JOIN solana_user_pda_current s
ON LOWER(s.login) = LOWER(btrim(access_server.login_value)) ON LOWER(s.login) = LOWER(btrim(access_server.login_value))
AND s.is_server = TRUE AND s.is_server = TRUE
@@ -201,12 +205,16 @@ BEGIN
FOR affected_user IN FOR affected_user IN
SELECT u.login SELECT u.login
FROM solana_user_pda_current u FROM solana_user_pda_current u
CROSS JOIN LATERAL jsonb_array_elements_text( CROSS JOIN LATERAL (
SELECT login_value
FROM jsonb_array_elements_text(
CASE CASE
WHEN btrim(COALESCE(u.access_servers_json, '')) = '' THEN '[]'::jsonb WHEN btrim(COALESCE(u.access_servers_json, '')) = '' THEN '[]'::jsonb
ELSE u.access_servers_json::jsonb ELSE u.access_servers_json::jsonb
END END
) AS access_server(login_value) ) 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) WHERE LOWER(btrim(access_server.login_value)) = LOWER(p_server_login)
LOOP LOOP
PERFORM shine_refresh_user_access_servers_for_user(affected_user.login); PERFORM shine_refresh_user_access_servers_for_user(affected_user.login);
@@ -27,6 +27,7 @@ public final class SolanaUserPdaImportService {
private static final ObjectMapper MAPPER = new ObjectMapper(); private static final ObjectMapper MAPPER = new ObjectMapper();
private static final HttpClient HTTP = HttpClient.newHttpClient(); private static final HttpClient HTTP = HttpClient.newHttpClient();
private static final String MAGIC = "SHiNE"; private static final String MAGIC = "SHiNE";
private static final int MAX_EFFECTIVE_ACCESS_SERVERS = 2;
private SolanaUserPdaImportService() {} private SolanaUserPdaImportService() {}
@@ -87,6 +88,7 @@ public final class SolanaUserPdaImportService {
String serverAddress = safe(serverProfile.serverAddress()); String serverAddress = safe(serverProfile.serverAddress());
if (serverAddress.isBlank()) continue; if (serverAddress.isBlank()) continue;
routes.putIfAbsent(normalized, new ParsedServerRoute(normalized, serverAddress)); routes.putIfAbsent(normalized, new ParsedServerRoute(normalized, serverAddress));
if (routes.size() >= MAX_EFFECTIVE_ACCESS_SERVERS) break;
} }
return new ArrayList<>(routes.values()); return new ArrayList<>(routes.values());
} }
@@ -234,7 +236,13 @@ public final class SolanaUserPdaImportService {
int n = u8(raw, c++); int n = u8(raw, c++);
String accessServerLogin = new String(raw, c, n, StandardCharsets.UTF_8); String accessServerLogin = new String(raw, c, n, StandardCharsets.UTF_8);
c += n; 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) { } else if (blockType == 50) {
int sessionsMode = u8(raw, c++); int sessionsMode = u8(raw, c++);
@@ -1057,12 +1057,16 @@ public final class PostgresStorageRepository
s.updated_at_ms, s.updated_at_ms,
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT) CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
FROM solana_user_pda_current u FROM solana_user_pda_current u
CROSS JOIN LATERAL jsonb_array_elements_text( CROSS JOIN LATERAL (
SELECT login_value
FROM jsonb_array_elements_text(
CASE CASE
WHEN btrim(COALESCE(u.access_servers_json, '')) = '' THEN '[]'::jsonb WHEN btrim(COALESCE(u.access_servers_json, '')) = '' THEN '[]'::jsonb
ELSE u.access_servers_json::jsonb ELSE u.access_servers_json::jsonb
END END
) AS access_server(login_value) ) WITH ORDINALITY AS access_server(login_value, ord)
WHERE ord <= 2
) AS access_server
JOIN solana_user_pda_current s JOIN solana_user_pda_current s
ON LOWER(s.login) = LOWER(btrim(access_server.login_value)) ON LOWER(s.login) = LOWER(btrim(access_server.login_value))
AND s.is_server = TRUE AND s.is_server = TRUE
@@ -1100,12 +1104,16 @@ public final class PostgresStorageRepository
FOR affected_user IN FOR affected_user IN
SELECT u.login SELECT u.login
FROM solana_user_pda_current u FROM solana_user_pda_current u
CROSS JOIN LATERAL jsonb_array_elements_text( CROSS JOIN LATERAL (
SELECT login_value
FROM jsonb_array_elements_text(
CASE CASE
WHEN btrim(COALESCE(u.access_servers_json, '')) = '' THEN '[]'::jsonb WHEN btrim(COALESCE(u.access_servers_json, '')) = '' THEN '[]'::jsonb
ELSE u.access_servers_json::jsonb ELSE u.access_servers_json::jsonb
END END
) AS access_server(login_value) ) 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) WHERE LOWER(btrim(access_server.login_value)) = LOWER(p_server_login)
LOOP LOOP
PERFORM shine_refresh_user_access_servers_for_user(affected_user.login); PERFORM shine_refresh_user_access_servers_for_user(affected_user.login);
+2 -2
View File
@@ -1,2 +1,2 @@
client.version=1.5.34 client.version=1.5.35
server.version=1.4.10 server.version=1.4.11
+19 -4
View File
@@ -7,6 +7,7 @@ import { readShineUserPda, updateShineUserPdaOnSolana } from '../services/shine-
import { getTopupSiteUrl } from '../services/solana-wallet-service.js'; import { getTopupSiteUrl } from '../services/solana-wallet-service.js';
export const pageMeta = { id: 'access-servers-view', title: 'Серверы доступа' }; export const pageMeta = { id: 'access-servers-view', title: 'Серверы доступа' };
const MAX_ACCESS_SERVERS = 2;
function escapeHtml(value) { function escapeHtml(value) {
return String(value || '') return String(value || '')
@@ -39,8 +40,9 @@ function uniqueLogins(values) {
if (!login || seen.has(login)) return; if (!login || seen.has(login)) return;
seen.add(login); seen.add(login);
result.push(login); result.push(login);
if (result.length >= MAX_ACCESS_SERVERS) return;
}); });
return result; return result.slice(0, MAX_ACCESS_SERVERS);
} }
function shortenSignature(value) { function shortenSignature(value) {
@@ -309,7 +311,7 @@ export function render({ navigate }) {
addTitle.textContent = 'Добавить сервер доступа'; addTitle.textContent = 'Добавить сервер доступа';
const addHint = document.createElement('p'); const addHint = document.createElement('p');
addHint.className = 'meta-muted'; addHint.className = 'meta-muted';
addHint.textContent = 'Введите логин сервера или несколько первых букв, затем выберите сервер из подсказок.'; addHint.textContent = 'Можно использовать максимум два сервера доступа. Введите логин сервера или несколько первых букв, затем выберите сервер из подсказок.';
const addInput = document.createElement('input'); const addInput = document.createElement('input');
addInput.className = 'input'; addInput.className = 'input';
addInput.type = 'text'; addInput.type = 'text';
@@ -329,7 +331,9 @@ export function render({ navigate }) {
addCard.append(addTitle, addHint, addInput, suggestEl, addStatus, addButton); addCard.append(addTitle, addHint, addInput, suggestEl, addStatus, addButton);
const refreshAddButton = () => { const refreshAddButton = () => {
addButton.disabled = operationBusy || (!selectedCandidate && !normalizeLogin(addInput.value)); addButton.disabled = operationBusy
|| currentAccessServers.length >= MAX_ACCESS_SERVERS
|| (!selectedCandidate && !normalizeLogin(addInput.value));
}; };
const setOperationBusy = (busy) => { const setOperationBusy = (busy) => {
@@ -348,7 +352,9 @@ export function render({ navigate }) {
addInput.value = candidate.login; addInput.value = candidate.login;
addStatus.textContent = `Выбран сервер @${candidate.login}${candidate.url ? ` (${candidate.url})` : ''}`; addStatus.textContent = `Выбран сервер @${candidate.login}${candidate.url ? ` (${candidate.url})` : ''}`;
} else { } else {
addStatus.textContent = 'Для изменения списка понадобится подпись root key.'; addStatus.textContent = currentAccessServers.length >= MAX_ACCESS_SERVERS
? 'У пользователя уже выбраны два сервера доступа. Сначала отключите один из них.'
: 'Для изменения списка понадобится подпись root key.';
} }
}; };
@@ -457,12 +463,17 @@ export function render({ navigate }) {
} }
currentAccessServers = rows; currentAccessServers = rows;
renderServerList(); renderServerList();
refreshAddButton();
listStatus.textContent = rows.length listStatus.textContent = rows.length
? `Найдено серверов доступа: ${rows.length}` ? `Найдено серверов доступа: ${rows.length}`
: 'В PDA пользователя пока нет серверов доступа.'; : 'В PDA пользователя пока нет серверов доступа.';
if (rows.length >= MAX_ACCESS_SERVERS) {
addStatus.textContent = 'У пользователя уже выбраны два сервера доступа. Сначала отключите один из них.';
}
} catch (error) { } catch (error) {
currentAccessServers = []; currentAccessServers = [];
renderServerList(); renderServerList();
refreshAddButton();
listStatus.textContent = error?.message || 'Не удалось прочитать список серверов доступа.'; listStatus.textContent = error?.message || 'Не удалось прочитать список серверов доступа.';
} }
}; };
@@ -654,6 +665,10 @@ export function render({ navigate }) {
addStatus.textContent = `Сервер @${login} уже есть в списке.`; addStatus.textContent = `Сервер @${login} уже есть в списке.`;
return; return;
} }
if (currentAccessServers.length >= MAX_ACCESS_SERVERS) {
addStatus.textContent = 'Нельзя добавить больше двух серверов доступа. Сначала отключите один из текущих.';
return;
}
try { try {
const resolved = await resolveShineServerByServerLogin({ serverLogin: login, solanaEndpoint }); const resolved = await resolveShineServerByServerLogin({ serverLogin: login, solanaEndpoint });
@@ -69,7 +69,7 @@ export async function resolveShineServerForUserLogin({ login, solanaEndpoint, fa
solanaEndpoint, solanaEndpoint,
}); });
const accessServers = Array.isArray(parsedUser?.accessServers) 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); const pickedServerLogin = accessServers[0] || normalizeShineServerLogin(fallbackServerLogin);
if (!pickedServerLogin) { if (!pickedServerLogin) {
+18 -3
View File
@@ -30,6 +30,7 @@ const BLOCK_TYPE_TRUSTED_STATE = 70;
const SESSIONS_MODE_MIXED = 1; const SESSIONS_MODE_MIXED = 1;
const SESSION_TYPE_USER = 1; const SESSION_TYPE_USER = 1;
const SESSION_TYPE_HOMESERVER = 100; const SESSION_TYPE_HOMESERVER = 100;
const MAX_EFFECTIVE_ACCESS_SERVERS = 2;
let solanaLibPromise = null; let solanaLibPromise = null;
function loadSolanaLib() { function loadSolanaLib() {
@@ -41,6 +42,19 @@ function normalizeLogin(login) {
return String(login || '').trim().toLowerCase(); 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) { function pushU32LE(buf, value) {
const n = Number(value) >>> 0; const n = Number(value) >>> 0;
buf.push(n & 0xff, (n >>> 8) & 0xff, (n >>> 16) & 0xff, (n >>> 24) & 0xff); 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); buf.push(syncServers.length & 0xff);
for (const value of syncServers) pushStrU8(buf, value); 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); buf.push(accessServers.length & 0xff);
for (const value of accessServers) pushStrU8(buf, value); for (const value of accessServers) pushStrU8(buf, value);
buf.push(Number(args.sessionsMode || SESSIONS_MODE_MIXED) & 0xff); buf.push(Number(args.sessionsMode || SESSIONS_MODE_MIXED) & 0xff);
@@ -252,7 +266,7 @@ function serializeUpdateUserPdaArgs(args) {
buf.push(syncServers.length & 0xff); buf.push(syncServers.length & 0xff);
for (const value of syncServers) pushStrU8(buf, value); 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); buf.push(accessServers.length & 0xff);
for (const value of accessServers) pushStrU8(buf, value); for (const value of accessServers) pushStrU8(buf, value);
buf.push(Number(args.sessionsMode || SESSIONS_MODE_MIXED) & 0xff); buf.push(Number(args.sessionsMode || SESSIONS_MODE_MIXED) & 0xff);
@@ -337,7 +351,7 @@ function createPdaState({
addressFormatVersion: serverProfile?.addressFormatVersion ?? 0, addressFormatVersion: serverProfile?.addressFormatVersion ?? 0,
serverAddress: serverProfile?.serverAddress ?? '', serverAddress: serverProfile?.serverAddress ?? '',
syncServers: serverProfile?.syncServers ? [...serverProfile.syncServers] : [], syncServers: serverProfile?.syncServers ? [...serverProfile.syncServers] : [],
accessServers: Array.isArray(accessServers) ? [...accessServers] : [], accessServers: normalizeAccessServers(accessServers),
sessionsMode: Number(sessionsMode || SESSIONS_MODE_MIXED) & 0xff, sessionsMode: Number(sessionsMode || SESSIONS_MODE_MIXED) & 0xff,
sessions: Array.isArray(sessions) ? sessions.map((x) => ({ sessions: Array.isArray(sessions) ? sessions.map((x) => ({
sessionType: Number(x?.sessionType || 0) & 0xff, sessionType: Number(x?.sessionType || 0) & 0xff,
@@ -493,6 +507,7 @@ export function parseShineUserPda(dataBytes) {
const accessCount = reader.readU8(); const accessCount = reader.readU8();
accessServers = []; accessServers = [];
for (let j = 0; j < accessCount; j += 1) accessServers.push(reader.readStrU8()); for (let j = 0; j < accessCount; j += 1) accessServers.push(reader.readStrU8());
accessServers = normalizeAccessServers(accessServers);
continue; continue;
} }
if (blockType === BLOCK_TYPE_SESSIONS) { if (blockType === BLOCK_TYPE_SESSIONS) {