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

452 lines
14 KiB
Vue

<script setup>
definePageMeta({
title: "Create Application",
middleware: ["auth"],
requiresAuth: true,
breadcrumb: [
{ name: "Dashboard", path: "/dashboard" },
{ name: "Applications", path: "/applications" },
{
name: "Create Application",
path: "/applications/create",
type: "current",
},
],
});
import { ref, reactive, computed } from "vue";
const { $swal } = useNuxtApp();
// Super simplified form - only what users really need
const applicationForm = reactive({
name: "",
slug: "",
description: "",
launchUrl: "",
setupType: "web-app", // Default to most common type
providerType: "oauth2", // Default to most common provider
});
// Loading state
const isLoading = ref(false);
// Super simple setup types - no tech jargon
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
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",
},
]);
// Simple validation
const isFormValid = computed(() => {
return (
applicationForm.name &&
applicationForm.description &&
applicationForm.launchUrl &&
applicationForm.setupType &&
applicationForm.providerType
);
});
// Auto-generate slug when name changes
const generateSlug = () => {
if (applicationForm.name) {
applicationForm.slug = applicationForm.name
.toLowerCase()
.replace(/[^a-z0-9]/g, "-")
.replace(/-+/g, "-")
.replace(/^-|-$/g, "");
}
};
// Create application - super simplified
const createApplication = async () => {
if (!isFormValid.value) return;
isLoading.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("Creating application:", applicationData);
const response = await $fetch("/api/applications", {
method: "POST",
body: applicationData,
});
console.log("Application created successfully:", response);
// Show success toast
$swal.fire({
title: "Application created successfully!",
text: "You can now access the application from the applications list.",
icon: "success",
confirmButtonText: "OK",
});
// Success - redirect back to applications list after short delay
setTimeout(async () => {
await navigateTo("/applications");
}, 1000);
} catch (error) {
console.error("Failed to create application:", error);
// Show error toast with specific message
let errorMessage = "Failed to create application. ";
if (error.data?.message) {
errorMessage += error.data.message;
} else {
errorMessage += "Please check the details and try again.";
}
$swal.fire({
title: "Failed to create application!",
text: errorMessage,
icon: "error",
confirmButtonText: "OK",
});
} finally {
isLoading.value = false;
}
};
// Get dynamic info based on selected provider
const getProviderInfo = () => {
const providerInfo = {
oauth2: {
title: "OAuth2 will be automatically configured",
description:
"We'll create secure OAuth2 credentials and configure the callback URL. Your app will support modern login standards used by Google, Microsoft, and other major platforms.",
},
saml: {
title: "SAML SSO will be automatically configured",
description:
"We'll set up SAML 2.0 single sign-on with enterprise-grade security. This is perfect for connecting with corporate identity systems and Active Directory.",
},
ldap: {
title: "LDAP directory integration will be configured",
description:
"We'll configure LDAP/Active Directory authentication. Users can log in with their existing network credentials without any additional setup.",
},
proxy: {
title: "Proxy authentication will be configured",
description:
"We'll set up forward authentication for legacy applications. This adds modern login to apps that don't support OAuth2 or SAML natively.",
},
};
return providerInfo[applicationForm.providerType] || providerInfo.oauth2;
};
const resetForm = () => {
Object.assign(applicationForm, {
name: "",
slug: "",
description: "",
launchUrl: "",
setupType: "web-app",
providerType: "oauth2",
});
};
</script>
<template>
<div>
<LayoutsBreadcrumb />
<!-- 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">
Add New Application
</h1>
<p class="text-gray-600 dark:text-gray-400">
Create an application for your users to access with single sign-on
</p>
</div>
<div class="flex space-x-3">
<rs-button @click="resetForm" variant="primary-outline">
<Icon name="ph:arrow-clockwise" class="w-4 h-4 mr-2" />
Clear Form
</rs-button>
<rs-button
@click="createApplication"
:disabled="!isFormValid || isLoading"
variant="primary"
>
<Icon
v-if="isLoading"
name="ph:spinner"
class="w-4 h-4 mr-2 animate-spin"
/>
<Icon v-else name="ph:plus" class="w-4 h-4 mr-2" />
{{ isLoading ? "Creating..." : "Create Application" }}
</rs-button>
</div>
</div>
</div>
<!-- Single Card Form - No Complex Steps -->
<rs-card>
<template #header>
<Icon name="ph:app-window" 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>
</template>
<style scoped>
input:focus {
@apply ring-2 ring-offset-2;
}
select:focus {
@apply ring-2 ring-offset-2;
}
</style>