Added ability to have different user roles

This commit is contained in:
Pinga 2025-02-04 14:31:17 +02:00
parent 9504733dbf
commit d07fa5481c
4 changed files with 157 additions and 22 deletions

View file

@ -104,7 +104,7 @@ class UsersController extends Controller
'password' => $password_hashed,
'username' => $username,
'verified' => $verified,
'roles_mask' => 6,
'roles_mask' => 4,
'status' => $status,
'registered' => \time()
]
@ -205,12 +205,21 @@ class UsersController extends Controller
$_SESSION['user_to_update'] = [$args];
$roles_new = [
'4' => ($user['roles_mask'] & 4) ? true : false, // Registrar
'8' => ($user['roles_mask'] & 8) ? true : false, // Accountant
'16' => ($user['roles_mask'] & 16) ? true : false, // Support
'32' => ($user['roles_mask'] & 32) ? true : false, // Auditor
'64' => ($user['roles_mask'] & 64) ? true : false, // Sales
];
return view($response,'admin/users/updateUser.twig', [
'user' => $user,
'currentUri' => $uri,
'registrars' => $registrars,
'user_asso' => $user_asso,
'registrar_name' => $registrar_name
'registrar_name' => $registrar_name,
'roles_new' => $roles_new
]);
} else {
// User does not exist, redirect to the users view
@ -240,6 +249,10 @@ class UsersController extends Controller
$password_confirmation = $data['password_confirmation'] ?? null;
$status = $data['status'] ?? null;
$verified = $data['verified'] ?? null;
$roles_mask = $data['roles_mask'] ?? 0;
$allowedRoles = [0, 2, 4, 8, 16, 32, 64];
$allowedRolesMask = array_sum($allowedRoles); // 124 (sum of allowed roles)
// Define validation rules
$validators = [
@ -249,6 +262,14 @@ class UsersController extends Controller
'verified' => v::in(['0', '1'])->setName('Verified'), // Ensure verified is checked as 0 or 1
];
// Add custom validation for roles_mask
$validators['roles_mask'] = v::oneOf(
v::intVal()->callback(function ($value) use ($allowedRolesMask) {
return ($value & ~$allowedRolesMask) === 0; // Ensure only allowed roles are included
}),
v::nullType() // Allow null as a valid value
)->setName('Roles Mask');
// Add password validation only if provided
if (!empty($password)) {
$validators['password'] = v::stringType()->notEmpty()->length(6, 255)->setName('Password');
@ -289,6 +310,11 @@ class UsersController extends Controller
return $response->withHeader('Location', '/user/update/'.$old_username)->withStatus(302);
}
if (in_array($roles_mask, [0, '0'], true)) {
$this->container->get('flash')->addMessage('error', 'No roles assigned. Please assign at least one role');
return $response->withHeader('Location', '/user/update/' . $old_username)->withStatus(302);
}
$db->beginTransaction();
try {
@ -301,6 +327,7 @@ class UsersController extends Controller
'username' => $username,
'verified' => $verified,
'status' => $status,
'roles_mask' => $roles_mask,
];
if (!empty($password)) {

View file

@ -93,7 +93,7 @@
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Role</label>
<label class="form-label">Type</label>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="role-toggle" name="role" value="admin">
<label class="form-check-label" for="role-toggle" id="role-label">Registrar</label>

View file

@ -94,25 +94,67 @@
<!-- Role -->
<div class="row">
<div class="col-md-12">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">{{ __('Role') }}</label>
<label class="form-label">{{ __('Type') }}</label>
<div class="form-control-plaintext">
{% if user.roles_mask == 0 %}
{{ __('Admin') }}
{% elseif user.roles_mask == 4 %}
{{ __('Registrar') }}
{% if user_asso is defined and user_asso is not null %}
{% else %}
{# Display Assigned Roles #}
{% set assigned_roles = [] %}
{% if roles_new['4'] %}
{% set assigned_roles = assigned_roles | merge([__('Registrar')]) %}
{% endif %}
{% if roles_new['8'] %}
{% set assigned_roles = assigned_roles | merge([__('Accountant')]) %}
{% endif %}
{% if roles_new['16'] %}
{% set assigned_roles = assigned_roles | merge([__('Support')]) %}
{% endif %}
{% if roles_new['32'] %}
{% set assigned_roles = assigned_roles | merge([__('Auditor')]) %}
{% endif %}
{% if roles_new['64'] %}
{% set assigned_roles = assigned_roles | merge([__('Sales')]) %}
{% endif %}
{{ assigned_roles | join(', ') }}
{# Show association if the user is a Registrar and associated with another registrar #}
{% if roles_new['4'] and user_asso is defined and user_asso is not null %}
- {{ __('Associated with Registrar') }} {{ registrar_name }}
{% endif %}
{% else %}
{{ __('Unknown Role') }}
{% endif %}
</div>
</div>
</div>
{% if user.roles_mask != 0 %}
{% if user.roles_mask == 4 %}
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">{{ __('Roles') }}</label>
<span class="badge bg-blue-lt">{{ __('Registrar Administrator') }}</span>
</div>
</div>
{% else %}
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">{{ __('Manage Roles') }}</label>
<select class="form-select" id="select-roles" name="roles_mask[]" multiple>
<option value="8" {{ roles_new['8'] ? 'selected' : '' }}>{{ __('Accountant') }}</option>
<option value="16" {{ roles_new['16'] ? 'selected' : '' }}>{{ __('Support') }}</option>
<option value="32" {{ roles_new['32'] ? 'selected' : '' }}>{{ __('Auditor') }}</option>
<option value="64" {{ roles_new['64'] ? 'selected' : '' }}>{{ __('Sales') }}</option>
</select>
</div>
</div>
{% endif %}
{% endif %}
</div>
</div>
<div class="card-footer text-end">
<div class="d-flex">
@ -126,4 +168,51 @@
</div>
{% include 'partials/footer.twig' %}
</div>
<script>
document.addEventListener("DOMContentLoaded", function () {
var el;
window.TomSelect && (new TomSelect(el = document.getElementById('select-roles'), {
copyClassesToDropdown: false,
dropdownParent: 'body',
controlInput: '<input>',
render:{
item: function(data,escape) {
if( data.customProperties ){
return '<div><span class="dropdown-item-indicator">' + data.customProperties + '</span>' + escape(data.text) + '</div>';
}
return '<div>' + escape(data.text) + '</div>';
},
option: function(data,escape){
if( data.customProperties ){
return '<div><span class="dropdown-item-indicator">' + data.customProperties + '</span>' + escape(data.text) + '</div>';
}
return '<div>' + escape(data.text) + '</div>';
},
},
}));
});
document.querySelector('form').addEventListener('submit', function (e) {
const select = document.getElementById('select-roles');
let rolesMask = 0;
// Loop through selected options and combine their values using bitwise OR
for (const option of select.selectedOptions) {
rolesMask |= parseInt(option.value);
}
// Create a hidden input to send the combined roles_mask to the server
const hiddenInput = document.createElement('input');
hiddenInput.type = 'hidden';
hiddenInput.name = 'roles_mask';
hiddenInput.value = rolesMask;
// Append the hidden input to the form
this.appendChild(hiddenInput);
// Optional: Disable the select to avoid sending duplicate data
select.disabled = true;
});
</script>
<script src="/assets/libs/tom-select/dist/js/tom-select.base.min.js" defer></script>
{% endblock %}

View file

@ -7,7 +7,7 @@
var table;
document.addEventListener("DOMContentLoaded", function(){
function userLinkFormatter(cell){
function emailLinkFormatter(cell){
var value = cell.getValue();
return `<span style="font-weight:bold;">${value}</a>`;
}
@ -28,14 +28,33 @@
function roleLabelFormatter(cell) {
var value = cell.getValue();
// Define role mappings with labels and styles
var roles = [
{ bit: 0, label: 'Administrator', class: 'status status-purple' },
{ bit: 4, label: 'Registrar', class: 'status status-indigo' },
{ bit: 8, label: 'Accountant', class: 'status status-green' },
{ bit: 16, label: 'Support', class: 'status status-azure' },
{ bit: 32, label: 'Auditor', class: 'status status-orange' },
{ bit: 64, label: 'Sales', class: 'status status-teal' }
];
// Special case for Administrator (no roles assigned)
if (value === 0) {
return '<span class="status status-purple">Administrator</span>';
} else if (value === 4) {
return '<span class="status status-indigo">Registrar</span>';
} else if (value === 6) {
return '<span class="status status-azure">Registrar Assistant</span>';
}
return value; // If the value is neither 0 nor 4, return it as is
// Check assigned roles using bitmask
var assignedRoles = roles
.filter(function (role) {
return role.bit !== 0 && (value & role.bit);
})
.map(function (role) {
return '<span class="' + role.class + '">' + role.label + '</span>';
});
// Join assigned roles with separators
return assignedRoles.length > 0 ? assignedRoles.join(', ') : '<span class="status status-secondary">Unknown Role</span>';
}
function verifiedFormatter(cell) {
@ -144,10 +163,10 @@
placeholder: "{{ __('No Data') }}",
columns:[
{formatter:"responsiveCollapse", width:30, minWidth:30, hozAlign:"center", resizable:false, headerSort:false, responsive:0},
{title:"{{ __('Name') }}", field:"username", width:200, resizable:false, headerSort:true, formatter: userLinkFormatter, responsive:0},
{title:"{{ __('Email') }}", field:"email", width:300, resizable:false, headerSort:true, responsive:2},
{title:"{{ __('Roles') }}", field:"roles_mask", width:200, resizable:false, headerSort:true, formatter: roleLabelFormatter, responsive:2},
{title:"{{ __('Verified') }}", field:"verified", width:200, resizable:false, headerSort:true, formatter: verifiedFormatter, responsive:2},
{title:"{{ __('Email') }}", field:"email", width:300, resizable:false, headerSort:true, formatter: emailLinkFormatter, responsive:0},
{title:"{{ __('User Name') }}", field:"username", width:200, resizable:false, headerSort:true, responsive:2},
{title:"{{ __('Roles') }}", field:"roles_mask", width:300, resizable:false, headerSort:true, formatter: roleLabelFormatter, responsive:2},
{title:"{{ __('Verified') }}", field:"verified", width:150, resizable:false, headerSort:true, formatter: verifiedFormatter, responsive:2},
{title:"{{ __('Status') }}", field:"status", width:200, resizable:false, headerSort:true, formatter: statusBadgeFormatter, responsive:2},
{title: "{{ __('Actions') }}", formatter: actionsFormatter, responsive:0, headerSort: false, download:false, hozAlign: "center", cellClick:function(e, cell){ e.stopPropagation(); }},
]