- Updated README.md to reflect the new project name and provide an overview of the Role-Based Access Control (RBAC) system. - Added new components for RBAC management, including: - PermissionExample.vue: Demonstrates permission-based navigation. - GroupCard.vue: Displays group information and assigned roles. - PermissionMatrix.vue: Visual representation of permissions across roles and resources. - RoleTemplates.vue: Quick role templates for applying pre-configured permissions. - StatsCards.vue: Displays statistics related to users, groups, and roles. - Introduced useRbacPermissions.js for managing permission checks. - Created docker-compose.yml for PostgreSQL and Redis services. - Developed comprehensive documentation for application management and Authentik integration. - Added multiple pages for managing applications, groups, roles, and users, including bulk operations and templates. - Updated navigation structure to include new RBAC management paths.
703 lines
24 KiB
Vue
703 lines
24 KiB
Vue
<script setup>
|
|
definePageMeta({
|
|
title: "Create Application",
|
|
middleware: ["auth"],
|
|
requiresAuth: true,
|
|
breadcrumb: [
|
|
{ name: "Dashboard", path: "/dashboard" },
|
|
{ name: "Applications", path: "/applications" },
|
|
{ name: "Create Application", path: "/applications/create", type: "current" }
|
|
]
|
|
});
|
|
|
|
import { ref, reactive, computed, onMounted } from 'vue'
|
|
|
|
// Simplified form state
|
|
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
|
|
})
|
|
|
|
// Step management
|
|
const currentStep = ref(1)
|
|
const totalSteps = 3
|
|
|
|
// Quick setup types with smart defaults
|
|
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: '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: '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: '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
|
|
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)
|
|
})
|
|
|
|
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
|
|
})
|
|
|
|
const hasRequiredOAuth2Fields = computed(() => {
|
|
if (applicationForm.providerType !== 'oauth2') return true
|
|
return applicationForm.clientId && applicationForm.clientSecret && applicationForm.redirectUris
|
|
})
|
|
|
|
// Methods
|
|
const generateSlug = () => {
|
|
if (applicationForm.name) {
|
|
applicationForm.slug = applicationForm.name
|
|
.toLowerCase()
|
|
.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)
|
|
}
|
|
}
|
|
|
|
const createApplication = async () => {
|
|
if (!isFormValid.value || !hasRequiredOAuth2Fields.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')
|
|
|
|
} catch (error) {
|
|
console.error('Failed to create application:', error)
|
|
} finally {
|
|
isLoading.value = false
|
|
}
|
|
}
|
|
|
|
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"
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div>
|
|
<LayoutsBreadcrumb />
|
|
|
|
<!-- Header -->
|
|
<div class="mb-6">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">Add New Application</h1>
|
|
<p class="text-gray-600 dark:text-gray-400">Create and configure an application with Authentik integration</p>
|
|
</div>
|
|
<div class="flex space-x-3">
|
|
<rs-button @click="resetForm" variant="primary-outline">
|
|
<Icon name="ph:arrow-clockwise" class="w-4 h-4 mr-2" />
|
|
Reset
|
|
</rs-button>
|
|
<rs-button
|
|
v-if="currentStep === totalSteps"
|
|
@click="createApplication"
|
|
:disabled="!isFormValid || !hasRequiredOAuth2Fields || isLoading"
|
|
>
|
|
<Icon name="ph:plus" class="w-4 h-4 mr-2" />
|
|
{{ isLoading ? 'Creating...' : 'Create Application' }}
|
|
</rs-button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Step Progress -->
|
|
<div class="mb-8">
|
|
<div class="flex items-center justify-center space-x-4">
|
|
<div
|
|
v-for="step in totalSteps"
|
|
:key="step"
|
|
class="flex items-center"
|
|
>
|
|
<div
|
|
class="flex items-center justify-center w-10 h-10 rounded-full border-2 transition-colors"
|
|
:class="{
|
|
'border-primary bg-primary text-white': currentStep >= step,
|
|
'border-gray-300 text-gray-300': currentStep < step
|
|
}"
|
|
>
|
|
<Icon v-if="currentStep > step" name="ph:check" class="w-5 h-5" />
|
|
<span v-else class="text-sm font-medium">{{ step }}</span>
|
|
</div>
|
|
<div v-if="step < totalSteps" class="w-16 h-0.5 mx-4"
|
|
:class="currentStep > step ? 'bg-primary' : 'bg-gray-300'"></div>
|
|
</div>
|
|
</div>
|
|
<div class="text-center mt-4">
|
|
<span class="text-sm text-gray-600 dark:text-gray-400">
|
|
Step {{ currentStep }} of {{ totalSteps }}:
|
|
{{ currentStep === 1 ? 'Basic Information' : currentStep === 2 ? 'Configuration' : 'Access Control' }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Step 1: Basic Information -->
|
|
<div v-if="currentStep === 1">
|
|
<rs-card>
|
|
<template #header>
|
|
<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">
|
|
<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="Display name for your application"
|
|
/>
|
|
|
|
<FormKit
|
|
v-model="applicationForm.slug"
|
|
type="text"
|
|
label="URL Slug"
|
|
placeholder="employee-portal"
|
|
validation="required"
|
|
validation-visibility="dirty"
|
|
help="URL-friendly identifier (auto-generated)"
|
|
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 the purpose and features of this application"
|
|
/>
|
|
|
|
<FormKit
|
|
v-model="applicationForm.launchUrl"
|
|
type="url"
|
|
label="Application URL"
|
|
placeholder="https://portal.company.com"
|
|
validation="required|url"
|
|
validation-visibility="dirty"
|
|
help="Where users will access your application"
|
|
/>
|
|
|
|
<FormKit
|
|
v-model="applicationForm.icon"
|
|
type="file"
|
|
label="Application Icon (Optional)"
|
|
help="Upload an icon for the application (PNG, JPG, SVG)"
|
|
accept="image/*"
|
|
/>
|
|
</div>
|
|
</FormKit>
|
|
</template>
|
|
</rs-card>
|
|
</div>
|
|
|
|
<!-- Step 2: Quick Setup or Advanced Configuration -->
|
|
<div v-if="currentStep === 2">
|
|
<rs-card>
|
|
<template #header>
|
|
<div class="flex items-center justify-between">
|
|
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Configuration Method</h3>
|
|
<rs-badge variant="info">Choose Setup Type</rs-badge>
|
|
</div>
|
|
</template>
|
|
<template #body>
|
|
<div class="space-y-6">
|
|
<p class="text-sm text-gray-600 dark:text-gray-400">
|
|
Select a setup type for quick configuration with smart defaults, or choose custom for manual setup.
|
|
</p>
|
|
|
|
<!-- Quick Setup Types -->
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div
|
|
v-for="setupType in setupTypes"
|
|
:key="setupType.id"
|
|
@click="selectSetupType(setupType.id)"
|
|
class="cursor-pointer p-5 border-2 rounded-lg transition-all hover:shadow-md relative"
|
|
:class="{
|
|
'border-primary bg-primary/5': applicationForm.setupType === setupType.id,
|
|
'border-gray-200 dark:border-gray-700 hover:border-primary/50': applicationForm.setupType !== setupType.id
|
|
}"
|
|
>
|
|
<div class="flex items-start space-x-4">
|
|
<div class="flex-shrink-0">
|
|
<div class="w-12 h-12 rounded-lg flex items-center justify-center"
|
|
:class="`bg-${setupType.color}-100 dark:bg-${setupType.color}-900/30`">
|
|
<Icon :name="setupType.icon" :class="`w-6 h-6 text-${setupType.color}-600 dark:text-${setupType.color}-400`" />
|
|
</div>
|
|
</div>
|
|
<div class="flex-1 min-w-0">
|
|
<div class="flex items-center space-x-2 mb-2">
|
|
<h4 class="text-sm font-medium text-gray-900 dark:text-white">
|
|
{{ setupType.name }}
|
|
</h4>
|
|
<rs-badge v-if="setupType.recommended" variant="success" size="xs">Recommended</rs-badge>
|
|
</div>
|
|
<p class="text-xs text-gray-600 dark:text-gray-400 mb-3">
|
|
{{ setupType.description }}
|
|
</p>
|
|
<div class="flex items-center justify-between">
|
|
<span class="text-xs font-medium text-gray-500">
|
|
Auto-configured
|
|
</span>
|
|
<Icon
|
|
v-if="applicationForm.setupType === setupType.id"
|
|
name="ph:check-circle-fill"
|
|
class="w-5 h-5 text-primary"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Advanced Configuration Toggle -->
|
|
<div v-if="applicationForm.setupType && applicationForm.setupType !== 'custom'" class="pt-4 border-t border-gray-200 dark:border-gray-700">
|
|
<rs-button
|
|
@click="toggleAdvancedConfig"
|
|
variant="secondary-outline"
|
|
size="sm"
|
|
class="w-full"
|
|
>
|
|
<Icon name="ph:gear" class="w-4 h-4 mr-2" />
|
|
{{ applicationForm.showAdvancedConfig ? 'Hide' : 'Show' }} Advanced Settings
|
|
</rs-button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</rs-card>
|
|
|
|
<!-- Advanced Configuration (Hidden by default) -->
|
|
<rs-card v-if="applicationForm.showAdvancedConfig || applicationForm.setupType === 'custom'" class="mt-6">
|
|
<template #header>
|
|
<div class="flex items-center">
|
|
<Icon name="ph:gear" class="w-5 h-5 mr-2 text-orange-600" />
|
|
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Advanced Configuration</h3>
|
|
<rs-badge variant="warning" class="ml-2">Expert Mode</rs-badge>
|
|
</div>
|
|
</template>
|
|
<template #body>
|
|
<div class="space-y-6">
|
|
<div class="p-3 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg">
|
|
<div class="flex items-start">
|
|
<Icon name="ph:warning" class="w-5 h-5 text-yellow-600 dark:text-yellow-400 mr-2 mt-0.5" />
|
|
<div class="text-sm text-yellow-800 dark:text-yellow-200">
|
|
<p class="font-medium mb-1">Advanced Configuration</p>
|
|
<p>Configure technical settings manually. Most applications should use the quick setup above.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Basic Advanced Settings -->
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<FormKit
|
|
v-model="applicationForm.publisher"
|
|
type="text"
|
|
label="Publisher"
|
|
placeholder="IT Department"
|
|
help="Organization or team responsible"
|
|
/>
|
|
|
|
<FormKit
|
|
v-model="applicationForm.status"
|
|
type="select"
|
|
label="Status"
|
|
:options="[
|
|
{ value: 'development', label: 'Development' },
|
|
{ value: 'active', label: 'Active' },
|
|
{ value: 'inactive', label: 'Inactive' }
|
|
]"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Provider Configuration -->
|
|
<div>
|
|
<h4 class="text-sm font-medium text-gray-900 dark:text-white mb-3">Provider Settings</h4>
|
|
<div class="space-y-4">
|
|
<FormKit
|
|
v-model="applicationForm.providerType"
|
|
type="select"
|
|
label="Provider Type"
|
|
:options="providerTypes"
|
|
/>
|
|
|
|
<!-- OAuth2 specific settings -->
|
|
<div v-if="applicationForm.providerType === 'oauth2'" class="space-y-4 p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
|
|
<div class="flex items-center justify-between mb-3">
|
|
<h5 class="text-sm font-medium text-gray-900 dark:text-white">OAuth2 Credentials</h5>
|
|
<rs-button @click="generateClientCredentials" variant="primary-outline" size="xs">
|
|
<Icon name="ph:key" class="w-3 h-3 mr-1" />
|
|
Generate
|
|
</rs-button>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<FormKit
|
|
v-model="applicationForm.clientId"
|
|
type="text"
|
|
label="Client ID"
|
|
placeholder="app_abc123"
|
|
validation="required"
|
|
validation-visibility="dirty"
|
|
/>
|
|
|
|
<FormKit
|
|
v-model="applicationForm.clientSecret"
|
|
type="password"
|
|
label="Client Secret"
|
|
placeholder="••••••••••••••••"
|
|
validation="required"
|
|
validation-visibility="dirty"
|
|
/>
|
|
</div>
|
|
|
|
<FormKit
|
|
v-model="applicationForm.redirectUris"
|
|
type="text"
|
|
label="Redirect URI"
|
|
placeholder="https://yourapp.com/auth/callback"
|
|
validation="required"
|
|
validation-visibility="dirty"
|
|
help="Where users return after authentication"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</rs-card>
|
|
</div>
|
|
|
|
<!-- Step 3: Access Control -->
|
|
<div v-if="currentStep === 3">
|
|
<rs-card>
|
|
<template #header>
|
|
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Who Can Access This Application?</h3>
|
|
</template>
|
|
<template #body>
|
|
<div class="space-y-6">
|
|
<p class="text-sm text-gray-600 dark:text-gray-400">
|
|
Select which groups should have access to this application. You can modify these settings later.
|
|
</p>
|
|
|
|
<!-- Group Selection -->
|
|
<div class="space-y-3">
|
|
<div
|
|
v-for="group in availableGroups"
|
|
:key="group.id"
|
|
@click="toggleGroupSelection(group.id)"
|
|
class="cursor-pointer p-4 border-2 rounded-lg transition-all hover:shadow-sm"
|
|
:class="{
|
|
'border-primary bg-primary/5': applicationForm.selectedGroups.includes(group.id),
|
|
'border-gray-200 dark:border-gray-700 hover:border-primary/50': !applicationForm.selectedGroups.includes(group.id)
|
|
}"
|
|
>
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center space-x-3">
|
|
<div class="flex-shrink-0">
|
|
<div class="w-10 h-10 bg-blue-100 dark:bg-blue-900/30 rounded-lg flex items-center justify-center">
|
|
<Icon name="ph:users-three" class="w-5 h-5 text-blue-600 dark:text-blue-400" />
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<h4 class="text-sm font-medium text-gray-900 dark:text-white">{{ group.name }}</h4>
|
|
<p class="text-xs text-gray-600 dark:text-gray-400">{{ group.description }}</p>
|
|
<span class="text-xs text-gray-500">{{ group.users }} users</span>
|
|
</div>
|
|
</div>
|
|
<Icon
|
|
v-if="applicationForm.selectedGroups.includes(group.id)"
|
|
name="ph:check-circle-fill"
|
|
class="w-6 h-6 text-primary"
|
|
/>
|
|
<Icon
|
|
v-else
|
|
name="ph:circle"
|
|
class="w-6 h-6 text-gray-300"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Policy Engine Mode (simplified) -->
|
|
<div class="pt-4 border-t border-gray-200 dark:border-gray-700">
|
|
<FormKit
|
|
v-model="applicationForm.policyEngineMode"
|
|
type="radio"
|
|
label="Access Policy"
|
|
:options="[
|
|
{ value: 'any', label: 'User needs access to ANY selected group' },
|
|
{ value: 'all', label: 'User needs access to ALL selected groups' }
|
|
]"
|
|
help="How multiple group memberships are evaluated"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</rs-card>
|
|
</div>
|
|
|
|
<!-- Navigation Buttons -->
|
|
<div class="flex items-center justify-between mt-8">
|
|
<rs-button
|
|
v-if="currentStep > 1"
|
|
@click="prevStep"
|
|
variant="secondary"
|
|
>
|
|
<Icon name="ph:arrow-left" class="w-4 h-4 mr-2" />
|
|
Previous
|
|
</rs-button>
|
|
<div v-else></div>
|
|
|
|
<rs-button
|
|
v-if="currentStep < totalSteps"
|
|
@click="nextStep"
|
|
:disabled="!isFormValid"
|
|
variant="primary"
|
|
>
|
|
Next Step
|
|
<Icon name="ph:arrow-right" class="w-4 h-4 ml-2" />
|
|
</rs-button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
input:focus {
|
|
@apply ring-2 ring-offset-2;
|
|
}
|
|
|
|
select:focus {
|
|
@apply ring-2 ring-offset-2;
|
|
}
|
|
</style> |