up
This commit is contained in:
@@ -1,14 +1,17 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import Login from '../views/auth/Login.vue'
|
||||
import Setup from '../views/auth/Setup.vue'
|
||||
import Register from '../views/auth/Register.vue'
|
||||
import Dashboard from '../views/Dashboard.vue'
|
||||
import Users from '../views/Users.vue'
|
||||
import Restaurants from '../views/Restaurants.vue'
|
||||
import AdminSettings from '../views/AdminSettings.vue'
|
||||
import NotFound from '../views/NotFound.vue'
|
||||
|
||||
const routes = [
|
||||
{ path: '/login', component: Login, meta: { title: 'Login' } },
|
||||
{ path: '/setup', component: Setup, meta: { title: 'Setup' } },
|
||||
{ path: '/register', component: Register, meta: { title: 'Register' } },
|
||||
{
|
||||
path: '/',
|
||||
redirect: '/dashboard'
|
||||
@@ -18,14 +21,21 @@ const routes = [
|
||||
component: Dashboard,
|
||||
meta: { requiresAuth: true, title: 'Dashboard' }
|
||||
},
|
||||
{ path: '/users',
|
||||
{
|
||||
path: '/users',
|
||||
component: Users,
|
||||
meta: { requiresAuth: true, title: 'Users' }
|
||||
},
|
||||
{ path: '/restaurants',
|
||||
{
|
||||
path: '/restaurants',
|
||||
component: Restaurants,
|
||||
meta: { requiresAuth: true, title: 'Restaurants' }
|
||||
},
|
||||
{
|
||||
path: '/settings',
|
||||
component: AdminSettings,
|
||||
meta: { requiresAuth: true, title: 'Settings' }
|
||||
},
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
name: 'NotFound',
|
||||
|
||||
37
frontend/src/views/AdminSettings.vue
Normal file
37
frontend/src/views/AdminSettings.vue
Normal 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>
|
||||
@@ -118,11 +118,32 @@
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import AppLayout from '../components/Layout/AppLayout.vue'
|
||||
|
||||
const stats = ref({
|
||||
totalUsers: 0,
|
||||
activeSessions: 0,
|
||||
systemHealth: 98,
|
||||
uptime: '99.9%'
|
||||
const stats = ref({ totalUsers: 0, activeSessions: 0, systemHealth: 100, uptime: '99.9%' })
|
||||
|
||||
async function loadStats() {
|
||||
try {
|
||||
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([])
|
||||
|
||||
@@ -11,20 +11,28 @@
|
||||
<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">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">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">
|
||||
<tbody>
|
||||
<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">{{ user.id }}</td>
|
||||
<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 @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">
|
||||
<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>
|
||||
<button @click="openModal('edit', user)" class="text-blue-600">Edit</button>
|
||||
<button @click="deleteUser(user.id)" class="text-red-600">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -36,6 +44,10 @@
|
||||
<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">Email</label>
|
||||
<input v-model="form.email" 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" />
|
||||
@@ -75,6 +87,11 @@ function formatDate(dateStr: string) {
|
||||
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) {
|
||||
modalMode.value = mode;
|
||||
if (mode === 'create') {
|
||||
|
||||
@@ -67,7 +67,9 @@
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<button
|
||||
|
||||
72
frontend/src/views/auth/Register.vue
Normal file
72
frontend/src/views/auth/Register.vue
Normal 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>
|
||||
@@ -51,6 +51,26 @@
|
||||
<p v-if="validation.login" class="mt-1 text-xs text-red-600">{{ validation.login }}</p>
|
||||
</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>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">Password</label>
|
||||
<div class="relative">
|
||||
@@ -133,21 +153,24 @@ import { ref, computed, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
const form = ref({ login: '', password: '' })
|
||||
const form = ref({ login: '', email: '', password: '' });
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
const showPassword = ref(false)
|
||||
|
||||
const validation = computed(() => {
|
||||
const errors: any = {}
|
||||
const errors: any = {};
|
||||
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) {
|
||||
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 pwd = form.value.password
|
||||
@@ -178,29 +201,33 @@ const strengthBarColor = computed(() => {
|
||||
})
|
||||
|
||||
async function handleSetup() {
|
||||
if (Object.keys(validation.value).length > 0) return
|
||||
if (Object.keys(validation.value).length > 0) return;
|
||||
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
loading.value = true;
|
||||
error.value = '';
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/setup', {
|
||||
method: 'POST',
|
||||
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) {
|
||||
router.push('/login')
|
||||
router.push('/login');
|
||||
} else {
|
||||
error.value = data.error || 'Failed to create account'
|
||||
error.value = data.error || 'Failed to create account';
|
||||
}
|
||||
} catch (e) {
|
||||
error.value = 'Network error. Please try again.'
|
||||
error.value = 'Network error. Please try again.';
|
||||
} finally {
|
||||
loading.value = false
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user