Доработать UX и отмену pairing по коду

This commit is contained in:
AidarKC
2026-06-15 13:13:16 +04:00
parent 9fcdcd087b
commit 9a489801c5
18 changed files with 361 additions and 30 deletions
@@ -199,6 +199,22 @@ public final class EspPairingRequestsDAO {
});
}
public void markCanceled(String pairingId, String rejectReason, long updatedAtMs) throws SQLException {
updateSimple(pairingId, """
UPDATE esp_pairing_requests
SET status = 'canceled',
reject_reason = ?,
approved_by_session_id = NULL,
encrypted_payload = NULL,
updated_at_ms = ?
WHERE pairing_id = ?
""", ps -> {
ps.setString(1, rejectReason);
ps.setLong(2, updatedAtMs);
ps.setString(3, pairingId);
});
}
public int expirePending(long nowMs) throws SQLException {
try (Connection c = db.getConnection();
PreparedStatement ps = c.prepareStatement("""
@@ -9,6 +9,7 @@ import server.logic.ws_protocol.JSON.handlers.auth.Net_CreateAuthSession__Handle
import server.logic.ws_protocol.JSON.handlers.auth.Net_ListSessions_Handler;
import server.logic.ws_protocol.JSON.handlers.auth.Net_ListEspPairingRequests_Handler;
import server.logic.ws_protocol.JSON.handlers.auth.Net_ApproveEspPairing_Handler;
import server.logic.ws_protocol.JSON.handlers.auth.Net_CancelEspPairing_Handler;
import server.logic.ws_protocol.JSON.handlers.auth.Net_GetEspPairingStatus_Handler;
import server.logic.ws_protocol.JSON.handlers.auth.Net_RejectEspPairing_Handler;
@@ -27,6 +28,7 @@ import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_ListEspPairingRe
// --- NEW v2 entities ---
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_ApproveEspPairing_Request;
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_CancelEspPairing_Request;
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_GetEspPairingStatus_Request;
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_RejectEspPairing_Request;
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_SessionChallenge_Request;
@@ -138,6 +140,7 @@ public final class JsonHandlerRegistry {
Map.entry("ListEspPairingRequests", new Net_ListEspPairingRequests_Handler()),
Map.entry("ApproveEspPairing", new Net_ApproveEspPairing_Handler()),
Map.entry("RejectEspPairing", new Net_RejectEspPairing_Handler()),
Map.entry("CancelEspPairing", new Net_CancelEspPairing_Handler()),
Map.entry("GetEspPairingStatus", new Net_GetEspPairingStatus_Handler()),
// --- blockchain ---
@@ -202,6 +205,7 @@ public final class JsonHandlerRegistry {
Map.entry("ListEspPairingRequests", Net_ListEspPairingRequests_Request.class),
Map.entry("ApproveEspPairing", Net_ApproveEspPairing_Request.class),
Map.entry("RejectEspPairing", Net_RejectEspPairing_Request.class),
Map.entry("CancelEspPairing", Net_CancelEspPairing_Request.class),
Map.entry("GetEspPairingStatus", Net_GetEspPairingStatus_Request.class),
// --- blockchain ---
@@ -27,6 +27,7 @@ final class EspPairingSupport {
static final String STATE_CREATED = "created";
static final String STATE_APPROVED = "approved";
static final String STATE_REJECTED = "rejected";
static final String STATE_CANCELED = "canceled";
static final String STATE_EXPIRED = "expired";
private static final SecureRandom RANDOM = new SecureRandom();
@@ -0,0 +1,60 @@
package server.logic.ws_protocol.JSON.handlers.auth;
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.auth.entyties.Net_CancelEspPairing_Request;
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_CancelEspPairing_Response;
import server.logic.ws_protocol.JSON.utils.AuthKeyUtils;
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
import server.logic.ws_protocol.WireCodes;
import shine.db.dao.EspPairingRequestsDAO;
import shine.db.entities.EspPairingRequestEntry;
public class Net_CancelEspPairing_Handler implements JsonMessageHandler {
@Override
public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
Net_CancelEspPairing_Request req = (Net_CancelEspPairing_Request) baseReq;
String pairingId = req.getPairingId() == null ? "" : req.getPairingId().trim();
if (pairingId.isBlank()) {
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "EMPTY_PAIRING_ID", "Пустой pairingId");
}
String requesterSessionKey = req.getRequesterSessionKey();
if (requesterSessionKey == null || requesterSessionKey.isBlank()) {
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "EMPTY_REQUESTER_SESSION_KEY", "Пустой requesterSessionKey");
}
try {
requesterSessionKey = AuthKeyUtils.normalize(requesterSessionKey, "requesterSessionKey");
AuthKeyUtils.parseEd25519PublicKey(requesterSessionKey, "requesterSessionKey");
} catch (Exception e) {
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_REQUESTER_SESSION_KEY", "Некорректный requesterSessionKey");
}
long now = System.currentTimeMillis();
EspPairingRequestsDAO.getInstance().expirePending(now);
EspPairingRequestEntry row = EspPairingRequestsDAO.getInstance().getByPairingId(pairingId);
if (row == null) {
return NetExceptionResponseFactory.error(req, 404, "PAIRING_NOT_FOUND", "Pairing-заявка не найдена");
}
if (!requesterSessionKey.equals(row.getRequesterSessionKey())) {
return NetExceptionResponseFactory.error(req, 422, "PAIRING_OF_ANOTHER_REQUESTER", "Нельзя отменять pairing другого устройства");
}
if (!EspPairingSupport.STATE_CREATED.equals(row.getStatus())) {
return NetExceptionResponseFactory.error(req, 422, "PAIRING_NOT_PENDING", "Заявка уже не находится в статусе created");
}
EspPairingRequestsDAO.getInstance().markCanceled(pairingId, "canceled_by_requester", now);
Net_CancelEspPairing_Response resp = new Net_CancelEspPairing_Response();
resp.setOp(req.getOp());
resp.setRequestId(req.getRequestId());
resp.setStatus(WireCodes.Status.OK);
resp.setPairingId(pairingId);
resp.setState(EspPairingSupport.STATE_CANCELED);
return resp;
}
}
@@ -83,15 +83,23 @@ public class Net_StartEspPairing_Handler implements JsonMessageHandler {
}
String configuredPasswordHash = settings.getPasswordHash() == null ? "" : settings.getPasswordHash().trim();
boolean requiresPassword = !configuredPasswordHash.isBlank();
if (requiresPassword && !configuredPasswordHash.equals(passwordHash)) {
boolean suppliedPassword = passwordHash != null && !passwordHash.isBlank();
if ((requiresPassword && !configuredPasswordHash.equals(passwordHash))
|| (!requiresPassword && suppliedPassword)) {
return NetExceptionResponseFactory.error(req, 422, "PAIRING_PASSWORD_INVALID", "Неверный pairing-пароль");
}
if (!requiresPassword && passwordHash != null && !passwordHash.isBlank()) {
passwordHash = "";
}
String clientPlatform = AuthSessionTypeSupport.normalizeClientPlatform(req.getRequesterClientPlatform());
int ttlSeconds = EspPairingSupport.normalizeTtlSeconds(settings.getTtlSeconds());
List<ConnectionContext> approverConnections = EspPairingSupport.findOnlineTrustedConnections(canonicalLogin);
if (approverConnections.isEmpty()) {
return NetExceptionResponseFactory.error(
req,
422,
"PAIRING_NO_TRUSTED_SESSION_ONLINE",
"Нет ни одной активной доверенной сессии пользователя в сети"
);
}
EspPairingSupport.PairingFingerprint fingerprint = EspPairingSupport.deriveFingerprint(
canonicalLogin,
requesterSessionKey,
@@ -117,7 +125,6 @@ public class Net_StartEspPairing_Handler implements JsonMessageHandler {
entry.setDeliveredToHomeserver(false);
EspPairingRequestsDAO.getInstance().insert(entry);
List<ConnectionContext> approverConnections = EspPairingSupport.findOnlineTrustedConnections(canonicalLogin);
boolean delivered = false;
for (ConnectionContext targetCtx : approverConnections) {
String eventId = NetIdGenerator.eventId("pair");
@@ -0,0 +1,24 @@
package server.logic.ws_protocol.JSON.handlers.auth.entyties;
import server.logic.ws_protocol.JSON.entyties.Net_Request;
public class Net_CancelEspPairing_Request extends Net_Request {
private String pairingId;
private String requesterSessionKey;
public String getPairingId() {
return pairingId;
}
public void setPairingId(String pairingId) {
this.pairingId = pairingId;
}
public String getRequesterSessionKey() {
return requesterSessionKey;
}
public void setRequesterSessionKey(String requesterSessionKey) {
this.requesterSessionKey = requesterSessionKey;
}
}
@@ -0,0 +1,24 @@
package server.logic.ws_protocol.JSON.handlers.auth.entyties;
import server.logic.ws_protocol.JSON.entyties.Net_Response;
public class Net_CancelEspPairing_Response extends Net_Response {
private String pairingId;
private String state;
public String getPairingId() {
return pairingId;
}
public void setPairingId(String pairingId) {
this.pairingId = pairingId;
}
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
}
@@ -94,6 +94,44 @@ public class IT_07_EspPairing {
);
assertEquals(200, JsonParsers.status(startNoPasswordResp), "StartEspPairing without password must be 200");
String startWrongPasswordResp = requesterWs.call(
"StartEspPairing",
JsonBuilders.startEspPairing(LOGIN, passwordHash, requesterNoPasswordMaterial.sessionKey(), 1, "Android", 1),
t
);
assertErrorFormat(startWrongPasswordResp, "StartEspPairing", "PAIRING_PASSWORD_INVALID");
SessionMaterial cancelMaterial = newSessionMaterial();
String startCancelableResp = requesterWs.call(
"StartEspPairing",
JsonBuilders.startEspPairing(LOGIN, "", cancelMaterial.sessionKey(), 1, "Android", 1),
t
);
assertEquals(200, JsonParsers.status(startCancelableResp), "StartEspPairing for cancel must be 200");
String cancelPairingId = JsonParsers.payloadText(startCancelableResp, "pairingId");
String cancelResp = requesterWs.call(
"CancelEspPairing",
JsonBuilders.cancelEspPairing(cancelPairingId, cancelMaterial.sessionKey()),
t
);
assertEquals(200, JsonParsers.status(cancelResp), "CancelEspPairing must be 200");
assertEquals("canceled", JsonParsers.payloadText(cancelResp, "state"));
String closeResp = clientWs.call(
"CloseActiveSession",
JsonBuilders.closeActiveSession(clientSession.sessionId(), 0, ""),
t
);
assertEquals(200, JsonParsers.status(closeResp), "CloseActiveSession must be 200");
SessionMaterial requesterOfflineMaterial = newSessionMaterial();
String startOfflineResp = requesterWs.call(
"StartEspPairing",
JsonBuilders.startEspPairing(LOGIN, "", requesterOfflineMaterial.sessionKey(), 1, "Android", 1),
t
);
assertErrorFormat(startOfflineResp, "StartEspPairing", "PAIRING_NO_TRUSTED_SESSION_ONLINE");
String forbiddenResp = requesterWs.call(
"ListEspPairingRequests#anonymous",
JsonBuilders.listEspPairingRequests(),
@@ -333,6 +333,20 @@ public final class JsonBuilders {
""".formatted(requestId, pairingId, reason == null ? "" : reason);
}
public static String cancelEspPairing(String pairingId, String requesterSessionKey) {
String requestId = TestIds.next("esp-cancel");
return """
{
"op": "CancelEspPairing",
"requestId": "%s",
"payload": {
"pairingId": "%s",
"requesterSessionKey": "%s"
}
}
""".formatted(requestId, pairingId, requesterSessionKey);
}
public static String getEspPairingStatus(String pairingId) {
String requestId = TestIds.next("esp-status");
return """