<template>
<div class="api-keys-panel">
<div class="api-keys-header">
<h2 class="api-keys-title">API Keys</h2>
<GnButton variant="primary" icon="ph ph-plus" @click="openCreateModal">
New Key
</GnButton>
</div>
<p class="api-keys-description">
API keys allow headless clients (scripts, voice assistants, IoT devices)
to authenticate without browser-based OAuth.
</p>
<div v-if="tokensStore.loading" class="api-keys-loading">Loading...</div>
<div v-else-if="!tokensStore.tokens.length" class="api-keys-empty">
No API keys yet. Create one to get started.
</div>
<div v-else class="api-keys-table-wrapper">
<div class="api-keys-table">
<div class="api-keys-row api-keys-header-row">
<span class="col-name">Name</span>
<span class="col-prefix">Key</span>
<span class="col-created">Created</span>
<span class="col-last-used">Last used</span>
<span class="col-actions"></span>
</div>
<div
v-for="token in tokensStore.tokens"
:key="token.id"
class="api-keys-row">
<span class="col-name">{{ token.name }}</span>
<span class="col-prefix">{{ token.token_prefix }}</span>
<span class="col-created">{{ formatDate(token.created_at) }}</span>
<span class="col-last-used">{{ formatDate(token.last_used_at) || 'Never' }}</span>
<span class="col-actions">
<GnIconButton
icon="ph ph-trash"
label="Revoke"
variant="ghost"
@click="onRevoke(token.id, token.name)"
/>
</span>
</div>
</div>
</div>
<CreateKeyModal
:open="createModalOpen"
:creating="tokensStore.creating"
@close="createModalOpen = false"
@submit="onCreate"
/>
<ShowTokenModal
:open="showTokenModal"
:token="createdToken"
@close="showTokenModal = false"
/>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useApiTokensStore } from '@/stores/apiTokens.js'
import { useConfirm } from '@/composables/useConfirm.js'
import { useToast } from 'gnexus-ui-kit/vue'
import CreateKeyModal from './CreateKeyModal.vue'
import ShowTokenModal from './ShowTokenModal.vue'
const tokensStore = useApiTokensStore()
const confirm = useConfirm()
const toast = useToast()
const createModalOpen = ref(false)
const showTokenModal = ref(false)
const createdToken = ref('')
onMounted(() => {
tokensStore.fetchTokens()
})
function openCreateModal() {
createModalOpen.value = true
}
async function onCreate(name) {
try {
const token = await tokensStore.createToken(name)
createModalOpen.value = false
createdToken.value = token.token
showTokenModal.value = true
} catch (err) {
toast.error({ title: 'Failed to create key', text: err.message })
}
}
async function onRevoke(id, name) {
const ok = await confirm(`Revoke API key "${name}"? This cannot be undone.`)
if (!ok) return
try {
await tokensStore.revokeToken(id)
toast.success({ title: 'API key revoked' })
} catch (err) {
toast.error({ title: 'Failed to revoke key', text: err.message })
}
}
function formatDate(iso) {
if (!iso) return null
const d = new Date(iso)
return d.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' })
}
</script>
<style scoped lang="scss">
.api-keys-panel {
max-width: 800px;
}
.api-keys-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.api-keys-title {
font-size: 1.1rem;
font-weight: 600;
margin: 0;
color: var(--color-text, #a9b1d6);
}
.api-keys-description {
margin: 0 0 16px;
font-size: 0.875rem;
color: var(--color-text-secondary, #a9b1d6);
}
.api-keys-loading,
.api-keys-empty {
padding: 24px;
text-align: center;
color: var(--color-text-dark, #787c99);
font-size: 0.9rem;
}
.api-keys-row {
display: grid;
grid-template-columns: 2fr 1fr 1fr 1fr auto;
gap: 12px;
align-items: center;
padding: 10px 12px;
font-size: 0.875rem;
border-bottom: 1px solid var(--color-border, #3b4261);
}
.api-keys-header-row {
font-weight: 600;
color: var(--color-text-secondary, #a9b1d6);
border-bottom-width: 2px;
}
.col-name {
color: var(--color-text, #a9b1d6);
}
.col-prefix {
font-family: monospace;
color: var(--color-text-secondary, #a9b1d6);
}
.col-created,
.col-last-used {
color: var(--color-text-dark, #787c99);
}
.col-actions {
text-align: right;
}
</style>