Files
api/web/components/ApiKey/EffectivePermissions.vue
Eli Bosley 674323fd87 feat: generated UI API key management + OAuth-like API Key Flows (#1609)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* API Key Authorization flow with consent screen, callback support, and
a Tools page.
* Schema-driven API Key creation UI with permission presets, templates,
and Developer Authorization Link.
* Effective Permissions preview and a new multi-select permission
control.

* **UI Improvements**
* Mask/toggle API keys, copy-to-clipboard with toasts, improved select
labels, new label styles, tab wrapping, and accordionized color
controls.

* **Documentation**
  * Public guide for the API Key authorization flow and scopes added.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-08-27 12:37:39 -04:00

132 lines
4.0 KiB
Vue

<script setup lang="ts">
import { computed, watch } from 'vue';
import { useLazyQuery } from '@vue/apollo-composable';
import { Badge } from '@unraid/ui';
import { PREVIEW_EFFECTIVE_PERMISSIONS } from './permissions-preview.query';
import type { AuthAction, Role, PreviewEffectivePermissionsQuery } from '~/composables/gql/graphql';
interface RawPermission {
resource: string;
actions: AuthAction[];
}
interface Props {
roles?: Role[];
rawPermissions?: RawPermission[];
showHeader?: boolean;
headerText?: string;
}
const props = withDefaults(defineProps<Props>(), {
roles: () => [],
rawPermissions: () => [],
showHeader: true,
headerText: 'Effective Permissions',
});
// Query for effective permissions
const { load: loadEffectivePermissions, loading, result } = useLazyQuery<PreviewEffectivePermissionsQuery>(PREVIEW_EFFECTIVE_PERMISSIONS);
// Computed property for effective permissions from the result
const effectivePermissions = computed(() => {
return result.value?.previewEffectivePermissions || [];
});
// Format action for display - show the actual enum value or formatted string
const formatAction = (action: string): string => {
if (action === '*') return 'ALL ACTIONS';
// If it's already an enum value like CREATE_ANY, READ_ANY, show as-is
if (action.includes('_')) {
return action; // Keep the original enum format
}
// If it's in scope format like 'create:any' or just 'create', format for display
if (action.includes(':')) {
return action.split(':')[0].toUpperCase() + ':' + action.split(':')[1].toUpperCase();
}
// For simple verbs, uppercase them
return action.toUpperCase();
};
// Watch for changes to roles and permissions and reload
watch(
() => ({
roles: props.roles,
rawPermissions: props.rawPermissions,
}),
async ({ roles, rawPermissions }) => {
// Skip if no roles or permissions
if ((!roles || roles.length === 0) && (!rawPermissions || rawPermissions.length === 0)) {
return;
}
try {
// Transform permissions to the format expected by the query
const permissions = rawPermissions?.map(perm => ({
resource: perm.resource,
actions: perm.actions
})) || [];
// Call load with the parameters
await loadEffectivePermissions(null, {
roles: roles || [],
permissions: permissions.length > 0 ? permissions : undefined,
});
} catch (error) {
console.error('Failed to load effective permissions:', error);
}
},
{ immediate: true, deep: true }
);
</script>
<template>
<div class="w-full">
<h3 v-if="showHeader" class="text-sm font-semibold mb-3 flex items-center gap-2">
{{ headerText }}
<span v-if="loading" class="text-xs text-muted-foreground">(loading...)</span>
</h3>
<!-- Show effective permissions -->
<div v-if="effectivePermissions.length > 0 && !loading" class="space-y-2">
<div class="text-xs text-muted-foreground mb-2">
These are the actual permissions that will be granted based on selected roles and custom permissions:
</div>
<div class="space-y-2 max-h-64 overflow-y-auto">
<div
v-for="perm in effectivePermissions"
:key="perm.resource"
class="text-xs bg-background p-2 rounded border border-muted"
>
<div class="flex items-center gap-2 mb-1">
<span class="font-medium">{{ perm.resource }}</span>
</div>
<div class="flex flex-wrap gap-1">
<Badge
v-for="action in perm.actions"
:key="action"
variant="green"
size="xs"
>
{{ formatAction(action) }}
</Badge>
</div>
</div>
</div>
</div>
<!-- Show loading state -->
<div v-else-if="loading" class="text-xs text-muted-foreground">
Loading permissions...
</div>
<!-- Show message when no permissions selected -->
<div v-else class="text-xs text-muted-foreground italic">
No permissions selected yet
</div>
</div>
</template>