+
Setup Admin Account
-
{{ error }}
+
{{ error }}
+
+
diff --git a/frontend/src/views/auth/Login.vue b/frontend/src/views/auth/Login.vue
new file mode 100644
index 0000000..0006f69
--- /dev/null
+++ b/frontend/src/views/auth/Login.vue
@@ -0,0 +1,129 @@
+
+
+
+
+
+
+
Welcome Back
+
Sign in to your account
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/views/auth/Setup.vue b/frontend/src/views/auth/Setup.vue
new file mode 100644
index 0000000..5f5b29d
--- /dev/null
+++ b/frontend/src/views/auth/Setup.vue
@@ -0,0 +1,206 @@
+
+
+
+
+
+
+
Setup Admin Account
+
Create your administrator account
+
+
+
+
+
+
+
+
+
diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js
new file mode 100644
index 0000000..5b76f1a
--- /dev/null
+++ b/frontend/tailwind.config.js
@@ -0,0 +1,41 @@
+/** @type {import('tailwindcss').Config} */
+export default {
+ content: [
+ "./index.html",
+ "./src/**/*.{vue,js,ts,jsx,tsx}",
+ ],
+ theme: {
+ extend: {
+ colors: {
+ primary: {
+ 50: '#eff6ff',
+ 100: '#dbeafe',
+ 200: '#bfdbfe',
+ 300: '#93c5fd',
+ 400: '#60a5fa',
+ 500: '#3b82f6',
+ 600: '#2563eb',
+ 700: '#1d4ed8',
+ 800: '#1e40af',
+ 900: '#1e3a8a',
+ }
+ },
+ animation: {
+ 'fade-in': 'fadeIn 0.5s ease-in-out',
+ 'slide-in': 'slideIn 0.3s ease-out',
+ 'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
+ },
+ keyframes: {
+ fadeIn: {
+ '0%': { opacity: '0' },
+ '100%': { opacity: '1' },
+ },
+ slideIn: {
+ '0%': { transform: 'translateX(-100%)' },
+ '100%': { transform: 'translateX(0)' },
+ }
+ }
+ },
+ },
+ plugins: [],
+}
diff --git a/src/main/java/su/xserver/iikocon/MainVerticle.java b/src/main/java/su/xserver/iikocon/MainVerticle.java
index bdaf915..1a28316 100644
--- a/src/main/java/su/xserver/iikocon/MainVerticle.java
+++ b/src/main/java/su/xserver/iikocon/MainVerticle.java
@@ -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
stopPromise) {
+ if (dbPool != null) {
+ dbPool.close();
+ }
+ stopPromise.complete();
}
}
diff --git a/src/main/java/su/xserver/iikocon/SetupHandler.java b/src/main/java/su/xserver/iikocon/SetupHandler.java
index ae85fa1..8e2209a 100644
--- a/src/main/java/su/xserver/iikocon/SetupHandler.java
+++ b/src/main/java/su/xserver/iikocon/SetupHandler.java
@@ -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());
}
});
}
diff --git a/src/main/java/su/xserver/iikocon/UserService.java b/src/main/java/su/xserver/iikocon/UserService.java
index ddb884b..a388234 100644
--- a/src/main/java/su/xserver/iikocon/UserService.java
+++ b/src/main/java/su/xserver/iikocon/UserService.java
@@ -30,48 +30,59 @@ public class UserService {
ip VARCHAR(45)
)
""";
+
return pool.query(createTable).execute().mapEmpty();
}
public Future 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 createUser(String login, String password, String ip) {
String hash = BCrypt.hashpw(password, BCrypt.gensalt());
+
Map 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 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 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;
+ }
}
}