diff --git a/SHiNE-server/shine-server-db/src/main/java/shine/db/dao/UserAccessServersCurrentDAO.java b/SHiNE-server/shine-server-db/src/main/java/shine/db/dao/UserAccessServersCurrentDAO.java index c53d0120..8b8fc312 100644 --- a/SHiNE-server/shine-server-db/src/main/java/shine/db/dao/UserAccessServersCurrentDAO.java +++ b/SHiNE-server/shine-server-db/src/main/java/shine/db/dao/UserAccessServersCurrentDAO.java @@ -36,6 +36,48 @@ public final class UserAccessServersCurrentDAO { } } + /** + * Возвращает первый access server пользователя ровно в порядке + * solana_user_pda_current.access_servers_json (access_servers[0]). + * + * В user_access_servers_current порядок массива намеренно не хранится, + * поэтому для auth-routing первичный сервер читается напрямую из PDA snapshot. + */ + public UserAccessServerRouteEntry findPrimaryByUserLogin(String userLogin) throws SQLException { + String sql = """ + SELECT + u.login AS user_login, + s.login AS server_login, + s.server_address AS server_url, + s.client_key AS server_client_key + FROM solana_user_pda_current u + CROSS JOIN LATERAL ( + SELECT BTRIM(a.login_value) AS server_login + FROM jsonb_array_elements_text( + CASE + WHEN BTRIM(COALESCE(u.access_servers_json, '')) = '' THEN '[]'::jsonb + ELSE u.access_servers_json::jsonb + END + ) WITH ORDINALITY AS a(login_value, ord) + WHERE a.ord = 1 + ) primary_access + JOIN solana_user_pda_current s + ON s.normalized_login = LOWER(primary_access.server_login) + AND s.is_server = TRUE + AND BTRIM(COALESCE(s.server_address, '')) <> '' + WHERE u.normalized_login = LOWER(BTRIM(?)) + LIMIT 1 + """; + try (Connection c = db.getConnection(); + PreparedStatement ps = c.prepareStatement(sql)) { + ps.setString(1, userLogin); + try (ResultSet rs = ps.executeQuery()) { + if (!rs.next()) return null; + return mapRow(rs); + } + } + } + public List listByUserLogin(Connection c, String userLogin) throws SQLException { String sql = """ SELECT user_login, server_login, server_url, server_client_key diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/ConnectionContext.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/ConnectionContext.java index 37bf1a9a..fcc783dd 100644 --- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/ConnectionContext.java +++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/ConnectionContext.java @@ -27,6 +27,11 @@ public class ConnectionContext { public static final int AUTH_STATUS_AUTH_IN_PROGRESS = 1; // выполнен challenge (AuthChallenge или SessionChallenge) public static final int AUTH_STATUS_USER = 2; // авторизованный пользователь + // При успешной авторизации отдельно фиксируем отношение пользователя к этому серверу. + public static final int USER_SERVER_SCOPE_NONE = 0; + public static final int USER_SERVER_SCOPE_LOCAL = 1; + public static final int USER_SERVER_SCOPE_REMOTE = 2; + // Полный пользователь из runtime БД (current users / solana_user_pda_current) private CurrentUserEntry currentUserEntry; @@ -73,6 +78,12 @@ public class ConnectionContext { */ private int authenticationStatus = AUTH_STATUS_NONE; + /** + * LOCAL — этот сервер является первым access server пользователя. + * REMOTE — пользователь криптографически авторизован, но его access server другой. + */ + private int userServerScope = USER_SERVER_SCOPE_NONE; + /** * WebSocket-сессия Jetty для данного подключения. * Нужна, чтобы через ConnectionContext можно было отправлять сообщения клиенту. @@ -197,6 +208,28 @@ public class ConnectionContext { return authenticationStatus == AUTH_STATUS_NONE; } + public int getUserServerScope() { + return userServerScope; + } + + public void setUserServerScope(int userServerScope) { + this.userServerScope = userServerScope; + } + + public boolean isLocalAuthenticatedUser() { + return isAuthenticatedUser() && userServerScope == USER_SERVER_SCOPE_LOCAL; + } + + public boolean isRemoteAuthenticatedUser() { + return isAuthenticatedUser() && userServerScope == USER_SERVER_SCOPE_REMOTE; + } + + public String getUserServerScopeName() { + return userServerScope == USER_SERVER_SCOPE_LOCAL ? "LOCAL" + : userServerScope == USER_SERVER_SCOPE_REMOTE ? "REMOTE" + : "NONE"; + } + public void reset() { currentUserEntry = null; activeSessionEntry = null; @@ -209,6 +242,7 @@ public class ConnectionContext { sessionLoginNonceExpiresAtMs = 0; authenticationStatus = AUTH_STATUS_NONE; + userServerScope = USER_SERVER_SCOPE_NONE; wsSession = null; serverConnection = false; remoteServerLogin = null; @@ -222,6 +256,7 @@ public class ConnectionContext { "login='" + getLogin() + '\'' + ", sessionId=" + sessionId + ", authenticationStatus=" + authenticationStatus + + ", userServerScope=" + getUserServerScopeName() + '}'; } } diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/JsonHandlerRegistry.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/JsonHandlerRegistry.java index 0ef5cc0d..4e2cf7c2 100644 --- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/JsonHandlerRegistry.java +++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/JsonHandlerRegistry.java @@ -4,6 +4,7 @@ import server.logic.ws_protocol.JSON.entyties.Net_Request; import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler; import server.logic.ws_protocol.JSON.handlers.auth.Net_AuthChallenge_Handler; +import server.logic.ws_protocol.JSON.handlers.auth.Net_ResolveLoginForAuth_Handler; import server.logic.ws_protocol.JSON.handlers.auth.Net_CloseActiveSession_Handler; import server.logic.ws_protocol.JSON.handlers.auth.Net_CreateAuthSession__Handler; import server.logic.ws_protocol.JSON.handlers.auth.Net_ListSessions_Handler; @@ -22,6 +23,7 @@ import server.logic.ws_protocol.JSON.handlers.auth.Net_UpsertEspPairingSettings_ // --- auth entities --- import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_AuthChallenge_Request; +import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_ResolveLoginForAuth_Request; import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_CloseActiveSession_Request; import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_CreateAuthSession_Request; import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_ListSessions_Request; @@ -155,6 +157,7 @@ public final class JsonHandlerRegistry { Map.entry("TestUploadFreeAvatar", new Net_TestUploadFreeAvatar_Handler()), // --- auth --- + Map.entry("ResolveLoginForAuth", new Net_ResolveLoginForAuth_Handler()), Map.entry("AuthChallenge", new Net_AuthChallenge_Handler()), Map.entry("CreateAuthSession", new Net_CreateAuthSession__Handler()), Map.entry("CloseActiveSession", new Net_CloseActiveSession_Handler()), @@ -242,6 +245,7 @@ public final class JsonHandlerRegistry { Map.entry("TestUploadFreeAvatar", Net_TestUploadFreeAvatar_Request.class), // --- auth --- + Map.entry("ResolveLoginForAuth", Net_ResolveLoginForAuth_Request.class), Map.entry("AuthChallenge", Net_AuthChallenge_Request.class), Map.entry("CreateAuthSession", Net_CreateAuthSession_Request.class), Map.entry("CloseActiveSession", Net_CloseActiveSession_Request.class), diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/JsonInboundProcessor.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/JsonInboundProcessor.java index dcba0cf4..3d8133e5 100644 --- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/JsonInboundProcessor.java +++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/JsonInboundProcessor.java @@ -205,6 +205,22 @@ public final class JsonInboundProcessor { ctxLogin, ctxSessionId, safe(op), safe(requestId), shorten(safeToString(request), 1200)); } + // REMOTE-пользователь успешно авторизован, но этот сервер не является + // его access server. Такой контекст нужен для realtime/call signaling, + // однако обычные homeserver-операции здесь запрещены централизованно. + if (ctx != null + && ctx.isRemoteAuthenticatedUser() + && !RemoteUserOperationPolicy.isAllowed(op)) { + Net_Exception_Response err = NetExceptionResponseFactory.error( + op, + requestId, + 403, + "USER_NOT_LOCAL", + "Пользователь авторизован, но относится к другому access server" + ); + return writeResponse(err); + } + // 5) Вызываем хэндлер Net_Response response; try { diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/RemoteUserOperationPolicy.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/RemoteUserOperationPolicy.java new file mode 100644 index 00000000..a4715a48 --- /dev/null +++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/RemoteUserOperationPolicy.java @@ -0,0 +1,31 @@ +package server.logic.ws_protocol.JSON; + +import java.util.Set; + +/** + * REMOTE-пользователь полностью авторизован криптографически, но этот сервер + * не является его access server. Поэтому ему разрешён только минимальный набор + * realtime/звонковых операций. + */ +public final class RemoteUserOperationPolicy { + + private static final Set ALLOWED = Set.of( + "Ping", + "GetServerInfo", + "GetCallIceConfig", + "CallInviteBroadcast", + "CallSignalToSession", + "SendSignal", + "CallDeliveryReport", + "ClientErrorLog", + "ClientDebugLog", + "CloseActiveSession", + "ResolveLoginForAuth" + ); + + private RemoteUserOperationPolicy() {} + + public static boolean isAllowed(String op) { + return op != null && ALLOWED.contains(op); + } +} diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/AuthAccessScopeSupport.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/AuthAccessScopeSupport.java new file mode 100644 index 00000000..3c34f1c2 --- /dev/null +++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/AuthAccessScopeSupport.java @@ -0,0 +1,81 @@ +package server.logic.ws_protocol.JSON.handlers.auth; + +import server.logic.ws_protocol.JSON.ConnectionContext; +import shine.db.dao.CurrentUsersDAO; +import shine.db.dao.UserAccessServersCurrentDAO; +import shine.db.entities.CurrentUserEntry; +import shine.db.entities.UserAccessServerRouteEntry; +import utils.config.AppConfig; + +import java.sql.SQLException; +import java.util.Locale; + +/** + * Единая серверная логика определения, является ли текущий сервер + * домашним access server для пользователя. + */ +public final class AuthAccessScopeSupport { + + public static final String RESOLUTION_LOCAL = "LOCAL"; + public static final String RESOLUTION_REMOTE = "REMOTE"; + public static final String RESOLUTION_NOT_FOUND = "NOT_FOUND"; + public static final String RESOLUTION_NO_ACCESS_SERVER = "NO_ACCESS_SERVER"; + + private static final String SERVER_LOGIN_CONFIG = "server.SHiNE.login"; + + private AuthAccessScopeSupport() {} + + public static Resolution resolveLogin(String login) throws SQLException { + String cleanLogin = login == null ? "" : login.trim(); + if (cleanLogin.isBlank()) { + return new Resolution(RESOLUTION_NOT_FOUND, null, null, null); + } + + CurrentUserEntry user = CurrentUsersDAO.getInstance().getByLogin(cleanLogin); + if (user == null) { + return new Resolution(RESOLUTION_NOT_FOUND, null, null, null); + } + + UserAccessServerRouteEntry route = + UserAccessServersCurrentDAO.getInstance().findPrimaryByUserLogin(user.getLogin()); + if (route == null) { + return new Resolution(RESOLUTION_NO_ACCESS_SERVER, user, null, null); + } + + String ownServerLogin = currentServerLogin(); + String routeServerLogin = normalize(route.getServerLogin()); + String resolution = ownServerLogin != null && ownServerLogin.equals(routeServerLogin) + ? RESOLUTION_LOCAL + : RESOLUTION_REMOTE; + + return new Resolution(resolution, user, route, ownServerLogin); + } + + public static int connectionScopeForLogin(String login) throws SQLException { + Resolution resolution = resolveLogin(login); + return RESOLUTION_LOCAL.equals(resolution.resolution()) + ? ConnectionContext.USER_SERVER_SCOPE_LOCAL + : ConnectionContext.USER_SERVER_SCOPE_REMOTE; + } + + public static String connectionScopeName(int scope) { + return scope == ConnectionContext.USER_SERVER_SCOPE_LOCAL ? "LOCAL" : "REMOTE"; + } + + public static String currentServerLogin() { + return normalize(AppConfig.getInstance().getParam(SERVER_LOGIN_CONFIG)); + } + + private static String normalize(String value) { + if (value == null) return null; + String normalized = value.trim().toLowerCase(Locale.ROOT); + return normalized.isEmpty() ? null : normalized; + } + + public record Resolution( + String resolution, + CurrentUserEntry user, + UserAccessServerRouteEntry primaryAccessServer, + String currentServerLogin + ) {} +} diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_AuthChallenge_Handler.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_AuthChallenge_Handler.java index 1a3aed35..ccccf696 100644 --- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_AuthChallenge_Handler.java +++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_AuthChallenge_Handler.java @@ -71,6 +71,7 @@ public class Net_AuthChallenge_Handler implements JsonMessageHandler { } ctx.setCurrentUser(solanaUserEntry); + ctx.setUserServerScope(AuthAccessScopeSupport.connectionScopeForLogin(solanaUserEntry.getLogin())); ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_AUTH_IN_PROGRESS); byte[] buf = new byte[32]; diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_CreateAuthSession__Handler.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_CreateAuthSession__Handler.java index 4ca98505..63e29da6 100644 --- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_CreateAuthSession__Handler.java +++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_CreateAuthSession__Handler.java @@ -414,10 +414,15 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler { return err; } + // Между AuthChallenge и CreateAuthSession routing мог измениться, + // поэтому окончательный LOCAL/REMOTE scope вычисляем повторно. + int userServerScope = AuthAccessScopeSupport.connectionScopeForLogin(canonicalLogin); + // --- обновляем контекст --- ctx.setActiveSession(activeSessionEntry); ctx.setSessionId(sessionId); ctx.setAuthNonce(null); + ctx.setUserServerScope(userServerScope); ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_USER); ActiveConnectionsRegistry.getInstance().register(ctx); @@ -428,6 +433,7 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler { resp.setRequestId(req.getRequestId()); resp.setStatus(WireCodes.Status.OK); resp.setSessionId(sessionId); + resp.setConnectionScope(AuthAccessScopeSupport.connectionScopeName(userServerScope)); return resp; } diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_ResolveLoginForAuth_Handler.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_ResolveLoginForAuth_Handler.java new file mode 100644 index 00000000..74291aa6 --- /dev/null +++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_ResolveLoginForAuth_Handler.java @@ -0,0 +1,45 @@ +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_ResolveLoginForAuth_Request; +import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_ResolveLoginForAuth_Response; +import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory; +import server.logic.ws_protocol.WireCodes; +import shine.db.entities.CurrentUserEntry; +import shine.db.entities.UserAccessServerRouteEntry; + +public class Net_ResolveLoginForAuth_Handler implements JsonMessageHandler { + + @Override + public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception { + Net_ResolveLoginForAuth_Request req = (Net_ResolveLoginForAuth_Request) baseReq; + String login = req.getLogin() == null ? "" : req.getLogin().trim(); + if (login.isBlank()) { + return NetExceptionResponseFactory.error( + req, WireCodes.Status.BAD_REQUEST, "EMPTY_LOGIN", "Пустой login"); + } + + AuthAccessScopeSupport.Resolution resolved = AuthAccessScopeSupport.resolveLogin(login); + + Net_ResolveLoginForAuth_Response resp = new Net_ResolveLoginForAuth_Response(); + resp.setOp(req.getOp()); + resp.setRequestId(req.getRequestId()); + resp.setStatus(WireCodes.Status.OK); + resp.setResolution(resolved.resolution()); + + CurrentUserEntry user = resolved.user(); + if (user != null) { + resp.setLogin(user.getLogin()); + } + + UserAccessServerRouteEntry route = resolved.primaryAccessServer(); + if (route != null) { + resp.setAccessServerLogin(route.getServerLogin()); + resp.setAccessServerUrl(route.getServerUrl()); + } + return resp; + } +} diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_SessionLogin_Handler.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_SessionLogin_Handler.java index c2dea576..3e817cd8 100644 --- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_SessionLogin_Handler.java +++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_SessionLogin_Handler.java @@ -292,10 +292,13 @@ public class Net_SessionLogin_Handler implements JsonMessageHandler { session.setClientPlatform(clientPlatform); session.setUserLanguage(userLanguage); + int userServerScope = AuthAccessScopeSupport.connectionScopeForLogin(user.getLogin()); + // ctx ctx.setActiveSession(session); ctx.setCurrentUser(user); ctx.setSessionId(sessionId); + ctx.setUserServerScope(userServerScope); ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_USER); ActiveConnectionsRegistry.getInstance().register(ctx); @@ -306,6 +309,7 @@ public class Net_SessionLogin_Handler implements JsonMessageHandler { resp.setRequestId(req.getRequestId()); resp.setStatus(WireCodes.Status.OK); resp.setStoragePwd(session.getStoragePwd()); + resp.setConnectionScope(AuthAccessScopeSupport.connectionScopeName(userServerScope)); return resp; } diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_StartEspPairing_Handler.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_StartEspPairing_Handler.java index ed6254e8..1c7ed79b 100644 --- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_StartEspPairing_Handler.java +++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_StartEspPairing_Handler.java @@ -66,6 +66,16 @@ public class Net_StartEspPairing_Handler implements JsonMessageHandler { } String canonicalLogin = user.getLogin(); + AuthAccessScopeSupport.Resolution authResolution = AuthAccessScopeSupport.resolveLogin(canonicalLogin); + if (!AuthAccessScopeSupport.RESOLUTION_LOCAL.equals(authResolution.resolution())) { + return NetExceptionResponseFactory.error( + req, + 409, + "USER_NOT_LOCAL", + "Вход через доверенное устройство нужно начинать на access server пользователя" + ); + } + EspPairingSettingsEntry settings = EspPairingSettingsDAO.getInstance().getByLogin(canonicalLogin); boolean enabled = settings == null || settings.isEnabled(); if (!enabled) { @@ -100,14 +110,6 @@ public class Net_StartEspPairing_Handler implements JsonMessageHandler { String clientPlatform = AuthSessionTypeSupport.normalizeClientPlatform(req.getRequesterClientPlatform()); int ttlSeconds = EspPairingSupport.DEFAULT_TTL_SECONDS; List approverConnections = EspPairingSupport.findOnlineTrustedConnections(canonicalLogin); - if (approverConnections.isEmpty()) { - return NetExceptionResponseFactory.error( - req, - 422, - "PAIRING_NO_TRUSTED_SESSION_ONLINE", - "Нет ни одной активной доверенной сессии пользователя в сети" - ); - } EspPairingSupport.PairingFingerprint fingerprint = EspPairingSupport.deriveFingerprint( canonicalLogin, requesterSessionKey, diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/entyties/Net_CreateAuthSession_Response.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/entyties/Net_CreateAuthSession_Response.java index 78b17822..e96f5b54 100644 --- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/entyties/Net_CreateAuthSession_Response.java +++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/entyties/Net_CreateAuthSession_Response.java @@ -22,6 +22,7 @@ public class Net_CreateAuthSession_Response extends Net_Response { /** Идентификатор сессии, base64 от 32 байт. */ private String sessionId; + private String connectionScope; public String getSessionId() { return sessionId; @@ -30,4 +31,7 @@ public class Net_CreateAuthSession_Response extends Net_Response { public void setSessionId(String sessionId) { this.sessionId = sessionId; } + + public String getConnectionScope() { return connectionScope; } + public void setConnectionScope(String connectionScope) { this.connectionScope = connectionScope; } } \ No newline at end of file diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/entyties/Net_ResolveLoginForAuth_Request.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/entyties/Net_ResolveLoginForAuth_Request.java new file mode 100644 index 00000000..7305db6a --- /dev/null +++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/entyties/Net_ResolveLoginForAuth_Request.java @@ -0,0 +1,11 @@ +package server.logic.ws_protocol.JSON.handlers.auth.entyties; + +import server.logic.ws_protocol.JSON.entyties.Net_Request; + +/** Первый шаг UI-входа: определить существование пользователя и его access server. */ +public class Net_ResolveLoginForAuth_Request extends Net_Request { + private String login; + + public String getLogin() { return login; } + public void setLogin(String login) { this.login = login; } +} diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/entyties/Net_ResolveLoginForAuth_Response.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/entyties/Net_ResolveLoginForAuth_Response.java new file mode 100644 index 00000000..57849499 --- /dev/null +++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/entyties/Net_ResolveLoginForAuth_Response.java @@ -0,0 +1,31 @@ +package server.logic.ws_protocol.JSON.handlers.auth.entyties; + +import server.logic.ws_protocol.JSON.entyties.Net_Response; + +/** + * Ответ ResolveLoginForAuth. + * + * resolution: + * - LOCAL пользователь существует и относится к этому серверу; + * - REMOTE пользователь существует, но его первый access server другой; + * - NOT_FOUND пользователя нет в текущем Solana PDA snapshot; + * - NO_ACCESS_SERVER пользователь существует, но корректный access server не найден. + */ +public class Net_ResolveLoginForAuth_Response extends Net_Response { + private String resolution; + private String login; + private String accessServerLogin; + private String accessServerUrl; + + public String getResolution() { return resolution; } + public void setResolution(String resolution) { this.resolution = resolution; } + + public String getLogin() { return login; } + public void setLogin(String login) { this.login = login; } + + public String getAccessServerLogin() { return accessServerLogin; } + public void setAccessServerLogin(String accessServerLogin) { this.accessServerLogin = accessServerLogin; } + + public String getAccessServerUrl() { return accessServerUrl; } + public void setAccessServerUrl(String accessServerUrl) { this.accessServerUrl = accessServerUrl; } +} diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/entyties/Net_SessionLogin_Response.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/entyties/Net_SessionLogin_Response.java index 4b4f2526..ca7913b3 100644 --- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/entyties/Net_SessionLogin_Response.java +++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/entyties/Net_SessionLogin_Response.java @@ -9,6 +9,7 @@ import server.logic.ws_protocol.JSON.entyties.Net_Response; public class Net_SessionLogin_Response extends Net_Response { private String storagePwd; + private String connectionScope; public String getStoragePwd() { return storagePwd; @@ -17,4 +18,7 @@ public class Net_SessionLogin_Response extends Net_Response { public void setStoragePwd(String storagePwd) { this.storagePwd = storagePwd; } + + public String getConnectionScope() { return connectionScope; } + public void setConnectionScope(String connectionScope) { this.connectionScope = connectionScope; } } \ No newline at end of file diff --git a/VERSION.properties b/VERSION.properties index 20b9c72a..638df9af 100644 --- a/VERSION.properties +++ b/VERSION.properties @@ -1,2 +1,2 @@ -client.version=1.7.3 -server.version=1.6.1 +client.version=1.7.4 +server.version=1.6.2 diff --git a/docs/API/02_Authentication_API.md b/docs/API/02_Authentication_API.md index 470434b4..299ed4f0 100644 --- a/docs/API/02_Authentication_API.md +++ b/docs/API/02_Authentication_API.md @@ -4,6 +4,7 @@ Здесь четыре базовых метода обычной авторизации: +- `ResolveLoginForAuth` - `AuthChallenge` - `CreateAuthSession` - `SessionChallenge` @@ -67,6 +68,57 @@ ed25519/BASE64_PUBLIC_KEY --- +## 1.1. `ResolveLoginForAuth` + +Первый шаг интерактивного входа. Операция не заменяет `GetUser` и не меняет его семантику. +Она предназначена только для выбора правильного access server перед вводом пароля +или запуском входа через доверенное устройство. + +Запрос: + +```json +{ + "op": "ResolveLoginForAuth", + "requestId": "login-route-1", + "payload": { + "login": "alice" + } +} +``` + +Успешный ответ всегда имеет `status=200`, а `payload.resolution` принимает одно из значений: + +- `LOCAL` — пользователь существует, и `access_servers[0]` указывает на этот сервер; +- `REMOTE` — пользователь существует, но его первый access server другой; +- `NOT_FOUND` — пользователя нет в локально синхронизированном Solana PDA snapshot; +- `NO_ACCESS_SERVER` — пользователь существует, но корректный первый access server не найден. + +Для `LOCAL` и `REMOTE` сервер возвращает канонический `login`. Для `REMOTE` +также возвращаются `accessServerLogin` и `accessServerUrl`, чтобы UI сразу мог +перенаправить пользователя на правильную точку входа. + +Пример `REMOTE`: + +```json +{ + "op": "ResolveLoginForAuth", + "requestId": "login-route-1", + "status": 200, + "ok": true, + "payload": { + "resolution": "REMOTE", + "login": "Alice", + "accessServerLogin": "shine-node-2", + "accessServerUrl": "wss://node2.example.org/ws" + } +} +``` + +Важно: это UX-проверка, а не security boundary. `AuthChallenge/CreateAuthSession` +повторно определяют отношение пользователя к текущему серверу. Пользователь другого +access server может криптографически авторизоваться напрямую, но получает +`connectionScope=REMOTE`. + ## 2. `AuthChallenge` ### Запрос @@ -339,3 +391,33 @@ SESSION_LOGIN:{sessionId}:{timeMs}:{nonce} } } ``` + +### LOCAL/REMOTE scope успешной авторизации + +`CreateAuthSession` и `SessionLogin` дополнительно возвращают: + +```json +{ + "connectionScope": "LOCAL" +} +``` + +или: + +```json +{ + "connectionScope": "REMOTE" +} +``` + +`REMOTE` означает, что пользователь успешно доказал владение ключом/сессией, но +текущий сервер не является его `access_servers[0]`. Такой WebSocket регистрируется +как активное соединение и может использоваться для realtime звонков, однако +центральная политика сервера разрешает ему только технические и call-signaling +операции. Обычные homeserver-операции отвечают `403 / USER_NOT_LOCAL`. + +Разрешённый минимальный набор для `REMOTE`: `Ping`, `GetServerInfo`, +`GetCallIceConfig`, `CallInviteBroadcast`, `CallSignalToSession`, `SendSignal`, +`CallDeliveryReport`, `ClientErrorLog`, `ClientDebugLog`, `CloseActiveSession` +и `ResolveLoginForAuth`. + diff --git a/docs/API/03_Session_Management_API.md b/docs/API/03_Session_Management_API.md index d7c28a04..6d304b49 100644 --- a/docs/API/03_Session_Management_API.md +++ b/docs/API/03_Session_Management_API.md @@ -313,9 +313,14 @@ TTL заявки фиксирован на сервере и сейчас все - `400 / BAD_PAYLOAD_TYPE` - `422 / PAIRING_NOT_AVAILABLE` - `422 / PAIRING_PASSWORD_INVALID` — pairing-пароль не подходит. Та же ошибка возвращается и если новое устройство ввело пароль, а у пользователя режим pairing включён без пароля. -- `422 / PAIRING_NO_TRUSTED_SESSION_ONLINE` — сейчас нет ни одной онлайн доверённой сессии пользователя, поэтому код не создаётся. +Код создаётся даже если сейчас нет ни одной онлайн доверённой сессии пользователя. +В таком случае `trustedSessionOnline=false`, а pairing-заявка остаётся в состоянии +`created` до подтверждения, отмены или истечения TTL. - `429 / PAIRING_RATE_LIMITED` +Также `StartTrustedDeviceLogin` разрешён только на первом access server пользователя. +На другом сервере операция возвращает `409 / USER_NOT_LOCAL`. + ### 5.4. `ListTrustedDeviceLoginRequests` Доступно для любой уже авторизованной доверенной сессии пользователя. diff --git a/docs/API/09_Operations_Index.md b/docs/API/09_Operations_Index.md index acad5a65..8cf2e7d1 100644 --- a/docs/API/09_Operations_Index.md +++ b/docs/API/09_Operations_Index.md @@ -16,6 +16,7 @@ | `SearchUsers` | `01_User_Registration_API.md` | поиск логинов по префиксу | | `TestGetFreeAvatarQuota` | `14_Test_Free_Avatar_Upload_API.md` | временный тестовый просмотр остатка бесплатных загрузок аватара | | `TestUploadFreeAvatar` | `14_Test_Free_Avatar_Upload_API.md` | временная тестовая бесплатная загрузка маленького аватара в Arweave | +| `ResolveLoginForAuth` | `02_Authentication_API.md` | проверка login перед входом: LOCAL / REMOTE / NOT_FOUND / NO_ACCESS_SERVER + URL правильного access server | | `AuthChallenge` | `02_Authentication_API.md` | challenge для создания новой сессии | | `CreateAuthSession` | `02_Authentication_API.md` | создание новой авторизованной сессии | | `SessionChallenge` | `02_Authentication_API.md` | challenge для входа в существующую сессию | diff --git a/shine-UI/js/pages/login-other-device-view.js b/shine-UI/js/pages/login-other-device-view.js index 13f1b1b2..ab9d0945 100644 --- a/shine-UI/js/pages/login-other-device-view.js +++ b/shine-UI/js/pages/login-other-device-view.js @@ -73,7 +73,7 @@ export function render({ navigate }) { screen.append( renderHeader({ title: 'Войти через другое устройство', - leftAction: { label: '←', onClick: () => navigate('login-view') }, + leftAction: { label: '←', onClick: () => { void cancelActivePairingAndBack(); } }, }), ); @@ -82,22 +82,17 @@ export function render({ navigate }) { panel.innerHTML = '

Войти через другое устройство

'; const formCard = document.createElement('div'); - formCard.className = 'card stack'; + formCard.className = 'card stack login-device-preparation'; formCard.innerHTML = ` - - -