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.
This commit is contained in:
parent
bb98dc0262
commit
ef5526baf1
@ -35,10 +35,10 @@ export function useVoiceReader() {
|
||||
};
|
||||
|
||||
const handleKeydown = (event) => {
|
||||
if (event.ctrlKey && event.key === "r") {
|
||||
event.preventDefault();
|
||||
toggleReading();
|
||||
}
|
||||
// if (event.ctrlKey && event.key === "r") {
|
||||
// event.preventDefault();
|
||||
// toggleReading();
|
||||
// }
|
||||
};
|
||||
|
||||
const announce = (message) => {
|
||||
|
588
pages/applications/[id]/edit.vue
Normal file
588
pages/applications/[id]/edit.vue
Normal file
@ -0,0 +1,588 @@
|
||||
<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>
|
316
pages/applications/[id]/index.vue
Normal file
316
pages/applications/[id]/index.vue
Normal file
@ -0,0 +1,316 @@
|
||||
<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>
|
File diff suppressed because it is too large
Load Diff
@ -5,156 +5,260 @@ definePageMeta({
|
||||
requiresAuth: true,
|
||||
breadcrumb: [
|
||||
{ name: "Dashboard", path: "/dashboard" },
|
||||
{ name: "Applications", path: "/applications", type: "current" }
|
||||
]
|
||||
{ name: "Applications", path: "/applications", type: "current" },
|
||||
],
|
||||
});
|
||||
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { ref, onMounted, computed } from "vue";
|
||||
|
||||
// Sample application data (same as before)
|
||||
const applications = ref([
|
||||
{
|
||||
id: '1',
|
||||
name: 'corradAF',
|
||||
slug: 'corradaf',
|
||||
description: 'Main RBAC Application',
|
||||
status: 'active',
|
||||
provider: 'OAuth2/OIDC',
|
||||
authentikId: 'authentik-app-1',
|
||||
launchUrl: 'https://corradaf.company.com',
|
||||
icon: null,
|
||||
publisher: 'CorradAF Team',
|
||||
lastSync: '2024-01-15T10:30:00Z',
|
||||
syncEnabled: true,
|
||||
userCount: 45,
|
||||
groupCount: 8,
|
||||
roleCount: 12,
|
||||
resourceCount: 35
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'HR System',
|
||||
slug: 'hr-system',
|
||||
description: 'Human Resources Management',
|
||||
status: 'active',
|
||||
provider: 'SAML',
|
||||
authentikId: 'authentik-app-2',
|
||||
launchUrl: 'https://hr.company.com',
|
||||
icon: null,
|
||||
publisher: 'HR Department',
|
||||
lastSync: '2024-01-15T09:15:00Z',
|
||||
syncEnabled: true,
|
||||
userCount: 28,
|
||||
groupCount: 5,
|
||||
roleCount: 8,
|
||||
resourceCount: 22
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Finance System',
|
||||
slug: 'finance-system',
|
||||
description: 'Financial Management Platform',
|
||||
status: 'development',
|
||||
provider: 'OAuth2/OIDC',
|
||||
authentikId: null,
|
||||
launchUrl: 'https://finance.company.com',
|
||||
icon: null,
|
||||
publisher: 'Finance Department',
|
||||
lastSync: null,
|
||||
syncEnabled: false,
|
||||
userCount: 12,
|
||||
groupCount: 3,
|
||||
roleCount: 6,
|
||||
resourceCount: 18
|
||||
// State management
|
||||
const applications = ref([]);
|
||||
const isLoading = ref(true);
|
||||
const isDeleting = ref(false);
|
||||
const searchQuery = ref("");
|
||||
const selectedStatus = ref("all");
|
||||
const selectedProvider = ref("all");
|
||||
|
||||
// Fetch applications from Authentik
|
||||
const fetchApplications = async () => {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
const response = await $fetch("/api/applications");
|
||||
|
||||
// Map Authentik response to our frontend format
|
||||
applications.value =
|
||||
response.results?.map((app) => {
|
||||
// Better provider detection based on actual provider type
|
||||
let providerName = "No Provider";
|
||||
if (app.provider_obj) {
|
||||
providerName =
|
||||
app.provider_obj.verbose_name ||
|
||||
app.provider_obj.name ||
|
||||
"Unknown Provider";
|
||||
} else if (app.provider) {
|
||||
// If provider exists but no provider_obj, it might be loading
|
||||
providerName = "Loading...";
|
||||
}
|
||||
|
||||
return {
|
||||
id: app.slug, // Use slug as ID for consistency with Authentik API
|
||||
name: app.name,
|
||||
slug: app.slug,
|
||||
description: app.meta_description || "No description provided",
|
||||
status: "active", // Authentik applications are active by default
|
||||
provider: providerName,
|
||||
authentikId: app.pk, // Keep the original pk for reference
|
||||
launchUrl: app.launch_url || app.meta_launch_url || "#",
|
||||
icon: app.meta_icon || null,
|
||||
publisher: app.meta_publisher || "System",
|
||||
createdAt: app.created || new Date().toISOString(),
|
||||
};
|
||||
}) || [];
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch applications:", error);
|
||||
applications.value = [];
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
])
|
||||
|
||||
const isLoading = ref(false)
|
||||
const isSyncing = ref(false)
|
||||
const searchQuery = ref('')
|
||||
const selectedStatus = ref('all')
|
||||
const selectedProvider = ref('all')
|
||||
};
|
||||
|
||||
// Computed properties
|
||||
const filteredApplications = computed(() => {
|
||||
let filtered = applications.value.filter(app => app && app.name) // Only include valid apps with names
|
||||
let filtered = applications.value.filter((app) => app && app.name);
|
||||
|
||||
if (searchQuery.value) {
|
||||
const query = searchQuery.value.toLowerCase()
|
||||
filtered = filtered.filter(app =>
|
||||
(app.name && app.name.toLowerCase().includes(query)) ||
|
||||
(app.description && app.description.toLowerCase().includes(query)) ||
|
||||
(app.publisher && app.publisher.toLowerCase().includes(query))
|
||||
)
|
||||
const query = searchQuery.value.toLowerCase();
|
||||
filtered = filtered.filter(
|
||||
(app) =>
|
||||
(app.name && app.name.toLowerCase().includes(query)) ||
|
||||
(app.description && app.description.toLowerCase().includes(query)) ||
|
||||
(app.publisher && app.publisher.toLowerCase().includes(query))
|
||||
);
|
||||
}
|
||||
|
||||
if (selectedStatus.value !== 'all') {
|
||||
filtered = filtered.filter(app => app.status === selectedStatus.value)
|
||||
if (selectedStatus.value !== "all") {
|
||||
filtered = filtered.filter((app) => app.status === selectedStatus.value);
|
||||
}
|
||||
|
||||
if (selectedProvider.value !== 'all') {
|
||||
filtered = filtered.filter(app => app.provider === selectedProvider.value)
|
||||
if (selectedProvider.value !== "all") {
|
||||
filtered = filtered.filter(
|
||||
(app) => app.provider === selectedProvider.value
|
||||
);
|
||||
}
|
||||
|
||||
return filtered
|
||||
})
|
||||
return filtered;
|
||||
});
|
||||
|
||||
const stats = computed(() => ({
|
||||
totalApps: applications.value.filter(app => app && app.name).length,
|
||||
activeApps: applications.value.filter(app => app && app.status === 'active').length,
|
||||
totalUsers: applications.value.reduce((sum, app) => sum + (app?.userCount || 0), 0)
|
||||
}))
|
||||
totalApps: applications.value.length,
|
||||
activeApps: applications.value.filter((app) => app.status === "active")
|
||||
.length,
|
||||
totalUsers: 0, // We'll need to fetch this from groups/users if needed
|
||||
}));
|
||||
|
||||
const providers = computed(() => {
|
||||
const uniqueProviders = [...new Set(applications.value
|
||||
.filter(app => app && app.provider)
|
||||
.map(app => app.provider))]
|
||||
return uniqueProviders.map(provider => ({ value: provider, label: provider }))
|
||||
})
|
||||
const uniqueProviders = [
|
||||
...new Set(
|
||||
applications.value
|
||||
.filter((app) => app && app.provider)
|
||||
.map((app) => app.provider)
|
||||
),
|
||||
];
|
||||
return uniqueProviders.map((provider) => ({
|
||||
value: provider,
|
||||
label: provider,
|
||||
}));
|
||||
});
|
||||
|
||||
// Methods
|
||||
const deleteApplication = async (applicationId) => {
|
||||
if (!confirm('Are you sure you want to delete this application? This action cannot be undone.')) {
|
||||
return
|
||||
const deleteApplication = async (applicationSlug) => {
|
||||
// Get the application name for the confirmation message
|
||||
const app = applications.value.find((app) => app.id === applicationSlug);
|
||||
if (!app) return;
|
||||
|
||||
// Check if this is a protected application
|
||||
if (isProtectedApplication(app.name)) {
|
||||
const { $swal } = useNuxtApp();
|
||||
$swal.fire({
|
||||
icon: "warning",
|
||||
title: "Protected Application",
|
||||
text: "This application is used for system authentication and cannot be deleted.",
|
||||
confirmButtonText: "Understood",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const index = applications.value.findIndex(app => app.id === applicationId)
|
||||
if (index > -1) {
|
||||
applications.value.splice(index, 1)
|
||||
console.log('Application deleted successfully')
|
||||
// Use SweetAlert for confirmation
|
||||
const { $swal } = useNuxtApp();
|
||||
const result = await $swal.fire({
|
||||
icon: "warning",
|
||||
title: "Delete Application?",
|
||||
text: `Are you sure you want to delete "${app.name}"? This action cannot be undone.`,
|
||||
showCancelButton: true,
|
||||
confirmButtonText: "Yes, delete it",
|
||||
confirmButtonColor: "#dc2626",
|
||||
cancelButtonText: "Cancel",
|
||||
reverseButtons: true,
|
||||
});
|
||||
|
||||
// If user didn't confirm, abort
|
||||
if (!result.isConfirmed) return;
|
||||
|
||||
console.log(
|
||||
`🗑️ Frontend: Deleting application with slug: ${applicationSlug}`
|
||||
);
|
||||
|
||||
try {
|
||||
isDeleting.value = true;
|
||||
await $fetch(`/api/applications/${applicationSlug}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
|
||||
// Remove from local state (now using slug as id)
|
||||
const index = applications.value.findIndex(
|
||||
(app) => app.id === applicationSlug
|
||||
);
|
||||
if (index > -1) {
|
||||
applications.value.splice(index, 1);
|
||||
}
|
||||
|
||||
console.log("✅ Application deleted successfully");
|
||||
|
||||
// Use SweetAlert instead of alert
|
||||
const { $swal } = useNuxtApp();
|
||||
$swal.fire({
|
||||
icon: "success",
|
||||
title: "Success",
|
||||
text: "Application deleted successfully!",
|
||||
timer: 2000,
|
||||
showConfirmButton: false,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("❌ Failed to delete application:", error);
|
||||
|
||||
// Show more specific error message
|
||||
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.";
|
||||
}
|
||||
|
||||
// Use SweetAlert instead of alert
|
||||
const { $swal } = useNuxtApp();
|
||||
$swal.fire({
|
||||
icon: "error",
|
||||
title: "Error",
|
||||
text: errorMessage,
|
||||
});
|
||||
} finally {
|
||||
isDeleting.value = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
if (!dateString) return 'Never'
|
||||
return new Date(dateString).toLocaleDateString() + ' ' + new Date(dateString).toLocaleTimeString()
|
||||
}
|
||||
if (!dateString) return "Never";
|
||||
return (
|
||||
new Date(dateString).toLocaleDateString() +
|
||||
" " +
|
||||
new Date(dateString).toLocaleTimeString()
|
||||
);
|
||||
};
|
||||
|
||||
const getStatusVariant = (status) => {
|
||||
switch (status) {
|
||||
case 'active': return 'success'
|
||||
case 'development': return 'warning'
|
||||
case 'inactive': return 'secondary'
|
||||
default: return 'secondary'
|
||||
case "active":
|
||||
return "success";
|
||||
case "development":
|
||||
return "warning";
|
||||
case "inactive":
|
||||
return "secondary";
|
||||
default:
|
||||
return "secondary";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Check if an application is protected (cannot be deleted)
|
||||
const isProtectedApplication = (appName) => {
|
||||
// List of protected applications that should not be deleted
|
||||
const protectedApps = [
|
||||
"CORRAD RBAC", // Main SSO application
|
||||
"corrad-rbac",
|
||||
"Authentik",
|
||||
"authentik",
|
||||
"CORRAD RBAC test2", // From the screenshot
|
||||
];
|
||||
|
||||
// Case-insensitive check
|
||||
return protectedApps.some(
|
||||
(name) => appName && appName.toLowerCase().includes(name.toLowerCase())
|
||||
);
|
||||
};
|
||||
|
||||
// Initialize
|
||||
onMounted(() => {
|
||||
// Load initial data
|
||||
})
|
||||
fetchApplications();
|
||||
});
|
||||
</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">Applications</h1>
|
||||
<p class="text-gray-600 dark:text-gray-400">Manage applications integrated with Authentik</p>
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
Applications
|
||||
</h1>
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
Manage applications integrated with Authentik
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex space-x-3">
|
||||
<rs-button @click="navigateTo('/applications/create')">
|
||||
@ -171,13 +275,22 @@ onMounted(() => {
|
||||
<template #body>
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-8 h-8 bg-blue-100 dark:bg-blue-900 rounded-lg flex items-center justify-center">
|
||||
<Icon name="ph:app-window" class="w-5 h-5 text-blue-600 dark:text-blue-400" />
|
||||
<div
|
||||
class="w-8 h-8 bg-blue-100 dark:bg-blue-900 rounded-lg flex items-center justify-center"
|
||||
>
|
||||
<Icon
|
||||
name="ph:app-window"
|
||||
class="w-5 h-5 text-blue-600 dark:text-blue-400"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">Total Applications</p>
|
||||
<p class="text-2xl font-bold text-gray-900 dark:text-white">{{ stats.totalApps }}</p>
|
||||
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
Total Applications
|
||||
</p>
|
||||
<p class="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
{{ stats.totalApps }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -187,13 +300,22 @@ onMounted(() => {
|
||||
<template #body>
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-8 h-8 bg-green-100 dark:bg-green-900 rounded-lg flex items-center justify-center">
|
||||
<Icon name="ph:check-circle" class="w-5 h-5 text-green-600 dark:text-green-400" />
|
||||
<div
|
||||
class="w-8 h-8 bg-green-100 dark:bg-green-900 rounded-lg flex items-center justify-center"
|
||||
>
|
||||
<Icon
|
||||
name="ph:check-circle"
|
||||
class="w-5 h-5 text-green-600 dark:text-green-400"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">Active Applications</p>
|
||||
<p class="text-2xl font-bold text-gray-900 dark:text-white">{{ stats.activeApps }}</p>
|
||||
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
Active Applications
|
||||
</p>
|
||||
<p class="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
{{ stats.activeApps }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -203,64 +325,35 @@ onMounted(() => {
|
||||
<template #body>
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-8 h-8 bg-orange-100 dark:bg-orange-900 rounded-lg flex items-center justify-center">
|
||||
<Icon name="ph:users" class="w-5 h-5 text-orange-600 dark:text-orange-400" />
|
||||
<div
|
||||
class="w-8 h-8 bg-orange-100 dark:bg-orange-900 rounded-lg flex items-center justify-center"
|
||||
>
|
||||
<Icon
|
||||
name="ph:users"
|
||||
class="w-5 h-5 text-orange-600 dark:text-orange-400"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">Total App Users</p>
|
||||
<p class="text-2xl font-bold text-gray-900 dark:text-white">{{ stats.totalUsers }}</p>
|
||||
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
Total App Users
|
||||
</p>
|
||||
<p class="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
{{ stats.totalUsers }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</rs-card>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<rs-card class="mb-6">
|
||||
<template #body>
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div class="md:col-span-2">
|
||||
<FormKit
|
||||
v-model="searchQuery"
|
||||
type="search"
|
||||
placeholder="Search applications..."
|
||||
:classes="{ outer: 'mb-0' }"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<FormKit
|
||||
v-model="selectedStatus"
|
||||
type="select"
|
||||
:options="[
|
||||
{ value: 'all', label: 'All Statuses' },
|
||||
{ value: 'active', label: 'Active' },
|
||||
{ value: 'development', label: 'Development' },
|
||||
{ value: 'inactive', label: 'Inactive' }
|
||||
]"
|
||||
:classes="{ outer: 'mb-0' }"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<FormKit
|
||||
v-model="selectedProvider"
|
||||
type="select"
|
||||
:options="[
|
||||
{ value: 'all', label: 'All Providers' },
|
||||
...providers
|
||||
]"
|
||||
:classes="{ outer: 'mb-0' }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</rs-card>
|
||||
|
||||
<!-- Applications Table -->
|
||||
<rs-card>
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white">All Applications</h3>
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white">
|
||||
All Applications
|
||||
</h3>
|
||||
<rs-badge variant="info">{{ stats.totalApps }} applications</rs-badge>
|
||||
</div>
|
||||
</template>
|
||||
@ -269,81 +362,111 @@ onMounted(() => {
|
||||
:field="['name', 'status', 'provider', 'users', 'created', 'actions']"
|
||||
:data="filteredApplications"
|
||||
:advanced="true"
|
||||
:options="{
|
||||
variant: 'default',
|
||||
striped: false,
|
||||
bordered: false,
|
||||
hover: true
|
||||
}"
|
||||
:optionsAdvanced="{
|
||||
sortable: true,
|
||||
outsideBorder: false
|
||||
}"
|
||||
:pageSize="10"
|
||||
:loading="isLoading"
|
||||
>
|
||||
<!-- Name Column -->
|
||||
<template #name="{ value }">
|
||||
<div class="flex items-center" v-if="value">
|
||||
<div class="flex-shrink-0 h-10 w-10">
|
||||
<div class="h-10 w-10 rounded-lg bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center">
|
||||
<span class="text-sm font-medium text-white">{{ value.name ? value.name.charAt(0).toUpperCase() : '?' }}</span>
|
||||
<div
|
||||
class="h-10 w-10 rounded-lg bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center"
|
||||
>
|
||||
<span class="text-sm font-medium text-white">{{
|
||||
value.name ? value.name.charAt(0).toUpperCase() : "?"
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<div class="text-sm font-medium text-gray-900 dark:text-white">{{ value.name }}</div>
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">{{ value.description || 'No description' }}</div>
|
||||
<div
|
||||
class="text-sm font-medium text-gray-900 dark:text-white hover:text-primary cursor-pointer"
|
||||
@click="navigateTo(`/applications/${value.id}`)"
|
||||
>
|
||||
{{ value.name }}
|
||||
</div>
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ value.description || "No description" }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Status Column -->
|
||||
<template #status="{ value }">
|
||||
<rs-badge :variant="getStatusVariant(value?.status || 'inactive')" v-if="value">
|
||||
{{ value.status || 'Unknown' }}
|
||||
<rs-badge
|
||||
:variant="getStatusVariant(value?.status || 'inactive')"
|
||||
v-if="value"
|
||||
>
|
||||
{{ value.status || "Unknown" }}
|
||||
</rs-badge>
|
||||
</template>
|
||||
|
||||
<!-- Provider Column -->
|
||||
<template #provider="{ value }">
|
||||
<div class="flex items-center" v-if="value && value.provider">
|
||||
<Icon
|
||||
:name="value.provider === 'OAuth2/OIDC' ? 'ph:key' : 'ph:shield'"
|
||||
class="w-4 h-4 mr-2 text-gray-400"
|
||||
<Icon
|
||||
:name="
|
||||
value.provider === 'OAuth2/OIDC' ? 'ph:key' : 'ph:shield'
|
||||
"
|
||||
class="w-4 h-4 mr-2 text-gray-400"
|
||||
/>
|
||||
<span class="text-sm text-gray-900 dark:text-white">{{ value.provider }}</span>
|
||||
<span class="text-sm text-gray-900 dark:text-white">{{
|
||||
value.provider
|
||||
}}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Users Column -->
|
||||
<template #users="{ value }">
|
||||
<div class="text-sm" v-if="value">
|
||||
<div class="text-gray-900 dark:text-white">{{ value.userCount || 0 }} users</div>
|
||||
<div class="text-gray-500 dark:text-gray-400">{{ value.roleCount || 0 }} roles</div>
|
||||
<div class="text-gray-900 dark:text-white">
|
||||
{{ value.userCount || 0 }} users
|
||||
</div>
|
||||
<div class="text-gray-500 dark:text-gray-400">
|
||||
{{ value.roleCount || 0 }} roles
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Created Column -->
|
||||
<template #created="{ value }">
|
||||
<div class="text-sm" v-if="value">
|
||||
<div class="text-gray-900 dark:text-white">{{ formatDate(value.createdAt) }}</div>
|
||||
<div class="text-gray-500 dark:text-gray-400">{{ value.createdBy || 'System' }}</div>
|
||||
<div class="text-gray-900 dark:text-white">
|
||||
{{ formatDate(value.createdAt) }}
|
||||
</div>
|
||||
<div class="text-gray-500 dark:text-gray-400">
|
||||
{{ value.createdBy || "System" }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Actions Column -->
|
||||
<template #actions="{ value }">
|
||||
<div class="flex items-center space-x-2" v-if="value && value.id">
|
||||
<button class="text-primary hover:text-primary/80" @click="navigateTo(`/applications/${value.id}`)">
|
||||
<div
|
||||
class="flex items-center space-x-2"
|
||||
v-if="value && value.id && !isProtectedApplication(value.name)"
|
||||
>
|
||||
<button
|
||||
class="text-primary hover:text-primary/80 p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
@click="navigateTo(`/applications/${value.id}`)"
|
||||
title="View Application"
|
||||
>
|
||||
<Icon name="ph:eye" class="w-4 h-4" />
|
||||
</button>
|
||||
<button class="text-primary hover:text-primary/80" @click="navigateTo(`/applications/${value.id}/edit`)">
|
||||
<button
|
||||
class="text-primary hover:text-primary/80 p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
@click="navigateTo(`/applications/${value.id}/edit`)"
|
||||
title="Edit Application"
|
||||
>
|
||||
<Icon name="ph:pencil" class="w-4 h-4" />
|
||||
</button>
|
||||
<button class="text-red-600 hover:text-red-800" @click="deleteApplication(value.id)">
|
||||
<button
|
||||
class="text-red-600 hover:text-red-800 p-2 rounded-lg hover:bg-red-50 dark:hover:bg-red-900/20"
|
||||
@click="deleteApplication(value.id)"
|
||||
title="Delete Application"
|
||||
>
|
||||
<Icon name="ph:trash" class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2" v-else>-</div>
|
||||
</template>
|
||||
</RsTable>
|
||||
</template>
|
||||
@ -353,4 +476,4 @@ onMounted(() => {
|
||||
|
||||
<style scoped>
|
||||
/* Component specific styles */
|
||||
</style>
|
||||
</style>
|
||||
|
@ -101,15 +101,27 @@ const submitForm = async () => {
|
||||
// Simulate API call
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
// Success handling
|
||||
alert('User created successfully!')
|
||||
// Success handling using SweetAlert
|
||||
const { $swal } = useNuxtApp();
|
||||
$swal.fire({
|
||||
icon: 'success',
|
||||
title: 'Success',
|
||||
text: 'User created successfully!',
|
||||
timer: 2000,
|
||||
showConfirmButton: false
|
||||
})
|
||||
|
||||
// Reset form or redirect
|
||||
// resetForm()
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error creating user:', error)
|
||||
alert('Error creating user. Please try again.')
|
||||
const { $swal } = useNuxtApp();
|
||||
$swal.fire({
|
||||
icon: 'error',
|
||||
title: 'Error',
|
||||
text: 'Error creating user. Please try again.'
|
||||
})
|
||||
} finally {
|
||||
isSubmitting.value = false
|
||||
}
|
||||
|
@ -218,17 +218,6 @@ onMounted(() => {
|
||||
:field="['group', 'members', 'status', 'parentGroup', 'actions']"
|
||||
:data="groups"
|
||||
:advanced="true"
|
||||
:options="{
|
||||
variant: 'default',
|
||||
striped: false,
|
||||
bordered: false,
|
||||
hover: true
|
||||
}"
|
||||
:optionsAdvanced="{
|
||||
sortable: true,
|
||||
outsideBorder: false
|
||||
}"
|
||||
:pageSize="10"
|
||||
>
|
||||
<!-- Group Column -->
|
||||
<template #group="{ value }">
|
||||
|
@ -244,17 +244,6 @@ onMounted(() => {
|
||||
:field="['role', 'application', 'status', 'users', 'actions']"
|
||||
:data="roles"
|
||||
:advanced="true"
|
||||
:options="{
|
||||
variant: 'default',
|
||||
striped: false,
|
||||
bordered: false,
|
||||
hover: true
|
||||
}"
|
||||
:optionsAdvanced="{
|
||||
sortable: true,
|
||||
outsideBorder: false
|
||||
}"
|
||||
:pageSize="10"
|
||||
>
|
||||
<!-- Role Column -->
|
||||
<template #role="{ value }">
|
||||
|
223
pages/test-api.vue
Normal file
223
pages/test-api.vue
Normal file
@ -0,0 +1,223 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-gray-50 py-8">
|
||||
<div class="max-w-4xl mx-auto px-4">
|
||||
<h1 class="text-3xl font-bold text-gray-900 mb-8">API Testing Page</h1>
|
||||
|
||||
<!-- Test Application Creation -->
|
||||
<div class="bg-white rounded-lg shadow p-6 mb-6">
|
||||
<h2 class="text-xl font-semibold mb-4">Test Application Creation</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-4">
|
||||
<button
|
||||
@click="testCreateApp('oauth2')"
|
||||
:disabled="loading"
|
||||
class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded disabled:opacity-50"
|
||||
>
|
||||
Test OAuth2 App
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="testCreateApp('ldap')"
|
||||
:disabled="loading"
|
||||
class="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded disabled:opacity-50"
|
||||
>
|
||||
Test LDAP App
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="testCreateApp('saml')"
|
||||
:disabled="loading"
|
||||
class="bg-purple-500 hover:bg-purple-600 text-white px-4 py-2 rounded disabled:opacity-50"
|
||||
>
|
||||
Test SAML App
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="testCreateApp('proxy')"
|
||||
:disabled="loading"
|
||||
class="bg-orange-500 hover:bg-orange-600 text-white px-4 py-2 rounded disabled:opacity-50"
|
||||
>
|
||||
Test Proxy App
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<button
|
||||
@click="testListApps"
|
||||
:disabled="loading"
|
||||
class="bg-gray-500 hover:bg-gray-600 text-white px-4 py-2 rounded disabled:opacity-50 mr-2"
|
||||
>
|
||||
List Applications
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="testAuthentikConnection"
|
||||
:disabled="loading"
|
||||
class="bg-indigo-500 hover:bg-indigo-600 text-white px-4 py-2 rounded disabled:opacity-50"
|
||||
>
|
||||
Test Authentik Connection
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="text-blue-600 mb-4">
|
||||
⏳ {{ loadingMessage }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Results Display -->
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<h2 class="text-xl font-semibold mb-4">Results</h2>
|
||||
|
||||
<div v-if="result" class="mb-4">
|
||||
<div class="flex items-center mb-2">
|
||||
<span class="text-sm font-medium text-gray-500 mr-2">Status:</span>
|
||||
<span :class="result.success ? 'text-green-600' : 'text-red-600'" class="font-medium">
|
||||
{{ result.success ? '✅ Success' : '❌ Error' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-50 rounded p-4 overflow-auto">
|
||||
<pre class="text-sm">{{ JSON.stringify(result.data, null, 2) }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!result" class="text-gray-500 text-center py-8">
|
||||
Click a test button above to see results here
|
||||
</div>
|
||||
|
||||
<button
|
||||
v-if="result"
|
||||
@click="result = null"
|
||||
class="mt-4 bg-gray-300 hover:bg-gray-400 text-gray-700 px-4 py-2 rounded"
|
||||
>
|
||||
Clear Results
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// State
|
||||
const loading = ref(false)
|
||||
const loadingMessage = ref('')
|
||||
const result = ref(null)
|
||||
|
||||
// Test functions
|
||||
const testCreateApp = async (providerType) => {
|
||||
loading.value = true
|
||||
loadingMessage.value = `Creating ${providerType.toUpperCase()} application...`
|
||||
|
||||
try {
|
||||
const timestamp = Date.now()
|
||||
const testApp = {
|
||||
name: `Test ${providerType.toUpperCase()} ${timestamp}`,
|
||||
slug: `test-${providerType}-${timestamp}`,
|
||||
meta_description: `Test ${providerType} application for debugging`,
|
||||
meta_launch_url: 'http://localhost:5000/test',
|
||||
meta_publisher: 'IT Department - Test',
|
||||
setupType: 'web-app',
|
||||
providerType: providerType
|
||||
}
|
||||
|
||||
console.log(`🧪 Testing ${providerType} application creation:`, testApp)
|
||||
|
||||
const response = await $fetch('/api/applications', {
|
||||
method: 'POST',
|
||||
body: testApp
|
||||
})
|
||||
|
||||
result.value = {
|
||||
success: true,
|
||||
data: {
|
||||
message: `${providerType.toUpperCase()} application created successfully!`,
|
||||
application: response,
|
||||
requestData: testApp
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ ${providerType} test failed:`, error)
|
||||
result.value = {
|
||||
success: false,
|
||||
data: {
|
||||
error: error.message,
|
||||
statusCode: error.statusCode,
|
||||
details: error.data,
|
||||
requestData: testApp
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
loadingMessage.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
const testListApps = async () => {
|
||||
loading.value = true
|
||||
loadingMessage.value = 'Fetching applications list...'
|
||||
|
||||
try {
|
||||
const response = await $fetch('/api/applications')
|
||||
|
||||
result.value = {
|
||||
success: true,
|
||||
data: {
|
||||
message: 'Applications fetched successfully!',
|
||||
count: response.results?.length || 0,
|
||||
applications: response.results || []
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ List apps test failed:', error)
|
||||
result.value = {
|
||||
success: false,
|
||||
data: {
|
||||
error: error.message,
|
||||
statusCode: error.statusCode,
|
||||
details: error.data
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
loadingMessage.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
const testAuthentikConnection = async () => {
|
||||
loading.value = true
|
||||
loadingMessage.value = 'Testing Authentik connection...'
|
||||
|
||||
try {
|
||||
const response = await $fetch('/api/test-authentik')
|
||||
|
||||
result.value = {
|
||||
success: true,
|
||||
data: {
|
||||
message: 'Authentik connection test successful!',
|
||||
connectionTest: response
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Authentik connection test failed:', error)
|
||||
result.value = {
|
||||
success: false,
|
||||
data: {
|
||||
error: error.message,
|
||||
statusCode: error.statusCode,
|
||||
details: error.data
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
loadingMessage.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
// Page metadata
|
||||
useHead({
|
||||
title: 'API Testing - CorradAF RBAC'
|
||||
})
|
||||
</script>
|
@ -164,17 +164,6 @@ onMounted(() => {
|
||||
:field="['user', 'department', 'status', 'lastLogin', 'actions']"
|
||||
:data="users"
|
||||
:advanced="true"
|
||||
:options="{
|
||||
variant: 'default',
|
||||
striped: false,
|
||||
bordered: false,
|
||||
hover: true
|
||||
}"
|
||||
:optionsAdvanced="{
|
||||
sortable: true,
|
||||
outsideBorder: false
|
||||
}"
|
||||
:pageSize="10"
|
||||
>
|
||||
<!-- User Column -->
|
||||
<template #user="{ value }">
|
||||
|
@ -1,25 +1,24 @@
|
||||
import { authentikFetch } from '../../utils/authentik';
|
||||
import { authentikFetch, clearAuthentikCache } from '../../utils/authentik';
|
||||
import { requireAuth } from '../../utils/auth';
|
||||
|
||||
// /api/applications/[id] - Handle GET, PUT, DELETE for specific application
|
||||
// Note: [id] is actually the application slug for consistency with Authentik API
|
||||
export default defineEventHandler(async (event) => {
|
||||
const method = getMethod(event);
|
||||
const id = getRouterParam(event, 'id');
|
||||
const slug = getRouterParam(event, 'id'); // This is actually a slug
|
||||
|
||||
// Require authentication
|
||||
await requireAuth(event);
|
||||
|
||||
if (!id) {
|
||||
if (!slug) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: 'Application ID is required'
|
||||
message: 'Application slug is required'
|
||||
});
|
||||
}
|
||||
|
||||
switch (method) {
|
||||
case 'GET':
|
||||
// Make GET public for testing - no auth required
|
||||
try {
|
||||
const application = await authentikFetch(`/core/applications/${id}/`);
|
||||
const application = await authentikFetch(`/core/applications/${slug}/`);
|
||||
return application;
|
||||
} catch (error) {
|
||||
throw createError({
|
||||
@ -29,38 +28,85 @@ export default defineEventHandler(async (event) => {
|
||||
}
|
||||
|
||||
case 'PUT':
|
||||
// Require authentication for updating applications
|
||||
await requireAuth(event);
|
||||
|
||||
try {
|
||||
const body = await readBody(event);
|
||||
console.log('🔄 Updating application:', slug, body);
|
||||
|
||||
const application = await authentikFetch(`/core/applications/${id}/`, {
|
||||
method: 'PUT',
|
||||
body: {
|
||||
name: body.name,
|
||||
slug: body.slug,
|
||||
meta_description: body.description,
|
||||
meta_publisher: 'CorradAF RBAC'
|
||||
}
|
||||
// Prepare the update payload with all fields from frontend
|
||||
const updatePayload = {
|
||||
name: body.name,
|
||||
slug: body.slug,
|
||||
meta_description: body.meta_description || body.description,
|
||||
meta_publisher: body.meta_publisher || 'CorradAF RBAC',
|
||||
meta_launch_url: body.meta_launch_url || body.launchUrl
|
||||
};
|
||||
|
||||
console.log('📦 Update payload:', updatePayload);
|
||||
|
||||
const application = await authentikFetch(`/core/applications/${slug}/`, {
|
||||
method: 'PATCH', // Use PATCH instead of PUT to update only specified fields
|
||||
body: updatePayload
|
||||
});
|
||||
|
||||
return application;
|
||||
console.log('✅ Application updated successfully:', application.name);
|
||||
|
||||
// Clear Authentik cache to ensure changes take effect immediately
|
||||
try {
|
||||
await clearAuthentikCache();
|
||||
console.log('✅ Cache cleared after application update');
|
||||
} catch (cacheError) {
|
||||
console.warn('⚠️ Cache clearing failed but application was updated:', cacheError.message);
|
||||
// Continue even if cache clearing fails
|
||||
}
|
||||
|
||||
// Add success message to the response
|
||||
return {
|
||||
...application,
|
||||
message: 'Application updated successfully'
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('❌ Update failed:', error);
|
||||
throw createError({
|
||||
statusCode: error.statusCode || 500,
|
||||
message: error.message
|
||||
message: error.message || 'Failed to update application'
|
||||
});
|
||||
}
|
||||
|
||||
case 'DELETE':
|
||||
// Require authentication for deleting applications
|
||||
await requireAuth(event);
|
||||
|
||||
try {
|
||||
await authentikFetch(`/core/applications/${id}/`, {
|
||||
console.log(`🗑️ Attempting to delete application with slug: ${slug}`);
|
||||
|
||||
// Direct DELETE using slug (no extra GET request needed)
|
||||
await authentikFetch(`/core/applications/${slug}/`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
return { message: 'Application deleted successfully' };
|
||||
console.log(`✅ Successfully deleted application ${slug}`);
|
||||
|
||||
// Clear Authentik cache to ensure changes take effect immediately
|
||||
try {
|
||||
await clearAuthentikCache();
|
||||
console.log('✅ Cache cleared after application deletion');
|
||||
} catch (cacheError) {
|
||||
console.warn('⚠️ Cache clearing failed but application was deleted:', cacheError.message);
|
||||
// Continue even if cache clearing fails
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Application deleted successfully'
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`❌ Delete failed for application ${slug}:`, error);
|
||||
throw createError({
|
||||
statusCode: error.statusCode || 500,
|
||||
message: error.message
|
||||
message: error.message || 'Failed to delete application'
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,56 +1,218 @@
|
||||
import { authentikFetch, createAuthentikApplication, createAuthentikProvider, linkProviderToApplication } from '../../utils/authentik';
|
||||
import { authentikFetch, clearAuthentikCache } from '../../utils/authentik';
|
||||
import { requireAuth } from '../../utils/auth';
|
||||
|
||||
// /api/applications - Handle GET and POST
|
||||
// Simplified /api/applications endpoint
|
||||
export default defineEventHandler(async (event) => {
|
||||
const method = getMethod(event);
|
||||
|
||||
// Require authentication for all application endpoints
|
||||
await requireAuth(event);
|
||||
|
||||
switch (method) {
|
||||
case 'GET':
|
||||
// Public endpoint for listing applications
|
||||
try {
|
||||
const applications = await authentikFetch('/core/applications/');
|
||||
return applications;
|
||||
const response = await authentikFetch('/core/applications/');
|
||||
return response;
|
||||
} catch (error) {
|
||||
throw createError({
|
||||
statusCode: error.statusCode || 500,
|
||||
message: error.message
|
||||
});
|
||||
console.error('❌ Failed to fetch applications:', error.message);
|
||||
throw error;
|
||||
}
|
||||
|
||||
case 'POST':
|
||||
// TODO: Add authentication later - for now make it public for testing
|
||||
await requireAuth(event);
|
||||
|
||||
try {
|
||||
const body = await readBody(event);
|
||||
console.log(`🔨 Creating application: ${body.name} (${body.providerType})`);
|
||||
|
||||
// Create application in Authentik
|
||||
const application = await createAuthentikApplication({
|
||||
// Simplified application creation - just the essentials
|
||||
// Create application first
|
||||
const applicationData = {
|
||||
name: body.name,
|
||||
slug: body.slug || body.name.toLowerCase().replace(/\s+/g, '-'),
|
||||
meta_description: body.description,
|
||||
meta_publisher: 'CorradAF RBAC'
|
||||
});
|
||||
slug: body.slug || body.name.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, ''),
|
||||
meta_description: body.meta_description || body.description,
|
||||
meta_publisher: body.meta_publisher || 'CorradAF RBAC',
|
||||
meta_launch_url: body.meta_launch_url || body.launchUrl
|
||||
};
|
||||
|
||||
// Create OAuth2 provider if web application
|
||||
if (body.type === 'web-app') {
|
||||
const provider = await createAuthentikProvider({
|
||||
name: `${body.name} OAuth2`,
|
||||
client_type: 'confidential',
|
||||
redirect_uris: body.redirectUris,
|
||||
authorization_flow: body.authorizationFlow || 'default-authentication-flow'
|
||||
let application;
|
||||
try {
|
||||
application = await authentikFetch('/core/applications/', {
|
||||
method: 'POST',
|
||||
body: applicationData
|
||||
});
|
||||
|
||||
// Link provider to application
|
||||
await linkProviderToApplication(application.pk, provider.pk);
|
||||
console.log('✅ Application created successfully');
|
||||
} catch (appError) {
|
||||
console.error('❌ Application creation failed:', appError.message);
|
||||
if (appError.data) {
|
||||
console.error('Details:', JSON.stringify(appError.data, null, 2));
|
||||
}
|
||||
throw appError;
|
||||
}
|
||||
|
||||
return application;
|
||||
// Create provider based on type (simplified presets)
|
||||
if (body.providerType && application.pk) {
|
||||
try {
|
||||
console.log(`🔨 Creating ${body.providerType.toUpperCase()} provider...`);
|
||||
|
||||
// Get required flows for all providers
|
||||
const flows = await authentikFetch('/flows/instances/');
|
||||
|
||||
// Find required flows
|
||||
const authFlows = flows.results.filter(flow => flow.designation === 'authentication');
|
||||
const invalidationFlows = flows.results.filter(flow => flow.designation === 'invalidation');
|
||||
|
||||
const defaultAuthFlow = authFlows.find(flow => flow.slug === 'default-authentication-flow') || authFlows[0];
|
||||
const defaultInvalidationFlow = invalidationFlows.find(flow => flow.slug === 'default-invalidation-flow') || invalidationFlows[0];
|
||||
|
||||
if (!defaultAuthFlow) {
|
||||
throw new Error('No authentication flow found - required for all providers');
|
||||
}
|
||||
|
||||
|
||||
let provider = null;
|
||||
|
||||
switch (body.providerType) {
|
||||
case 'oauth2':
|
||||
// OAuth2/OIDC provider preset
|
||||
const oauth2Data = {
|
||||
name: `${body.name} OAuth2`,
|
||||
client_type: 'confidential',
|
||||
authorization_flow: defaultAuthFlow.pk,
|
||||
invalidation_flow: defaultInvalidationFlow?.pk,
|
||||
redirect_uris: [
|
||||
{
|
||||
matching_mode: 'strict',
|
||||
url: `${body.meta_launch_url || body.launchUrl}/auth/callback`
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
try {
|
||||
provider = await authentikFetch('/providers/oauth2/', {
|
||||
method: 'POST',
|
||||
body: oauth2Data
|
||||
});
|
||||
console.log('✅ OAuth2 provider created');
|
||||
} catch (oauth2Error) {
|
||||
console.error('❌ OAuth2 provider failed:', oauth2Error.message);
|
||||
if (oauth2Error.data) {
|
||||
console.error('Details:', JSON.stringify(oauth2Error.data, null, 2));
|
||||
}
|
||||
throw oauth2Error;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'saml':
|
||||
// SAML provider preset
|
||||
const samlData = {
|
||||
name: `${body.name} SAML`,
|
||||
acs_url: `${body.meta_launch_url || body.launchUrl}/saml/acs`,
|
||||
audience: body.slug,
|
||||
issuer: `corradaf-${body.slug}`,
|
||||
sp_binding: 'post',
|
||||
authorization_flow: defaultAuthFlow.pk,
|
||||
invalidation_flow: defaultInvalidationFlow?.pk
|
||||
};
|
||||
|
||||
try {
|
||||
provider = await authentikFetch('/providers/saml/', {
|
||||
method: 'POST',
|
||||
body: samlData
|
||||
});
|
||||
console.log('✅ SAML provider created');
|
||||
} catch (samlError) {
|
||||
console.error('❌ SAML provider failed:', samlError.message);
|
||||
if (samlError.data) {
|
||||
console.error('Details:', JSON.stringify(samlError.data, null, 2));
|
||||
}
|
||||
throw samlError;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'ldap':
|
||||
// LDAP provider preset
|
||||
const ldapData = {
|
||||
name: `${body.name} LDAP`,
|
||||
base_dn: 'dc=ldap,dc=goauthentik,dc=io',
|
||||
authorization_flow: defaultAuthFlow.pk,
|
||||
invalidation_flow: defaultInvalidationFlow?.pk
|
||||
};
|
||||
|
||||
try {
|
||||
provider = await authentikFetch('/providers/ldap/', {
|
||||
method: 'POST',
|
||||
body: ldapData
|
||||
});
|
||||
console.log('✅ LDAP provider created');
|
||||
} catch (ldapError) {
|
||||
console.error('❌ LDAP provider failed:', ldapError.message);
|
||||
if (ldapError.data) {
|
||||
console.error('Details:', JSON.stringify(ldapError.data, null, 2));
|
||||
}
|
||||
throw ldapError;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'proxy':
|
||||
// Proxy provider preset
|
||||
const proxyData = {
|
||||
name: `${body.name} Proxy`,
|
||||
external_host: body.meta_launch_url || body.launchUrl,
|
||||
internal_host: body.meta_launch_url || body.launchUrl,
|
||||
authorization_flow: defaultAuthFlow.pk,
|
||||
invalidation_flow: defaultInvalidationFlow?.pk
|
||||
};
|
||||
|
||||
try {
|
||||
provider = await authentikFetch('/providers/proxy/', {
|
||||
method: 'POST',
|
||||
body: proxyData
|
||||
});
|
||||
console.log('✅ Proxy provider created');
|
||||
} catch (proxyError) {
|
||||
console.error('❌ Proxy provider failed:', proxyError.message);
|
||||
if (proxyError.data) {
|
||||
console.error('Details:', JSON.stringify(proxyError.data, null, 2));
|
||||
}
|
||||
throw proxyError;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Link provider to application
|
||||
if (provider && provider.pk) {
|
||||
await authentikFetch(`/core/applications/${application.slug}/`, {
|
||||
method: 'PATCH',
|
||||
body: {
|
||||
provider: provider.pk
|
||||
}
|
||||
});
|
||||
console.log(`✅ ${body.providerType.toUpperCase()} provider linked to application`);
|
||||
} else {
|
||||
console.warn('⚠️ Provider was not created properly');
|
||||
}
|
||||
} catch (providerError) {
|
||||
console.warn('⚠️ Provider creation failed, but application was created:', providerError.message);
|
||||
// Don't fail the entire operation if provider creation fails
|
||||
}
|
||||
}
|
||||
|
||||
// Clear Authentik cache to ensure changes take effect immediately
|
||||
try {
|
||||
await clearAuthentikCache();
|
||||
console.log('✅ Cache cleared after application creation');
|
||||
} catch (cacheError) {
|
||||
console.warn('⚠️ Cache clearing failed but application was created:', cacheError.message);
|
||||
// Continue even if cache clearing fails
|
||||
}
|
||||
|
||||
return {
|
||||
...application,
|
||||
message: 'Application created successfully'
|
||||
};
|
||||
} catch (error) {
|
||||
throw createError({
|
||||
statusCode: error.statusCode || 500,
|
||||
message: error.message
|
||||
});
|
||||
console.error('❌ Failed to create application:', error.message);
|
||||
throw error;
|
||||
}
|
||||
|
||||
default:
|
||||
|
46
server/api/test-authentik.js
Normal file
46
server/api/test-authentik.js
Normal file
@ -0,0 +1,46 @@
|
||||
// Simple test endpoint to verify Authentik connection
|
||||
export default defineEventHandler(async (event) => {
|
||||
const config = useRuntimeConfig();
|
||||
|
||||
try {
|
||||
console.log('🧪 Testing Authentik connection...');
|
||||
console.log('🔗 Authentik URL:', config.public.authentikUrl);
|
||||
console.log('🔑 API Token exists:', !!config.authentik?.apiToken);
|
||||
console.log('🔑 API Token length:', config.authentik?.apiToken?.length || 0);
|
||||
|
||||
// Test basic API access
|
||||
const response = await $fetch(`${config.public.authentikUrl}/api/v3/core/applications/`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${config.authentik.apiToken}`
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Authentik connection successful!',
|
||||
data: {
|
||||
authentikUrl: config.public.authentikUrl,
|
||||
applicationsCount: response.results?.length || 0,
|
||||
tokenExists: !!config.authentik?.apiToken
|
||||
}
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Authentik connection test failed:', error);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
status: error.response?.status,
|
||||
message: error.message,
|
||||
details: error.response?.statusText
|
||||
},
|
||||
debugging: {
|
||||
authentikUrl: config.public.authentikUrl,
|
||||
tokenExists: !!config.authentik?.apiToken,
|
||||
tokenLength: config.authentik?.apiToken?.length || 0
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
@ -1,38 +1,63 @@
|
||||
// Authentication utilities for API routes
|
||||
// Authentication utilities for API routes - Updated for cookie-based auth
|
||||
export const requireAuth = async (event) => {
|
||||
const config = useRuntimeConfig();
|
||||
|
||||
const authHeader = getHeader(event, 'Authorization');
|
||||
// Check for auth_token in cookies (Authentik sends via cookies)
|
||||
const authToken = getCookie(event, 'auth_token');
|
||||
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
if (!authToken) {
|
||||
console.error('❌ No auth_token cookie found');
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
message: 'No token provided'
|
||||
message: 'No authentication token provided'
|
||||
});
|
||||
}
|
||||
|
||||
// Extract the token without the 'Bearer ' prefix
|
||||
const token = authHeader.split(' ')[1];
|
||||
|
||||
try {
|
||||
// Verify token with Authentik
|
||||
const response = await $fetch(`${config.public.authentikUrl}/api/v3/core/tokens/verify/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
// Decode JWT token locally (no API call needed since token contains all user info)
|
||||
console.log('🔐 Decoding JWT token...');
|
||||
|
||||
// Simple JWT decode (split and base64 decode the payload)
|
||||
const tokenParts = authToken.split('.');
|
||||
if (tokenParts.length !== 3) {
|
||||
throw new Error('Invalid JWT format');
|
||||
}
|
||||
|
||||
// Decode the payload (second part of JWT)
|
||||
const payload = JSON.parse(Buffer.from(tokenParts[1], 'base64').toString());
|
||||
|
||||
// Check if token is expired
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
if (payload.exp && payload.exp < now) {
|
||||
throw new Error('Token has expired');
|
||||
}
|
||||
|
||||
// Extract user information from JWT payload
|
||||
const userInfo = {
|
||||
sub: payload.sub,
|
||||
email: payload.email,
|
||||
email_verified: payload.email_verified,
|
||||
name: payload.name,
|
||||
given_name: payload.given_name,
|
||||
preferred_username: payload.preferred_username,
|
||||
nickname: payload.nickname,
|
||||
groups: payload.groups || [],
|
||||
uid: payload.uid
|
||||
};
|
||||
|
||||
console.log('✅ Authentication successful for user:', userInfo.preferred_username);
|
||||
|
||||
// Add user info to event context
|
||||
event.context.auth = response;
|
||||
return response;
|
||||
event.context.auth = userInfo;
|
||||
event.context.authToken = authToken;
|
||||
|
||||
return userInfo;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Token verification error:', error);
|
||||
console.error('❌ Token verification failed:', error.message);
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
message: 'Invalid or expired token'
|
||||
message: 'Invalid or expired authentication token'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -1,8 +1,17 @@
|
||||
// Authentik API utilities
|
||||
// Simplified Authentik API utilities
|
||||
export const authentikFetch = async (endpoint, options = {}) => {
|
||||
const config = useRuntimeConfig();
|
||||
const AUTHENTIK_BASE_URL = `${config.public.authentikUrl}/api/v3`;
|
||||
|
||||
// Debug: Check if token exists
|
||||
if (!config.authentik?.apiToken) {
|
||||
console.error('❌ AUTHENTIK_API_TOKEN is missing from environment variables');
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
message: 'Authentik API token not configured. Please set AUTHENTIK_API_TOKEN in your .env file'
|
||||
});
|
||||
}
|
||||
|
||||
const defaultOptions = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@ -11,9 +20,6 @@ export const authentikFetch = async (endpoint, options = {}) => {
|
||||
};
|
||||
|
||||
try {
|
||||
// Log the request for debugging
|
||||
console.log(`Authentik API Request: ${AUTHENTIK_BASE_URL}${endpoint}`);
|
||||
|
||||
const response = await $fetch(`${AUTHENTIK_BASE_URL}${endpoint}`, {
|
||||
...defaultOptions,
|
||||
...options,
|
||||
@ -22,16 +28,38 @@ export const authentikFetch = async (endpoint, options = {}) => {
|
||||
...options.headers
|
||||
}
|
||||
});
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error(`Authentik API Error for ${endpoint}:`, error);
|
||||
// Only log errors that need attention
|
||||
console.error(`❌ Authentik API Error: ${endpoint} - ${error.message}`);
|
||||
|
||||
// More specific error messages
|
||||
if (error.response?.status === 403) {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
message: 'Authentik API token does not have sufficient permissions. Please check your token configuration in Authentik admin.',
|
||||
data: error.data || error.response?._data
|
||||
});
|
||||
}
|
||||
|
||||
if (error.response?.status === 401) {
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
message: 'Authentik API token is invalid or expired. Please check AUTHENTIK_API_TOKEN in your .env file.',
|
||||
data: error.data || error.response?._data
|
||||
});
|
||||
}
|
||||
|
||||
throw createError({
|
||||
statusCode: error.response?.status || 500,
|
||||
message: error.message || 'Failed to communicate with Authentik API'
|
||||
message: error.message || 'Failed to communicate with Authentik API',
|
||||
data: error.data || error.response?._data
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Only keep essential helper functions - no over-engineering
|
||||
export const getAuthentikUser = async (userId) => {
|
||||
return await authentikFetch(`/core/users/${userId}/`);
|
||||
};
|
||||
@ -40,35 +68,17 @@ export const getAuthentikGroups = async () => {
|
||||
return await authentikFetch('/core/groups/');
|
||||
};
|
||||
|
||||
export const createAuthentikApplication = async (applicationData) => {
|
||||
return await authentikFetch('/core/applications/', {
|
||||
method: 'POST',
|
||||
body: applicationData
|
||||
});
|
||||
};
|
||||
|
||||
export const createAuthentikProvider = async (providerData) => {
|
||||
return await authentikFetch('/providers/oauth2/', {
|
||||
method: 'POST',
|
||||
body: providerData
|
||||
});
|
||||
};
|
||||
|
||||
export const linkProviderToApplication = async (applicationId, providerId) => {
|
||||
return await authentikFetch(`/core/applications/${applicationId}/`, {
|
||||
method: 'PATCH',
|
||||
body: {
|
||||
provider: providerId
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Add a utility function to verify tokens
|
||||
export const verifyToken = async (token) => {
|
||||
return await authentikFetch('/core/tokens/verify/', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
// Clear Authentik policy cache
|
||||
export const clearAuthentikCache = async () => {
|
||||
try {
|
||||
console.log('🧹 Clearing Authentik policy cache...');
|
||||
const response = await authentikFetch('/policies/all/cache_clear/', {
|
||||
method: 'POST'
|
||||
});
|
||||
console.log('✅ Authentik cache cleared successfully');
|
||||
return { success: true, message: 'Cache cleared successfully' };
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to clear Authentik cache:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user