11 12 25
Добавил Вывсти список активных сессий!!
This commit is contained in:
parent
a6be7b75aa
commit
096246542d
@ -4,6 +4,8 @@ import shine.db.SqliteDbController;
|
|||||||
import shine.db.entities.ActiveSession;
|
import shine.db.entities.ActiveSession;
|
||||||
|
|
||||||
import java.sql.*;
|
import java.sql.*;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DAO для таблицы active_sessions.
|
* DAO для таблицы active_sessions.
|
||||||
@ -123,9 +125,46 @@ public final class ActiveSessionsDAO {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить список всех активных сессий пользователя по loginId.
|
||||||
|
*/
|
||||||
|
public List<ActiveSession> getByLoginId(long loginId) throws SQLException {
|
||||||
|
String sql = """
|
||||||
|
SELECT
|
||||||
|
sessionId,
|
||||||
|
loginId,
|
||||||
|
sessionPwd,
|
||||||
|
storagePwd,
|
||||||
|
sessionCreatedAtMs,
|
||||||
|
lastAuthirificatedAtMs,
|
||||||
|
pushEndpoint,
|
||||||
|
pushP256dhKey,
|
||||||
|
pushAuthKey,
|
||||||
|
clientIp,
|
||||||
|
clientInfoFromClient,
|
||||||
|
clientInfoFromRequest,
|
||||||
|
userLanguage
|
||||||
|
FROM active_sessions
|
||||||
|
WHERE loginId = ?
|
||||||
|
""";
|
||||||
|
|
||||||
|
List<ActiveSession> result = new ArrayList<>();
|
||||||
|
|
||||||
|
try (PreparedStatement ps = db.getConnection().prepareStatement(sql)) {
|
||||||
|
ps.setLong(1, loginId);
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
while (rs.next()) {
|
||||||
|
result.add(mapRow(rs));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Обновить только lastAuthirificatedAtMs для конкретной сессии.
|
* Обновить только lastAuthirificatedAtMs для конкретной сессии.
|
||||||
* (оставлено для совместимости)
|
* (оставляю для совместимости, вдруг ещё где-то используется)
|
||||||
*/
|
*/
|
||||||
public void updateLastAuthirificatedAtMs(String sessionId, long lastAuthMs) throws SQLException {
|
public void updateLastAuthirificatedAtMs(String sessionId, long lastAuthMs) throws SQLException {
|
||||||
String sql = """
|
String sql = """
|
||||||
|
|||||||
@ -5,12 +5,14 @@ import server.logic.ws_protocol.JSON.entyties.Auth.Net_AuthChallenge_Request;
|
|||||||
import server.logic.ws_protocol.JSON.entyties.Auth.Net_CreateAuthSession_Request;
|
import server.logic.ws_protocol.JSON.entyties.Auth.Net_CreateAuthSession_Request;
|
||||||
import server.logic.ws_protocol.JSON.entyties.Auth.Net_RefreshSession_Request;
|
import server.logic.ws_protocol.JSON.entyties.Auth.Net_RefreshSession_Request;
|
||||||
import server.logic.ws_protocol.JSON.entyties.Auth.Net_CloseActiveSession_Request;
|
import server.logic.ws_protocol.JSON.entyties.Auth.Net_CloseActiveSession_Request;
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Auth.Net_ListSessions_Request;
|
||||||
import server.logic.ws_protocol.JSON.entyties.tempToTest.Net_AddUser_Request;
|
import server.logic.ws_protocol.JSON.entyties.tempToTest.Net_AddUser_Request;
|
||||||
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
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_AuthChallenge_Handler;
|
||||||
import server.logic.ws_protocol.JSON.handlers.auth.Net_CreateAuthSession__Handler;
|
import server.logic.ws_protocol.JSON.handlers.auth.Net_CreateAuthSession__Handler;
|
||||||
import server.logic.ws_protocol.JSON.handlers.auth.Net_RefreshSession_Handler;
|
import server.logic.ws_protocol.JSON.handlers.auth.Net_RefreshSession_Handler;
|
||||||
import server.logic.ws_protocol.JSON.handlers.auth.Net_CloseActiveSession_Handler;
|
import server.logic.ws_protocol.JSON.handlers.auth.Net_CloseActiveSession_Handler;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.auth.Net_ListSessions_Handler;
|
||||||
import server.logic.ws_protocol.JSON.handlers.tempToTest.Net_AddUser_Handler;
|
import server.logic.ws_protocol.JSON.handlers.tempToTest.Net_AddUser_Handler;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@ -18,11 +20,11 @@ import java.util.Map;
|
|||||||
/**
|
/**
|
||||||
* JsonHandlerRegistry — единое место, где руками регистрируются
|
* JsonHandlerRegistry — единое место, где руками регистрируются
|
||||||
* JSON-операции: op → handler и op → requestClass.
|
* JSON-операции: op → handler и op → requestClass.
|
||||||
*.
|
*
|
||||||
* Если нужно добавить новый запрос:
|
* Если нужно добавить новый запрос:
|
||||||
* 1) создаёшь класс NetXXXRequest / NetXXXResponse,
|
* 1) создаёшь класс NetXXXRequest / NetXXXResponse,
|
||||||
* 2) создаёшь JsonMessageHandler (NetXXXHandler),
|
* 2) создаёшь JsonMessageHandler (NetXXXHandler),
|
||||||
* 3) добавляешь оп в HANDLERS и REQUEST_TYPES.
|
* 3) добавляешь op в HANDLERS и REQUEST_TYPES.
|
||||||
*/
|
*/
|
||||||
public final class JsonHandlerRegistry {
|
public final class JsonHandlerRegistry {
|
||||||
|
|
||||||
@ -31,7 +33,8 @@ public final class JsonHandlerRegistry {
|
|||||||
"AddUser", new Net_AddUser_Handler(),
|
"AddUser", new Net_AddUser_Handler(),
|
||||||
"AuthChallenge", new Net_AuthChallenge_Handler(),
|
"AuthChallenge", new Net_AuthChallenge_Handler(),
|
||||||
"CreateAuthSession", new Net_CreateAuthSession__Handler(),
|
"CreateAuthSession", new Net_CreateAuthSession__Handler(),
|
||||||
"CloseActiveSession", new Net_CloseActiveSession_Handler()
|
"CloseActiveSession", new Net_CloseActiveSession_Handler(),
|
||||||
|
"ListSessions", new Net_ListSessions_Handler()
|
||||||
// сюда потом добавишь другие операции
|
// сюда потом добавишь другие операции
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -40,7 +43,8 @@ public final class JsonHandlerRegistry {
|
|||||||
"AddUser", Net_AddUser_Request.class,
|
"AddUser", Net_AddUser_Request.class,
|
||||||
"AuthChallenge", Net_AuthChallenge_Request.class,
|
"AuthChallenge", Net_AuthChallenge_Request.class,
|
||||||
"CreateAuthSession", Net_CreateAuthSession_Request.class,
|
"CreateAuthSession", Net_CreateAuthSession_Request.class,
|
||||||
"CloseActiveSession", Net_CloseActiveSession_Request.class
|
"CloseActiveSession", Net_CloseActiveSession_Request.class,
|
||||||
|
"ListSessions", Net_ListSessions_Request.class
|
||||||
);
|
);
|
||||||
|
|
||||||
private JsonHandlerRegistry() {
|
private JsonHandlerRegistry() {
|
||||||
|
|||||||
@ -0,0 +1,54 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.entyties.Auth;
|
||||||
|
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Запрос ListSessions — список активных сессий пользователя.
|
||||||
|
*
|
||||||
|
* Режимы безопасности такие же, как у CloseActiveSession:
|
||||||
|
*
|
||||||
|
* 1) Пользователь уже авторизован (AUTH_STATUS_USER):
|
||||||
|
* - поля timeMs и signatureB64 могут быть пустыми и игнорируются.
|
||||||
|
*
|
||||||
|
* 2) Пользователь в статусе AUTH_STATUS_AUTH_IN_PROGRESS:
|
||||||
|
* - требуется подпись Ed25519 над строкой
|
||||||
|
* "AUTHORIFICATED:" + timeMs + authNonce
|
||||||
|
* (authNonce сохранён в ctx.authNonce после AuthChallenge).
|
||||||
|
*
|
||||||
|
* 3) Анонимный клиент (AUTH_STATUS_NONE или нет пользователя в ctx):
|
||||||
|
* - возвращается ошибка NOT_AUTHENTICATED.
|
||||||
|
*
|
||||||
|
* JSON:
|
||||||
|
* {
|
||||||
|
* "op": "ListSessions",
|
||||||
|
* "requestId": "...",
|
||||||
|
* "payload": {
|
||||||
|
* "timeMs": 1733310000000, // при AUTH_IN_PROGRESS
|
||||||
|
* "signatureB64": "base64-подпись" // при AUTH_IN_PROGRESS
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
public class Net_ListSessions_Request extends Net_Request {
|
||||||
|
|
||||||
|
/** Время на стороне клиента (мс с 1970-01-01). Используется при AUTH_IN_PROGRESS. */
|
||||||
|
private long timeMs;
|
||||||
|
|
||||||
|
/** Подпись Ed25519 над строкой "AUTHORIFICATED:" + timeMs + authNonce (base64). */
|
||||||
|
private String signatureB64;
|
||||||
|
|
||||||
|
public long getTimeMs() {
|
||||||
|
return timeMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTimeMs(long timeMs) {
|
||||||
|
this.timeMs = timeMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSignatureB64() {
|
||||||
|
return signatureB64;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSignatureB64(String signatureB64) {
|
||||||
|
this.signatureB64 = signatureB64;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,103 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.entyties.Auth;
|
||||||
|
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ответ на ListSessions.
|
||||||
|
*
|
||||||
|
* При успехе:
|
||||||
|
* - status = 200;
|
||||||
|
* - payload:
|
||||||
|
* {
|
||||||
|
* "sessions": [
|
||||||
|
* {
|
||||||
|
* "sessionId": "...",
|
||||||
|
* "clientInfoFromClient": "...",
|
||||||
|
* "clientInfoFromRequest": "...",
|
||||||
|
* "geo": "Country, City" | "unknown",
|
||||||
|
* "lastAuthirificatedAtMs": 1733310000000
|
||||||
|
* },
|
||||||
|
* ...
|
||||||
|
* ]
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
public class Net_ListSessions_Response extends Net_Response {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Список активных сессий для текущего пользователя.
|
||||||
|
*/
|
||||||
|
private List<SessionInfo> sessions;
|
||||||
|
|
||||||
|
public List<SessionInfo> getSessions() {
|
||||||
|
return sessions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSessions(List<SessionInfo> sessions) {
|
||||||
|
this.sessions = sessions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Описание одной активной сессии.
|
||||||
|
*/
|
||||||
|
public static class SessionInfo {
|
||||||
|
|
||||||
|
/** Идентификатор сессии, base64 от 32 байт. */
|
||||||
|
private String sessionId;
|
||||||
|
|
||||||
|
/** Что прислал клиент в CreateAuthSession/RefreshSession (clientInfo). */
|
||||||
|
private String clientInfoFromClient;
|
||||||
|
|
||||||
|
/** Краткая строка, собранная сервером из HTTP-запроса (UA, платформа и т.п.). */
|
||||||
|
private String clientInfoFromRequest;
|
||||||
|
|
||||||
|
/** Строка геолокации вида "Country, City" или "unknown". */
|
||||||
|
private String geo;
|
||||||
|
|
||||||
|
/** Время последней успешной авторизации/refresh (мс с 1970-01-01). */
|
||||||
|
private long lastAuthirificatedAtMs;
|
||||||
|
|
||||||
|
// --- getters / setters ---
|
||||||
|
|
||||||
|
public String getSessionId() {
|
||||||
|
return sessionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSessionId(String sessionId) {
|
||||||
|
this.sessionId = sessionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getClientInfoFromClient() {
|
||||||
|
return clientInfoFromClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setClientInfoFromClient(String clientInfoFromClient) {
|
||||||
|
this.clientInfoFromClient = clientInfoFromClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getClientInfoFromRequest() {
|
||||||
|
return clientInfoFromRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setClientInfoFromRequest(String clientInfoFromRequest) {
|
||||||
|
this.clientInfoFromRequest = clientInfoFromRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getGeo() {
|
||||||
|
return geo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setGeo(String geo) {
|
||||||
|
this.geo = geo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getLastAuthirificatedAtMs() {
|
||||||
|
return lastAuthirificatedAtMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLastAuthirificatedAtMs(long lastAuthirificatedAtMs) {
|
||||||
|
this.lastAuthirificatedAtMs = lastAuthirificatedAtMs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,169 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.handlers.auth;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import server.logic.ws_protocol.JSON.ConnectionContext;
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Auth.Net_ListSessions_Request;
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Auth.Net_ListSessions_Response;
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Auth.Net_ListSessions_Response.SessionInfo;
|
||||||
|
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.utils.NetExceptionResponseFactory;
|
||||||
|
import server.logic.ws_protocol.WireCodes;
|
||||||
|
import shine.db.dao.ActiveSessionsDAO;
|
||||||
|
import shine.db.entities.ActiveSession;
|
||||||
|
import shine.db.entities.SolanaUser;
|
||||||
|
import shine.geo.GeoLookupService;
|
||||||
|
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Хэндлер ListSessions.
|
||||||
|
*
|
||||||
|
* Назначение:
|
||||||
|
* - вернуть список всех активных сессий текущего пользователя
|
||||||
|
* (по loginId из ctx/solanaUser).
|
||||||
|
*
|
||||||
|
* Безопасность:
|
||||||
|
* - анонимный клиент → NOT_AUTHENTICATED (UNVERIFIED);
|
||||||
|
* - AUTH_STATUS_USER → достаточно факта авторизации;
|
||||||
|
* - AUTH_STATUS_AUTH_IN_PROGRESS → требуется подпись, как в CreateAuthSession/CloseActiveSession.
|
||||||
|
*/
|
||||||
|
public class Net_ListSessions_Handler implements JsonMessageHandler {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(Net_ListSessions_Handler.class);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
|
||||||
|
Net_ListSessions_Request req = (Net_ListSessions_Request) baseReq;
|
||||||
|
|
||||||
|
// 1) Проверяем, что вообще есть пользователь в контексте
|
||||||
|
if (ctx == null || ctx.getSolanaUser() == null) {
|
||||||
|
return NetExceptionResponseFactory.error(
|
||||||
|
req,
|
||||||
|
WireCodes.Status.UNVERIFIED,
|
||||||
|
"NOT_AUTHENTICATED",
|
||||||
|
"Операция доступна только для авторизованных пользователей"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SolanaUser user = ctx.getSolanaUser();
|
||||||
|
long currentLoginId = user.getLoginId();
|
||||||
|
|
||||||
|
int authStatus = ctx.getAuthenticationStatus();
|
||||||
|
if (authStatus != ConnectionContext.AUTH_STATUS_USER
|
||||||
|
&& authStatus != ConnectionContext.AUTH_STATUS_AUTH_IN_PROGRESS) {
|
||||||
|
|
||||||
|
return NetExceptionResponseFactory.error(
|
||||||
|
req,
|
||||||
|
WireCodes.Status.UNVERIFIED,
|
||||||
|
"BAD_AUTH_STATUS",
|
||||||
|
"Операция ListSessions недоступна в текущем статусе аутентификации"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Если мы ещё на шаге AUTH_IN_PROGRESS — проверяем подпись
|
||||||
|
if (authStatus == ConnectionContext.AUTH_STATUS_AUTH_IN_PROGRESS) {
|
||||||
|
String authNonce = ctx.getAuthNonce();
|
||||||
|
if (authNonce == null) {
|
||||||
|
return NetExceptionResponseFactory.error(
|
||||||
|
req,
|
||||||
|
WireCodes.Status.BAD_REQUEST,
|
||||||
|
"NO_STEP1_CONTEXT",
|
||||||
|
"Шаг 1 авторизации не был корректно выполнен для данного соединения"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
long timeMs = req.getTimeMs();
|
||||||
|
String signatureB64 = req.getSignatureB64();
|
||||||
|
|
||||||
|
if (signatureB64 == null || signatureB64.isBlank()) {
|
||||||
|
return NetExceptionResponseFactory.error(
|
||||||
|
req,
|
||||||
|
WireCodes.Status.BAD_REQUEST,
|
||||||
|
"EMPTY_SIGNATURE",
|
||||||
|
"Подпись обязательна при статусе AUTH_IN_PROGRESS"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
long nowMs = System.currentTimeMillis();
|
||||||
|
long diff = Math.abs(nowMs - timeMs);
|
||||||
|
if (diff > Net_CreateAuthSession__Handler.ALLOWED_SKEW_MS) {
|
||||||
|
return NetExceptionResponseFactory.error(
|
||||||
|
req,
|
||||||
|
WireCodes.Status.BAD_REQUEST,
|
||||||
|
"TIME_SKEW",
|
||||||
|
"Время клиента отличается от сервера более чем на 30 секунд"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean sigOk;
|
||||||
|
try {
|
||||||
|
sigOk = Net_CreateAuthSession__Handler.verifyAuthorificatedSignature(
|
||||||
|
user,
|
||||||
|
authNonce,
|
||||||
|
timeMs,
|
||||||
|
signatureB64
|
||||||
|
);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
return NetExceptionResponseFactory.error(
|
||||||
|
req,
|
||||||
|
WireCodes.Status.BAD_REQUEST,
|
||||||
|
"BAD_BASE64",
|
||||||
|
"Некорректный формат Base64 для ключа или подписи"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sigOk) {
|
||||||
|
return NetExceptionResponseFactory.error(
|
||||||
|
req,
|
||||||
|
WireCodes.Status.UNVERIFIED,
|
||||||
|
"BAD_SIGNATURE",
|
||||||
|
"Подпись не прошла проверку"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) Тянем все активные сессии пользователя из БД
|
||||||
|
List<ActiveSession> sessions;
|
||||||
|
try {
|
||||||
|
sessions = ActiveSessionsDAO.getInstance().getByLoginId(currentLoginId);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
log.error("Ошибка БД при получении списка сессий для loginId={}", currentLoginId, e);
|
||||||
|
return NetExceptionResponseFactory.error(
|
||||||
|
req,
|
||||||
|
WireCodes.Status.SERVER_DATA_ERROR,
|
||||||
|
"DB_ERROR_LIST_SESSIONS",
|
||||||
|
"Ошибка доступа к базе данных при получении списка сессий"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) Собираем DTO с геолокацией
|
||||||
|
List<SessionInfo> resultList = new ArrayList<>();
|
||||||
|
for (ActiveSession s : sessions) {
|
||||||
|
SessionInfo info = new SessionInfo();
|
||||||
|
info.setSessionId(s.getSessionId());
|
||||||
|
info.setClientInfoFromClient(s.getClientInfoFromClient());
|
||||||
|
info.setClientInfoFromRequest(s.getClientInfoFromRequest());
|
||||||
|
info.setLastAuthirificatedAtMs(s.getLastAuthirificatedAtMs());
|
||||||
|
|
||||||
|
String ip = s.getClientIp();
|
||||||
|
String geo = GeoLookupService.resolveCountryCityOrIpWithCache(ip);
|
||||||
|
info.setGeo(geo);
|
||||||
|
|
||||||
|
resultList.add(info);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5) Формируем ответ
|
||||||
|
Net_ListSessions_Response resp = new Net_ListSessions_Response();
|
||||||
|
resp.setOp(req.getOp());
|
||||||
|
resp.setRequestId(req.getRequestId());
|
||||||
|
resp.setStatus(WireCodes.Status.OK);
|
||||||
|
resp.setSessions(resultList);
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user