477 lines
19 KiB
Java
477 lines
19 KiB
Java
package su.xserver.iikocon;
|
||
|
||
import io.vertx.config.ConfigRetriever;
|
||
import io.vertx.config.ConfigRetrieverOptions;
|
||
import io.vertx.config.ConfigStoreOptions;
|
||
import io.vertx.core.AbstractVerticle;
|
||
import io.vertx.core.Future;
|
||
import io.vertx.core.Promise;
|
||
import io.vertx.core.http.HttpServer;
|
||
import io.vertx.core.json.JsonObject;
|
||
import io.vertx.ext.web.Router;
|
||
import io.vertx.ext.web.handler.BodyHandler;
|
||
import io.vertx.ext.web.handler.SessionHandler;
|
||
import io.vertx.ext.web.handler.StaticHandler;
|
||
import io.vertx.ext.web.sstore.SessionStore;
|
||
|
||
import io.vertx.ext.web.sstore.redis.RedisSessionStore;
|
||
import org.slf4j.Logger;
|
||
import org.slf4j.LoggerFactory;
|
||
import su.xserver.iikocon.config.AppConfig;
|
||
import su.xserver.iikocon.handler.AdminHandler;
|
||
import su.xserver.iikocon.handler.AuthHandler;
|
||
import su.xserver.iikocon.handler.SecurityHandler;
|
||
import su.xserver.iikocon.handler.SetupHandler;
|
||
import su.xserver.iikocon.iiko.IikoOlapClient;
|
||
import su.xserver.iikocon.service.*;
|
||
|
||
import java.util.ArrayList;
|
||
import java.util.List;
|
||
|
||
public class MainVerticle extends AbstractVerticle {
|
||
|
||
private final Logger log = LoggerFactory.getLogger("[MainVerticle]");
|
||
|
||
private DataBaseService db;
|
||
private RedisService redis;
|
||
private HttpServer httpServer;
|
||
private AppConfig config;
|
||
private SessionStore sessionStore;
|
||
|
||
private UserService userService;
|
||
private RestaurantService restaurantService;
|
||
private SettingsService settingsService;
|
||
|
||
@Override
|
||
public void start(Promise<Void> startPromise) {
|
||
|
||
ConfigStoreOptions classpathStore = new ConfigStoreOptions()
|
||
.setType("file")
|
||
.setFormat("json")
|
||
.setConfig(new JsonObject().put("path", "config.json").put("hierarchical", true))
|
||
.setOptional(false);
|
||
|
||
ConfigRetrieverOptions options = new ConfigRetrieverOptions()
|
||
.addStore(classpathStore);
|
||
|
||
ConfigRetriever retriever = ConfigRetriever.create(vertx, options);
|
||
|
||
retriever.getConfig()
|
||
.onSuccess(cfg -> {
|
||
config = AppConfig.from(cfg);
|
||
|
||
db = new DataBaseService(vertx, config.database);
|
||
redis = new RedisService(vertx, config.redis);
|
||
|
||
// Инициализация сервисов
|
||
userService = new UserService(db.getPool());
|
||
restaurantService = new RestaurantService(db.getPool());
|
||
settingsService = new SettingsService(db.getPool());
|
||
|
||
// Инициализация БД (создание таблицы users)
|
||
userService.initDatabase().onFailure(err -> {
|
||
log.error("Failed to initialize database", err);
|
||
startPromise.fail(err);
|
||
});
|
||
restaurantService.initDatabase().onFailure(err -> {
|
||
log.error("Failed to initialize database", err);
|
||
startPromise.fail(err);
|
||
});
|
||
settingsService.initDatabase().onFailure(err -> {
|
||
log.error("Failed to initialize database", err);
|
||
startPromise.fail(err);
|
||
});
|
||
|
||
createRouterAndStartHttp(startPromise);
|
||
|
||
})
|
||
.onFailure(startPromise::fail);
|
||
|
||
}
|
||
|
||
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;
|
||
|
||
sessionStore = RedisSessionStore.create(vertx, redis.getRedis());
|
||
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);
|
||
|
||
SecurityHandler securityHandlers = new SecurityHandler(settingsService);
|
||
|
||
// Обработчики безопасности (порядок важен)
|
||
router.route().handler(securityHandlers.hostValidator());
|
||
router.route().handler(securityHandlers.proxyHeadersHandler());
|
||
router.route().handler(securityHandlers.cspHeader());
|
||
|
||
// CORS для разработки
|
||
router.route().handler(ctx -> {
|
||
ctx.response()
|
||
.putHeader("Access-Control-Allow-Origin", "*")
|
||
.putHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||
.putHeader("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
||
.putHeader("Access-Control-Allow-Credentials", "true");
|
||
|
||
if (ctx.request().method().name().equals("OPTIONS")) {
|
||
ctx.response().end();
|
||
} else {
|
||
ctx.next();
|
||
}
|
||
});
|
||
|
||
// ------ Раздаём Vue статику ------
|
||
router.route("/assets/*").handler(StaticHandler.create("webroot/assets"));
|
||
|
||
router.route("/favicon.ico").handler(ctx -> ctx.response().sendFile("webroot/favicon.ico"));
|
||
|
||
// ------ SPA fallback: отдаём index.html на все не-API запросы ------
|
||
router.route().handler(ctx -> {
|
||
if (ctx.request().path().startsWith("/api")) {
|
||
ctx.next();
|
||
} else {
|
||
ctx.response()
|
||
.putHeader("Content-Type", "text/html")
|
||
.sendFile("webroot/index.html");
|
||
}
|
||
});
|
||
|
||
// Health Checks
|
||
HealthCheckService healthCheckService = new HealthCheckService(vertx, redis, db);
|
||
healthCheckService.registerHealthCheck(router);
|
||
|
||
// API маршруты
|
||
AuthHandler authHandler = new AuthHandler(userService);
|
||
SetupHandler setupHandler = new SetupHandler(userService);
|
||
|
||
router.get("/api/status").handler(setupHandler::checkStatus);
|
||
|
||
router.post("/api/setup").handler(setupHandler::handleSetup);
|
||
|
||
router.post("/api/login").handler(authHandler::handleLogin);
|
||
|
||
router.post("/api/logout").handler(authHandler::handleLogout);
|
||
|
||
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");
|
||
String password = body.getString("password");
|
||
String ip = rc.request().remoteAddress().host();
|
||
if (login == null || email == null || password == null) {
|
||
rc.response().setStatusCode(400).end("Missing fields");
|
||
return;
|
||
}
|
||
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()));
|
||
}));
|
||
|
||
// В initRouter после настройки authHandler, до объявления /api/admin/*:
|
||
router.route("/api/profile").handler(authHandler::requireAuth);
|
||
router.get("/api/profile").handler(rc -> {
|
||
Integer userId = rc.session().get("userId");
|
||
userService.getProfile(userId)
|
||
.onSuccess(profile -> rc.response().putHeader("Content-Type", "application/json").end(profile.encode()))
|
||
.onFailure(err -> rc.response().setStatusCode(500).end(err.getMessage()));
|
||
});
|
||
router.put("/api/profile").handler(rc -> {
|
||
Integer userId = rc.session().get("userId");
|
||
JsonObject body = rc.body().asJsonObject();
|
||
String email = body.getString("email");
|
||
String password = body.getString("password");
|
||
String language = body.getString("language");
|
||
userService.updateProfile(userId, email, password, language)
|
||
.onSuccess(v -> {
|
||
if (language != null) rc.session().put("language", language);
|
||
rc.response().end(new JsonObject().put("success", true).encode());
|
||
})
|
||
.onFailure(err -> rc.response().setStatusCode(500).end(err.getMessage()));
|
||
});
|
||
router.put("/api/admin/language").handler(rc -> {
|
||
Integer userId = rc.session().get("userId");
|
||
JsonObject body = rc.body().asJsonObject();
|
||
String language = body.getString("language");
|
||
if (language == null || (!"en".equals(language) && !"ru".equals(language))) {
|
||
rc.response().setStatusCode(400).end("Invalid language");
|
||
return;
|
||
}
|
||
userService.updateLanguage(userId, language)
|
||
.onSuccess(v -> {
|
||
rc.session().put("language", language);
|
||
rc.response().end(new JsonObject().put("success", true).encode());
|
||
})
|
||
.onFailure(err -> rc.response().setStatusCode(500).end(err.getMessage()));
|
||
});
|
||
|
||
// Затем существующий блок router.route("/api/admin/*").handler(authHandler::requireAuth);
|
||
router.route("/api/admin/*").handler(authHandler::requireAuth);
|
||
// Добавить проверку роли для чувствительных эндпоинтов:
|
||
// router.route("/api/settings/meta*").handler(AdminHandler::requireAdmin);
|
||
// router.route("/api/admin/settings*").handler(AdminHandler::requireAdmin);
|
||
// router.route("/api/admin/active-sessions").handler(AdminHandler::requireAdmin);
|
||
|
||
router.get("/api/admin/users").handler(rc -> userService.getAllUsers().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.post("/api/admin/users").handler(rc -> {
|
||
JsonObject body = rc.body().asJsonObject();
|
||
String login = body.getString("login");
|
||
String email = body.getString("email");
|
||
String password = body.getString("password");
|
||
String role = body.getString("role");
|
||
String ip = rc.request().remoteAddress().host();
|
||
if (login == null || email == null || password == null) {
|
||
rc.response().setStatusCode(400).end("Missing login, email or password");
|
||
return;
|
||
}
|
||
if (role == null || role.isEmpty()) role = "user";
|
||
userService.createUser(login, email, password, ip, true, role)
|
||
.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 email = body.getString("email");
|
||
String password = body.getString("password");
|
||
String role = body.getString("role");
|
||
String ip = rc.request().remoteAddress().host();
|
||
if (login == null || email == null) {
|
||
rc.response().setStatusCode(400).end("Missing login or email");
|
||
return;
|
||
}
|
||
userService.updateUser(id, login, email, password, ip, role)
|
||
.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"));
|
||
Integer currentUserId = rc.session().get("userId");
|
||
|
||
if (currentUserId != null && currentUserId == id) {
|
||
rc.response().setStatusCode(403).end(new JsonObject()
|
||
.put("error", "You cannot delete your own account")
|
||
.encode());
|
||
return;
|
||
}
|
||
|
||
userService.deleteUser(id)
|
||
.onSuccess(v -> rc.response().end())
|
||
.onFailure(err -> rc.response().setStatusCode(500).end(err.getMessage()));
|
||
});
|
||
|
||
router.put("/api/admin/users/:id/activate").handler(rc -> {
|
||
int id = Integer.parseInt(rc.pathParam("id"));
|
||
boolean active = Boolean.parseBoolean(rc.queryParam("active").getFirst());
|
||
Integer currentUserId = rc.session().get("userId");
|
||
|
||
if (currentUserId != null && currentUserId == id) {
|
||
rc.response().setStatusCode(403).end(new JsonObject().put("error", "You cannot deactivate yourself").encode());
|
||
return;
|
||
}
|
||
|
||
userService.setActive(id, active)
|
||
.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");
|
||
if (userId != null) {
|
||
rc.response()
|
||
.putHeader("Content-Type", "application/json")
|
||
.end(new JsonObject()
|
||
.put("id", userId)
|
||
.put("login", rc.session().get("login"))
|
||
.encode());
|
||
} else {
|
||
rc.response().setStatusCode(401).end();
|
||
}
|
||
});
|
||
|
||
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.get("/api/admin/restaurants/:id/check").handler(rc -> {
|
||
int id = Integer.parseInt(rc.pathParam("id"));
|
||
restaurantService.findById(id)
|
||
.onSuccess(rest -> {
|
||
if (rest == null) {
|
||
rc.response().setStatusCode(404).end();
|
||
} else {
|
||
IikoOlapClient iiko = new IikoOlapClient(vertx, rest);
|
||
|
||
iiko.checkConnection()
|
||
.onSuccess(res -> rc.response().putHeader("Content-Type", "application/json").end(res.encode()))
|
||
.onFailure(err -> rc.response().putHeader("Content-Type", "application/json").end(
|
||
new JsonObject().put("success", false).put("error", err.getMessage()).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");
|
||
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, https)
|
||
.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");
|
||
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, https)
|
||
.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()));
|
||
});
|
||
|
||
// Получение всех настроек
|
||
router.get("/api/settings").handler(rc -> {
|
||
settingsService.getPublicSettings()
|
||
.onSuccess(settings -> rc.response().putHeader("Content-Type", "application/json").end(settings.encode()))
|
||
.onFailure(err -> rc.response().setStatusCode(500).end());
|
||
});
|
||
|
||
// Получить метаданные всех настроек (для построения формы)
|
||
router.route("/api/admin/settings*").handler(AdminHandler::requireAdmin);
|
||
router.get("/api/admin/settings/meta").handler(rc -> {
|
||
settingsService.getMetadata()
|
||
.onSuccess(meta -> rc.response().putHeader("Content-Type", "application/json").end(meta.encode()))
|
||
.onFailure(err -> rc.response().setStatusCode(500).end(err.getMessage()));
|
||
});
|
||
|
||
// Получить все настройки со значениями по умолчанию
|
||
router.get("/api/admin/settings").handler(rc -> {
|
||
settingsService.getAllWithDefaults()
|
||
.onSuccess(settings -> rc.response().putHeader("Content-Type", "application/json").end(settings.encode()))
|
||
.onFailure(err -> rc.response().setStatusCode(500).end(err.getMessage()));
|
||
});
|
||
|
||
// Обновление настроек (админ)
|
||
router.put("/api/admin/settings").handler(rc -> {
|
||
JsonObject body = rc.body().asJsonObject();
|
||
List<Future<Void>> futures = new ArrayList<>(); // явно указываем тип Future<Void>
|
||
body.forEach(entry -> futures.add(settingsService.set(entry.getKey(), entry.getValue().toString())));
|
||
Future.all(futures)
|
||
.onSuccess(v -> rc.response().end())
|
||
.onFailure(err -> rc.response().setStatusCode(500).end(err.getMessage()));
|
||
});
|
||
|
||
return router;
|
||
}
|
||
|
||
private void startHttp(Router router, Promise<Void> startPromise) {
|
||
httpServer = vertx.createHttpServer();
|
||
httpServer.requestHandler(router).listen(config.server.port, config.server.host)
|
||
.onSuccess(server -> {
|
||
log.info("HTTP server started on port {}", server.actualPort());
|
||
startPromise.complete();
|
||
})
|
||
.onFailure(throwable -> {
|
||
log.error(throwable.getMessage());
|
||
startPromise.fail(throwable);
|
||
});
|
||
}
|
||
|
||
@Override
|
||
public void stop(Promise<Void> stopPromise) {
|
||
this.httpServer.close()
|
||
.onSuccess(server -> {
|
||
log.info("Stop HTTP server");
|
||
this.db.disconnect();
|
||
this.redis.disconnect();
|
||
this.vertx.close()
|
||
.onSuccess(vertx -> {
|
||
log.info("Stop Vert.x");
|
||
stopPromise.complete();
|
||
})
|
||
.onFailure(throwable -> log.info(throwable.getMessage()));
|
||
})
|
||
.onFailure(throwable -> log.info(throwable.getMessage()));
|
||
}
|
||
}
|