- Simplified the application creation process by consolidating form fields and enhancing validation. - Updated the create application page to streamline user experience with clearer provider options and improved layout. - Implemented SweetAlert for success and error notifications during user actions, replacing traditional alerts. - Enhanced the applications index page with dynamic filtering and improved data fetching from the Authentik API. - Refactored API endpoints to utilize slugs for application identification, ensuring consistency with Authentik's structure. - Improved authentication handling by updating the requireAuth utility to support cookie-based authentication.
316 lines
12 KiB
Vue
316 lines
12 KiB
Vue
<script setup>
|
|
definePageMeta({
|
|
title: "View Application",
|
|
middleware: ["auth"],
|
|
requiresAuth: true,
|
|
breadcrumb: [
|
|
{ name: "Dashboard", path: "/dashboard" },
|
|
{ name: "Applications", path: "/applications" },
|
|
{ name: "View Application", path: "", type: "current" },
|
|
],
|
|
});
|
|
|
|
import { ref, onMounted, computed } from "vue";
|
|
|
|
// Get application ID from route
|
|
const route = useRoute();
|
|
const applicationId = route.params.id;
|
|
|
|
// No need for separate toast reference as we're using $swal
|
|
|
|
// State management
|
|
const application = ref(null);
|
|
const isLoading = ref(true);
|
|
const isDeleting = ref(false);
|
|
|
|
// Fetch application details
|
|
const fetchApplication = async () => {
|
|
try {
|
|
isLoading.value = true;
|
|
const response = await $fetch(`/api/applications/${applicationId}`);
|
|
|
|
// Transform response to match our frontend format
|
|
application.value = {
|
|
id: response.slug,
|
|
name: response.name,
|
|
slug: response.slug,
|
|
description: response.meta_description || "No description provided",
|
|
status: "active", // Authentik applications are active by default
|
|
provider: response.provider_obj?.verbose_name || response.provider_obj?.name || "No Provider",
|
|
authentikId: response.pk,
|
|
launchUrl: response.launch_url || response.meta_launch_url || "#",
|
|
icon: response.meta_icon || null,
|
|
publisher: response.meta_publisher || "System",
|
|
createdAt: response.created || new Date().toISOString(),
|
|
providerType: response.provider_obj?.verbose_name_plural || "Unknown",
|
|
providerDetails: response.provider_obj || null,
|
|
};
|
|
} catch (error) {
|
|
console.error("Failed to fetch application:", error);
|
|
application.value = null;
|
|
} finally {
|
|
isLoading.value = false;
|
|
}
|
|
};
|
|
|
|
// Delete application
|
|
const deleteApplication = async () => {
|
|
if (
|
|
!confirm(
|
|
"Are you sure you want to delete this application? This action cannot be undone and will remove all associated configurations."
|
|
)
|
|
) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
isDeleting.value = true;
|
|
await $fetch(`/api/applications/${applicationId}`, {
|
|
method: "DELETE",
|
|
});
|
|
|
|
console.log("✅ Application deleted successfully");
|
|
|
|
// Use SweetAlert instead of toast
|
|
const { $swal } = useNuxtApp();
|
|
$swal.fire({
|
|
icon: 'success',
|
|
title: 'Success',
|
|
text: 'Application deleted successfully!',
|
|
timer: 2000,
|
|
showConfirmButton: false
|
|
});
|
|
|
|
// Redirect to applications list
|
|
await navigateTo("/applications");
|
|
} catch (error) {
|
|
console.error("❌ Failed to delete application:", error);
|
|
|
|
let errorMessage = "Failed to delete application. ";
|
|
if (error.data?.message) {
|
|
errorMessage += error.data.message;
|
|
} else if (error.statusCode === 404) {
|
|
errorMessage += "Application not found.";
|
|
} else if (error.statusCode === 401) {
|
|
errorMessage += "You are not authorized to delete this application.";
|
|
} else {
|
|
errorMessage += "Please try again.";
|
|
}
|
|
|
|
const { $swal } = useNuxtApp();
|
|
$swal.fire({
|
|
icon: 'error',
|
|
title: 'Error',
|
|
text: errorMessage
|
|
});
|
|
} finally {
|
|
isDeleting.value = false;
|
|
}
|
|
};
|
|
|
|
// Computed properties
|
|
const providerTypeIcon = computed(() => {
|
|
if (!application.value?.providerDetails) return "ph:shield";
|
|
|
|
const providerType = application.value.providerDetails.verbose_name_plural?.toLowerCase();
|
|
if (providerType?.includes("oauth")) return "ph:key";
|
|
if (providerType?.includes("saml")) return "ph:certificate";
|
|
if (providerType?.includes("ldap")) return "ph:tree-structure";
|
|
if (providerType?.includes("proxy")) return "ph:arrows-left-right";
|
|
return "ph:shield";
|
|
});
|
|
|
|
const statusVariant = computed(() => {
|
|
return application.value?.status === "active" ? "success" : "secondary";
|
|
});
|
|
|
|
const formatDate = (dateString) => {
|
|
if (!dateString) return "Never";
|
|
return new Date(dateString).toLocaleDateString() + " " + new Date(dateString).toLocaleTimeString();
|
|
};
|
|
|
|
// Initialize
|
|
onMounted(() => {
|
|
fetchApplication();
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<div>
|
|
<LayoutsBreadcrumb />
|
|
|
|
<!-- Loading State -->
|
|
<div v-if="isLoading" class="flex items-center justify-center py-12">
|
|
<div class="text-center">
|
|
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto"></div>
|
|
<p class="text-gray-600 dark:text-gray-400 mt-4">Loading application details...</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Error State -->
|
|
<div v-else-if="!application" class="text-center py-12">
|
|
<Icon name="ph:warning-circle" class="w-16 h-16 text-red-500 mx-auto mb-4" />
|
|
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-2">Application Not Found</h3>
|
|
<p class="text-gray-600 dark:text-gray-400 mb-6">
|
|
The application you're looking for doesn't exist or you don't have permission to view it.
|
|
</p>
|
|
<rs-button @click="navigateTo('/applications')" variant="primary">
|
|
<Icon name="ph:arrow-left" class="w-4 h-4 mr-2" />
|
|
Back to Applications
|
|
</rs-button>
|
|
</div>
|
|
|
|
<!-- Application Details -->
|
|
<div v-else>
|
|
<!-- Header -->
|
|
<div class="mb-6">
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center space-x-4">
|
|
<div class="flex-shrink-0">
|
|
<div class="h-12 w-12 rounded-lg bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center">
|
|
<span class="text-lg font-bold text-white">
|
|
{{ application.name ? application.name.charAt(0).toUpperCase() : "?" }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">
|
|
{{ application.name }}
|
|
</h1>
|
|
<p class="text-gray-600 dark:text-gray-400">
|
|
{{ application.description }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div class="flex space-x-3">
|
|
<rs-button @click="navigateTo('/applications')" variant="secondary">
|
|
<Icon name="ph:arrow-left" class="w-4 h-4 mr-2" />
|
|
Back to List
|
|
</rs-button>
|
|
<rs-button @click="navigateTo(`/applications/${applicationId}/edit`)" variant="primary-outline">
|
|
<Icon name="ph:pencil" class="w-4 h-4 mr-2" />
|
|
Edit Application
|
|
</rs-button>
|
|
<rs-button
|
|
@click="deleteApplication"
|
|
:disabled="isDeleting"
|
|
variant="danger"
|
|
>
|
|
<Icon name="ph:trash" class="w-4 h-4 mr-2" />
|
|
{{ isDeleting ? 'Deleting...' : 'Delete' }}
|
|
</rs-button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Application Information Cards -->
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
|
<!-- Basic Information -->
|
|
<rs-card>
|
|
<template #header>
|
|
<Icon name="ph:info" class="text-blue-600" />
|
|
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Basic Information</h3>
|
|
</template>
|
|
<template #body>
|
|
<div class="space-y-4">
|
|
<div class="flex justify-between items-start">
|
|
<span class="text-sm font-medium text-gray-600 dark:text-gray-400">Status</span>
|
|
<rs-badge :variant="statusVariant">{{ application.status }}</rs-badge>
|
|
</div>
|
|
|
|
<div class="flex justify-between items-start">
|
|
<span class="text-sm font-medium text-gray-600 dark:text-gray-400">Application ID</span>
|
|
<span class="text-sm text-gray-900 dark:text-white font-mono">{{ application.slug }}</span>
|
|
</div>
|
|
|
|
<div class="flex justify-between items-start">
|
|
<span class="text-sm font-medium text-gray-600 dark:text-gray-400">Launch URL</span>
|
|
<a
|
|
:href="application.launchUrl"
|
|
target="_blank"
|
|
class="text-sm text-primary hover:underline flex items-center"
|
|
>
|
|
{{ application.launchUrl }}
|
|
<Icon name="ph:arrow-square-out" class="w-3 h-3 ml-1" />
|
|
</a>
|
|
</div>
|
|
|
|
<div class="flex justify-between items-start">
|
|
<span class="text-sm font-medium text-gray-600 dark:text-gray-400">Publisher</span>
|
|
<span class="text-sm text-gray-900 dark:text-white">{{ application.publisher }}</span>
|
|
</div>
|
|
|
|
<div class="flex justify-between items-start">
|
|
<span class="text-sm font-medium text-gray-600 dark:text-gray-400">Created</span>
|
|
<span class="text-sm text-gray-900 dark:text-white">{{ formatDate(application.createdAt) }}</span>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</rs-card>
|
|
|
|
<!-- Provider Information -->
|
|
<rs-card>
|
|
<template #header>
|
|
<Icon :name="providerTypeIcon" class="text-green-600" />
|
|
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Authentication Provider</h3>
|
|
</template>
|
|
<template #body>
|
|
<div class="space-y-4">
|
|
<div class="flex justify-between items-start">
|
|
<span class="text-sm font-medium text-gray-600 dark:text-gray-400">Provider Type</span>
|
|
<span class="text-sm text-gray-900 dark:text-white">{{ application.provider }}</span>
|
|
</div>
|
|
|
|
<div v-if="application.providerDetails" class="space-y-3">
|
|
<div class="flex justify-between items-start">
|
|
<span class="text-sm font-medium text-gray-600 dark:text-gray-400">Provider ID</span>
|
|
<span class="text-sm text-gray-900 dark:text-white font-mono">{{ application.providerDetails.pk }}</span>
|
|
</div>
|
|
|
|
<div v-if="application.providerDetails.client_id" class="flex justify-between items-start">
|
|
<span class="text-sm font-medium text-gray-600 dark:text-gray-400">Client ID</span>
|
|
<span class="text-sm text-gray-900 dark:text-white font-mono">{{ application.providerDetails.client_id }}</span>
|
|
</div>
|
|
|
|
<div v-if="application.providerDetails.authorization_flow_name" class="flex justify-between items-start">
|
|
<span class="text-sm font-medium text-gray-600 dark:text-gray-400">Auth Flow</span>
|
|
<span class="text-sm text-gray-900 dark:text-white">{{ application.providerDetails.authorization_flow_name }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-else class="text-center py-4">
|
|
<Icon name="ph:warning" class="w-8 h-8 text-orange-500 mx-auto mb-2" />
|
|
<p class="text-sm text-orange-600 dark:text-orange-400">No provider configured</p>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</rs-card>
|
|
</div>
|
|
|
|
<!-- Access Control -->
|
|
<rs-card>
|
|
<template #header>
|
|
<Icon name="ph:users" class="text-purple-600" />
|
|
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Access Control</h3>
|
|
</template>
|
|
<template #body>
|
|
<div class="text-center py-8">
|
|
<Icon name="ph:shield-check" class="w-12 h-12 text-gray-400 mx-auto mb-4" />
|
|
<h4 class="text-lg font-medium text-gray-900 dark:text-white mb-2">Access Control Configuration</h4>
|
|
<p class="text-gray-600 dark:text-gray-400 mb-6">
|
|
Configure which users and groups can access this application.
|
|
</p>
|
|
<rs-button variant="primary-outline" disabled>
|
|
<Icon name="ph:gear" class="w-4 h-4 mr-2" />
|
|
Configure Access (Coming Soon)
|
|
</rs-button>
|
|
</div>
|
|
</template>
|
|
</rs-card>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
/* Component specific styles */
|
|
</style> |