- 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.
589 lines
19 KiB
Vue
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>
|