diff --git a/Dev_Docs/API/01_User_Registration_API.md b/Dev_Docs/API/01_User_Registration_API.md index 4fe4f50..7801ce5 100644 --- a/Dev_Docs/API/01_User_Registration_API.md +++ b/Dev_Docs/API/01_User_Registration_API.md @@ -1,14 +1,14 @@ # API для разработчиков: Регистрация пользователя -Этот файл описывает временный раздел API, связанный с заведением пользователя на сервере и проверкой, существует ли пользователь. +Этот файл описывает раздел API, связанный с проверкой наличия пользователя на сервере и dev/test операциями. Сейчас здесь три метода: -- `AddUser` — временная серверная регистрация пользователя; +- `AddUser` — операция отключена (регистрация только через Solana); - `GetUser` — временная серверная проверка существования пользователя и чтение его базовых данных; - `SearchUsers` — dev/test поиск логинов по префиксу. -Их логика пока вспомогательная и dev-oriented: сервер сам хранит эти данные локально и сам отвечает на existence-check. В будущем оба сценария должны быть заменены на нормальную работу напрямую через Solana, но пока этот контракт нужен клиентам для разработки и интеграции. +Регистрация выполняется через Solana (`shine_users`). Сервер при входе может лениво импортировать пользователя из Solana PDA в локальную БД, если записи ещё нет. ## Статус документа @@ -22,12 +22,7 @@ ### Назначение -Временная регистрация локального пользователя на сервере. - -Сервер: - -- создаёт запись в `solana_users`; -- создаёт стартовое состояние в `blockchain_state`. +Операция отключена. Используется только как явный ответ клиентам старых версий. ### Запрос @@ -46,29 +41,16 @@ } ``` -### Успешный ответ +### Пример ответа ```json { "op": "AddUser", "requestId": "reg-001", - "status": 200, - "ok": true, - "payload": { - } -} -``` - -### Пример ошибки - -```json -{ - "op": "AddUser", - "requestId": "reg-001", - "status": 409, + "status": 410, "ok": false, - "error": "USER_ALREADY_EXISTS", - "message": "Пользователь с таким login уже существует", + "error": "ADD_USER_DISABLED", + "message": "Серверная регистрация AddUser отключена. Используйте регистрацию через Solana.", "payload": { } } @@ -76,14 +58,7 @@ ### Специфические коды ошибок `AddUser` -- `400 / BAD_FIELDS` — не переданы обязательные поля регистрации. -- `400 / BAD_BLOCKCHAIN_NAME` — `blockchainName` не соответствует формату `-NNN`. -- `400 / BAD_KEY_FORMAT` — один из ключей не является корректным `Base64(32 bytes)`. -- `409 / USER_ALREADY_EXISTS` — пользователь с таким `login` уже есть. -- `409 / BLOCKCHAIN_ALREADY_EXISTS` — такой `blockchainName` уже занят. -- `409 / BLOCKCHAIN_STATE_ALREADY_EXISTS` — стартовое состояние blockchain уже существует. -- `501 / DB_ERROR` — ошибка БД при создании пользователя. -- `500 / INTERNAL_ERROR` — непредвиденная внутренняя ошибка сервера. +- `410 / ADD_USER_DISABLED` — серверная регистрация отключена, используйте Solana-first flow. --- @@ -95,9 +70,8 @@ Важно: -- это временное решение; -- позже клиент должен проверять existence/identity напрямую через Solana; -- на финальный production flow не стоит жёстко завязывать архитектуру клиента на `GetUser`. +- это server-side existence-check; +- если пользователя нет в локальной БД, он может быть импортирован при авторизации из Solana PDA. ### Запрос @@ -209,7 +183,7 @@ ## 4. Короткое резюме -- `AddUser` — временная регистрация пользователя на сервере. -- `GetUser` — временная проверка существования пользователя на сервере. +- `AddUser` — отключен (`410 / ADD_USER_DISABLED`). +- `GetUser` — проверка существования пользователя на сервере. - `SearchUsers` — временный поиск пользователей по префиксу. -- И регистрация, и existence-check позже должны быть переведены на Solana. +- Регистрация выполняется только через Solana. diff --git a/Dev_Docs/API/02_Authentication_API.md b/Dev_Docs/API/02_Authentication_API.md index 93acc2d..73fe70f 100644 --- a/Dev_Docs/API/02_Authentication_API.md +++ b/Dev_Docs/API/02_Authentication_API.md @@ -73,6 +73,7 @@ ed25519/BASE64_PUBLIC_KEY - `400 / EMPTY_LOGIN` — пустой `login`. - `400 / ALREADY_AUTHED` — по текущему соединению уже выполнена авторизация. - `422 / UNKNOWN_USER` — пользователь с таким `login` не найден. +- `501 / SOLANA_IMPORT_FAILED` — сервер не смог проверить/импортировать пользователя из Solana при lazy-import. - `500 / INTERNAL_ERROR` — непредвиденная внутренняя ошибка сервера, если появится вне штатного сценария. --- diff --git a/Dev_Docs/API/09_Operations_Index.md b/Dev_Docs/API/09_Operations_Index.md index d53ebac..8bd4b4c 100644 --- a/Dev_Docs/API/09_Operations_Index.md +++ b/Dev_Docs/API/09_Operations_Index.md @@ -12,7 +12,7 @@ | Операция | Раздел документации | Кратко | | --- | --- | --- | -| `AddUser` | `01_User_Registration_API.md` | временная регистрация пользователя | +| `AddUser` | `01_User_Registration_API.md` | отключено (`410 / ADD_USER_DISABLED`) | | `GetUser` | `01_User_Registration_API.md` | чтение/проверка пользователя | | `SearchUsers` | `01_User_Registration_API.md` | поиск логинов по префиксу | | `AuthChallenge` | `02_Authentication_API.md` | challenge для создания новой сессии | diff --git a/VERSION.properties b/VERSION.properties index 8b28560..1526f59 100644 --- a/VERSION.properties +++ b/VERSION.properties @@ -1,2 +1,2 @@ -client.version=1.2.93 -server.version=1.2.87 +client.version=1.2.94 +server.version=1.2.88 diff --git a/shine-UI/js/pages/registration-payment-view.js b/shine-UI/js/pages/registration-payment-view.js index 63b182a..79636b6 100644 --- a/shine-UI/js/pages/registration-payment-view.js +++ b/shine-UI/js/pages/registration-payment-view.js @@ -15,6 +15,7 @@ import { registerUserOnSolana } from '../services/solana-register-service.js'; export const pageMeta = { id: 'registration-payment-view', title: 'Оплата регистрации', showAppChrome: false }; const MIN_REQUIRED_SOL = 0.01; +const SOLANA_SYNC_WAIT_SEC = 15; function parseBalanceSol(value) { const parsed = Number.parseFloat(String(value || '').replace(',', '.')); @@ -198,18 +199,7 @@ export function render({ navigate }) { } } - // Регистрация на сервере SHiNE - submitButton.textContent = 'Регистрация на сервере...'; - await authService.reconnect(state.entrySettings.shineServer); - const result = await authService.registerUserWithKeyBundle(state.registrationDraft.login, keyBundle); - state.registrationDraft.flowType = 'registration'; - state.registrationDraft.sessionId = result.sessionId; - state.registrationDraft.storagePwd = result.storagePwd; - state.registrationDraft.pendingKeyBundle = result.keyBundle; - state.registrationDraft.pendingSessionMaterial = result.sessionMaterial; - - setAuthInfo(`Регистрация завершена. Вы вошли как @${result.login}. Далее откройте вкладку «Каналы».`); - navigate('registration-keys-view'); + renderSolanaDoneStage({ navigate, status, keyBundle }); } catch (error) { const message = toUserMessage(error, 'Не удалось завершить регистрацию.'); setAuthError(message); @@ -254,3 +244,94 @@ export function render({ navigate }) { return screen; } + +function renderSolanaDoneStage({ navigate, status, keyBundle }) { + const screen = document.querySelector('section.stack'); + if (!screen) return; + const card = screen.querySelector('.card.stack'); + if (!card) return; + + let remainingSec = SOLANA_SYNC_WAIT_SEC; + let canTryLogin = false; + let timerId = null; + + const info = document.createElement('p'); + info.className = 'auth-copy'; + info.textContent = 'Регистрация в Solana прошла успешно.'; + + const hint = document.createElement('p'); + hint.className = 'meta-muted'; + hint.textContent = 'Подождите 10–15 секунд, пока запись обновится в блокчейне. После этого можно входить на сервер.'; + + const timer = document.createElement('p'); + timer.className = 'meta-muted'; + timer.textContent = `До попытки входа: ${remainingSec} сек`; + + const tryLoginBtn = document.createElement('button'); + tryLoginBtn.className = 'primary-btn'; + tryLoginBtn.type = 'button'; + tryLoginBtn.textContent = `Попробовать войти (${remainingSec})`; + tryLoginBtn.disabled = true; + + const backBtn = document.createElement('button'); + backBtn.className = 'ghost-btn'; + backBtn.type = 'button'; + backBtn.textContent = 'Назад'; + backBtn.addEventListener('click', () => navigate('register-view')); + + const stopTimer = () => { + if (timerId) { + window.clearInterval(timerId); + timerId = null; + } + }; + + const updateTimerUi = () => { + if (remainingSec > 0) { + timer.textContent = `До попытки входа: ${remainingSec} сек`; + tryLoginBtn.textContent = `Попробовать войти (${remainingSec})`; + tryLoginBtn.disabled = true; + } else { + canTryLogin = true; + timer.textContent = 'Можно входить на сервер.'; + tryLoginBtn.textContent = 'Попробовать войти на сервер'; + tryLoginBtn.disabled = false; + stopTimer(); + } + }; + + tryLoginBtn.addEventListener('click', async () => { + if (!canTryLogin) return; + status.style.display = 'none'; + tryLoginBtn.disabled = true; + tryLoginBtn.textContent = 'Вход...'; + try { + await authService.reconnect(state.entrySettings.shineServer); + const result = await authService.createSessionForExistingUser( + state.registrationDraft.login, + state.registrationDraft.password, + ); + state.registrationDraft.flowType = 'registration'; + state.registrationDraft.sessionId = result.sessionId; + state.registrationDraft.storagePwd = result.storagePwd; + state.registrationDraft.pendingKeyBundle = keyBundle; + state.registrationDraft.pendingSessionMaterial = result.sessionMaterial; + setAuthInfo(`Регистрация завершена. Вы вошли как @${result.login}. Далее откройте вкладку «Каналы».`); + navigate('registration-keys-view'); + } catch (error) { + status.className = 'status-line is-unavailable'; + status.textContent = toUserMessage(error, 'Пока не удалось войти. Попробуйте ещё раз через несколько секунд.'); + status.style.display = ''; + tryLoginBtn.disabled = false; + tryLoginBtn.textContent = 'Попробовать войти на сервер'; + } + }); + + card.innerHTML = ''; + card.append(info, hint, timer, tryLoginBtn, backBtn, status); + updateTimerUi(); + timerId = window.setInterval(() => { + remainingSec -= 1; + updateTimerUi(); + }, 1000); +} diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_AuthChallenge_Handler.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_AuthChallenge_Handler.java index 7cb0eb7..1aca1a9 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_AuthChallenge_Handler.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_AuthChallenge_Handler.java @@ -11,6 +11,8 @@ import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory; import server.logic.ws_protocol.WireCodes; import shine.db.dao.SolanaUsersDAO; import shine.db.entities.SolanaUserEntry; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.security.SecureRandom; @@ -30,6 +32,7 @@ import java.security.SecureRandom; */ public class Net_AuthChallenge_Handler implements JsonMessageHandler { + private static final Logger log = LoggerFactory.getLogger(Net_AuthChallenge_Handler.class); private static final SecureRandom RANDOM = new SecureRandom(); @Override @@ -58,6 +61,22 @@ public class Net_AuthChallenge_Handler implements JsonMessageHandler { } SolanaUserEntry solanaUserEntry = SolanaUsersDAO.getInstance().getByLogin(login); + if (solanaUserEntry == null) { + try { + solanaUserEntry = SolanaUserPdaImportService.findOrImportByLogin(login); + if (solanaUserEntry != null) { + log.info("AuthChallenge: пользователь {} импортирован из Solana PDA", solanaUserEntry.getLogin()); + } + } catch (Exception e) { + log.error("AuthChallenge: ошибка lazy-import пользователя {} из Solana", login, e); + return NetExceptionResponseFactory.error( + req, + WireCodes.Status.SERVER_DATA_ERROR, + "SOLANA_IMPORT_FAILED", + "Ошибка проверки пользователя в Solana" + ); + } + } if (solanaUserEntry == null) { return NetExceptionResponseFactory.error( req, @@ -84,4 +103,4 @@ public class Net_AuthChallenge_Handler implements JsonMessageHandler { return resp; } -} \ No newline at end of file +} diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/SolanaUserPdaImportService.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/SolanaUserPdaImportService.java new file mode 100644 index 0000000..ae197e0 --- /dev/null +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/SolanaUserPdaImportService.java @@ -0,0 +1,267 @@ +package server.logic.ws_protocol.JSON.handlers.auth; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import shine.db.dao.SolanaUsersDAO; +import shine.db.dao.UserCreateDAO; +import shine.db.entities.SolanaUserEntry; +import utils.config.SolanaProgramsConfig; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +/** + * Lazy-import пользователя из Solana PDA в локальную БД сервера. + * Используется при входе, если в solana_users нет записи по login. + */ +public final class SolanaUserPdaImportService { + + private static final Logger log = LoggerFactory.getLogger(SolanaUserPdaImportService.class); + private static final ObjectMapper MAPPER = new ObjectMapper(); + private static final HttpClient HTTP = HttpClient.newHttpClient(); + private static final int USER_PDA_SPACE = 768; + private static final String MAGIC = "SHiNE"; + + private SolanaUserPdaImportService() {} + + public static SolanaUserEntry findOrImportByLogin(String loginRaw) throws Exception { + String login = normalizeLogin(loginRaw); + if (login == null) return null; + + SolanaUsersDAO usersDao = SolanaUsersDAO.getInstance(); + SolanaUserEntry existing = usersDao.getByLogin(login); + if (existing != null) return existing; + + ParsedSolanaUser parsed = fetchFromSolana(login); + if (parsed == null) return null; + + long now = System.currentTimeMillis(); + long sizeLimit = parsed.paidLimitBytes > 0 ? parsed.paidLimitBytes : 100_000L; + boolean inserted = UserCreateDAO.getInstance().insertUserWithBlockchain( + parsed.login, + parsed.blockchainName, + parsed.deviceKeyB64, // в текущей модели solanaKey = deviceKey + parsed.blockchainKeyB64, + parsed.deviceKeyB64, + sizeLimit, + now + ); + if (!inserted) { + return usersDao.getByLogin(login); + } + return usersDao.getByLogin(login); + } + + private static ParsedSolanaUser fetchFromSolana(String login) throws Exception { + String loginB58 = toBase58(login.getBytes(StandardCharsets.UTF_8)); + String lenB58 = toBase58(new byte[]{(byte) login.length()}); + + String reqJson = """ + { + "jsonrpc":"2.0", + "id":1, + "method":"getProgramAccounts", + "params":[ + "%s", + { + "encoding":"base64", + "filters":[ + {"dataSize":%d}, + {"memcmp":{"offset":61,"bytes":"%s"}}, + {"memcmp":{"offset":62,"bytes":"%s"}} + ] + } + ] + } + """.formatted(SolanaProgramsConfig.SHINE_USERS_PROGRAM_ID, USER_PDA_SPACE, lenB58, loginB58); + + HttpRequest req = HttpRequest.newBuilder() + .uri(URI.create(SolanaProgramsConfig.SOLANA_RPC_URL)) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(reqJson)) + .build(); + + HttpResponse resp = HTTP.send(req, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + if (resp.statusCode() < 200 || resp.statusCode() >= 300) { + throw new IllegalStateException("Solana RPC HTTP status=" + resp.statusCode()); + } + + JsonNode root = MAPPER.readTree(resp.body()); + if (root.hasNonNull("error")) { + throw new IllegalStateException("Solana RPC error: " + root.get("error").toString()); + } + + JsonNode result = root.path("result"); + if (!result.isArray() || result.isEmpty()) return null; + + for (JsonNode item : result) { + JsonNode dataNode = item.path("account").path("data"); + if (!dataNode.isArray() || dataNode.size() < 1) continue; + String b64 = dataNode.get(0).asText(""); + if (b64.isBlank()) continue; + byte[] raw = Base64.getDecoder().decode(b64); + ParsedSolanaUser parsed = parseUserPda(raw); + if (parsed != null && parsed.login.equalsIgnoreCase(login)) { + return parsed; + } + } + return null; + } + + private static ParsedSolanaUser parseUserPda(byte[] raw) { + if (raw == null || raw.length < 128) return null; + if (!MAGIC.equals(new String(raw, 0, 5, StandardCharsets.UTF_8))) return null; + + int recordLen = u16le(raw, 7); + if (recordLen < 73 || recordLen > raw.length) return null; + + int c = 9; + c += 8; // created_at_ms + c += 8; // updated_at_ms + c += 4; // record_number + c += 32; // prev_record_hash + + int loginLen = u8(raw, c++); + if (loginLen <= 0 || c + loginLen > recordLen) return null; + String login = new String(raw, c, loginLen, StandardCharsets.UTF_8); + c += loginLen; + + int blocksCount = u8(raw, c++); + String blockchainName = null; + byte[] blockchainKey32 = null; + byte[] deviceKey32 = null; + long paidLimitBytes = 0L; + + for (int i = 0; i < blocksCount; i++) { + int blockType = u8(raw, c++); + int blockVer = u8(raw, c++); + if (blockVer != 0) return null; + + if (blockType == 1) { + c += 32; + } else if (blockType == 2) { + deviceKey32 = slice(raw, c, 32); + c += 32; + } else if (blockType == 3) { + int count = u8(raw, c++); + for (int j = 0; j < count; j++) { + c += 1; // blockchain_type + int bchLen = u8(raw, c++); + blockchainName = new String(raw, c, bchLen, StandardCharsets.UTF_8); + c += bchLen; + blockchainKey32 = slice(raw, c, 32); + c += 32; + paidLimitBytes = u64le(raw, c); + c += 8; + c += 8; // used_bytes + c += 4; // last_block_number + c += 32; // last_block_hash + c += 64; // last_block_signature + int arweavePresent = u8(raw, c++); + if (arweavePresent == 1) { + int arLen = u8(raw, c++); + c += arLen; + } else if (arweavePresent != 0) { + return null; + } + } + } else if (blockType == 4) { + c += 1 + 32; + int addrLen = u8(raw, c++); + c += addrLen; + int syncCount = u8(raw, c++); + for (int j = 0; j < syncCount; j++) { + int n = u8(raw, c++); + c += n; + } + } else if (blockType == 40) { + int accessCount = u8(raw, c++); + for (int j = 0; j < accessCount; j++) { + int n = u8(raw, c++); + c += n; + } + } else if (blockType == 50) { + c += 1; + } else { + return null; + } + + if (c > recordLen) return null; + } + + if (blockchainName == null || blockchainKey32 == null || deviceKey32 == null) return null; + return new ParsedSolanaUser( + login, + blockchainName, + Base64.getEncoder().encodeToString(blockchainKey32), + Base64.getEncoder().encodeToString(deviceKey32), + paidLimitBytes + ); + } + + private static String normalizeLogin(String login) { + if (login == null) return null; + String s = login.trim(); + if (s.isEmpty()) return null; + return s.toLowerCase(); + } + + private static int u8(byte[] b, int o) { return b[o] & 0xFF; } + private static int u16le(byte[] b, int o) { + return (b[o] & 0xFF) | ((b[o + 1] & 0xFF) << 8); + } + private static long u64le(byte[] b, int o) { + long out = 0L; + for (int i = 0; i < 8; i++) out |= ((long) (b[o + i] & 0xFF)) << (8 * i); + return out; + } + private static byte[] slice(byte[] b, int o, int len) { + byte[] out = new byte[len]; + System.arraycopy(b, o, out, 0, len); + return out; + } + + // Нужен для memcmp.bytes в Solana RPC. + private static String toBase58(byte[] input) { + final char[] alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz".toCharArray(); + int zeros = 0; + while (zeros < input.length && input[zeros] == 0) zeros++; + byte[] tmp = new byte[input.length * 2]; + int j = tmp.length; + int startAt = zeros; + while (startAt < input.length) { + int mod = divmod58(input, startAt); + if (input[startAt] == 0) startAt++; + tmp[--j] = (byte) alphabet[mod]; + } + while (j < tmp.length && tmp[j] == alphabet[0]) j++; + while (--zeros >= 0) tmp[--j] = (byte) alphabet[0]; + return new String(tmp, j, tmp.length - j, StandardCharsets.US_ASCII); + } + + private static int divmod58(byte[] number, int startAt) { + int remainder = 0; + for (int i = startAt; i < number.length; i++) { + int digit256 = number[i] & 0xFF; + int temp = remainder * 256 + digit256; + number[i] = (byte) (temp / 58); + remainder = temp % 58; + } + return remainder; + } + + private record ParsedSolanaUser( + String login, + String blockchainName, + String blockchainKeyB64, + String deviceKeyB64, + long paidLimitBytes + ) {} +} + diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/Net_AddUser_Handler.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/Net_AddUser_Handler.java index ff545c3..d2756b0 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/Net_AddUser_Handler.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/Net_AddUser_Handler.java @@ -1,185 +1,26 @@ package server.logic.ws_protocol.JSON.handlers.tempToTest; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import server.logic.ws_protocol.Base64Ws; import server.logic.ws_protocol.JSON.ConnectionContext; import server.logic.ws_protocol.JSON.entyties.Net_Request; import server.logic.ws_protocol.JSON.entyties.Net_Response; import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler; import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_AddUser_Request; -import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_AddUser_Response; import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory; -import server.logic.ws_protocol.WireCodes; -import shine.db.SqliteDbController; -import shine.db.dao.BlockchainStateDAO; -import shine.db.dao.SolanaUsersDAO; -import shine.db.entities.BlockchainStateEntry; -import shine.db.entities.SolanaUserEntry; -import utils.blockchain.BlockchainNameUtil; - -import java.sql.Connection; -import java.sql.SQLException; +/** + * AddUser отключен: регистрация работает через Solana. + */ public class Net_AddUser_Handler implements JsonMessageHandler { - private static final Logger log = LoggerFactory.getLogger(Net_AddUser_Handler.class); - - /** TEST ONLY */ - private static final int TEST_BCH_LIMIT = 1_000_000; - @Override public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) { Net_AddUser_Request req = (Net_AddUser_Request) baseRequest; - - if (req.getLogin() == null || req.getLogin().isBlank() - || req.getBlockchainName() == null || req.getBlockchainName().isBlank() - || req.getSolanaKey() == null || req.getSolanaKey().isBlank() - || req.getBlockchainKey() == null || req.getBlockchainKey().isBlank() - || req.getDeviceKey() == null || req.getDeviceKey().isBlank()) { - - return NetExceptionResponseFactory.error( - req, - WireCodes.Status.BAD_REQUEST, - "BAD_FIELDS", - "Некорректные поля: login/blockchainName/solanaKey/blockchainKey/deviceKey" - ); - } - - // blockchainName должен быть вида: -NNN - if (!BlockchainNameUtil.isBlockchainNameMatchesLogin(req.getBlockchainName(), req.getLogin())) { - return NetExceptionResponseFactory.error( - req, - WireCodes.Status.BAD_REQUEST, - "BAD_BLOCKCHAIN_NAME", - "blockchainName должен быть вида -NNN (пример: anya-001)" - ); - } - - int limit = (req.getBchLimit() == null || req.getBchLimit() <= 0) - ? TEST_BCH_LIMIT - : req.getBchLimit(); - - try { - // базовая валидация форматов ключей: Base64(32 bytes) - byte[] solanaKey32; - byte[] blockchainKey32; - byte[] deviceKey32; - - try { - solanaKey32 = Base64Ws.decodeLen(req.getSolanaKey(), 32, "solanaKey"); - blockchainKey32 = Base64Ws.decodeLen(req.getBlockchainKey(), 32, "blockchainKey"); - deviceKey32 = Base64Ws.decodeLen(req.getDeviceKey(), 32, "deviceKey"); - } catch (IllegalArgumentException e) { - return NetExceptionResponseFactory.error( - req, - WireCodes.Status.BAD_REQUEST, - "BAD_KEY_FORMAT", - e.getMessage() - ); - } - - // (переменные не используются дальше, но оставляем для ясности проверки длины) - if (solanaKey32.length != 32 || blockchainKey32.length != 32 || deviceKey32.length != 32) { - return NetExceptionResponseFactory.error( - req, - WireCodes.Status.BAD_REQUEST, - "BAD_KEY_FORMAT", - "solanaKey/blockchainKey/deviceKey должны быть Base64(32 bytes)" - ); - } - - SolanaUsersDAO usersDAO = SolanaUsersDAO.getInstance(); - BlockchainStateDAO stateDAO = BlockchainStateDAO.getInstance(); - - SqliteDbController db = SqliteDbController.getInstance(); - - try (Connection c = db.getConnection()) { - c.setAutoCommit(false); - - // 1. Проверяем, что пользователя нет (case-insensitive) - if (usersDAO.getByLogin(c, req.getLogin()) != null) { - return NetExceptionResponseFactory.error( - req, - 409, - "USER_ALREADY_EXISTS", - "Пользователь с таким login уже существует" - ); - } - - // 2. Проверяем, что blockchainName ещё нет (case-sensitive, как в БД) - if (usersDAO.existsByBlockchainName(c, req.getBlockchainName())) { - return NetExceptionResponseFactory.error( - req, - 409, - "BLOCKCHAIN_ALREADY_EXISTS", - "Пользователь с таким blockchainName уже существует" - ); - } - - // 3. На всякий случай оставляем старую проверку blockchain_state, - // потому что эта таблица нужна серверу (состояние цепочки/лимиты). - if (stateDAO.getByBlockchainName(c, req.getBlockchainName()) != null) { - return NetExceptionResponseFactory.error( - req, - 409, - "BLOCKCHAIN_STATE_ALREADY_EXISTS", - "blockchain_state уже существует" - ); - } - - // 4. Создаём пользователя (все поля теперь лежат в solana_users) - SolanaUserEntry user = new SolanaUserEntry(); - user.setLogin(req.getLogin()); - user.setBlockchainName(req.getBlockchainName()); - user.setSolanaKey(req.getSolanaKey()); - user.setBlockchainKey(req.getBlockchainKey()); - user.setDeviceKey(req.getDeviceKey()); - - usersDAO.insert(c, user); - - // 5. Создаём INITIAL blockchain_state (для работы сервера) - BlockchainStateEntry st = new BlockchainStateEntry(); - st.setBlockchainName(req.getBlockchainName()); - st.setLogin(req.getLogin()); - st.setBlockchainKey(req.getBlockchainKey()); // Base64(32) - st.setLastBlockNumber(-1); - st.setLastBlockHash(new byte[32]); - st.setFileSizeBytes(0); - st.setSizeLimit(limit); - st.setUpdatedAtMs(System.currentTimeMillis()); - - stateDAO.upsert(c, st); - - c.commit(); - } - - Net_AddUser_Response resp = new Net_AddUser_Response(); - resp.setOp(req.getOp()); - resp.setRequestId(req.getRequestId()); - resp.setStatus(WireCodes.Status.OK); - - log.info("✅ AddUser ok: login={}, blockchainName={}, limit={}", - req.getLogin(), req.getBlockchainName(), limit); - - return resp; - - } catch (SQLException e) { - log.error("❌ DB error AddUser", e); - return NetExceptionResponseFactory.error( - req, - WireCodes.Status.SERVER_DATA_ERROR, - "DB_ERROR", - "Ошибка БД" - ); - } catch (Exception e) { - log.error("❌ Internal error AddUser", e); - return NetExceptionResponseFactory.error( - req, - WireCodes.Status.INTERNAL_ERROR, - "INTERNAL_ERROR", - NetExceptionResponseFactory.detailedMessage("Внутренняя ошибка сервера при AddUser", e) - ); - } + return NetExceptionResponseFactory.error( + req, + 410, + "ADD_USER_DISABLED", + "Серверная регистрация AddUser отключена. Используйте регистрацию через Solana." + ); } } +