mirror of
https://github.com/itflow-org/itflow
synced 2026-11-11 23:17:23 +00:00
314 lines
14 KiB
PHP
314 lines
14 KiB
PHP
<?php
|
|
|
|
/*
|
|
* ITFlow - roles: what an agent's role lets them open.
|
|
*
|
|
* A role is an admin role - every part of ITFlow, every client, and
|
|
* Administration (users, roles, settings) - or a level for each module:
|
|
* none, read, modify (add, edit, archive and the everyday work) or full
|
|
* (delete as well). Which clients an agent sees is set on the agent
|
|
* (functions/user.php). The levels are kept in user_role_permissions, one
|
|
* row per role and module, and read once a request by lookupUserPermission()
|
|
* in functions/auth.php.
|
|
*
|
|
* Shared by Admin > Roles (admin/roles.php, admin/modals/role/*,
|
|
* admin/post/roles.php).
|
|
*/
|
|
|
|
/** The levels a role can give a module. */
|
|
const ROLE_LEVELS = [0 => 'None', 1 => 'Read', 2 => 'Modify', 3 => 'Full'];
|
|
|
|
/** A level's colour - None grey, Read blue, Modify amber, Full red. */
|
|
const ROLE_LEVEL_TONES = [0 => 'secondary', 1 => 'primary', 2 => 'warning', 3 => 'danger'];
|
|
|
|
/*
|
|
* The modules a role gives levels for, by module_name: label, icon, about
|
|
* (what is in it) and levels (what each level adds - Modify and Full take in
|
|
* the ones before). 'max' is the highest level that means anything.
|
|
*/
|
|
function roleModuleInfo() {
|
|
return [
|
|
'module_client' => [
|
|
'label' => 'Clients', 'icon' => 'fa-users', 'about' => 'Clients, contacts, locations and vendors',
|
|
'levels' => [1 => 'See them', 2 => 'Add, edit, import and archive them', 3 => 'Delete them, and anonymize a contact'],
|
|
],
|
|
'module_support' => [
|
|
'label' => 'Support', 'icon' => 'fa-life-ring', 'about' => 'Tickets, projects, assets and documentation',
|
|
'levels' => [1 => 'See them', 2 => 'Work tickets and projects, and add, edit and archive assets and documentation', 3 => 'Delete them'],
|
|
],
|
|
'module_credential' => [
|
|
'label' => 'Credentials', 'icon' => 'fa-key', 'about' => 'Client usernames, passwords and 2FA codes',
|
|
'levels' => [1 => 'See and copy them', 2 => 'Add, edit and share them', 3 => 'Delete them'],
|
|
],
|
|
'module_sales' => [
|
|
'label' => 'Sales', 'icon' => 'fa-file-invoice', 'about' => 'Quotes, invoices, recurring invoices and products',
|
|
'levels' => [1 => 'See them', 2 => 'Create, edit and send them - and record payments, with Financial at Modify too', 3 => 'Delete them, and edit or refund a payment'],
|
|
],
|
|
'module_financial' => [
|
|
'label' => 'Financial', 'icon' => 'fa-piggy-bank', 'about' => 'Payments, income, expenses, accounts, budgets and trips',
|
|
'levels' => [1 => 'See them', 2 => 'Add, edit and archive them, and reconcile accounts', 3 => 'Delete them, and undo a reconciliation'],
|
|
],
|
|
'module_reporting' => [
|
|
'label' => 'Reports', 'icon' => 'fa-chart-line', 'about' => 'The reports - each needs read access to what it reports on too',
|
|
'levels' => [1 => 'Open the reports'], 'max' => 1,
|
|
],
|
|
'module_ai' => [
|
|
'label' => 'AI', 'icon' => 'fa-robot', 'about' => 'Rewording, summaries and drafts, by the AI providers in Admin - each needs access to what it works on too',
|
|
'levels' => [1 => 'Use AI'], 'max' => 1,
|
|
],
|
|
];
|
|
}
|
|
|
|
/*
|
|
* The modules, in their order - module_id => module_name, label, icon,
|
|
* about, levels and max. One the list above doesn't know is named from its
|
|
* module_name and its description.
|
|
*/
|
|
function roleModules() {
|
|
global $mysqli;
|
|
static $modules = null;
|
|
if ($modules !== null) {
|
|
return $modules;
|
|
}
|
|
$info = roleModuleInfo();
|
|
$modules = [];
|
|
$sql = mysqli_query($mysqli, "SELECT module_id, module_name, module_description FROM modules ORDER BY module_id ASC");
|
|
while ($row = mysqli_fetch_assoc($sql)) {
|
|
$name = $row['module_name'];
|
|
$modules[intval($row['module_id'])] = ($info[$name] ?? [
|
|
'label' => ucfirst(str_replace('module_', '', $name)), 'icon' => 'fa-cube', 'about' => (string) $row['module_description'],
|
|
'levels' => [1 => 'See it', 2 => 'Add, edit and archive', 3 => 'Delete'],
|
|
]) + ['module_name' => $name, 'max' => 3];
|
|
}
|
|
return $modules;
|
|
}
|
|
|
|
/** Each role's levels - role_id => [module_id => level]. All roles when $role_ids is empty. */
|
|
function roleLevels(array $role_ids = []) {
|
|
global $mysqli;
|
|
$where = $role_ids ? 'WHERE user_role_id IN (' . implode(',', array_map('intval', $role_ids)) . ')' : '';
|
|
$levels = [];
|
|
$sql = mysqli_query($mysqli, "SELECT user_role_id, module_id, user_role_permission_level FROM user_role_permissions $where");
|
|
while ($row = mysqli_fetch_row($sql)) {
|
|
if (intval($row[2]) > 0) {
|
|
$levels[intval($row[0])][intval($row[1])] = min(3, intval($row[2]));
|
|
}
|
|
}
|
|
return $levels;
|
|
}
|
|
|
|
/*
|
|
* The roles, admins first then by name - role_id => the row with levels
|
|
* ([module_id => level]), agents (active ones) and agents_archived. All of
|
|
* them, or the current or archived ones.
|
|
*/
|
|
function roleRows($archived = null) {
|
|
global $mysqli;
|
|
$where = $archived === null ? '1' : ($archived ? 'role_archived_at IS NOT NULL' : 'role_archived_at IS NULL');
|
|
$rows = [];
|
|
$sql = mysqli_query($mysqli, "SELECT role_id, role_name, role_description, role_is_admin, role_created_at, role_updated_at, role_archived_at,
|
|
(SELECT COUNT(*) FROM users WHERE user_role_id = role_id AND user_type = 1 AND user_archived_at IS NULL) AS agents,
|
|
(SELECT COUNT(*) FROM users WHERE user_role_id = role_id AND user_type = 1 AND user_archived_at IS NOT NULL) AS agents_archived
|
|
FROM user_roles WHERE role_type = 1 AND $where
|
|
ORDER BY role_is_admin DESC, role_name ASC");
|
|
while ($row = mysqli_fetch_assoc($sql)) {
|
|
$rows[intval($row['role_id'])] = $row;
|
|
}
|
|
$levels = roleLevels(array_keys($rows));
|
|
foreach ($rows as $role_id => $row) {
|
|
$rows[$role_id]['levels'] = $levels[$role_id] ?? [];
|
|
}
|
|
return $rows;
|
|
}
|
|
|
|
/** One role as roleRows() gives it, or null. */
|
|
function roleRow($role_id) {
|
|
$role_id = intval($role_id);
|
|
foreach (roleRows() as $id => $row) {
|
|
if ($id === $role_id) {
|
|
return $row;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/** A few of each role's active agents for its row - role_id => [user rows], up to $limit each. */
|
|
function roleAgents(array $role_ids, $limit = 5) {
|
|
global $mysqli;
|
|
$agents = [];
|
|
if (!$role_ids) {
|
|
return $agents;
|
|
}
|
|
$sql = mysqli_query($mysqli, "SELECT user_id, user_name, user_avatar, user_role_id FROM users
|
|
WHERE user_type = 1 AND user_archived_at IS NULL AND user_role_id IN (" . implode(',', array_map('intval', $role_ids)) . ")
|
|
ORDER BY user_name ASC");
|
|
while ($row = mysqli_fetch_assoc($sql)) {
|
|
$role_id = intval($row['user_role_id']);
|
|
if (count($agents[$role_id] ?? []) < $limit) {
|
|
$agents[$role_id][] = $row;
|
|
}
|
|
}
|
|
return $agents;
|
|
}
|
|
|
|
/*
|
|
* What a role opens, as pills - one per module it has, its level in that
|
|
* level's colour; an admin role as one pill. HTML.
|
|
*/
|
|
function roleAccessHtml(array $row) {
|
|
if (intval($row['role_is_admin'])) {
|
|
return '<span class="badge rounded-pill bg-primary-subtle text-primary-emphasis fw-normal" title="Every part of ITFlow, every client, and Administration"><i class="fas fa-user-shield me-1"></i>Admin - everything</span>';
|
|
}
|
|
$pills = [];
|
|
foreach (roleModules() as $module_id => $module) {
|
|
$level = min(intval($row['levels'][$module_id] ?? 0), $module['max']);
|
|
if (!$level) {
|
|
continue;
|
|
}
|
|
$tone = ROLE_LEVEL_TONES[$level];
|
|
$title = $module['label'] . ' - ' . implode('; ', array_slice($module['levels'], 0, $level));
|
|
$pills[] = '<span class="badge rounded-pill bg-' . $tone . '-subtle text-' . $tone . '-emphasis fw-normal" title="' . escapeHtml($title) . '"><i class="fas fa-fw ' . $module['icon'] . ' me-1"></i>'
|
|
. escapeHtml($module['label']) . ($module['max'] > 1 ? ' · ' . ROLE_LEVELS[$level] : '') . '</span>';
|
|
}
|
|
return $pills ? '<span class="d-inline-flex flex-wrap gap-1">' . implode('', $pills) . '</span>' : '<span class="small text-secondary">Nothing yet - it opens no part of ITFlow</span>';
|
|
}
|
|
|
|
/*
|
|
* The New, Edit and Copy role window's fields, checked - [fields, error].
|
|
* fields: name, description, admin (0 or 1) and levels ([module_id =>
|
|
* level], every module, held to what it can have). $role_id is the role
|
|
* being edited, 0 for a new one.
|
|
*/
|
|
function roleFromInput(array $in, $role_id = 0) {
|
|
global $mysqli;
|
|
$name = trim(mb_substr(preg_replace('/\s+/u', ' ', strip_tags((string) ($in['name'] ?? ''))), 0, 200));
|
|
$description = trim(mb_substr(strip_tags((string) ($in['description'] ?? '')), 0, 200));
|
|
if ($name === '') {
|
|
return [null, 'A role needs a name.'];
|
|
}
|
|
$taken = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT role_name, role_archived_at FROM user_roles
|
|
WHERE LOWER(role_name) = LOWER('" . escapeSql($name) . "') AND role_id <> " . intval($role_id) . " LIMIT 1"));
|
|
if ($taken) {
|
|
return [null, 'There is already a role called ' . $taken['role_name'] . ($taken['role_archived_at'] ? ' - it is archived, so restore it instead' : '') . '.'];
|
|
}
|
|
$levels = [];
|
|
foreach (roleModules() as $module_id => $module) {
|
|
$level = intval($in['levels'][$module_id] ?? 0);
|
|
$levels[$module_id] = max(0, min($level, $module['max']));
|
|
}
|
|
return [['name' => $name, 'description' => $description, 'admin' => !empty($in['admin']) ? 1 : 0, 'levels' => $levels], ''];
|
|
}
|
|
|
|
/*
|
|
* Why a role can't stop being an admin role - '' when it can. Not your own
|
|
* (you'd be shut out of this page), and not while its agents are the only
|
|
* active admins.
|
|
*/
|
|
function roleAdminBlocker(array $row) {
|
|
global $mysqli, $session_user_role;
|
|
if (!intval($row['role_is_admin'])) {
|
|
return '';
|
|
}
|
|
if (intval($row['role_id']) === intval($session_user_role)) {
|
|
return 'It is your own role - another admin can change it.';
|
|
}
|
|
$others = intval(mysqli_fetch_row(mysqli_query($mysqli, "SELECT COUNT(*) FROM users JOIN user_roles ON role_id = user_role_id
|
|
WHERE user_type = 1 AND user_status = 1 AND user_archived_at IS NULL AND role_is_admin = 1 AND role_id <> " . intval($row['role_id'])))[0]);
|
|
if ($others === 0 && intval($row['agents'])) {
|
|
return 'Its agents are the only admins - nobody would be left to run Admin.';
|
|
}
|
|
return '';
|
|
}
|
|
|
|
/* Why a role can't be archived - '' when it can. Not while active agents have it or invites waiting are for it, nor your own. */
|
|
function roleArchiveBlocker(array $row) {
|
|
global $session_user_role;
|
|
if (intval($row['role_id']) === intval($session_user_role)) {
|
|
return 'It is your own role.';
|
|
}
|
|
if (intval($row['agents'])) {
|
|
$n = intval($row['agents']);
|
|
return $n . ' agent' . ($n === 1 ? ' has' : 's have') . ' it - give them another role first.';
|
|
}
|
|
if (($n = userInvitesWithRole($row['role_id'])) > 0) {
|
|
return $n . ' invite' . ($n === 1 ? ' is' : 's are') . ' waiting with it - cancel ' . ($n === 1 ? 'it' : 'them') . ' in Users, Invited, and invite again with another role.';
|
|
}
|
|
return '';
|
|
}
|
|
|
|
/*
|
|
* Why a role can't be deleted - '' when it can. As archiving, and not while
|
|
* an archived agent has it either, as they'd come back with no role.
|
|
*/
|
|
function roleDeleteBlocker(array $row) {
|
|
$blocker = roleArchiveBlocker($row);
|
|
if ($blocker !== '') {
|
|
return $blocker;
|
|
}
|
|
if (intval($row['agents_archived'])) {
|
|
$n = intval($row['agents_archived']);
|
|
return $n . ' archived agent' . ($n === 1 ? ' has' : 's have') . ' it - archive the role instead.';
|
|
}
|
|
return '';
|
|
}
|
|
|
|
/*
|
|
* What changed on a role, in words for the audit log - "admin taken off,
|
|
* Sales Read to Modify, Reports given Read". Raw; '' when nothing did.
|
|
*/
|
|
function roleChangeText(array $before, array $fields) {
|
|
$changes = [];
|
|
if (($before['role_name'] ?? '') !== $fields['name']) {
|
|
$changes[] = 'renamed from ' . $before['role_name'];
|
|
}
|
|
if ((string) ($before['role_description'] ?? '') !== $fields['description']) {
|
|
$changes[] = 'description changed';
|
|
}
|
|
if (intval($before['role_is_admin'] ?? 0) !== $fields['admin']) {
|
|
$changes[] = $fields['admin'] ? 'made an admin role' : 'admin taken off';
|
|
}
|
|
foreach (roleModules() as $module_id => $module) {
|
|
$was = intval($before['levels'][$module_id] ?? 0);
|
|
$now = intval($fields['levels'][$module_id] ?? 0);
|
|
if ($was !== $now) {
|
|
$changes[] = $module['label'] . ' ' . ROLE_LEVELS[$was] . ' to ' . ROLE_LEVELS[$now];
|
|
}
|
|
}
|
|
return implode(', ', $changes);
|
|
}
|
|
|
|
/* Save a role's fields (roleFromInput()) - a new role when $role_id is 0. Returns its id. */
|
|
function roleSave(array $fields, $role_id = 0) {
|
|
global $mysqli;
|
|
$role_id = intval($role_id);
|
|
$set = "role_name = '" . escapeSql($fields['name']) . "', role_description = '" . escapeSql($fields['description']) . "', role_is_admin = " . intval($fields['admin']);
|
|
if ($role_id) {
|
|
mysqli_query($mysqli, "UPDATE user_roles SET $set WHERE role_id = $role_id");
|
|
} else {
|
|
mysqli_query($mysqli, "INSERT INTO user_roles SET $set, role_type = 1");
|
|
$role_id = intval(mysqli_insert_id($mysqli));
|
|
}
|
|
// An admin role keeps its levels too, for if it stops being one
|
|
mysqli_query($mysqli, "DELETE FROM user_role_permissions WHERE user_role_id = $role_id");
|
|
foreach ($fields['levels'] as $module_id => $level) {
|
|
if ($level > 0) {
|
|
mysqli_query($mysqli, "INSERT INTO user_role_permissions SET user_role_id = $role_id, module_id = " . intval($module_id) . ", user_role_permission_level = " . intval($level));
|
|
}
|
|
}
|
|
return $role_id;
|
|
}
|
|
|
|
/* Archive or restore a role. */
|
|
function roleSetArchived($role_id, $archived) {
|
|
global $mysqli;
|
|
mysqli_query($mysqli, "UPDATE user_roles SET role_archived_at = " . ($archived ? 'NOW()' : 'NULL') . " WHERE role_id = " . intval($role_id));
|
|
}
|
|
|
|
/* Delete a role and its levels - check roleDeleteBlocker() first. */
|
|
function roleDelete($role_id) {
|
|
global $mysqli;
|
|
$role_id = intval($role_id);
|
|
mysqli_query($mysqli, "DELETE FROM user_role_permissions WHERE user_role_id = $role_id");
|
|
mysqli_query($mysqli, "DELETE FROM user_roles WHERE role_id = $role_id");
|
|
}
|