This commit is contained in:
2026-04-10 15:26:51 +03:00
parent a8a2239d37
commit 5821006bf2
68 changed files with 3292 additions and 19 deletions

1
.gitignore vendored
View File

@@ -183,3 +183,4 @@ nbdist/
!.vscode/tasks.json !.vscode/tasks.json
!.vscode/launch.json !.vscode/launch.json
!.vscode/extensions.json !.vscode/extensions.json
/build/

12
Dockerfile Normal file
View File

@@ -0,0 +1,12 @@
FROM gradle:8.14.3-jdk21 AS build
LABEL authors="DANIL_BODRY"
WORKDIR /app
COPY . .
RUN gradle clean build
FROM eclipse-temurin:21-jre
WORKDIR /app
COPY --from=build /app/build/libs/*-fat.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

View File

@@ -16,7 +16,7 @@ repositories {
val vertxVersion = "5.0.10" val vertxVersion = "5.0.10"
val mainVerticleName = "com.example.starter.MainVerticle" val mainVerticleName = "su.xserver.iikocon.MainVerticle"
val launcherClassName = "io.vertx.launcher.application.VertxApplication" val launcherClassName = "io.vertx.launcher.application.VertxApplication"
application { application {
@@ -32,7 +32,21 @@ dependencies {
implementation("io.vertx:vertx-health-check") implementation("io.vertx:vertx-health-check")
implementation("io.vertx:vertx-web") implementation("io.vertx:vertx-web")
implementation("io.vertx:vertx-mysql-client") implementation("io.vertx:vertx-mysql-client")
implementation("io.vertx:vertx-redis-client")
implementation("io.vertx:vertx-web-sstore-redis")
implementation("io.vertx:vertx-mail-client") implementation("io.vertx:vertx-mail-client")
// https://mvnrepository.com/artifact/org.mindrot/jbcrypt
implementation("org.mindrot:jbcrypt:0.4")
// https://mvnrepository.com/artifact/org.slf4j/slf4j-api
implementation("org.slf4j:slf4j-api:2.0.17")
// https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-slf4j2-impl
implementation("org.apache.logging.log4j:log4j-slf4j2-impl:2.25.3")
// https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core
implementation("org.apache.logging.log4j:log4j-core:2.25.3")
// https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-api
implementation("org.apache.logging.log4j:log4j-api:2.25.3")
} }
java { java {
@@ -58,3 +72,40 @@ tasks.withType<Test> {
tasks.withType<JavaExec> { tasks.withType<JavaExec> {
args = listOf(mainVerticleName) args = listOf(mainVerticleName)
} }
tasks.register("collectAllDependencies") {
group = "project"
description = "Сбор всех зависимостей для офлайн работы"
doLast {
val outDir = File("${rootDir}/libs")
outDir.mkdirs()
val allArtifacts = configurations
.filter { it.isCanBeResolved }
.flatMap { config ->
config.resolvedConfiguration.lenientConfiguration.allModuleDependencies.flatMap { dep ->
dep.allModuleArtifacts
}
}
// ❗ Исключаем артефакты локальных проектов (например, :library)
.filterNot { artifact ->
artifact.id.componentIdentifier.displayName.startsWith("project ")
}
.distinctBy { it.file.name }
allArtifacts.forEach { artifact ->
val outFile = outDir.resolve(artifact.file.name)
if (!outFile.exists() && artifact.file.exists()) {
println("⬇️ Copying ${artifact.moduleVersion.id.group}:${artifact.name}:${artifact.moduleVersion.id.version} ...")
artifact.file.inputStream().use { input ->
outFile.outputStream().use { output ->
input.copyTo(output)
}
}
}
}
println("✅ All dependencies are collected in \"libs\"")
}
}

43
docker-compose.yml Normal file
View File

@@ -0,0 +1,43 @@
services:
mariadb:
image: mariadb:10.11
container_name: admin-mariadb
restart: unless-stopped
environment:
MARIADB_ROOT_PASSWORD: rootpass
MARIADB_DATABASE: admin_db
MARIADB_USER: admin_user
MARIADB_PASSWORD: admin_pass
ports:
- "3306:3306"
volumes:
- mariadb_data:/var/lib/mysql
redis:
image: redis:7-alpine
container_name: admin-redis
restart: unless-stopped
ports:
- "6379:6379"
app:
build: .
container_name: iiko-app
restart: unless-stopped
ports:
- "8080:8080"
depends_on:
- mariadb
- redis
environment:
DB_HOST: mariadb
DB_PORT: 3306
DB_NAME: admin_db
DB_USER: admin_user
DB_PASSWORD: admin_pass
REDIS_HOST: redis
REDIS_PORT: 6379
HTTP_PORT: 8080
volumes:
mariadb_data:

39
frontend/.gitignore vendored Normal file
View File

@@ -0,0 +1,39 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo
.eslintcache
# Cypress
/cypress/videos/
/cypress/screenshots/
# Vitest
__screenshots__/
# Vite
*.timestamp-*-*.mjs

3
frontend/.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

38
frontend/README.md Normal file
View File

@@ -0,0 +1,38 @@
# frontend
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VS Code](https://code.visualstudio.com/) + [Vue (Official)](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
## Recommended Browser Setup
- Chromium-based browsers (Chrome, Edge, Brave, etc.):
- [Vue.js devtools](https://chromewebstore.google.com/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd)
- [Turn on Custom Object Formatter in Chrome DevTools](http://bit.ly/object-formatters)
- Firefox:
- [Vue.js devtools](https://addons.mozilla.org/en-US/firefox/addon/vue-js-devtools/)
- [Turn on Custom Object Formatter in Firefox DevTools](https://fxdx.dev/firefox-devtools-custom-object-formatters/)
## Customize configuration
See [Vite Configuration Reference](https://vite.dev/config/).
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Compile and Minify for Production
```sh
npm run build
```

13
frontend/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

8
frontend/jsconfig.json Normal file
View File

@@ -0,0 +1,8 @@
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
},
"exclude": ["node_modules", "dist"]
}

2561
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

24
frontend/package.json Normal file
View File

@@ -0,0 +1,24 @@
{
"name": "frontend",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"axios": "^1.15.0",
"vue": "^3.5.31",
"vue-router": "^4.6.4"
},
"devDependencies": {
"@vitejs/plugin-vue": "^6.0.5",
"vite": "^8.0.3",
"vite-plugin-vue-devtools": "^8.1.1"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
}

BIN
frontend/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

3
frontend/src/App.vue Normal file
View File

@@ -0,0 +1,3 @@
<template>
<router-view />
</template>

5
frontend/src/main.ts Normal file
View File

@@ -0,0 +1,5 @@
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
createApp(App).use(router).mount('#app')

View File

@@ -0,0 +1,37 @@
import { createRouter, createWebHistory } from 'vue-router'
import Login from '../views/Login.vue'
import Setup from '../views/Setup.vue'
import Dashboard from '../views/Dashboard.vue'
const routes = [
{ path: '/login', component: Login },
{ path: '/setup', component: Setup },
{ path: '/', component: Dashboard, meta: { requiresAuth: true } }
]
const router = createRouter({
history: createWebHistory(),
routes
})
router.beforeEach(async (to, from, next) => {
const requiresAuth = to.matched.some(record => record.meta.requiresAuth)
const loggedIn = await checkAuth() // запрос к /api/admin/users или специальному endpoint
if (requiresAuth && !loggedIn) {
next('/login')
} else {
next()
}
})
async function checkAuth(): Promise<boolean> {
try {
const res = await fetch('/api/admin/users')
return res.ok
} catch {
return false
}
}
export default router

View File

@@ -0,0 +1,43 @@
<template>
<div>
<h2>Dashboard</h2>
<button @click="logout">Logout</button>
<h3>Users</h3>
<table>
<thead>
<tr><th>ID</th><th>Login</th><th>Created</th><th>IP</th></tr>
</thead>
<tbody>
<tr v-for="user in users" :key="user.id">
<td>{{ user.id }}</td>
<td>{{ user.login }}</td>
<td>{{ user.created }}</td>
<td>{{ user.ip }}</td>
</tr>
</tbody>
</table>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import axios from 'axios'
const router = useRouter()
const users = ref([])
onMounted(async () => {
try {
const res = await axios.get('/api/admin/users')
users.value = res.data
} catch {
router.push('/login')
}
})
async function logout() {
await axios.post('/api/logout')
router.push('/login')
}
</script>

View File

@@ -0,0 +1,30 @@
<template>
<div>
<h2>Login</h2>
<form @submit.prevent="login">
<input v-model="loginForm.login" placeholder="Login" />
<input v-model="loginForm.password" type="password" placeholder="Password" />
<button type="submit">Login</button>
</form>
<p v-if="error">{{ error }}</p>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import axios from 'axios'
const router = useRouter()
const loginForm = ref({ login: '', password: '' })
const error = ref('')
async function login() {
try {
await axios.post('/api/login', loginForm.value)
router.push('/')
} catch (e) {
error.value = 'Invalid credentials'
}
}
</script>

View File

@@ -0,0 +1,30 @@
<template>
<div>
<h2>Setup Admin Account</h2>
<form @submit.prevent="setup">
<input v-model="form.login" placeholder="Admin login" />
<input v-model="form.password" type="password" placeholder="Password (min 6 chars)" />
<button type="submit">Create Admin</button>
</form>
<p v-if="error">{{ error }}</p>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import axios from 'axios'
const router = useRouter()
const form = ref({ login: '', password: '' })
const error = ref('')
async function setup() {
try {
await axios.post('/api/setup', form.value)
router.push('/login')
} catch (e) {
error.value = e.response?.data || 'Setup failed'
}
}
</script>

15
frontend/vite.config.ts Normal file
View File

@@ -0,0 +1,15 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
server: {
proxy: {
'/api': 'http://localhost:8080' // для разработки
}
},
build: {
outDir: '../src/main/resources/webroot', // после сборки копируем в ресурсы Vert.x
emptyOutDir: true
}
})

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
libs/jbcrypt-0.4.jar Normal file

Binary file not shown.

BIN
libs/jspecify-1.0.0.jar Normal file

Binary file not shown.

BIN
libs/log4j-api-2.25.3.jar Normal file

Binary file not shown.

BIN
libs/log4j-core-2.25.3.jar Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
libs/picocli-4.7.4.jar Normal file

Binary file not shown.

BIN
libs/slf4j-api-2.0.17.jar Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
libs/vertx-core-5.0.10.jar Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
libs/vertx-web-5.0.10.jar Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -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");
});
}
}

View 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();
}
};
}

View 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();
}
}

View 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");
}
});
}
}

View 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);
}
}