diff --git a/composables/useVoiceReader.js b/composables/useVoiceReader.js index 4778dd1..1ef040c 100644 --- a/composables/useVoiceReader.js +++ b/composables/useVoiceReader.js @@ -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) => { diff --git a/pages/applications/[id]/edit.vue b/pages/applications/[id]/edit.vue new file mode 100644 index 0000000..9007a37 --- /dev/null +++ b/pages/applications/[id]/edit.vue @@ -0,0 +1,588 @@ + + + + + diff --git a/pages/applications/[id]/index.vue b/pages/applications/[id]/index.vue new file mode 100644 index 0000000..f3617b5 --- /dev/null +++ b/pages/applications/[id]/index.vue @@ -0,0 +1,316 @@ + + + + + \ No newline at end of file diff --git a/pages/applications/create.vue b/pages/applications/create.vue index 9c9a562..fd62e3b 100644 --- a/pages/applications/create.vue +++ b/pages/applications/create.vue @@ -6,364 +6,261 @@ definePageMeta({ breadcrumb: [ { name: "Dashboard", path: "/dashboard" }, { name: "Applications", path: "/applications" }, - { name: "Create Application", path: "/applications/create", type: "current" } - ] + { + name: "Create Application", + path: "/applications/create", + type: "current", + }, + ], }); -import { ref, reactive, computed, onMounted } from 'vue' +import { ref, reactive, computed } from "vue"; -// Simplified form state +const { $swal } = useNuxtApp(); + +// Super simplified form - only what users really need const applicationForm = reactive({ - // Step 1: Basic Info - name: '', - slug: '', - description: '', - launchUrl: '', - icon: null, - - // Step 2: Quick Setup (NEW - primary method) - useQuickSetup: true, - setupType: '', - - // Advanced Configuration (hidden by default) - showAdvancedConfig: false, - publisher: '', - status: 'development', - - // Provider Configuration (simplified) - providerType: 'oauth2', - clientId: '', - clientSecret: '', - redirectUris: '', - - // Access Control (simplified) - policyEngineMode: 'any', - selectedGroups: [], - - // Authentik Integration - syncToAuthentik: true, - createProvider: true -}) + name: "", + slug: "", + description: "", + launchUrl: "", + setupType: "web-app", // Default to most common type + providerType: "oauth2", // Default to most common provider +}); -// Step management -const currentStep = ref(1) -const totalSteps = 3 +// Loading state +const isLoading = ref(false); -// Quick setup types with smart defaults +// Super simple setup types - no tech jargon const setupTypes = ref([ { - id: 'web-app', - name: '๐ŸŒ Web Application', - description: 'Standard web application with OAuth2 authentication', - icon: 'ph:globe', - color: 'blue', - recommended: true, - defaults: { - providerType: 'oauth2', - status: 'development', - policyEngineMode: 'any', - createProvider: true - } + id: "web-app", + name: "๐ŸŒ Website/Web App", + description: "For websites and web applications that users log into", + popular: true, }, { - id: 'api-service', - name: '๐Ÿ”Œ API Service', - description: 'Backend API or microservice with service-to-service auth', - icon: 'ph:api', - color: 'green', - defaults: { - providerType: 'oauth2', - status: 'development', - policyEngineMode: 'all', - createProvider: true - } + id: "mobile-app", + name: "๐Ÿ“ฑ Mobile App", + description: "For mobile apps (iPhone, Android) that need login", + popular: false, }, { - id: 'enterprise-app', - name: '๐Ÿข Enterprise Application', - description: 'Enterprise app with SAML SSO and strict policies', - icon: 'ph:building', - color: 'purple', - defaults: { - providerType: 'saml', - status: 'development', - policyEngineMode: 'all', - createProvider: true - } + id: "desktop-app", + name: "๐Ÿ’ป Desktop App", + description: "For desktop software that needs user authentication", + popular: false, }, - { - id: 'custom', - name: 'โš™๏ธ Custom Configuration', - description: 'Manual configuration for specific requirements', - icon: 'ph:gear', - color: 'gray', - defaults: { - providerType: 'oauth2', - status: 'development', - policyEngineMode: 'any', - createProvider: false - } - } -]) +]); -// Simplified provider types +// Simple provider presets - industry standards const providerTypes = ref([ - { value: 'oauth2', label: '๐Ÿ” OAuth2/OIDC', description: 'Modern web authentication (Recommended)' }, - { value: 'saml', label: '๐Ÿข SAML', description: 'Enterprise SSO standard' }, - { value: 'proxy', label: '๐Ÿ”„ Proxy', description: 'Forward authentication proxy' } -]) - -// Available groups (simplified) -const availableGroups = ref([ - { id: '1', name: 'All Users', description: 'Everyone can access', users: 120 }, - { id: '2', name: 'IT Department', description: 'IT team members', users: 15 }, - { id: '3', name: 'Management', description: 'Managers and executives', users: 8 }, - { id: '4', name: 'HR Department', description: 'Human resources team', users: 6 } -]) - -// Loading states -const isLoading = ref(false) - -// Computed properties -const selectedSetupType = computed(() => { - return setupTypes.value.find(t => t.id === applicationForm.setupType) -}) + { + id: "oauth2", + name: "๐Ÿ” OAuth2/OIDC", + description: "Modern login standard - works with most apps (Recommended)", + popular: true, + technical: "OAuth2/OpenID Connect", + }, + { + id: "saml", + name: "๐Ÿข SAML SSO", + description: "Enterprise single sign-on - common in large companies", + popular: false, + technical: "SAML 2.0", + }, + { + id: "ldap", + name: "๐Ÿ“ LDAP Directory", + description: "Connect to Active Directory or LDAP servers", + popular: false, + technical: "LDAP/Active Directory", + }, + { + id: "proxy", + name: "๐Ÿ”„ Proxy Auth", + description: "For older apps that don't support modern login", + popular: false, + technical: "Forward Auth Proxy", + }, +]); +// Simple validation const isFormValid = computed(() => { - if (currentStep.value === 1) { - return applicationForm.name && applicationForm.description && applicationForm.launchUrl - } - if (currentStep.value === 2) { - return applicationForm.useQuickSetup ? applicationForm.setupType : true - } - return true -}) + return ( + applicationForm.name && + applicationForm.description && + applicationForm.launchUrl && + applicationForm.setupType && + applicationForm.providerType + ); +}); -const hasRequiredOAuth2Fields = computed(() => { - if (applicationForm.providerType !== 'oauth2') return true - return applicationForm.clientId && applicationForm.clientSecret && applicationForm.redirectUris -}) - -// Methods +// Auto-generate slug when name changes const generateSlug = () => { if (applicationForm.name) { applicationForm.slug = applicationForm.name .toLowerCase() - .replace(/[^a-z0-9]/g, '-') - .replace(/-+/g, '-') - .replace(/^-|-$/g, '') + .replace(/[^a-z0-9]/g, "-") + .replace(/-+/g, "-") + .replace(/^-|-$/g, ""); } -} - -const selectSetupType = (typeId) => { - applicationForm.setupType = typeId - applicationForm.useQuickSetup = true - applicationForm.showAdvancedConfig = false - - // Apply defaults - const setupType = setupTypes.value.find(t => t.id === typeId) - if (setupType && typeId !== 'custom') { - Object.assign(applicationForm, setupType.defaults) - - // Auto-generate OAuth2 credentials if needed - if (setupType.defaults.providerType === 'oauth2') { - generateClientCredentials() - } - } else if (typeId === 'custom') { - applicationForm.showAdvancedConfig = true - applicationForm.createProvider = false - } -} - -const generateClientCredentials = () => { - applicationForm.clientId = 'app_' + Math.random().toString(36).substring(2, 15) - applicationForm.clientSecret = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15) - - // Auto-generate redirect URI based on launch URL - if (applicationForm.launchUrl) { - try { - const url = new URL(applicationForm.launchUrl) - applicationForm.redirectUris = `${url.origin}/auth/callback` - } catch (e) { - applicationForm.redirectUris = 'https://yourapp.com/auth/callback' - } - } -} - -const toggleAdvancedConfig = () => { - applicationForm.showAdvancedConfig = !applicationForm.showAdvancedConfig - if (applicationForm.showAdvancedConfig) { - applicationForm.useQuickSetup = false - applicationForm.setupType = 'custom' - } -} - -const nextStep = () => { - if (currentStep.value < totalSteps && isFormValid.value) { - currentStep.value++ - } -} - -const prevStep = () => { - if (currentStep.value > 1) { - currentStep.value-- - } -} - -const toggleGroupSelection = (groupId) => { - const index = applicationForm.selectedGroups.indexOf(groupId) - if (index > -1) { - applicationForm.selectedGroups.splice(index, 1) - } else { - applicationForm.selectedGroups.push(groupId) - } -} +}; +// Create application - super simplified const createApplication = async () => { - if (!isFormValid.value || !hasRequiredOAuth2Fields.value) return - - isLoading.value = true - + if (!isFormValid.value) return; + + isLoading.value = true; + try { const applicationData = { name: applicationForm.name, slug: applicationForm.slug, - description: applicationForm.description, - launchUrl: applicationForm.launchUrl, - icon: applicationForm.icon, - publisher: applicationForm.publisher || 'IT Department', - status: applicationForm.status, - - setupType: applicationForm.useQuickSetup ? applicationForm.setupType : 'custom', - - provider: { - type: applicationForm.providerType, - clientId: applicationForm.clientId, - clientSecret: applicationForm.clientSecret, - redirectUris: applicationForm.redirectUris.split('\n').filter(uri => uri.trim()) - }, - - policyEngineMode: applicationForm.policyEngineMode, - selectedGroups: applicationForm.selectedGroups, - - syncToAuthentik: applicationForm.syncToAuthentik, - createProvider: applicationForm.createProvider - } - - console.log('Creating application:', applicationData) - - // Simulate API call - await new Promise(resolve => setTimeout(resolve, 2000)) - - // Success - redirect - await navigateTo('/applications') - + meta_description: applicationForm.description, + meta_launch_url: applicationForm.launchUrl, + meta_publisher: "IT Department", + setupType: applicationForm.setupType, + providerType: applicationForm.providerType, + }; + + console.log("Creating application:", applicationData); + + const response = await $fetch("/api/applications", { + method: "POST", + body: applicationData, + }); + + console.log("Application created successfully:", response); + + // Show success toast + $swal.fire({ + title: "Application created successfully!", + text: "You can now access the application from the applications list.", + icon: "success", + confirmButtonText: "OK", + }); + + // Success - redirect back to applications list after short delay + setTimeout(async () => { + await navigateTo("/applications"); + }, 1000); } catch (error) { - console.error('Failed to create application:', error) + console.error("Failed to create application:", error); + + // Show error toast with specific message + let errorMessage = "Failed to create application. "; + if (error.data?.message) { + errorMessage += error.data.message; + } else { + errorMessage += "Please check the details and try again."; + } + + $swal.fire({ + title: "Failed to create application!", + text: errorMessage, + icon: "error", + confirmButtonText: "OK", + }); } finally { - isLoading.value = false + isLoading.value = false; } -} +}; + +// Get dynamic info based on selected provider +const getProviderInfo = () => { + const providerInfo = { + oauth2: { + title: "OAuth2 will be automatically configured", + description: + "We'll create secure OAuth2 credentials and configure the callback URL. Your app will support modern login standards used by Google, Microsoft, and other major platforms.", + }, + saml: { + title: "SAML SSO will be automatically configured", + description: + "We'll set up SAML 2.0 single sign-on with enterprise-grade security. This is perfect for connecting with corporate identity systems and Active Directory.", + }, + ldap: { + title: "LDAP directory integration will be configured", + description: + "We'll configure LDAP/Active Directory authentication. Users can log in with their existing network credentials without any additional setup.", + }, + proxy: { + title: "Proxy authentication will be configured", + description: + "We'll set up forward authentication for legacy applications. This adds modern login to apps that don't support OAuth2 or SAML natively.", + }, + }; + + return providerInfo[applicationForm.providerType] || providerInfo.oauth2; +}; const resetForm = () => { Object.assign(applicationForm, { - name: '', - slug: '', - description: '', - launchUrl: '', - icon: null, - useQuickSetup: true, - setupType: '', - showAdvancedConfig: false, - publisher: '', - status: 'development', - providerType: 'oauth2', - clientId: '', - clientSecret: '', - redirectUris: '', - policyEngineMode: 'any', - selectedGroups: [], - syncToAuthentik: true, - createProvider: true - }) - currentStep.value = 1 -} - -// Initialize -onMounted(() => { - // Set default setup type - applicationForm.setupType = 'web-app' - applicationForm.selectedGroups = ['1'] // Default to "All Users" -}) + name: "", + slug: "", + description: "", + launchUrl: "", + setupType: "web-app", + providerType: "oauth2", + }); +}; @@ -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(); +}); @@ -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" >