From af757ff2241c1cbdc1cebdf0b1b247742b940577 Mon Sep 17 00:00:00 2001 From: Danil-Bodry Date: Sat, 18 Apr 2026 11:33:21 +0300 Subject: [PATCH] up --- frontend/src/components/Layout/AppLayout.vue | 43 ++-- frontend/src/router/index.ts | 28 ++- frontend/src/views/Dashboard.vue | 38 +++- frontend/src/views/Restaurants.vue | 140 +++++++++++++ frontend/src/views/Users.vue | 124 +++++++++++ .../su/xserver/iikocon/DateRangeSetup.java | 50 +++++ .../java/su/xserver/iikocon/MainVerticle.java | 94 +++++++++ .../su/xserver/iikocon/ProxyVerticle.java | 194 ++++++++++++++++++ .../su/xserver/iikocon/RestaurantService.java | 41 ++++ .../java/su/xserver/iikocon/UserService.java | 26 +++ .../iikocon/service/HealthCheckService.java | 2 + 11 files changed, 753 insertions(+), 27 deletions(-) create mode 100644 frontend/src/views/Restaurants.vue create mode 100644 frontend/src/views/Users.vue create mode 100644 src/main/java/su/xserver/iikocon/DateRangeSetup.java create mode 100644 src/main/java/su/xserver/iikocon/ProxyVerticle.java diff --git a/frontend/src/components/Layout/AppLayout.vue b/frontend/src/components/Layout/AppLayout.vue index e0a7ee5..963a37f 100644 --- a/frontend/src/components/Layout/AppLayout.vue +++ b/frontend/src/components/Layout/AppLayout.vue @@ -39,6 +39,17 @@ Users + + + + + Restaurants + + diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 04278eb..279a411 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -2,19 +2,29 @@ import { createRouter, createWebHistory } from 'vue-router' import Login from '../views/auth/Login.vue' import Setup from '../views/auth/Setup.vue' import Dashboard from '../views/Dashboard.vue' +import Users from '../views/Users.vue' +import Restaurants from '../views/Restaurants.vue' import NotFound from '../views/NotFound.vue' const routes = [ { path: '/login', component: Login, meta: { title: 'Login' } }, { path: '/setup', component: Setup, meta: { title: 'Setup' } }, + { + path: '/', + redirect: '/dashboard' + }, { path: '/dashboard', component: Dashboard, meta: { requiresAuth: true, title: 'Dashboard' } }, - { - path: '/', - redirect: '/dashboard' + { path: '/users', + component: Users, + meta: { requiresAuth: true, title: 'Users' } + }, + { path: '/restaurants', + component: Restaurants, + meta: { requiresAuth: true, title: 'Restaurants' } }, { path: '/:pathMatch(.*)*', @@ -46,6 +56,18 @@ router.beforeEach(async (to, from, next) => { console.error('Failed to check status', e) } + if (to.path === '/login') { + try { + const meRes = await fetch('/api/admin/me'); + if (meRes.ok) { + next('/dashboard'); + return; + } + } catch (e) { + // игнорируем ошибку, продолжаем + } + } + // Check authentication const requiresAuth = to.matched.some(record => record.meta.requiresAuth) diff --git a/frontend/src/views/Dashboard.vue b/frontend/src/views/Dashboard.vue index f227729..38efb93 100644 --- a/frontend/src/views/Dashboard.vue +++ b/frontend/src/views/Dashboard.vue @@ -103,7 +103,7 @@
-
+
{{ service.name }}
{{ 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 @@ + + + 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)); })