@ -322,8 +322,7 @@
< v-btn icon size = "sm" variant = "outline" :click = "() => openDetailModal(r)" :title = "$t('common.detail')" >
< i class = "fas fa-eye" > < / i >
< / v-btn >
< v-btn icon size = "sm" variant = "outline" :click = "() => openEditModal(r)" :title = "$t('common.edit')"
:disabled="r.is_system">
< v-btn icon size = "sm" variant = "outline" :click = "() => openEditModal(r)" :title = "$t('common.edit')" >
< i class = "fas fa-edit" > < / i >
< / v-btn >
< v-btn icon size = "sm" color = "danger" variant = "outline" :click = "() => deleteRole(r)"
@ -372,10 +371,33 @@
< span style = "color: var(--text-color-secondary); font-size: var(--font-size-sm);" >
{{ $t('role.permissions_desc') }}
< / span >
< v-btn size = "sm" color = "primary" :click = "openPermissionSelector" :disabled = "currentRole?.is_system ">
< v-btn size = "sm" color = "primary" @ click = "showPermForm = true ">
< i class = "fas fa-plus" > < / i > {{ $t('role.add_permission') }}
< / v-btn >
< / div >
<!-- Add Permission Inline Form -->
< div v-if = "showPermForm"
style="display: flex; gap: 8px; align-items: flex-end; padding: 12px; background: var(--bg-color-tertiary); border-radius: var(--radius-md); flex-wrap: wrap;">
< div style = "display: flex; flex-direction: column; gap: 4px; flex: 1; min-width: 120px;" >
< label style = "font-size: 12px; color: var(--text-color-secondary);" > Scope< / label >
< v-input type = "text" v:value = "newPerm.scope" placeholder = "vb" style = "margin: 0;" / >
< / div >
< div style = "display: flex; flex-direction: column; gap: 4px; flex: 2; min-width: 180px;" >
< label style = "font-size: 12px; color: var(--text-color-secondary);" > Permission ID< / label >
< v-input type = "text" v:value = "newPerm.permission_id" placeholder = "resource:instance:*" style = "margin: 0;" / >
< / div >
< div style = "display: flex; flex-direction: column; gap: 4px; width: 100px;" >
< label style = "font-size: 12px; color: var(--text-color-secondary);" > Level< / label >
< v-input v:value = "newPerm.level" type = 'select' :opts = "{options: permission_levels}" style = "margin: 0;" >
< / v-input >
< / div >
< v-btn size = "sm" color = "primary" :click = "addPermission" :disabled = "permAdding" >
{{ permAdding ? '...' : $t('common.save') }}
< / v-btn >
< v-btn size = "sm" variant = "outline" @ click = "showPermForm = false" > {{ $t('common.cancel') }}< / v-btn >
< / div >
< div class = "permission-list" >
< div v-for = "p in rolePermissions" class = "permission-item" >
< div class = "permission-info" >
@ -385,12 +407,12 @@
< div class = "permission-level" >
< span :class = "'level-badge level-' + p.level" > {{ formatLevel(p.level) }}< / span >
< / div >
< v-btn v-if= "!currentRole?.is_system" icon size = "xs" color = "danger" variant = "ghost"
:click="() => removePermission(p)" : title="$t('common.remove')">
< v-btn icon size = "xs" color = "danger" variant = "ghost" :click = "() => removePermission(p)"
:title="$t('common.remove')">
< i class = "fas fa-times" > < / i >
< / v-btn >
< / div >
< div v-if = "rolePermissions.length === 0 " class = "empty-state" >
< div v-if = "rolePermissions.length === 0 && !showPermForm " class = "empty-state" >
< i class = "fas fa-shield-alt" style = "font-size: 48px; color: var(--text-color-disabled);" > < / i >
< p > {{ $t('role.no_permissions') }}< / p >
< / div >
@ -404,7 +426,7 @@
< i class = "fas fa-search" style = "color: var(--text-color-tertiary);" > < / i >
< input type = "text" v:value = "userSearchQuery" :placeholder = "$t('role.search_users')" >
< / div >
< v-btn size = "sm" color = "primary" :click = "openUserSelector" :disabled = "currentRole?.is_system" >
< v-btn size = "sm" color = "primary" :click = "openUserSelector" >
< i class = "fas fa-plus" > < / i > {{ $t('role.add_user') }}
< / v-btn >
< / div >
@ -432,32 +454,6 @@
< / div >
< / v-dialog >
<!-- Permission Selector Dialog -->
< v-dialog v:visible = "showPermissionSelector" width = "600px" :title = "$t('role.select_permissions')" >
< div style = "display: flex; flex-direction: column; gap: 16px;" >
< div class = "search-box" >
< i class = "fas fa-search" style = "color: var(--text-color-tertiary);" > < / i >
< input type = "text" v:value = "permissionSearchQuery" :placeholder = "$t('role.search_permissions')" >
< / div >
< div class = "available-permission-list" >
< div v-for = "p in filteredAvailablePermissions" class = "available-permission-item" >
< input type = "checkbox" :checked = "isPermissionSelected(p.id)" @ change = "togglePermission(p.id)" >
< div class = "permission-info" >
< div class = "permission-id" > {{ p.permission_id }}< / div >
< div class = "permission-scope" > {{ p.scope }}< / div >
< / div >
< / div >
< div v-if = "filteredAvailablePermissions.length === 0" class = "empty-state" >
< p > {{ $t('role.no_available_permissions') }}< / p >
< / div >
< / div >
< / div >
< div vslot = "footer" >
< v-btn variant = "outline" :click = "closePermissionSelector" > {{ $t('common.cancel') }}< / v-btn >
< v-btn color = "primary" :click = "saveRolePermissions" > {{ $t('common.save') }}< / v-btn >
< / div >
< / v-dialog >
<!-- User Selector Dialog -->
< v-dialog v:visible = "showUserSelector" width = "600px" :title = "$t('role.select_users')" >
< div style = "display: flex; flex-direction: column; gap: 16px;" >
@ -498,6 +494,13 @@
name: "",
description: ""
};
permission_levels = [
{value: 1, label: $t('permission.level.create') || 'Create'},
{value: 2, label: $t('permission.level.read') || 'Read'},
{value: 4, label: $t('permission.level.write') || 'Write'},
{value: 6, label: $t('permission.level.rw') || 'Read+Write'},
{value: 7, label: $t('permission.level.admin') || 'Admin'},
]
// Detail modal data
showDetailModal = false;
@ -506,10 +509,9 @@
// Permissions data
rolePermissions = [];
showPermissionSelector = false;
permissionSearchQuery = "";
availablePermissions = [];
selectedPermissionIds = [];
showPermForm = false;
permAdding = false;
newPerm = {scope: 'vb', permission_id: '', level: 7};
// Users data
roleUsers = [];
@ -635,94 +637,27 @@
const err = await res.json();
throw new Error(err.message);
}
rolePermissions = res || [];
rolePermissions = await res.json() || [];
} catch (e) {
$message.error(e.message);
}
};
openPermissionSelector = async () => {
selectedPermissionIds = rolePermissions.map(p => p.id);
showPermissionSelector = true;
await loadAvailablePermissions();
};
closePermissionSelector = () => {
showPermissionSelector = false;
permissionSearchQuery = "";
selectedPermissionIds = [];
};
loadAvailablePermissions = async () => {
// Get all permissions from user's own permissions or a dedicated endpoint
// For now, we'll use a mock approach - in real implementation,
// you might need a /api/permissions endpoint to list all available permissions
try {
// This is a placeholder - adjust based on your actual API
const res = await fetch('/api/auth/me');
if (res.status !== 200) {
const err = await res.json();
throw new Error(err.message);
}
const me = await res.json();
if (me.permissions) {
// Extract unique permission IDs
const uniquePerms = [];
const seen = new Set();
me.permissions.forEach(p => {
if (!seen.has(p.permission_id)) {
seen.add(p.permission_id);
uniquePerms.push({
id: p.permission_id,
permission_id: p.permission_id,
scope: p.scope,
level: p.level
});
}
});
availablePermissions = uniquePerms;
}
} catch (e) {
// Fallback: use current role permissions as base
availablePermissions = rolePermissions.map(p => ({
id: p.id,
permission_id: p.permission_id,
scope: p.scope,
level: p.level
}));
}
};
filteredAvailablePermissions = () => {
if (!permissionSearchQuery) return availablePermissions;
const query = permissionSearchQuery.toLowerCase();
return availablePermissions.filter(p =>
p.permission_id.toLowerCase().includes(query) ||
p.scope.toLowerCase().includes(query)
);
};
isPermissionSelected = (id) => {
return selectedPermissionIds.includes(id);
};
togglePermission = (id) => {
const idx = selectedPermissionIds.indexOf(id);
if (idx > -1) {
selectedPermissionIds.splice(idx, 1);
} else {
selectedPermissionIds.push(id);
addPermission = async () => {
if (!currentRole || !newPerm.permission_id || !newPerm.scope) {
$message.error('Scope and Permission ID are required');
return;
}
// Trigger reactivity
selectedPermissionIds = [...selectedPermissionIds];
};
saveRolePermissions = async () => {
if (!currentRole) return;
permAdding = true;
try {
const res = await fetch(`/api/roles/${currentRole.id}/permissions`, {
method: 'PUT', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({
permission_ids: selectedPermissionIds
method: 'PUT', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
permissions: [{
scope: newPerm.scope,
permission_id: newPerm.permission_id,
level: parseInt(newPerm.level)
}]
})
});
if (res.status !== 200) {
@ -730,23 +665,24 @@
throw new Error(err.message);
}
$message.success($t('role.permissions_updated'));
closePermissionSelector();
showPermForm = false;
newPerm = {scope: 'vb', permission_id: '', level: 7};
loadRolePermissions();
} catch (e) {
$message.error(e.message);
} finally {
permAdding = false;
}
};
removePermission = async (p) => {
if (!currentRole || currentRole.is_system ) return;
if (!currentRole) return;
try {
await $message.confirm($t('role.remove_permission_confirm'));
const newPermissions = rolePermissions
.filter(rp => rp.id !== p.id)
.map(rp => rp.id);
const res = await fetch(`/api/roles/${currentRole.id}/permissions`, {
method: 'PUT', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({
permission_ids: newPermissions
method: 'PUT', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
remove: [p.id]
})
});
if (res.status !== 200) {
@ -761,48 +697,21 @@
};
formatLevel = (level) => {
const levels = {
1: $t('permission.level.create') || 'Create',
2: $t('permission.level.read') || 'Read',
4: $t('permission.level.write') || 'Write',
6: $t('permission.level.rw') || 'Read+Write',
7: $t('permission.level.admin') || 'Admin'
};
return levels[level] || `Level ${level}`;
const item = permission_levels.find(l => l.value === level);
return item ? item.label : 'Level ' + level;
};
// User functions
loadRoleUsers = async () => {
if (!currentRole) return;
try {
// Get users who have this role
// We need to fetch users and filter by role
const params = new URLSearchParams({page_size: 1000});
let res = await fetch('/api/users?' + params);
const res = await fetch(`/api/roles/${currentRole.id}/users?page=1&page_size=100`);
if (res.status !== 200) {
const err = await res.json();
throw new Error(err.message);
}
const data = await res.json();
const allUsers = data.items || [];
// For each user, check if they have this role
const usersWithRole = [];
for (const user of allUsers) {
try {
res = await fetch(`/api/users/${user.id}/roles`);
if (res.status !== 200) {
const err = await res.json();
throw new Error(err.message);
}
const userRoles = await res.json();
if (userRoles.some(ur => ur.id === currentRole.id)) {
usersWithRole.push(user);
}
} catch (e) {
// Skip users we can't check
}
}
roleUsers = usersWithRole;
roleUsers = data.items || [];
} catch (e) {
$message.error(e.message);
}
@ -835,8 +744,8 @@
const params = new URLSearchParams({keyword: availableUserSearchQuery, limit: 50});
const res = await fetch('/api/auth/users?' + params);
const json_ = await res.json();
if (json_.code !== 200) throw new Error(json_.message);
availableUsers = json_.data ?.items || [];
if (res.status !== 200) throw new Error(json_.message);
availableUsers = json_?.items || [];
} catch (e) {
$message.error(e.message);
}
@ -860,45 +769,15 @@
saveRoleUsers = async () => {
if (!currentRole) return;
try {
// Update each user's roles
const currentUserIds = roleUsers.map(u => u.id);
// Users to add
const toAdd = selectedUserIds.filter(id => !currentUserIds.includes(id));
// Users to remove
const toRemove = currentUserIds.filter(id => !selectedUserIds.includes(id));
let res = null
for (const userId of toAdd) {
res = await fetch(`/api/users/${userId}/roles`);
if (res.status !== 200) {
const err = await res.json();
throw new Error(err.message);
}
const userRoles = await res.json();
const roleIds = [...userRoles.map(r => r.id), currentRole.id];
res = await fetch(`/api/users/${userId}/roles`, {method: 'PUT', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({role_ids: roleIds})});
if (res.status !== 200) {
const err = await res.json();
throw new Error(err.message);
}
}
for (const userId of toRemove) {
res = await fetch(`/api/users/${userId}/roles`);
if (res.status !== 200) {
const err = await res.json();
throw new Error(err.message);
}
const userRoles = await res.json();
const roleIds = userRoles.filter(r => r.id !== currentRole.id).map(r => r.id);
res = await fetch(`/api/users/${userId}/roles`, {method: 'PUT', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({role_ids: roleIds})});
const res = await fetch(`/api/roles/${currentRole.id}/users`, {
method: 'PUT',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({user_ids: selectedUserIds})
});
if (res.status !== 200) {
const err = await res.json();
throw new Error(err.message);
}
}
$message.success($t('role.users_updated'));
closeUserSelector();
loadRoleUsers();
@ -911,14 +790,12 @@
if (!currentRole || currentRole.is_system) return;
try {
await $message.confirm($t('role.remove_user_confirm'));
let res = await fetch(`/api/users/${u.id}/roles`);
if (res.status !== 200) {
const err = await res.json();
throw new Error(err.message);
}
const userRoles = await res.json();
const roleIds = userRoles.filter(r => r.id !== currentRole.id).map(r => r.id);
res = await fetch(`/api/users/${u.id}/roles`, {method: 'PUT', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({role_ids: roleIds})});
const newIds = roleUsers.filter(ru => ru.id !== u.id).map(ru => ru.id);
const res = await fetch(`/api/roles/${currentRole.id}/users`, {
method: 'PUT',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({user_ids: newIds})
});
if (res.status !== 200) {
const err = await res.json();
throw new Error(err.message);