This commit is contained in:
2026-04-18 12:20:44 +03:00
parent af757ff224
commit 2068154656
13 changed files with 423 additions and 92 deletions

View File

@@ -1,14 +1,17 @@
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router'
import Login from '../views/auth/Login.vue' import Login from '../views/auth/Login.vue'
import Setup from '../views/auth/Setup.vue' import Setup from '../views/auth/Setup.vue'
import Register from '../views/auth/Register.vue'
import Dashboard from '../views/Dashboard.vue' import Dashboard from '../views/Dashboard.vue'
import Users from '../views/Users.vue' import Users from '../views/Users.vue'
import Restaurants from '../views/Restaurants.vue' import Restaurants from '../views/Restaurants.vue'
import AdminSettings from '../views/AdminSettings.vue'
import NotFound from '../views/NotFound.vue' import NotFound from '../views/NotFound.vue'
const routes = [ const routes = [
{ path: '/login', component: Login, meta: { title: 'Login' } }, { path: '/login', component: Login, meta: { title: 'Login' } },
{ path: '/setup', component: Setup, meta: { title: 'Setup' } }, { path: '/setup', component: Setup, meta: { title: 'Setup' } },
{ path: '/register', component: Register, meta: { title: 'Register' } },
{ {
path: '/', path: '/',
redirect: '/dashboard' redirect: '/dashboard'
@@ -18,14 +21,21 @@ const routes = [
component: Dashboard, component: Dashboard,
meta: { requiresAuth: true, title: 'Dashboard' } meta: { requiresAuth: true, title: 'Dashboard' }
}, },
{ path: '/users', {
path: '/users',
component: Users, component: Users,
meta: { requiresAuth: true, title: 'Users' } meta: { requiresAuth: true, title: 'Users' }
}, },
{ path: '/restaurants', {
path: '/restaurants',
component: Restaurants, component: Restaurants,
meta: { requiresAuth: true, title: 'Restaurants' } meta: { requiresAuth: true, title: 'Restaurants' }
}, },
{
path: '/settings',
component: AdminSettings,
meta: { requiresAuth: true, title: 'Settings' }
},
{ {
path: '/:pathMatch(.*)*', path: '/:pathMatch(.*)*',
name: 'NotFound', name: 'NotFound',

View File

@@ -0,0 +1,37 @@
<template>
<AppLayout>
<div class="card">
<h1 class="text-2xl font-bold mb-6">Application Settings</h1>
<form @submit.prevent="saveSettings" class="space-y-4 max-w-lg">
<div v-for="(value, key) in settings" :key="key">
<label class="block text-sm font-medium text-gray-700">{{ key }}</label>
<input v-model="settings[key]" type="text" class="input-field mt-1" />
</div>
<button type="submit" class="btn-primary">Save Changes</button>
</form>
</div>
</AppLayout>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import AppLayout from '../components/Layout/AppLayout.vue'
const settings = ref<Record<string, string>>({})
async function loadSettings() {
const res = await fetch('/api/settings')
settings.value = await res.json()
}
async function saveSettings() {
await fetch('/api/admin/settings', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(settings.value)
})
alert('Settings saved')
}
onMounted(loadSettings)
</script>

View File

@@ -118,11 +118,32 @@
import { ref, onMounted, onUnmounted } from 'vue' import { ref, onMounted, onUnmounted } from 'vue'
import AppLayout from '../components/Layout/AppLayout.vue' import AppLayout from '../components/Layout/AppLayout.vue'
const stats = ref({ const stats = ref({ totalUsers: 0, activeSessions: 0, systemHealth: 100, uptime: '99.9%' })
totalUsers: 0,
activeSessions: 0, async function loadStats() {
systemHealth: 98, try {
uptime: '99.9%' const [usersRes, sessionsRes, healthRes] = await Promise.all([
fetch('/api/admin/users'),
fetch('/api/admin/active-sessions'),
fetch('/api/health')
])
const users = await usersRes.json()
const sessions = await sessionsRes.json()
const health = await healthRes.json()
stats.value.totalUsers = users.length
stats.value.activeSessions = sessions.count || 0
const upCount = health.checks?.filter(c => c.status === 'UP').length || 0
const total = health.checks?.length || 1
stats.value.systemHealth = Math.round((upCount / total) * 100)
} catch (e) { console.error(e) }
}
onMounted(() => {
loadStats()
const interval = setInterval(loadStats, 5000)
onUnmounted(() => clearInterval(interval))
}) })
const recentUsers = ref([]) const recentUsers = ref([])

View File

@@ -11,20 +11,28 @@
<tr> <tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">ID</th> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">ID</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Login</th> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Login</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Email</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Active</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">IP</th> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">IP</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Created</th> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Created</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Actions</th> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Actions</th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-gray-200"> <tbody>
<tr v-for="user in users" :key="user.id"> <tr v-for="user in users" :key="user.id">
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{{ user.id }}</td> <td class="px-6 py-4 whitespace-nowrap text-sm">{{ user.id }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{{ user.login }}</td> <td class="px-6 py-4 whitespace-nowrap text-sm">{{ user.login }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ user.ip || '-' }}</td> <td class="px-6 py-4 whitespace-nowrap text-sm">{{ user.email }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ formatDate(user.created) }}</td> <td class="px-6 py-4 whitespace-nowrap text-sm">
<button @click="toggleActive(user)" :class="user.active ? 'text-green-600' : 'text-red-600'">
{{ user.active ? 'Active' : 'Inactive' }}
</button>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm">{{ user.ip || '-' }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm">{{ formatDate(user.created) }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm space-x-2"> <td class="px-6 py-4 whitespace-nowrap text-sm space-x-2">
<button @click="openModal('edit', user)" class="text-blue-600 hover:text-blue-800">Edit</button> <button @click="openModal('edit', user)" class="text-blue-600">Edit</button>
<button @click="deleteUser(user.id)" class="text-red-600 hover:text-red-800">Delete</button> <button @click="deleteUser(user.id)" class="text-red-600">Delete</button>
</td> </td>
</tr> </tr>
</tbody> </tbody>
@@ -36,6 +44,10 @@
<div class="bg-white rounded-lg p-6 w-full max-w-md"> <div class="bg-white rounded-lg p-6 w-full max-w-md">
<h2 class="text-xl font-bold mb-4">{{ modalTitle }}</h2> <h2 class="text-xl font-bold mb-4">{{ modalTitle }}</h2>
<form @submit.prevent="submitUser"> <form @submit.prevent="submitUser">
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700">Email</label>
<input v-model="form.email" type="text" required class="input-field mt-1" />
</div>
<div class="mb-4"> <div class="mb-4">
<label class="block text-sm font-medium text-gray-700">Login</label> <label class="block text-sm font-medium text-gray-700">Login</label>
<input v-model="form.login" type="text" required class="input-field mt-1" /> <input v-model="form.login" type="text" required class="input-field mt-1" />
@@ -75,6 +87,11 @@ function formatDate(dateStr: string) {
return new Date(dateStr).toLocaleString(); return new Date(dateStr).toLocaleString();
} }
async function toggleActive(user: any) {
await fetch(`/api/admin/users/${user.id}/activate?active=${!user.active}`, { method: 'PUT' })
await loadUsers()
}
function openModal(mode: 'create' | 'edit', user: any = null) { function openModal(mode: 'create' | 'edit', user: any = null) {
modalMode.value = mode; modalMode.value = mode;
if (mode === 'create') { if (mode === 'create') {

View File

@@ -67,7 +67,9 @@
<input type="checkbox" class="rounded border-gray-300 text-primary-600 focus:ring-primary-500" /> <input type="checkbox" class="rounded border-gray-300 text-primary-600 focus:ring-primary-500" />
<span class="ml-2 text-sm text-gray-600">Remember me</span> <span class="ml-2 text-sm text-gray-600">Remember me</span>
</label> </label>
<a href="#" class="text-sm text-primary-600 hover:text-primary-700">Forgot password?</a> <router-link to="/register" class="text-sm text-primary-600 hover:text-primary-700">
Create account
</router-link>
</div> </div>
<button <button

View File

@@ -0,0 +1,72 @@
<template>
<div class="min-h-screen flex items-center justify-center bg-gradient-to-br from-primary-50 via-white to-primary-50">
<div class="w-full max-w-md">
<div class="text-center mb-8">
<div class="inline-flex items-center justify-center w-16 h-16 bg-gradient-to-br from-primary-500 to-primary-700 rounded-xl mb-4">
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z" />
</svg>
</div>
<h1 class="text-3xl font-bold text-gray-900">Create Account</h1>
<p class="text-gray-600 mt-2">Register and wait for admin approval</p>
</div>
<div class="bg-white rounded-2xl shadow-xl p-8">
<form @submit.prevent="handleRegister" class="space-y-6">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Username</label>
<input v-model="form.login" type="text" required minlength="3" class="input-field" />
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Email</label>
<input v-model="form.email" type="email" required class="input-field" />
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Password</label>
<input v-model="form.password" type="password" required minlength="6" class="input-field" />
</div>
<button type="submit" :disabled="loading" class="w-full btn-primary py-3">
<span v-if="!loading">Register</span>
<span v-else>Loading...</span>
</button>
</form>
<p v-if="success" class="mt-4 text-green-600 text-center">Account created! Wait for admin activation.</p>
<p v-if="error" class="mt-4 text-red-600 text-center">{{ error }}</p>
<p class="mt-4 text-center text-sm text-gray-600">
Already have an account? <router-link to="/login" class="text-primary-600">Login</router-link>
</p>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const form = ref({ login: '', email: '', password: '' })
const loading = ref(false)
const error = ref('')
const success = ref(false)
async function handleRegister() {
loading.value = true
error.value = ''
success.value = false
try {
const res = await fetch('/api/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(form.value)
})
if (res.ok) {
success.value = true
form.value = { login: '', email: '', password: '' }
} else {
const data = await res.json()
error.value = data.error || 'Registration failed'
}
} catch (e) {
error.value = 'Network error'
} finally {
loading.value = false
}
}
</script>

View File

@@ -51,6 +51,26 @@
<p v-if="validation.login" class="mt-1 text-xs text-red-600">{{ validation.login }}</p> <p v-if="validation.login" class="mt-1 text-xs text-red-600">{{ validation.login }}</p>
</div> </div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Email</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg class="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 12a4 4 0 10-8 0 4 4 0 008 0zm0 0v1.5a2.5 2.5 0 005 0V12a9 9 0 10-9 9m4.5-1.206a8.959 8.959 0 01-4.5 1.207" />
</svg>
</div>
<input
v-model="form.email"
type="email"
required
class="input-field pl-10"
:class="{ 'border-red-300 focus:ring-red-500': validation.email }"
placeholder="admin@example.com"
/>
</div>
<p v-if="validation.email" class="mt-1 text-xs text-red-600">{{ validation.email }}</p>
</div>
<div> <div>
<label class="block text-sm font-medium text-gray-700 mb-2">Password</label> <label class="block text-sm font-medium text-gray-700 mb-2">Password</label>
<div class="relative"> <div class="relative">
@@ -133,21 +153,24 @@ import { ref, computed, watch } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
const router = useRouter() const router = useRouter()
const form = ref({ login: '', password: '' }) const form = ref({ login: '', email: '', password: '' });
const loading = ref(false) const loading = ref(false)
const error = ref('') const error = ref('')
const showPassword = ref(false) const showPassword = ref(false)
const validation = computed(() => { const validation = computed(() => {
const errors: any = {} const errors: any = {};
if (form.value.login && form.value.login.length < 3) { if (form.value.login && form.value.login.length < 3) {
errors.login = 'Username must be at least 3 characters' errors.login = 'Username must be at least 3 characters';
}
if (form.value.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.value.email)) {
errors.email = 'Please enter a valid email address';
} }
if (form.value.password && form.value.password.length < 6) { if (form.value.password && form.value.password.length < 6) {
errors.password = 'Password must be at least 6 characters' errors.password = 'Password must be at least 6 characters';
} }
return errors return errors;
}) });
const passwordStrength = computed(() => { const passwordStrength = computed(() => {
const pwd = form.value.password const pwd = form.value.password
@@ -178,29 +201,33 @@ const strengthBarColor = computed(() => {
}) })
async function handleSetup() { async function handleSetup() {
if (Object.keys(validation.value).length > 0) return if (Object.keys(validation.value).length > 0) return;
loading.value = true loading.value = true;
error.value = '' error.value = '';
try { try {
const res = await fetch('/api/setup', { const res = await fetch('/api/setup', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(form.value) body: JSON.stringify({
login: form.value.login,
email: form.value.email,
password: form.value.password
}) })
});
const data = await res.json() const data = await res.json();
if (res.ok) { if (res.ok) {
router.push('/login') router.push('/login');
} else { } else {
error.value = data.error || 'Failed to create account' error.value = data.error || 'Failed to create account';
} }
} catch (e) { } catch (e) {
error.value = 'Network error. Please try again.' error.value = 'Network error. Please try again.';
} finally { } finally {
loading.value = false loading.value = false;
} }
} }
</script> </script>

View File

@@ -4,6 +4,7 @@ import io.vertx.config.ConfigRetriever;
import io.vertx.config.ConfigRetrieverOptions; import io.vertx.config.ConfigRetrieverOptions;
import io.vertx.config.ConfigStoreOptions; import io.vertx.config.ConfigStoreOptions;
import io.vertx.core.AbstractVerticle; import io.vertx.core.AbstractVerticle;
import io.vertx.core.Future;
import io.vertx.core.Promise; import io.vertx.core.Promise;
import io.vertx.core.http.HttpServer; import io.vertx.core.http.HttpServer;
import io.vertx.core.json.JsonObject; import io.vertx.core.json.JsonObject;
@@ -14,6 +15,7 @@ import io.vertx.ext.web.handler.StaticHandler;
import io.vertx.ext.web.sstore.LocalSessionStore; import io.vertx.ext.web.sstore.LocalSessionStore;
import io.vertx.ext.web.sstore.SessionStore; import io.vertx.ext.web.sstore.SessionStore;
import io.vertx.ext.web.sstore.redis.RedisSessionStore;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import su.xserver.iikocon.config.AppConfig; import su.xserver.iikocon.config.AppConfig;
@@ -21,6 +23,10 @@ import su.xserver.iikocon.service.DataBaseService;
import su.xserver.iikocon.service.HealthCheckService; import su.xserver.iikocon.service.HealthCheckService;
import su.xserver.iikocon.service.RedisService; import su.xserver.iikocon.service.RedisService;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
public class MainVerticle extends AbstractVerticle { public class MainVerticle extends AbstractVerticle {
private final Logger log = LoggerFactory.getLogger("[MainVerticle]"); private final Logger log = LoggerFactory.getLogger("[MainVerticle]");
@@ -32,6 +38,7 @@ public class MainVerticle extends AbstractVerticle {
private UserService userService; private UserService userService;
private RestaurantService restaurantService; private RestaurantService restaurantService;
private SettingsService settingsService;
@Override @Override
public void start(Promise<Void> startPromise) { public void start(Promise<Void> startPromise) {
@@ -57,18 +64,18 @@ public class MainVerticle extends AbstractVerticle {
// Инициализация сервисов // Инициализация сервисов
userService = new UserService(db.getPool()); userService = new UserService(db.getPool());
restaurantService = new RestaurantService(db.getPool()); restaurantService = new RestaurantService(db.getPool());
settingsService = new SettingsService(db.getPool());
// Инициализация БД (создание таблицы users) // Инициализация БД (создание таблицы users)
userService.initDatabase() userService.initDatabase().onFailure(err -> {
.onSuccess(v -> log.info("Database initialized successfully"))
.onFailure(err -> {
log.error("Failed to initialize database", err); log.error("Failed to initialize database", err);
startPromise.fail(err); startPromise.fail(err);
}); });
restaurantService.initDatabase().onFailure(err -> {
restaurantService.initDatabase() log.error("Failed to initialize database", err);
.onSuccess(v -> log.info("Database initialized successfully")) startPromise.fail(err);
.onFailure(err -> { });
settingsService.initDatabase().onFailure(err -> {
log.error("Failed to initialize database", err); log.error("Failed to initialize database", err);
startPromise.fail(err); startPromise.fail(err);
}); });
@@ -90,7 +97,7 @@ public class MainVerticle extends AbstractVerticle {
.setSessionCookieName("admin.session") .setSessionCookieName("admin.session")
.setCookieHttpOnlyFlag(true) .setCookieHttpOnlyFlag(true)
.setCookieSecureFlag(false) .setCookieSecureFlag(false)
.setSessionTimeout(3600000); // 1 час .setSessionTimeout(3600000);
// Роутер // Роутер
Router router = Router.router(vertx); Router router = Router.router(vertx);
@@ -144,6 +151,21 @@ public class MainVerticle extends AbstractVerticle {
router.post("/api/logout").handler(authHandler::handleLogout); router.post("/api/logout").handler(authHandler::handleLogout);
router.post("/api/register").handler(rc -> {
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()));
});
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 -> { router.get("/api/admin/users").handler(rc -> userService.getAllUsers().onComplete(ar -> {
@@ -159,13 +181,14 @@ public class MainVerticle extends AbstractVerticle {
router.post("/api/admin/users").handler(rc -> { router.post("/api/admin/users").handler(rc -> {
JsonObject body = rc.body().asJsonObject(); JsonObject body = rc.body().asJsonObject();
String login = body.getString("login"); String login = body.getString("login");
String email = body.getString("email");
String password = body.getString("password"); String password = body.getString("password");
String ip = rc.request().remoteAddress().host(); String ip = rc.request().remoteAddress().host();
if (login == null || password == null) { if (login == null || email == null || password == null) {
rc.response().setStatusCode(400).end("Missing login or password"); rc.response().setStatusCode(400).end("Missing login, email or password");
return; return;
} }
userService.createUser(login, password, ip) userService.createUser(login, email, password, ip, true)
.onSuccess(v -> rc.response().setStatusCode(201).end()) .onSuccess(v -> rc.response().setStatusCode(201).end())
.onFailure(err -> rc.response().setStatusCode(500).end(err.getMessage())); .onFailure(err -> rc.response().setStatusCode(500).end(err.getMessage()));
}); });
@@ -174,13 +197,14 @@ public class MainVerticle extends AbstractVerticle {
int id = Integer.parseInt(rc.pathParam("id")); int id = Integer.parseInt(rc.pathParam("id"));
JsonObject body = rc.body().asJsonObject(); JsonObject body = rc.body().asJsonObject();
String login = body.getString("login"); String login = body.getString("login");
String email = body.getString("email");
String password = body.getString("password"); String password = body.getString("password");
String ip = rc.request().remoteAddress().host(); String ip = rc.request().remoteAddress().host();
if (login == null) { if (login == null || email == null) {
rc.response().setStatusCode(400).end("Missing login"); rc.response().setStatusCode(400).end("Missing login or email");
return; return;
} }
userService.updateUser(id, login, password, ip) userService.updateUser(id, login, email, password, ip)
.onSuccess(v -> rc.response().end()) .onSuccess(v -> rc.response().end())
.onFailure(err -> rc.response().setStatusCode(500).end(err.getMessage())); .onFailure(err -> rc.response().setStatusCode(500).end(err.getMessage()));
}); });
@@ -192,6 +216,14 @@ public class MainVerticle extends AbstractVerticle {
.onFailure(err -> rc.response().setStatusCode(500).end(err.getMessage())); .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").get(0));
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 -> { router.get("/api/admin/me").handler(rc -> {
Integer userId = rc.session().get("userId"); Integer userId = rc.session().get("userId");
@@ -265,6 +297,29 @@ public class MainVerticle extends AbstractVerticle {
.onFailure(err -> rc.response().setStatusCode(500).end(err.getMessage())); .onFailure(err -> rc.response().setStatusCode(500).end(err.getMessage()));
}); });
// Получение всех настроек
router.get("/api/settings").handler(rc -> {
settingsService.getAll()
.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()));
});
// Количество активных сессий (на основе Redis)
router.get("/api/admin/active-sessions").handler(rc -> {
// TODO: реализовать подсчёт активных сессий через Redis или другой механизм
rc.response().end(new JsonObject().put("count", 0).encode());
});
return router; return router;
} }

View File

@@ -23,7 +23,7 @@ public class RestaurantService {
CREATE TABLE IF NOT EXISTS restaurants ( CREATE TABLE IF NOT EXISTS restaurants (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) UNIQUE NOT NULL, name VARCHAR(255) UNIQUE NOT NULL,
login VARCHAR(255) UNIQUE NOT NULL, login VARCHAR(255) NOT NULL,
password VARCHAR(255) NOT NULL, password VARCHAR(255) NOT NULL,
host VARCHAR(255) NOT NULL, host VARCHAR(255) NOT NULL,
created TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,

View File

@@ -0,0 +1,67 @@
package su.xserver.iikocon;
import io.vertx.core.Future;
import io.vertx.core.json.JsonObject;
import io.vertx.sqlclient.Pool;
import io.vertx.sqlclient.templates.SqlTemplate;
import java.util.Map;
public class SettingsService {
private final Pool pool;
public SettingsService(Pool pool) { this.pool = pool; }
public Future<Void> initDatabase() {
String createTable = """
CREATE TABLE IF NOT EXISTS app_settings (
setting_key VARCHAR(255) PRIMARY KEY,
setting_value TEXT
)
""";
return pool.query(createTable).execute()
.compose(v -> setIfAbsent("site_name", "Admin Panel"))
.compose(v -> setIfAbsent("site_description", "Powerful administration dashboard"))
.compose(v -> setIfAbsent("theme", "light"))
.compose(v -> setIfAbsent("items_per_page", "20"))
.compose(v -> setIfAbsent("enable_registration", "true"))
.compose(v -> setIfAbsent("maintenance_mode", "false"))
.compose(v -> setIfAbsent("default_language", "en"))
.compose(v -> setIfAbsent("session_timeout_minutes", "60"))
.compose(v -> setIfAbsent("logo_url", "/assets/logo.png"))
.mapEmpty();
}
private Future<Void> setIfAbsent(String key, String defaultValue) {
return get(key).compose(existing -> {
if (existing == null) {
return set(key, defaultValue);
}
return Future.succeededFuture();
});
}
public Future<String> get(String key) {
return SqlTemplate.forQuery(pool, "SELECT setting_value FROM app_settings WHERE setting_key = #{key}")
.execute(Map.of("key", key))
.map(rows -> rows.iterator().hasNext() ? rows.iterator().next().getString("setting_value") : null);
}
public Future<Void> set(String key, String value) {
return SqlTemplate.forUpdate(pool,
"INSERT INTO app_settings (setting_key, setting_value) VALUES (#{key}, #{value}) " +
"ON DUPLICATE KEY UPDATE setting_value = #{value}")
.execute(Map.of("key", key, "value", value))
.mapEmpty();
}
public Future<JsonObject> getAll() {
return pool.query("SELECT setting_key, setting_value FROM app_settings")
.execute()
.map(rows -> {
JsonObject json = new JsonObject();
rows.forEach(row -> json.put(row.getString("setting_key"), row.getString("setting_value")));
return json;
});
}
}

View File

@@ -37,6 +37,11 @@ public class SetupHandler {
String login = body.getString("login"); String login = body.getString("login");
String password = body.getString("password"); String password = body.getString("password");
String email = body.getString("email");
if (email == null || email.isBlank()) {
email = login + "@admin.local"; // значение по умолчанию
}
if (login == null || password == null || login.length() < 3 || password.length() < 6) { if (login == null || password == null || login.length() < 3 || password.length() < 6) {
ctx.response().setStatusCode(400) ctx.response().setStatusCode(400)
@@ -47,7 +52,7 @@ public class SetupHandler {
} }
String ip = ctx.request().remoteAddress().host(); String ip = ctx.request().remoteAddress().host();
userService.createUser(login, password, ip).onComplete(cr -> { userService.createUser(login, email, password, ip, true).onComplete(cr -> {
if (cr.succeeded()) { if (cr.succeeded()) {
ctx.response().setStatusCode(201) ctx.response().setStatusCode(201)
.end(new JsonObject().put("success", true).encode()); .end(new JsonObject().put("success", true).encode());

View File

@@ -24,13 +24,14 @@ public class UserService {
CREATE TABLE IF NOT EXISTS users ( CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
login VARCHAR(255) UNIQUE NOT NULL, login VARCHAR(255) UNIQUE NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL, password VARCHAR(255) NOT NULL,
active BOOLEAN DEFAULT FALSE,
ip VARCHAR(45),
created TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
ip VARCHAR(45)
) )
"""; """;
return pool.query(createTable).execute().mapEmpty(); return pool.query(createTable).execute().mapEmpty();
} }
@@ -40,20 +41,38 @@ public class UserService {
.map(rows -> rows.iterator().next().getLong("cnt")); .map(rows -> rows.iterator().next().getLong("cnt"));
} }
public Future<Void> createUser(String login, String password, String ip) { public Future<Void> createUser(String login, String email, String password, String ip, boolean active) {
String hash = BCrypt.hashpw(password, BCrypt.gensalt()); String hash = BCrypt.hashpw(password, BCrypt.gensalt());
Map<String, Object> params = Map.of(
Map<String, Object> params = new HashMap<>(); "login", login,
params.put("login", login); "email", email,
params.put("password", hash); "password", hash,
params.put("ip", ip); "ip", ip,
"active", active
);
return SqlTemplate.forUpdate(pool, return SqlTemplate.forUpdate(pool,
"INSERT INTO users (login, password, ip) VALUES (#{login}, #{password}, #{ip})") "INSERT INTO users (login, email, password, ip, active) VALUES (#{login}, #{email}, #{password}, #{ip}, #{active})")
.execute(params) .execute(params)
.mapEmpty(); .mapEmpty();
} }
// Существующий метод оставляем, но он будет создавать неактивного пользователя (active = false)
public Future<Void> createUser(String login, String email, String password, String ip) {
return createUser(login, email, password, ip, false);
}
public Future<Void> setActive(int id, boolean active) {
return SqlTemplate.forUpdate(pool, "UPDATE users SET active = #{active} WHERE id = #{id}")
.execute(Map.of("id", id, "active", active)).mapEmpty();
}
public Future<JsonObject> findByEmail(String email) {
return SqlTemplate.forQuery(pool, "SELECT id, login, email, password, active, ip, created, updated FROM users WHERE email = #{email}")
.mapTo(this::toJson)
.execute(Map.of("email", email))
.map(rows -> rows.iterator().hasNext() ? rows.iterator().next() : null);
}
public Future<JsonObject> findByLogin(String login) { public Future<JsonObject> findByLogin(String login) {
return SqlTemplate.forQuery(pool, return SqlTemplate.forQuery(pool,
"SELECT id, login, password, created, updated, ip FROM users WHERE login = #{login}") "SELECT id, login, password, created, updated, ip FROM users WHERE login = #{login}")
@@ -71,42 +90,30 @@ public class UserService {
} }
public Future<JsonArray> getAllUsers() { public Future<JsonArray> getAllUsers() {
return pool.query("SELECT id, login, created, updated, ip FROM users ORDER BY id") return pool.query("SELECT id, login, email, active, ip, created, updated FROM users ORDER BY id")
.execute() .execute()
.map(rows -> { .map(rows -> {
JsonArray array = new JsonArray(); JsonArray array = new JsonArray();
for (Row row : rows) { rows.forEach(row -> array.add(toJson(row)));
array.add(new JsonObject()
.put("id", row.getInteger("id"))
.put("login", row.getString("login"))
.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; return array;
}); });
} }
public Future<Void> updateUser(int id, String login, String password, String ip) { public Future<Void> updateUser(int id, String login, String email, String password, String ip) {
Map<String, Object> params = new HashMap<>(); Map<String, Object> params = new HashMap<>();
params.put("id", id); params.put("id", id);
params.put("login", login); params.put("login", login);
params.put("email", email);
params.put("ip", ip); params.put("ip", ip);
String sql; String sql;
if (password != null && !password.isEmpty()) { if (password != null && !password.isEmpty()) {
String hash = BCrypt.hashpw(password, BCrypt.gensalt()); String hash = BCrypt.hashpw(password, BCrypt.gensalt());
params.put("password", hash); params.put("password", hash);
sql = "UPDATE users SET login = #{login}, password = #{password}, ip = #{ip} WHERE id = #{id}"; sql = "UPDATE users SET login = #{login}, email = #{email}, password = #{password}, ip = #{ip} WHERE id = #{id}";
} else { } else {
sql = "UPDATE users SET login = #{login}, ip = #{ip} WHERE id = #{id}"; sql = "UPDATE users SET login = #{login}, email = #{email}, ip = #{ip} WHERE id = #{id}";
} }
return SqlTemplate.forUpdate(pool, sql).execute(params).mapEmpty();
return SqlTemplate.forUpdate(pool, sql)
.execute(params)
.mapEmpty();
} }
public Future<Void> deleteUser(int id) { public Future<Void> deleteUser(int id) {
@@ -122,4 +129,15 @@ public class UserService {
return false; return false;
} }
} }
private JsonObject toJson(Row row) {
return new JsonObject()
.put("id", row.getInteger("id"))
.put("login", row.getString("login"))
.put("email", row.getString("email"))
.put("active", row.getBoolean("active"))
.put("ip", row.getString("ip"))
.put("created", row.getLocalDateTime("created") != null ? row.getLocalDateTime("created").toString() : null)
.put("updated", row.getLocalDateTime("updated") != null ? row.getLocalDateTime("updated").toString() : null);
}
} }

View File

@@ -31,7 +31,7 @@ public class HealthCheckService {
long time = System.currentTimeMillis() - start; long time = System.currentTimeMillis() - start;
if ("PONG".equalsIgnoreCase(response.toString())) { if ("PONG".equalsIgnoreCase(response.toString())) {
JsonObject data = new JsonObject() JsonObject data = new JsonObject()
.put("name", "redis") .put("name", "Redis")
.put("latency_ms", time); .put("latency_ms", time);
future.complete(Status.OK(data)); future.complete(Status.OK(data));
} else { } else {
@@ -42,7 +42,7 @@ public class HealthCheckService {
}); });
// Database check // Database check
healthCheckHandler.register("database", future -> { healthCheckHandler.register("DataBase", future -> {
long start = System.currentTimeMillis(); long start = System.currentTimeMillis();
dbService.getPool().query("SELECT 1").execute() dbService.getPool().query("SELECT 1").execute()
.onSuccess(rs -> { .onSuccess(rs -> {