Промежуточная версия

в которой надо дорабоать

1. Исправить ошибки и сделать что бы работала вторая слева вкладка. ТОесть АПИ для сервера я сделал (пока они возвращают весь список сообщений целиком - всем большим списком сообщений в канал - для мвп это устраивает,и по этому только три АПИ функции добавилось)

  Там какието ошибки на клиенте ( я только сгенерил код - но гдето вылетает) по UI можешь исправлять переделывать - моешь оставить калечное как есть - мне пока не важно. Важно увидить что каналы и сообщения и публичная переписка в каналах блокчейна работает

2. потестировать и сделать корректное завершение сессии (там есть глюки при завершении сесии)
This commit is contained in:
AidarKC 2026-04-03 11:04:59 +03:00
parent 78e62997d1
commit 8a83ac85d9
11 changed files with 541 additions and 3 deletions

View File

@ -169,7 +169,7 @@ tasks.register('deployServer', JavaExec) {
// можно переопределить при запуске: // можно переопределить при запуске:
// ./gradlew deployServer -Dit.remoteHost=... -Dit.wsUri=... // ./gradlew deployServer -Dit.remoteHost=... -Dit.wsUri=...
dependsOn shadowJar dependsOn shadowJar
systemProperty "it.remoteHost", System.getProperty("it.remoteHost", "10.147.20.7") systemProperty "it.remoteHost", System.getProperty("it.remoteHost", "194.87.0.247")
systemProperty "it.remoteUser", System.getProperty("it.remoteUser", "user") systemProperty "it.remoteUser", System.getProperty("it.remoteUser", "user")
systemProperty "it.remoteDir", System.getProperty("it.remoteDir", "/home/user/docker/shine-server") systemProperty "it.remoteDir", System.getProperty("it.remoteDir", "/home/user/docker/shine-server")
systemProperty "it.remoteDataDir", System.getProperty("it.remoteDataDir", "/home/user/docker/shine-server/data") systemProperty "it.remoteDataDir", System.getProperty("it.remoteDataDir", "/home/user/docker/shine-server/data")

View File

@ -0,0 +1,105 @@
const MAX_CONTEXT_LEN = 2000;
const RECENT_WINDOW_MS = 5000;
let transport = null;
let transportDepth = 0;
const recentFingerprints = new Map();
function nowTs() {
return Date.now();
}
function cleanString(value, maxLen = 1000) {
if (value == null) return '';
const normalized = String(value).replace(/\s+/g, ' ').trim();
if (normalized.length <= maxLen) return normalized;
return `${normalized.slice(0, Math.max(0, maxLen - 3))}...`;
}
function stringifyContext(context) {
if (context == null) return '';
try {
const raw = JSON.stringify(context);
if (!raw) return '';
if (raw.length <= MAX_CONTEXT_LEN) return raw;
return `${raw.slice(0, MAX_CONTEXT_LEN - 3)}...`;
} catch (error) {
return cleanString(`context_json_error:${error?.message || error}`, MAX_CONTEXT_LEN);
}
}
function makeFingerprint(payload) {
return [
payload.kind,
payload.message,
payload.sourceUrl,
payload.lineNumber,
payload.columnNumber,
payload.requestOp,
].join('|');
}
function isDuplicate(fingerprint) {
const ts = nowTs();
const prev = recentFingerprints.get(fingerprint);
recentFingerprints.set(fingerprint, ts);
for (const [key, time] of recentFingerprints.entries()) {
if (ts - time > RECENT_WINDOW_MS) {
recentFingerprints.delete(key);
}
}
return prev != null && ts - prev < RECENT_WINDOW_MS;
}
function buildPayload(details = {}) {
return {
kind: cleanString(details.kind || 'client_error', 64),
message: cleanString(details.message || details.reason || 'Unknown client error', 500),
stack: cleanString(details.stack || details.error?.stack || '', 8000),
sourceUrl: cleanString(details.sourceUrl || details.fileName || '', 240),
lineNumber: Number.isFinite(details.lineNumber) ? details.lineNumber : null,
columnNumber: Number.isFinite(details.columnNumber) ? details.columnNumber : null,
route: cleanString(details.route || window.location?.hash || '', 200),
href: cleanString(details.href || window.location?.href || '', 240),
userAgent: cleanString(details.userAgent || navigator.userAgent || '', 240),
clientTs: Number.isFinite(details.clientTs) ? details.clientTs : nowTs(),
requestOp: cleanString(details.requestOp || '', 64),
requestIdRef: cleanString(details.requestIdRef || '', 128),
contextJson: stringifyContext({
title: document.title || '',
pageVisibility: document.visibilityState || '',
...details.context,
}),
};
}
export function setClientErrorTransport(fn) {
transport = typeof fn === 'function' ? fn : null;
}
export async function captureClientError(details = {}) {
const payload = buildPayload(details);
if (!payload.message) return false;
const fingerprint = details.dedupeKey || makeFingerprint(payload);
if (isDuplicate(fingerprint)) return false;
console.error('[client-error]', payload.kind, payload.message, details.error || '');
if (!transport || details.skipTransport === true || transportDepth > 0) {
return false;
}
try {
transportDepth += 1;
await transport(payload);
return true;
} catch (error) {
console.warn('client error transport failed', error);
return false;
} finally {
transportDepth = Math.max(0, transportDepth - 1);
}
}

View File

@ -0,0 +1,100 @@
package server.logic.ws_protocol.JSON.handlers.system;
import org.eclipse.jetty.websocket.api.Session;
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.system.entyties.Net_ClientErrorLog_Request;
import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_ClientErrorLog_Response;
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
import server.logic.ws_protocol.WireCodes;
import java.net.SocketAddress;
/**
* ClientErrorLog технический endpoint для фронтенд-ошибок.
* Не требует авторизации: клиент должен иметь возможность отправить ошибку
* даже если логин/сессия ещё не установлены.
*/
public class Net_ClientErrorLog_Handler implements JsonMessageHandler {
private static final Logger log = LoggerFactory.getLogger(Net_ClientErrorLog_Handler.class);
@Override
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
Net_ClientErrorLog_Request req = (Net_ClientErrorLog_Request) baseRequest;
if (req.getMessage() == null || req.getMessage().isBlank()) {
return NetExceptionResponseFactory.error(
req,
WireCodes.Status.BAD_REQUEST,
"BAD_FIELDS",
"Поле message обязательно для ClientErrorLog"
);
}
long serverTs = System.currentTimeMillis();
String login = safe(ctx != null ? ctx.getLogin() : null);
String sessionId = safe(ctx != null ? ctx.getSessionId() : null);
String remote = safe(remoteAddress(ctx));
log.error(
"CLIENT_FRONTEND_ERROR kind={} clientTs={} serverTs={} login={} sessionId={} remote={} route={} href={} sourceUrl={} line={} column={} requestOp={} requestIdRef={} message={} userAgent={} context={}",
clip(req.getKind(), 64),
req.getClientTs(),
serverTs,
clip(login, 64),
clip(sessionId, 128),
clip(remote, 128),
clip(req.getRoute(), 200),
clip(req.getHref(), 240),
clip(req.getSourceUrl(), 240),
req.getLineNumber(),
req.getColumnNumber(),
clip(req.getRequestOp(), 64),
clip(req.getRequestIdRef(), 128),
clip(req.getMessage(), 500),
clip(req.getUserAgent(), 240),
clip(req.getContextJson(), 2000)
);
if (req.getStack() != null && !req.getStack().isBlank()) {
log.error("CLIENT_FRONTEND_ERROR_STACK requestId={} stack={}",
clip(req.getRequestId(), 128),
clip(req.getStack(), 8000));
}
Net_ClientErrorLog_Response resp = new Net_ClientErrorLog_Response();
resp.setOp(req.getOp());
resp.setRequestId(req.getRequestId());
resp.setStatus(WireCodes.Status.OK);
resp.setAccepted(true);
resp.setServerTs(serverTs);
return resp;
}
private static String remoteAddress(ConnectionContext ctx) {
if (ctx == null) return "";
Session ws = ctx.getWsSession();
if (ws == null) return "";
SocketAddress remote = ws.getRemoteAddress();
return remote != null ? remote.toString() : "";
}
private static String safe(String value) {
return value == null ? "" : value.trim();
}
private static String clip(String value, int maxLen) {
String cleaned = safe(value)
.replace('\n', ' ')
.replace('\r', ' ');
if (cleaned.length() <= maxLen) {
return cleaned;
}
return cleaned.substring(0, Math.max(0, maxLen - 3)) + "...";
}
}

View File

@ -0,0 +1,38 @@
package server.logic.ws_protocol.JSON.handlers.system;
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.system.entyties.Net_GetServerInfo_Request;
import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_GetServerInfo_Response;
import server.logic.ws_protocol.WireCodes;
import utils.config.AppConfig;
/**
* GetServerInfo технический запрос без авторизации.
* Возвращает базовую публичную информацию о сервере, чтобы клиент
* мог проверить доступность узла и показать его в списке серверов.
*/
public class Net_GetServerInfo_Handler implements JsonMessageHandler {
private static final AppConfig CONFIG = AppConfig.getInstance();
@Override
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
Net_GetServerInfo_Request req = (Net_GetServerInfo_Request) baseRequest;
Net_GetServerInfo_Response resp = new Net_GetServerInfo_Response();
resp.setOp(req.getOp());
resp.setRequestId(req.getRequestId());
resp.setStatus(WireCodes.Status.OK);
resp.setUrl(CONFIG.getStringOrEmpty("server.info.url"));
resp.setVersion(CONFIG.getStringOrEmpty("server.version"));
resp.setPhysicalRegion(CONFIG.getStringOrEmpty("server.info.physicalRegion"));
resp.setDescription(CONFIG.getStringOrEmpty("server.info.description"));
resp.setOrigin(CONFIG.getStringOrEmpty("server.info.origin"));
resp.setExtraInfo(CONFIG.getStringOrEmpty("server.info.extraInfo"));
return resp;
}
}

View File

@ -0,0 +1,81 @@
package server.logic.ws_protocol.JSON.handlers.system.entyties;
import server.logic.ws_protocol.JSON.entyties.Net_Request;
/**
* ClientErrorLog:
* {
* "op": "ClientErrorLog",
* "requestId": "req-1",
* "payload": {
* "kind": "global_error",
* "message": "...",
* "stack": "...",
* "sourceUrl": "...",
* "lineNumber": 10,
* "columnNumber": 20,
* "route": "#/channel-view/own-0",
* "href": "https://example/#/channel-view/own-0",
* "userAgent": "...",
* "clientTs": 1700000000123,
* "requestOp": "GetChannelMessages",
* "requestIdRef": "GetChannelMessages-123",
* "contextJson": "{...}"
* }
* }
*/
public class Net_ClientErrorLog_Request extends Net_Request {
private String kind;
private String message;
private String stack;
private String sourceUrl;
private Integer lineNumber;
private Integer columnNumber;
private String route;
private String href;
private String userAgent;
private Long clientTs;
private String requestOp;
private String requestIdRef;
private String contextJson;
public String getKind() { return kind; }
public void setKind(String kind) { this.kind = kind; }
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
public String getStack() { return stack; }
public void setStack(String stack) { this.stack = stack; }
public String getSourceUrl() { return sourceUrl; }
public void setSourceUrl(String sourceUrl) { this.sourceUrl = sourceUrl; }
public Integer getLineNumber() { return lineNumber; }
public void setLineNumber(Integer lineNumber) { this.lineNumber = lineNumber; }
public Integer getColumnNumber() { return columnNumber; }
public void setColumnNumber(Integer columnNumber) { this.columnNumber = columnNumber; }
public String getRoute() { return route; }
public void setRoute(String route) { this.route = route; }
public String getHref() { return href; }
public void setHref(String href) { this.href = href; }
public String getUserAgent() { return userAgent; }
public void setUserAgent(String userAgent) { this.userAgent = userAgent; }
public Long getClientTs() { return clientTs; }
public void setClientTs(Long clientTs) { this.clientTs = clientTs; }
public String getRequestOp() { return requestOp; }
public void setRequestOp(String requestOp) { this.requestOp = requestOp; }
public String getRequestIdRef() { return requestIdRef; }
public void setRequestIdRef(String requestIdRef) { this.requestIdRef = requestIdRef; }
public String getContextJson() { return contextJson; }
public void setContextJson(String contextJson) { this.contextJson = contextJson; }
}

View File

@ -0,0 +1,15 @@
package server.logic.ws_protocol.JSON.handlers.system.entyties;
import server.logic.ws_protocol.JSON.entyties.Net_Response;
public class Net_ClientErrorLog_Response extends Net_Response {
private long serverTs;
private boolean accepted;
public long getServerTs() { return serverTs; }
public void setServerTs(long serverTs) { this.serverTs = serverTs; }
public boolean isAccepted() { return accepted; }
public void setAccepted(boolean accepted) { this.accepted = accepted; }
}

View File

@ -0,0 +1,14 @@
package server.logic.ws_protocol.JSON.handlers.system.entyties;
import server.logic.ws_protocol.JSON.entyties.Net_Request;
/**
* GetServerInfo:
* {
* "op": "GetServerInfo",
* "requestId": "req-1",
* "payload": {}
* }
*/
public class Net_GetServerInfo_Request extends Net_Request {
}

View File

@ -0,0 +1,47 @@
package server.logic.ws_protocol.JSON.handlers.system.entyties;
import server.logic.ws_protocol.JSON.entyties.Net_Response;
/**
* Ответ GetServerInfo:
* {
* "op": "GetServerInfo",
* "requestId": "req-1",
* "status": 200,
* "payload": {
* "url": "...",
* "version": "...",
* "physicalRegion": "...",
* "description": "...",
* "origin": "...",
* "extraInfo": "..."
* }
* }
*/
public class Net_GetServerInfo_Response extends Net_Response {
private String url;
private String version;
private String physicalRegion;
private String description;
private String origin;
private String extraInfo;
public String getUrl() { return url; }
public void setUrl(String url) { this.url = url; }
public String getVersion() { return version; }
public void setVersion(String version) { this.version = version; }
public String getPhysicalRegion() { return physicalRegion; }
public void setPhysicalRegion(String physicalRegion) { this.physicalRegion = physicalRegion; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public String getOrigin() { return origin; }
public void setOrigin(String origin) { this.origin = origin; }
public String getExtraInfo() { return extraInfo; }
public void setExtraInfo(String extraInfo) { this.extraInfo = extraInfo; }
}

View File

@ -0,0 +1,44 @@
package server.logic.ws_protocol.JSON.utils;
import server.logic.ws_protocol.Base64Ws;
/**
* Утилиты для строковых публичных ключей, используемых в auth/session API.
*
* Поддерживаемые форматы:
* - legacy: BASE64(32 bytes)
* - explicit: ed25519/BASE64(32 bytes)
*/
public final class AuthKeyUtils {
private AuthKeyUtils() {}
public static String normalize(String key, String fieldName) {
if (key == null) throw new IllegalArgumentException(fieldName + " is null");
String trimmed = key.trim();
if (trimmed.isEmpty()) throw new IllegalArgumentException(fieldName + " is empty");
return trimmed;
}
public static byte[] parseEd25519PublicKey(String key, String fieldName) {
String normalized = normalize(key, fieldName);
int slash = normalized.indexOf('/');
if (slash < 0) {
return Base64Ws.decodeLen(normalized, 32, fieldName);
}
String algorithm = normalized.substring(0, slash).trim();
String encodedKey = normalized.substring(slash + 1).trim();
if (algorithm.isEmpty() || encodedKey.isEmpty()) {
throw new IllegalArgumentException(fieldName + " has bad algorithm/key format");
}
if (!"ed25519".equalsIgnoreCase(algorithm)) {
throw new UnsupportedOperationException(fieldName + " algorithm is not supported: " + algorithm);
}
return Base64Ws.decodeLen(encodedKey, 32, fieldName);
}
}

View File

@ -7,7 +7,7 @@ import java.util.Objects;
public class IT_DeployRestartAndRunRemoteMain { public class IT_DeployRestartAndRunRemoteMain {
// ====== НАСТРОЙКИ (можно переопределять systemProperty) ====== // ====== НАСТРОЙКИ (можно переопределять systemProperty) ======
private static final String REMOTE_HOST = System.getProperty("it.remoteHost", "10.147.20.7"); private static final String REMOTE_HOST = System.getProperty("it.remoteHost", "194.87.0.247");
private static final String REMOTE_USER = System.getProperty("it.remoteUser", "user"); private static final String REMOTE_USER = System.getProperty("it.remoteUser", "user");
private static final String REMOTE_DIR = System.getProperty("it.remoteDir", "/home/user/docker/shine-server"); private static final String REMOTE_DIR = System.getProperty("it.remoteDir", "/home/user/docker/shine-server");
@ -103,4 +103,4 @@ public class IT_DeployRestartAndRunRemoteMain {
try { Thread.sleep(ms); } try { Thread.sleep(ms); }
catch (InterruptedException e) { Thread.currentThread().interrupt(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
} }
} }

View File

@ -0,0 +1,94 @@
package test.it.cases;
import test.it.utils.json.JsonBuilders;
import test.it.utils.json.JsonParsers;
import test.it.utils.log.TestResult;
import test.it.utils.ws.WsSession;
import java.time.Duration;
import static org.junit.jupiter.api.Assertions.fail;
/**
* IT_00_TechnicalRequests
* Проверяет технические запросы без авторизации:
* - Ping
* - GetServerInfo
*/
public class IT_00_TechnicalRequests {
public static void main(String[] args) {
String summary = run();
System.out.println(summary);
}
public static String run() {
TestResult r = new TestResult("IT_00_TechnicalRequests");
Duration t = Duration.ofSeconds(5);
try (WsSession ws = WsSession.open()) {
checkPing(r, ws, t);
checkGetServerInfo(r, ws, t);
} catch (Throwable e) {
r.fail("IT_00_TechnicalRequests упал: " + e.getMessage());
}
return r.summaryLine();
}
private static void checkPing(TestResult r, WsSession ws, Duration t) {
String resp = ws.call("Ping", JsonBuilders.ping(System.currentTimeMillis()), t);
if (JsonParsers.status(resp) != 200) {
r.fail("Ping: ожидали status=200, resp=" + resp);
fail("Ping unexpected status");
}
if (!Boolean.TRUE.equals(JsonParsers.ok(resp))) {
r.fail("Ping: ожидали ok=true, resp=" + resp);
fail("Ping unexpected ok");
}
Long ts = JsonParsers.pingTs(resp);
if (ts == null || ts <= 0) {
r.fail("Ping: сервер не вернул payload.ts, resp=" + resp);
fail("Ping missing ts");
}
r.ok("Ping: OK, ts=" + ts);
}
private static void checkGetServerInfo(TestResult r, WsSession ws, Duration t) {
String resp = ws.call("GetServerInfo", JsonBuilders.getServerInfo(), t);
if (JsonParsers.status(resp) != 200) {
r.fail("GetServerInfo: ожидали status=200, resp=" + resp);
fail("GetServerInfo unexpected status");
}
if (!Boolean.TRUE.equals(JsonParsers.ok(resp))) {
r.fail("GetServerInfo: ожидали ok=true, resp=" + resp);
fail("GetServerInfo unexpected ok");
}
if (!JsonParsers.payloadIsObject(resp)) {
r.fail("GetServerInfo: payload должен быть объектом, resp=" + resp);
fail("GetServerInfo payload is not object");
}
assertStringField(resp, "url", r);
String version = assertStringField(resp, "version", r);
assertStringField(resp, "physicalRegion", r);
assertStringField(resp, "description", r);
assertStringField(resp, "origin", r);
assertStringField(resp, "extraInfo", r);
r.ok("GetServerInfo: OK, version=" + version);
}
private static String assertStringField(String resp, String field, TestResult r) {
String value = JsonParsers.payloadText(resp, field);
if (value == null) {
r.fail("GetServerInfo: отсутствует поле payload." + field + ", resp=" + resp);
fail("GetServerInfo missing field: " + field);
}
return value;
}
}