up
This commit is contained in:
@@ -19,3 +19,20 @@
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { watch } from 'vue'
|
||||
import { useSettingsStore } from './stores/settings'
|
||||
|
||||
const settings = useSettingsStore()
|
||||
|
||||
watch(() => settings.siteDescription, (desc) => {
|
||||
let meta = document.querySelector('meta[name="description"]')
|
||||
if (!meta) {
|
||||
meta = document.createElement('meta')
|
||||
meta.setAttribute('name', 'description')
|
||||
document.head.appendChild(meta)
|
||||
}
|
||||
meta.setAttribute('content', desc || '')
|
||||
}, { immediate: true })
|
||||
</script>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="text-xl font-bold text-gray-900">AdminPanel</span>
|
||||
<span class="text-xl font-bold text-gray-900">{{ settings.siteName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -124,6 +124,9 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useSettingsStore } from '../../stores/settings'
|
||||
|
||||
const settings = useSettingsStore()
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
@@ -3,8 +3,15 @@ import { createPinia } from 'pinia'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import './style.css'
|
||||
import { useSettingsStore } from './stores/settings'
|
||||
|
||||
const app = createApp(App)
|
||||
app.use(createPinia())
|
||||
const pinia = createPinia()
|
||||
app.use(pinia)
|
||||
app.use(router)
|
||||
app.mount('#app')
|
||||
|
||||
// Загружаем настройки до монтирования
|
||||
const settingsStore = useSettingsStore()
|
||||
settingsStore.loadSettings().then(() => {
|
||||
app.mount('#app')
|
||||
})
|
||||
|
||||
@@ -7,11 +7,12 @@ import Users from '../views/Users.vue'
|
||||
import Restaurants from '../views/Restaurants.vue'
|
||||
import AdminSettings from '../views/AdminSettings.vue'
|
||||
import NotFound from '../views/NotFound.vue'
|
||||
import { useSettingsStore } from '../stores/settings'
|
||||
|
||||
const routes = [
|
||||
{ path: '/login', component: Login, meta: { title: 'Login' } },
|
||||
{ path: '/register', component: Register, meta: { title: 'Register' } },
|
||||
{ path: '/setup', component: Setup, meta: { title: 'Setup' } },
|
||||
{ path: '/login', component: Login, meta: { title: 'Login', requiresAuth: false } },
|
||||
{ path: '/register', component: Register, meta: { title: 'Register', requiresAuth: false } },
|
||||
{ path: '/setup', component: Setup, meta: { title: 'Setup', requiresAuth: false } },
|
||||
{
|
||||
path: '/',
|
||||
redirect: '/dashboard'
|
||||
@@ -50,14 +51,20 @@ const router = createRouter({
|
||||
})
|
||||
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
// Update page title
|
||||
document.title = `${to.meta.title || 'Admin Panel'} | AdminPanel`
|
||||
// Загружаем настройки приложения, если они ещё не загружены
|
||||
const settings = useSettingsStore()
|
||||
if (!settings.siteName) {
|
||||
await settings.loadSettings()
|
||||
}
|
||||
|
||||
// Check if setup is needed
|
||||
// Устанавливаем заголовок страницы с использованием site_name
|
||||
const pageTitle = to.meta.title ? `${to.meta.title} | ${settings.siteName}` : settings.siteName
|
||||
document.title = pageTitle
|
||||
|
||||
// Проверка необходимости установки (setup)
|
||||
try {
|
||||
const statusRes = await fetch('/api/status')
|
||||
const status = await statusRes.json()
|
||||
|
||||
if (status.needsSetup && to.path !== '/setup') {
|
||||
next('/setup')
|
||||
return
|
||||
@@ -66,21 +73,29 @@ 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');
|
||||
const meRes = await fetch('/api/admin/me')
|
||||
if (meRes.ok) {
|
||||
next('/dashboard');
|
||||
return;
|
||||
next('/dashboard')
|
||||
return
|
||||
}
|
||||
} catch (e) {
|
||||
// игнорируем ошибку, продолжаем
|
||||
}
|
||||
}
|
||||
|
||||
// Check authentication
|
||||
const requiresAuth = to.matched.some(record => record.meta.requiresAuth)
|
||||
// Проверка доступности регистрации
|
||||
if (to.path === '/register') {
|
||||
if (!settings.enableRegistration) {
|
||||
next('/login')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Проверка аутентификации для защищённых маршрутов
|
||||
const requiresAuth = to.matched.some(record => record.meta.requiresAuth)
|
||||
if (requiresAuth) {
|
||||
try {
|
||||
const res = await fetch('/api/admin/me')
|
||||
|
||||
24
frontend/src/stores/settings.ts
Normal file
24
frontend/src/stores/settings.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export const useSettingsStore = defineStore('settings', () => {
|
||||
const siteName = ref('Admin Panel')
|
||||
const siteDescription = ref('')
|
||||
const enableRegistration = ref(true)
|
||||
|
||||
async function loadSettings() {
|
||||
try {
|
||||
const res = await fetch('/api/settings')
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
siteName.value = data.site_name || 'Admin Panel'
|
||||
siteDescription.value = data.site_description || ''
|
||||
enableRegistration.value = data.enable_registration !== 'false'
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load settings', e)
|
||||
}
|
||||
}
|
||||
|
||||
return { siteName, siteDescription, enableRegistration, loadSettings }
|
||||
})
|
||||
@@ -11,8 +11,9 @@
|
||||
<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">HTTPS</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">Created</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Actions</th>
|
||||
</tr>
|
||||
@@ -21,8 +22,16 @@
|
||||
<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">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="rest.https"
|
||||
@change="toggleHttps(rest)"
|
||||
class="rounded border-gray-300 text-primary-600 focus:ring-primary-500 w-4 h-4"
|
||||
/>
|
||||
</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">{{ 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>
|
||||
@@ -38,23 +47,38 @@
|
||||
<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">
|
||||
<!-- 1. Имя ресторана -->
|
||||
<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>
|
||||
<!-- 2. Хост -->
|
||||
<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>
|
||||
<!-- 3. HTTPS чекбокс -->
|
||||
<div class="mb-4 flex items-center">
|
||||
<input type="checkbox" v-model="form.https" class="rounded border-gray-300 text-primary-600 focus:ring-primary-500 w-4 h-4 mr-2" />
|
||||
<label class="text-sm font-medium text-gray-700">HTTPS</label>
|
||||
</div>
|
||||
<!-- 4. Логин -->
|
||||
<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>
|
||||
<!-- 5. Пароль (отключаем автозаполнение) -->
|
||||
<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" />
|
||||
<input
|
||||
v-model="form.password"
|
||||
:required="modalMode === 'create'"
|
||||
type="password"
|
||||
<!-- autocomplete="new-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>
|
||||
@@ -72,7 +96,7 @@ 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 form = ref({ id: null, name: '', login: '', password: '', host: '', https: false });
|
||||
const modalTitle = ref('');
|
||||
|
||||
async function loadRestaurants() {
|
||||
@@ -88,10 +112,17 @@ function formatDate(dateStr: string) {
|
||||
function openModal(mode: 'create' | 'edit', rest: any = null) {
|
||||
modalMode.value = mode;
|
||||
if (mode === 'create') {
|
||||
form.value = { id: null, name: '', login: '', password: '', host: '' };
|
||||
form.value = { id: null, name: '', login: '', password: '', host: '', https: false };
|
||||
modalTitle.value = 'Create Restaurant';
|
||||
} else {
|
||||
form.value = { id: rest.id, name: rest.name, login: rest.login, password: '', host: rest.host };
|
||||
form.value = {
|
||||
id: rest.id,
|
||||
name: rest.name,
|
||||
login: rest.login,
|
||||
password: '',
|
||||
host: rest.host,
|
||||
https: rest.https || false
|
||||
};
|
||||
modalTitle.value = 'Edit Restaurant';
|
||||
}
|
||||
modalOpen.value = true;
|
||||
@@ -101,31 +132,65 @@ function closeModal() {
|
||||
modalOpen.value = false;
|
||||
}
|
||||
|
||||
async function toggleHttps(rest: any) {
|
||||
const newHttps = !rest.https;
|
||||
const payload = {
|
||||
name: rest.name,
|
||||
host: rest.host,
|
||||
login: rest.login,
|
||||
https: newHttps
|
||||
// пароль не передаём, он останется прежним
|
||||
};
|
||||
try {
|
||||
const res = await fetch(`/api/admin/restaurants/${rest.id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
if (res.ok) {
|
||||
// Обновляем локальное состояние или перезагружаем список
|
||||
rest.https = newHttps;
|
||||
// Альтернатива: await loadRestaurants();
|
||||
} else {
|
||||
alert('Failed to update HTTPS status');
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Network error');
|
||||
}
|
||||
}
|
||||
|
||||
async function submitRestaurant() {
|
||||
try {
|
||||
const payload = {
|
||||
name: form.value.name,
|
||||
login: form.value.login,
|
||||
host: form.value.host,
|
||||
https: form.value.https,
|
||||
login: form.value.login,
|
||||
...(form.value.password ? { password: form.value.password } : {})
|
||||
};
|
||||
if (modalMode.value === 'create') {
|
||||
await fetch('/api/admin/restaurants', {
|
||||
if (!form.value.password) {
|
||||
alert('Password is required');
|
||||
return;
|
||||
}
|
||||
const res = await fetch('/api/admin/restaurants', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
if (!res.ok) throw new Error('Create failed');
|
||||
} else {
|
||||
await fetch(`/api/admin/restaurants/${form.value.id}`, {
|
||||
const res = await fetch(`/api/admin/restaurants/${form.value.id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
if (!res.ok) throw new Error('Update failed');
|
||||
}
|
||||
await loadRestaurants();
|
||||
closeModal();
|
||||
} catch (e) {
|
||||
alert('Operation failed');
|
||||
alert('Operation failed: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,13 +24,13 @@
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm">{{ user.login }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm">{{ user.email }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm">
|
||||
<button
|
||||
<input
|
||||
v-if="user.id !== currentUserId"
|
||||
@click="toggleActive(user)"
|
||||
:class="user.active ? 'text-green-600' : 'text-red-600'"
|
||||
>
|
||||
{{ user.active ? 'Active' : 'Inactive' }}
|
||||
</button>
|
||||
type="checkbox"
|
||||
:checked="user.active"
|
||||
@change="toggleActive(user)"
|
||||
class="rounded border-gray-300 text-primary-600 focus:ring-primary-500 w-4 h-4"
|
||||
/>
|
||||
<span v-else class="text-gray-400 text-sm">(You)</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm">{{ user.ip || '-' }}</td>
|
||||
|
||||
@@ -67,7 +67,11 @@
|
||||
<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>
|
||||
</label>
|
||||
<router-link to="/register" class="text-sm text-primary-600 hover:text-primary-700">
|
||||
<router-link
|
||||
v-if="settings.enableRegistration"
|
||||
to="/register"
|
||||
class="text-sm text-primary-600 hover:text-primary-700"
|
||||
>
|
||||
Create account
|
||||
</router-link>
|
||||
</div>
|
||||
@@ -99,6 +103,9 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useSettingsStore } from '../../stores/settings'
|
||||
|
||||
const settings = useSettingsStore()
|
||||
|
||||
const router = useRouter()
|
||||
const form = ref({ login: '', password: '' })
|
||||
|
||||
Reference in New Issue
Block a user