v0
This commit is contained in:
@@ -1,18 +0,0 @@
|
||||
package com.example.starter;
|
||||
|
||||
import io.vertx.core.Future;
|
||||
import io.vertx.core.VerticleBase;
|
||||
|
||||
public class MainVerticle extends VerticleBase {
|
||||
|
||||
@Override
|
||||
public Future<?> start() {
|
||||
return vertx.createHttpServer().requestHandler(req -> {
|
||||
req.response()
|
||||
.putHeader("content-type", "text/plain")
|
||||
.end("Hello from Vert.x!");
|
||||
}).listen(8888).onSuccess(http -> {
|
||||
System.out.println("HTTP server started on port 8888");
|
||||
});
|
||||
}
|
||||
}
|
||||
54
src/main/java/su/xserver/iikocon/AuthHandler.java
Normal file
54
src/main/java/su/xserver/iikocon/AuthHandler.java
Normal file
@@ -0,0 +1,54 @@
|
||||
package su.xserver.iikocon;
|
||||
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import io.vertx.ext.web.RoutingContext;
|
||||
import io.vertx.ext.web.Session;
|
||||
|
||||
public class AuthHandler {
|
||||
private final UserService userService;
|
||||
|
||||
public AuthHandler(UserService userService) {
|
||||
this.userService = userService;
|
||||
}
|
||||
|
||||
public void handleLogin(RoutingContext ctx) {
|
||||
JsonObject body = ctx.body().asJsonObject();
|
||||
String login = body.getString("login");
|
||||
String password = body.getString("password");
|
||||
|
||||
if (login == null || password == null) {
|
||||
ctx.response().setStatusCode(400).end("Missing credentials");
|
||||
return;
|
||||
}
|
||||
|
||||
userService.findByLogin(login).onComplete(ar -> {
|
||||
if (ar.succeeded() && ar.result() != null) {
|
||||
JsonObject user = ar.result();
|
||||
if (userService.checkPassword(password, user.getString("password"))) {
|
||||
Session session = ctx.session();
|
||||
session.put("userId", user.getInteger("id"));
|
||||
session.put("login", user.getString("login"));
|
||||
ctx.response().end(new JsonObject().put("success", true).put("login", user.getString("login")).encode());
|
||||
} else {
|
||||
ctx.response().setStatusCode(401).end("Invalid credentials");
|
||||
}
|
||||
} else {
|
||||
ctx.response().setStatusCode(401).end("Invalid credentials");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void handleLogout(RoutingContext ctx) {
|
||||
ctx.session().destroy();
|
||||
ctx.response().end(new JsonObject().put("success", true).encode());
|
||||
}
|
||||
|
||||
public void requireAuth(RoutingContext ctx) {
|
||||
Session session = ctx.session();
|
||||
if (session == null || session.get("userId") == null) {
|
||||
ctx.response().setStatusCode(401).end("Unauthorized");
|
||||
} else {
|
||||
ctx.next();
|
||||
}
|
||||
};
|
||||
}
|
||||
158
src/main/java/su/xserver/iikocon/MainVerticle.java
Normal file
158
src/main/java/su/xserver/iikocon/MainVerticle.java
Normal file
@@ -0,0 +1,158 @@
|
||||
package su.xserver.iikocon;
|
||||
|
||||
import io.vertx.core.AbstractVerticle;
|
||||
import io.vertx.core.Promise;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import io.vertx.ext.healthchecks.HealthChecks;
|
||||
import io.vertx.ext.healthchecks.Status;
|
||||
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 io.vertx.mysqlclient.MySQLConnectOptions;
|
||||
import io.vertx.redis.client.Redis;
|
||||
import io.vertx.redis.client.RedisAPI;
|
||||
import io.vertx.redis.client.RedisOptions;
|
||||
import io.vertx.sqlclient.Pool;
|
||||
|
||||
import io.vertx.sqlclient.PoolOptions;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
public class MainVerticle extends AbstractVerticle {
|
||||
private static final Logger log = LoggerFactory.getLogger(MainVerticle.class);
|
||||
|
||||
private Pool dbPool;
|
||||
private Redis redisClient;
|
||||
private UserService userService;
|
||||
|
||||
@Override
|
||||
public void start(Promise<Void> startPromise) {
|
||||
// Конфигурация из переменных окружения
|
||||
JsonObject config = new JsonObject()
|
||||
.put("db_host", System.getenv().getOrDefault("DB_HOST", "localhost"))
|
||||
.put("db_port", Integer.parseInt(System.getenv().getOrDefault("DB_PORT", "3306")))
|
||||
.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")));
|
||||
|
||||
// Подключение к MariaDB
|
||||
MySQLConnectOptions connectOptions = new MySQLConnectOptions()
|
||||
.setHost(config.getString("db_host"))
|
||||
.setPort(config.getInteger("db_port"))
|
||||
.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 час
|
||||
|
||||
// Роутер
|
||||
Router router = Router.router(vertx);
|
||||
router.route().handler(BodyHandler.create());
|
||||
router.route().handler(sessionHandler);
|
||||
|
||||
// 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 -> {
|
||||
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);
|
||||
|
||||
// Регистрация первого администратора (если таблица пуста)
|
||||
router.post("/api/setup").handler(setupHandler::handleSetup);
|
||||
|
||||
// Логин
|
||||
router.post("/api/login").handler(authHandler::handleLogin);
|
||||
|
||||
// Выход
|
||||
router.post("/api/logout").handler(authHandler::handleLogout);
|
||||
|
||||
// Защищённые маршруты (требуют сессии)
|
||||
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());
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Запуск 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();
|
||||
} else {
|
||||
log.error("Failed to start", ar.cause());
|
||||
startPromise.fail(ar.cause());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
if (dbPool != null) dbPool.close();
|
||||
}
|
||||
}
|
||||
39
src/main/java/su/xserver/iikocon/SetupHandler.java
Normal file
39
src/main/java/su/xserver/iikocon/SetupHandler.java
Normal file
@@ -0,0 +1,39 @@
|
||||
package su.xserver.iikocon;
|
||||
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import io.vertx.ext.web.RoutingContext;
|
||||
|
||||
public class SetupHandler {
|
||||
private final UserService userService;
|
||||
|
||||
public SetupHandler(UserService userService) {
|
||||
this.userService = userService;
|
||||
}
|
||||
|
||||
public void handleSetup(RoutingContext ctx) {
|
||||
// Проверяем, есть ли уже пользователи
|
||||
userService.countUsers().onComplete(ar -> {
|
||||
if (ar.succeeded() && ar.result() == 0) {
|
||||
JsonObject body = ctx.body().asJsonObject();
|
||||
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)");
|
||||
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());
|
||||
} else {
|
||||
ctx.response().setStatusCode(500).end("Failed to create admin: " + cr.cause().getMessage());
|
||||
}
|
||||
});
|
||||
} else {
|
||||
ctx.response().setStatusCode(403).end("Setup already completed");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
84
src/main/java/su/xserver/iikocon/UserService.java
Normal file
84
src/main/java/su/xserver/iikocon/UserService.java
Normal file
@@ -0,0 +1,84 @@
|
||||
package su.xserver.iikocon;
|
||||
|
||||
import io.vertx.core.Future;
|
||||
import io.vertx.core.json.JsonArray;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import io.vertx.sqlclient.Pool;
|
||||
import io.vertx.sqlclient.Row;
|
||||
import io.vertx.sqlclient.templates.SqlTemplate;
|
||||
import org.mindrot.jbcrypt.BCrypt;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class UserService {
|
||||
private final Pool pool;
|
||||
|
||||
public UserService(Pool pool) {
|
||||
this.pool = pool;
|
||||
}
|
||||
|
||||
public Future<Void> initDatabase() {
|
||||
String createTable = """
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
login VARCHAR(255) UNIQUE NOT NULL,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
ip VARCHAR(45)
|
||||
)
|
||||
""";
|
||||
return pool.query(createTable).execute().mapEmpty();
|
||||
}
|
||||
|
||||
public Future<Long> countUsers() {
|
||||
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})")
|
||||
.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}")
|
||||
.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("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()
|
||||
.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("ip", row.getString("ip")));
|
||||
}
|
||||
return array;
|
||||
});
|
||||
}
|
||||
|
||||
public boolean checkPassword(String plain, String hash) {
|
||||
return BCrypt.checkpw(plain, hash);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user