717 lines
22 KiB
Vue
717 lines
22 KiB
Vue
|
|
<template>
|
||
|
|
<div>
|
||
|
|
<v-row>
|
||
|
|
<v-col cols="12">
|
||
|
|
<h1 class="text-h3 mb-4">Settings</h1>
|
||
|
|
<p class="text-subtitle-1 text-grey mb-4">
|
||
|
|
Manage API keys and organization settings
|
||
|
|
</p>
|
||
|
|
</v-col>
|
||
|
|
</v-row>
|
||
|
|
|
||
|
|
<v-row v-if="!auth0.isAuthenticated.value">
|
||
|
|
<v-col cols="12">
|
||
|
|
<v-alert type="info" variant="tonal">
|
||
|
|
Please log in to manage settings.
|
||
|
|
</v-alert>
|
||
|
|
</v-col>
|
||
|
|
</v-row>
|
||
|
|
|
||
|
|
<v-row v-else-if="!selectedOrganization">
|
||
|
|
<v-col cols="12">
|
||
|
|
<v-alert type="info" variant="tonal">
|
||
|
|
Please select an organization to manage API keys.
|
||
|
|
</v-alert>
|
||
|
|
</v-col>
|
||
|
|
</v-row>
|
||
|
|
|
||
|
|
<v-row v-else>
|
||
|
|
<!-- Organizations Section -->
|
||
|
|
<v-col cols="12">
|
||
|
|
<v-card class="mb-4">
|
||
|
|
<v-card-title class="d-flex justify-space-between align-center">
|
||
|
|
<span>Organizations</span>
|
||
|
|
<v-btn
|
||
|
|
v-if="isAdmin"
|
||
|
|
color="primary"
|
||
|
|
prepend-icon="mdi-plus"
|
||
|
|
@click="openAddOrgDialog"
|
||
|
|
>
|
||
|
|
Create Organization
|
||
|
|
</v-btn>
|
||
|
|
</v-card-title>
|
||
|
|
<v-card-text>
|
||
|
|
<v-list v-if="organizations.length > 0">
|
||
|
|
<v-list-group
|
||
|
|
v-for="org in organizations"
|
||
|
|
:key="org.id"
|
||
|
|
class="mb-2"
|
||
|
|
>
|
||
|
|
<template #activator="{ props }">
|
||
|
|
<v-list-item
|
||
|
|
v-bind="props"
|
||
|
|
:active="org.id === selectedOrganization?.id"
|
||
|
|
border
|
||
|
|
>
|
||
|
|
<template #prepend>
|
||
|
|
<v-icon icon="mdi-domain" class="mr-2" />
|
||
|
|
</template>
|
||
|
|
<v-list-item-title class="font-weight-bold">
|
||
|
|
{{ org.name }}
|
||
|
|
</v-list-item-title>
|
||
|
|
<v-list-item-subtitle>
|
||
|
|
<v-chip size="small" class="mr-2" variant="tonal">
|
||
|
|
{{ org.apiKeys?.length || 0 }} API keys
|
||
|
|
</v-chip>
|
||
|
|
<v-chip size="small" variant="tonal">
|
||
|
|
{{ org.users?.length || 0 }} users
|
||
|
|
</v-chip>
|
||
|
|
</v-list-item-subtitle>
|
||
|
|
<template v-if="isAdmin" #append>
|
||
|
|
<v-btn
|
||
|
|
icon="mdi-delete"
|
||
|
|
variant="text"
|
||
|
|
color="error"
|
||
|
|
size="small"
|
||
|
|
@click.stop="openDeleteOrgDialog(org)"
|
||
|
|
/>
|
||
|
|
</template>
|
||
|
|
</v-list-item>
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<!-- Expandable content showing users -->
|
||
|
|
<v-list-item>
|
||
|
|
<v-list-item-title class="text-subtitle-2 mb-2">
|
||
|
|
<v-icon icon="mdi-account-multiple" size="small" class="mr-1" />
|
||
|
|
Users
|
||
|
|
</v-list-item-title>
|
||
|
|
<v-list density="compact" class="ml-4">
|
||
|
|
<v-list-item
|
||
|
|
v-for="user in org.users"
|
||
|
|
:key="user.id"
|
||
|
|
class="text-caption"
|
||
|
|
>
|
||
|
|
<template #prepend>
|
||
|
|
<v-icon :icon="getUserProviderIcon(user.id)" size="small" class="mr-2" />
|
||
|
|
</template>
|
||
|
|
<v-list-item-title class="text-body-2">
|
||
|
|
<v-chip v-if="getUserProvider(user.id)" size="x-small" variant="tonal" class="mr-2">
|
||
|
|
{{ getUserProvider(user.id) }}
|
||
|
|
</v-chip>
|
||
|
|
{{ getUserId(user.id) }}
|
||
|
|
</v-list-item-title>
|
||
|
|
<v-list-item-subtitle class="text-caption">
|
||
|
|
<span class="text-grey">Full ID: {{ user.id }}</span>
|
||
|
|
</v-list-item-subtitle>
|
||
|
|
</v-list-item>
|
||
|
|
<v-list-item v-if="!org.users || org.users.length === 0">
|
||
|
|
<v-list-item-title class="text-caption text-grey">
|
||
|
|
No users
|
||
|
|
</v-list-item-title>
|
||
|
|
</v-list-item>
|
||
|
|
</v-list>
|
||
|
|
</v-list-item>
|
||
|
|
</v-list-group>
|
||
|
|
</v-list>
|
||
|
|
<v-alert v-else type="info" variant="tonal">
|
||
|
|
<div class="d-flex flex-column align-center">
|
||
|
|
<p>You don't belong to any organizations yet.</p>
|
||
|
|
<p v-if="isAdmin" class="text-caption">Create one to get started.</p>
|
||
|
|
<p v-else class="text-caption">Contact your administrator to get access to an organization.</p>
|
||
|
|
</div>
|
||
|
|
</v-alert>
|
||
|
|
</v-card-text>
|
||
|
|
</v-card>
|
||
|
|
</v-col>
|
||
|
|
|
||
|
|
<!-- API Keys Section -->
|
||
|
|
<v-col cols="12">
|
||
|
|
<v-card>
|
||
|
|
<v-card-title class="d-flex justify-space-between align-center">
|
||
|
|
<span>API Keys - {{ selectedOrganization.name }}</span>
|
||
|
|
<v-btn
|
||
|
|
color="primary"
|
||
|
|
prepend-icon="mdi-plus"
|
||
|
|
@click="openAddKeyDialog"
|
||
|
|
>
|
||
|
|
Add API Key
|
||
|
|
</v-btn>
|
||
|
|
</v-card-title>
|
||
|
|
<v-card-text>
|
||
|
|
<v-list v-if="selectedOrganization.apiKeys && selectedOrganization.apiKeys.length > 0">
|
||
|
|
<v-list-item
|
||
|
|
v-for="apiKey in selectedOrganization.apiKeys"
|
||
|
|
:key="apiKey.id"
|
||
|
|
class="mb-2"
|
||
|
|
border
|
||
|
|
>
|
||
|
|
<template #prepend>
|
||
|
|
<v-icon icon="mdi-key" class="mr-2" />
|
||
|
|
</template>
|
||
|
|
<v-list-item-title class="font-weight-bold">
|
||
|
|
{{ apiKey.name }}
|
||
|
|
</v-list-item-title>
|
||
|
|
<v-list-item-subtitle>
|
||
|
|
<div class="mt-2">
|
||
|
|
<v-chip size="small" class="mr-2" variant="tonal">
|
||
|
|
{{ apiKey.refs?.length || 0 }} refs
|
||
|
|
</v-chip>
|
||
|
|
<v-chip
|
||
|
|
v-if="apiKey.read"
|
||
|
|
size="small"
|
||
|
|
class="mr-2"
|
||
|
|
color="blue"
|
||
|
|
variant="tonal"
|
||
|
|
>
|
||
|
|
Read
|
||
|
|
</v-chip>
|
||
|
|
<v-chip
|
||
|
|
v-if="apiKey.publish"
|
||
|
|
size="small"
|
||
|
|
color="green"
|
||
|
|
variant="tonal"
|
||
|
|
>
|
||
|
|
Publish
|
||
|
|
</v-chip>
|
||
|
|
</div>
|
||
|
|
<div v-if="apiKey.refs && apiKey.refs.length > 0" class="mt-2">
|
||
|
|
<strong>Refs:</strong> {{ apiKey.refs.join(', ') }}
|
||
|
|
</div>
|
||
|
|
</v-list-item-subtitle>
|
||
|
|
<template #append>
|
||
|
|
<v-btn
|
||
|
|
icon="mdi-delete"
|
||
|
|
variant="text"
|
||
|
|
color="error"
|
||
|
|
size="small"
|
||
|
|
@click.stop="openDeleteKeyDialog(apiKey)"
|
||
|
|
/>
|
||
|
|
</template>
|
||
|
|
</v-list-item>
|
||
|
|
</v-list>
|
||
|
|
<v-alert v-else type="info" variant="tonal">
|
||
|
|
No API keys yet. Create one to get started.
|
||
|
|
</v-alert>
|
||
|
|
</v-card-text>
|
||
|
|
</v-card>
|
||
|
|
</v-col>
|
||
|
|
</v-row>
|
||
|
|
|
||
|
|
<!-- Create Organization Dialog -->
|
||
|
|
<v-dialog v-model="addOrgDialog" max-width="500">
|
||
|
|
<v-card>
|
||
|
|
<v-card-title>Create Organization</v-card-title>
|
||
|
|
<v-card-text>
|
||
|
|
<v-text-field
|
||
|
|
v-model="newOrgName"
|
||
|
|
label="Organization Name"
|
||
|
|
variant="outlined"
|
||
|
|
:error-messages="orgNameError"
|
||
|
|
hint="A unique name for your organization"
|
||
|
|
persistent-hint
|
||
|
|
@keyup.enter="createOrganization"
|
||
|
|
/>
|
||
|
|
</v-card-text>
|
||
|
|
<v-card-actions>
|
||
|
|
<v-spacer />
|
||
|
|
<v-btn variant="text" @click="addOrgDialog = false">Cancel</v-btn>
|
||
|
|
<v-btn
|
||
|
|
color="primary"
|
||
|
|
variant="flat"
|
||
|
|
:loading="creatingOrg"
|
||
|
|
:disabled="!newOrgName"
|
||
|
|
@click="createOrganization"
|
||
|
|
>
|
||
|
|
Create
|
||
|
|
</v-btn>
|
||
|
|
</v-card-actions>
|
||
|
|
</v-card>
|
||
|
|
</v-dialog>
|
||
|
|
|
||
|
|
<!-- Add API Key Dialog -->
|
||
|
|
<v-dialog v-model="addKeyDialog" max-width="600">
|
||
|
|
<v-card>
|
||
|
|
<v-card-title>Create API Key</v-card-title>
|
||
|
|
<v-card-text>
|
||
|
|
<v-text-field
|
||
|
|
v-model="newKeyName"
|
||
|
|
label="Key Name"
|
||
|
|
variant="outlined"
|
||
|
|
:error-messages="keyNameError"
|
||
|
|
class="mb-4"
|
||
|
|
hint="A descriptive name for this API key"
|
||
|
|
persistent-hint
|
||
|
|
/>
|
||
|
|
|
||
|
|
<v-combobox
|
||
|
|
v-model="newKeyRefs"
|
||
|
|
label="Refs"
|
||
|
|
variant="outlined"
|
||
|
|
multiple
|
||
|
|
chips
|
||
|
|
:error-messages="refsError"
|
||
|
|
class="mb-4"
|
||
|
|
hint="Which refs this key can access (e.g., production, staging)"
|
||
|
|
persistent-hint
|
||
|
|
/>
|
||
|
|
|
||
|
|
<v-checkbox
|
||
|
|
v-model="newKeyRead"
|
||
|
|
label="Read Access"
|
||
|
|
hint="Allow reading schemas and supergraphs"
|
||
|
|
persistent-hint
|
||
|
|
/>
|
||
|
|
|
||
|
|
<v-checkbox
|
||
|
|
v-model="newKeyPublish"
|
||
|
|
label="Publish Access"
|
||
|
|
hint="Allow publishing/updating subgraph schemas"
|
||
|
|
persistent-hint
|
||
|
|
/>
|
||
|
|
</v-card-text>
|
||
|
|
<v-card-actions>
|
||
|
|
<v-spacer />
|
||
|
|
<v-btn variant="text" @click="addKeyDialog = false">Cancel</v-btn>
|
||
|
|
<v-btn
|
||
|
|
color="primary"
|
||
|
|
variant="flat"
|
||
|
|
:loading="creatingKey"
|
||
|
|
:disabled="!canCreateKey"
|
||
|
|
@click="createAPIKey"
|
||
|
|
>
|
||
|
|
Create Key
|
||
|
|
</v-btn>
|
||
|
|
</v-card-actions>
|
||
|
|
</v-card>
|
||
|
|
</v-dialog>
|
||
|
|
|
||
|
|
<!-- Show API Key Dialog (only shown once after creation) -->
|
||
|
|
<v-dialog v-model="showKeyDialog" max-width="600" persistent>
|
||
|
|
<v-card>
|
||
|
|
<v-card-title class="d-flex align-center">
|
||
|
|
<v-icon icon="mdi-alert" color="warning" class="mr-2" />
|
||
|
|
Save Your API Key
|
||
|
|
</v-card-title>
|
||
|
|
<v-card-text>
|
||
|
|
<v-alert type="warning" variant="tonal" class="mb-4">
|
||
|
|
This is the only time you'll see this key. Please copy it now and store it securely.
|
||
|
|
</v-alert>
|
||
|
|
|
||
|
|
<div class="mb-4">
|
||
|
|
<strong>Key Name:</strong> {{ createdKey?.name }}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<v-text-field
|
||
|
|
:model-value="createdKey?.key"
|
||
|
|
label="API Key"
|
||
|
|
variant="outlined"
|
||
|
|
readonly
|
||
|
|
class="mb-2"
|
||
|
|
>
|
||
|
|
<template #append-inner>
|
||
|
|
<v-btn
|
||
|
|
icon="mdi-content-copy"
|
||
|
|
variant="text"
|
||
|
|
size="small"
|
||
|
|
@click="copyKey"
|
||
|
|
/>
|
||
|
|
</template>
|
||
|
|
</v-text-field>
|
||
|
|
|
||
|
|
<div class="text-caption text-grey">
|
||
|
|
Use this key in the X-API-Key header when making requests to the schema registry.
|
||
|
|
</div>
|
||
|
|
</v-card-text>
|
||
|
|
<v-card-actions>
|
||
|
|
<v-spacer />
|
||
|
|
<v-btn
|
||
|
|
color="primary"
|
||
|
|
variant="flat"
|
||
|
|
@click="closeKeyDialog"
|
||
|
|
>
|
||
|
|
I've Saved the Key
|
||
|
|
</v-btn>
|
||
|
|
</v-card-actions>
|
||
|
|
</v-card>
|
||
|
|
</v-dialog>
|
||
|
|
|
||
|
|
<!-- Delete Organization Confirmation Dialog -->
|
||
|
|
<v-dialog v-model="deleteOrgDialog" max-width="500">
|
||
|
|
<v-card>
|
||
|
|
<v-card-title class="text-error">
|
||
|
|
<v-icon icon="mdi-alert" color="error" class="mr-2" />
|
||
|
|
Delete Organization
|
||
|
|
</v-card-title>
|
||
|
|
<v-card-text>
|
||
|
|
<v-alert type="warning" variant="tonal" class="mb-4">
|
||
|
|
This action cannot be undone. All API keys and data for this organization will be permanently removed.
|
||
|
|
</v-alert>
|
||
|
|
<p>Are you sure you want to delete <strong>{{ orgToDelete?.name }}</strong>?</p>
|
||
|
|
</v-card-text>
|
||
|
|
<v-card-actions>
|
||
|
|
<v-spacer />
|
||
|
|
<v-btn variant="text" @click="deleteOrgDialog = false">Cancel</v-btn>
|
||
|
|
<v-btn
|
||
|
|
color="error"
|
||
|
|
variant="flat"
|
||
|
|
:loading="deletingOrg"
|
||
|
|
@click="deleteOrganization"
|
||
|
|
>
|
||
|
|
Delete Organization
|
||
|
|
</v-btn>
|
||
|
|
</v-card-actions>
|
||
|
|
</v-card>
|
||
|
|
</v-dialog>
|
||
|
|
|
||
|
|
<!-- Delete API Key Confirmation Dialog -->
|
||
|
|
<v-dialog v-model="deleteKeyDialog" max-width="500">
|
||
|
|
<v-card>
|
||
|
|
<v-card-title class="text-error">
|
||
|
|
<v-icon icon="mdi-alert" color="error" class="mr-2" />
|
||
|
|
Delete API Key
|
||
|
|
</v-card-title>
|
||
|
|
<v-card-text>
|
||
|
|
<v-alert type="warning" variant="tonal" class="mb-4">
|
||
|
|
This action cannot be undone. Services using this key will lose access.
|
||
|
|
</v-alert>
|
||
|
|
<p>Are you sure you want to delete the API key <strong>{{ keyToDelete?.name }}</strong>?</p>
|
||
|
|
</v-card-text>
|
||
|
|
<v-card-actions>
|
||
|
|
<v-spacer />
|
||
|
|
<v-btn variant="text" @click="deleteKeyDialog = false">Cancel</v-btn>
|
||
|
|
<v-btn
|
||
|
|
color="error"
|
||
|
|
variant="flat"
|
||
|
|
:loading="deletingKey"
|
||
|
|
@click="deleteAPIKey"
|
||
|
|
>
|
||
|
|
Delete Key
|
||
|
|
</v-btn>
|
||
|
|
</v-card-actions>
|
||
|
|
</v-card>
|
||
|
|
</v-dialog>
|
||
|
|
|
||
|
|
<v-snackbar v-model="snackbar" :timeout="3000" :color="snackbarColor">
|
||
|
|
{{ snackbarText }}
|
||
|
|
</v-snackbar>
|
||
|
|
</div>
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<script setup lang="ts">
|
||
|
|
import { useAuth0 } from '@auth0/auth0-vue'
|
||
|
|
|
||
|
|
import { useOrganizationSelector } from '~/composables/useOrganizationSelector'
|
||
|
|
import {
|
||
|
|
useAddApiKeyMutation,
|
||
|
|
useAddOrganizationMutation,
|
||
|
|
useRemoveApiKeyMutation,
|
||
|
|
useRemoveOrganizationMutation,
|
||
|
|
} from '~/graphql/generated'
|
||
|
|
|
||
|
|
const auth0 = useAuth0()
|
||
|
|
const { selectedOrganization, organizations, refetch, selectOrganization } = useOrganizationSelector()
|
||
|
|
|
||
|
|
// Check if user has admin role
|
||
|
|
const isAdmin = computed(() => {
|
||
|
|
if (!auth0?.isAuthenticated?.value || !auth0?.user?.value) return false
|
||
|
|
|
||
|
|
const roles = auth0.user.value['https://unbound.se/roles']
|
||
|
|
return Array.isArray(roles) && roles.includes('admin')
|
||
|
|
})
|
||
|
|
|
||
|
|
// Create Organization Dialog
|
||
|
|
const addOrgDialog = ref(false)
|
||
|
|
const newOrgName = ref('')
|
||
|
|
const orgNameError = ref('')
|
||
|
|
const creatingOrg = ref(false)
|
||
|
|
|
||
|
|
// Add API Key Dialog
|
||
|
|
const addKeyDialog = ref(false)
|
||
|
|
const newKeyName = ref('')
|
||
|
|
const newKeyRefs = ref<string[]>([])
|
||
|
|
const newKeyRead = ref(true)
|
||
|
|
const newKeyPublish = ref(false)
|
||
|
|
const keyNameError = ref('')
|
||
|
|
const refsError = ref('')
|
||
|
|
const creatingKey = ref(false)
|
||
|
|
|
||
|
|
// Computed property for button state to ensure reactivity
|
||
|
|
const canCreateKey = computed(() => {
|
||
|
|
return newKeyName.value.trim() !== '' && newKeyRefs.value.length > 0
|
||
|
|
})
|
||
|
|
|
||
|
|
// Show created key dialog
|
||
|
|
const showKeyDialog = ref(false)
|
||
|
|
const createdKey = ref<any>(null)
|
||
|
|
|
||
|
|
// Snackbar
|
||
|
|
const snackbar = ref(false)
|
||
|
|
const snackbarText = ref('')
|
||
|
|
const snackbarColor = ref('success')
|
||
|
|
|
||
|
|
// Delete Organization Dialog
|
||
|
|
const deleteOrgDialog = ref(false)
|
||
|
|
const orgToDelete = ref<any>(null)
|
||
|
|
const deletingOrg = ref(false)
|
||
|
|
|
||
|
|
// Delete API Key Dialog
|
||
|
|
const deleteKeyDialog = ref(false)
|
||
|
|
const keyToDelete = ref<any>(null)
|
||
|
|
const deletingKey = ref(false)
|
||
|
|
|
||
|
|
// Mutations
|
||
|
|
const { mutate: addAPIKeyMutation } = useAddApiKeyMutation({})
|
||
|
|
const { mutate: addOrganizationMutation } = useAddOrganizationMutation({})
|
||
|
|
const { mutate: removeAPIKeyMutation } = useRemoveApiKeyMutation({})
|
||
|
|
const { mutate: removeOrganizationMutation } = useRemoveOrganizationMutation({})
|
||
|
|
|
||
|
|
const openAddOrgDialog = () => {
|
||
|
|
newOrgName.value = ''
|
||
|
|
orgNameError.value = ''
|
||
|
|
addOrgDialog.value = true
|
||
|
|
}
|
||
|
|
|
||
|
|
const createOrganization = async () => {
|
||
|
|
if (!newOrgName.value) {
|
||
|
|
orgNameError.value = 'Organization name is required'
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
creatingOrg.value = true
|
||
|
|
orgNameError.value = ''
|
||
|
|
|
||
|
|
try {
|
||
|
|
const result = await addOrganizationMutation({
|
||
|
|
name: newOrgName.value,
|
||
|
|
})
|
||
|
|
|
||
|
|
if (!result?.data?.addOrganization) {
|
||
|
|
throw new Error('Failed to create organization')
|
||
|
|
}
|
||
|
|
|
||
|
|
snackbarText.value = `Organization "${newOrgName.value}" created successfully!`
|
||
|
|
snackbarColor.value = 'success'
|
||
|
|
snackbar.value = true
|
||
|
|
addOrgDialog.value = false
|
||
|
|
newOrgName.value = ''
|
||
|
|
|
||
|
|
// Refresh organizations and select the new one
|
||
|
|
await refetch()
|
||
|
|
selectOrganization(result.data.addOrganization.id)
|
||
|
|
} catch (error: any) {
|
||
|
|
orgNameError.value = error.message || 'Failed to create organization'
|
||
|
|
snackbarText.value = 'Failed to create organization'
|
||
|
|
snackbarColor.value = 'error'
|
||
|
|
snackbar.value = true
|
||
|
|
} finally {
|
||
|
|
creatingOrg.value = false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const openAddKeyDialog = () => {
|
||
|
|
newKeyName.value = ''
|
||
|
|
newKeyRefs.value = []
|
||
|
|
newKeyRead.value = true
|
||
|
|
newKeyPublish.value = false
|
||
|
|
keyNameError.value = ''
|
||
|
|
refsError.value = ''
|
||
|
|
addKeyDialog.value = true
|
||
|
|
}
|
||
|
|
|
||
|
|
const createAPIKey = async () => {
|
||
|
|
if (!newKeyName.value) {
|
||
|
|
keyNameError.value = 'Key name is required'
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
if (newKeyRefs.value.length === 0) {
|
||
|
|
refsError.value = 'At least one ref is required'
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!selectedOrganization.value) {
|
||
|
|
snackbarText.value = 'No organization selected'
|
||
|
|
snackbarColor.value = 'error'
|
||
|
|
snackbar.value = true
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
creatingKey.value = true
|
||
|
|
keyNameError.value = ''
|
||
|
|
refsError.value = ''
|
||
|
|
|
||
|
|
try {
|
||
|
|
const result = await addAPIKeyMutation({
|
||
|
|
input: {
|
||
|
|
name: newKeyName.value,
|
||
|
|
organizationId: selectedOrganization.value.id,
|
||
|
|
refs: newKeyRefs.value,
|
||
|
|
read: newKeyRead.value,
|
||
|
|
publish: newKeyPublish.value,
|
||
|
|
},
|
||
|
|
})
|
||
|
|
|
||
|
|
if (!result?.data?.addAPIKey) {
|
||
|
|
throw new Error('Failed to create API key')
|
||
|
|
}
|
||
|
|
|
||
|
|
createdKey.value = result.data.addAPIKey
|
||
|
|
addKeyDialog.value = false
|
||
|
|
showKeyDialog.value = true
|
||
|
|
|
||
|
|
// Refresh organizations to show new key
|
||
|
|
await refetch()
|
||
|
|
} catch (error: any) {
|
||
|
|
keyNameError.value = error.message || 'Failed to create API key'
|
||
|
|
snackbarText.value = 'Failed to create API key'
|
||
|
|
snackbarColor.value = 'error'
|
||
|
|
snackbar.value = true
|
||
|
|
} finally {
|
||
|
|
creatingKey.value = false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const copyKey = async () => {
|
||
|
|
try {
|
||
|
|
await navigator.clipboard.writeText(createdKey.value?.key || '')
|
||
|
|
snackbarText.value = 'API key copied to clipboard'
|
||
|
|
snackbarColor.value = 'success'
|
||
|
|
snackbar.value = true
|
||
|
|
} catch (_err) {
|
||
|
|
snackbarText.value = 'Failed to copy'
|
||
|
|
snackbarColor.value = 'error'
|
||
|
|
snackbar.value = true
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const closeKeyDialog = () => {
|
||
|
|
showKeyDialog.value = false
|
||
|
|
createdKey.value = null
|
||
|
|
}
|
||
|
|
|
||
|
|
// Helper functions to parse user IDs
|
||
|
|
const getUserProvider = (userId: string) => {
|
||
|
|
if (!userId) return ''
|
||
|
|
const parts = userId.split('|')
|
||
|
|
if (parts.length === 2) {
|
||
|
|
// Map provider names to more readable formats
|
||
|
|
const providerMap: Record<string, string> = {
|
||
|
|
'google-oauth2': 'Google',
|
||
|
|
'auth0': 'Email/Password',
|
||
|
|
'github': 'GitHub',
|
||
|
|
'twitter': 'Twitter',
|
||
|
|
'facebook': 'Facebook',
|
||
|
|
'windowslive': 'Microsoft',
|
||
|
|
'linkedin': 'LinkedIn',
|
||
|
|
}
|
||
|
|
return providerMap[parts[0]] || parts[0]
|
||
|
|
}
|
||
|
|
return ''
|
||
|
|
}
|
||
|
|
|
||
|
|
const getUserId = (userId: string) => {
|
||
|
|
if (!userId) return ''
|
||
|
|
const parts = userId.split('|')
|
||
|
|
if (parts.length === 2) {
|
||
|
|
return parts[1]
|
||
|
|
}
|
||
|
|
return userId
|
||
|
|
}
|
||
|
|
|
||
|
|
const getUserProviderIcon = (userId: string) => {
|
||
|
|
if (!userId) return 'mdi-account'
|
||
|
|
const parts = userId.split('|')
|
||
|
|
if (parts.length === 2) {
|
||
|
|
const iconMap: Record<string, string> = {
|
||
|
|
'google-oauth2': 'mdi-google',
|
||
|
|
'auth0': 'mdi-email',
|
||
|
|
'github': 'mdi-github',
|
||
|
|
'twitter': 'mdi-twitter',
|
||
|
|
'facebook': 'mdi-facebook',
|
||
|
|
'windowslive': 'mdi-microsoft',
|
||
|
|
'linkedin': 'mdi-linkedin',
|
||
|
|
}
|
||
|
|
return iconMap[parts[0]] || 'mdi-account'
|
||
|
|
}
|
||
|
|
return 'mdi-account'
|
||
|
|
}
|
||
|
|
|
||
|
|
const openDeleteOrgDialog = (org: any) => {
|
||
|
|
orgToDelete.value = org
|
||
|
|
deleteOrgDialog.value = true
|
||
|
|
}
|
||
|
|
|
||
|
|
const deleteOrganization = async () => {
|
||
|
|
if (!orgToDelete.value) return
|
||
|
|
|
||
|
|
deletingOrg.value = true
|
||
|
|
|
||
|
|
try {
|
||
|
|
const result = await removeOrganizationMutation({
|
||
|
|
organizationId: orgToDelete.value.id,
|
||
|
|
})
|
||
|
|
|
||
|
|
if (!result?.data?.removeOrganization) {
|
||
|
|
throw new Error('Failed to delete organization')
|
||
|
|
}
|
||
|
|
|
||
|
|
snackbarText.value = `Organization "${orgToDelete.value.name}" deleted successfully`
|
||
|
|
snackbarColor.value = 'success'
|
||
|
|
snackbar.value = true
|
||
|
|
deleteOrgDialog.value = false
|
||
|
|
orgToDelete.value = null
|
||
|
|
|
||
|
|
// Refresh organizations
|
||
|
|
await refetch()
|
||
|
|
|
||
|
|
// If deleted org was selected, select another one
|
||
|
|
if (organizations.value.length > 0) {
|
||
|
|
selectOrganization(organizations.value[0].id)
|
||
|
|
}
|
||
|
|
} catch (error: any) {
|
||
|
|
snackbarText.value = error.message || 'Failed to delete organization'
|
||
|
|
snackbarColor.value = 'error'
|
||
|
|
snackbar.value = true
|
||
|
|
} finally {
|
||
|
|
deletingOrg.value = false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const openDeleteKeyDialog = (apiKey: any) => {
|
||
|
|
keyToDelete.value = apiKey
|
||
|
|
deleteKeyDialog.value = true
|
||
|
|
}
|
||
|
|
|
||
|
|
const deleteAPIKey = async () => {
|
||
|
|
if (!keyToDelete.value || !selectedOrganization.value) return
|
||
|
|
|
||
|
|
deletingKey.value = true
|
||
|
|
|
||
|
|
try {
|
||
|
|
const result = await removeAPIKeyMutation({
|
||
|
|
organizationId: selectedOrganization.value.id,
|
||
|
|
keyName: keyToDelete.value.name,
|
||
|
|
})
|
||
|
|
|
||
|
|
if (!result?.data?.removeAPIKey) {
|
||
|
|
throw new Error('Failed to delete API key')
|
||
|
|
}
|
||
|
|
|
||
|
|
snackbarText.value = `API key "${keyToDelete.value.name}" deleted successfully`
|
||
|
|
snackbarColor.value = 'success'
|
||
|
|
snackbar.value = true
|
||
|
|
deleteKeyDialog.value = false
|
||
|
|
keyToDelete.value = null
|
||
|
|
|
||
|
|
// Refresh organizations
|
||
|
|
await refetch()
|
||
|
|
} catch (error: any) {
|
||
|
|
snackbarText.value = error.message || 'Failed to delete API key'
|
||
|
|
snackbarColor.value = 'error'
|
||
|
|
snackbar.value = true
|
||
|
|
} finally {
|
||
|
|
deletingKey.value = false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
</script>
|