{{ service.latency }}ms
@@ -115,7 +115,7 @@
diff --git a/frontend/src/views/Users.vue b/frontend/src/views/Users.vue
new file mode 100644
index 0000000..98f2236
--- /dev/null
+++ b/frontend/src/views/Users.vue
@@ -0,0 +1,124 @@
+
+
+
+
Users Management
+
+
+
+
+
+
+
+ | ID |
+ Login |
+ IP |
+ Created |
+ Actions |
+
+
+
+
+ | {{ user.id }} |
+ {{ user.login }} |
+ {{ user.ip || '-' }} |
+ {{ formatDate(user.created) }} |
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/java/su/xserver/iikocon/DateRangeSetup.java b/src/main/java/su/xserver/iikocon/DateRangeSetup.java
new file mode 100644
index 0000000..09a85a2
--- /dev/null
+++ b/src/main/java/su/xserver/iikocon/DateRangeSetup.java
@@ -0,0 +1,50 @@
+package su.xserver.iikocon;
+
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+
+public class DateRangeSetup {
+ public static void main(String[] args) {
+ // Параметры по умолчанию
+ String login = "4444";
+ String password = "4444";
+ String server = "folk-amber-co.iiko.it";
+ String presetId = "7ddc40c3-9d5f-408f-aa1e-652964b36c6c";
+
+ // Вычисление dateFrom и dateTo
+ LocalDate today = LocalDate.now();
+ LocalDate dateFrom = today.minusDays(7);
+ LocalDate dateTo = today;
+
+ // Переопределение из аргументов командной строки
+ if (args.length > 0 && args[0] != null && !args[0].isEmpty()) {
+ try {
+ dateFrom = LocalDate.parse(args[0]);
+ } catch (DateTimeParseException e) {
+ System.err.println("Ошибка парсинга dateFrom: " + args[0] + ". Используется значение по умолчанию.");
+ }
+ }
+
+ if (args.length > 1 && args[1] != null && !args[1].isEmpty()) {
+ try {
+ dateTo = LocalDate.parse(args[1]);
+ } catch (DateTimeParseException e) {
+ System.err.println("Ошибка парсинга dateTo: " + args[1] + ". Используется значение по умолчанию.");
+ }
+ }
+
+ // Форматирование дат в YYYY-MM-DD
+ DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+ String formattedDateFrom = dateFrom.format(formatter);
+ String formattedDateTo = dateTo.format(formatter);
+
+ // Вывод переменных (можно заменить на дальнейшее использование)
+ System.out.println("login=" + login);
+ System.out.println("password=" + password);
+ System.out.println("server=" + server);
+ System.out.println("presetId=" + presetId);
+ System.out.println("dateFrom=" + formattedDateFrom);
+ System.out.println("dateTo=" + formattedDateTo);
+ }
+}
diff --git a/src/main/java/su/xserver/iikocon/MainVerticle.java b/src/main/java/su/xserver/iikocon/MainVerticle.java
index 91ab754..d4ee32f 100644
--- a/src/main/java/su/xserver/iikocon/MainVerticle.java
+++ b/src/main/java/su/xserver/iikocon/MainVerticle.java
@@ -156,6 +156,42 @@ public class MainVerticle extends AbstractVerticle {
}
}));
+ router.post("/api/admin/users").handler(rc -> {
+ JsonObject body = rc.body().asJsonObject();
+ String login = body.getString("login");
+ String password = body.getString("password");
+ String ip = rc.request().remoteAddress().host();
+ if (login == null || password == null) {
+ rc.response().setStatusCode(400).end("Missing login or password");
+ return;
+ }
+ userService.createUser(login, password, ip)
+ .onSuccess(v -> rc.response().setStatusCode(201).end())
+ .onFailure(err -> rc.response().setStatusCode(500).end(err.getMessage()));
+ });
+
+ router.put("/api/admin/users/:id").handler(rc -> {
+ int id = Integer.parseInt(rc.pathParam("id"));
+ JsonObject body = rc.body().asJsonObject();
+ String login = body.getString("login");
+ String password = body.getString("password");
+ String ip = rc.request().remoteAddress().host();
+ if (login == null) {
+ rc.response().setStatusCode(400).end("Missing login");
+ return;
+ }
+ userService.updateUser(id, login, password, ip)
+ .onSuccess(v -> rc.response().end())
+ .onFailure(err -> rc.response().setStatusCode(500).end(err.getMessage()));
+ });
+
+ router.delete("/api/admin/users/:id").handler(rc -> {
+ int id = Integer.parseInt(rc.pathParam("id"));
+ userService.deleteUser(id)
+ .onSuccess(v -> rc.response().end())
+ .onFailure(err -> rc.response().setStatusCode(500).end(err.getMessage()));
+ });
+
// Получение текущего пользователя
router.get("/api/admin/me").handler(rc -> {
Integer userId = rc.session().get("userId");
@@ -171,6 +207,64 @@ public class MainVerticle extends AbstractVerticle {
}
});
+ router.get("/api/admin/restaurants").handler(rc -> restaurantService.getAllRestaurants().onComplete(ar -> {
+ if (ar.succeeded()) {
+ rc.response()
+ .putHeader("Content-Type", "application/json")
+ .end(ar.result().encode());
+ } else {
+ rc.response().setStatusCode(500).end(ar.cause().getMessage());
+ }
+ }));
+
+ router.get("/api/admin/restaurants/:id").handler(rc -> {
+ int id = Integer.parseInt(rc.pathParam("id"));
+ restaurantService.findById(id)
+ .onSuccess(rest -> {
+ if (rest == null) rc.response().setStatusCode(404).end();
+ else rc.response().putHeader("Content-Type", "application/json").end(rest.encode());
+ })
+ .onFailure(err -> rc.response().setStatusCode(500).end(err.getMessage()));
+ });
+
+ router.post("/api/admin/restaurants").handler(rc -> {
+ JsonObject body = rc.body().asJsonObject();
+ String name = body.getString("name");
+ String login = body.getString("login");
+ String password = body.getString("password");
+ String host = body.getString("host");
+ if (name == null || login == null || password == null || host == null) {
+ rc.response().setStatusCode(400).end("Missing fields");
+ return;
+ }
+ restaurantService.createRestaurant(name, login, password, host)
+ .onSuccess(v -> rc.response().setStatusCode(201).end())
+ .onFailure(err -> rc.response().setStatusCode(500).end(err.getMessage()));
+ });
+
+ router.put("/api/admin/restaurants/:id").handler(rc -> {
+ int id = Integer.parseInt(rc.pathParam("id"));
+ JsonObject body = rc.body().asJsonObject();
+ String name = body.getString("name");
+ String login = body.getString("login");
+ String password = body.getString("password");
+ String host = body.getString("host");
+ if (name == null || login == null || host == null) {
+ rc.response().setStatusCode(400).end("Missing required fields");
+ return;
+ }
+ restaurantService.updateRestaurant(id, name, login, password, host)
+ .onSuccess(v -> rc.response().end())
+ .onFailure(err -> rc.response().setStatusCode(500).end(err.getMessage()));
+ });
+
+ router.delete("/api/admin/restaurants/:id").handler(rc -> {
+ int id = Integer.parseInt(rc.pathParam("id"));
+ restaurantService.deleteRestaurant(id)
+ .onSuccess(v -> rc.response().end())
+ .onFailure(err -> rc.response().setStatusCode(500).end(err.getMessage()));
+ });
+
return router;
}
diff --git a/src/main/java/su/xserver/iikocon/ProxyVerticle.java b/src/main/java/su/xserver/iikocon/ProxyVerticle.java
new file mode 100644
index 0000000..26320cd
--- /dev/null
+++ b/src/main/java/su/xserver/iikocon/ProxyVerticle.java
@@ -0,0 +1,194 @@
+package su.xserver.iikocon;
+
+import io.vertx.core.AbstractVerticle;
+import io.vertx.core.Promise;
+import io.vertx.core.http.HttpMethod;
+import io.vertx.core.json.Json;
+import io.vertx.core.json.JsonObject;
+import io.vertx.ext.web.Router;
+import io.vertx.ext.web.RoutingContext;
+import io.vertx.ext.web.client.WebClient;
+import io.vertx.ext.web.client.WebClientOptions;
+import io.vertx.ext.web.codec.BodyCodec;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.HexFormat;
+
+public class ProxyVerticle extends AbstractVerticle {
+
+ private WebClient webClient;
+
+ @Override
+ public void start(Promise
startPromise) {
+ webClient = WebClient.create(vertx, new WebClientOptions()
+ .setSsl(true)
+ .setTrustAll(true)
+ .setVerifyHost(false));
+
+ Router router = Router.router(vertx);
+ router.post("/api/proxy").handler(this::handlePost);
+ router.get("/api/proxy").handler(this::handleGet);
+
+ int port = 8080;
+ vertx.createHttpServer()
+ .requestHandler(router)
+ .listen(port).onComplete(http -> {
+ if (http.succeeded()) {
+ System.out.println("Proxy server started on port " + port);
+ startPromise.complete();
+ } else {
+ startPromise.fail(http.cause());
+ }
+ });
+ }
+
+ private void handlePost(RoutingContext ctx) {
+ String apiServer = System.getenv("IIKO_API_SERVER");
+ String apiLogin = System.getenv("IIKO_API_LOGIN");
+ String apiPass = System.getenv("IIKO_API_PASS");
+ String externalEndpoint = System.getenv("IIKO_API_ENDPOINT");
+ if (externalEndpoint == null || externalEndpoint.isBlank()) {
+ externalEndpoint = "/your-endpoint";
+ }
+
+ if (apiServer == null || apiLogin == null || apiPass == null) {
+ fail(ctx, 500, "Missing required environment variables: IIKO_API_SERVER, IIKO_API_LOGIN, IIKO_API_PASS");
+ return;
+ }
+
+ JsonObject body = ctx.body().asJsonObject();
+ if (body == null) {
+ fail(ctx, 400, "Request body must be JSON");
+ return;
+ }
+
+ String signature = sha1(apiPass);
+ String authUrl = "https://" + apiServer + ":443/resto/api/auth?login=" + apiLogin + "&pass=" + signature;
+ String finalExternalEndpoint = externalEndpoint;
+ webClient.getAbs(authUrl)
+ .as(BodyCodec.string())
+ .send()
+ .onSuccess(authResp -> {
+ if (authResp.statusCode() != 200) {
+ fail(ctx, authResp.statusCode(), "Authentication failed: " + authResp.statusMessage());
+ return;
+ }
+ String token = authResp.body();
+ String targetUrl = "https://" + apiServer + finalExternalEndpoint;
+ webClient.request(HttpMethod.POST, targetUrl)
+ .putHeader("Content-Type", "application/json")
+ .as(BodyCodec.jsonObject())
+ .sendJsonObject(body)
+ .onSuccess(apiResp -> {
+ webClient.getAbs("https://" + apiServer + ":443/resto/api/logout?key=" + token)
+ .send()
+ .onFailure(err -> System.err.println("Logout failed: " + err.getMessage()));
+ if (apiResp.statusCode() == 200) {
+ ctx.response().setStatusCode(200).end(apiResp.body().encode());
+ } else {
+ fail(ctx, apiResp.statusCode(), "External API error: " + apiResp.statusMessage());
+ }
+ })
+ .onFailure(err -> fail(ctx, 500, "Request to external API failed: " + err.getMessage()));
+ })
+ .onFailure(err -> fail(ctx, 500, "Auth request failed: " + err.getMessage()));
+ }
+
+ private void handleGet(RoutingContext ctx) {
+ String presetId = ctx.queryParam("presetId").stream().findFirst().orElse(null);
+ String dateFrom = ctx.queryParam("dateFrom").stream().findFirst().orElse(null);
+ String dateTo = ctx.queryParam("dateTo").stream().findFirst().orElse(null);
+ String server = ctx.queryParam("server").stream().findFirst().orElse(null);
+ String password = ctx.queryParam("password").stream().findFirst().orElse(null);
+ String login = ctx.queryParam("login").stream().findFirst().orElse(null);
+ String type = ctx.queryParam("type").stream().findFirst().orElse(null);
+ String rootType = ctx.queryParam("rootType").stream().findFirst().orElse(null);
+
+ if (server == null || login == null || password == null) {
+ fail(ctx, 400, "Missing required parameters: server, login, password");
+ return;
+ }
+
+ String signature = sha1(password);
+ String authUrl = "https://" + server + ":443/resto/api/auth?login=" + login + "&pass=" + signature;
+ webClient.getAbs(authUrl)
+ .as(BodyCodec.string())
+ .send()
+ .onSuccess(authResp -> {
+ if (authResp.statusCode() != 200) {
+ fail(ctx, authResp.statusCode(), "Authentication failed: " + authResp.statusMessage());
+ return;
+ }
+ String token = authResp.body();
+ String dataUrl;
+ if ("entity".equals(type)) {
+ dataUrl = "https://" + server + "/resto/api/v2/entities/list?key=" + token;
+ if (rootType != null && !rootType.isBlank()) {
+ dataUrl += "&rootType=" + rootType;
+ }
+ } else {
+ if (presetId == null || dateFrom == null || dateTo == null) {
+ fail(ctx, 400, "Missing presetId, dateFrom or dateTo for report request");
+ return;
+ }
+ dataUrl = "https://" + server + "/resto/api/v2/reports/olap/byPresetId/" + presetId +
+ "?key=" + token + "&dateFrom=" + dateFrom + "&dateTo=" + dateTo;
+ }
+ System.out.println("URL: " + dataUrl);
+ webClient.getAbs(dataUrl)
+ .as(BodyCodec.jsonObject())
+ .send()
+ .onSuccess(dataResp -> {
+ // logout (fire and forget)
+ webClient.getAbs("https://" + server + ":443/resto/api/logout?key=" + token)
+ .send()
+ .onFailure(err -> System.err.println("Logout failed: " + err.getMessage()));
+ if (dataResp.statusCode() == 200) {
+ JsonObject responseBody = dataResp.body();
+ if ("entity".equals(type)) {
+ ctx.response().setStatusCode(200).end(responseBody.encode());
+ } else {
+ Object data = responseBody.getValue("data");
+ if (data == null) {
+ ctx.response().setStatusCode(200).end(responseBody.encode());
+ } else {
+ // data может быть массивом, объектом или другим типом
+ ctx.response().setStatusCode(200).end(Json.encode(data));
+ }
+ }
+ } else {
+ fail(ctx, dataResp.statusCode(), "External API error: " + dataResp.statusMessage());
+ }
+ })
+ .onFailure(err -> fail(ctx, 500, "Data request failed: " + err.getMessage()));
+ })
+ .onFailure(err -> fail(ctx, 500, "Auth request failed: " + err.getMessage()));
+ }
+
+ private String sha1(String input) {
+ try {
+ MessageDigest md = MessageDigest.getInstance("SHA-1");
+ byte[] digest = md.digest(input.getBytes());
+ return HexFormat.of().formatHex(digest).toLowerCase();
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private void fail(RoutingContext ctx, int status, String message) {
+ System.err.println("Error: " + message);
+ ctx.response().setStatusCode(status).end(new JsonObject().put("error", message).encode());
+ }
+}
+
+// > GET /api/proxy?server=folk-amber-co.iiko.it&login=4444&password=4444&presetId=7ddc40c3-9d5f-408f-aa1e-652964b36c6c&dateFrom=2026-04-10&dateTo=2026-04-17 HTTP/1.1
+// > Host: localhost:8080
+// > access-token: ddb4ab653b9194ec1ea5448cee2a8a26282b0866c1d4a86e98e9b0f84bc91944
+// > User-Agent: v2raytun/ios
+// > X-App-Version: 2.4.3
+// > X-Device-Model: iPhone 11 Pro
+// > X-Device-OS: iOS
+// > X-HWID: HHS8JDJN-F2EB-HFBS-KMWX-234FA7B95JSC
+// > X-Ver-OS: 26.0
+// > Accept: */*
diff --git a/src/main/java/su/xserver/iikocon/RestaurantService.java b/src/main/java/su/xserver/iikocon/RestaurantService.java
index a084a49..72c975b 100644
--- a/src/main/java/su/xserver/iikocon/RestaurantService.java
+++ b/src/main/java/su/xserver/iikocon/RestaurantService.java
@@ -90,4 +90,45 @@ public class RestaurantService {
return array;
});
}
+
+ public Future findById(int id) {
+ return SqlTemplate.forQuery(pool,
+ "SELECT id, name, login, password, host, created, updated FROM restaurants WHERE id = #{id}")
+ .mapTo(row -> new JsonObject()
+ .put("id", row.getInteger("id"))
+ .put("name", row.getString("name"))
+ .put("login", row.getString("login"))
+ .put("password", row.getString("password"))
+ .put("host", row.getString("host"))
+ .put("created", row.getLocalDateTime("created") != null ? row.getLocalDateTime("created").toString() : null)
+ .put("updated", row.getLocalDateTime("updated") != null ? row.getLocalDateTime("updated").toString() : null))
+ .execute(Collections.singletonMap("id", id))
+ .map(rows -> rows.iterator().hasNext() ? rows.iterator().next() : null);
+ }
+
+ public Future updateRestaurant(int id, String name, String login, String password, String host) {
+ Map params = new HashMap<>();
+ params.put("id", id);
+ params.put("name", name);
+ params.put("login", login);
+ params.put("host", host);
+
+ String sql;
+ if (password != null && !password.isEmpty()) {
+ params.put("password", password);
+ sql = "UPDATE restaurants SET name = #{name}, login = #{login}, password = #{password}, host = #{host} WHERE id = #{id}";
+ } else {
+ sql = "UPDATE restaurants SET name = #{name}, login = #{login}, host = #{host} WHERE id = #{id}";
+ }
+
+ return SqlTemplate.forUpdate(pool, sql)
+ .execute(params)
+ .mapEmpty();
+ }
+
+ public Future deleteRestaurant(int id) {
+ return SqlTemplate.forUpdate(pool, "DELETE FROM restaurants WHERE id = #{id}")
+ .execute(Collections.singletonMap("id", id))
+ .mapEmpty();
+ }
}
diff --git a/src/main/java/su/xserver/iikocon/UserService.java b/src/main/java/su/xserver/iikocon/UserService.java
index a388234..fcb7535 100644
--- a/src/main/java/su/xserver/iikocon/UserService.java
+++ b/src/main/java/su/xserver/iikocon/UserService.java
@@ -89,6 +89,32 @@ public class UserService {
});
}
+ public Future updateUser(int id, String login, String password, String ip) {
+ Map params = new HashMap<>();
+ params.put("id", id);
+ params.put("login", login);
+ params.put("ip", ip);
+
+ String sql;
+ if (password != null && !password.isEmpty()) {
+ String hash = BCrypt.hashpw(password, BCrypt.gensalt());
+ params.put("password", hash);
+ sql = "UPDATE users SET login = #{login}, password = #{password}, ip = #{ip} WHERE id = #{id}";
+ } else {
+ sql = "UPDATE users SET login = #{login}, ip = #{ip} WHERE id = #{id}";
+ }
+
+ return SqlTemplate.forUpdate(pool, sql)
+ .execute(params)
+ .mapEmpty();
+ }
+
+ public Future deleteUser(int id) {
+ return SqlTemplate.forUpdate(pool, "DELETE FROM users WHERE id = #{id}")
+ .execute(Collections.singletonMap("id", id))
+ .mapEmpty();
+ }
+
public boolean checkPassword(String plain, String hash) {
try {
return BCrypt.checkpw(plain, hash);
diff --git a/src/main/java/su/xserver/iikocon/service/HealthCheckService.java b/src/main/java/su/xserver/iikocon/service/HealthCheckService.java
index ab4cdf3..99b0afc 100644
--- a/src/main/java/su/xserver/iikocon/service/HealthCheckService.java
+++ b/src/main/java/su/xserver/iikocon/service/HealthCheckService.java
@@ -31,6 +31,7 @@ public class HealthCheckService {
long time = System.currentTimeMillis() - start;
if ("PONG".equalsIgnoreCase(response.toString())) {
JsonObject data = new JsonObject()
+ .put("name", "redis")
.put("latency_ms", time);
future.complete(Status.OK(data));
} else {
@@ -47,6 +48,7 @@ public class HealthCheckService {
.onSuccess(rs -> {
long time = System.currentTimeMillis() - start;
JsonObject data = new JsonObject()
+ .put("name", "database")
.put("latency_ms", time);
future.complete(Status.OK(data));
})