up
This commit is contained in:
@@ -39,6 +39,17 @@
|
||||
Users
|
||||
</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
|
||||
to="/settings"
|
||||
class="flex items-center px-4 py-3 text-gray-700 rounded-lg hover:bg-gray-100 transition-colors"
|
||||
@@ -111,28 +122,28 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {computed, ref} from 'vue'
|
||||
import { computed, ref, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const userName = ref('Loading...')
|
||||
const userLogin = ref('')
|
||||
|
||||
const userName = ref('Admin User')
|
||||
const userInitials = computed(() => {
|
||||
return userName.value.split(' ').map(n => n[0]).join('').toUpperCase()
|
||||
})
|
||||
|
||||
const pageTitle = computed(() => {
|
||||
const titles: Record<string, string> = {
|
||||
'/dashboard': 'Dashboard',
|
||||
'/users': 'Users Management',
|
||||
'/settings': 'Settings'
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const res = await fetch('/api/admin/me')
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
userLogin.value = data.login
|
||||
userName.value = data.login // или можно сделать красивое отображение
|
||||
}
|
||||
} catch (e) {
|
||||
userName.value = 'User'
|
||||
}
|
||||
return titles[route.path] || 'Admin Panel'
|
||||
})
|
||||
|
||||
async function logout() {
|
||||
await fetch('/api/logout', { method: 'POST' })
|
||||
router.push('/login')
|
||||
}
|
||||
const userInitials = computed(() => {
|
||||
return (userName.value[0] || 'U').toUpperCase()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -2,19 +2,29 @@ import { createRouter, createWebHistory } from 'vue-router'
|
||||
import Login from '../views/auth/Login.vue'
|
||||
import Setup from '../views/auth/Setup.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'
|
||||
|
||||
const routes = [
|
||||
{ path: '/login', component: Login, meta: { title: 'Login' } },
|
||||
{ path: '/setup', component: Setup, meta: { title: 'Setup' } },
|
||||
{
|
||||
path: '/',
|
||||
redirect: '/dashboard'
|
||||
},
|
||||
{
|
||||
path: '/dashboard',
|
||||
component: Dashboard,
|
||||
meta: { requiresAuth: true, title: 'Dashboard' }
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
redirect: '/dashboard'
|
||||
{ path: '/users',
|
||||
component: Users,
|
||||
meta: { requiresAuth: true, title: 'Users' }
|
||||
},
|
||||
{ path: '/restaurants',
|
||||
component: Restaurants,
|
||||
meta: { requiresAuth: true, title: 'Restaurants' }
|
||||
},
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
@@ -46,6 +56,18 @@ router.beforeEach(async (to, from, next) => {
|
||||
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
|
||||
const requiresAuth = to.matched.some(record => record.meta.requiresAuth)
|
||||
|
||||
|
||||
@@ -103,7 +103,7 @@
|
||||
<div class="space-y-4">
|
||||
<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="['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>
|
||||
</div>
|
||||
<span class="text-sm text-gray-500">{{ service.latency }}ms</span>
|
||||
@@ -115,7 +115,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import AppLayout from '../components/Layout/AppLayout.vue'
|
||||
|
||||
const stats = ref({
|
||||
@@ -126,12 +126,34 @@ const stats = ref({
|
||||
})
|
||||
|
||||
const recentUsers = ref([])
|
||||
const systemServices = ref([
|
||||
{ name: 'Database', status: 'healthy', latency: 12 },
|
||||
{ name: 'Redis Cache', status: 'healthy', latency: 3 },
|
||||
{ name: 'API Gateway', status: 'healthy', latency: 45 },
|
||||
{ name: 'File Storage', status: 'healthy', latency: 28 }
|
||||
])
|
||||
const systemServices = ref([])
|
||||
|
||||
async function loadHealth() {
|
||||
try {
|
||||
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 () => {
|
||||
await loadData()
|
||||
|
||||
140
frontend/src/views/Restaurants.vue
Normal file
140
frontend/src/views/Restaurants.vue
Normal 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>
|
||||
124
frontend/src/views/Users.vue
Normal file
124
frontend/src/views/Users.vue
Normal 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>
|
||||
50
src/main/java/su/xserver/iikocon/DateRangeSetup.java
Normal file
50
src/main/java/su/xserver/iikocon/DateRangeSetup.java
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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 -> {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
194
src/main/java/su/xserver/iikocon/ProxyVerticle.java
Normal file
194
src/main/java/su/xserver/iikocon/ProxyVerticle.java
Normal 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: */*
|
||||
@@ -90,4 +90,45 @@ public class RestaurantService {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
try {
|
||||
return BCrypt.checkpw(plain, hash);
|
||||
|
||||
@@ -31,6 +31,7 @@ public class HealthCheckService {
|
||||
long time = System.currentTimeMillis() - start;
|
||||
if ("PONG".equalsIgnoreCase(response.toString())) {
|
||||
JsonObject data = new JsonObject()
|
||||
.put("name", "redis")
|
||||
.put("latency_ms", time);
|
||||
future.complete(Status.OK(data));
|
||||
} else {
|
||||
@@ -47,6 +48,7 @@ public class HealthCheckService {
|
||||
.onSuccess(rs -> {
|
||||
long time = System.currentTimeMillis() - start;
|
||||
JsonObject data = new JsonObject()
|
||||
.put("name", "database")
|
||||
.put("latency_ms", time);
|
||||
future.complete(Status.OK(data));
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user