EDMS/components/dms/auth/ExternalRBACIntegration.vue
2025-06-05 14:57:08 +08:00

997 lines
34 KiB
Vue

<script setup>
import { ref, computed, onMounted } from 'vue';
const props = defineProps({
provider: {
type: String,
default: 'ldap' // ldap, oauth2, saml, oidc
},
autoSync: {
type: Boolean,
default: true
}
});
const emit = defineEmits([
'user-authenticated',
'permission-updated',
'sync-complete',
'error'
]);
// Local state
const isConnected = ref(false);
const isConnecting = ref(false);
const isSyncing = ref(false);
const lastSyncTime = ref(null);
const connectionStatus = ref('disconnected');
const externalUsers = ref([]);
const externalRoles = ref([]);
const externalGroups = ref([]);
const mappedPermissions = ref([]);
const syncErrors = ref([]);
// Configuration for different providers
const providerConfigs = {
ldap: {
name: 'LDAP/Active Directory',
icon: 'mdi:server-network',
fields: [
{ key: 'server', label: 'LDAP Server', type: 'text', required: true },
{ key: 'port', label: 'Port', type: 'number', default: 389 },
{ key: 'baseDN', label: 'Base DN', type: 'text', required: true },
{ key: 'userDN', label: 'User DN', type: 'text' },
{ key: 'bindDN', label: 'Bind DN', type: 'text' },
{ key: 'bindPassword', label: 'Bind Password', type: 'password' },
{ key: 'userFilter', label: 'User Filter', type: 'text', default: '(objectClass=user)' },
{ key: 'groupFilter', label: 'Group Filter', type: 'text', default: '(objectClass=group)' },
{ key: 'enableSSL', label: 'Enable SSL', type: 'boolean', default: false }
]
},
oauth2: {
name: 'OAuth 2.0',
icon: 'mdi:shield-account',
fields: [
{ key: 'clientId', label: 'Client ID', type: 'text', required: true },
{ key: 'clientSecret', label: 'Client Secret', type: 'password', required: true },
{ key: 'authorizationUrl', label: 'Authorization URL', type: 'url', required: true },
{ key: 'tokenUrl', label: 'Token URL', type: 'url', required: true },
{ key: 'userInfoUrl', label: 'User Info URL', type: 'url' },
{ key: 'scope', label: 'Scope', type: 'text', default: 'openid profile email' },
{ key: 'redirectUri', label: 'Redirect URI', type: 'url' }
]
},
saml: {
name: 'SAML 2.0',
icon: 'mdi:security',
fields: [
{ key: 'entityId', label: 'Entity ID', type: 'text', required: true },
{ key: 'ssoUrl', label: 'SSO URL', type: 'url', required: true },
{ key: 'sloUrl', label: 'SLO URL', type: 'url' },
{ key: 'certificate', label: 'X.509 Certificate', type: 'textarea', required: true },
{ key: 'signRequests', label: 'Sign Requests', type: 'boolean', default: true },
{ key: 'wantAssertionsSigned', label: 'Want Assertions Signed', type: 'boolean', default: true }
]
},
oidc: {
name: 'OpenID Connect',
icon: 'mdi:openid',
fields: [
{ key: 'issuerUrl', label: 'Issuer URL', type: 'url', required: true },
{ key: 'clientId', label: 'Client ID', type: 'text', required: true },
{ key: 'clientSecret', label: 'Client Secret', type: 'password', required: true },
{ key: 'scope', label: 'Scope', type: 'text', default: 'openid profile email' },
{ key: 'responseType', label: 'Response Type', type: 'select', options: ['code', 'id_token', 'token'], default: 'code' }
]
}
};
// Configuration form data
const config = ref({});
const showConfigDialog = ref(false);
const showMappingDialog = ref(false);
const showTestDialog = ref(false);
// Role mapping configuration
const roleMappings = ref([
{
externalRole: 'Domain Admins',
internalRole: 'superadmin',
permissions: ['read', 'write', 'delete', 'admin']
},
{
externalRole: 'Document Managers',
internalRole: 'admin',
permissions: ['read', 'write', 'delete']
},
{
externalRole: 'All Users',
internalRole: 'user',
permissions: ['read']
}
]);
// Group mapping configuration
const groupMappings = ref([
{
externalGroup: 'CN=Finance,OU=Departments,DC=company,DC=com',
internalDepartment: 'Finance',
defaultRole: 'user',
permissions: ['finance_access']
},
{
externalGroup: 'CN=HR,OU=Departments,DC=company,DC=com',
internalDepartment: 'HR',
defaultRole: 'user',
permissions: ['hr_access']
}
]);
// Computed properties
const currentProvider = computed(() => {
return providerConfigs[props.provider] || providerConfigs.ldap;
});
const syncStatus = computed(() => {
if (isSyncing.value) return { text: 'Syncing...', color: 'yellow' };
if (syncErrors.value.length > 0) return { text: 'Sync Errors', color: 'red' };
if (lastSyncTime.value) return { text: 'Up to date', color: 'green' };
return { text: 'Never synced', color: 'gray' };
});
// Methods
const connectToProvider = async () => {
isConnecting.value = true;
connectionStatus.value = 'connecting';
try {
// Simulate connection to external provider
await new Promise(resolve => setTimeout(resolve, 2000));
// Validate configuration
const validation = validateConfiguration();
if (!validation.isValid) {
throw new Error(`Configuration error: ${validation.errors.join(', ')}`);
}
// Test connection
const connectionTest = await testConnection();
if (!connectionTest.success) {
throw new Error(`Connection failed: ${connectionTest.error}`);
}
isConnected.value = true;
connectionStatus.value = 'connected';
// Auto-sync if enabled
if (props.autoSync) {
await syncUsers();
}
} catch (error) {
connectionStatus.value = 'error';
emit('error', { type: 'connection', message: error.message });
} finally {
isConnecting.value = false;
}
};
const disconnectFromProvider = () => {
isConnected.value = false;
connectionStatus.value = 'disconnected';
externalUsers.value = [];
externalRoles.value = [];
externalGroups.value = [];
};
const testConnection = async () => {
try {
// Simulate connection test based on provider type
switch (props.provider) {
case 'ldap':
return await testLDAPConnection();
case 'oauth2':
return await testOAuth2Connection();
case 'saml':
return await testSAMLConnection();
case 'oidc':
return await testOIDCConnection();
default:
return { success: false, error: 'Unknown provider' };
}
} catch (error) {
return { success: false, error: error.message };
}
};
const testLDAPConnection = async () => {
// Simulate LDAP connection test
await new Promise(resolve => setTimeout(resolve, 1500));
// Mock validation
if (!config.value.server || !config.value.baseDN) {
return { success: false, error: 'Server and Base DN are required' };
}
return {
success: true,
data: {
server: config.value.server,
users: 150,
groups: 25
}
};
};
const testOAuth2Connection = async () => {
// Simulate OAuth2 connection test
await new Promise(resolve => setTimeout(resolve, 1000));
if (!config.value.clientId || !config.value.clientSecret) {
return { success: false, error: 'Client ID and Secret are required' };
}
return {
success: true,
data: {
provider: 'OAuth2',
scope: config.value.scope,
authUrl: config.value.authorizationUrl
}
};
};
const testSAMLConnection = async () => {
// Simulate SAML connection test
await new Promise(resolve => setTimeout(resolve, 1200));
if (!config.value.entityId || !config.value.ssoUrl) {
return { success: false, error: 'Entity ID and SSO URL are required' };
}
return {
success: true,
data: {
entityId: config.value.entityId,
ssoUrl: config.value.ssoUrl
}
};
};
const testOIDCConnection = async () => {
// Simulate OIDC connection test
await new Promise(resolve => setTimeout(resolve, 1100));
if (!config.value.issuerUrl || !config.value.clientId) {
return { success: false, error: 'Issuer URL and Client ID are required' };
}
return {
success: true,
data: {
issuer: config.value.issuerUrl,
clientId: config.value.clientId
}
};
};
const validateConfiguration = () => {
const errors = [];
const requiredFields = currentProvider.value.fields.filter(field => field.required);
for (const field of requiredFields) {
if (!config.value[field.key]) {
errors.push(`${field.label} is required`);
}
}
return {
isValid: errors.length === 0,
errors
};
};
const syncUsers = async () => {
if (!isConnected.value) return;
isSyncing.value = true;
syncErrors.value = [];
try {
// Simulate user sync
await new Promise(resolve => setTimeout(resolve, 3000));
// Mock external users data
externalUsers.value = [
{
id: 'ext_user_1',
username: 'aiman.fakhrullah',
email: 'aiman@company.com',
fullName: 'Aiman Fakhrullah',
groups: ['Finance', 'All Users'],
roles: ['Document Managers'],
lastLogin: '2023-12-20T10:30:00Z',
status: 'active'
},
{
id: 'ext_user_2',
username: 'sarah.ahmed',
email: 'sarah@company.com',
fullName: 'Sarah Ahmed',
groups: ['HR', 'All Users'],
roles: ['All Users'],
lastLogin: '2023-12-19T15:45:00Z',
status: 'active'
}
];
// Mock external roles
externalRoles.value = [
{ name: 'Domain Admins', members: 5 },
{ name: 'Document Managers', members: 12 },
{ name: 'All Users', members: 150 }
];
// Mock external groups
externalGroups.value = [
{ name: 'Finance', dn: 'CN=Finance,OU=Departments,DC=company,DC=com', members: 25 },
{ name: 'HR', dn: 'CN=HR,OU=Departments,DC=company,DC=com', members: 18 },
{ name: 'IT', dn: 'CN=IT,OU=Departments,DC=company,DC=com', members: 15 }
];
// Apply role mappings
mapPermissions();
lastSyncTime.value = new Date().toISOString();
emit('sync-complete', {
users: externalUsers.value.length,
roles: externalRoles.value.length,
groups: externalGroups.value.length
});
} catch (error) {
syncErrors.value.push(error.message);
emit('error', { type: 'sync', message: error.message });
} finally {
isSyncing.value = false;
}
};
const mapPermissions = () => {
mappedPermissions.value = [];
// Map users to internal roles based on external roles/groups
for (const user of externalUsers.value) {
for (const roleMapping of roleMappings.value) {
if (user.roles.includes(roleMapping.externalRole)) {
mappedPermissions.value.push({
userId: user.id,
username: user.username,
externalRole: roleMapping.externalRole,
internalRole: roleMapping.internalRole,
permissions: roleMapping.permissions,
source: 'role_mapping'
});
}
}
for (const groupMapping of groupMappings.value) {
if (user.groups.some(group => group === groupMapping.externalGroup.split(',')[0].replace('CN=', ''))) {
mappedPermissions.value.push({
userId: user.id,
username: user.username,
externalGroup: groupMapping.externalGroup,
internalRole: groupMapping.defaultRole,
department: groupMapping.internalDepartment,
permissions: groupMapping.permissions,
source: 'group_mapping'
});
}
}
}
emit('permission-updated', mappedPermissions.value);
};
const addRoleMapping = () => {
roleMappings.value.push({
externalRole: '',
internalRole: 'user',
permissions: ['read']
});
};
const removeRoleMapping = (index) => {
roleMappings.value.splice(index, 1);
};
const addGroupMapping = () => {
groupMappings.value.push({
externalGroup: '',
internalDepartment: '',
defaultRole: 'user',
permissions: []
});
};
const removeGroupMapping = (index) => {
groupMappings.value.splice(index, 1);
};
const saveConfiguration = () => {
// Save configuration to backend
console.log('Saving configuration:', config.value);
showConfigDialog.value = false;
};
const saveMappings = () => {
// Save mappings to backend
console.log('Saving mappings:', { roleMappings: roleMappings.value, groupMappings: groupMappings.value });
showMappingDialog.value = false;
// Re-map permissions with new mappings
if (isConnected.value) {
mapPermissions();
}
};
const getConnectionStatusColor = () => {
switch (connectionStatus.value) {
case 'connected': return 'green';
case 'connecting': return 'yellow';
case 'error': return 'red';
default: return 'gray';
}
};
const formatDate = (dateString) => {
return new Date(dateString).toLocaleString();
};
// Initialize configuration
onMounted(() => {
// Initialize config with default values
config.value = {};
for (const field of currentProvider.value.fields) {
if (field.default !== undefined) {
config.value[field.key] = field.default;
}
}
});
</script>
<template>
<div class="external-rbac-integration">
<!-- Header -->
<div class="flex items-center justify-between mb-6">
<div class="flex items-center space-x-3">
<Icon :name="currentProvider.icon" class="w-8 h-8 text-blue-600" />
<div>
<h2 class="text-2xl font-bold text-gray-900 dark:text-gray-100">
{{ currentProvider.name }} Integration
</h2>
<p class="text-sm text-gray-500 dark:text-gray-400">
External authentication and role-based access control
</p>
</div>
</div>
<div class="flex items-center space-x-2">
<span
class="px-3 py-1 rounded-full text-sm font-medium"
:class="`bg-${getConnectionStatusColor()}-100 text-${getConnectionStatusColor()}-800 dark:bg-${getConnectionStatusColor()}-900/20 dark:text-${getConnectionStatusColor()}-200`"
>
{{ connectionStatus.charAt(0).toUpperCase() + connectionStatus.slice(1) }}
</span>
<button
v-if="!isConnected"
@click="connectToProvider"
:disabled="isConnecting"
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50"
>
<span v-if="isConnecting">Connecting...</span>
<span v-else>Connect</span>
</button>
<button
v-else
@click="disconnectFromProvider"
class="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700"
>
Disconnect
</button>
</div>
</div>
<!-- Status Cards -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
<div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-600 p-6">
<div class="flex items-center">
<Icon name="mdi:account-group" class="w-8 h-8 text-blue-600 mr-3" />
<div>
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">External Users</p>
<p class="text-2xl font-bold text-gray-900 dark:text-gray-100">{{ externalUsers.length }}</p>
</div>
</div>
</div>
<div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-600 p-6">
<div class="flex items-center">
<Icon name="mdi:shield-account" class="w-8 h-8 text-green-600 mr-3" />
<div>
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">External Roles</p>
<p class="text-2xl font-bold text-gray-900 dark:text-gray-100">{{ externalRoles.length }}</p>
</div>
</div>
</div>
<div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-600 p-6">
<div class="flex items-center">
<Icon name="mdi:folder-account" class="w-8 h-8 text-purple-600 mr-3" />
<div>
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">External Groups</p>
<p class="text-2xl font-bold text-gray-900 dark:text-gray-100">{{ externalGroups.length }}</p>
</div>
</div>
</div>
<div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-600 p-6">
<div class="flex items-center">
<Icon name="mdi:sync" class="w-8 h-8 mr-3" :class="`text-${syncStatus.color}-600`" />
<div>
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Sync Status</p>
<p class="text-sm font-bold" :class="`text-${syncStatus.color}-600`">{{ syncStatus.text }}</p>
<p v-if="lastSyncTime" class="text-xs text-gray-500 dark:text-gray-400">
{{ formatDate(lastSyncTime) }}
</p>
</div>
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="flex flex-wrap gap-3 mb-8">
<button
@click="showConfigDialog = true"
class="px-4 py-2 bg-gray-600 text-white rounded hover:bg-gray-700"
>
<Icon name="mdi:cog" class="w-4 h-4 mr-2 inline" />
Configure
</button>
<button
@click="showMappingDialog = true"
class="px-4 py-2 bg-indigo-600 text-white rounded hover:bg-indigo-700"
>
<Icon name="mdi:map" class="w-4 h-4 mr-2 inline" />
Role Mappings
</button>
<button
@click="showTestDialog = true"
class="px-4 py-2 bg-orange-600 text-white rounded hover:bg-orange-700"
>
<Icon name="mdi:test-tube" class="w-4 h-4 mr-2 inline" />
Test Connection
</button>
<button
@click="syncUsers"
:disabled="!isConnected || isSyncing"
class="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700 disabled:opacity-50"
>
<Icon name="mdi:sync" class="w-4 h-4 mr-2 inline" />
<span v-if="isSyncing">Syncing...</span>
<span v-else>Sync Now</span>
</button>
</div>
<!-- External Users Table -->
<div v-if="externalUsers.length > 0" class="mb-8">
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">External Users</h3>
<div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-600 overflow-hidden">
<div class="overflow-x-auto">
<table class="w-full divide-y divide-gray-200 dark:divide-gray-600">
<thead class="bg-gray-50 dark:bg-gray-700">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
User
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Groups
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Roles
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Last Login
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Status
</th>
</tr>
</thead>
<tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-600">
<tr v-for="user in externalUsers" :key="user.id" class="hover:bg-gray-50 dark:hover:bg-gray-700">
<td class="px-6 py-4 whitespace-nowrap">
<div>
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">{{ user.fullName }}</div>
<div class="text-sm text-gray-500 dark:text-gray-400">{{ user.email }}</div>
</div>
</td>
<td class="px-6 py-4">
<div class="flex flex-wrap gap-1">
<span
v-for="group in user.groups"
:key="group"
class="px-2 py-1 text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900/20 dark:text-blue-200 rounded-full"
>
{{ group }}
</span>
</div>
</td>
<td class="px-6 py-4">
<div class="flex flex-wrap gap-1">
<span
v-for="role in user.roles"
:key="role"
class="px-2 py-1 text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-200 rounded-full"
>
{{ role }}
</span>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
{{ formatDate(user.lastLogin) }}
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span
class="px-2 py-1 text-xs font-medium rounded-full"
:class="
user.status === 'active'
? 'bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-200'
: 'bg-red-100 text-red-800 dark:bg-red-900/20 dark:text-red-200'
"
>
{{ user.status }}
</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Mapped Permissions -->
<div v-if="mappedPermissions.length > 0" class="mb-8">
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">Mapped Permissions</h3>
<div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-600 overflow-hidden">
<div class="overflow-x-auto">
<table class="w-full divide-y divide-gray-200 dark:divide-gray-600">
<thead class="bg-gray-50 dark:bg-gray-700">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
User
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Source
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Internal Role
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Permissions
</th>
</tr>
</thead>
<tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-600">
<tr v-for="permission in mappedPermissions" :key="`${permission.userId}-${permission.source}`" class="hover:bg-gray-50 dark:hover:bg-gray-700">
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-gray-100">
{{ permission.username }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
{{ permission.externalRole || permission.externalGroup || 'Unknown' }}
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="px-2 py-1 text-xs font-medium bg-purple-100 text-purple-800 dark:bg-purple-900/20 dark:text-purple-200 rounded-full">
{{ permission.internalRole }}
</span>
</td>
<td class="px-6 py-4">
<div class="flex flex-wrap gap-1">
<span
v-for="perm in permission.permissions"
:key="perm"
class="px-2 py-1 text-xs font-medium bg-yellow-100 text-yellow-800 dark:bg-yellow-900/20 dark:text-yellow-200 rounded-full"
>
{{ perm }}
</span>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Configuration Dialog -->
<rs-modal :visible="showConfigDialog" @close="showConfigDialog = false" size="2xl">
<template #header>
<h3 class="text-lg font-semibold">{{ currentProvider.name }} Configuration</h3>
</template>
<template #body>
<div class="space-y-4">
<div
v-for="field in currentProvider.fields"
:key="field.key"
class="grid grid-cols-1 gap-2"
>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ field.label }}
<span v-if="field.required" class="text-red-500">*</span>
</label>
<input
v-if="field.type === 'text' || field.type === 'url'"
v-model="config[field.key]"
:type="field.type"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700"
/>
<input
v-else-if="field.type === 'number'"
v-model="config[field.key]"
type="number"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700"
/>
<input
v-else-if="field.type === 'password'"
v-model="config[field.key]"
type="password"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700"
/>
<textarea
v-else-if="field.type === 'textarea'"
v-model="config[field.key]"
rows="4"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700"
></textarea>
<select
v-else-if="field.type === 'select'"
v-model="config[field.key]"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700"
>
<option v-for="option in field.options" :key="option" :value="option">
{{ option }}
</option>
</select>
<label
v-else-if="field.type === 'boolean'"
class="flex items-center"
>
<input
v-model="config[field.key]"
type="checkbox"
class="mr-2"
/>
{{ field.label }}
</label>
</div>
</div>
</template>
<template #footer>
<div class="flex justify-end space-x-2">
<button
@click="showConfigDialog = false"
class="px-4 py-2 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-700"
>
Cancel
</button>
<button
@click="saveConfiguration"
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
Save Configuration
</button>
</div>
</template>
</rs-modal>
<!-- Role Mapping Dialog -->
<rs-modal :visible="showMappingDialog" @close="showMappingDialog = false" size="4xl">
<template #header>
<h3 class="text-lg font-semibold">Role & Group Mappings</h3>
</template>
<template #body>
<div class="space-y-8">
<!-- Role Mappings -->
<div>
<div class="flex items-center justify-between mb-4">
<h4 class="text-md font-medium text-gray-900 dark:text-gray-100">Role Mappings</h4>
<button
@click="addRoleMapping"
class="px-3 py-1 bg-blue-600 text-white rounded hover:bg-blue-700 text-sm"
>
Add Mapping
</button>
</div>
<div class="space-y-3">
<div
v-for="(mapping, index) in roleMappings"
:key="index"
class="grid grid-cols-1 md:grid-cols-4 gap-4 p-4 border border-gray-200 dark:border-gray-600 rounded-lg"
>
<div>
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
External Role
</label>
<input
v-model="mapping.externalRole"
type="text"
class="w-full px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700"
/>
</div>
<div>
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
Internal Role
</label>
<select
v-model="mapping.internalRole"
class="w-full px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700"
>
<option value="superadmin">Super Admin</option>
<option value="admin">Admin</option>
<option value="user">User</option>
</select>
</div>
<div>
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
Permissions
</label>
<input
v-model="mapping.permissions"
type="text"
placeholder="read, write, delete"
class="w-full px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700"
/>
</div>
<div class="flex items-end">
<button
@click="removeRoleMapping(index)"
class="px-2 py-1 text-red-600 hover:text-red-800 text-sm"
>
Remove
</button>
</div>
</div>
</div>
</div>
<!-- Group Mappings -->
<div>
<div class="flex items-center justify-between mb-4">
<h4 class="text-md font-medium text-gray-900 dark:text-gray-100">Group Mappings</h4>
<button
@click="addGroupMapping"
class="px-3 py-1 bg-green-600 text-white rounded hover:bg-green-700 text-sm"
>
Add Mapping
</button>
</div>
<div class="space-y-3">
<div
v-for="(mapping, index) in groupMappings"
:key="index"
class="grid grid-cols-1 md:grid-cols-5 gap-4 p-4 border border-gray-200 dark:border-gray-600 rounded-lg"
>
<div>
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
External Group DN
</label>
<input
v-model="mapping.externalGroup"
type="text"
class="w-full px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700"
/>
</div>
<div>
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
Department
</label>
<input
v-model="mapping.internalDepartment"
type="text"
class="w-full px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700"
/>
</div>
<div>
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
Default Role
</label>
<select
v-model="mapping.defaultRole"
class="w-full px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700"
>
<option value="superadmin">Super Admin</option>
<option value="admin">Admin</option>
<option value="user">User</option>
</select>
</div>
<div>
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
Permissions
</label>
<input
v-model="mapping.permissions"
type="text"
placeholder="department_access"
class="w-full px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700"
/>
</div>
<div class="flex items-end">
<button
@click="removeGroupMapping(index)"
class="px-2 py-1 text-red-600 hover:text-red-800 text-sm"
>
Remove
</button>
</div>
</div>
</div>
</div>
</div>
</template>
<template #footer>
<div class="flex justify-end space-x-2">
<button
@click="showMappingDialog = false"
class="px-4 py-2 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-700"
>
Cancel
</button>
<button
@click="saveMappings"
class="px-4 py-2 bg-indigo-600 text-white rounded hover:bg-indigo-700"
>
Save Mappings
</button>
</div>
</template>
</rs-modal>
</div>
</template>
<style scoped>
.external-rbac-integration table {
min-width: 600px;
}
</style>