-
-
-
+
+
+
+ Application Type
+
+
+
+
+
{{ type.name.split(" ")[0] }}
+
+
+ {{ type.name.substring(2) }}
+
+
+ {{ type.description }}
+
+
+ Most Popular
-
-
{{ group.name }}
-
{{ group.description }}
-
{{ group.users }} users
-
-
-
+
+
+
-
-
-
-
+
+
+
+
+ Authentication Method
+
+
+ Choose how users will log into this application
+
+
+
+
+
+
{{ provider.name.split(" ")[0] }}
+
+
+ {{ provider.name.substring(2) }}
+
+
+ {{ provider.description }}
+
+
+
+ Recommended
+
+
{{
+ provider.technical
+ }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ getProviderInfo().title }}
+
+
+ {{ getProviderInfo().description }}
+
+
+
-
-
-
-
-
-
-
-
- Previous
-
-
-
-
- Next Step
-
-
-
+
+
+
@@ -700,4 +448,4 @@ input:focus {
select:focus {
@apply ring-2 ring-offset-2;
}
-
\ No newline at end of file
+
diff --git a/pages/applications/index.vue b/pages/applications/index.vue
index 04fe7c0..ce56a82 100644
--- a/pages/applications/index.vue
+++ b/pages/applications/index.vue
@@ -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();
+});
-
+
-
Applications
-
Manage applications integrated with Authentik
+
+ Applications
+
+
+ Manage applications integrated with Authentik
+
@@ -171,13 +275,22 @@ onMounted(() => {
-
-
Total Applications
-
{{ stats.totalApps }}
+
+ Total Applications
+
+
+ {{ stats.totalApps }}
+
@@ -187,13 +300,22 @@ onMounted(() => {
-
-
Active Applications
-
{{ stats.activeApps }}
+
+ Active Applications
+
+
+ {{ stats.activeApps }}
+
@@ -203,64 +325,35 @@ onMounted(() => {
-
-
Total App Users
-
{{ stats.totalUsers }}
+
+ Total App Users
+
+
+ {{ stats.totalUsers }}
+
-
-
-
-
-
-
-
-
All Applications
+
+ All Applications
+
{{ stats.totalApps }} applications
@@ -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"
>
-
-
{{ value.name ? value.name.charAt(0).toUpperCase() : '?' }}
+
+ {{
+ value.name ? value.name.charAt(0).toUpperCase() : "?"
+ }}
-
{{ value.name }}
-
{{ value.description || 'No description' }}
+
+ {{ value.name }}
+
+
+ {{ value.description || "No description" }}
+
-
- {{ value.status || 'Unknown' }}
+
+ {{ value.status || "Unknown" }}
-
- {{ value.provider }}
+ {{
+ value.provider
+ }}
-
{{ value.userCount || 0 }} users
-
{{ value.roleCount || 0 }} roles
+
+ {{ value.userCount || 0 }} users
+
+
+ {{ value.roleCount || 0 }} roles
+
-
{{ formatDate(value.createdAt) }}
-
{{ value.createdBy || 'System' }}
+
+ {{ formatDate(value.createdAt) }}
+
+
+ {{ value.createdBy || "System" }}
+
-
-
+
+
-
+
-
+
+ -
@@ -353,4 +476,4 @@ onMounted(() => {
\ No newline at end of file
+
diff --git a/pages/create-user/index.vue b/pages/create-user/index.vue
index e6520bf..015191f 100644
--- a/pages/create-user/index.vue
+++ b/pages/create-user/index.vue
@@ -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
}
diff --git a/pages/groups/index.vue b/pages/groups/index.vue
index 7a955ab..7c0fb9f 100644
--- a/pages/groups/index.vue
+++ b/pages/groups/index.vue
@@ -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"
>
diff --git a/pages/roles/index.vue b/pages/roles/index.vue
index fe82075..950bb85 100644
--- a/pages/roles/index.vue
+++ b/pages/roles/index.vue
@@ -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"
>
diff --git a/pages/test-api.vue b/pages/test-api.vue
new file mode 100644
index 0000000..fed3e6c
--- /dev/null
+++ b/pages/test-api.vue
@@ -0,0 +1,223 @@
+
+
+
+
API Testing Page
+
+
+
+
Test Application Creation
+
+
+
+ Test OAuth2 App
+
+
+
+ Test LDAP App
+
+
+
+ Test SAML App
+
+
+
+ Test Proxy App
+
+
+
+
+
+ List Applications
+
+
+
+ Test Authentik Connection
+
+
+
+
+ โณ {{ loadingMessage }}
+
+
+
+
+
+
Results
+
+
+
+ Status:
+
+ {{ result.success ? 'โ
Success' : 'โ Error' }}
+
+
+
+
+
{{ JSON.stringify(result.data, null, 2) }}
+
+
+
+
+ Click a test button above to see results here
+
+
+
+ Clear Results
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pages/users/index.vue b/pages/users/index.vue
index 386cfca..ec9fd86 100644
--- a/pages/users/index.vue
+++ b/pages/users/index.vue
@@ -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"
>
diff --git a/server/api/applications/[id].js b/server/api/applications/[id].js
index 5040a16..4cd5e95 100644
--- a/server/api/applications/[id].js
+++ b/server/api/applications/[id].js
@@ -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'
});
}
diff --git a/server/api/applications/index.js b/server/api/applications/index.js
index fe49ee3..4c78341 100644
--- a/server/api/applications/index.js
+++ b/server/api/applications/index.js
@@ -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:
diff --git a/server/api/test-authentik.js b/server/api/test-authentik.js
new file mode 100644
index 0000000..6ea682c
--- /dev/null
+++ b/server/api/test-authentik.js
@@ -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
+ }
+ };
+ }
+});
\ No newline at end of file
diff --git a/server/utils/auth.js b/server/utils/auth.js
index e51a6d0..eccd3d4 100644
--- a/server/utils/auth.js
+++ b/server/utils/auth.js
@@ -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'
});
}
};
diff --git a/server/utils/authentik.js b/server/utils/authentik.js
index 8c7b947..560a2f1 100644
--- a/server/utils/authentik.js
+++ b/server/utils/authentik.js
@@ -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;
+ }
};
\ No newline at end of file