This commit is contained in:
2026-04-18 11:33:21 +03:00
parent c4e113a494
commit af757ff224
11 changed files with 753 additions and 27 deletions

View File

@@ -39,6 +39,17 @@
Users Users
</router-link> </router-link>
<router-link
to="/restaurants"
class="flex items-center px-4 py-3 text-gray-700 rounded-lg hover:bg-gray-100 transition-colors"
:class="{ 'bg-primary-50 text-primary-700': $route.path === '/restaurants' }"
>
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
</svg>
Restaurants
</router-link>
<router-link <router-link
to="/settings" to="/settings"
class="flex items-center px-4 py-3 text-gray-700 rounded-lg hover:bg-gray-100 transition-colors" class="flex items-center px-4 py-3 text-gray-700 rounded-lg hover:bg-gray-100 transition-colors"
@@ -111,28 +122,28 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {computed, ref} from 'vue' import { computed, ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const userName = ref('Loading...')
const userLogin = ref('')
const userName = ref('Admin User') onMounted(async () => {
const userInitials = computed(() => { try {
return userName.value.split(' ').map(n => n[0]).join('').toUpperCase() const res = await fetch('/api/admin/me')
}) if (res.ok) {
const data = await res.json()
const pageTitle = computed(() => { userLogin.value = data.login
const titles: Record<string, string> = { userName.value = data.login // или можно сделать красивое отображение
'/dashboard': 'Dashboard', }
'/users': 'Users Management', } catch (e) {
'/settings': 'Settings' userName.value = 'User'
} }
return titles[route.path] || 'Admin Panel'
}) })
async function logout() { const userInitials = computed(() => {
await fetch('/api/logout', { method: 'POST' }) return (userName.value[0] || 'U').toUpperCase()
router.push('/login') })
}
</script> </script>

View File

@@ -2,19 +2,29 @@ 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 Dashboard from '../views/Dashboard.vue' import Dashboard from '../views/Dashboard.vue'
import Users from '../views/Users.vue'
import Restaurants from '../views/Restaurants.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: '/',
redirect: '/dashboard'
},
{ {
path: '/dashboard', path: '/dashboard',
component: Dashboard, component: Dashboard,
meta: { requiresAuth: true, title: 'Dashboard' } meta: { requiresAuth: true, title: 'Dashboard' }
}, },
{ { path: '/users',
path: '/', component: Users,
redirect: '/dashboard' meta: { requiresAuth: true, title: 'Users' }
},
{ path: '/restaurants',
component: Restaurants,
meta: { requiresAuth: true, title: 'Restaurants' }
}, },
{ {
path: '/:pathMatch(.*)*', path: '/:pathMatch(.*)*',
@@ -46,6 +56,18 @@ router.beforeEach(async (to, from, next) => {
console.error('Failed to check status', e) console.error('Failed to check status', e)
} }
if (to.path === '/login') {
try {
const meRes = await fetch('/api/admin/me');
if (meRes.ok) {
next('/dashboard');
return;
}
} catch (e) {
// игнорируем ошибку, продолжаем
}
}
// Check authentication // Check authentication
const requiresAuth = to.matched.some(record => record.meta.requiresAuth) const requiresAuth = to.matched.some(record => record.meta.requiresAuth)

View File

@@ -103,7 +103,7 @@
<div class="space-y-4"> <div class="space-y-4">
<div v-for="service in systemServices" :key="service.name" class="flex items-center justify-between"> <div v-for="service in systemServices" :key="service.name" class="flex items-center justify-between">
<div class="flex items-center space-x-3"> <div class="flex items-center space-x-3">
<div :class="['w-2 h-2 rounded-full', service.status === 'healthy' ? 'bg-green-500' : 'bg-red-500']"></div> <div :class="['w-2 h-2 rounded-full', service.status === 'up' ? 'bg-green-500' : 'bg-red-500']"></div>
<span class="text-gray-700">{{ service.name }}</span> <span class="text-gray-700">{{ service.name }}</span>
</div> </div>
<span class="text-sm text-gray-500">{{ service.latency }}ms</span> <span class="text-sm text-gray-500">{{ service.latency }}ms</span>
@@ -115,7 +115,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } 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({
@@ -126,12 +126,34 @@ const stats = ref({
}) })
const recentUsers = ref([]) const recentUsers = ref([])
const systemServices = ref([ const systemServices = ref([])
{ name: 'Database', status: 'healthy', latency: 12 },
{ name: 'Redis Cache', status: 'healthy', latency: 3 }, async function loadHealth() {
{ name: 'API Gateway', status: 'healthy', latency: 45 }, try {
{ name: 'File Storage', status: 'healthy', latency: 28 } const res = await fetch('/api/health')
]) const data = await res.json()
if (data.checks) {
systemServices.value = data.checks.map(check => ({
name: check.data?.name || check.id,
status: check.status.toLowerCase(),
latency: check.data?.latency_ms || 0
}))
}
} catch (e) {
console.error('Health check failed', e)
}
}
let interval: number
onMounted(async () => {
await loadData()
await loadHealth()
interval = window.setInterval(loadHealth, 5000)
})
onUnmounted(() => {
if (interval) clearInterval(interval)
})
onMounted(async () => { onMounted(async () => {
await loadData() await loadData()

View File

@@ -0,0 +1,140 @@
<template>
<AppLayout>
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-900">Restaurants</h1>
<button @click="openModal('create')" class="btn-primary">+ Add Restaurant</button>
</div>
<div class="card overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<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">Name</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">Host</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>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
<tr v-for="rest in restaurants" :key="rest.id">
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{{ rest.id }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{{ rest.name }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ rest.login }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ rest.host }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ formatDate(rest.created) }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm space-x-2">
<button @click="openModal('edit', rest)" class="text-blue-600 hover:text-blue-800">Edit</button>
<button @click="deleteRestaurant(rest.id)" class="text-red-600 hover:text-red-800">Delete</button>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Modal -->
<div v-if="modalOpen" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div class="bg-white rounded-lg p-6 w-full max-w-md">
<h2 class="text-xl font-bold mb-4">{{ modalTitle }}</h2>
<form @submit.prevent="submitRestaurant">
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700">Name</label>
<input v-model="form.name" type="text" required class="input-field mt-1" />
</div>
<div class="mb-4">
<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" />
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700">Password</label>
<input v-model="form.password" :required="modalMode === 'create'" type="password" class="input-field mt-1" />
<p v-if="modalMode === 'edit'" class="text-xs text-gray-500 mt-1">Leave blank to keep current password</p>
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700">Host</label>
<input v-model="form.host" type="text" required class="input-field mt-1" />
</div>
<div class="flex justify-end space-x-2">
<button type="button" @click="closeModal" class="btn-secondary">Cancel</button>
<button type="submit" class="btn-primary">Save</button>
</div>
</form>
</div>
</div>
</AppLayout>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import AppLayout from '../components/Layout/AppLayout.vue';
const restaurants = ref([]);
const modalOpen = ref(false);
const modalMode = ref<'create' | 'edit'>('create');
const form = ref({ id: null, name: '', login: '', password: '', host: '' });
const modalTitle = ref('');
async function loadRestaurants() {
const res = await fetch('/api/admin/restaurants');
restaurants.value = await res.json();
}
function formatDate(dateStr: string) {
if (!dateStr) return '-';
return new Date(dateStr).toLocaleString();
}
function openModal(mode: 'create' | 'edit', rest: any = null) {
modalMode.value = mode;
if (mode === 'create') {
form.value = { id: null, name: '', login: '', password: '', host: '' };
modalTitle.value = 'Create Restaurant';
} else {
form.value = { id: rest.id, name: rest.name, login: rest.login, password: '', host: rest.host };
modalTitle.value = 'Edit Restaurant';
}
modalOpen.value = true;
}
function closeModal() {
modalOpen.value = false;
}
async function submitRestaurant() {
try {
const payload = {
name: form.value.name,
login: form.value.login,
host: form.value.host,
...(form.value.password ? { password: form.value.password } : {})
};
if (modalMode.value === 'create') {
await fetch('/api/admin/restaurants', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
} else {
await fetch(`/api/admin/restaurants/${form.value.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
}
await loadRestaurants();
closeModal();
} catch (e) {
alert('Operation failed');
}
}
async function deleteRestaurant(id: number) {
if (confirm('Are you sure?')) {
await fetch(`/api/admin/restaurants/${id}`, { method: 'DELETE' });
await loadRestaurants();
}
}
onMounted(loadRestaurants);
</script>

View File

@@ -0,0 +1,124 @@
<template>
<AppLayout>
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-900">Users Management</h1>
<button @click="openModal('create')" class="btn-primary">+ Add User</button>
</div>
<div class="card overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<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">Login</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">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
<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 text-gray-900">{{ 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 text-gray-500">{{ formatDate(user.created) }}</td>
<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="deleteUser(user.id)" class="text-red-600 hover:text-red-800">Delete</button>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Modal -->
<div v-if="modalOpen" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div class="bg-white rounded-lg p-6 w-full max-w-md">
<h2 class="text-xl font-bold mb-4">{{ modalTitle }}</h2>
<form @submit.prevent="submitUser">
<div class="mb-4">
<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" />
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700">Password</label>
<input v-model="form.password" :required="modalMode === 'create'" type="password" class="input-field mt-1" />
<p v-if="modalMode === 'edit'" class="text-xs text-gray-500 mt-1">Leave blank to keep current password</p>
</div>
<div class="flex justify-end space-x-2">
<button type="button" @click="closeModal" class="btn-secondary">Cancel</button>
<button type="submit" class="btn-primary">Save</button>
</div>
</form>
</div>
</div>
</AppLayout>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import AppLayout from '../components/Layout/AppLayout.vue';
const users = ref([]);
const modalOpen = ref(false);
const modalMode = ref<'create' | 'edit'>('create');
const form = ref({ id: null, login: '', password: '' });
const modalTitle = ref('');
async function loadUsers() {
const res = await fetch('/api/admin/users');
users.value = await res.json();
}
function formatDate(dateStr: string) {
if (!dateStr) return '-';
return new Date(dateStr).toLocaleString();
}
function openModal(mode: 'create' | 'edit', user: any = null) {
modalMode.value = mode;
if (mode === 'create') {
form.value = { id: null, login: '', password: '' };
modalTitle.value = 'Create User';
} else {
form.value = { id: user.id, login: user.login, password: '' };
modalTitle.value = 'Edit User';
}
modalOpen.value = true;
}
function closeModal() {
modalOpen.value = false;
}
async function submitUser() {
try {
if (modalMode.value === 'create') {
await fetch('/api/admin/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ login: form.value.login, password: form.value.password })
});
} else {
await fetch(`/api/admin/users/${form.value.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ login: form.value.login, password: form.value.password || undefined })
});
}
await loadUsers();
closeModal();
} catch (e) {
alert('Operation failed');
}
}
async function deleteUser(id: number) {
if (confirm('Are you sure?')) {
await fetch(`/api/admin/users/${id}`, { method: 'DELETE' });
await loadUsers();
}
}
onMounted(loadUsers);
</script>

View File

@@ -0,0 +1,50 @@
package su.xserver.iikocon;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
public class DateRangeSetup {
public static void main(String[] args) {
// Параметры по умолчанию
String login = "4444";
String password = "4444";
String server = "folk-amber-co.iiko.it";
String presetId = "7ddc40c3-9d5f-408f-aa1e-652964b36c6c";
// Вычисление dateFrom и dateTo
LocalDate today = LocalDate.now();
LocalDate dateFrom = today.minusDays(7);
LocalDate dateTo = today;
// Переопределение из аргументов командной строки
if (args.length > 0 && args[0] != null && !args[0].isEmpty()) {
try {
dateFrom = LocalDate.parse(args[0]);
} catch (DateTimeParseException e) {
System.err.println("Ошибка парсинга dateFrom: " + args[0] + ". Используется значение по умолчанию.");
}
}
if (args.length > 1 && args[1] != null && !args[1].isEmpty()) {
try {
dateTo = LocalDate.parse(args[1]);
} catch (DateTimeParseException e) {
System.err.println("Ошибка парсинга dateTo: " + args[1] + ". Используется значение по умолчанию.");
}
}
// Форматирование дат в YYYY-MM-DD
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
String formattedDateFrom = dateFrom.format(formatter);
String formattedDateTo = dateTo.format(formatter);
// Вывод переменных (можно заменить на дальнейшее использование)
System.out.println("login=" + login);
System.out.println("password=" + password);
System.out.println("server=" + server);
System.out.println("presetId=" + presetId);
System.out.println("dateFrom=" + formattedDateFrom);
System.out.println("dateTo=" + formattedDateTo);
}
}

View File

@@ -156,6 +156,42 @@ public class MainVerticle extends AbstractVerticle {
} }
})); }));
router.post("/api/admin/users").handler(rc -> {
JsonObject body = rc.body().asJsonObject();
String login = body.getString("login");
String password = body.getString("password");
String ip = rc.request().remoteAddress().host();
if (login == null || password == null) {
rc.response().setStatusCode(400).end("Missing login or password");
return;
}
userService.createUser(login, password, ip)
.onSuccess(v -> rc.response().setStatusCode(201).end())
.onFailure(err -> rc.response().setStatusCode(500).end(err.getMessage()));
});
router.put("/api/admin/users/:id").handler(rc -> {
int id = Integer.parseInt(rc.pathParam("id"));
JsonObject body = rc.body().asJsonObject();
String login = body.getString("login");
String password = body.getString("password");
String ip = rc.request().remoteAddress().host();
if (login == null) {
rc.response().setStatusCode(400).end("Missing login");
return;
}
userService.updateUser(id, login, password, ip)
.onSuccess(v -> rc.response().end())
.onFailure(err -> rc.response().setStatusCode(500).end(err.getMessage()));
});
router.delete("/api/admin/users/:id").handler(rc -> {
int id = Integer.parseInt(rc.pathParam("id"));
userService.deleteUser(id)
.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");
@@ -171,6 +207,64 @@ public class MainVerticle extends AbstractVerticle {
} }
}); });
router.get("/api/admin/restaurants").handler(rc -> restaurantService.getAllRestaurants().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());
}
}));
router.get("/api/admin/restaurants/:id").handler(rc -> {
int id = Integer.parseInt(rc.pathParam("id"));
restaurantService.findById(id)
.onSuccess(rest -> {
if (rest == null) rc.response().setStatusCode(404).end();
else rc.response().putHeader("Content-Type", "application/json").end(rest.encode());
})
.onFailure(err -> rc.response().setStatusCode(500).end(err.getMessage()));
});
router.post("/api/admin/restaurants").handler(rc -> {
JsonObject body = rc.body().asJsonObject();
String name = body.getString("name");
String login = body.getString("login");
String password = body.getString("password");
String host = body.getString("host");
if (name == null || login == null || password == null || host == null) {
rc.response().setStatusCode(400).end("Missing fields");
return;
}
restaurantService.createRestaurant(name, login, password, host)
.onSuccess(v -> rc.response().setStatusCode(201).end())
.onFailure(err -> rc.response().setStatusCode(500).end(err.getMessage()));
});
router.put("/api/admin/restaurants/:id").handler(rc -> {
int id = Integer.parseInt(rc.pathParam("id"));
JsonObject body = rc.body().asJsonObject();
String name = body.getString("name");
String login = body.getString("login");
String password = body.getString("password");
String host = body.getString("host");
if (name == null || login == null || host == null) {
rc.response().setStatusCode(400).end("Missing required fields");
return;
}
restaurantService.updateRestaurant(id, name, login, password, host)
.onSuccess(v -> rc.response().end())
.onFailure(err -> rc.response().setStatusCode(500).end(err.getMessage()));
});
router.delete("/api/admin/restaurants/:id").handler(rc -> {
int id = Integer.parseInt(rc.pathParam("id"));
restaurantService.deleteRestaurant(id)
.onSuccess(v -> rc.response().end())
.onFailure(err -> rc.response().setStatusCode(500).end(err.getMessage()));
});
return router; return router;
} }

View File

@@ -0,0 +1,194 @@
package su.xserver.iikocon;
import io.vertx.core.AbstractVerticle;
import io.vertx.core.Promise;
import io.vertx.core.http.HttpMethod;
import io.vertx.core.json.Json;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.client.WebClient;
import io.vertx.ext.web.client.WebClientOptions;
import io.vertx.ext.web.codec.BodyCodec;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HexFormat;
public class ProxyVerticle extends AbstractVerticle {
private WebClient webClient;
@Override
public void start(Promise<Void> startPromise) {
webClient = WebClient.create(vertx, new WebClientOptions()
.setSsl(true)
.setTrustAll(true)
.setVerifyHost(false));
Router router = Router.router(vertx);
router.post("/api/proxy").handler(this::handlePost);
router.get("/api/proxy").handler(this::handleGet);
int port = 8080;
vertx.createHttpServer()
.requestHandler(router)
.listen(port).onComplete(http -> {
if (http.succeeded()) {
System.out.println("Proxy server started on port " + port);
startPromise.complete();
} else {
startPromise.fail(http.cause());
}
});
}
private void handlePost(RoutingContext ctx) {
String apiServer = System.getenv("IIKO_API_SERVER");
String apiLogin = System.getenv("IIKO_API_LOGIN");
String apiPass = System.getenv("IIKO_API_PASS");
String externalEndpoint = System.getenv("IIKO_API_ENDPOINT");
if (externalEndpoint == null || externalEndpoint.isBlank()) {
externalEndpoint = "/your-endpoint";
}
if (apiServer == null || apiLogin == null || apiPass == null) {
fail(ctx, 500, "Missing required environment variables: IIKO_API_SERVER, IIKO_API_LOGIN, IIKO_API_PASS");
return;
}
JsonObject body = ctx.body().asJsonObject();
if (body == null) {
fail(ctx, 400, "Request body must be JSON");
return;
}
String signature = sha1(apiPass);
String authUrl = "https://" + apiServer + ":443/resto/api/auth?login=" + apiLogin + "&pass=" + signature;
String finalExternalEndpoint = externalEndpoint;
webClient.getAbs(authUrl)
.as(BodyCodec.string())
.send()
.onSuccess(authResp -> {
if (authResp.statusCode() != 200) {
fail(ctx, authResp.statusCode(), "Authentication failed: " + authResp.statusMessage());
return;
}
String token = authResp.body();
String targetUrl = "https://" + apiServer + finalExternalEndpoint;
webClient.request(HttpMethod.POST, targetUrl)
.putHeader("Content-Type", "application/json")
.as(BodyCodec.jsonObject())
.sendJsonObject(body)
.onSuccess(apiResp -> {
webClient.getAbs("https://" + apiServer + ":443/resto/api/logout?key=" + token)
.send()
.onFailure(err -> System.err.println("Logout failed: " + err.getMessage()));
if (apiResp.statusCode() == 200) {
ctx.response().setStatusCode(200).end(apiResp.body().encode());
} else {
fail(ctx, apiResp.statusCode(), "External API error: " + apiResp.statusMessage());
}
})
.onFailure(err -> fail(ctx, 500, "Request to external API failed: " + err.getMessage()));
})
.onFailure(err -> fail(ctx, 500, "Auth request failed: " + err.getMessage()));
}
private void handleGet(RoutingContext ctx) {
String presetId = ctx.queryParam("presetId").stream().findFirst().orElse(null);
String dateFrom = ctx.queryParam("dateFrom").stream().findFirst().orElse(null);
String dateTo = ctx.queryParam("dateTo").stream().findFirst().orElse(null);
String server = ctx.queryParam("server").stream().findFirst().orElse(null);
String password = ctx.queryParam("password").stream().findFirst().orElse(null);
String login = ctx.queryParam("login").stream().findFirst().orElse(null);
String type = ctx.queryParam("type").stream().findFirst().orElse(null);
String rootType = ctx.queryParam("rootType").stream().findFirst().orElse(null);
if (server == null || login == null || password == null) {
fail(ctx, 400, "Missing required parameters: server, login, password");
return;
}
String signature = sha1(password);
String authUrl = "https://" + server + ":443/resto/api/auth?login=" + login + "&pass=" + signature;
webClient.getAbs(authUrl)
.as(BodyCodec.string())
.send()
.onSuccess(authResp -> {
if (authResp.statusCode() != 200) {
fail(ctx, authResp.statusCode(), "Authentication failed: " + authResp.statusMessage());
return;
}
String token = authResp.body();
String dataUrl;
if ("entity".equals(type)) {
dataUrl = "https://" + server + "/resto/api/v2/entities/list?key=" + token;
if (rootType != null && !rootType.isBlank()) {
dataUrl += "&rootType=" + rootType;
}
} else {
if (presetId == null || dateFrom == null || dateTo == null) {
fail(ctx, 400, "Missing presetId, dateFrom or dateTo for report request");
return;
}
dataUrl = "https://" + server + "/resto/api/v2/reports/olap/byPresetId/" + presetId +
"?key=" + token + "&dateFrom=" + dateFrom + "&dateTo=" + dateTo;
}
System.out.println("URL: " + dataUrl);
webClient.getAbs(dataUrl)
.as(BodyCodec.jsonObject())
.send()
.onSuccess(dataResp -> {
// logout (fire and forget)
webClient.getAbs("https://" + server + ":443/resto/api/logout?key=" + token)
.send()
.onFailure(err -> System.err.println("Logout failed: " + err.getMessage()));
if (dataResp.statusCode() == 200) {
JsonObject responseBody = dataResp.body();
if ("entity".equals(type)) {
ctx.response().setStatusCode(200).end(responseBody.encode());
} else {
Object data = responseBody.getValue("data");
if (data == null) {
ctx.response().setStatusCode(200).end(responseBody.encode());
} else {
// data может быть массивом, объектом или другим типом
ctx.response().setStatusCode(200).end(Json.encode(data));
}
}
} else {
fail(ctx, dataResp.statusCode(), "External API error: " + dataResp.statusMessage());
}
})
.onFailure(err -> fail(ctx, 500, "Data request failed: " + err.getMessage()));
})
.onFailure(err -> fail(ctx, 500, "Auth request failed: " + err.getMessage()));
}
private String sha1(String input) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-1");
byte[] digest = md.digest(input.getBytes());
return HexFormat.of().formatHex(digest).toLowerCase();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
private void fail(RoutingContext ctx, int status, String message) {
System.err.println("Error: " + message);
ctx.response().setStatusCode(status).end(new JsonObject().put("error", message).encode());
}
}
// > GET /api/proxy?server=folk-amber-co.iiko.it&login=4444&password=4444&presetId=7ddc40c3-9d5f-408f-aa1e-652964b36c6c&dateFrom=2026-04-10&dateTo=2026-04-17 HTTP/1.1
// > Host: localhost:8080
// > access-token: ddb4ab653b9194ec1ea5448cee2a8a26282b0866c1d4a86e98e9b0f84bc91944
// > User-Agent: v2raytun/ios
// > X-App-Version: 2.4.3
// > X-Device-Model: iPhone 11 Pro
// > X-Device-OS: iOS
// > X-HWID: HHS8JDJN-F2EB-HFBS-KMWX-234FA7B95JSC
// > X-Ver-OS: 26.0
// > Accept: */*

View File

@@ -90,4 +90,45 @@ public class RestaurantService {
return array; return array;
}); });
} }
public Future<JsonObject> findById(int id) {
return SqlTemplate.forQuery(pool,
"SELECT id, name, login, password, host, created, updated FROM restaurants WHERE id = #{id}")
.mapTo(row -> new JsonObject()
.put("id", row.getInteger("id"))
.put("name", row.getString("name"))
.put("login", row.getString("login"))
.put("password", row.getString("password"))
.put("host", row.getString("host"))
.put("created", row.getLocalDateTime("created") != null ? row.getLocalDateTime("created").toString() : null)
.put("updated", row.getLocalDateTime("updated") != null ? row.getLocalDateTime("updated").toString() : null))
.execute(Collections.singletonMap("id", id))
.map(rows -> rows.iterator().hasNext() ? rows.iterator().next() : null);
}
public Future<Void> updateRestaurant(int id, String name, String login, String password, String host) {
Map<String, Object> params = new HashMap<>();
params.put("id", id);
params.put("name", name);
params.put("login", login);
params.put("host", host);
String sql;
if (password != null && !password.isEmpty()) {
params.put("password", password);
sql = "UPDATE restaurants SET name = #{name}, login = #{login}, password = #{password}, host = #{host} WHERE id = #{id}";
} else {
sql = "UPDATE restaurants SET name = #{name}, login = #{login}, host = #{host} WHERE id = #{id}";
}
return SqlTemplate.forUpdate(pool, sql)
.execute(params)
.mapEmpty();
}
public Future<Void> deleteRestaurant(int id) {
return SqlTemplate.forUpdate(pool, "DELETE FROM restaurants WHERE id = #{id}")
.execute(Collections.singletonMap("id", id))
.mapEmpty();
}
} }

View File

@@ -89,6 +89,32 @@ public class UserService {
}); });
} }
public Future<Void> updateUser(int id, String login, String password, String ip) {
Map<String, Object> params = new HashMap<>();
params.put("id", id);
params.put("login", login);
params.put("ip", ip);
String sql;
if (password != null && !password.isEmpty()) {
String hash = BCrypt.hashpw(password, BCrypt.gensalt());
params.put("password", hash);
sql = "UPDATE users SET login = #{login}, password = #{password}, ip = #{ip} WHERE id = #{id}";
} else {
sql = "UPDATE users SET login = #{login}, ip = #{ip} WHERE id = #{id}";
}
return SqlTemplate.forUpdate(pool, sql)
.execute(params)
.mapEmpty();
}
public Future<Void> deleteUser(int id) {
return SqlTemplate.forUpdate(pool, "DELETE FROM users WHERE id = #{id}")
.execute(Collections.singletonMap("id", id))
.mapEmpty();
}
public boolean checkPassword(String plain, String hash) { public boolean checkPassword(String plain, String hash) {
try { try {
return BCrypt.checkpw(plain, hash); return BCrypt.checkpw(plain, hash);

View File

@@ -31,6 +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("latency_ms", time); .put("latency_ms", time);
future.complete(Status.OK(data)); future.complete(Status.OK(data));
} else { } else {
@@ -47,6 +48,7 @@ public class HealthCheckService {
.onSuccess(rs -> { .onSuccess(rs -> {
long time = System.currentTimeMillis() - start; long time = System.currentTimeMillis() - start;
JsonObject data = new JsonObject() JsonObject data = new JsonObject()
.put("name", "database")
.put("latency_ms", time); .put("latency_ms", time);
future.complete(Status.OK(data)); future.complete(Status.OK(data));
}) })