up
This commit is contained in:
@@ -28,10 +28,8 @@ public class AuthHandler {
|
||||
boolean passwordOk = userService.checkPassword(password, user.getString("password"));
|
||||
|
||||
if (passwordOk) {
|
||||
// Надёжное получение флага активности
|
||||
Boolean active = user.getBoolean("active");
|
||||
if (active == null) {
|
||||
// Если поле отсутствует, пробуем получить как Integer (на случай TINYINT)
|
||||
Integer activeInt = user.getInteger("active");
|
||||
active = activeInt != null && activeInt == 1;
|
||||
}
|
||||
@@ -41,6 +39,16 @@ public class AuthHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
// Получаем реальный IP клиента (с учётом прокси, если настроен)
|
||||
String clientIp = ctx.get("realClientIp");
|
||||
if (clientIp == null) {
|
||||
clientIp = ctx.request().remoteAddress().host();
|
||||
}
|
||||
|
||||
// Обновляем IP в БД (асинхронно, не дожидаемся ответа)
|
||||
userService.updateUserIp(user.getInteger("id"), clientIp)
|
||||
.onFailure(err -> System.err.println("Failed to update IP for user " + user.getInteger("id") + ": " + err.getMessage()));
|
||||
|
||||
Session session = ctx.session();
|
||||
session.put("userId", user.getInteger("id"));
|
||||
session.put("login", user.getString("login"));
|
||||
|
||||
@@ -78,30 +78,54 @@ public class MainVerticle extends AbstractVerticle {
|
||||
startPromise.fail(err);
|
||||
});
|
||||
|
||||
Router router = initRouter();
|
||||
|
||||
startHttp(router, startPromise);
|
||||
createRouterAndStartHttp(startPromise);
|
||||
|
||||
})
|
||||
.onFailure(startPromise::fail);
|
||||
|
||||
}
|
||||
|
||||
private Router initRouter() {
|
||||
private void createRouterAndStartHttp(Promise<Void> startPromise) {
|
||||
settingsService.get("session_timeout_minutes")
|
||||
.compose(timeoutStr -> {
|
||||
long timeoutMinutes = 60; // default
|
||||
if (timeoutStr != null && !timeoutStr.isEmpty()) {
|
||||
try {
|
||||
timeoutMinutes = Long.parseLong(timeoutStr);
|
||||
} catch (NumberFormatException ignored) {}
|
||||
}
|
||||
long timeoutMs = timeoutMinutes * 60 * 1000;
|
||||
|
||||
// Настройка сессий (используем LocalSessionStore для простоты)
|
||||
SessionStore sessionStore = LocalSessionStore.create(vertx);
|
||||
SessionHandler sessionHandler = SessionHandler.create(sessionStore)
|
||||
.setSessionCookieName("admin.session")
|
||||
.setCookieHttpOnlyFlag(true)
|
||||
.setCookieSecureFlag(false)
|
||||
.setSessionTimeout(3600000);
|
||||
SessionStore sessionStore = LocalSessionStore.create(vertx);
|
||||
SessionHandler sessionHandler = SessionHandler.create(sessionStore)
|
||||
.setSessionCookieName("admin.session")
|
||||
.setCookieHttpOnlyFlag(true)
|
||||
.setCookieSecureFlag(false)
|
||||
.setSessionTimeout(timeoutMs);
|
||||
|
||||
Router router = initRouter(sessionHandler);
|
||||
startHttp(router, startPromise);
|
||||
return Future.succeededFuture();
|
||||
})
|
||||
.onFailure(err -> {
|
||||
log.error("Failed to get session timeout", err);
|
||||
startPromise.fail(err);
|
||||
});
|
||||
}
|
||||
|
||||
private Router initRouter(SessionHandler sessionHandler) {
|
||||
|
||||
// Роутер
|
||||
Router router = Router.router(vertx);
|
||||
router.route().handler(BodyHandler.create());
|
||||
router.route().handler(sessionHandler);
|
||||
|
||||
SecurityHandlers securityHandlers = new SecurityHandlers(settingsService);
|
||||
|
||||
// Обработчики безопасности (порядок важен)
|
||||
router.route().handler(securityHandlers.hostValidator());
|
||||
router.route().handler(securityHandlers.proxyHeadersHandler());
|
||||
router.route().handler(securityHandlers.cspHeader());
|
||||
|
||||
// CORS для разработки
|
||||
router.route().handler(ctx -> {
|
||||
ctx.response()
|
||||
@@ -149,7 +173,12 @@ public class MainVerticle extends AbstractVerticle {
|
||||
|
||||
router.post("/api/logout").handler(authHandler::handleLogout);
|
||||
|
||||
router.post("/api/register").handler(rc -> {
|
||||
router.post("/api/register").handler(rc -> settingsService.get("enable_registration").onComplete(regCheck -> {
|
||||
if (regCheck.succeeded() && "false".equals(regCheck.result())) {
|
||||
rc.response().setStatusCode(403).end(new JsonObject().put("error", "Registration is disabled").encode());
|
||||
return;
|
||||
}
|
||||
// существующий код регистрации
|
||||
JsonObject body = rc.body().asJsonObject();
|
||||
String login = body.getString("login");
|
||||
String email = body.getString("email");
|
||||
@@ -162,7 +191,7 @@ public class MainVerticle extends AbstractVerticle {
|
||||
userService.createUser(login, email, password, ip)
|
||||
.onSuccess(v -> rc.response().setStatusCode(201).end(new JsonObject().put("success", true).encode()))
|
||||
.onFailure(err -> rc.response().setStatusCode(500).end(err.getMessage()));
|
||||
});
|
||||
}));
|
||||
|
||||
router.route("/api/admin/*").handler(authHandler::requireAuth);
|
||||
|
||||
@@ -226,7 +255,7 @@ public class MainVerticle extends AbstractVerticle {
|
||||
|
||||
router.put("/api/admin/users/:id/activate").handler(rc -> {
|
||||
int id = Integer.parseInt(rc.pathParam("id"));
|
||||
boolean active = Boolean.parseBoolean(rc.queryParam("active").get(0));
|
||||
boolean active = Boolean.parseBoolean(rc.queryParam("active").getFirst());
|
||||
Integer currentUserId = rc.session().get("userId");
|
||||
|
||||
if (currentUserId != null && currentUserId == id) {
|
||||
@@ -280,11 +309,12 @@ public class MainVerticle extends AbstractVerticle {
|
||||
String login = body.getString("login");
|
||||
String password = body.getString("password");
|
||||
String host = body.getString("host");
|
||||
boolean https = body.getBoolean("https", false);
|
||||
if (name == null || login == null || password == null || host == null) {
|
||||
rc.response().setStatusCode(400).end("Missing fields");
|
||||
return;
|
||||
}
|
||||
restaurantService.createRestaurant(name, login, password, host)
|
||||
restaurantService.createRestaurant(name, login, password, host, https)
|
||||
.onSuccess(v -> rc.response().setStatusCode(201).end())
|
||||
.onFailure(err -> rc.response().setStatusCode(500).end(err.getMessage()));
|
||||
});
|
||||
@@ -296,11 +326,12 @@ public class MainVerticle extends AbstractVerticle {
|
||||
String login = body.getString("login");
|
||||
String password = body.getString("password");
|
||||
String host = body.getString("host");
|
||||
boolean https = body.getBoolean("https", false);
|
||||
if (name == null || login == null || host == null) {
|
||||
rc.response().setStatusCode(400).end("Missing required fields");
|
||||
return;
|
||||
}
|
||||
restaurantService.updateRestaurant(id, name, login, password, host)
|
||||
restaurantService.updateRestaurant(id, name, login, password, host, https)
|
||||
.onSuccess(v -> rc.response().end())
|
||||
.onFailure(err -> rc.response().setStatusCode(500).end(err.getMessage()));
|
||||
});
|
||||
@@ -314,9 +345,9 @@ public class MainVerticle extends AbstractVerticle {
|
||||
|
||||
// Получение всех настроек
|
||||
router.get("/api/settings").handler(rc -> {
|
||||
settingsService.getAll()
|
||||
settingsService.getPublicSettings()
|
||||
.onSuccess(settings -> rc.response().putHeader("Content-Type", "application/json").end(settings.encode()))
|
||||
.onFailure(err -> rc.response().setStatusCode(500).end(err.getMessage()));
|
||||
.onFailure(err -> rc.response().setStatusCode(500).end());
|
||||
});
|
||||
|
||||
// Получить метаданные всех настроек (для построения формы)
|
||||
|
||||
@@ -23,9 +23,10 @@ public class RestaurantService {
|
||||
CREATE TABLE IF NOT EXISTS restaurants (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(255) UNIQUE NOT NULL,
|
||||
login VARCHAR(255) NOT NULL,
|
||||
login VARCHAR(255) UNIQUE NOT NULL,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
host VARCHAR(255) NOT NULL,
|
||||
https BOOLEAN DEFAULT FALSE,
|
||||
created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
)
|
||||
@@ -40,16 +41,16 @@ public class RestaurantService {
|
||||
.map(rows -> rows.iterator().next().getLong("cnt"));
|
||||
}
|
||||
|
||||
public Future<Void> createRestaurant(String name, String login, String password, String host) {
|
||||
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("name", name);
|
||||
params.put("login", login);
|
||||
params.put("password", password);
|
||||
params.put("host", host);
|
||||
|
||||
public Future<Void> createRestaurant(String name, String login, String password, String host, boolean https) {
|
||||
Map<String, Object> params = Map.of(
|
||||
"name", name,
|
||||
"login", login,
|
||||
"password", password,
|
||||
"host", host,
|
||||
"https", https
|
||||
);
|
||||
return SqlTemplate.forUpdate(pool,
|
||||
"INSERT INTO restaurants (name, login, password, host) VALUES (#{name}, #{login}, #{password}, #{host})")
|
||||
"INSERT INTO restaurants (name, login, password, host, https) VALUES (#{name}, #{login}, #{password}, #{host}, #{https})")
|
||||
.execute(params)
|
||||
.mapEmpty();
|
||||
}
|
||||
@@ -72,7 +73,7 @@ public class RestaurantService {
|
||||
}
|
||||
|
||||
public Future<JsonArray> getAllRestaurants() {
|
||||
return pool.query("SELECT id, name, login, created, updated, host FROM restaurants ORDER BY id")
|
||||
return pool.query("SELECT id, name, login, created, updated, https, host FROM restaurants ORDER BY id")
|
||||
.execute()
|
||||
.map(rows -> {
|
||||
JsonArray array = new JsonArray();
|
||||
@@ -85,6 +86,7 @@ public class RestaurantService {
|
||||
row.getLocalDateTime("created").toString() : null)
|
||||
.put("updated", row.getLocalDateTime("updated") != null ?
|
||||
row.getLocalDateTime("updated").toString() : null)
|
||||
.put("https", row.getBoolean("https"))
|
||||
.put("host", row.getString("host")));
|
||||
}
|
||||
return array;
|
||||
@@ -93,12 +95,13 @@ public class RestaurantService {
|
||||
|
||||
public Future<JsonObject> findById(int id) {
|
||||
return SqlTemplate.forQuery(pool,
|
||||
"SELECT id, name, login, password, host, created, updated FROM restaurants WHERE id = #{id}")
|
||||
"SELECT id, name, login, password, https, 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("https", row.getBoolean("https"))
|
||||
.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))
|
||||
@@ -106,24 +109,21 @@ public class RestaurantService {
|
||||
.map(rows -> rows.iterator().hasNext() ? rows.iterator().next() : null);
|
||||
}
|
||||
|
||||
public Future<Void> updateRestaurant(int id, String name, String login, String password, String host) {
|
||||
public Future<Void> updateRestaurant(int id, String name, String login, String password, String host, boolean https) {
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("id", id);
|
||||
params.put("name", name);
|
||||
params.put("login", login);
|
||||
params.put("host", host);
|
||||
|
||||
params.put("https", https);
|
||||
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}";
|
||||
sql = "UPDATE restaurants SET name = #{name}, login = #{login}, password = #{password}, host = #{host}, https = #{https} WHERE id = #{id}";
|
||||
} else {
|
||||
sql = "UPDATE restaurants SET name = #{name}, login = #{login}, host = #{host} WHERE id = #{id}";
|
||||
sql = "UPDATE restaurants SET name = #{name}, login = #{login}, host = #{host}, https = #{https} WHERE id = #{id}";
|
||||
}
|
||||
|
||||
return SqlTemplate.forUpdate(pool, sql)
|
||||
.execute(params)
|
||||
.mapEmpty();
|
||||
return SqlTemplate.forUpdate(pool, sql).execute(params).mapEmpty();
|
||||
}
|
||||
|
||||
public Future<Void> deleteRestaurant(int id) {
|
||||
|
||||
114
src/main/java/su/xserver/iikocon/SecurityHandlers.java
Normal file
114
src/main/java/su/xserver/iikocon/SecurityHandlers.java
Normal file
@@ -0,0 +1,114 @@
|
||||
package su.xserver.iikocon;
|
||||
|
||||
import io.vertx.core.Handler;
|
||||
import io.vertx.core.http.HttpServerRequest;
|
||||
import io.vertx.core.net.SocketAddress;
|
||||
import io.vertx.ext.web.RoutingContext;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public class SecurityHandlers {
|
||||
private final SettingsService settings;
|
||||
|
||||
public SecurityHandlers(SettingsService settings) {
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
public Handler<RoutingContext> hostValidator() {
|
||||
return ctx -> settings.get("allowed_hosts").onComplete(ar -> {
|
||||
if (ar.succeeded() && ar.result() != null && !ar.result().isEmpty()) {
|
||||
String allowedHosts = ar.result();
|
||||
String requestHost = ctx.request().getHeader("Host");
|
||||
if (requestHost == null) {
|
||||
ctx.response().setStatusCode(400).end("Bad Request: Missing Host header");
|
||||
return;
|
||||
}
|
||||
String hostWithoutPort = requestHost.split(":")[0];
|
||||
Set<String> allowedSet = new HashSet<>(Arrays.asList(allowedHosts.split(",")));
|
||||
if (!allowedSet.contains(hostWithoutPort) && !allowedSet.contains(requestHost)) {
|
||||
ctx.response().setStatusCode(403).end("Forbidden: Invalid Host header");
|
||||
return;
|
||||
}
|
||||
}
|
||||
ctx.next();
|
||||
});
|
||||
}
|
||||
|
||||
public Handler<RoutingContext> cspHeader() {
|
||||
return ctx -> settings.get("enable_csp").onComplete(ar -> {
|
||||
if (ar.succeeded() && "true".equals(ar.result())) {
|
||||
ctx.response().putHeader("Content-Security-Policy",
|
||||
"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'");
|
||||
}
|
||||
ctx.next();
|
||||
});
|
||||
}
|
||||
|
||||
public Handler<RoutingContext> proxyHeadersHandler() {
|
||||
return ctx -> settings.get("use_proxy_headers").onComplete(useProxy -> {
|
||||
if (!useProxy.succeeded() || !"true".equals(useProxy.result())) {
|
||||
ctx.next();
|
||||
return;
|
||||
}
|
||||
settings.get("trusted_proxies").onComplete(trusted -> {
|
||||
if (!trusted.succeeded() || trusted.result() == null) {
|
||||
ctx.next();
|
||||
return;
|
||||
}
|
||||
String trustedProxies = trusted.result();
|
||||
SocketAddress remoteAddr = ctx.request().remoteAddress();
|
||||
if (remoteAddr == null) {
|
||||
ctx.next();
|
||||
return;
|
||||
}
|
||||
String clientIp = remoteAddr.host();
|
||||
if (isIpTrusted(clientIp, trustedProxies)) {
|
||||
String realIp = getRealIpFromHeaders(ctx.request());
|
||||
if (realIp != null) {
|
||||
ctx.put("realClientIp", realIp);
|
||||
ctx.put("originalClientIp", clientIp);
|
||||
}
|
||||
}
|
||||
ctx.next();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private boolean isIpTrusted(String ip, String trustedList) {
|
||||
String[] ips = trustedList.split(",");
|
||||
for (String trusted : ips) {
|
||||
trusted = trusted.trim();
|
||||
if (trusted.contains("/")) {
|
||||
if (ipMatchesCidr(ip, trusted)) return true;
|
||||
} else if (ip.equals(trusted)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean ipMatchesCidr(String ip, String cidr) {
|
||||
try {
|
||||
String[] parts = cidr.split("/");
|
||||
String network = parts[0];
|
||||
int prefix = Integer.parseInt(parts[1]);
|
||||
return ip.startsWith(network.substring(0, network.lastIndexOf('.')));
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private String getRealIpFromHeaders(HttpServerRequest request) {
|
||||
String xff = request.getHeader("X-Forwarded-For");
|
||||
if (xff != null && !xff.isEmpty()) {
|
||||
return xff.split(",")[0].trim();
|
||||
}
|
||||
String xri = request.getHeader("X-Real-IP");
|
||||
if (xri != null && !xri.isEmpty()) {
|
||||
return xri;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import io.vertx.core.json.JsonObject;
|
||||
import io.vertx.sqlclient.Pool;
|
||||
import io.vertx.sqlclient.templates.SqlTemplate;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class SettingsService {
|
||||
@@ -29,24 +30,24 @@ public class SettingsService {
|
||||
.put("type", "textarea")
|
||||
.put("rows", 2)
|
||||
);
|
||||
meta.add(new JsonObject()
|
||||
.put("key", "theme")
|
||||
.put("label", "Theme")
|
||||
.put("description", "Default color scheme")
|
||||
.put("type", "select")
|
||||
.put("options", new JsonArray()
|
||||
.add(new JsonObject().put("value", "light").put("label", "Light"))
|
||||
.add(new JsonObject().put("value", "dark").put("label", "Dark"))
|
||||
.add(new JsonObject().put("value", "auto").put("label", "Auto (system preference)"))
|
||||
)
|
||||
);
|
||||
meta.add(new JsonObject()
|
||||
.put("key", "items_per_page")
|
||||
.put("label", "Items Per Page")
|
||||
.put("description", "Number of items shown in tables")
|
||||
.put("type", "number")
|
||||
.put("required", true)
|
||||
);
|
||||
// meta.add(new JsonObject()
|
||||
// .put("key", "theme")
|
||||
// .put("label", "Theme")
|
||||
// .put("description", "Default color scheme")
|
||||
// .put("type", "select")
|
||||
// .put("options", new JsonArray()
|
||||
// .add(new JsonObject().put("value", "light").put("label", "Light"))
|
||||
// .add(new JsonObject().put("value", "dark").put("label", "Dark"))
|
||||
// .add(new JsonObject().put("value", "auto").put("label", "Auto (system preference)"))
|
||||
// )
|
||||
// );
|
||||
// meta.add(new JsonObject()
|
||||
// .put("key", "items_per_page")
|
||||
// .put("label", "Items Per Page")
|
||||
// .put("description", "Number of items shown in tables")
|
||||
// .put("type", "number")
|
||||
// .put("required", true)
|
||||
// );
|
||||
meta.add(new JsonObject()
|
||||
.put("key", "enable_registration")
|
||||
.put("label", "Allow Public Registration")
|
||||
@@ -59,17 +60,17 @@ public class SettingsService {
|
||||
.put("description", "When enabled, only admins can access the site")
|
||||
.put("type", "boolean")
|
||||
);
|
||||
meta.add(new JsonObject()
|
||||
.put("key", "default_language")
|
||||
.put("label", "Default Language")
|
||||
.put("description", "Interface language")
|
||||
.put("type", "select")
|
||||
.put("options", new JsonArray()
|
||||
.add(new JsonObject().put("value", "en").put("label", "English"))
|
||||
.add(new JsonObject().put("value", "ru").put("label", "Русский"))
|
||||
.add(new JsonObject().put("value", "es").put("label", "Español"))
|
||||
)
|
||||
);
|
||||
// meta.add(new JsonObject()
|
||||
// .put("key", "default_language")
|
||||
// .put("label", "Default Language")
|
||||
// .put("description", "Interface language")
|
||||
// .put("type", "select")
|
||||
// .put("options", new JsonArray()
|
||||
// .add(new JsonObject().put("value", "en").put("label", "English"))
|
||||
// .add(new JsonObject().put("value", "ru").put("label", "Русский"))
|
||||
// .add(new JsonObject().put("value", "es").put("label", "Español"))
|
||||
// )
|
||||
// );
|
||||
meta.add(new JsonObject()
|
||||
.put("key", "session_timeout_minutes")
|
||||
.put("label", "Session Timeout (minutes)")
|
||||
@@ -77,19 +78,45 @@ public class SettingsService {
|
||||
.put("type", "number")
|
||||
.put("required", true)
|
||||
);
|
||||
// meta.add(new JsonObject()
|
||||
// .put("key", "logo_url")
|
||||
// .put("label", "Logo URL")
|
||||
// .put("description", "Path or URL to custom logo image")
|
||||
// .put("type", "text")
|
||||
// );
|
||||
|
||||
// Безопасность и прокси
|
||||
meta.add(new JsonObject()
|
||||
.put("key", "logo_url")
|
||||
.put("label", "Logo URL")
|
||||
.put("description", "Path or URL to custom logo image")
|
||||
.put("key", "use_proxy_headers")
|
||||
.put("label", "Use Proxy Headers")
|
||||
.put("description", "Respect X-Forwarded-* headers from trusted proxies")
|
||||
.put("type", "boolean")
|
||||
);
|
||||
meta.add(new JsonObject()
|
||||
.put("key", "trusted_proxies")
|
||||
.put("label", "Trusted Proxies")
|
||||
.put("description", "Comma-separated IP addresses of trusted proxies (e.g., 127.0.0.1,10.0.0.0/8)")
|
||||
.put("type", "text")
|
||||
);
|
||||
meta.add(new JsonObject()
|
||||
.put("key", "enable_csp")
|
||||
.put("label", "Enable CSP")
|
||||
.put("description", "Add Content-Security-Policy header")
|
||||
.put("type", "boolean")
|
||||
);
|
||||
meta.add(new JsonObject()
|
||||
.put("key", "allowed_hosts")
|
||||
.put("label", "Allowed Hosts")
|
||||
.put("description", "Comma-separated list of allowed Host headers (empty = allow all)")
|
||||
.put("type", "text")
|
||||
);
|
||||
|
||||
return Future.succeededFuture(meta);
|
||||
}
|
||||
|
||||
public Future<JsonObject> getAllWithDefaults() {
|
||||
return getAll().compose(values -> {
|
||||
JsonObject result = new JsonObject();
|
||||
// Получаем метаданные, чтобы знать ключи
|
||||
return getMetadata().map(meta -> {
|
||||
for (Object item : meta) {
|
||||
JsonObject m = (JsonObject) item;
|
||||
@@ -107,13 +134,17 @@ public class SettingsService {
|
||||
return switch (key) {
|
||||
case "site_name" -> "Admin Panel";
|
||||
case "site_description" -> "";
|
||||
case "theme" -> "light";
|
||||
case "items_per_page" -> "20";
|
||||
// case "theme" -> "light";
|
||||
// case "items_per_page" -> "20";
|
||||
case "enable_registration" -> "true";
|
||||
case "maintenance_mode" -> "false";
|
||||
case "default_language" -> "en";
|
||||
// case "default_language" -> "en";
|
||||
case "session_timeout_minutes" -> "60";
|
||||
case "logo_url" -> "";
|
||||
// case "logo_url" -> "";
|
||||
case "use_proxy_headers" -> "true";
|
||||
case "trusted_proxies" -> "127.0.0.1";
|
||||
case "enable_csp" -> "true";
|
||||
case "allowed_hosts" -> "";
|
||||
default -> "";
|
||||
};
|
||||
}
|
||||
@@ -170,4 +201,21 @@ public class SettingsService {
|
||||
return json;
|
||||
});
|
||||
}
|
||||
|
||||
// Публичные настройки (для фронтенда)
|
||||
public Future<JsonObject> getPublicSettings() {
|
||||
return getAll().map(all -> {
|
||||
JsonObject publicOnly = new JsonObject();
|
||||
// Только безопасные для отображения ключи
|
||||
List<String> publicKeys = List.of(
|
||||
"site_name", "site_description", "enable_registration"
|
||||
);
|
||||
for (String key : publicKeys) {
|
||||
String val = all.getString(key);
|
||||
if (val != null) publicOnly.put(key, val);
|
||||
}
|
||||
return publicOnly;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -51,8 +51,11 @@ public class SetupHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
String ip = ctx.request().remoteAddress().host();
|
||||
userService.createUser(login, email, password, ip, true).onComplete(cr -> {
|
||||
String clientIp = ctx.get("realClientIp");
|
||||
if (clientIp == null) {
|
||||
clientIp = ctx.request().remoteAddress().host();
|
||||
}
|
||||
userService.createUser(login, email, password, clientIp, true).onComplete(cr -> {
|
||||
if (cr.succeeded()) {
|
||||
ctx.response().setStatusCode(201)
|
||||
.end(new JsonObject().put("success", true).encode());
|
||||
|
||||
@@ -139,6 +139,12 @@ public class UserService {
|
||||
return SqlTemplate.forUpdate(pool, sql).execute(params).mapEmpty();
|
||||
}
|
||||
|
||||
public Future<Void> updateUserIp(int userId, String ip) {
|
||||
return pool.preparedQuery("UPDATE users SET ip = ? WHERE id = ?")
|
||||
.execute(Tuple.of(ip, userId))
|
||||
.mapEmpty();
|
||||
}
|
||||
|
||||
public Future<Void> deleteUser(int id) {
|
||||
return SqlTemplate.forUpdate(pool, "DELETE FROM users WHERE id = #{id}")
|
||||
.execute(Collections.singletonMap("id", id))
|
||||
|
||||
Reference in New Issue
Block a user