MCP Tokenverwaltung in Firmeneinstellungen ergänzen
This commit is contained in:
@@ -5,6 +5,7 @@ import {
|
||||
} from "~/composables/useTaxEvaluation"
|
||||
|
||||
const auth = useAuthStore()
|
||||
const toast = useToast()
|
||||
const defaultFeatures = {
|
||||
objects: true,
|
||||
calendar: true,
|
||||
@@ -119,6 +120,17 @@ const itemInfo = ref({
|
||||
projectTypes: []
|
||||
})
|
||||
|
||||
const canManageMcpTokens = computed(() => Boolean(auth.user?.is_admin || auth.hasPermission("mcp.tokens.write")))
|
||||
const mcpTokens = ref([])
|
||||
const mcpTokensLoading = ref(false)
|
||||
const mcpTokenCreating = ref(false)
|
||||
const mcpTokenDeletingId = ref(null)
|
||||
const createdMcpToken = ref("")
|
||||
const mcpTokenForm = reactive({
|
||||
name: "Codex MCP Token",
|
||||
expiresAt: ""
|
||||
})
|
||||
|
||||
const setupPage = async () => {
|
||||
itemInfo.value = auth.activeTenantData
|
||||
console.log(itemInfo.value)
|
||||
@@ -153,7 +165,86 @@ const saveFeatures = async () => {
|
||||
await updateTenant({features: features.value})
|
||||
}
|
||||
|
||||
const formatMcpTokenDate = (value) => {
|
||||
if (!value) return "Nie"
|
||||
|
||||
return new Date(value).toLocaleString("de-DE", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit"
|
||||
})
|
||||
}
|
||||
|
||||
const loadMcpTokens = async () => {
|
||||
if (!canManageMcpTokens.value) return
|
||||
|
||||
mcpTokensLoading.value = true
|
||||
try {
|
||||
const res = await useNuxtApp().$api("/api/mcp/tokens")
|
||||
mcpTokens.value = res?.rows || []
|
||||
} catch (error) {
|
||||
toast.add({ title: "MCP Tokens konnten nicht geladen werden", color: "error" })
|
||||
} finally {
|
||||
mcpTokensLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const createMcpToken = async () => {
|
||||
if (!mcpTokenForm.name?.trim()) {
|
||||
toast.add({ title: "Name fehlt", description: "Bitte gib einen Namen für den Token an.", color: "orange" })
|
||||
return
|
||||
}
|
||||
|
||||
mcpTokenCreating.value = true
|
||||
createdMcpToken.value = ""
|
||||
|
||||
try {
|
||||
const res = await useNuxtApp().$api("/api/mcp/tokens", {
|
||||
method: "POST",
|
||||
body: {
|
||||
name: mcpTokenForm.name.trim(),
|
||||
expiresAt: mcpTokenForm.expiresAt || null
|
||||
}
|
||||
})
|
||||
|
||||
createdMcpToken.value = res?.token || ""
|
||||
toast.add({ title: "MCP Token erstellt", color: "success" })
|
||||
await loadMcpTokens()
|
||||
} catch (error) {
|
||||
toast.add({ title: "MCP Token konnte nicht erstellt werden", color: "error" })
|
||||
} finally {
|
||||
mcpTokenCreating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const copyCreatedMcpToken = async () => {
|
||||
if (!createdMcpToken.value) return
|
||||
|
||||
await navigator.clipboard.writeText(createdMcpToken.value)
|
||||
toast.add({ title: "Token kopiert", color: "success" })
|
||||
}
|
||||
|
||||
const deactivateMcpToken = async (token) => {
|
||||
if (!token?.id || !confirm(`MCP Token "${token.name}" deaktivieren?`)) return
|
||||
|
||||
mcpTokenDeletingId.value = token.id
|
||||
try {
|
||||
await useNuxtApp().$api(`/api/mcp/tokens/${token.id}`, {
|
||||
method: "DELETE"
|
||||
})
|
||||
toast.add({ title: "MCP Token deaktiviert", color: "success" })
|
||||
await loadMcpTokens()
|
||||
} catch (error) {
|
||||
toast.add({ title: "MCP Token konnte nicht deaktiviert werden", color: "error" })
|
||||
} finally {
|
||||
mcpTokenDeletingId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
setupPage()
|
||||
onMounted(loadMcpTokens)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -167,6 +258,8 @@ setupPage()
|
||||
label: 'Dokubox'
|
||||
},{
|
||||
label: 'Rechnung & Kontakt'
|
||||
},{
|
||||
label: 'Integrationen'
|
||||
},{
|
||||
label: 'Funktionen'
|
||||
}
|
||||
@@ -259,6 +352,124 @@ setupPage()
|
||||
</UCard>
|
||||
|
||||
</div>
|
||||
<div v-else-if="item.label === 'Integrationen'">
|
||||
<UCard class="mt-5">
|
||||
<div class="space-y-6">
|
||||
<div class="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
|
||||
<div>
|
||||
<h3 class="text-base font-semibold text-highlighted">MCP Tokens</h3>
|
||||
<p class="text-sm text-muted">Verwalte dauerhafte Tokens für Codex und andere MCP Clients.</p>
|
||||
</div>
|
||||
<UButton
|
||||
icon="i-heroicons-arrow-path"
|
||||
variant="outline"
|
||||
:loading="mcpTokensLoading"
|
||||
:disabled="!canManageMcpTokens"
|
||||
@click="loadMcpTokens"
|
||||
>
|
||||
Aktualisieren
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
<UAlert
|
||||
v-if="!canManageMcpTokens"
|
||||
title="Keine Berechtigung"
|
||||
description="Du benötigst Adminrechte oder die Berechtigung mcp.tokens.write."
|
||||
color="orange"
|
||||
variant="outline"
|
||||
/>
|
||||
|
||||
<div v-else class="space-y-6">
|
||||
<div class="grid gap-4 md:grid-cols-3">
|
||||
<UFormField label="Name" class="md:col-span-2">
|
||||
<UInput v-model="mcpTokenForm.name" placeholder="z.B. Codex MCP Token" />
|
||||
</UFormField>
|
||||
<UFormField label="Ablaufdatum">
|
||||
<UInput v-model="mcpTokenForm.expiresAt" type="date" />
|
||||
</UFormField>
|
||||
</div>
|
||||
|
||||
<UButton
|
||||
icon="i-heroicons-plus"
|
||||
:loading="mcpTokenCreating"
|
||||
@click="createMcpToken"
|
||||
>
|
||||
Token erstellen
|
||||
</UButton>
|
||||
|
||||
<UAlert
|
||||
v-if="createdMcpToken"
|
||||
title="Token wurde erstellt"
|
||||
description="Der Token wird nur jetzt vollständig angezeigt."
|
||||
color="success"
|
||||
variant="outline"
|
||||
>
|
||||
<template #description>
|
||||
<div class="mt-3 space-y-3">
|
||||
<UTextarea
|
||||
:model-value="createdMcpToken"
|
||||
readonly
|
||||
autoresize
|
||||
/>
|
||||
<UButton
|
||||
icon="i-heroicons-clipboard-document"
|
||||
variant="outline"
|
||||
@click="copyCreatedMcpToken"
|
||||
>
|
||||
Token kopieren
|
||||
</UButton>
|
||||
</div>
|
||||
</template>
|
||||
</UAlert>
|
||||
|
||||
<div class="overflow-x-auto rounded-md border border-default">
|
||||
<table class="min-w-full divide-y divide-default text-sm">
|
||||
<thead class="bg-muted">
|
||||
<tr>
|
||||
<th class="px-4 py-3 text-left font-medium">Name</th>
|
||||
<th class="px-4 py-3 text-left font-medium">Prefix</th>
|
||||
<th class="px-4 py-3 text-left font-medium">Erstellt</th>
|
||||
<th class="px-4 py-3 text-left font-medium">Zuletzt genutzt</th>
|
||||
<th class="px-4 py-3 text-left font-medium">Läuft ab</th>
|
||||
<th class="px-4 py-3 text-left font-medium">Status</th>
|
||||
<th class="px-4 py-3 text-right font-medium">Aktion</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-default">
|
||||
<tr v-if="!mcpTokensLoading && !mcpTokens.length">
|
||||
<td colspan="7" class="px-4 py-6 text-center text-muted">Noch keine MCP Tokens vorhanden.</td>
|
||||
</tr>
|
||||
<tr v-for="token in mcpTokens" :key="token.id">
|
||||
<td class="px-4 py-3">{{ token.name }}</td>
|
||||
<td class="px-4 py-3 font-mono text-xs">{{ token.keyPrefix }}</td>
|
||||
<td class="px-4 py-3">{{ formatMcpTokenDate(token.createdAt) }}</td>
|
||||
<td class="px-4 py-3">{{ formatMcpTokenDate(token.lastUsedAt) }}</td>
|
||||
<td class="px-4 py-3">{{ formatMcpTokenDate(token.expiresAt) }}</td>
|
||||
<td class="px-4 py-3">
|
||||
<UBadge :color="token.active ? 'success' : 'neutral'" variant="soft">
|
||||
{{ token.active ? "Aktiv" : "Inaktiv" }}
|
||||
</UBadge>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-right">
|
||||
<UButton
|
||||
v-if="token.active"
|
||||
icon="i-heroicons-archive-box"
|
||||
color="error"
|
||||
variant="ghost"
|
||||
:loading="mcpTokenDeletingId === token.id"
|
||||
@click="deactivateMcpToken(token)"
|
||||
>
|
||||
Deaktivieren
|
||||
</UButton>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</UCard>
|
||||
</div>
|
||||
<div v-else-if="item.label === 'Funktionen'">
|
||||
<UCard class="mt-5">
|
||||
<UAlert
|
||||
|
||||
Reference in New Issue
Block a user