Запрос для работы с параметрами пользователя работают!! И тесты на них проходят!!
This commit is contained in:
AidarKC 2026-01-05 16:45:37 +03:00
parent eb122456ab
commit a6a5089379
2 changed files with 87 additions and 125 deletions

View File

@ -14,9 +14,14 @@ import java.util.List;
* - методы с Connection НЕ закрывают соединение * - методы с Connection НЕ закрывают соединение
* - методы без Connection сами открывают и закрывают соединение * - методы без Connection сами открывают и закрывают соединение
* *
* ВАЖНО по логике времени: * ЛОГИКА time_ms:
* - сам DAO делает "технический upsert" * - БД принимает запись только если она "новее" (time_ms строго больше текущего).
* - правила "не принимать более старый time_ms" должны проверяться в handler-е, в транзакции. * - Реализовано атомарно одним SQL: UPSERT + WHERE users_params.time_ms < excluded.time_ms
*
* Возврат результата:
* - upsertIfNewer(...) возвращает количество изменённых строк:
* 1 = вставили/обновили
* 0 = проигнорировали (запись уже новее или равная)
*/ */
public final class UserParamsDAO { public final class UserParamsDAO {
@ -34,10 +39,14 @@ public final class UserParamsDAO {
return instance; return instance;
} }
// -------------------- UPSERT -------------------- // -------------------- UPSERT (IF NEWER) --------------------
/** UPSERT с внешним соединением. Соединение НЕ закрывает. */ /**
public void upsert(Connection c, UserParamEntry e) throws SQLException { * Атомарный UPSERT "только если новее".
*
* @return 1 если вставили/обновили; 0 если запись не тронули (existing.time_ms >= incoming.time_ms).
*/
public int upsertIfNewer(Connection c, UserParamEntry e) throws SQLException {
String sql = """ String sql = """
INSERT INTO users_params ( INSERT INTO users_params (
login, login,
@ -53,6 +62,7 @@ public final class UserParamsDAO {
value = excluded.value, value = excluded.value,
device_key = excluded.device_key, device_key = excluded.device_key,
signature = excluded.signature signature = excluded.signature
WHERE users_params.time_ms < excluded.time_ms
"""; """;
try (PreparedStatement ps = c.prepareStatement(sql)) { try (PreparedStatement ps = c.prepareStatement(sql)) {
@ -67,14 +77,14 @@ public final class UserParamsDAO {
if (e.getSignature() != null) ps.setString(6, e.getSignature()); if (e.getSignature() != null) ps.setString(6, e.getSignature());
else ps.setNull(6, Types.VARCHAR); else ps.setNull(6, Types.VARCHAR);
ps.executeUpdate(); return ps.executeUpdate(); // 1 или 0
} }
} }
/** UPSERT без внешнего соединения. Сам открывает/закрывает. */ /** То же самое, но сам открывает/закрывает соединение. */
public void upsert(UserParamEntry e) throws SQLException { public int upsertIfNewer(UserParamEntry e) throws SQLException {
try (Connection c = db.getConnection()) { try (Connection c = db.getConnection()) {
upsert(c, e); return upsertIfNewer(c, e);
} }
} }

View File

@ -21,32 +21,22 @@ import utils.crypto.Ed25519Util;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.sql.Connection; import java.sql.Connection;
import java.sql.SQLException; import java.sql.SQLException;
import java.sql.Statement;
import java.util.Base64; import java.util.Base64;
/** /**
* Net_UpsertUserParam_Handler * Net_UpsertUserParam_Handler
* *
* Делает: * Делает (MVP, без "сессий"):
* 1) Проверяет, что пользователь существует и что device_key действительно его. * 1) Проверка входных полей.
* 2) Проверяет, что нет "более нового" значения этого param (time_ms монотонно растёт). * 2) Проверка подписи Ed25519 по device_key.
* 3) Проверяет подпись Ed25519 по device_key. * 3) Проверка, что пользователь существует и что device_key принадлежит этому login.
* 4) Пишет в БД только если time_ms строго больше текущего сохранённого. * 4) Атомарная запись в БД "только если time_ms новее" (UPSERT + WHERE).
* *
* БОЛЬШОЙ КОММЕНТ ПРО АВТОРИЗАЦИЮ НА БУДУЩЕЕ: * ВАЖНО:
* --------------------------------------------------------------------------------- * - НИКАКИХ ручных транзакций / BEGIN здесь нет.
* Сейчас (MVP) этот эндпоинт намеренно не делает полноценную "сессию/авторизацию", * - autoCommit=true, каждый statement завершённый сам по себе.
* потому что целостность обеспечивается криптографией: сервер проверяет подпись * - Гонки не страшны: если за время проверок кто-то записал более новый time_ms,
* и то, что device_key принадлежит login. * наш финальный UPSERT просто вернёт 0 обновлённых строк.
*
* В будущем, если понадобится "ограничить кто может писать параметры", можно добавить:
* - проверку активной сессии (active_sessions) и соответствие login в сессии;
* - rate-limit на пользователя;
* - отдельные права на запись конкретных param.
*
* Но возможно это вообще не потребуется, если модель безопасности строится
* строго на подписи и владении device_key (как сейчас).
* ---------------------------------------------------------------------------------
*/ */
public class Net_UpsertUserParam_Handler implements JsonMessageHandler { public class Net_UpsertUserParam_Handler implements JsonMessageHandler {
@ -79,6 +69,7 @@ public class Net_UpsertUserParam_Handler implements JsonMessageHandler {
final String signatureB64 = req.getSignature().trim(); final String signatureB64 = req.getSignature().trim();
try { try {
// ---------------- Base64 decode ----------------
byte[] pubKey32; byte[] pubKey32;
byte[] sig64; byte[] sig64;
try { try {
@ -110,6 +101,7 @@ public class Net_UpsertUserParam_Handler implements JsonMessageHandler {
); );
} }
// ---------------- Signature verify ----------------
String signText = ShineSignatureConstants.USER_PARAMETER_PREFIX String signText = ShineSignatureConstants.USER_PARAMETER_PREFIX
+ login + login
+ param + param
@ -128,22 +120,15 @@ public class Net_UpsertUserParam_Handler implements JsonMessageHandler {
); );
} }
// ---------------- DB checks + upsert ----------------
SqliteDbController db = SqliteDbController.getInstance(); SqliteDbController db = SqliteDbController.getInstance();
SolanaUsersDAO usersDAO = SolanaUsersDAO.getInstance(); SolanaUsersDAO usersDAO = SolanaUsersDAO.getInstance();
UserParamsDAO paramsDAO = UserParamsDAO.getInstance(); UserParamsDAO paramsDAO = UserParamsDAO.getInstance();
try (Connection c = db.getConnection()) { try (Connection c = db.getConnection()) {
boolean oldAuto = c.getAutoCommit(); // 1) user exists
c.setAutoCommit(false);
try (Statement st = c.createStatement()) {
st.execute("BEGIN IMMEDIATE");
}
try {
SolanaUserEntry user = usersDAO.getByLogin(c, login); SolanaUserEntry user = usersDAO.getByLogin(c, login);
if (user == null) { if (user == null) {
c.rollback();
return NetExceptionResponseFactory.error( return NetExceptionResponseFactory.error(
req, req,
404, 404,
@ -152,9 +137,9 @@ public class Net_UpsertUserParam_Handler implements JsonMessageHandler {
); );
} }
// 2) device key must match the user's stored deviceKey
String userDeviceKey = user.getDeviceKey(); String userDeviceKey = user.getDeviceKey();
if (userDeviceKey == null || userDeviceKey.isBlank()) { if (userDeviceKey == null || userDeviceKey.isBlank()) {
c.rollback();
return NetExceptionResponseFactory.error( return NetExceptionResponseFactory.error(
req, req,
WireCodes.Status.SERVER_DATA_ERROR, WireCodes.Status.SERVER_DATA_ERROR,
@ -164,7 +149,6 @@ public class Net_UpsertUserParam_Handler implements JsonMessageHandler {
} }
if (!userDeviceKey.trim().equals(deviceKeyB64)) { if (!userDeviceKey.trim().equals(deviceKeyB64)) {
c.rollback();
return NetExceptionResponseFactory.error( return NetExceptionResponseFactory.error(
req, req,
403, 403,
@ -173,35 +157,7 @@ public class Net_UpsertUserParam_Handler implements JsonMessageHandler {
); );
} }
UserParamEntry existing = paramsDAO.getByLoginAndParam(c, login, param); // 3) atomic upsert-if-newer
// если есть более новое запрет
if (existing != null && existing.getTimeMs() > timeMs) {
c.rollback();
return NetExceptionResponseFactory.error(
req,
409,
"PARAM_NEWER_EXISTS",
"Уже есть более новое значение этого параметра (time_ms больше)"
);
}
// если time_ms равен ничего не делаем (твой кейс)
if (existing != null && existing.getTimeMs() == timeMs) {
c.commit();
c.setAutoCommit(oldAuto);
Net_UpsertUserParam_Response resp = new Net_UpsertUserParam_Response();
resp.setOp(req.getOp());
resp.setRequestId(req.getRequestId());
resp.setStatus(WireCodes.Status.OK);
log.info(" UpsertUserParam noop (same time_ms): login={}, param={}, time_ms={}",
login, param, timeMs);
return resp;
}
// иначе existing==null или existingTime < timeMs -> пишем
UserParamEntry e = new UserParamEntry( UserParamEntry e = new UserParamEntry(
login, login,
param, param,
@ -211,25 +167,21 @@ public class Net_UpsertUserParam_Handler implements JsonMessageHandler {
signatureB64 signatureB64
); );
paramsDAO.upsert(c, e); int changed = paramsDAO.upsertIfNewer(c, e);
c.commit();
c.setAutoCommit(oldAuto);
Net_UpsertUserParam_Response resp = new Net_UpsertUserParam_Response(); Net_UpsertUserParam_Response resp = new Net_UpsertUserParam_Response();
resp.setOp(req.getOp()); resp.setOp(req.getOp());
resp.setRequestId(req.getRequestId()); resp.setRequestId(req.getRequestId());
resp.setStatus(WireCodes.Status.OK); resp.setStatus(WireCodes.Status.OK);
log.info("✅ UpsertUserParam ok: login={}, param={}, time_ms={}", login, param, timeMs); if (changed == 1) {
return resp; log.info("✅ UpsertUserParam applied: login={}, param={}, time_ms={}", login, param, timeMs);
} else {
} catch (SQLException ex) { // 0 строк значит в БД уже есть time_ms >= incoming
c.rollback(); log.info(" UpsertUserParam ignored (not newer): login={}, param={}, time_ms={}", login, param, timeMs);
throw ex;
} finally {
c.setAutoCommit(oldAuto);
} }
return resp;
} }
} catch (SQLException e) { } catch (SQLException e) {