This commit is contained in:
2026-05-04 15:08:03 +03:00
parent a61c527ef9
commit 1ca4c90b88
7 changed files with 141 additions and 20 deletions

View File

@@ -37,6 +37,7 @@ dependencies {
implementation(platform("io.vertx:vertx-stack-depchain:$vertxVersion")) implementation(platform("io.vertx:vertx-stack-depchain:$vertxVersion"))
implementation("io.vertx:vertx-launcher-application") implementation("io.vertx:vertx-launcher-application")
implementation("io.vertx:vertx-web-client") implementation("io.vertx:vertx-web-client")
implementation("io.vertx:vertx-web-proxy")
implementation("io.vertx:vertx-config") implementation("io.vertx:vertx-config")
implementation("io.vertx:vertx-sql-client-templates") implementation("io.vertx:vertx-sql-client-templates")
implementation("io.vertx:vertx-health-check") implementation("io.vertx:vertx-health-check")

View File

@@ -34,11 +34,13 @@ services:
environment: environment:
PMA_HOST: iiko-db PMA_HOST: iiko-db
PMA_PORT: 3306 PMA_PORT: 3306
PMA_USER: root
PMA_PASSWORD: DVjXT_kew508
UPLOAD_LIMIT: 10M UPLOAD_LIMIT: 10M
PMA_ABSOLUTE_URI: https://iiko-app.dev.xserver.su/phpmyadmin/ PMA_ABSOLUTE_URI: https://iiko-app.dev.xserver.su/phpmyadmin/
TZ: Europe/Moscow TZ: Europe/Moscow
ports: ports:
- "7102:80" # - "7102:80"
iiko-redis: iiko-redis:
image: redis:latest image: redis:latest
@@ -75,5 +77,9 @@ services:
REDIS__HOST: iiko-redis REDIS__HOST: iiko-redis
REDIS__PORT: 6379 REDIS__PORT: 6379
SERVER__PORT: 7104 SERVER__PORT: 7104
PMA__ENABLED: true
PMA__BASE_PATH: /phpmyadmin
PMA__UPSTREAM: http://iiko-pma:80/
volumes: volumes:
- $PWD/app/logs:/app/logs - $PWD/app/logs:/app/logs

View File

@@ -186,7 +186,7 @@
</svg> </svg>
</router-link> </router-link>
<a <a
href="/phpmyadmin" href="/phpmyadmin/"
v-if="userStore.role === 'admin'" v-if="userStore.role === 'admin'"
target="_self" target="_self"
class="p-2 text-gray-400 hover:text-gray-600 rounded-lg hover:bg-gray-100 transition-colors" class="p-2 text-gray-400 hover:text-gray-600 rounded-lg hover:bg-gray-100 transition-colors"

View File

@@ -6,9 +6,14 @@ import io.vertx.config.ConfigStoreOptions;
import io.vertx.core.AbstractVerticle; import io.vertx.core.AbstractVerticle;
import io.vertx.core.Future; import io.vertx.core.Future;
import io.vertx.core.Promise; import io.vertx.core.Promise;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.http.HttpServer; import io.vertx.core.http.HttpServer;
import io.vertx.core.json.JsonObject; import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.Router; import io.vertx.ext.web.Router;
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.client.HttpRequest;
import io.vertx.ext.web.client.HttpResponse;
import io.vertx.ext.web.client.WebClient;
import io.vertx.ext.web.handler.BodyHandler; import io.vertx.ext.web.handler.BodyHandler;
import io.vertx.ext.web.handler.SessionHandler; import io.vertx.ext.web.handler.SessionHandler;
import io.vertx.ext.web.handler.StaticHandler; import io.vertx.ext.web.handler.StaticHandler;
@@ -23,6 +28,7 @@ import su.xserver.iikocon.iiko.IikoHandler;
import su.xserver.iikocon.iiko.IikoOlapClient; import su.xserver.iikocon.iiko.IikoOlapClient;
import su.xserver.iikocon.service.*; import su.xserver.iikocon.service.*;
import java.net.URI;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -42,7 +48,10 @@ public class MainVerticle extends AbstractVerticle {
private SettingsService settingsService; private SettingsService settingsService;
@Override @Override
public void start(Promise<Void> startPromise) { public void start(Promise<Void> startPromise) throws ClassNotFoundException {
Class.forName("com.mysql.cj.jdbc.Driver");
Class.forName("org.postgresql.Driver");
ConfigStoreOptions classpathStore = new ConfigStoreOptions() ConfigStoreOptions classpathStore = new ConfigStoreOptions()
.setType("file") .setType("file")
@@ -119,12 +128,107 @@ public class MainVerticle extends AbstractVerticle {
}); });
} }
private void setupPhpmyadminProxy(Router router) {
if (config.pma == null || !config.pma.enabled) return;
String upstream = config.pma.upstream;
String basePath = config.pma.basePath;
final URI upstreamUri = URI.create(upstream);
final String host = upstreamUri.getHost();
int portTmp = upstreamUri.getPort();
if (portTmp == -1) {
portTmp = "https".equals(upstreamUri.getScheme()) ? 443 : 80;
}
final int port = portTmp;
final WebClient webClient = WebClient.create(vertx);
router.route(basePath + "/*").handler(ctx -> {
if (ctx.session() != null && "admin".equals(ctx.session().get("role"))) {
ctx.next();
} else {
ctx.response().putHeader("Location", "/").setStatusCode(302).end();
}
});
router.route(basePath + "/*").handler(ctx -> {
String targetPathBase = ctx.request().path().substring(basePath.length());
if (targetPathBase.isEmpty()) targetPathBase = "/";
String targetPath = targetPathBase;
String query = ctx.request().query();
if (query != null && !query.isEmpty()) {
targetPath += "?" + query;
}
final String targetPathFinal = targetPath;
final HttpRequest<Buffer> proxyReq = webClient.request(
ctx.request().method(), port, host, targetPathFinal
);
ctx.request().headers().forEach(header -> {
if (!"host".equalsIgnoreCase(header.getKey())) {
proxyReq.putHeader(header.getKey(), header.getValue());
}
});
proxyReq.putHeader("Host", host + ":" + port);
ctx.request().bodyHandler(body -> {
if (body != null && body.length() > 0) {
proxyReq.sendBuffer(body)
.onSuccess(resp -> sendResponse(ctx, resp))
.onFailure(err -> sendError(ctx, err));
} else {
proxyReq.send()
.onSuccess(resp -> sendResponse(ctx, resp))
.onFailure(err -> sendError(ctx, err));
}
});
});
}
private void sendResponse(RoutingContext ctx, HttpResponse<Buffer> resp) {
ctx.response().setStatusCode(resp.statusCode());
resp.headers().forEach(h -> ctx.response().putHeader(h.getKey(), h.getValue()));
ctx.response().end(resp.body());
}
private void sendError(RoutingContext ctx, Throwable err) {
log.error("Proxy error: {}", err.getMessage());
ctx.response().setStatusCode(502).end("Bad Gateway: " + err.getMessage());
}
private Router initRouter(SessionHandler sessionHandler) { private Router initRouter(SessionHandler sessionHandler) {
Router router = Router.router(vertx); Router router = Router.router(vertx);
router.route().handler(BodyHandler.create());
router.route().handler(ctx -> {
long start = System.currentTimeMillis();
String method = ctx.request().method().name();
String path = ctx.request().path();
final String remoteIp = ctx.get("realClientIp") != null ?
ctx.get("realClientIp") :
ctx.request().remoteAddress().host();
ctx.addBodyEndHandler(v -> {
long duration = System.currentTimeMillis() - start;
log.info("{} {} - {} ms - {} - {}",
method, path, duration, ctx.response().getStatusCode(), remoteIp);
});
ctx.next();
});
router.route().handler(ctx -> {
String path = ctx.request().path();
if (path != null && path.startsWith(config.pma.basePath + "/")) {
ctx.next(); // пропускаем BodyHandler для прокси
} else {
BodyHandler.create().handle(ctx);
}
});
router.route().handler(sessionHandler); router.route().handler(sessionHandler);
setupPhpmyadminProxy(router);
SecurityHandler securityHandlers = new SecurityHandler(settingsService); SecurityHandler securityHandlers = new SecurityHandler(settingsService);
// Обработчики безопасности // Обработчики безопасности
@@ -147,21 +251,6 @@ public class MainVerticle extends AbstractVerticle {
} }
}); });
router.route().handler(ctx -> {
long start = System.currentTimeMillis();
String method = ctx.request().method().name();
String path = ctx.request().path();
final String remoteIp = ctx.get("realClientIp") != null ?
ctx.get("realClientIp") :
ctx.request().remoteAddress().host();
ctx.addBodyEndHandler(v -> {
long duration = System.currentTimeMillis() - start;
log.info("{} {} - {} ms - {} - {}",
method, path, duration, ctx.response().getStatusCode(), remoteIp);
});
ctx.next();
});
// ------ Раздаём Vue статику ------ // ------ Раздаём Vue статику ------
router.route("/assets/*").handler(StaticHandler.create("webroot/assets")); router.route("/assets/*").handler(StaticHandler.create("webroot/assets"));

View File

@@ -8,6 +8,7 @@ public class AppConfig {
public ServerConfig server; public ServerConfig server;
public DatabaseConfig database; public DatabaseConfig database;
public RedisConfig redis; public RedisConfig redis;
public PhpMyAdminConfig pma;
public static AppConfig from(JsonObject json) { public static AppConfig from(JsonObject json) {
JsonObject resolved = json.copy(); JsonObject resolved = json.copy();
@@ -94,7 +95,8 @@ public class AppConfig {
return new JsonObject() return new JsonObject()
.put("server", server.json().getJsonObject("server")) .put("server", server.json().getJsonObject("server"))
.put("database", database.json().getJsonObject("database")) .put("database", database.json().getJsonObject("database"))
.put("redis", redis.json().getJsonObject("redis")); .put("redis", redis.json().getJsonObject("redis"))
.put("pma", pma.json().getJsonObject("pma"));
} }
@Override @Override

View File

@@ -0,0 +1,18 @@
package su.xserver.iikocon.config;
import io.vertx.core.json.JsonObject;
public class PhpMyAdminConfig {
public boolean enabled;
public String upstream;
public String basePath;
public JsonObject json() {
return new JsonObject()
.put("pma", new JsonObject()
.put("enabled", enabled)
.put("upstream", upstream)
.put("basePath", basePath)
);
}
}

View File

@@ -18,5 +18,10 @@
"password": null, "password": null,
"maxPoolSize": 6, "maxPoolSize": 6,
"maxWaitingHandlers": 6 "maxWaitingHandlers": 6
},
"pma": {
"enabled": false,
"basePath": "/pma",
"upstream": "http://localhost:80/"
} }
} }