Newer
Older
navi-1 / webclient / src / components / settings / ApiKeysPanel.vue
<template>
  <div class="api-keys-panel">
    <div class="api-keys-header">
      <h2 class="api-keys-title">{{ t('settings.keysTitle') }}</h2>
      <GnButton variant="primary" icon="ph ph-plus" @click="openCreateModal">
        {{ t('settings.keysCreate') }}
      </GnButton>
    </div>

    <p class="api-keys-description">{{ t('settings.keysDescription') }}</p>

    <GnAlert v-if="authStore.isNoAuthMode" variant="warning" class="api-keys-warning">
      <i class="ph ph-warning-circle"></i>
      {{ t('settings.keysNoAuthWarning') }}
    </GnAlert>

    <div v-if="tokensStore.loading" class="api-keys-loading">
      <GnLoader circle :label="t('settings.keysLoading')" />
    </div>

    <GnTable
      v-else
      :columns="columns"
      :rows="tokensStore.tokens"
      :empty-text="t('settings.keysEmpty')"
    >
      <template #cell-token_prefix="{ row }">
        <code>{{ row.token_prefix }}</code>
      </template>
      <template #cell-created_at="{ row }">
        {{ formatDate(row.created_at) }}
      </template>
      <template #cell-last_used_at="{ row }">
        {{ formatDate(row.last_used_at) || t('common.never') }}
      </template>
      <template #cell-actions="{ row }">
        <GnIconButton
          icon="ph ph-trash"
          :label="t('common.revoke')"
          @click="onRevoke(row.id, row.name)"
        />
      </template>
    </GnTable>

    <CreateKeyModal
      :open="createModalOpen"
      :creating="tokensStore.creating"
      @close="createModalOpen = false"
      @submit="onCreate"
    />

    <ShowTokenModal
      :open="showTokenModal"
      :token="createdToken"
      @close="showTokenModal = false"
    />
  </div>
</template>

<script setup>
import { computed, ref, onMounted } from 'vue'
import { locale, t } from '@/i18n/index.js'
import { useApiTokensStore } from '@/stores/apiTokens.js'
import { useAuthStore } from '@/stores/auth.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 authStore = useAuthStore()
const confirm = useConfirm()
const toast = useToast()

const createModalOpen = ref(false)
const showTokenModal = ref(false)
const createdToken = ref('')

// Computed, not a constant: a table built once would keep the language that was
// active when the panel opened.
const columns = computed(() => [
  { key: 'name', label: t('col.name') },
  { key: 'token_prefix', label: t('col.key') },
  { key: 'created_at', label: t('col.created') },
  { key: 'last_used_at', label: t('col.lastUsed') },
  { key: 'actions', label: '' },
])

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: t('toast.keyCreateFailed'), text: err.message })
  }
}

async function onRevoke(id, name) {
  const ok = await confirm(t('confirm.revokeKey', { name }))
  if (!ok) return
  try {
    await tokensStore.revokeToken(id)
    toast.success({ title: t('toast.keyRevoked') })
  } catch (err) {
    toast.error({ title: t('toast.keyRevokeFailed'), text: err.message })
  }
}

function formatDate(iso) {
  if (!iso) return null
  const d = new Date(iso)
  return d.toLocaleDateString(locale.value, { year: 'numeric', month: 'short', day: 'numeric' })
}
</script>

<style scoped lang="scss">
.api-keys-panel {
  margin-bottom: 24px;
}

.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(--gn-color-text-medium);
}

.api-keys-description {
  margin: 0 0 16px;
  font-size: 0.875rem;
  color: var(--gn-color-text-medium);
}

.api-keys-warning {
  margin-bottom: 16px;

  i {
    font-size: 16px;
    flex-shrink: 0;
  }
}

.api-keys-loading {
  padding: 24px;
  text-align: center;
}

.api-keys-panel code {
  font-family: var(--gn-font-family-base);
  color: var(--gn-color-text-medium);
}
</style>