Переделал и запросы и тесты (но ещё не тетил)
This commit is contained in:
AidarKC 2025-12-22 13:15:39 +03:00
parent 7f92dc5f51
commit c140e3aae4
16 changed files with 1444 additions and 216 deletions

View File

@ -40,6 +40,7 @@ public final class BlockchainStateDAO {
file_size_bytes, file_size_bytes,
last_global_number, last_global_number,
last_global_hash, last_global_hash,
updated_at_ms,
line0_last_number, line0_last_hash, line0_last_number, line0_last_hash,
line1_last_number, line1_last_hash, line1_last_number, line1_last_hash,
line2_last_number, line2_last_hash, line2_last_number, line2_last_hash,
@ -47,8 +48,7 @@ public final class BlockchainStateDAO {
line4_last_number, line4_last_hash, line4_last_number, line4_last_hash,
line5_last_number, line5_last_hash, line5_last_number, line5_last_hash,
line6_last_number, line6_last_hash, line6_last_number, line6_last_hash,
line7_last_number, line7_last_hash, line7_last_number, line7_last_hash
updated_at_ms
FROM blockchain_state FROM blockchain_state
WHERE blockchainName = ? WHERE blockchainName = ?
"""; """;
@ -81,6 +81,7 @@ public final class BlockchainStateDAO {
file_size_bytes, file_size_bytes,
last_global_number, last_global_number,
last_global_hash, last_global_hash,
updated_at_ms,
line0_last_number, line0_last_hash, line0_last_number, line0_last_hash,
line1_last_number, line1_last_hash, line1_last_number, line1_last_hash,
line2_last_number, line2_last_hash, line2_last_number, line2_last_hash,
@ -88,10 +89,9 @@ public final class BlockchainStateDAO {
line4_last_number, line4_last_hash, line4_last_number, line4_last_hash,
line5_last_number, line5_last_hash, line5_last_number, line5_last_hash,
line6_last_number, line6_last_hash, line6_last_number, line6_last_hash,
line7_last_number, line7_last_hash, line7_last_number, line7_last_hash
updated_at_ms
) VALUES ( ) VALUES (
?,?,?,?,?,?,?,?, ?,?,?,?,?,?,?,?,?,
?,?, ?,?,
?,?, ?,?,
?,?, ?,?,
@ -100,7 +100,7 @@ public final class BlockchainStateDAO {
?,?, ?,?,
?,?, ?,?,
?,?, ?,?,
? ?,?
) )
ON CONFLICT(blockchainName) ON CONFLICT(blockchainName)
DO UPDATE SET DO UPDATE SET
@ -111,6 +111,7 @@ public final class BlockchainStateDAO {
file_size_bytes = excluded.file_size_bytes, file_size_bytes = excluded.file_size_bytes,
last_global_number = excluded.last_global_number, last_global_number = excluded.last_global_number,
last_global_hash = excluded.last_global_hash, last_global_hash = excluded.last_global_hash,
updated_at_ms = excluded.updated_at_ms,
line0_last_number = excluded.line0_last_number, line0_last_number = excluded.line0_last_number,
line0_last_hash = excluded.line0_last_hash, line0_last_hash = excluded.line0_last_hash,
line1_last_number = excluded.line1_last_number, line1_last_number = excluded.line1_last_number,
@ -126,12 +127,12 @@ public final class BlockchainStateDAO {
line6_last_number = excluded.line6_last_number, line6_last_number = excluded.line6_last_number,
line6_last_hash = excluded.line6_last_hash, line6_last_hash = excluded.line6_last_hash,
line7_last_number = excluded.line7_last_number, line7_last_number = excluded.line7_last_number,
line7_last_hash = excluded.line7_last_hash, line7_last_hash = excluded.line7_last_hash
updated_at_ms = excluded.updated_at_ms
"""; """;
try (PreparedStatement ps = c.prepareStatement(sql)) { try (PreparedStatement ps = c.prepareStatement(sql)) {
int i = 1; int i = 1;
ps.setString(i++, e.getBlockchainName()); ps.setString(i++, e.getBlockchainName());
ps.setString(i++, nn(e.getLogin())); ps.setString(i++, nn(e.getLogin()));
ps.setString(i++, nn(e.getPublicKeyBase64())); ps.setString(i++, nn(e.getPublicKeyBase64()));
@ -140,19 +141,20 @@ public final class BlockchainStateDAO {
ps.setLong(i++, e.getFileSizeBytes()); ps.setLong(i++, e.getFileSizeBytes());
ps.setInt(i++, e.getLastGlobalNumber()); ps.setInt(i++, e.getLastGlobalNumber());
ps.setString(i++, nn(e.getLastGlobalHash())); ps.setString(i++, nn(e.getLastGlobalHash()));
ps.setLong(i++, e.getUpdatedAtMs());
for (int line = 0; line < 8; line++) { for (int line = 0; line < 8; line++) {
ps.setInt(i++, e.getLastLineNumber(line)); ps.setInt(i++, e.getLastLineNumber(line));
ps.setString(i++, nn(e.getLastLineHash(line))); ps.setString(i++, nn(e.getLastLineHash(line)));
} }
ps.setLong(i++, e.getUpdatedAtMs());
ps.executeUpdate(); ps.executeUpdate();
} }
} }
private BlockchainStateEntry mapRow(ResultSet rs) throws SQLException { private BlockchainStateEntry mapRow(ResultSet rs) throws SQLException {
BlockchainStateEntry e = new BlockchainStateEntry(); BlockchainStateEntry e = new BlockchainStateEntry();
e.setBlockchainName(rs.getString("blockchainName")); e.setBlockchainName(rs.getString("blockchainName"));
e.setLogin(rs.getString("login")); e.setLogin(rs.getString("login"));
e.setPublicKeyBase64(rs.getString("public_key_base64")); e.setPublicKeyBase64(rs.getString("public_key_base64"));
@ -164,12 +166,13 @@ public final class BlockchainStateDAO {
e.setLastGlobalNumber(rs.getInt("last_global_number")); e.setLastGlobalNumber(rs.getInt("last_global_number"));
e.setLastGlobalHash(rs.getString("last_global_hash")); e.setLastGlobalHash(rs.getString("last_global_hash"));
e.setUpdatedAtMs(rs.getLong("updated_at_ms"));
for (int line = 0; line < 8; line++) { for (int line = 0; line < 8; line++) {
e.setLastLineNumber(line, rs.getInt("line" + line + "_last_number")); e.setLastLineNumber(line, rs.getInt("line" + line + "_last_number"));
e.setLastLineHash(line, rs.getString("line" + line + "_last_hash")); e.setLastLineHash(line, rs.getString("line" + line + "_last_hash"));
} }
e.setUpdatedAtMs(rs.getLong("updated_at_ms"));
return e; return e;
} }

View File

@ -9,7 +9,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
* *
* Позволяет: * Позволяет:
* - получить ConnectionContext по sessionId; * - получить ConnectionContext по sessionId;
* - получить все активные подключения пользователя по loginId; * - получить все активные подключения пользователя по login;
* - удалить подключение при закрытии WebSocket. * - удалить подключение при закрытии WebSocket.
* *
* найти все подключения пользователя: * найти все подключения пользователя:
@ -34,27 +34,27 @@ public final class ActiveConnectionsRegistry {
// sessionId (String) -> ConnectionContext // sessionId (String) -> ConnectionContext
private final ConcurrentHashMap<String, ConnectionContext> bySessionId = new ConcurrentHashMap<>(); private final ConcurrentHashMap<String, ConnectionContext> bySessionId = new ConcurrentHashMap<>();
// loginId -> множество ConnectionContext для этого пользователя // login (String) -> множество ConnectionContext для этого пользователя
private final ConcurrentHashMap<Long, Set<ConnectionContext>> byLoginId = new ConcurrentHashMap<>(); private final ConcurrentHashMap<String, Set<ConnectionContext>> byLogin = new ConcurrentHashMap<>();
/** /**
* Зарегистрировать авторизованное подключение. * Зарегистрировать авторизованное подключение.
* Ожидается, что в ctx уже выставлены loginId и sessionId. * Ожидается, что в ctx уже выставлены login и sessionId.
*/ */
public void register(ConnectionContext ctx) { public void register(ConnectionContext ctx) {
if (ctx == null) return; if (ctx == null) return;
String sessionId = ctx.getSessionId(); String sessionId = ctx.getSessionId();
Long loginId = ctx.getLoginId(); String login = ctx.getLogin();
if (sessionId == null || loginId == null) { if (sessionId == null || login == null || login.isBlank()) {
return; return;
} }
bySessionId.put(sessionId, ctx); bySessionId.put(sessionId, ctx);
byLoginId byLogin
.computeIfAbsent(loginId, id -> new CopyOnWriteArraySet<>()) .computeIfAbsent(login, id -> new CopyOnWriteArraySet<>())
.add(ctx); .add(ctx);
} }
@ -65,18 +65,18 @@ public final class ActiveConnectionsRegistry {
if (ctx == null) return; if (ctx == null) return;
String sessionId = ctx.getSessionId(); String sessionId = ctx.getSessionId();
Long loginId = ctx.getLoginId(); String login = ctx.getLogin();
if (sessionId != null) { if (sessionId != null) {
bySessionId.remove(sessionId); bySessionId.remove(sessionId);
} }
if (loginId != null) { if (login != null) {
Set<ConnectionContext> set = byLoginId.get(loginId); Set<ConnectionContext> set = byLogin.get(login);
if (set != null) { if (set != null) {
set.remove(ctx); set.remove(ctx);
if (set.isEmpty()) { if (set.isEmpty()) {
byLoginId.remove(loginId); byLogin.remove(login);
} }
} }
} }
@ -90,13 +90,13 @@ public final class ActiveConnectionsRegistry {
ConnectionContext ctx = bySessionId.remove(sessionId); ConnectionContext ctx = bySessionId.remove(sessionId);
if (ctx != null) { if (ctx != null) {
Long loginId = ctx.getLoginId(); String login = ctx.getLogin();
if (loginId != null) { if (login != null) {
Set<ConnectionContext> set = byLoginId.get(loginId); Set<ConnectionContext> set = byLogin.get(login);
if (set != null) { if (set != null) {
set.remove(ctx); set.remove(ctx);
if (set.isEmpty()) { if (set.isEmpty()) {
byLoginId.remove(loginId); byLogin.remove(login);
} }
} }
} }
@ -112,10 +112,11 @@ public final class ActiveConnectionsRegistry {
} }
/** /**
* Получить все активные подключения пользователя по loginId. * Получить все активные подключения пользователя по login.
*/ */
public Set<ConnectionContext> getByLoginId(long loginId) { public Set<ConnectionContext> getByLogin(String login) {
Set<ConnectionContext> set = byLoginId.get(loginId); if (login == null) return Set.of();
Set<ConnectionContext> set = byLogin.get(login);
if (set == null) { if (set == null) {
return Set.of(); return Set.of();
} }

View File

@ -77,16 +77,12 @@ public class ConnectionContext {
this.activeSessionEntry = activeSessionEntry; this.activeSessionEntry = activeSessionEntry;
} }
// --- Удобные геттеры для логина --- // --- Удобный геттер для логина ---
public String getLogin() { public String getLogin() {
return solanaUserEntry != null ? solanaUserEntry.getLogin() : null; return solanaUserEntry != null ? solanaUserEntry.getLogin() : null;
} }
public Long getLoginId() {
return solanaUserEntry != null ? solanaUserEntry.getLoginId() : null;
}
// --- sessionId / sessionPwd --- // --- sessionId / sessionPwd ---
public String getSessionId() { public String getSessionId() {
@ -149,7 +145,6 @@ public class ConnectionContext {
public String toString() { public String toString() {
return "ConnectionContext{" + return "ConnectionContext{" +
"login='" + getLogin() + '\'' + "login='" + getLogin() + '\'' +
", loginId=" + getLoginId() +
", sessionId=" + sessionId + ", sessionId=" + sessionId +
", authenticationStatus=" + authenticationStatus + ", authenticationStatus=" + authenticationStatus +
'}'; '}';

View File

@ -4,17 +4,17 @@ import server.logic.ws_protocol.JSON.entyties.Net_Request;
public final class Net_AddBlock_Request extends Net_Request { public final class Net_AddBlock_Request extends Net_Request {
private String login; // обязателен private String login; // обязателен
private long blockchainId; // обязателен private String blockchainName; // обязателен
private int globalNumber; // обязателен private int globalNumber; // обязателен
private String prevGlobalHash; // HEX(64) или "" для нулевого private String prevGlobalHash; // HEX(64) или "" для нулевого
private String blockBytesB64; // байты FULL-блока (raw+sig+hash) в Base64 private String blockBytesB64; // байты FULL-блока (raw+sig+hash) в Base64
public String getLogin() { return login; } public String getLogin() { return login; }
public void setLogin(String login) { this.login = login; } public void setLogin(String login) { this.login = login; }
public long getBlockchainId() { return blockchainId; } public String getBlockchainName() { return blockchainName; }
public void setBlockchainId(long blockchainId) { this.blockchainId = blockchainId; } public void setBlockchainName(String blockchainName) { this.blockchainName = blockchainName; }
public int getGlobalNumber() { return globalNumber; } public int getGlobalNumber() { return globalNumber; }
public void setGlobalNumber(int globalNumber) { this.globalNumber = globalNumber; } public void setGlobalNumber(int globalNumber) { this.globalNumber = globalNumber; }

View File

@ -12,8 +12,7 @@ import server.logic.ws_protocol.JSON.entyties.Net_Request;
* "requestId": "test-add-1", * "requestId": "test-add-1",
* "payload": { * "payload": {
* "login": "anya", * "login": "anya",
* "loginId": 100211, * "blockchainName": "anya0001",
* "bchId": 4222,
* "loginKey": "base64-ed25519-public-key-login", * "loginKey": "base64-ed25519-public-key-login",
* "deviceKey": "base64-ed25519-public-key-device", * "deviceKey": "base64-ed25519-public-key-device",
* "bchLimit": 1000000 * "bchLimit": 1000000
@ -25,57 +24,23 @@ import server.logic.ws_protocol.JSON.entyties.Net_Request;
public class Net_AddUser_Request extends Net_Request { public class Net_AddUser_Request extends Net_Request {
private String login; private String login;
private long loginId; private String blockchainName;
private long bchId;
private String loginKey; private String loginKey;
private String deviceKey; private String deviceKey;
private Integer bchLimit; private Integer bchLimit;
public String getLogin() { public String getLogin() { return login; }
return login; public void setLogin(String login) { this.login = login; }
}
public void setLogin(String login) { public String getBlockchainName() { return blockchainName; }
this.login = login; public void setBlockchainName(String blockchainName) { this.blockchainName = blockchainName; }
}
public long getLoginId() { public String getLoginKey() { return loginKey; }
return loginId; public void setLoginKey(String loginKey) { this.loginKey = loginKey; }
}
public void setLoginId(long loginId) { public String getDeviceKey() { return deviceKey; }
this.loginId = loginId; public void setDeviceKey(String deviceKey) { this.deviceKey = deviceKey; }
}
public long getBchId() { public Integer getBchLimit() { return bchLimit; }
return bchId; public void setBchLimit(Integer bchLimit) { this.bchLimit = bchLimit; }
}
public void setBchId(long bchId) {
this.bchId = bchId;
}
public String getLoginKey() {
return loginKey;
}
public void setLoginKey(String loginKey) {
this.loginKey = loginKey;
}
public String getDeviceKey() {
return deviceKey;
}
public void setDeviceKey(String deviceKey) {
this.deviceKey = deviceKey;
}
public Integer getBchLimit() {
return bchLimit;
}
public void setBchLimit(Integer bchLimit) {
this.bchLimit = bchLimit;
}
} }

View File

@ -63,7 +63,7 @@ public class Net_CloseActiveSession_Handler implements JsonMessageHandler {
} }
SolanaUserEntry user = ctx.getSolanaUser(); SolanaUserEntry user = ctx.getSolanaUser();
long currentLoginId = user.getLoginId(); String currentLogin = user.getLogin();
int authStatus = ctx.getAuthenticationStatus(); int authStatus = ctx.getAuthenticationStatus();
if (authStatus != ConnectionContext.AUTH_STATUS_USER if (authStatus != ConnectionContext.AUTH_STATUS_USER
@ -180,7 +180,7 @@ public class Net_CloseActiveSession_Handler implements JsonMessageHandler {
); );
} }
if (targetSession.getLoginId() != currentLoginId) { if (currentLogin == null || !currentLogin.equals(targetSession.getLogin())) {
return NetExceptionResponseFactory.error( return NetExceptionResponseFactory.error(
req, req,
WireCodes.Status.UNVERIFIED, WireCodes.Status.UNVERIFIED,
@ -236,10 +236,7 @@ public class Net_CloseActiveSession_Handler implements JsonMessageHandler {
if (isCurrentSession && ctxToClose == currentCtx) { if (isCurrentSession && ctxToClose == currentCtx) {
// Это текущее подключение: закрываем после отправки ответа. // Это текущее подключение: закрываем после отправки ответа.
new Thread(() -> { new Thread(() -> {
try { try { Thread.sleep(50); } catch (InterruptedException ignored) {}
Thread.sleep(50); // небольшая пауза, чтобы ответ ушёл
} catch (InterruptedException ignored) {
}
WsConnectionUtils.closeConnection( WsConnectionUtils.closeConnection(
ctxToClose, ctxToClose,
4000, 4000,

View File

@ -25,52 +25,14 @@ import java.sql.SQLException;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.util.Base64; import java.util.Base64;
/**
* Шаг 2 авторизации: проверка подписи и создание сессии.
*
* Клиент присылает в payload:
* - storagePwd (base64 от 32 байт)
* - timeMs (long, мс с 1970-01-01)
* - signatureB64 (подпись Ed25519 над строкой:
* "AUTHORIFICATED:" + timeMs + authNonce)
* - clientInfo (опционально, до 50 символов)
*
* authNonce клиент получил на шаге 1 (AuthChallenge) и сервер
* сохранил его в ctx.authNonce.
*
* При успехе:
* - создаётся запись ActiveSession в БД;
* - генерируется sessionId (base64 от 32 случайных байт);
* - генерируется sessionPwd (base64 от 32 случайных байт);
* - sessionCreatedAtMs и lastAuthirificatedAtMs = текущее время;
* - заполняются поля clientIp, clientInfoFromClient, clientInfoFromRequest, userLanguage;
* - возвращается sessionId и sessionPwd в ответе.
*
* При ошибке авторификации (битые данные, подпись, время и т.п.)
* соединение закрывается через WsConnectionUtils.
*/
public class Net_CreateAuthSession__Handler implements JsonMessageHandler { public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
private static final Logger log = LoggerFactory.getLogger(Net_CreateAuthSession__Handler.class); private static final Logger log = LoggerFactory.getLogger(Net_CreateAuthSession__Handler.class);
private static final SecureRandom RANDOM = new SecureRandom(); private static final SecureRandom RANDOM = new SecureRandom();
/** Допустимое расхождение времени клиента и сервера (мс). */
public static final long ALLOWED_SKEW_MS = 30_000L; public static final long ALLOWED_SKEW_MS = 30_000L;
/**
* Общая проверка подписи Ed25519 над строкой:
* "AUTHORIFICATED:" + timeMs + authNonce.
*
* Используется и в CreateAuthSession, и в CloseActiveSession (для статуса AUTH_IN_PROGRESS).
*
* @param user пользователь (используется deviceKey)
* @param authNonce одноразовый nonce из шага 1
* @param timeMs время на стороне клиента
* @param signatureB64 подпись в base64
* @return true подпись корректна; false подпись не проходит верификацию
* @throws IllegalArgumentException при некорректном base64 ключа/подписи
*/
public static boolean verifyAuthorificatedSignature( public static boolean verifyAuthorificatedSignature(
SolanaUserEntry user, SolanaUserEntry user,
String authNonce, String authNonce,
@ -92,7 +54,6 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception { public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
Net_CreateAuthSession_Request req = (Net_CreateAuthSession_Request) baseReq; Net_CreateAuthSession_Request req = (Net_CreateAuthSession_Request) baseReq;
// --- базовые проверки контекста шага 1 ---
if (ctx == null if (ctx == null
|| ctx.getSolanaUser() == null || ctx.getSolanaUser() == null
|| ctx.getAuthNonce() == null || ctx.getAuthNonce() == null
@ -109,15 +70,15 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
} }
SolanaUserEntry user = ctx.getSolanaUser(); SolanaUserEntry user = ctx.getSolanaUser();
Long loginId = user.getLoginId(); String login = user.getLogin();
if (loginId == null) { if (login == null || login.isBlank()) {
Net_Response err = NetExceptionResponseFactory.error( Net_Response err = NetExceptionResponseFactory.error(
req, req,
WireCodes.Status.SERVER_DATA_ERROR, WireCodes.Status.SERVER_DATA_ERROR,
"NO_LOGIN_ID", "NO_LOGIN",
"Для пользователя не задан loginId в БД" "Для пользователя не задан login в БД"
); );
WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: no loginId"); WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: no login");
return err; return err;
} }
@ -160,13 +121,11 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
return err; return err;
} }
// Короткая строка clientInfo от клиента (до 50 символов)
String clientInfoFromClient = req.getClientInfo(); String clientInfoFromClient = req.getClientInfo();
if (clientInfoFromClient != null && clientInfoFromClient.length() > 50) { if (clientInfoFromClient != null && clientInfoFromClient.length() > 50) {
clientInfoFromClient = clientInfoFromClient.substring(0, 50); clientInfoFromClient = clientInfoFromClient.substring(0, 50);
} }
// --- выбираем публичный ключ pubkey1 ---
String pubKeyB64 = user.getDeviceKey(); String pubKeyB64 = user.getDeviceKey();
if (pubKeyB64 == null || pubKeyB64.isBlank()) { if (pubKeyB64 == null || pubKeyB64.isBlank()) {
Net_Response err = NetExceptionResponseFactory.error( Net_Response err = NetExceptionResponseFactory.error(
@ -179,10 +138,8 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
return err; return err;
} }
// --- authNonce (challenge) мы сохранили в ctx.authNonce на шаге 1 ---
String authNonce = ctx.getAuthNonce(); String authNonce = ctx.getAuthNonce();
// --- проверяем подпись через общий метод ---
boolean sigOk; boolean sigOk;
try { try {
sigOk = verifyAuthorificatedSignature(user, authNonce, timeMs, signatureB64); sigOk = verifyAuthorificatedSignature(user, authNonce, timeMs, signatureB64);
@ -230,10 +187,7 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
} }
} }
} }
if (clientIp == null) clientIp = "";
if (clientIp == null) {
clientIp = "";
}
// --- создаём запись ActiveSession и сохраняем в БД --- // --- создаём запись ActiveSession и сохраняем в БД ---
ActiveSessionsDAO dao = ActiveSessionsDAO.getInstance(); ActiveSessionsDAO dao = ActiveSessionsDAO.getInstance();
@ -242,8 +196,8 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
try { try {
activeSessionEntry = new ActiveSessionEntry( activeSessionEntry = new ActiveSessionEntry(
sessionId, sessionId,
loginId, login,
newSessionPwd, // настоящий секрет сессии newSessionPwd,
storagePwd, storagePwd,
now, now,
now, now,
@ -258,7 +212,7 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
dao.insert(activeSessionEntry); dao.insert(activeSessionEntry);
} catch (SQLException e) { } catch (SQLException e) {
log.error("Ошибка БД при создании новой сессии для loginId={}", loginId, e); log.error("Ошибка БД при создании новой сессии для login={}", login, e);
Net_Response err = NetExceptionResponseFactory.error( Net_Response err = NetExceptionResponseFactory.error(
req, req,
WireCodes.Status.SERVER_DATA_ERROR, WireCodes.Status.SERVER_DATA_ERROR,

View File

@ -51,7 +51,7 @@ public class Net_ListSessions_Handler implements JsonMessageHandler {
} }
SolanaUserEntry user = ctx.getSolanaUser(); SolanaUserEntry user = ctx.getSolanaUser();
long currentLoginId = user.getLoginId(); String currentLogin = user.getLogin();
int authStatus = ctx.getAuthenticationStatus(); int authStatus = ctx.getAuthenticationStatus();
if (authStatus != ConnectionContext.AUTH_STATUS_USER if (authStatus != ConnectionContext.AUTH_STATUS_USER
@ -130,9 +130,9 @@ public class Net_ListSessions_Handler implements JsonMessageHandler {
// 3) Тянем все активные сессии пользователя из БД // 3) Тянем все активные сессии пользователя из БД
List<ActiveSessionEntry> sessions; List<ActiveSessionEntry> sessions;
try { try {
sessions = ActiveSessionsDAO.getInstance().getByLoginId(currentLoginId); sessions = ActiveSessionsDAO.getInstance().getByLogin(currentLogin);
} catch (SQLException e) { } catch (SQLException e) {
log.error("Ошибка БД при получении списка сессий для loginId={}", currentLoginId, e); log.error("Ошибка БД при получении списка сессий для login={}", currentLogin, e);
return NetExceptionResponseFactory.error( return NetExceptionResponseFactory.error(
req, req,
WireCodes.Status.SERVER_DATA_ERROR, WireCodes.Status.SERVER_DATA_ERROR,

View File

@ -95,14 +95,15 @@ public class Net_RefreshSession_Handler implements JsonMessageHandler {
); );
} }
// --- вытаскиваем пользователя по loginId --- // --- вытаскиваем пользователя по login из сессии ---
SolanaUserEntry solanaUserEntry = null; SolanaUserEntry solanaUserEntry;
long loginId = session.getLoginId(); String login = session.getLogin();
try { try {
SolanaUsersDAO usersDao = SolanaUsersDAO.getInstance(); SolanaUsersDAO usersDao = SolanaUsersDAO.getInstance();
solanaUserEntry = usersDao.getByLoginId(loginId); solanaUserEntry = usersDao.getByLogin(login);
} catch (SQLException e) { } catch (SQLException e) {
log.error("Ошибка БД при поиске пользователя по loginId={} из сессии", loginId, e); log.error("Ошибка БД при поиске пользователя по login={} из сессии", login, e);
return NetExceptionResponseFactory.error( return NetExceptionResponseFactory.error(
req, req,
WireCodes.Status.SERVER_DATA_ERROR, WireCodes.Status.SERVER_DATA_ERROR,

View File

@ -4,11 +4,11 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantLock;
public final class BlockchainLocks { public final class BlockchainLocks {
private static final ConcurrentHashMap<Long, ReentrantLock> MAP = new ConcurrentHashMap<>(); private static final ConcurrentHashMap<String, ReentrantLock> MAP = new ConcurrentHashMap<>();
private BlockchainLocks() {} private BlockchainLocks() {}
public static ReentrantLock lockFor(long blockchainId) { public static ReentrantLock lockFor(String blockchainName) {
return MAP.computeIfAbsent(blockchainId, id -> new ReentrantLock(true)); // fair=true return MAP.computeIfAbsent(blockchainName, id -> new ReentrantLock(true)); // fair=true
} }
} }

View File

@ -11,7 +11,6 @@ import shine.db.entities.SolanaUserEntry;
import java.sql.Connection; import java.sql.Connection;
import java.sql.SQLException; import java.sql.SQLException;
import java.sql.Types;
import java.util.Base64; import java.util.Base64;
/** /**
@ -28,9 +27,9 @@ public final class BlockchainStateService_new {
/** Результат атомарного addBlock */ /** Результат атомарного addBlock */
public static final class AddBlockResult { public static final class AddBlockResult {
public final int lineIndex; // 0..7 (пока ставим 0) public final int lineIndex; // 0..7 (пока ставим 0)
public final int httpStatus; // WireCodes.Status.* public final int httpStatus; // WireCodes.Status.*
public final String reasonCode; // null если ok public final String reasonCode; // null если ok
public final BlockchainStateEntry stateAfter; // состояние после (может быть null) public final BlockchainStateEntry stateAfter; // состояние после (может быть null)
public AddBlockResult(int lineIndex, int httpStatus, String reasonCode, BlockchainStateEntry stateAfter) { public AddBlockResult(int lineIndex, int httpStatus, String reasonCode, BlockchainStateEntry stateAfter) {
@ -69,7 +68,7 @@ public final class BlockchainStateService_new {
*/ */
public AddBlockResult addBlockAtomically( public AddBlockResult addBlockAtomically(
String login, String login,
long blockchainId, String blockchainName,
int globalNumber, int globalNumber,
String prevGlobalHash, String prevGlobalHash,
String blockBytesB64 String blockBytesB64
@ -91,11 +90,21 @@ public final class BlockchainStateService_new {
); );
} }
if (login == null || login.isBlank()) {
return new AddBlockResult(lineIndex, WireCodes.Status.BAD_REQUEST, "empty_login", null);
}
if (blockchainName == null || blockchainName.isBlank()) {
return new AddBlockResult(lineIndex, WireCodes.Status.BAD_REQUEST, "empty_blockchain_name", null);
}
if (blockBytes == null || blockBytes.length == 0) {
return new AddBlockResult(lineIndex, WireCodes.Status.BAD_REQUEST, "empty_block_bytes", null);
}
try (Connection c = db.getConnection()) { try (Connection c = db.getConnection()) {
boolean oldAutoCommit = c.getAutoCommit(); boolean oldAutoCommit = c.getAutoCommit();
c.setAutoCommit(false); c.setAutoCommit(false);
try { try {
// 1) получаем loginId по login // 1) получаем пользователя по login (если надо валидировать существование)
SolanaUserEntry u = solanaUsersDAO.getByLogin(c, login); SolanaUserEntry u = solanaUsersDAO.getByLogin(c, login);
if (u == null) { if (u == null) {
c.rollback(); c.rollback();
@ -106,13 +115,12 @@ public final class BlockchainStateService_new {
null null
); );
} }
long loginId = u.getLoginId();
// 2) вставляем блок в blocks // 2) вставляем блок в blocks
insertBlockRow(c, loginId, blockchainId, globalNumber, prevGlobalHash, blockBytes, lineIndex); insertBlockRow(c, login, blockchainName, globalNumber, prevGlobalHash, blockBytes, lineIndex);
// 3) обновляем агрегатное состояние blockchain_state // 3) обновляем агрегатное состояние blockchain_state (по blockchainName)
BlockchainStateEntry st = stateDAO.getByBlockchainId(c, blockchainId); BlockchainStateEntry st = stateDAO.getByBlockchainName(c, blockchainName);
if (st == null) { if (st == null) {
c.rollback(); c.rollback();
return new AddBlockResult( return new AddBlockResult(
@ -124,7 +132,6 @@ public final class BlockchainStateService_new {
} }
// MVP: обновляем последний глобальный номер. // MVP: обновляем последний глобальный номер.
// Хэш тут сейчас оставлен как заглушка лучше поставить фактический хэш нового блока.
st.setLastGlobalNumber(globalNumber); st.setLastGlobalNumber(globalNumber);
st.setLastGlobalHash(nn(prevGlobalHash)); // TODO: заменить на hash нового блока st.setLastGlobalHash(nn(prevGlobalHash)); // TODO: заменить на hash нового блока
st.setUpdatedAtMs(System.currentTimeMillis()); st.setUpdatedAtMs(System.currentTimeMillis());
@ -158,8 +165,8 @@ public final class BlockchainStateService_new {
private void insertBlockRow( private void insertBlockRow(
Connection c, Connection c,
long loginId, String login,
long blockchainId, String blockchainName,
int globalNumber, int globalNumber,
String prevGlobalHash, String prevGlobalHash,
byte[] blockBytes, byte[] blockBytes,
@ -167,8 +174,9 @@ public final class BlockchainStateService_new {
) throws SQLException { ) throws SQLException {
BlockEntry e = new BlockEntry(); BlockEntry e = new BlockEntry();
e.setLoginId(loginId);
e.setBlockchainId(blockchainId); e.setLogin(login);
e.setBchName(blockchainName);
e.setBlockGlobalNumber(globalNumber); e.setBlockGlobalNumber(globalNumber);
e.setBlockGlobalPreHashe(nn(prevGlobalHash)); e.setBlockGlobalPreHashe(nn(prevGlobalHash));
@ -182,10 +190,11 @@ public final class BlockchainStateService_new {
e.setBlockByte(blockBytes); e.setBlockByte(blockBytes);
e.setToLoginId(0); // NEW: nullable ссылки (не забиваем фейковыми нулями)
e.setToBlockchainId(0); e.setToLogin(null);
e.setToBlockGlobalNumber(0); e.setToBchName(null);
e.setToBlockHashe(""); e.setToBlockGlobalNumber(null);
e.setToBlockHashe(null);
blocksDAO.upsert(c, e); blocksDAO.upsert(c, e);
} }

View File

@ -16,7 +16,7 @@ public final class Net_AddBlock_new_Handler implements JsonMessageHandler {
var r = BlockchainStateService_new.getInstance().addBlockAtomically( var r = BlockchainStateService_new.getInstance().addBlockAtomically(
req.getLogin(), req.getLogin(),
req.getBlockchainId(), req.getBlockchainName(),
req.getGlobalNumber(), req.getGlobalNumber(),
req.getPrevGlobalHash(), req.getPrevGlobalHash(),
req.getBlockBytesB64() req.getBlockBytesB64()

View File

@ -27,16 +27,15 @@ public class Net_AddUser_Handler implements JsonMessageHandler {
Net_AddUser_Request req = (Net_AddUser_Request) baseRequest; Net_AddUser_Request req = (Net_AddUser_Request) baseRequest;
if (req.getLogin() == null || req.getLogin().isBlank() if (req.getLogin() == null || req.getLogin().isBlank()
|| req.getBlockchainName() == null || req.getBlockchainName().isBlank()
|| req.getLoginKey() == null || req.getLoginKey().isBlank() || req.getLoginKey() == null || req.getLoginKey().isBlank()
|| req.getDeviceKey() == null || req.getDeviceKey().isBlank() || req.getDeviceKey() == null || req.getDeviceKey().isBlank()) {
|| req.getLoginId() <= 0
|| req.getBchId() <= 0) {
return NetExceptionResponseFactory.error( return NetExceptionResponseFactory.error(
req, req,
WireCodes.Status.BAD_REQUEST, WireCodes.Status.BAD_REQUEST,
"BAD_FIELDS", "BAD_FIELDS",
"Некорректные поля: login/loginId/bchId/loginKey/deviceKey" "Некорректные поля: login/blockchainName/loginKey/deviceKey"
); );
} }
@ -47,9 +46,8 @@ public class Net_AddUser_Handler implements JsonMessageHandler {
SolanaUsersDAO dao = SolanaUsersDAO.getInstance(); SolanaUsersDAO dao = SolanaUsersDAO.getInstance();
SolanaUserEntry user = new SolanaUserEntry( SolanaUserEntry user = new SolanaUserEntry(
req.getLoginId(),
req.getLogin(), req.getLogin(),
req.getBchId(), req.getBlockchainName(),
req.getLoginKey(), req.getLoginKey(),
req.getDeviceKey(), req.getDeviceKey(),
limit limit
@ -62,8 +60,8 @@ public class Net_AddUser_Handler implements JsonMessageHandler {
resp.setRequestId(req.getRequestId()); resp.setRequestId(req.getRequestId());
resp.setStatus(WireCodes.Status.OK); resp.setStatus(WireCodes.Status.OK);
log.info("✅ AddUser ok: login={}, loginId={}, bchId={}, limit={}", log.info("✅ AddUser ok: login={}, blockchainName={}, limit={}",
req.getLogin(), req.getLoginId(), req.getBchId(), limit); req.getLogin(), req.getBlockchainName(), limit);
return resp; return resp;

View File

@ -24,7 +24,8 @@ public class Test_AddBlock_new_NoAuth {
private static final ObjectMapper JSON = new ObjectMapper(); private static final ObjectMapper JSON = new ObjectMapper();
private static final String TEST_LOGIN = "anya24"; private static final String TEST_LOGIN = "anya24";
private static final long TEST_BCH_ID = 4222L; // По твоему правилу: blockchainName = login + 4 цифры
private static final String TEST_BCH_NAME = TEST_LOGIN + "0001";
private static final byte[] LOGIN_PRIV_KEY; private static final byte[] LOGIN_PRIV_KEY;
private static final byte[] LOGIN_PUB_KEY; private static final byte[] LOGIN_PUB_KEY;
@ -66,7 +67,7 @@ public class Test_AddBlock_new_NoAuth {
String json = buildAddBlockJson( String json = buildAddBlockJson(
"test-add-header", "test-add-header",
TEST_BCH_ID, TEST_BCH_NAME,
0, 0,
ZERO64, // prevGlobalHash для первого блока нули ZERO64, // prevGlobalHash для первого блока нули
base64(headerFull) base64(headerFull)
@ -124,7 +125,7 @@ public class Test_AddBlock_new_NoAuth {
String json2 = buildAddBlockJson( String json2 = buildAddBlockJson(
"test-add-text", "test-add-text",
TEST_BCH_ID, TEST_BCH_NAME,
1, 1,
lastGlobalHashHex, // prevGlobalHash = хэш header'а из ответа сервера lastGlobalHashHex, // prevGlobalHash = хэш header'а из ответа сервера
base64(textFull) base64(textFull)
@ -181,7 +182,7 @@ public class Test_AddBlock_new_NoAuth {
byte[] prevLineHash32) { byte[] prevLineHash32) {
HeaderBody body = new HeaderBody( HeaderBody body = new HeaderBody(
TEST_BCH_ID, TEST_BCH_NAME, // было TEST_BCH_ID (long), теперь имя блокчейна (String)
TEST_LOGIN, TEST_LOGIN,
0, 0, 0, 0,
(short) 1, (short) 1,
@ -259,17 +260,17 @@ public class Test_AddBlock_new_NoAuth {
// ================================================================================= // =================================================================================
private static String buildAddBlockJson(String requestId, private static String buildAddBlockJson(String requestId,
long blockchainId, String blockchainName,
int globalNumber, int globalNumber,
String prevGlobalHashHex, String prevGlobalHashHex,
String blockBytesB64) { String blockBytesB64) {
return """ return """
{ {
"op": "AddBlock", "op": "AddBlock",
"requestId": "%s", "requestId": "%s",
"payload": { "payload": {
"login": "%s", "login": "%s",
"blockchainId": %d, "blockchainName": "%s",
"globalNumber": %d, "globalNumber": %d,
"prevGlobalHash": "%s", "prevGlobalHash": "%s",
"blockBytesB64": "%s" "blockBytesB64": "%s"

View File

@ -57,8 +57,8 @@ public class Test_AddUser_and_Authorification {
// Тестовые данные пользователя // Тестовые данные пользователя
private static final String TEST_LOGIN = "anya24"; private static final String TEST_LOGIN = "anya24";
private static final long TEST_LOGIN_ID = 1030120L; // По твоему правилу: blockchainName = login + 4 цифры
private static final long TEST_BCH_ID = 4222L; private static final String TEST_BCH_NAME = TEST_LOGIN + "0001";
private static final int TEST_BCH_LIMIT = 1_000_000; private static final int TEST_BCH_LIMIT = 1_000_000;
// Краткая строка clientInfo, которую клиент шлёт // Краткая строка clientInfo, которую клиент шлёт
@ -399,13 +399,6 @@ public class Test_AddUser_and_Authorification {
// SCENARIO 3 / 5 / 7: ListSessions // SCENARIO 3 / 5 / 7: ListSessions
// ========================================================== // ==========================================================
/**
* Общий сценарий: AuthChallenge ListSessions в статусе AUTH_IN_PROGRESS.
*
* @param title заголовок для вывода
* @param expectSession1Present ожидать ли первую сессию в списке
* @param expectSession2Present ожидать ли вторую сессию в списке
*/
private static void scenario3_ListSessions_AuthInProgress( private static void scenario3_ListSessions_AuthInProgress(
String title, String title,
boolean expectSession1Present, boolean expectSession1Present,
@ -476,8 +469,8 @@ public class Test_AddUser_and_Authorification {
boolean ok = boolean ok =
status == 200 status == 200
&& (expectSession1Present == has1) && (expectSession1Present == has1)
&& (expectSession2Present == has2); && (expectSession2Present == has2);
printTestResult( printTestResult(
"S-List/ListSessions (ожидаемые сессии)", "S-List/ListSessions (ожидаемые сессии)",
@ -767,8 +760,7 @@ public class Test_AddUser_and_Authorification {
"requestId": "test-add-1", "requestId": "test-add-1",
"payload": { "payload": {
"login": "%s", "login": "%s",
"loginId": %d, "bchName": "%s",
"bchId": %d,
"loginKey": "%s", "loginKey": "%s",
"deviceKey": "%s", "deviceKey": "%s",
"bchLimit": %d "bchLimit": %d
@ -776,8 +768,7 @@ public class Test_AddUser_and_Authorification {
} }
""".formatted( """.formatted(
TEST_LOGIN, TEST_LOGIN,
TEST_LOGIN_ID, TEST_BCH_NAME,
TEST_BCH_ID,
LOGIN_PUBKEY_B64, // loginKey LOGIN_PUBKEY_B64, // loginKey
DEVICE_PUBKEY_B64, // deviceKey DEVICE_PUBKEY_B64, // deviceKey
TEST_BCH_LIMIT TEST_BCH_LIMIT