- 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.
452 lines
14 KiB
Vue
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>
|