Files
api/web/components/ApiKey/ApiKeyManager.vue
Eli Bosley d37dc3bce2 feat: API key management (#1407)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Added a full-featured API key management UI, including creation,
listing, and deletion of API keys with customizable roles and
permissions.
  - Introduced a new page for API key management.
- Accordion UI components are now available for enhanced interface
interactions.
  - API now provides queries for possible API key roles and permissions.

- **Improvements**
- API key-related mutations are now grouped under a single field,
improving organization and usability.
  - Permissions can be assigned directly to API keys, not just roles.

- **Bug Fixes**
- Validation updated to require at least one role or permission when
creating an API key.

- **Documentation**
- Updated and added rules and configuration documentation for code
generation and testing.

- **Tests**
- Added and updated tests for new API key mutation logic; removed
obsolete tests for deprecated mutations.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-23 13:12:26 -04:00

126 lines
4.4 KiB
Vue

<script setup lang="ts">
import { ref, watchEffect } from 'vue';
import { useMutation, useQuery } from '@vue/apollo-composable';
import {
Button,
CardWrapper,
PageContainer,
} from '@unraid/ui';
import { DELETE_API_KEY, GET_API_KEY_META, GET_API_KEYS } from './apikey.query';
import ApiKeyCreate from './ApiKeyCreate.vue';
import { EyeIcon, EyeSlashIcon } from '@heroicons/vue/24/solid';
interface ApiKey {
id: string;
name: string;
description?: string;
createdAt: string;
roles: string[];
permissions: { resource: string; actions: string[] }[];
}
const { result, refetch } = useQuery<{ apiKeys: ApiKey[] }>(GET_API_KEYS);
const apiKeys = ref<ApiKey[]>([]);
watchEffect(() => {
apiKeys.value = result.value?.apiKeys || [];
});
const metaQuery = useQuery(GET_API_KEY_META);
const possibleRoles = ref<string[]>([]);
const possiblePermissions = ref<{ resource: string; actions: string[] }[]>([]);
watchEffect(() => {
possibleRoles.value = metaQuery.result.value?.apiKeyPossibleRoles || [];
possiblePermissions.value = metaQuery.result.value?.apiKeyPossiblePermissions || [];
});
const showCreate = ref(false);
const createdKey = ref<{ id: string; key: string } | null>(null);
const showKey = ref(false);
const { mutate: deleteKey } = useMutation(DELETE_API_KEY);
const deleteError = ref<string | null>(null);
function toggleShowKey() {
showKey.value = !showKey.value;
}
function onCreated(key: { id: string; key: string } | null) {
createdKey.value = key;
showCreate.value = false;
refetch();
}
async function _deleteKey(_id: string) {
if (!window.confirm('Are you sure you want to delete this API key? This action cannot be undone.')) return;
deleteError.value = null;
try {
await deleteKey({ input: { ids: [_id] } });
await refetch();
} catch (err: unknown) {
if (typeof err === 'object' && err !== null && 'message' in err && typeof (err as { message?: unknown }).message === 'string') {
deleteError.value = (err as { message: string }).message;
} else {
deleteError.value = 'Failed to delete API key.';
}
}
}
</script>
<template>
<PageContainer>
<CardWrapper>
<div class="flex items-center justify-between mb-4">
<h2 class="text-xl font-semibold">API Keys</h2>
<Button variant="primary" @click="showCreate = true">Create API Key</Button>
</div>
<div v-if="deleteError" class="mb-2 p-2 bg-red-100 text-red-700 border border-red-300 rounded">
{{ deleteError }}
</div>
<ul v-if="apiKeys.length" class="space-y-2 mb-4">
<li
v-for="key in apiKeys"
:key="key.id"
class="flex items-center justify-between p-2 border rounded"
>
<div>
<span class="font-medium">{{ key.name }}</span>
<div v-if="key.roles.length" class="mt-1">
<span class="font-semibold">Roles:</span>
<span>{{ key.roles.join(', ') }}</span>
</div>
<div v-if="key.permissions.length" class="mt-1">
<span class="font-semibold">Permissions:</span>
<ul class="ml-2">
<li v-for="perm in key.permissions" :key="perm.resource">
<span class="font-medium">{{ perm.resource }}</span>
<span v-if="perm.actions && perm.actions.length"> ({{ perm.actions.join(', ') }})</span>
</li>
</ul>
</div>
<div v-if="createdKey && createdKey.key && createdKey.id === key.id" class="mt-2 flex items-center gap-2">
<span>API Key created:</span>
<b>{{ showKey ? createdKey.key : '••••••••••••••••••••••••••••••••' }}</b>
<button type="button" class="focus:outline-none" @click="toggleShowKey">
<component :is="showKey ? EyeSlashIcon : EyeIcon" class="w-5 h-5 text-gray-500" />
</button>
</div>
</div>
<Button variant="destructive" size="sm" @click="_deleteKey(key.id)">Delete</Button>
</li>
</ul>
<div v-if="showCreate" class="mb-4 p-4 border rounded bg-muted">
<ApiKeyCreate
v-if="showCreate"
:possible-roles="possibleRoles"
:possible-permissions="possiblePermissions"
@created="onCreated"
@cancel="showCreate = false"
/>
</div>
</CardWrapper>
</PageContainer>
</template>