05 01 25
Дабавил соранение параметра пользователя. (бд и запрос на добавление), но пока не проверял
This commit is contained in:
parent
dd49c4de00
commit
bfffe44c4a
@ -125,10 +125,9 @@ public class DatabaseInitializer {
|
|||||||
CREATE TABLE IF NOT EXISTS users_params (
|
CREATE TABLE IF NOT EXISTS users_params (
|
||||||
login TEXT NOT NULL,
|
login TEXT NOT NULL,
|
||||||
param TEXT NOT NULL,
|
param TEXT NOT NULL,
|
||||||
bch_channel_id INTEGER NOT NULL DEFAULT 0,
|
|
||||||
value TEXT,
|
|
||||||
time_ms INTEGER NOT NULL,
|
time_ms INTEGER NOT NULL,
|
||||||
pubkey_num INTEGER NOT NULL,
|
value TEXT NOT NULL,
|
||||||
|
device_key TEXT,
|
||||||
signature TEXT,
|
signature TEXT,
|
||||||
FOREIGN KEY (login) REFERENCES solana_users(login),
|
FOREIGN KEY (login) REFERENCES solana_users(login),
|
||||||
UNIQUE (login, param)
|
UNIQUE (login, param)
|
||||||
|
|||||||
@ -7,7 +7,17 @@ import java.sql.*;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/** Здесь храним сохранённые параметры пользователей (в основном до какого сообщения просмотрены ленты) */
|
/**
|
||||||
|
* UserParamsDAO — хранение сохранённых параметров пользователя.
|
||||||
|
*
|
||||||
|
* Правило:
|
||||||
|
* - методы с Connection НЕ закрывают соединение
|
||||||
|
* - методы без Connection сами открывают и закрывают соединение
|
||||||
|
*
|
||||||
|
* ВАЖНО по логике времени:
|
||||||
|
* - сам DAO делает "технический upsert"
|
||||||
|
* - правила "не принимать более старый time_ms" должны проверяться в handler-е, в транзакции.
|
||||||
|
*/
|
||||||
public final class UserParamsDAO {
|
public final class UserParamsDAO {
|
||||||
|
|
||||||
private static volatile UserParamsDAO instance;
|
private static volatile UserParamsDAO instance;
|
||||||
@ -27,65 +37,68 @@ public final class UserParamsDAO {
|
|||||||
// -------------------- UPSERT --------------------
|
// -------------------- UPSERT --------------------
|
||||||
|
|
||||||
/** UPSERT с внешним соединением. Соединение НЕ закрывает. */
|
/** UPSERT с внешним соединением. Соединение НЕ закрывает. */
|
||||||
public void upsert(Connection c, UserParamEntry param) throws SQLException {
|
public void upsert(Connection c, UserParamEntry e) throws SQLException {
|
||||||
String sql = """
|
String sql = """
|
||||||
INSERT INTO users_params (
|
INSERT INTO users_params (
|
||||||
login,
|
login,
|
||||||
param,
|
param,
|
||||||
bch_channel_id,
|
|
||||||
value,
|
|
||||||
time_ms,
|
time_ms,
|
||||||
pubkey_num,
|
value,
|
||||||
|
device_key,
|
||||||
signature
|
signature
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
) VALUES (?, ?, ?, ?, ?, ?)
|
||||||
ON CONFLICT(login, param)
|
ON CONFLICT(login, param)
|
||||||
DO UPDATE SET
|
DO UPDATE SET
|
||||||
bch_channel_id = excluded.bch_channel_id,
|
|
||||||
value = excluded.value,
|
|
||||||
time_ms = excluded.time_ms,
|
time_ms = excluded.time_ms,
|
||||||
pubkey_num = excluded.pubkey_num,
|
value = excluded.value,
|
||||||
|
device_key = excluded.device_key,
|
||||||
signature = excluded.signature
|
signature = excluded.signature
|
||||||
""";
|
""";
|
||||||
|
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
ps.setString(1, param.getLogin());
|
ps.setString(1, e.getLogin());
|
||||||
ps.setString(2, param.getParam());
|
ps.setString(2, e.getParam());
|
||||||
ps.setLong(3, param.getBchChannelId());
|
ps.setLong(3, e.getTimeMs());
|
||||||
ps.setString(4, param.getValue());
|
ps.setString(4, e.getValue());
|
||||||
ps.setLong(5, param.getTimeMs());
|
|
||||||
ps.setInt(6, param.getPubkeyNum());
|
if (e.getDeviceKey() != null) ps.setString(5, e.getDeviceKey());
|
||||||
ps.setString(7, param.getSignature());
|
else ps.setNull(5, Types.VARCHAR);
|
||||||
|
|
||||||
|
if (e.getSignature() != null) ps.setString(6, e.getSignature());
|
||||||
|
else ps.setNull(6, Types.VARCHAR);
|
||||||
|
|
||||||
ps.executeUpdate();
|
ps.executeUpdate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** UPSERT без внешнего соединения. Сам открывает/закрывает. */
|
/** UPSERT без внешнего соединения. Сам открывает/закрывает. */
|
||||||
public void upsert(UserParamEntry param) throws SQLException {
|
public void upsert(UserParamEntry e) throws SQLException {
|
||||||
try (Connection c = db.getConnection()) {
|
try (Connection c = db.getConnection()) {
|
||||||
upsert(c, param);
|
upsert(c, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------- SELECT --------------------
|
// -------------------- SELECT --------------------
|
||||||
|
|
||||||
/** Получить параметр с внешним соединением. Соединение НЕ закрывает. */
|
/** Получить параметр по (login,param) с внешним соединением. Соединение НЕ закрывает. */
|
||||||
public UserParamEntry getByUserLoginAndParam(Connection c, String login, String paramName) throws SQLException {
|
public UserParamEntry getByLoginAndParam(Connection c, String login, String param) throws SQLException {
|
||||||
String sql = """
|
String sql = """
|
||||||
SELECT
|
SELECT
|
||||||
login,
|
login,
|
||||||
param,
|
param,
|
||||||
bch_channel_id,
|
|
||||||
value,
|
|
||||||
time_ms,
|
time_ms,
|
||||||
pubkey_num,
|
value,
|
||||||
|
device_key,
|
||||||
signature
|
signature
|
||||||
FROM users_params
|
FROM users_params
|
||||||
WHERE login = ? AND param = ?
|
WHERE login = ? AND param = ?
|
||||||
|
LIMIT 1
|
||||||
""";
|
""";
|
||||||
|
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
ps.setString(1, login);
|
ps.setString(1, login);
|
||||||
ps.setString(2, paramName);
|
ps.setString(2, param);
|
||||||
|
|
||||||
try (ResultSet rs = ps.executeQuery()) {
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
if (!rs.next()) return null;
|
if (!rs.next()) return null;
|
||||||
return mapRow(rs);
|
return mapRow(rs);
|
||||||
@ -93,59 +106,62 @@ public final class UserParamsDAO {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Получить параметр без внешнего соединения. Сам открывает/закрывает. */
|
/** Получить параметр по (login,param) без внешнего соединения. Сам открывает/закрывает. */
|
||||||
public UserParamEntry getByUserLoginAndParam(String login, String paramName) throws SQLException {
|
public UserParamEntry getByLoginAndParam(String login, String param) throws SQLException {
|
||||||
try (Connection c = db.getConnection()) {
|
try (Connection c = db.getConnection()) {
|
||||||
return getByUserLoginAndParam(c, login, paramName);
|
return getByLoginAndParam(c, login, param);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Получить все параметры пользователя с внешним соединением. Соединение НЕ закрывает. */
|
/** Получить все параметры пользователя с внешним соединением. */
|
||||||
public List<UserParamEntry> getByUserLogin(Connection c, String login) throws SQLException {
|
public List<UserParamEntry> getByLogin(Connection c, String login) throws SQLException {
|
||||||
String sql = """
|
String sql = """
|
||||||
SELECT
|
SELECT
|
||||||
login,
|
login,
|
||||||
param,
|
param,
|
||||||
bch_channel_id,
|
|
||||||
value,
|
|
||||||
time_ms,
|
time_ms,
|
||||||
pubkey_num,
|
value,
|
||||||
|
device_key,
|
||||||
signature
|
signature
|
||||||
FROM users_params
|
FROM users_params
|
||||||
WHERE login = ?
|
WHERE login = ?
|
||||||
ORDER BY time_ms DESC
|
ORDER BY time_ms DESC
|
||||||
""";
|
""";
|
||||||
|
|
||||||
List<UserParamEntry> result = new ArrayList<>();
|
List<UserParamEntry> list = new ArrayList<>();
|
||||||
|
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
ps.setString(1, login);
|
ps.setString(1, login);
|
||||||
try (ResultSet rs = ps.executeQuery()) {
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
while (rs.next()) result.add(mapRow(rs));
|
while (rs.next()) list.add(mapRow(rs));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
/** Получить все параметры пользователя без внешнего соединения. */
|
||||||
}
|
public List<UserParamEntry> getByLogin(String login) throws SQLException {
|
||||||
|
|
||||||
/** Получить все параметры пользователя без внешнего соединения. Сам открывает/закрывает. */
|
|
||||||
public List<UserParamEntry> getByUserLogin(String login) throws SQLException {
|
|
||||||
try (Connection c = db.getConnection()) {
|
try (Connection c = db.getConnection()) {
|
||||||
return getByUserLogin(c, login);
|
return getByLogin(c, login);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------- MAPPER --------------------
|
// -------------------- MAPPER --------------------
|
||||||
|
|
||||||
private UserParamEntry mapRow(ResultSet rs) throws SQLException {
|
private static UserParamEntry mapRow(ResultSet rs) throws SQLException {
|
||||||
return new UserParamEntry(
|
UserParamEntry e = new UserParamEntry();
|
||||||
rs.getString("login"),
|
e.setLogin(rs.getString("login"));
|
||||||
rs.getString("param"),
|
e.setParam(rs.getString("param"));
|
||||||
rs.getLong("bch_channel_id"),
|
e.setTimeMs(rs.getLong("time_ms"));
|
||||||
rs.getString("value"),
|
e.setValue(rs.getString("value"));
|
||||||
rs.getLong("time_ms"),
|
|
||||||
(short) rs.getInt("pubkey_num"),
|
String dk = rs.getString("device_key");
|
||||||
rs.getString("signature")
|
if (rs.wasNull()) dk = null;
|
||||||
);
|
e.setDeviceKey(dk);
|
||||||
|
|
||||||
|
String sig = rs.getString("signature");
|
||||||
|
if (rs.wasNull()) sig = null;
|
||||||
|
e.setSignature(sig);
|
||||||
|
|
||||||
|
return e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,31 +1,40 @@
|
|||||||
package shine.db.entities;
|
package shine.db.entities;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UserParamEntry — сохранённый параметр пользователя.
|
||||||
|
*
|
||||||
|
* Таблица: users_params
|
||||||
|
* - login TEXT NOT NULL
|
||||||
|
* - param TEXT NOT NULL
|
||||||
|
* - time_ms INTEGER NOT NULL
|
||||||
|
* - value TEXT NOT NULL
|
||||||
|
* - device_key TEXT NULL
|
||||||
|
* - signature TEXT NULL
|
||||||
|
*
|
||||||
|
* UNIQUE(login, param)
|
||||||
|
*
|
||||||
|
* Смысл:
|
||||||
|
* - в таблице всегда хранится "последнее" значение параметра по времени.
|
||||||
|
* - time_ms монотонно растёт для каждого (login,param) — сервер не принимает более старые обновления.
|
||||||
|
*/
|
||||||
public class UserParamEntry {
|
public class UserParamEntry {
|
||||||
|
|
||||||
private String login; // TEXT NOT NULL
|
private String login;
|
||||||
private String param;
|
private String param;
|
||||||
private long bchChannelId; // новый канал, 8 байт, может быть 0
|
private long timeMs;
|
||||||
private String value;
|
private String value;
|
||||||
private long timeMs; // время в мс
|
|
||||||
private short pubkeyNum;
|
|
||||||
private String signature;
|
|
||||||
|
|
||||||
public UserParamEntry() {
|
private String deviceKey; // base64(32) — можно хранить как "каким ключом подписано"
|
||||||
}
|
private String signature; // base64(64)
|
||||||
|
|
||||||
public UserParamEntry(String login,
|
public UserParamEntry() {}
|
||||||
String param,
|
|
||||||
long bchChannelId,
|
public UserParamEntry(String login, String param, long timeMs, String value, String deviceKey, String signature) {
|
||||||
String value,
|
|
||||||
long timeMs,
|
|
||||||
short pubkeyNum,
|
|
||||||
String signature) {
|
|
||||||
this.login = login;
|
this.login = login;
|
||||||
this.param = param;
|
this.param = param;
|
||||||
this.bchChannelId = bchChannelId;
|
|
||||||
this.value = value;
|
|
||||||
this.timeMs = timeMs;
|
this.timeMs = timeMs;
|
||||||
this.pubkeyNum = pubkeyNum;
|
this.value = value;
|
||||||
|
this.deviceKey = deviceKey;
|
||||||
this.signature = signature;
|
this.signature = signature;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -35,17 +44,14 @@ public class UserParamEntry {
|
|||||||
public String getParam() { return param; }
|
public String getParam() { return param; }
|
||||||
public void setParam(String param) { this.param = param; }
|
public void setParam(String param) { this.param = param; }
|
||||||
|
|
||||||
public long getBchChannelId() { return bchChannelId; }
|
public long getTimeMs() { return timeMs; }
|
||||||
public void setBchChannelId(long bchChannelId) { this.bchChannelId = bchChannelId; }
|
public void setTimeMs(long timeMs) { this.timeMs = timeMs; }
|
||||||
|
|
||||||
public String getValue() { return value; }
|
public String getValue() { return value; }
|
||||||
public void setValue(String value) { this.value = value; }
|
public void setValue(String value) { this.value = value; }
|
||||||
|
|
||||||
public long getTimeMs() { return timeMs; }
|
public String getDeviceKey() { return deviceKey; }
|
||||||
public void setTimeMs(long timeMs) { this.timeMs = timeMs; }
|
public void setDeviceKey(String deviceKey) { this.deviceKey = deviceKey; }
|
||||||
|
|
||||||
public short getPubkeyNum() { return pubkeyNum; }
|
|
||||||
public void setPubkeyNum(short pubkeyNum) { this.pubkeyNum = pubkeyNum; }
|
|
||||||
|
|
||||||
public String getSignature() { return signature; }
|
public String getSignature() { return signature; }
|
||||||
public void setSignature(String signature) { this.signature = signature; }
|
public void setSignature(String signature) { this.signature = signature; }
|
||||||
|
|||||||
@ -0,0 +1,20 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
OUTFILE="all_files.txt"
|
||||||
|
|
||||||
|
# очищаем или создаём файл
|
||||||
|
: > "$OUTFILE"
|
||||||
|
|
||||||
|
# собрать только *.java файлы и вывести их содержимое в файл
|
||||||
|
find . -type f -name "*.java" | sort | while read -r f; do
|
||||||
|
cat "$f" >> "$OUTFILE"
|
||||||
|
echo >> "$OUTFILE" # пустая строка-разделитель
|
||||||
|
done
|
||||||
|
|
||||||
|
# скопировать весь файл в буфер обмена (Wayland)
|
||||||
|
wl-copy < "$OUTFILE"
|
||||||
|
|
||||||
|
echo "Готово!"
|
||||||
|
echo "Все .java файлы собраны в $OUTFILE"
|
||||||
|
echo "Содержимое скопировано в буфер обмена (Wayland)"
|
||||||
@ -0,0 +1,253 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.handlers.userParams;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
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.userParams.entyties.Net_UpsertUserParam_Request;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_UpsertUserParam_Response;
|
||||||
|
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||||
|
import server.logic.ws_protocol.WireCodes;
|
||||||
|
import shine.db.SqliteDbController;
|
||||||
|
import shine.db.dao.SolanaUsersDAO;
|
||||||
|
import shine.db.dao.UserParamsDAO;
|
||||||
|
import shine.db.entities.SolanaUserEntry;
|
||||||
|
import shine.db.entities.UserParamEntry;
|
||||||
|
import utils.config.ShineSignatureConstants;
|
||||||
|
import utils.crypto.Ed25519Util;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.sql.Statement;
|
||||||
|
import java.util.Base64;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Net_UpsertUserParam_Handler
|
||||||
|
*
|
||||||
|
* Делает:
|
||||||
|
* 1) Проверяет, что пользователь существует и что device_key действительно его.
|
||||||
|
* 2) Проверяет, что нет "более нового" значения этого param (time_ms монотонно растёт).
|
||||||
|
* 3) Проверяет подпись Ed25519 по device_key.
|
||||||
|
* 4) Пишет в БД (insert или update существующей записи), но только если time_ms новее.
|
||||||
|
*
|
||||||
|
* БОЛЬШОЙ КОММЕНТ ПРО АВТОРИЗАЦИЮ НА БУДУЩЕЕ:
|
||||||
|
* ---------------------------------------------------------------------------------
|
||||||
|
* Сейчас (MVP) этот эндпоинт намеренно не делает полноценную "сессию/авторизацию",
|
||||||
|
* потому что целостность обеспечивается криптографией: сервер проверяет подпись
|
||||||
|
* и то, что device_key принадлежит login.
|
||||||
|
*
|
||||||
|
* В будущем, если понадобится "ограничить кто может писать параметры", можно добавить:
|
||||||
|
* - проверку активной сессии (active_sessions) и соответствие login в сессии;
|
||||||
|
* - rate-limit на пользователя;
|
||||||
|
* - отдельные права на запись конкретных param.
|
||||||
|
*
|
||||||
|
* Но возможно это вообще не потребуется, если модель безопасности строится
|
||||||
|
* строго на подписи и владении device_key (как сейчас).
|
||||||
|
* ---------------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
public class Net_UpsertUserParam_Handler implements JsonMessageHandler {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(Net_UpsertUserParam_Handler.class);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
|
||||||
|
Net_UpsertUserParam_Request req = (Net_UpsertUserParam_Request) baseRequest;
|
||||||
|
|
||||||
|
// ---- basic fields validation ----
|
||||||
|
if (req.getLogin() == null || req.getLogin().isBlank()
|
||||||
|
|| req.getParam() == null || req.getParam().isBlank()
|
||||||
|
|| req.getTime_ms() == null || req.getTime_ms() <= 0
|
||||||
|
|| req.getValue() == null
|
||||||
|
|| req.getDevice_key() == null || req.getDevice_key().isBlank()
|
||||||
|
|| req.getSignature() == null || req.getSignature().isBlank()) {
|
||||||
|
|
||||||
|
return NetExceptionResponseFactory.error(
|
||||||
|
req,
|
||||||
|
WireCodes.Status.BAD_REQUEST,
|
||||||
|
"BAD_FIELDS",
|
||||||
|
"Некорректные поля: login/param/time_ms/value/device_key/signature"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final String login = req.getLogin().trim();
|
||||||
|
final String param = req.getParam().trim();
|
||||||
|
final long timeMs = req.getTime_ms();
|
||||||
|
final String value = req.getValue(); // value может быть пустой строкой — это ок
|
||||||
|
final String deviceKeyB64 = req.getDevice_key().trim();
|
||||||
|
final String signatureB64 = req.getSignature().trim();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1) parse keys
|
||||||
|
byte[] pubKey32;
|
||||||
|
byte[] sig64;
|
||||||
|
try {
|
||||||
|
pubKey32 = Base64.getDecoder().decode(deviceKeyB64);
|
||||||
|
sig64 = Base64.getDecoder().decode(signatureB64);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
return NetExceptionResponseFactory.error(
|
||||||
|
req,
|
||||||
|
WireCodes.Status.BAD_REQUEST,
|
||||||
|
"BAD_BASE64",
|
||||||
|
"device_key/signature должны быть Base64"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pubKey32.length != 32) {
|
||||||
|
return NetExceptionResponseFactory.error(
|
||||||
|
req,
|
||||||
|
WireCodes.Status.BAD_REQUEST,
|
||||||
|
"BAD_DEVICE_KEY",
|
||||||
|
"device_key должен быть Base64(32 bytes)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (sig64.length != 64) {
|
||||||
|
return NetExceptionResponseFactory.error(
|
||||||
|
req,
|
||||||
|
WireCodes.Status.BAD_REQUEST,
|
||||||
|
"BAD_SIGNATURE",
|
||||||
|
"signature должна быть Base64(64 bytes)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// подписываемая строка
|
||||||
|
String signText = ShineSignatureConstants.USER_PARAMETER_PREFIX
|
||||||
|
+ login
|
||||||
|
+ param
|
||||||
|
+ timeMs
|
||||||
|
+ value;
|
||||||
|
|
||||||
|
byte[] signBytes = signText.getBytes(StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
// 3) verify signature (до БД можно, но нам всё равно нужна БД-проверка device_key->login)
|
||||||
|
boolean sigOk = Ed25519Util.verify(signBytes, sig64, pubKey32);
|
||||||
|
if (!sigOk) {
|
||||||
|
return NetExceptionResponseFactory.error(
|
||||||
|
req,
|
||||||
|
403,
|
||||||
|
"SIGNATURE_INVALID",
|
||||||
|
"Подпись не прошла проверку"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- DB checks + upsert in a transaction ----
|
||||||
|
SqliteDbController db = SqliteDbController.getInstance();
|
||||||
|
SolanaUsersDAO usersDAO = SolanaUsersDAO.getInstance();
|
||||||
|
UserParamsDAO paramsDAO = UserParamsDAO.getInstance();
|
||||||
|
|
||||||
|
try (Connection c = db.getConnection()) {
|
||||||
|
boolean oldAuto = c.getAutoCommit();
|
||||||
|
c.setAutoCommit(false);
|
||||||
|
|
||||||
|
// BEGIN IMMEDIATE — чтобы избежать гонок (две записи одного param параллельно)
|
||||||
|
try (Statement st = c.createStatement()) {
|
||||||
|
st.execute("BEGIN IMMEDIATE");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1) user exists + device_key is exactly his
|
||||||
|
SolanaUserEntry user = usersDAO.getByLogin(c, login);
|
||||||
|
if (user == null) {
|
||||||
|
c.rollback();
|
||||||
|
return NetExceptionResponseFactory.error(
|
||||||
|
req,
|
||||||
|
404,
|
||||||
|
"USER_NOT_FOUND",
|
||||||
|
"Пользователь не найден"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String userDeviceKey = user.getDeviceKey();
|
||||||
|
if (userDeviceKey == null || userDeviceKey.isBlank()) {
|
||||||
|
c.rollback();
|
||||||
|
return NetExceptionResponseFactory.error(
|
||||||
|
req,
|
||||||
|
WireCodes.Status.SERVER_DATA_ERROR,
|
||||||
|
"USER_DEVICE_KEY_EMPTY",
|
||||||
|
"У пользователя не задан deviceKey в БД"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// сравнение строкой: у тебя deviceKey хранится как Base64(32) (в идеале нормализовать)
|
||||||
|
if (!userDeviceKey.trim().equals(deviceKeyB64)) {
|
||||||
|
c.rollback();
|
||||||
|
return NetExceptionResponseFactory.error(
|
||||||
|
req,
|
||||||
|
403,
|
||||||
|
"DEVICE_KEY_MISMATCH",
|
||||||
|
"device_key не соответствует пользователю"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) no newer time_ms already stored
|
||||||
|
UserParamEntry existing = paramsDAO.getByLoginAndParam(c, login, param);
|
||||||
|
if (existing != null) {
|
||||||
|
long existingTime = existing.getTimeMs();
|
||||||
|
if (existingTime > timeMs) {
|
||||||
|
c.rollback();
|
||||||
|
return NetExceptionResponseFactory.error(
|
||||||
|
req,
|
||||||
|
409,
|
||||||
|
"PARAM_NEWER_EXISTS",
|
||||||
|
"Уже есть более новое значение этого параметра (time_ms больше)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (existingTime == timeMs) {
|
||||||
|
// если пришёл тот же time_ms — можно либо принять как идемпотентно,
|
||||||
|
// либо сравнить value/signature. Для MVP примем как идемпотентно,
|
||||||
|
// но всё равно сделаем upsert (обновит value/signature тем же временем).
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) upsert
|
||||||
|
UserParamEntry e = new UserParamEntry(
|
||||||
|
login,
|
||||||
|
param,
|
||||||
|
timeMs,
|
||||||
|
value,
|
||||||
|
deviceKeyB64,
|
||||||
|
signatureB64
|
||||||
|
);
|
||||||
|
|
||||||
|
paramsDAO.upsert(c, e);
|
||||||
|
|
||||||
|
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 ok: login={}, param={}, time_ms={}", login, param, timeMs);
|
||||||
|
return resp;
|
||||||
|
|
||||||
|
} catch (SQLException ex) {
|
||||||
|
c.rollback();
|
||||||
|
throw ex;
|
||||||
|
} finally {
|
||||||
|
c.setAutoCommit(oldAuto);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (SQLException e) {
|
||||||
|
log.error("❌ DB error UpsertUserParam", e);
|
||||||
|
return NetExceptionResponseFactory.error(
|
||||||
|
req,
|
||||||
|
WireCodes.Status.SERVER_DATA_ERROR,
|
||||||
|
"DB_ERROR",
|
||||||
|
"Ошибка БД"
|
||||||
|
);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("❌ Internal error UpsertUserParam", e);
|
||||||
|
return NetExceptionResponseFactory.error(
|
||||||
|
req,
|
||||||
|
WireCodes.Status.INTERNAL_ERROR,
|
||||||
|
"INTERNAL_ERROR",
|
||||||
|
"Внутренняя ошибка сервера"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,53 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.handlers.userParams.entyties;
|
||||||
|
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Запрос UpsertUserParam — добавить/обновить сохранённый параметр пользователя.
|
||||||
|
*
|
||||||
|
* Клиент отправляет:
|
||||||
|
*
|
||||||
|
* {
|
||||||
|
* "op": "UpsertUserParam",
|
||||||
|
* "requestId": "req-123",
|
||||||
|
* "payload": {
|
||||||
|
* "login": "anya",
|
||||||
|
* "param": "feed:lastSeenGlobal",
|
||||||
|
* "time_ms": 1736000000123,
|
||||||
|
* "value": "105",
|
||||||
|
* "device_key": "base64-ed25519-public-key-32",
|
||||||
|
* "signature": "base64-ed25519-signature-64"
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* Подпись считается от UTF-8 строки:
|
||||||
|
* USER_PARAMETER_PREFIX + login + param + time_ms + value
|
||||||
|
*/
|
||||||
|
public class Net_UpsertUserParam_Request extends Net_Request {
|
||||||
|
|
||||||
|
private String login;
|
||||||
|
private String param;
|
||||||
|
private Long time_ms;
|
||||||
|
private String value;
|
||||||
|
|
||||||
|
private String device_key;
|
||||||
|
private String signature;
|
||||||
|
|
||||||
|
public String getLogin() { return login; }
|
||||||
|
public void setLogin(String login) { this.login = login; }
|
||||||
|
|
||||||
|
public String getParam() { return param; }
|
||||||
|
public void setParam(String param) { this.param = param; }
|
||||||
|
|
||||||
|
public Long getTime_ms() { return time_ms; }
|
||||||
|
public void setTime_ms(Long time_ms) { this.time_ms = time_ms; }
|
||||||
|
|
||||||
|
public String getValue() { return value; }
|
||||||
|
public void setValue(String value) { this.value = value; }
|
||||||
|
|
||||||
|
public String getDevice_key() { return device_key; }
|
||||||
|
public void setDevice_key(String device_key) { this.device_key = device_key; }
|
||||||
|
|
||||||
|
public String getSignature() { return signature; }
|
||||||
|
public void setSignature(String signature) { this.signature = signature; }
|
||||||
|
}
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.handlers.userParams.entyties;
|
||||||
|
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ответ на UpsertUserParam.
|
||||||
|
*
|
||||||
|
* Успех:
|
||||||
|
* {
|
||||||
|
* "op": "UpsertUserParam",
|
||||||
|
* "requestId": "req-123",
|
||||||
|
* "status": 200,
|
||||||
|
* "payload": { }
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
public class Net_UpsertUserParam_Response extends Net_Response {
|
||||||
|
// MVP: без payload. При желании позже можно добавить created/updated.
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user