Md Afiq Iskandar ef5526baf1 Refactor Application Creation and Management Logic
- 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.
2025-06-17 11:53:15 +08:00

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>