- 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.
356 lines
12 KiB
Vue
356 lines
12 KiB
Vue
<script setup>
|
|
definePageMeta({
|
|
title: "Applications",
|
|
middleware: ["auth"],
|
|
requiresAuth: true,
|
|
breadcrumb: [
|
|
{ name: "Dashboard", path: "/dashboard" },
|
|
{ name: "Applications", path: "/applications", type: "current" }
|
|
]
|
|
});
|
|
|
|
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
|
|
}
|
|
])
|
|
|
|
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
|
|
|
|
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))
|
|
)
|
|
}
|
|
|
|
if (selectedStatus.value !== 'all') {
|
|
filtered = filtered.filter(app => app.status === selectedStatus.value)
|
|
}
|
|
|
|
if (selectedProvider.value !== 'all') {
|
|
filtered = filtered.filter(app => app.provider === selectedProvider.value)
|
|
}
|
|
|
|
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)
|
|
}))
|
|
|
|
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 }))
|
|
})
|
|
|
|
// Methods
|
|
const deleteApplication = async (applicationId) => {
|
|
if (!confirm('Are you sure you want to delete this application? This action cannot be undone.')) {
|
|
return
|
|
}
|
|
|
|
const index = applications.value.findIndex(app => app.id === applicationId)
|
|
if (index > -1) {
|
|
applications.value.splice(index, 1)
|
|
console.log('Application deleted successfully')
|
|
}
|
|
}
|
|
|
|
const formatDate = (dateString) => {
|
|
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'
|
|
}
|
|
}
|
|
|
|
// Initialize
|
|
onMounted(() => {
|
|
// Load initial data
|
|
})
|
|
</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>
|
|
</div>
|
|
<div class="flex space-x-3">
|
|
<rs-button @click="navigateTo('/applications/create')">
|
|
<Icon name="ph:plus" class="w-4 h-4 mr-2" />
|
|
Create Application
|
|
</rs-button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Stats Cards -->
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
|
|
<rs-card>
|
|
<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>
|
|
</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>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</rs-card>
|
|
|
|
<rs-card>
|
|
<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>
|
|
</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>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</rs-card>
|
|
|
|
<rs-card>
|
|
<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>
|
|
</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>
|
|
</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>
|
|
<rs-badge variant="info">{{ stats.totalApps }} applications</rs-badge>
|
|
</div>
|
|
</template>
|
|
<template #body>
|
|
<RsTable
|
|
: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>
|
|
</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>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Status Column -->
|
|
<template #status="{ value }">
|
|
<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"
|
|
/>
|
|
<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>
|
|
</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>
|
|
</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}`)">
|
|
<Icon name="ph:eye" class="w-4 h-4" />
|
|
</button>
|
|
<button class="text-primary hover:text-primary/80" @click="navigateTo(`/applications/${value.id}/edit`)">
|
|
<Icon name="ph:pencil" class="w-4 h-4" />
|
|
</button>
|
|
<button class="text-red-600 hover:text-red-800" @click="deleteApplication(value.id)">
|
|
<Icon name="ph:trash" class="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
</template>
|
|
</RsTable>
|
|
</template>
|
|
</rs-card>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
/* Component specific styles */
|
|
</style> |