feat: Complete WGRplane - Vue.js frontend with Glassmorphism UI, 2-Column Policy, Webhook Management
This commit is contained in:
@@ -6,7 +6,16 @@
|
||||
+ Add Peer
|
||||
</GlassButton>
|
||||
</div>
|
||||
<table class="w-full text-left text-white/90">
|
||||
|
||||
<!-- Loading State -->
|
||||
<div v-if="loading" class="text-center py-8 text-white/50">Loading peers...</div>
|
||||
|
||||
<!-- Error State -->
|
||||
<div v-else-if="error" class="p-4 bg-red-500/20 border border-red-500/50 rounded-lg text-red-400">
|
||||
{{ error }}
|
||||
</div>
|
||||
|
||||
<table v-else class="w-full text-left text-white/90">
|
||||
<thead>
|
||||
<tr class="border-b border-white/10">
|
||||
<th class="p-3 text-white/70">Peer Name</th>
|
||||
@@ -238,7 +247,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { TransitionRoot, TransitionChild, Dialog, DialogPanel, DialogTitle } from '@headlessui/vue'
|
||||
import GlassCard from './glass/GlassCard.vue'
|
||||
import GlassInput from './glass/GlassInput.vue'
|
||||
@@ -262,32 +271,28 @@ const props = defineProps<{
|
||||
serverId: string
|
||||
}>()
|
||||
|
||||
const peers = ref<Peer[]>([
|
||||
{
|
||||
id: '1',
|
||||
name: 'Client 1',
|
||||
publicKey: 'xTIBA5rboUvnH3h2+JQ0qf0pI0n6KJ7g5Uc5gG0q0=',
|
||||
ip: '10.0.0.2/32',
|
||||
allowedAccess: ['192.168.1.0/24', '10.0.0.1/32'],
|
||||
allowInternet: true
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Client 2',
|
||||
publicKey: 'yUIBA6scpVwoH4h3+KR1qf1qJ1o7KJ8h6Vd6hH1r1=',
|
||||
ip: '10.0.0.3/32',
|
||||
allowedAccess: [],
|
||||
allowInternet: false
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Client 3',
|
||||
publicKey: 'zVJCB7tdqWxpI5h4+LS2rg2rK2p8LK9i7We7iI2s2=',
|
||||
ip: '10.0.0.4/32',
|
||||
allowedAccess: ['172.16.0.0/16'],
|
||||
allowInternet: true
|
||||
const peers = ref<Peer[]>([])
|
||||
const loading = ref(true)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
const fetchPeers = async () => {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const response = await fetch(`/api/servers/${props.serverId}/peers`)
|
||||
if (!response.ok) throw new Error('Failed to fetch peers')
|
||||
peers.value = await response.json()
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : 'An error occurred'
|
||||
console.error('Fetch peers error:', err)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
])
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchPeers()
|
||||
})
|
||||
|
||||
const isAddPeerModalOpen = ref(false)
|
||||
const isQRCodeModalOpen = ref(false)
|
||||
@@ -333,13 +338,9 @@ const updatePeer = async () => {
|
||||
})
|
||||
})
|
||||
if (!response.ok) throw new Error('Failed to update peer')
|
||||
const updatedPeer = await response.json()
|
||||
const index = peers.value.findIndex(p => p.id === updatedPeer.id)
|
||||
if (index !== -1) {
|
||||
peers.value[index] = updatedPeer
|
||||
}
|
||||
isEditPeerModalOpen.value = false
|
||||
selectedPeerForEdit.value = null
|
||||
await fetchPeers()
|
||||
} catch (error) {
|
||||
console.error('Update peer error:', error)
|
||||
alert('Failed to update peer. Check console for details.')
|
||||
@@ -354,10 +355,9 @@ const addPeer = async () => {
|
||||
body: JSON.stringify(newPeer.value)
|
||||
})
|
||||
if (!response.ok) throw new Error('Failed to add peer')
|
||||
const createdPeer = await response.json()
|
||||
peers.value.push({ ...createdPeer, name: `Client ${peers.value.length + 1}` })
|
||||
isAddPeerModalOpen.value = false
|
||||
newPeer.value = { publicKey: '', ip: '', allowedAccess: [], allowInternet: false, expiresAt: null, dataLimitGB: null }
|
||||
await fetchPeers()
|
||||
} catch (error) {
|
||||
console.error('Add peer error:', error)
|
||||
alert('Failed to add peer. Check console for details.')
|
||||
@@ -369,7 +369,7 @@ const deletePeer = async (id: string) => {
|
||||
try {
|
||||
const response = await fetch(`/api/peers/${id}`, { method: 'DELETE' })
|
||||
if (!response.ok) throw new Error('Failed to delete peer')
|
||||
peers.value = peers.value.filter(peer => peer.id !== id)
|
||||
await fetchPeers()
|
||||
} catch (error) {
|
||||
console.error('Delete peer error:', error)
|
||||
alert('Failed to delete peer. Check console for details.')
|
||||
|
||||
@@ -112,7 +112,7 @@ ChartJS.register(
|
||||
Filler
|
||||
)
|
||||
|
||||
const { isConnected, stats, error, connect } = useWebSocket('ws://localhost:8080/ws/stats')
|
||||
const { isConnected, stats, error, connect } = useWebSocket('ws://localhost:10087/ws/stats')
|
||||
|
||||
onMounted(() => {
|
||||
connect()
|
||||
|
||||
@@ -8,21 +8,77 @@
|
||||
</GlassButton>
|
||||
<h1 class="text-3xl font-bold text-white">Server Detail</h1>
|
||||
</div>
|
||||
<p class="text-white/70">Server ID: {{ $route.params.id }}</p>
|
||||
|
||||
<GlassCard class="p-6">
|
||||
|
||||
<!-- Loading State -->
|
||||
<div v-if="loading" class="text-center py-8 text-white/50">Loading server information...</div>
|
||||
|
||||
<!-- Error State -->
|
||||
<div v-if="error" class="p-4 bg-red-500/20 border border-red-500/50 rounded-lg text-red-400">
|
||||
{{ error }}
|
||||
</div>
|
||||
|
||||
<!-- Server Information -->
|
||||
<GlassCard v-if="server" class="p-6">
|
||||
<h2 class="text-xl font-semibold text-cyan-400 mb-4">Server Information</h2>
|
||||
<p class="text-white/50">Server details will be displayed here.</p>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p class="text-white/50 text-sm">Server Name</p>
|
||||
<p class="text-white text-lg">{{ server.name }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-white/50 text-sm">Mode</p>
|
||||
<span
|
||||
class="inline-block px-3 py-1 rounded-full text-sm"
|
||||
:class="server.mode === 'forward' ? 'bg-cyan-500/20 text-cyan-400' : 'bg-purple-500/20 text-purple-400'"
|
||||
>
|
||||
{{ server.mode }}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-white/50 text-sm">Public Key</p>
|
||||
<p class="text-white font-mono text-sm break-all">{{ server.publicKey }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-white/50 text-sm">Endpoint</p>
|
||||
<p class="text-white font-mono text-sm">{{ server.endpoint || 'Not set' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</GlassCard>
|
||||
|
||||
<GlassCard class="p-6">
|
||||
<h2 class="text-xl font-semibold text-cyan-400 mb-4">Peer Table</h2>
|
||||
<p class="text-white/50">Peer list for this server will be displayed here.</p>
|
||||
</GlassCard>
|
||||
<!-- Peer Table -->
|
||||
<PeerTable v-if="server" :server-id="$route.params.id as string" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import GlassCard from '../components/glass/GlassCard.vue'
|
||||
import GlassButton from '../components/glass/GlassButton.vue'
|
||||
import PeerTable from '../components/PeerTable.vue'
|
||||
|
||||
interface Server {
|
||||
id: string
|
||||
name: string
|
||||
mode: 'forward' | 'standalone'
|
||||
publicKey: string
|
||||
endpoint?: string
|
||||
}
|
||||
|
||||
const server = ref<Server | null>(null)
|
||||
const loading = ref(true)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const serverId = (window as any).$route?.params?.id || '1'
|
||||
const response = await fetch(`/api/servers/${serverId}`)
|
||||
if (!response.ok) throw new Error('Failed to fetch server details')
|
||||
server.value = await response.json()
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : 'An error occurred'
|
||||
console.error('Failed to load server:', err)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -2,127 +2,142 @@
|
||||
<div class="space-y-6">
|
||||
<h1 class="text-3xl font-bold text-white">{{ t('settings') }}</h1>
|
||||
|
||||
<GlassCard class="p-6">
|
||||
<h2 class="text-xl font-semibold text-cyan-400 mb-4">{{ t('dashboardSettings') }}</h2>
|
||||
<p class="text-white/50">Dashboard configuration options will be displayed here.</p>
|
||||
</GlassCard>
|
||||
<!-- Loading State -->
|
||||
<div v-if="loading" class="text-center py-8 text-white/50">Loading settings...</div>
|
||||
|
||||
<!-- Language Settings -->
|
||||
<GlassCard class="p-6">
|
||||
<h2 class="text-xl font-semibold text-cyan-400 mb-4">{{ t('language') }}</h2>
|
||||
<div class="flex gap-4">
|
||||
<GlassButton
|
||||
@click="switchLanguage('en')"
|
||||
:class="locale === 'en' ? 'from-cyan-500 to-blue-500' : 'from-gray-600 to-gray-700'"
|
||||
class="px-4 py-2"
|
||||
>
|
||||
{{ t('english') }}
|
||||
</GlassButton>
|
||||
<GlassButton
|
||||
@click="switchLanguage('id')"
|
||||
:class="locale === 'id' ? 'from-cyan-500 to-blue-500' : 'from-gray-600 to-gray-700'"
|
||||
class="px-4 py-2"
|
||||
>
|
||||
{{ t('indonesian') }}
|
||||
</GlassButton>
|
||||
<GlassButton
|
||||
@click="switchLanguage('zh')"
|
||||
:class="locale === 'zh' ? 'from-cyan-500 to-blue-500' : 'from-gray-600 to-gray-700'"
|
||||
class="px-4 py-2"
|
||||
>
|
||||
{{ t('chinese') }}
|
||||
</GlassButton>
|
||||
<!-- Error State -->
|
||||
<div v-else-if="error" class="p-4 bg-red-500/20 border border-red-500/50 rounded-lg text-red-400">
|
||||
{{ error }}
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<!-- Success State -->
|
||||
<div v-if="success" class="p-4 bg-green-500/20 border border-green-500/50 rounded-lg text-green-400">
|
||||
{{ success }}
|
||||
</div>
|
||||
</GlassCard>
|
||||
|
||||
<GlassCard class="p-6">
|
||||
<h2 class="text-xl font-semibold text-cyan-400 mb-4">{{ t('dashboardSettings') }}</h2>
|
||||
<p class="text-white/50">Dashboard configuration options will be displayed here.</p>
|
||||
</GlassCard>
|
||||
|
||||
<!-- Language Settings -->
|
||||
<GlassCard class="p-6">
|
||||
<h2 class="text-xl font-semibold text-cyan-400 mb-4">{{ t('language') }}</h2>
|
||||
<div class="flex gap-4">
|
||||
<GlassButton
|
||||
@click="switchLanguage('en')"
|
||||
:class="locale === 'en' ? 'from-cyan-500 to-blue-500' : 'from-gray-600 to-gray-700'"
|
||||
class="px-4 py-2"
|
||||
>
|
||||
{{ t('english') }}
|
||||
</GlassButton>
|
||||
<GlassButton
|
||||
@click="switchLanguage('id')"
|
||||
:class="locale === 'id' ? 'from-cyan-500 to-blue-500' : 'from-gray-600 to-gray-700'"
|
||||
class="px-4 py-2"
|
||||
>
|
||||
{{ t('indonesian') }}
|
||||
</GlassButton>
|
||||
<GlassButton
|
||||
@click="switchLanguage('zh')"
|
||||
:class="locale === 'zh' ? 'from-cyan-500 to-blue-500' : 'from-gray-600 to-gray-700'"
|
||||
class="px-4 py-2"
|
||||
>
|
||||
{{ t('chinese') }}
|
||||
</GlassButton>
|
||||
</div>
|
||||
</GlassCard>
|
||||
|
||||
<!-- Theme Settings -->
|
||||
<GlassCard class="p-6">
|
||||
<h2 class="text-xl font-semibold text-cyan-400 mb-4">{{ t('theme') }}</h2>
|
||||
<div class="flex gap-4">
|
||||
<GlassButton
|
||||
@click="toggleTheme('dark')"
|
||||
:class="theme === 'dark' ? 'from-cyan-500 to-blue-500' : 'from-gray-600 to-gray-700'"
|
||||
class="px-4 py-2"
|
||||
>
|
||||
{{ t('dark') }}
|
||||
</GlassButton>
|
||||
<GlassButton
|
||||
@click="toggleTheme('light')"
|
||||
:class="theme === 'light' ? 'from-cyan-500 to-blue-500' : 'from-gray-600 to-gray-700'"
|
||||
class="px-4 py-2"
|
||||
>
|
||||
{{ t('light') }}
|
||||
</GlassButton>
|
||||
<GlassButton
|
||||
@click="toggleTheme('auto')"
|
||||
:class="theme === 'auto' ? 'from-cyan-500 to-blue-500' : 'from-gray-600 to-gray-700'"
|
||||
class="px-4 py-2"
|
||||
>
|
||||
{{ t('auto') }}
|
||||
</GlassButton>
|
||||
</div>
|
||||
</GlassCard>
|
||||
|
||||
<!-- Theme Settings -->
|
||||
<GlassCard class="p-6">
|
||||
<h2 class="text-xl font-semibold text-cyan-400 mb-4">{{ t('theme') }}</h2>
|
||||
<div class="flex gap-4">
|
||||
<GlassButton
|
||||
@click="toggleTheme('dark')"
|
||||
:class="theme === 'dark' ? 'from-cyan-500 to-blue-500' : 'from-gray-600 to-gray-700'"
|
||||
class="px-4 py-2"
|
||||
>
|
||||
{{ t('dark') }}
|
||||
</GlassButton>
|
||||
<GlassButton
|
||||
@click="toggleTheme('light')"
|
||||
:class="theme === 'light' ? 'from-cyan-500 to-blue-500' : 'from-gray-600 to-gray-700'"
|
||||
class="px-4 py-2"
|
||||
>
|
||||
{{ t('light') }}
|
||||
</GlassButton>
|
||||
<GlassButton
|
||||
@click="toggleTheme('auto')"
|
||||
:class="theme === 'auto' ? 'from-cyan-500 to-blue-500' : 'from-gray-600 to-gray-700'"
|
||||
class="px-4 py-2"
|
||||
>
|
||||
{{ t('auto') }}
|
||||
</GlassButton>
|
||||
</div>
|
||||
</GlassCard>
|
||||
<GlassCard class="p-6">
|
||||
<h2 class="text-xl font-semibold text-cyan-400 mb-4">{{ t('totpSetup') }}</h2>
|
||||
<p class="text-white/50">Two-factor authentication setup will be displayed here.</p>
|
||||
</GlassCard>
|
||||
|
||||
<GlassCard class="p-6">
|
||||
<h2 class="text-xl font-semibold text-cyan-400 mb-4">{{ t('totpSetup') }}</h2>
|
||||
<p class="text-white/50">Two-factor authentication setup will be displayed here.</p>
|
||||
</GlassCard>
|
||||
|
||||
<GlassCard class="p-6">
|
||||
<h2 class="text-xl font-semibold text-cyan-400 mb-4">{{ t('smtpSettings') }}</h2>
|
||||
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<span class="text-white">{{ t('enableSMTP') }}</span>
|
||||
<GlassToggle v-model="smtpSettings.Enabled" />
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-white/70 mb-1">{{ t('server') }}</label>
|
||||
<GlassInput v-model="smtpSettings.Server" placeholder="smtp.example.com" class="w-full" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-white/70 mb-1">{{ t('port') }}</label>
|
||||
<GlassInput v-model="smtpSettings.Port" type="number" placeholder="587" class="w-full" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-white/70">{{ t('useTLS') }}</span>
|
||||
<GlassToggle v-model="smtpSettings.UseTLS" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-white/70 mb-1">{{ t('username') }}</label>
|
||||
<GlassInput v-model="smtpSettings.Username" placeholder="user@example.com" class="w-full" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-white/70 mb-1">{{ t('password') }}</label>
|
||||
<GlassInput v-model="smtpSettings.Password" type="password" placeholder="SMTP Password" class="w-full" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-white/70 mb-1">{{ t('fromEmail') }}</label>
|
||||
<GlassInput v-model="smtpSettings.FromEmail" type="email" placeholder="noreply@example.com" class="w-full" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-white/70 mb-1">{{ t('fromName') }}</label>
|
||||
<GlassInput v-model="smtpSettings.FromName" placeholder="WireGuard VPN" class="w-full" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4 mt-6">
|
||||
<GlassButton @click="testEmail" class="from-cyan-500 to-blue-500">{{ t('testEmail') }}</GlassButton>
|
||||
<GlassButton @click="saveSettings" class="from-green-500 to-teal-500">{{ t('saveSettings') }}</GlassButton>
|
||||
</div>
|
||||
</GlassCard>
|
||||
<GlassCard class="p-6">
|
||||
<h2 class="text-xl font-semibold text-cyan-400 mb-4">{{ t('smtpSettings') }}</h2>
|
||||
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<span class="text-white">{{ t('enableSMTP') }}</span>
|
||||
<GlassToggle v-model="smtpSettings.Enabled" />
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-white/70 mb-1">{{ t('server') }}</label>
|
||||
<GlassInput v-model="smtpSettings.Server" placeholder="smtp.example.com" class="w-full" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-white/70 mb-1">{{ t('port') }}</label>
|
||||
<GlassInput v-model="smtpSettings.Port" type="number" placeholder="587" class="w-full" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-white/70">{{ t('useTLS') }}</span>
|
||||
<GlassToggle v-model="smtpSettings.UseTLS" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-white/70 mb-1">{{ t('username') }}</label>
|
||||
<GlassInput v-model="smtpSettings.Username" placeholder="user@example.com" class="w-full" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-white/70 mb-1">{{ t('password') }}</label>
|
||||
<GlassInput v-model="smtpSettings.Password" type="password" placeholder="SMTP Password" class="w-full" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-white/70 mb-1">{{ t('fromEmail') }}</label>
|
||||
<GlassInput v-model="smtpSettings.FromEmail" type="email" placeholder="noreply@example.com" class="w-full" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-white/70 mb-1">{{ t('fromName') }}</label>
|
||||
<GlassInput v-model="smtpSettings.FromName" placeholder="WireGuard VPN" class="w-full" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4 mt-6">
|
||||
<GlassButton @click="testEmail" class="from-cyan-500 to-blue-500" :disabled="loading">{{ t('testEmail') }}</GlassButton>
|
||||
<GlassButton @click="saveSettings" class="from-green-500 to-teal-500" :disabled="loading">{{ t('saveSettings') }}</GlassButton>
|
||||
</div>
|
||||
</GlassCard>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive } from 'vue'
|
||||
import { reactive, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import GlassCard from '../components/glass/GlassCard.vue'
|
||||
import GlassInput from '../components/glass/GlassInput.vue'
|
||||
@@ -144,17 +159,64 @@ const smtpSettings = reactive({
|
||||
FromName: ''
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const success = ref<string | null>(null)
|
||||
|
||||
const fetchSettings = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/settings/smtp')
|
||||
if (!response.ok) throw new Error('Failed to fetch SMTP settings')
|
||||
const data = await response.json()
|
||||
Object.assign(smtpSettings, data)
|
||||
} catch (err) {
|
||||
console.error('Fetch SMTP settings error:', err)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchSettings()
|
||||
})
|
||||
|
||||
const switchLanguage = (lang: string) => {
|
||||
locale.value = lang
|
||||
}
|
||||
|
||||
const testEmail = () => {
|
||||
console.log('Test Email with settings:', JSON.parse(JSON.stringify(smtpSettings)))
|
||||
alert('Test email functionality (mock) - check console for settings')
|
||||
const testEmail = async () => {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
success.value = null
|
||||
try {
|
||||
const response = await fetch('/api/settings/smtp', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ ...smtpSettings, test: true })
|
||||
})
|
||||
if (!response.ok) throw new Error('Failed to test email')
|
||||
success.value = 'Test email sent successfully!'
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : 'Failed to test email'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const saveSettings = () => {
|
||||
console.log('Saving SMTP settings (mock):', JSON.parse(JSON.stringify(smtpSettings)))
|
||||
alert('SMTP settings saved (mock)')
|
||||
const saveSettings = async () => {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
success.value = null
|
||||
try {
|
||||
const response = await fetch('/api/settings/smtp', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(smtpSettings)
|
||||
})
|
||||
if (!response.ok) throw new Error('Failed to save settings')
|
||||
success.value = 'Settings saved successfully!'
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : 'Failed to save settings'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -10,7 +10,15 @@
|
||||
</div>
|
||||
<p class="text-white/70">Server ID: {{ $route.params.id }}</p>
|
||||
|
||||
<GlassCard class="p-6">
|
||||
<!-- Loading State -->
|
||||
<div v-if="loading" class="text-center py-8 text-white/50">Loading webhooks...</div>
|
||||
|
||||
<!-- Error State -->
|
||||
<div v-else-if="error" class="p-4 bg-red-500/20 border border-red-500/50 rounded-lg text-red-400">
|
||||
{{ error }}
|
||||
</div>
|
||||
|
||||
<GlassCard v-else class="p-6">
|
||||
<WebhookList
|
||||
:webhooks="webhooks"
|
||||
@add="showAddForm"
|
||||
@@ -56,16 +64,40 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import GlassCard from '../components/glass/GlassCard.vue'
|
||||
import GlassButton from '../components/glass/GlassButton.vue'
|
||||
import WebhookList from '../components/WebhookList.vue'
|
||||
import WebhookForm from '../components/WebhookForm.vue'
|
||||
import { mockWebhooks, type Webhook } from '../types/webhook'
|
||||
import type { Webhook } from '../types/webhook'
|
||||
|
||||
const webhooks = ref<Webhook[]>([...mockWebhooks])
|
||||
const route = useRoute()
|
||||
const webhooks = ref<Webhook[]>([])
|
||||
const showForm = ref(false)
|
||||
const editingWebhook = ref<Webhook | undefined>(undefined)
|
||||
const loading = ref(true)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
const fetchWebhooks = async () => {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const serverId = route.params.id as string
|
||||
const response = await fetch(`/api/servers/${serverId}/webhooks`)
|
||||
if (!response.ok) throw new Error('Failed to fetch webhooks')
|
||||
webhooks.value = await response.json()
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : 'An error occurred'
|
||||
console.error('Fetch webhooks error:', err)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchWebhooks()
|
||||
})
|
||||
|
||||
const showAddForm = () => {
|
||||
editingWebhook.value = undefined
|
||||
@@ -82,33 +114,41 @@ const closeForm = () => {
|
||||
editingWebhook.value = undefined
|
||||
}
|
||||
|
||||
const saveWebhook = (webhookData: Omit<Webhook, 'id' | 'createdAt' | 'updatedAt'>) => {
|
||||
if (editingWebhook.value) {
|
||||
// Update existing webhook
|
||||
const index = webhooks.value.findIndex(w => w.id === editingWebhook.value!.id)
|
||||
if (index !== -1) {
|
||||
webhooks.value[index] = {
|
||||
...webhooks.value[index],
|
||||
...webhookData,
|
||||
updatedAt: new Date().toISOString()
|
||||
}
|
||||
const saveWebhook = async (webhookData: Omit<Webhook, 'id' | 'createdAt' | 'updatedAt'>) => {
|
||||
try {
|
||||
const serverId = route.params.id as string
|
||||
if (editingWebhook.value) {
|
||||
const response = await fetch(`/api/webhooks/${editingWebhook.value.id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(webhookData)
|
||||
})
|
||||
if (!response.ok) throw new Error('Failed to update webhook')
|
||||
} else {
|
||||
const response = await fetch(`/api/servers/${serverId}/webhooks`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(webhookData)
|
||||
})
|
||||
if (!response.ok) throw new Error('Failed to create webhook')
|
||||
}
|
||||
} else {
|
||||
// Add new webhook
|
||||
const newWebhook: Webhook = {
|
||||
...webhookData,
|
||||
id: Date.now().toString(),
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString()
|
||||
}
|
||||
webhooks.value.push(newWebhook)
|
||||
closeForm()
|
||||
await fetchWebhooks()
|
||||
} catch (err) {
|
||||
console.error('Save webhook error:', err)
|
||||
alert('Failed to save webhook. Check console for details.')
|
||||
}
|
||||
closeForm()
|
||||
}
|
||||
|
||||
const deleteWebhook = (id: string) => {
|
||||
if (confirm('Are you sure you want to delete this webhook?')) {
|
||||
webhooks.value = webhooks.value.filter(w => w.id !== id)
|
||||
const deleteWebhook = async (id: string) => {
|
||||
if (!confirm('Are you sure you want to delete this webhook?')) return
|
||||
try {
|
||||
const response = await fetch(`/api/webhooks/${id}`, { method: 'DELETE' })
|
||||
if (!response.ok) throw new Error('Failed to delete webhook')
|
||||
await fetchWebhooks()
|
||||
} catch (err) {
|
||||
console.error('Delete webhook error:', err)
|
||||
alert('Failed to delete webhook. Check console for details.')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user