This commit is contained in:
danilbodry-mac
2026-04-10 19:58:29 +03:00
parent 5821006bf2
commit c5287dc81d
18 changed files with 2495 additions and 161 deletions

View File

@@ -9,6 +9,7 @@ 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.LocalSessionStore;
import io.vertx.ext.web.sstore.SessionStore;
import io.vertx.ext.web.sstore.redis.RedisSessionStore;
import io.vertx.mysqlclient.MySQLConnectOptions;
@@ -27,7 +28,6 @@ public class MainVerticle extends AbstractVerticle {
private static final Logger log = LoggerFactory.getLogger(MainVerticle.class);
private Pool dbPool;
private Redis redisClient;
private UserService userService;
@Override
@@ -39,10 +39,10 @@ public class MainVerticle extends AbstractVerticle {
.put("db_name", System.getenv().getOrDefault("DB_NAME", "admin_db"))
.put("db_user", System.getenv().getOrDefault("DB_USER", "admin_user"))
.put("db_password", System.getenv().getOrDefault("DB_PASSWORD", "admin_pass"))
.put("redis_host", System.getenv().getOrDefault("REDIS_HOST", "localhost"))
.put("redis_port", Integer.parseInt(System.getenv().getOrDefault("REDIS_PORT", "6379")))
.put("http_port", Integer.parseInt(System.getenv().getOrDefault("HTTP_PORT", "8080")));
log.info("Starting with config: {}", config.encodePrettily());
// Подключение к MariaDB
MySQLConnectOptions connectOptions = new MySQLConnectOptions()
.setHost(config.getString("db_host"))
@@ -50,109 +50,145 @@ public class MainVerticle extends AbstractVerticle {
.setDatabase(config.getString("db_name"))
.setUser(config.getString("db_user"))
.setPassword(config.getString("db_password"));
PoolOptions poolOptions = new PoolOptions().setMaxSize(5);
dbPool = Pool.pool(vertx, connectOptions, poolOptions);
// Подключение к Redis
RedisOptions redisOptions = new RedisOptions()
.setConnectionString("redis://" + config.getString("redis_host") + ":" + config.getInteger("redis_port"));
redisClient = Redis.createClient(vertx, redisOptions);
// Инициализация сервисов
userService = new UserService(dbPool);
// Инициализация БД (создание таблицы users)
userService.initDatabase().compose(v -> {
// Настройка сессий с Redis
SessionStore sessionStore = RedisSessionStore.create(vertx, redisClient);
SessionHandler sessionHandler = SessionHandler.create(sessionStore)
.setSessionCookieName("admin.session")
.setCookieHttpOnlyFlag(true)
.setCookieSecureFlag(false) // для разработки, в продакшене true + HTTPS
.setSessionTimeout(3600000); // 1 час
userService.initDatabase()
.onSuccess(v -> log.info("Database initialized successfully"))
.onFailure(err -> {
log.error("Failed to initialize database", err);
startPromise.fail(err);
return;
});
// Роутер
Router router = Router.router(vertx);
router.route().handler(BodyHandler.create());
router.route().handler(sessionHandler);
// Настройка сессий (используем LocalSessionStore для простоты)
SessionStore sessionStore = LocalSessionStore.create(vertx);
SessionHandler sessionHandler = SessionHandler.create(sessionStore)
.setSessionCookieName("admin.session")
.setCookieHttpOnlyFlag(true)
.setCookieSecureFlag(false)
.setSessionTimeout(3600000); // 1 час
// Health Checks
HealthChecks hc = HealthChecks.create(vertx);
hc.register("database", promise -> dbPool.getConnection().onComplete(ar -> {
// Роутер
Router router = Router.router(vertx);
router.route().handler(BodyHandler.create());
router.route().handler(sessionHandler);
// 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();
}
});
// Health Checks
HealthChecks hc = HealthChecks.create(vertx);
hc.register("database", promise ->
dbPool.getConnection().onComplete(ar -> {
if (ar.succeeded()) {
ar.result().close();
promise.complete(Status.OK());
} else {
promise.fail(ar.cause());
}
}));
hc.register("redis", promise -> {
RedisAPI.api(redisClient)
.ping(Collections.singletonList("ping"))
.onSuccess(response -> {
if ("PONG".equals(response.toString())) {
promise.complete(Status.OK());
} else {
promise.fail("Unexpected ping response: " + response);
}
})
.onFailure(promise::fail);
});
router.get("/health").handler(rc -> hc.checkStatus().onComplete(ar -> {
})
);
router.get("/health").handler(rc ->
hc.checkStatus().onComplete(ar -> {
if (ar.succeeded()) {
rc.response().end(ar.result().toJson().encodePrettily());
} else {
rc.response().setStatusCode(503).end(ar.cause().getMessage());
}
}));
})
);
// Статическая раздача фронтенда (из webroot)
router.route("/*").handler(StaticHandler.create("webroot").setCachingEnabled(false).setIndexPage("index.html"));
// API маршруты
AuthHandler authHandler = new AuthHandler(userService);
SetupHandler setupHandler = new SetupHandler(userService);
// 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/setup").handler(setupHandler::handleSetup);
// Логин
router.post("/api/login").handler(authHandler::handleLogin);
// Логин
router.post("/api/login").handler(authHandler::handleLogin);
// Выход
router.post("/api/logout").handler(authHandler::handleLogout);
// Выход
router.post("/api/logout").handler(authHandler::handleLogout);
// Защищённые маршруты (требуют сессии)
router.route("/api/admin/*").handler(authHandler::requireAuth);
// Защищённые маршруты
router.route("/api/admin/*").handler(authHandler::requireAuth);
// Пример защищённого эндпоинта - получение списка пользователей
router.get("/api/admin/users").handler(rc -> {
userService.getAllUsers().onComplete(ar -> {
if (ar.succeeded()) {
rc.response().end(ar.result().encode());
} else {
rc.response().setStatusCode(500).end(ar.cause().getMessage());
}
});
// Получение списка пользователей
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());
}
});
});
// Запуск HTTP сервера
int port = config.getInteger("http_port");
return vertx.createHttpServer().requestHandler(router).listen(port);
}).onComplete(ar -> {
if (ar.succeeded()) {
log.info("Server started on port {}", config.getInteger("http_port"));
startPromise.complete();
// Получение текущего пользователя
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 {
log.error("Failed to start", ar.cause());
startPromise.fail(ar.cause());
rc.response().setStatusCode(401).end();
}
});
// Статическая раздача фронтенда
router.route("/*").handler(StaticHandler.create("webroot")
.setCachingEnabled(false)
.setIndexPage("index.html"));
// Запуск HTTP сервера
int port = config.getInteger("http_port");
vertx.createHttpServer()
.requestHandler(router)
.listen(port).onComplete(http -> {
if (http.succeeded()) {
log.info("HTTP server started on port {}", port);
startPromise.complete();
} else {
log.error("Failed to start HTTP server", http.cause());
startPromise.fail(http.cause());
}
});
}
@Override
public void stop() {
if (dbPool != null) dbPool.close();
public void stop(Promise<Void> stopPromise) {
if (dbPool != null) {
dbPool.close();
}
stopPromise.complete();
}
}

View File

@@ -10,29 +10,59 @@ public class SetupHandler {
this.userService = userService;
}
public void checkStatus(RoutingContext ctx) {
userService.countUsers().onComplete(ar -> {
if (ar.succeeded()) {
ctx.response()
.putHeader("Content-Type", "application/json")
.end(new JsonObject()
.put("needsSetup", ar.result() == 0)
.put("userCount", ar.result())
.encode());
} else {
ctx.response().setStatusCode(500).end(ar.cause().getMessage());
}
});
}
public void handleSetup(RoutingContext ctx) {
// Проверяем, есть ли уже пользователи
userService.countUsers().onComplete(ar -> {
if (ar.succeeded() && ar.result() == 0) {
JsonObject body = ctx.body().asJsonObject();
if (body == null) {
ctx.response().setStatusCode(400).end("Invalid JSON body");
return;
}
String login = body.getString("login");
String password = body.getString("password");
if (login == null || password == null || login.length() < 3 || password.length() < 6) {
ctx.response().setStatusCode(400).end("Invalid login or password (min 3/6 chars)");
ctx.response().setStatusCode(400)
.end(new JsonObject()
.put("error", "Invalid login or password (min 3/6 chars)")
.encode());
return;
}
String ip = ctx.request().remoteAddress().host();
userService.createUser(login, password, ip).onComplete(cr -> {
if (cr.succeeded()) {
ctx.response().setStatusCode(201).end(new JsonObject().put("success", true).encode());
ctx.response().setStatusCode(201)
.end(new JsonObject().put("success", true).encode());
} else {
ctx.response().setStatusCode(500).end("Failed to create admin: " + cr.cause().getMessage());
ctx.response().setStatusCode(500)
.end(new JsonObject()
.put("error", "Failed to create admin: " + cr.cause().getMessage())
.encode());
}
});
} else {
ctx.response().setStatusCode(403).end("Setup already completed");
ctx.response().setStatusCode(403)
.end(new JsonObject()
.put("error", "Setup already completed")
.encode());
}
});
}

View File

@@ -30,48 +30,59 @@ public class UserService {
ip VARCHAR(45)
)
""";
return pool.query(createTable).execute().mapEmpty();
}
public Future<Long> countUsers() {
return pool.query("SELECT COUNT(*) AS cnt FROM users").execute()
return pool.query("SELECT COUNT(*) AS cnt FROM users")
.execute()
.map(rows -> rows.iterator().next().getLong("cnt"));
}
public Future<Void> createUser(String login, String password, String ip) {
String hash = BCrypt.hashpw(password, BCrypt.gensalt());
Map<String, Object> params = new HashMap<>();
params.put("login", login);
params.put("password", hash);
params.put("ip", ip);
return SqlTemplate.forUpdate(pool, "INSERT INTO users (login, password, ip) VALUES (#{login}, #{password}, #{ip})")
return SqlTemplate.forUpdate(pool,
"INSERT INTO users (login, password, ip) VALUES (#{login}, #{password}, #{ip})")
.execute(params)
.mapEmpty();
}
public Future<JsonObject> findByLogin(String login) {
return SqlTemplate.forQuery(pool, "SELECT id, login, password, created, updated, ip FROM users WHERE login = #{login}")
return SqlTemplate.forQuery(pool,
"SELECT id, login, password, created, updated, ip FROM users WHERE login = #{login}")
.mapTo(row -> new JsonObject()
.put("id", row.getInteger("id"))
.put("login", row.getString("login"))
.put("password", row.getString("password"))
.put("created", row.getLocalDateTime("created").toString())
.put("updated", row.getLocalDateTime("updated").toString())
.put("created", row.getLocalDateTime("created") != null ?
row.getLocalDateTime("created").toString() : null)
.put("updated", row.getLocalDateTime("updated") != null ?
row.getLocalDateTime("updated").toString() : null)
.put("ip", row.getString("ip")))
.execute(Collections.singletonMap("login", login))
.map(rows -> rows.iterator().hasNext() ? rows.iterator().next() : null);
}
public Future<JsonArray> getAllUsers() {
return pool.query("SELECT id, login, created, updated, ip FROM users ORDER BY id").execute()
return pool.query("SELECT id, login, created, updated, ip FROM users ORDER BY id")
.execute()
.map(rows -> {
JsonArray array = new JsonArray();
for (Row row : rows) {
array.add(new JsonObject()
.put("id", row.getInteger("id"))
.put("login", row.getString("login"))
.put("created", row.getLocalDateTime("created").toString())
.put("updated", row.getLocalDateTime("updated").toString())
.put("created", row.getLocalDateTime("created") != null ?
row.getLocalDateTime("created").toString() : null)
.put("updated", row.getLocalDateTime("updated") != null ?
row.getLocalDateTime("updated").toString() : null)
.put("ip", row.getString("ip")));
}
return array;
@@ -79,6 +90,10 @@ public class UserService {
}
public boolean checkPassword(String plain, String hash) {
return BCrypt.checkpw(plain, hash);
try {
return BCrypt.checkpw(plain, hash);
} catch (Exception e) {
return false;
}
}
}