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

589 lines
19 KiB
Vue

<script setup>
definePageMeta({
title: "Edit Application",
middleware: ["auth"],
requiresAuth: true,
breadcrumb: [
{ name: "Dashboard", path: "/dashboard" },
{ name: "Applications", path: "/applications" },
{ name: "Edit Application", path: "", type: "current" },
],
});
import { ref, reactive, computed, onMounted } from "vue";
// Get application ID from route
const route = useRoute();
const applicationId = route.params.id;
const { $swal } = useNuxtApp();
// Form state - exactly like create page but will be populated with existing data
const applicationForm = reactive({
name: "",
slug: "",
description: "",
launchUrl: "",
setupType: "web-app", // Default to most common type
providerType: "oauth2", // Default to most common provider
});
// Loading states
const isLoading = ref(true);
const isSaving = ref(false);
// Original application data for comparison
const originalApplication = ref(null);
// Super simple setup types - no tech jargon (exactly like create page)
const setupTypes = ref([
{
id: "web-app",
name: "🌐 Website/Web App",
description: "For websites and web applications that users log into",
popular: true,
},
{
id: "mobile-app",
name: "📱 Mobile App",
description: "For mobile apps (iPhone, Android) that need login",
popular: false,
},
{
id: "desktop-app",
name: "💻 Desktop App",
description: "For desktop software that needs user authentication",
popular: false,
},
]);
// Simple provider presets - industry standards (exactly like create page)
const providerTypes = ref([
{
id: "oauth2",
name: "🔐 OAuth2/OIDC",
description: "Modern login standard - works with most apps (Recommended)",
popular: true,
technical: "OAuth2/OpenID Connect",
},
{
id: "saml",
name: "🏢 SAML SSO",
description: "Enterprise single sign-on - common in large companies",
popular: false,
technical: "SAML 2.0",
},
{
id: "ldap",
name: "📁 LDAP Directory",
description: "Connect to Active Directory or LDAP servers",
popular: false,
technical: "LDAP/Active Directory",
},
{
id: "proxy",
name: "🔄 Proxy Auth",
description: "For older apps that don't support modern login",
popular: false,
technical: "Forward Auth Proxy",
},
]);
// Fetch existing application data
const fetchApplication = async () => {
try {
isLoading.value = true;
const response = await $fetch(`/api/applications/${applicationId}`);
// Store original data
originalApplication.value = response;
// Populate form with existing data
applicationForm.name = response.name || "";
applicationForm.slug = response.slug || "";
applicationForm.description = response.meta_description || "";
applicationForm.launchUrl =
response.launch_url || response.meta_launch_url || "";
// Try to detect the setup type and provider type from existing data
// This is a best guess based on available information
applicationForm.setupType = "web-app"; // Default since we can't reliably detect this
// Try to detect provider type from provider_obj
if (response.provider_obj?.verbose_name_plural) {
const providerName =
response.provider_obj.verbose_name_plural.toLowerCase();
if (providerName.includes("oauth")) {
applicationForm.providerType = "oauth2";
} else if (providerName.includes("saml")) {
applicationForm.providerType = "saml";
} else if (providerName.includes("ldap")) {
applicationForm.providerType = "ldap";
} else if (providerName.includes("proxy")) {
applicationForm.providerType = "proxy";
}
}
} catch (error) {
console.error("Failed to fetch application:", error);
// Use SweetAlert instead of toast
$swal.fire({
icon: 'error',
title: 'Error',
text: 'Failed to load application data. Please try again.'
});
await navigateTo("/applications");
} finally {
isLoading.value = false;
}
};
// Simple validation (exactly like create page)
const isFormValid = computed(() => {
return (
applicationForm.name &&
applicationForm.description &&
applicationForm.launchUrl &&
applicationForm.setupType &&
applicationForm.providerType
);
});
// Check if form has changes
const hasChanges = computed(() => {
if (!originalApplication.value) return false;
return (
applicationForm.name !== originalApplication.value.name ||
applicationForm.slug !== originalApplication.value.slug ||
applicationForm.description !==
(originalApplication.value.meta_description || "") ||
applicationForm.launchUrl !==
(originalApplication.value.launch_url ||
originalApplication.value.meta_launch_url ||
"")
);
});
// Auto-generate slug when name changes (exactly like create page)
const generateSlug = () => {
if (
applicationForm.name &&
applicationForm.name !== originalApplication.value?.name
) {
applicationForm.slug = applicationForm.name
.toLowerCase()
.replace(/[^a-z0-9]/g, "-")
.replace(/-+/g, "-")
.replace(/^-|-$/g, "");
}
};
// Update application - super simplified (like create page)
const updateApplication = async () => {
if (!isFormValid.value) return;
isSaving.value = true;
try {
const applicationData = {
name: applicationForm.name,
slug: applicationForm.slug,
meta_description: applicationForm.description,
meta_launch_url: applicationForm.launchUrl,
meta_publisher: "IT Department",
setupType: applicationForm.setupType,
providerType: applicationForm.providerType,
};
console.log("Updating application:", applicationData);
const response = await $fetch(`/api/applications/${applicationId}`, {
method: "PUT",
body: applicationData,
});
console.log("Application updated successfully:", response);
// Show success message with SweetAlert instead of toast
$swal.fire({
icon: 'success',
title: 'Success',
text: response.message || "Application updated successfully!",
timer: 2000,
showConfirmButton: false
});
// Success - redirect to view page after short delay using the new slug
setTimeout(async () => {
// Use the updated slug from the response instead of the old applicationId
const newSlug = response.slug || applicationId;
await navigateTo(`/applications/${newSlug}`);
}, 1000);
} catch (error) {
console.error("Failed to update application:", error);
// Show error message with SweetAlert instead of toast
let errorMessage = "Failed to update application. ";
if (error.data?.message) {
errorMessage += error.data.message;
} else {
errorMessage += "Please check the details and try again.";
}
$swal.fire({
icon: 'error',
title: 'Error',
text: errorMessage
});
} finally {
isSaving.value = false;
}
};
// Get dynamic info based on selected provider (updated for edit context)
const getProviderInfo = () => {
const providerInfo = {
oauth2: {
title: "OAuth2 configuration will be updated",
description:
"Changes to OAuth2 settings may require updating your application's callback URLs and client configuration.",
},
saml: {
title: "SAML SSO configuration will be updated",
description:
"Changes to SAML settings may require updating your application's SAML configuration and certificates.",
},
ldap: {
title: "LDAP directory integration will be updated",
description:
"Changes to LDAP settings may affect how users authenticate with their network credentials.",
},
proxy: {
title: "Proxy authentication will be updated",
description:
"Changes to proxy settings may affect how the authentication proxy forwards requests to your application.",
},
};
return providerInfo[applicationForm.providerType] || providerInfo.oauth2;
};
// Reset form to original values
const resetForm = () => {
if (originalApplication.value) {
applicationForm.name = originalApplication.value.name || "";
applicationForm.slug = originalApplication.value.slug || "";
applicationForm.description =
originalApplication.value.meta_description || "";
applicationForm.launchUrl =
originalApplication.value.launch_url ||
originalApplication.value.meta_launch_url ||
"";
}
};
// 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 data...
</p>
</div>
</div>
<!-- Edit Form -->
<div v-else>
<!-- Header -->
<div class="mb-6">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">
Edit Application
</h1>
<p class="text-gray-600 dark:text-gray-400">
Update application configuration and settings
</p>
</div>
<div class="flex space-x-3">
<rs-button
@click="navigateTo(`/applications/${applicationId}`)"
variant="secondary"
>
<Icon name="ph:arrow-left" class="w-4 h-4 mr-2" />
Cancel
</rs-button>
<rs-button
@click="resetForm"
variant="primary-outline"
:disabled="!hasChanges"
>
<Icon name="ph:arrow-clockwise" class="w-4 h-4 mr-2" />
Reset Changes
</rs-button>
<rs-button
@click="updateApplication"
:disabled="!isFormValid || !hasChanges || isSaving"
variant="primary"
>
<Icon
v-if="isSaving"
name="ph:spinner"
class="w-4 h-4 mr-2 animate-spin"
/>
<Icon v-else name="ph:floppy-disk" class="w-4 h-4 mr-2" />
{{ isSaving ? "Saving..." : "Save Changes" }}
</rs-button>
</div>
</div>
</div>
<!-- Changes Indicator -->
<div v-if="hasChanges" class="mb-6">
<div
class="bg-orange-50 dark:bg-orange-900/20 border border-orange-200 dark:border-orange-800 rounded-lg p-4"
>
<div class="flex items-center">
<Icon
name="ph:warning"
class="w-5 h-5 text-orange-600 dark:text-orange-400 mr-2"
/>
<span class="text-sm text-orange-700 dark:text-orange-300">
You have unsaved changes. Don't forget to save your modifications.
</span>
</div>
</div>
</div>
<!-- Single Card Form - Exactly like create page -->
<rs-card>
<template #header>
<Icon name="ph:pencil" class="text-blue-600" />
<h3 class="text-lg font-medium text-gray-900 dark:text-white">
Application Details
</h3>
</template>
<template #body>
<FormKit type="form" :actions="false">
<div class="space-y-6">
<!-- Basic Information -->
<div class="space-y-4">
<h4
class="text-sm font-medium text-gray-700 dark:text-gray-300"
>
Basic Information
</h4>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormKit
v-model="applicationForm.name"
type="text"
label="Application Name"
placeholder="e.g., Employee Portal"
validation="required"
validation-visibility="dirty"
@input="generateSlug"
help="What should we call this application?"
/>
<FormKit
v-model="applicationForm.slug"
type="text"
label="URL Identifier"
placeholder="employee-portal"
validation="required"
validation-visibility="dirty"
help="Auto-generated from name"
disabled
/>
</div>
<FormKit
v-model="applicationForm.description"
type="textarea"
label="Description"
placeholder="Brief description of what this application does"
validation="required"
validation-visibility="dirty"
rows="3"
help="Explain what users will use this application for"
/>
<FormKit
v-model="applicationForm.launchUrl"
type="url"
label="Application Website"
placeholder="https://portal.company.com"
validation="required|url"
validation-visibility="dirty"
help="Where users will go to access this application"
/>
</div>
<!-- Application Type -->
<div class="space-y-4">
<h4
class="text-sm font-medium text-gray-700 dark:text-gray-300"
>
Application Type
</h4>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div
v-for="type in setupTypes"
:key="type.id"
@click="applicationForm.setupType = type.id"
class="relative p-4 border-2 rounded-lg cursor-pointer transition-colors hover:border-blue-500"
:class="{
'border-blue-500 bg-blue-50 dark:bg-blue-900/20':
applicationForm.setupType === type.id,
'border-gray-200 dark:border-gray-700':
applicationForm.setupType !== type.id,
}"
>
<div class="flex items-start space-x-3">
<div class="text-2xl">{{ type.name.split(" ")[0] }}</div>
<div class="flex-1">
<h3
class="text-sm font-medium text-gray-900 dark:text-white"
>
{{ type.name.substring(2) }}
</h3>
<p
class="text-xs text-gray-600 dark:text-gray-400 mt-1"
>
{{ type.description }}
</p>
<div v-if="type.popular" class="mt-2">
<rs-badge variant="primary">Most Popular</rs-badge>
</div>
</div>
</div>
<div
v-if="applicationForm.setupType === type.id"
class="absolute top-2 right-2"
>
<Icon
name="ph:check-circle-fill"
class="w-5 h-5 text-blue-500"
/>
</div>
</div>
</div>
</div>
<!-- Authentication Provider -->
<div class="space-y-4">
<h4
class="text-sm font-medium text-gray-700 dark:text-gray-300"
>
Authentication Method
</h4>
<p class="text-xs text-gray-600 dark:text-gray-400">
Choose how users will log into this application
</p>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div
v-for="provider in providerTypes"
:key="provider.id"
@click="applicationForm.providerType = provider.id"
class="relative p-4 border-2 rounded-lg cursor-pointer transition-colors hover:border-green-500"
:class="{
'border-green-500 bg-green-50 dark:bg-green-900/20':
applicationForm.providerType === provider.id,
'border-gray-200 dark:border-gray-700':
applicationForm.providerType !== provider.id,
}"
>
<div class="flex items-start space-x-3">
<div class="text-xl">
{{ provider.name.split(" ")[0] }}
</div>
<div class="flex-1">
<h3
class="text-sm font-medium text-gray-900 dark:text-white"
>
{{ provider.name.substring(2) }}
</h3>
<p
class="text-xs text-gray-600 dark:text-gray-400 mt-1"
>
{{ provider.description }}
</p>
<div class="flex items-center space-x-2 mt-2">
<div v-if="provider.popular" class="mt-1">
<rs-badge variant="success" size="xs"
>Recommended</rs-badge
>
</div>
<span class="text-xs text-gray-500">{{
provider.technical
}}</span>
</div>
</div>
</div>
<div
v-if="applicationForm.providerType === provider.id"
class="absolute top-2 right-2"
>
<Icon
name="ph:check-circle-fill"
class="w-5 h-5 text-green-500"
/>
</div>
</div>
</div>
</div>
<!-- Dynamic Info Box Based on Provider -->
<div
class="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4"
>
<div class="flex items-start space-x-3">
<Icon
name="ph:info"
class="w-5 h-5 text-blue-600 dark:text-blue-400 mt-0.5"
/>
<div class="flex-1">
<h4
class="text-sm font-medium text-blue-900 dark:text-blue-100"
>
{{ getProviderInfo().title }}
</h4>
<p class="text-sm text-blue-700 dark:text-blue-300 mt-1">
{{ getProviderInfo().description }}
</p>
</div>
</div>
</div>
</div>
</FormKit>
</template>
</rs-card>
</div>
</div>
</template>
<style scoped>
input:focus {
@apply ring-2 ring-offset-2;
}
select:focus {
@apply ring-2 ring-offset-2;
}
</style>