baseline
This commit is contained in:
359
pages/admin/users/_form.phtml
Normal file
359
pages/admin/users/_form.phtml
Normal file
@@ -0,0 +1,359 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @var array $values
|
||||
* @var bool $passwordRequired
|
||||
* @var string|null $passwordLabel
|
||||
* @var string|null $passwordLegend
|
||||
* @var string|null $passwordConfirmLabel
|
||||
* @var string|null $formId
|
||||
* @var bool $detailsOpenAll
|
||||
* @var bool $showTenants
|
||||
* @var array $tenants
|
||||
* @var array $selectedTenantIds
|
||||
* @var bool $showRoles
|
||||
* @var array $roles
|
||||
* @var array $selectedRoleIds
|
||||
* @var bool $showDepartments
|
||||
* @var array $departments
|
||||
* @var array $selectedDepartmentIds
|
||||
* @var bool $isReadOnly
|
||||
* @var bool $showPermissions
|
||||
* @var array $permissionRows
|
||||
*/
|
||||
|
||||
use MintyPHP\Session;
|
||||
use MintyPHP\Service\UserService;
|
||||
use MintyPHP\I18n;
|
||||
|
||||
$values = $values ?? [];
|
||||
$isOwnAccount = (int) ($_SESSION['user']['id'] ?? 0) === (int) ($values['id'] ?? 0);
|
||||
$passwordRequired = $passwordRequired ?? false;
|
||||
$passwordLabel = $passwordLabel ?? t('Password');
|
||||
$passwordLegend = $passwordLegend ?? t('Password');
|
||||
$passwordConfirmLabel = $passwordConfirmLabel ?? t('Password (again)');
|
||||
$formId = $formId ?? 'user-form';
|
||||
$detailsOpenAll = $detailsOpenAll ?? false;
|
||||
$showTenants = $showTenants ?? false;
|
||||
$tenants = $tenants ?? [];
|
||||
$selectedTenantIds = $selectedTenantIds ?? [];
|
||||
$showRoles = $showRoles ?? false;
|
||||
$roles = $roles ?? [];
|
||||
$selectedRoleIds = $selectedRoleIds ?? [];
|
||||
$showDepartments = $showDepartments ?? false;
|
||||
$departments = $departments ?? [];
|
||||
$selectedDepartmentIds = $selectedDepartmentIds ?? [];
|
||||
$primaryTenantId = (int) ($values['primary_tenant_id'] ?? 0);
|
||||
if (!$primaryTenantId && count($selectedTenantIds) === 1) {
|
||||
$primaryTenantId = (int) $selectedTenantIds[0];
|
||||
}
|
||||
$isReadOnly = $isReadOnly ?? false;
|
||||
$permissionRows = $permissionRows ?? [];
|
||||
$showPermissions = $showPermissions ?? false;
|
||||
$minLength = UserService::passwordMinLength();
|
||||
$requiredAttr = $passwordRequired ? 'required' : '';
|
||||
$readonlyAttr = $isReadOnly ? 'readonly' : '';
|
||||
$disabledAttr = $isReadOnly ? 'disabled' : '';
|
||||
$dataDisabledAttr = $isReadOnly ? 'data-disabled="true"' : '';
|
||||
$locales = defined('APP_LOCALES') ? APP_LOCALES : [I18n::$defaultLocale];
|
||||
$primaryTenantDisabled = $isReadOnly || count($selectedTenantIds) === 1;
|
||||
$primaryTenantDisabledAttr = $primaryTenantDisabled ? 'disabled' : '';
|
||||
$showOrganization = $showTenants || $showDepartments || $showRoles;
|
||||
|
||||
?>
|
||||
<form id="<?php e($formId); ?>" method="post">
|
||||
<div class="app-tabs" data-tabs data-tabs-param="tab" data-tabs-storage-key="admin-user-form">
|
||||
<div class="app-tabs-nav">
|
||||
<button type="button" data-tab="profile" data-tab-default><?php e(t('Profile')); ?></button>
|
||||
<button type="button" data-tab="access"><?php e(t('Access & password')); ?></button>
|
||||
<?php if ($showOrganization): ?>
|
||||
<button type="button" data-tab="organization"><?php e(t('Organization')); ?></button>
|
||||
<?php endif; ?>
|
||||
<button type="button" data-tab="masterdata"><?php e(t('Master data')); ?></button>
|
||||
<?php if ($showPermissions): ?>
|
||||
<button type="button" data-tab="permissions"><?php e(t('Permissions')); ?></button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div data-tab-panel="profile">
|
||||
<details name="identity-data" open>
|
||||
<summary><?php e(t('Identity')); ?></summary>
|
||||
<hr>
|
||||
<div class="grid">
|
||||
<label for="first_name">
|
||||
<span><?php e(t('First name')); ?></span>
|
||||
<input required type="text" name="first_name" id="first_name" value="<?php e($values['first_name'] ?? ''); ?>"
|
||||
<?php e($readonlyAttr); ?> />
|
||||
</label>
|
||||
<label for="last_name">
|
||||
<span><?php e(t('Last name')); ?></span>
|
||||
<input required type="text" name="last_name" id="last_name" value="<?php e($values['last_name'] ?? ''); ?>"
|
||||
<?php e($readonlyAttr); ?> />
|
||||
</label>
|
||||
</div>
|
||||
<label for="email">
|
||||
<span><?php e(t('Email')); ?></span>
|
||||
<input required type="email" name="email" id="email" value="<?php e($values['email'] ?? ''); ?>" <?php e($readonlyAttr); ?> />
|
||||
</label>
|
||||
</details>
|
||||
<hr>
|
||||
<details name="profile-data" open>
|
||||
<summary><?php e(t('Profile')); ?></summary>
|
||||
<hr>
|
||||
<label for="job_title">
|
||||
<span><?php e(t('Job title')); ?></span>
|
||||
<input type="text" name="job_title" id="job_title" value="<?php e($values['job_title'] ?? ''); ?>"
|
||||
<?php e($readonlyAttr); ?> />
|
||||
</label>
|
||||
<label for="profile_description">
|
||||
<span><?php e(t('Profile description')); ?></span>
|
||||
<textarea name="profile_description" id="profile_description" rows="4"
|
||||
<?php e($readonlyAttr); ?>><?php e($values['profile_description'] ?? ''); ?></textarea>
|
||||
</label>
|
||||
</details>
|
||||
<hr>
|
||||
<details name="preferences" open>
|
||||
<summary><?php e(t('Preferences')); ?></summary>
|
||||
<hr>
|
||||
<div class="grid">
|
||||
<?php if (allowUserTheme()): ?>
|
||||
<label for="theme">
|
||||
<span><?php e(t('Theme')); ?></span>
|
||||
<?php $selectedTheme = $values['theme'] ?? 'light'; ?>
|
||||
<select name="theme" id="theme" <?php e($disabledAttr); ?>>
|
||||
<option value="light" <?php if ($selectedTheme === 'light') { ?>selected<?php } ?>>
|
||||
<?php e(t('Light')); ?>
|
||||
</option>
|
||||
<option value="dark" <?php if ($selectedTheme === 'dark') { ?>selected<?php } ?>>
|
||||
<?php e(t('Dark')); ?>
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
<?php endif; ?>
|
||||
<label for="locale">
|
||||
<span><?php e(t('Language')); ?></span>
|
||||
<?php $selectedLocale = $values['locale'] ?? (I18n::$locale ?? I18n::$defaultLocale); ?>
|
||||
<select name="locale" id="locale" <?php e($disabledAttr); ?>>
|
||||
<?php foreach ($locales as $locale): ?>
|
||||
<?php
|
||||
$label = strtoupper($locale);
|
||||
if ($locale === 'de') {
|
||||
$label = t('German');
|
||||
} elseif ($locale === 'en') {
|
||||
$label = t('English');
|
||||
}
|
||||
?>
|
||||
<option value="<?php e($locale); ?>" <?php if ($selectedLocale === $locale) { ?>selected<?php } ?>>
|
||||
<?php e($label); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<div data-tab-panel="access">
|
||||
<details name="access-data" open>
|
||||
<summary><?php e(t('Access & password')); ?></summary>
|
||||
<hr>
|
||||
<div class="grid">
|
||||
<div>
|
||||
<label for="password">
|
||||
<span><?php e($passwordLabel); ?></span>
|
||||
<input <?php e($requiredAttr); ?> type="password" name="password" id="password"
|
||||
minlength="<?php e($minLength); ?>" autocomplete="new-password" <?php e($disabledAttr); ?> />
|
||||
</label>
|
||||
<label for="password2">
|
||||
<span><?php e($passwordConfirmLabel); ?></span>
|
||||
<input <?php e($requiredAttr); ?> type="password" name="password2" id="password2"
|
||||
minlength="<?php e($minLength); ?>" autocomplete="new-password" <?php e($disabledAttr); ?> />
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-hint" data-password-hints data-password-input="#password" data-confirm-input="#password2"
|
||||
data-email-input="#email" data-min-length="<?php e($minLength); ?>">
|
||||
<strong><?php e(t('Password requirements')); ?></strong>
|
||||
<ul class="form-hint-list">
|
||||
<?php foreach (UserService::passwordHints() as $hint): ?>
|
||||
<li data-rule="<?php e($hint['rule']); ?>"><?php e($hint['text']); ?></li>
|
||||
<?php endforeach; ?>
|
||||
<li data-rule="match"><?php e(t('Passwords must match')); ?></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<?php if (!$isOwnAccount): ?>
|
||||
<label for="active">
|
||||
<input type="checkbox" name="active" id="active" value="1" <?php e(($values['active'] ?? 0) ? 'checked' : ''); ?>
|
||||
<?php e($disabledAttr); ?> />
|
||||
<span><?php e(t('Active')); ?></span>
|
||||
</label>
|
||||
<?php endif; ?>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<?php if ($showOrganization): ?>
|
||||
<div data-tab-panel="organization">
|
||||
<?php if ($showTenants): ?>
|
||||
<details name="assignments-tenants" open>
|
||||
<summary><?php e(t('Tenants')); ?></summary>
|
||||
<hr>
|
||||
<label>
|
||||
<span>
|
||||
<?php e(t('Primary tenant')); ?>
|
||||
</span>
|
||||
</label>
|
||||
<?php if ($primaryTenantDisabled && $primaryTenantId > 0): ?>
|
||||
<input type="hidden" name="primary_tenant_id" value="<?php e((string) $primaryTenantId); ?>">
|
||||
<?php endif; ?>
|
||||
<select id="user-primary-tenant" name="primary_tenant_id" <?php e($primaryTenantDisabledAttr); ?>
|
||||
<?php e($disabledAttr); ?>>
|
||||
<?php if (count($selectedTenantIds) !== 1): ?>
|
||||
<option value="">
|
||||
<?php e(t('Select primary tenant')); ?>
|
||||
</option>
|
||||
<?php endif; ?>
|
||||
<?php foreach ($tenants as $tenant): ?>
|
||||
<?php
|
||||
$tenantId = (int) ($tenant['id'] ?? 0);
|
||||
if (!in_array($tenantId, $selectedTenantIds ?? [], true)) {
|
||||
continue;
|
||||
}
|
||||
$selected = $tenantId === $primaryTenantId ? 'selected' : '';
|
||||
?>
|
||||
<option value="<?php e((string) $tenantId); ?>" <?php e($selected); ?>>
|
||||
<?php e($tenant['description'] ?? ''); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<?php multiSelectForm('user-tenants', 'tenant_ids', 'Assigned tenants', 'Select tenants', $tenants, $selectedTenantIds, $isReadOnly); ?>
|
||||
</details>
|
||||
<hr>
|
||||
<?php endif; ?>
|
||||
<?php if ($showDepartments || $showRoles): ?>
|
||||
<details name="assignments-structure" open>
|
||||
<summary><?php e(t('Departments & roles')); ?></summary>
|
||||
<hr>
|
||||
<div class="grid">
|
||||
<?php if ($showDepartments): ?>
|
||||
<div>
|
||||
<?php multiSelectForm('user-departments', 'department_ids', 'Assigned departments', 'Select departments', $departments, $selectedDepartmentIds, $isReadOnly); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if ($showRoles): ?>
|
||||
<div>
|
||||
<?php multiSelectForm('user-roles', 'role_ids', 'Assigned roles', 'Select roles', $roles, $selectedRoleIds, $isReadOnly); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</details>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div data-tab-panel="masterdata">
|
||||
<details name="contact-data" open>
|
||||
<summary><?php e(t('Contact')); ?></summary>
|
||||
<hr>
|
||||
<div class="grid">
|
||||
<label for="phone">
|
||||
<span><?php e(t('Phone')); ?></span>
|
||||
<input type="tel" name="phone" id="phone" autocomplete="tel" value="<?php e($values['phone'] ?? ''); ?>"
|
||||
<?php e($readonlyAttr); ?> />
|
||||
</label>
|
||||
<label for="mobile">
|
||||
<span><?php e(t('Mobile')); ?></span>
|
||||
<input type="tel" name="mobile" id="mobile" autocomplete="tel" value="<?php e($values['mobile'] ?? ''); ?>"
|
||||
<?php e($readonlyAttr); ?> />
|
||||
</label>
|
||||
<label for="short_dial">
|
||||
<span><?php e(t('Short dial')); ?></span>
|
||||
<input type="text" name="short_dial" id="short_dial" inputmode="numeric"
|
||||
value="<?php e($values['short_dial'] ?? ''); ?>" <?php e($readonlyAttr); ?> />
|
||||
</label>
|
||||
</div>
|
||||
</details>
|
||||
<hr>
|
||||
<details name="address-data" open>
|
||||
<summary><?php e(t('Address')); ?></summary>
|
||||
<hr>
|
||||
<label for="address">
|
||||
<span><?php e(t('Address')); ?></span>
|
||||
<input type="text" name="address" id="address" value="<?php e($values['address'] ?? ''); ?>" <?php e($readonlyAttr); ?> />
|
||||
</label>
|
||||
<div class="grid">
|
||||
<label for="postal_code">
|
||||
<span><?php e(t('Postal code')); ?></span>
|
||||
<input type="text" name="postal_code" id="postal_code" value="<?php e($values['postal_code'] ?? ''); ?>" <?php e($readonlyAttr); ?> />
|
||||
</label>
|
||||
<label for="city">
|
||||
<span><?php e(t('City')); ?></span>
|
||||
<input type="text" name="city" id="city" value="<?php e($values['city'] ?? ''); ?>" <?php e($readonlyAttr); ?> />
|
||||
</label>
|
||||
</div>
|
||||
<div class="grid">
|
||||
<label for="region">
|
||||
<span><?php e(t('Region')); ?></span>
|
||||
<input type="text" name="region" id="region" value="<?php e($values['region'] ?? ''); ?>" <?php e($readonlyAttr); ?> />
|
||||
</label>
|
||||
<label for="country">
|
||||
<span><?php e(t('Country')); ?></span>
|
||||
<input type="text" name="country" id="country" value="<?php e($values['country'] ?? ''); ?>" <?php e($readonlyAttr); ?> />
|
||||
</label>
|
||||
</div>
|
||||
</details>
|
||||
<hr>
|
||||
<details name="employment-data" open>
|
||||
<summary><?php e(t('Employment')); ?></summary>
|
||||
<hr>
|
||||
<label for="hire_date">
|
||||
<span><?php e(t('Hire date')); ?></span>
|
||||
<input type="date" name="hire_date" id="hire_date" value="<?php e($values['hire_date'] ?? ''); ?>" <?php e($readonlyAttr); ?> />
|
||||
</label>
|
||||
</details>
|
||||
</div>
|
||||
<?php if ($showPermissions): ?>
|
||||
<div data-tab-panel="permissions">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?php e(t('Description')); ?></th>
|
||||
<th><?php e(t('Permission key')); ?></th>
|
||||
<th><?php e(t('Permission origin')); ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($permissionRows as $permissionRow): ?>
|
||||
<?php
|
||||
$permissionKey = (string) ($permissionRow['key'] ?? '');
|
||||
$labelKey = 'perm.' . $permissionKey;
|
||||
$label = t($labelKey);
|
||||
if ($label === $labelKey) {
|
||||
$label = (string) ($permissionRow['description'] ?? '');
|
||||
if ($label === '') {
|
||||
$label = $permissionKey;
|
||||
}
|
||||
}
|
||||
$rolesList = [];
|
||||
if (!empty($permissionRow['roles']) && is_array($permissionRow['roles'])) {
|
||||
$rolesList = $permissionRow['roles'];
|
||||
}
|
||||
?>
|
||||
<tr>
|
||||
<td><?php e($label); ?></td>
|
||||
<td><code><?php e($permissionKey); ?></code></td>
|
||||
<td>
|
||||
<?php if ($rolesList): ?>
|
||||
<?php foreach ($rolesList as $roleLabel): ?>
|
||||
<span class="badge" data-variant="neutral"><?php e($roleLabel); ?></span>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php Session::getCsrfInput(); ?>
|
||||
</form>
|
||||
41
pages/admin/users/activate($id).php
Normal file
41
pages/admin/users/activate($id).php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\UserService;
|
||||
use MintyPHP\Http\Request;
|
||||
use MintyPHP\Service\TenantScopeService;
|
||||
|
||||
Guard::requireLogin();
|
||||
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
|
||||
Router::redirect('admin/users');
|
||||
return;
|
||||
}
|
||||
|
||||
$uuid = trim((string) ($id ?? ''));
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
$user = $uuid !== '' ? UserService::findByUuid($uuid) : null;
|
||||
$userId = (int) ($user['id'] ?? 0);
|
||||
if ($userId > 0 && $currentUserId !== $userId && !TenantScopeService::canAccess('users', $userId, $currentUserId)) {
|
||||
if (Request::wantsJson()) {
|
||||
http_response_code(403);
|
||||
Router::json(['error' => 'permission_denied']);
|
||||
return;
|
||||
}
|
||||
Router::redirect('error/forbidden');
|
||||
return;
|
||||
}
|
||||
$result = UserService::setActiveByUuid($uuid, true, $currentUserId);
|
||||
if ($result['ok'] ?? false) {
|
||||
if (Request::wantsJson()) {
|
||||
http_response_code(204);
|
||||
return;
|
||||
}
|
||||
} elseif (Request::wantsJson()) {
|
||||
http_response_code((int) ($result['status'] ?? 404));
|
||||
Router::json(['error' => 'not_found']);
|
||||
return;
|
||||
}
|
||||
|
||||
Router::redirect('admin/users');
|
||||
41
pages/admin/users/avatar($id).php
Normal file
41
pages/admin/users/avatar($id).php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\UserAvatarService;
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Service\UserService;
|
||||
use MintyPHP\Service\TenantScopeService;
|
||||
|
||||
Guard::requireLogin();
|
||||
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
|
||||
Router::redirect('admin/users');
|
||||
return;
|
||||
}
|
||||
|
||||
$uuid = trim((string) ($id ?? ''));
|
||||
if (!UserAvatarService::isValidUuid($uuid)) {
|
||||
Flash::error('User not found', 'admin/users', 'user_not_found');
|
||||
Router::redirect('admin/users');
|
||||
return;
|
||||
}
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
$user = $uuid !== '' ? UserService::findByUuid($uuid) : null;
|
||||
$userId = (int) ($user['id'] ?? 0);
|
||||
if ($userId > 0 && $currentUserId !== $userId && !TenantScopeService::canAccess('users', $userId, $currentUserId)) {
|
||||
Router::redirect('error/forbidden');
|
||||
return;
|
||||
}
|
||||
|
||||
$result = UserAvatarService::saveUpload($uuid, $_FILES['avatar'] ?? []);
|
||||
if (!($result['ok'] ?? false)) {
|
||||
$error = $result['error'] ?? t('Upload failed');
|
||||
Flash::error($error, "admin/users/edit/{$uuid}", 'avatar_upload_failed');
|
||||
Router::redirect("admin/users/edit/{$uuid}");
|
||||
return;
|
||||
}
|
||||
|
||||
Flash::success('Avatar updated', "admin/users/edit/{$uuid}", 'avatar_updated');
|
||||
Router::redirect("admin/users/edit/{$uuid}");
|
||||
34
pages/admin/users/avatar-delete($id).php
Normal file
34
pages/admin/users/avatar-delete($id).php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\UserAvatarService;
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Service\UserService;
|
||||
use MintyPHP\Service\TenantScopeService;
|
||||
|
||||
Guard::requireLogin();
|
||||
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
|
||||
Router::redirect('admin/users');
|
||||
return;
|
||||
}
|
||||
|
||||
$uuid = trim((string) ($id ?? ''));
|
||||
if (!UserAvatarService::isValidUuid($uuid)) {
|
||||
Flash::error('User not found', 'admin/users', 'user_not_found');
|
||||
Router::redirect('admin/users');
|
||||
return;
|
||||
}
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
$user = $uuid !== '' ? UserService::findByUuid($uuid) : null;
|
||||
$userId = (int) ($user['id'] ?? 0);
|
||||
if ($userId > 0 && $currentUserId !== $userId && !TenantScopeService::canAccess('users', $userId, $currentUserId)) {
|
||||
Router::redirect('error/forbidden');
|
||||
return;
|
||||
}
|
||||
|
||||
UserAvatarService::delete($uuid);
|
||||
Flash::success('Avatar removed', "admin/users/edit/{$uuid}", 'avatar_removed');
|
||||
Router::redirect("admin/users/edit/{$uuid}");
|
||||
37
pages/admin/users/avatar-file().php
Normal file
37
pages/admin/users/avatar-file().php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Service\UserAvatarService;
|
||||
use MintyPHP\Service\UserService;
|
||||
use MintyPHP\Service\TenantScopeService;
|
||||
use MintyPHP\Support\Guard;
|
||||
|
||||
Guard::requireLogin();
|
||||
|
||||
$uuid = trim((string) ($_GET['uuid'] ?? ''));
|
||||
$size = isset($_GET['size']) ? (int) $_GET['size'] : null;
|
||||
if (!UserAvatarService::isValidUuid($uuid)) {
|
||||
http_response_code(404);
|
||||
return;
|
||||
}
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
$user = $uuid !== '' ? UserService::findByUuid($uuid) : null;
|
||||
$userId = (int) ($user['id'] ?? 0);
|
||||
if ($userId > 0 && $currentUserId !== $userId && !TenantScopeService::canAccess('users', $userId, $currentUserId)) {
|
||||
http_response_code(403);
|
||||
return;
|
||||
}
|
||||
|
||||
$path = UserAvatarService::findAvatarPath($uuid, $size);
|
||||
if (!$path || !is_file($path)) {
|
||||
http_response_code(404);
|
||||
return;
|
||||
}
|
||||
|
||||
$mime = UserAvatarService::detectMime($path);
|
||||
header('Content-Type: ' . $mime);
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
header('Content-Security-Policy: sandbox');
|
||||
header('Cache-Control: private, max-age=300');
|
||||
header('Content-Length: ' . filesize($path));
|
||||
readfile($path);
|
||||
4
pages/admin/users/avatar-file(none).phtml
Normal file
4
pages/admin/users/avatar-file(none).phtml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?php
|
||||
|
||||
http_response_code(404);
|
||||
return;
|
||||
135
pages/admin/users/bulk($action).php
Normal file
135
pages/admin/users/bulk($action).php
Normal file
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Http\Request;
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\PermissionService;
|
||||
use MintyPHP\Service\UserService;
|
||||
use MintyPHP\Service\MailService;
|
||||
use MintyPHP\I18n;
|
||||
|
||||
Guard::requireLogin();
|
||||
|
||||
$action = strtolower(trim((string) ($action ?? '')));
|
||||
$handlers = [
|
||||
'activate' => 'handleActivate',
|
||||
'deactivate' => 'handleDeactivate',
|
||||
'delete' => 'handleDelete',
|
||||
'send-access' => 'handleSendAccess',
|
||||
];
|
||||
if (!isset($handlers[$action])) {
|
||||
Router::json(['ok' => false, 'error' => 'invalid_action']);
|
||||
}
|
||||
if ($action === 'delete') {
|
||||
Guard::requirePermissionOrForbidden(PermissionService::USERS_DELETE);
|
||||
}
|
||||
if ($action === 'send-access') {
|
||||
Guard::requirePermissionOrForbidden(PermissionService::USERS_UPDATE);
|
||||
}
|
||||
|
||||
$raw = $_POST['uuids'] ?? '';
|
||||
$uuids = [];
|
||||
if (is_array($raw)) {
|
||||
$uuids = $raw;
|
||||
} elseif (is_string($raw)) {
|
||||
$uuids = array_filter(array_map('trim', explode(',', $raw)));
|
||||
}
|
||||
$uuids = array_values(array_unique(array_filter($uuids)));
|
||||
if (!$uuids) {
|
||||
Router::json(['ok' => false, 'error' => 'no_selection']);
|
||||
}
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
call_user_func($handlers[$action], $uuids, $currentUserId);
|
||||
|
||||
function handleActivate(array $uuids, int $currentUserId): void
|
||||
{
|
||||
$result = UserService::setActiveByUuids($uuids, true, $currentUserId);
|
||||
if (!($result['ok'] ?? false)) {
|
||||
Router::json(['ok' => false, 'error' => 'update_failed']);
|
||||
}
|
||||
Router::json(['ok' => true, 'count' => (int) ($result['count'] ?? 0)]);
|
||||
}
|
||||
|
||||
function handleDeactivate(array $uuids, int $currentUserId): void
|
||||
{
|
||||
if ($currentUserId > 0) {
|
||||
$currentUser = UserService::findById($currentUserId);
|
||||
$currentUuid = $currentUser['uuid'] ?? '';
|
||||
if ($currentUuid !== '') {
|
||||
$uuids = array_values(array_filter($uuids, static fn ($uuid) => $uuid !== $currentUuid));
|
||||
}
|
||||
if (!$uuids) {
|
||||
Router::json(['ok' => false, 'error' => 'self_deactivate']);
|
||||
}
|
||||
}
|
||||
|
||||
$result = UserService::setActiveByUuids($uuids, false, $currentUserId);
|
||||
if (!($result['ok'] ?? false)) {
|
||||
Router::json(['ok' => false, 'error' => 'update_failed']);
|
||||
}
|
||||
Router::json(['ok' => true, 'count' => (int) ($result['count'] ?? 0)]);
|
||||
}
|
||||
|
||||
function handleDelete(array $uuids, int $currentUserId): void
|
||||
{
|
||||
$result = UserService::deleteByUuids($uuids, $currentUserId);
|
||||
if (!($result['ok'] ?? false)) {
|
||||
Router::json(['ok' => false, 'error' => $result['error'] ?? 'delete_failed']);
|
||||
}
|
||||
Router::json(['ok' => true, 'count' => (int) ($result['count'] ?? 0)]);
|
||||
}
|
||||
|
||||
function handleSendAccess(array $uuids, int $currentUserId): void
|
||||
{
|
||||
$successCount = 0;
|
||||
$failedCount = 0;
|
||||
|
||||
foreach ($uuids as $uuid) {
|
||||
$user = UserService::findByUuid($uuid);
|
||||
if (!$user || empty($user['email'])) {
|
||||
$failedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$locale = $user['locale'] ?? (I18n::$locale ?? I18n::$defaultLocale);
|
||||
$name = trim(($user['first_name'] ?? '') . ' ' . ($user['last_name'] ?? ''));
|
||||
$isGerman = strpos((string) $locale, 'de') === 0;
|
||||
$greeting = $isGerman ? 'Hallo' : 'Hello';
|
||||
if ($name !== '') {
|
||||
$greeting .= ' ' . $name;
|
||||
}
|
||||
$greeting .= ',';
|
||||
|
||||
$loginUrl = appUrl(Request::withLocale('login', $locale));
|
||||
$resetUrl = appUrl(Request::withLocale('password/forgot', $locale));
|
||||
$previousLocale = I18n::$locale ?? null;
|
||||
I18n::$locale = $locale;
|
||||
$subject = t('Your access details');
|
||||
I18n::$locale = $previousLocale;
|
||||
|
||||
$vars = [
|
||||
'app_name' => appTitle(),
|
||||
'app_logo_url' => appLogoUrlAbsolute(128),
|
||||
'imprint_url' => appUrl(Request::withLocale('imprint', $locale)),
|
||||
'privacy_url' => appUrl(Request::withLocale('privacy', $locale)),
|
||||
'greeting' => $greeting,
|
||||
'username' => (string) ($user['email'] ?? ''),
|
||||
'login_url' => $loginUrl,
|
||||
'reset_url' => $resetUrl,
|
||||
];
|
||||
|
||||
$result = MailService::sendTemplate('access_info', $vars, (string) $user['email'], $subject, $locale);
|
||||
if ($result['ok'] ?? false) {
|
||||
$successCount++;
|
||||
} else {
|
||||
$failedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
Router::json([
|
||||
'ok' => $successCount > 0,
|
||||
'count' => $successCount,
|
||||
'failed' => $failedCount,
|
||||
]);
|
||||
}
|
||||
93
pages/admin/users/create().php
Normal file
93
pages/admin/users/create().php
Normal file
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Buffer;
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\I18n;
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\PermissionService;
|
||||
use MintyPHP\Service\UserService;
|
||||
use MintyPHP\Service\TenantScopeService;
|
||||
use MintyPHP\Service\TenantService;
|
||||
use MintyPHP\Service\RoleService;
|
||||
use MintyPHP\Service\DepartmentService;
|
||||
use MintyPHP\Service\SettingService;
|
||||
|
||||
Guard::requirePermissionOrForbidden(PermissionService::USERS_CREATE);
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
$allowedTenantIds = TenantScopeService::getUserTenantIds($currentUserId);
|
||||
|
||||
$tenants = TenantService::list();
|
||||
if ($allowedTenantIds) {
|
||||
$tenants = array_values(array_filter($tenants, static function (array $tenant) use ($allowedTenantIds): bool {
|
||||
$tenantId = (int) ($tenant['id'] ?? 0);
|
||||
return $tenantId > 0 && in_array($tenantId, $allowedTenantIds, true);
|
||||
}));
|
||||
} elseif (TenantScopeService::isStrict()) {
|
||||
$tenants = [];
|
||||
}
|
||||
$roles = RoleService::list();
|
||||
$departments = $allowedTenantIds ? DepartmentService::listByTenantIds($allowedTenantIds) : [];
|
||||
if (!$allowedTenantIds && !TenantScopeService::isStrict()) {
|
||||
$departments = DepartmentService::list();
|
||||
}
|
||||
|
||||
$errors = [];
|
||||
$form = [
|
||||
'first_name' => '',
|
||||
'last_name' => '',
|
||||
'email' => '',
|
||||
'profile_description' => '',
|
||||
'job_title' => '',
|
||||
'phone' => '',
|
||||
'mobile' => '',
|
||||
'short_dial' => '',
|
||||
'address' => '',
|
||||
'postal_code' => '',
|
||||
'city' => '',
|
||||
'country' => '',
|
||||
'region' => '',
|
||||
'hire_date' => '',
|
||||
'totp_secret' => '',
|
||||
'locale' => I18n::$locale ?? I18n::$defaultLocale,
|
||||
'active' => 1,
|
||||
];
|
||||
|
||||
if (isset($_POST['email'])) {
|
||||
$input = $_POST;
|
||||
$tenantIds = UserService::normalizeIdInput($input['tenant_ids'] ?? []);
|
||||
if ($allowedTenantIds) {
|
||||
$tenantIds = array_values(array_intersect($tenantIds, $allowedTenantIds));
|
||||
} elseif (TenantScopeService::isStrict()) {
|
||||
$tenantIds = [];
|
||||
}
|
||||
$defaultTenantId = SettingService::getDefaultTenantId();
|
||||
if (TenantScopeService::isStrict() && !$tenantIds && !$defaultTenantId) {
|
||||
$errors[] = t('Please select at least one tenant');
|
||||
}
|
||||
$input['tenant_ids'] = $tenantIds;
|
||||
$result = UserService::createFromAdmin($input, $currentUserId);
|
||||
$form = $result['form'] ?? $form;
|
||||
$errors = array_merge($errors, $result['errors'] ?? []);
|
||||
|
||||
if (($result['ok'] ?? false) && !$errors) {
|
||||
$action = (string) ($_POST['action'] ?? 'create');
|
||||
if ($action === 'create_close') {
|
||||
Flash::success('User created', 'admin/users', 'user_created');
|
||||
Router::redirect('admin/users');
|
||||
} else {
|
||||
$uuid = (string) ($result['uuid'] ?? '');
|
||||
if ($uuid !== '') {
|
||||
$target = "admin/users/edit/{$uuid}";
|
||||
Flash::success('User created', $target, 'user_created');
|
||||
Router::redirect($target);
|
||||
} else {
|
||||
Flash::success('User created', 'admin/users', 'user_created');
|
||||
Router::redirect('admin/users');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Buffer::set('title', t('Create user'));
|
||||
51
pages/admin/users/create(default).phtml
Normal file
51
pages/admin/users/create(default).phtml
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @var array<int, string> $errors
|
||||
* @var array $form
|
||||
*/
|
||||
|
||||
|
||||
|
||||
?>
|
||||
<div class="app-details-container">
|
||||
|
||||
<section>
|
||||
<div class="app-breadcrumb">
|
||||
<a href="/admin">Startseite</a><i class="bi bi-chevron-right"></i><a
|
||||
href="/admin/users"><?php e(t('Users')); ?></a><i class="bi bi-chevron-right"></i><?php e(t('Create user')); ?>
|
||||
</div>
|
||||
<div class="app-details-titlebar">
|
||||
<h1>
|
||||
<a href="admin/users" title="<?php e(t('Cancel')); ?>"><i class="bi bi-arrow-left"></i></a>
|
||||
<?php e(t('Create user')); ?>
|
||||
</h1>
|
||||
<div class="app-details-titlebar-actions">
|
||||
<button type="submit" form="user-form" name="action" value="create" class="secondary outline">
|
||||
<?php e(t('Create')); ?>
|
||||
</button>
|
||||
<button type="submit" form="user-form" name="action" value="create_close" class="primary">
|
||||
<?php e(t('Create & close')); ?>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<?php if (!empty($errors)): ?>
|
||||
<div class="notice" data-variant="error">
|
||||
<ul>
|
||||
<?php foreach ($errors as $error): ?>
|
||||
<li><?php e($error); ?></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php
|
||||
$values = $form ?? [];
|
||||
$passwordRequired = true;
|
||||
$detailsOpenAll = true;
|
||||
$passwordLabel = t('Password');
|
||||
$passwordLegend = t('Password');
|
||||
$passwordConfirmLabel = t('Password (again)');
|
||||
require __DIR__ . '/_form.phtml';
|
||||
?>
|
||||
</section>
|
||||
</div>
|
||||
106
pages/admin/users/data().php
Normal file
106
pages/admin/users/data().php
Normal file
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\UserService;
|
||||
use MintyPHP\Service\UserAvatarService;
|
||||
use MintyPHP\Service\TenantScopeService;
|
||||
|
||||
if (!isset($_SESSION['user'])) {
|
||||
http_response_code(401);
|
||||
Router::json(['data' => [], 'total' => 0]);
|
||||
}
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
|
||||
$limit = (int) ($_GET['limit'] ?? 10);
|
||||
$offset = (int) ($_GET['offset'] ?? 0);
|
||||
$search = trim((string) ($_GET['search'] ?? ''));
|
||||
$order = (string) ($_GET['order'] ?? 'last_name');
|
||||
$dir = (string) ($_GET['dir'] ?? 'asc');
|
||||
$active = $_GET['active'] ?? 'all';
|
||||
$createdFrom = trim((string) ($_GET['created_from'] ?? ''));
|
||||
$createdTo = trim((string) ($_GET['created_to'] ?? ''));
|
||||
$tenant = trim((string) ($_GET['tenant'] ?? ''));
|
||||
$roles = $_GET['roles'] ?? '';
|
||||
$departments = $_GET['departments'] ?? '';
|
||||
$emailVerified = $_GET['email_verified'] ?? 'all';
|
||||
|
||||
$result = UserService::listPaged([
|
||||
'limit' => $limit,
|
||||
'offset' => $offset,
|
||||
'search' => $search,
|
||||
'order' => $order,
|
||||
'dir' => $dir,
|
||||
'active' => $active,
|
||||
'created_from' => $createdFrom,
|
||||
'created_to' => $createdTo,
|
||||
'tenant' => $tenant,
|
||||
'roles' => $roles,
|
||||
'departments' => $departments,
|
||||
'email_verified' => $emailVerified,
|
||||
'tenantUserId' => $currentUserId,
|
||||
]);
|
||||
|
||||
$rows = [];
|
||||
foreach ($result['rows'] as $row) {
|
||||
$tenantLabels = $row['tenant_labels'] ?? [];
|
||||
if (is_string($tenantLabels)) {
|
||||
$tenantList = $tenantLabels !== ''
|
||||
? array_values(array_filter(array_map('trim', explode('||', $tenantLabels))))
|
||||
: [];
|
||||
} elseif (is_array($tenantLabels)) {
|
||||
$tenantList = array_values(array_filter(array_map('trim', $tenantLabels)));
|
||||
} else {
|
||||
$tenantList = [];
|
||||
}
|
||||
$roleLabels = $row['role_labels'] ?? [];
|
||||
if (is_string($roleLabels)) {
|
||||
$roleList = $roleLabels !== ''
|
||||
? array_values(array_filter(array_map('trim', explode('||', $roleLabels))))
|
||||
: [];
|
||||
} elseif (is_array($roleLabels)) {
|
||||
$roleList = array_values(array_filter(array_map('trim', $roleLabels)));
|
||||
} else {
|
||||
$roleList = [];
|
||||
}
|
||||
$departmentLabels = $row['department_labels'] ?? [];
|
||||
if (is_string($departmentLabels)) {
|
||||
$departmentList = $departmentLabels !== ''
|
||||
? array_values(array_filter(array_map('trim', explode('||', $departmentLabels))))
|
||||
: [];
|
||||
} elseif (is_array($departmentLabels)) {
|
||||
$departmentList = array_values(array_filter(array_map('trim', $departmentLabels)));
|
||||
} else {
|
||||
$departmentList = [];
|
||||
}
|
||||
$primaryTenantLabel = (string) ($row['primary_tenant_label'] ?? '');
|
||||
if ($primaryTenantLabel === '' && count($tenantList) === 1) {
|
||||
$primaryTenantLabel = (string) $tenantList[0];
|
||||
}
|
||||
$uuid = (string) ($row['uuid'] ?? '');
|
||||
$rows[] = [
|
||||
'id' => $row['id'] ?? null,
|
||||
'uuid' => $uuid,
|
||||
'first_name' => $row['first_name'] ?? '',
|
||||
'last_name' => $row['last_name'] ?? '',
|
||||
'email' => $row['email'] ?? '',
|
||||
'phone' => $row['phone'] ?? '',
|
||||
'mobile' => $row['mobile'] ?? '',
|
||||
'short_dial' => $row['short_dial'] ?? '',
|
||||
'tenants' => [
|
||||
'items' => $tenantList,
|
||||
'primary' => $primaryTenantLabel,
|
||||
],
|
||||
'departments' => $departmentList,
|
||||
'roles' => $roleList,
|
||||
'created' => dt($row['created'] ?? ''),
|
||||
'modified' => dt($row['modified'] ?? ''),
|
||||
'active' => (int) ($row['active'] ?? 0),
|
||||
'has_avatar' => $uuid !== '' && UserAvatarService::hasAvatar($uuid),
|
||||
];
|
||||
}
|
||||
|
||||
Router::json([
|
||||
'data' => $rows,
|
||||
'total' => $result['total'] ?? 0,
|
||||
]);
|
||||
53
pages/admin/users/deactivate($id).php
Normal file
53
pages/admin/users/deactivate($id).php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\UserService;
|
||||
use MintyPHP\Http\Request;
|
||||
use MintyPHP\Service\TenantScopeService;
|
||||
|
||||
Guard::requireLogin();
|
||||
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
|
||||
Router::redirect('admin/users');
|
||||
return;
|
||||
}
|
||||
|
||||
$uuid = trim((string) ($id ?? ''));
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
$user = $uuid !== '' ? UserService::findByUuid($uuid) : null;
|
||||
$userId = (int) ($user['id'] ?? 0);
|
||||
if ($userId > 0 && $currentUserId !== $userId && !TenantScopeService::canAccess('users', $userId, $currentUserId)) {
|
||||
if (Request::wantsJson()) {
|
||||
http_response_code(403);
|
||||
Router::json(['error' => 'permission_denied']);
|
||||
return;
|
||||
}
|
||||
Router::redirect('error/forbidden');
|
||||
return;
|
||||
}
|
||||
$result = UserService::setActiveByUuid($uuid, false, $currentUserId);
|
||||
if (!($result['ok'] ?? false)) {
|
||||
if (($result['error'] ?? '') === 'self_deactivate') {
|
||||
Flash::error('You cannot deactivate your own account', 'admin/users', 'user_self_deactivate');
|
||||
if (Request::wantsJson()) {
|
||||
http_response_code(400);
|
||||
Router::json(['error' => 'self_deactivate']);
|
||||
return;
|
||||
}
|
||||
} elseif (Request::wantsJson()) {
|
||||
http_response_code((int) ($result['status'] ?? 404));
|
||||
Router::json(['error' => 'not_found']);
|
||||
return;
|
||||
}
|
||||
Router::redirect('admin/users');
|
||||
return;
|
||||
}
|
||||
|
||||
if (Request::wantsJson()) {
|
||||
http_response_code(204);
|
||||
return;
|
||||
}
|
||||
|
||||
Router::redirect('admin/users');
|
||||
61
pages/admin/users/delete($id).php
Normal file
61
pages/admin/users/delete($id).php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\UserService;
|
||||
use MintyPHP\Service\PermissionService;
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Http\Request;
|
||||
use MintyPHP\Service\TenantScopeService;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermissionOrForbidden(PermissionService::USERS_DELETE);
|
||||
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
|
||||
Router::redirect('admin/users');
|
||||
return;
|
||||
}
|
||||
|
||||
$uuid = trim((string) ($id ?? ''));
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
$user = $uuid !== '' ? UserService::findByUuid($uuid) : null;
|
||||
$userId = (int) ($user['id'] ?? 0);
|
||||
if ($userId > 0 && $currentUserId !== $userId && !TenantScopeService::canAccess('users', $userId, $currentUserId)) {
|
||||
if (Request::wantsJson()) {
|
||||
http_response_code(403);
|
||||
Router::json(['error' => 'permission_denied']);
|
||||
return;
|
||||
}
|
||||
Router::redirect('error/forbidden');
|
||||
return;
|
||||
}
|
||||
$result = UserService::deleteByUuid($uuid, $currentUserId);
|
||||
|
||||
if (!($result['ok'] ?? false)) {
|
||||
if (($result['error'] ?? '') === 'self_delete') {
|
||||
Flash::error('You cannot delete your own account', 'admin/users', 'user_self_delete');
|
||||
if (Request::wantsJson()) {
|
||||
http_response_code(400);
|
||||
Router::json(['error' => 'self_delete']);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
Flash::error('User not found', 'admin/users', 'user_not_found');
|
||||
if (Request::wantsJson()) {
|
||||
http_response_code(404);
|
||||
Router::json(['error' => 'not_found']);
|
||||
return;
|
||||
}
|
||||
}
|
||||
Router::redirect('admin/users');
|
||||
return;
|
||||
}
|
||||
|
||||
if (Request::wantsJson()) {
|
||||
http_response_code(204);
|
||||
Router::json([]);
|
||||
return;
|
||||
}
|
||||
|
||||
Flash::success('User deleted', 'admin/users', 'user_deleted');
|
||||
Router::redirect('admin/users');
|
||||
160
pages/admin/users/edit($id).php
Normal file
160
pages/admin/users/edit($id).php
Normal file
@@ -0,0 +1,160 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Buffer;
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\I18n;
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Repository\UserRoleRepository;
|
||||
use MintyPHP\Repository\UserTenantRepository;
|
||||
use MintyPHP\Repository\UserDepartmentRepository;
|
||||
use MintyPHP\Repository\RolePermissionRepository;
|
||||
use MintyPHP\Service\AuthService;
|
||||
use MintyPHP\Service\DepartmentService;
|
||||
use MintyPHP\Service\RoleService;
|
||||
use MintyPHP\Service\TenantService;
|
||||
use MintyPHP\Service\UserService;
|
||||
use MintyPHP\Service\PermissionService;
|
||||
use MintyPHP\Service\TenantScopeService;
|
||||
|
||||
Guard::requireLogin();
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
if ($currentUserId > 0) {
|
||||
PermissionService::getUserPermissions($currentUserId, true);
|
||||
}
|
||||
|
||||
$uuid = trim((string) ($id ?? ''));
|
||||
$user = $uuid !== '' ? UserService::findByUuid($uuid) : null;
|
||||
if (!$user) {
|
||||
Flash::error('User not found', 'admin/users', 'user_not_found');
|
||||
Router::redirect('admin/users');
|
||||
}
|
||||
|
||||
$userId = (int) ($user['id'] ?? 0);
|
||||
$canViewUsers = PermissionService::userHas($currentUserId, PermissionService::USERS_VIEW);
|
||||
$canUpdateUser = PermissionService::userHas($currentUserId, PermissionService::USERS_UPDATE);
|
||||
$canUpdateSelf = PermissionService::userHas($currentUserId, PermissionService::USERS_SELF_UPDATE);
|
||||
$canUpdateAssignments = PermissionService::userHas($currentUserId, PermissionService::USERS_UPDATE_ASSIGNMENTS);
|
||||
$isOwnAccount = $currentUserId === $userId;
|
||||
$canEditUser = $isOwnAccount ? ($canUpdateUser || $canUpdateSelf) : $canUpdateUser;
|
||||
$canEditAssignments = $canEditUser && $canUpdateAssignments;
|
||||
if (!$isOwnAccount && !TenantScopeService::canAccess('users', $userId, $currentUserId)) {
|
||||
Router::redirect('error/forbidden');
|
||||
return;
|
||||
}
|
||||
if (!$canViewUsers && !$canEditUser) {
|
||||
Router::redirect('error/forbidden');
|
||||
return;
|
||||
}
|
||||
$creatorId = (int) ($user['created_by'] ?? 0);
|
||||
if ($creatorId > 0) {
|
||||
$creator = UserService::findById($creatorId);
|
||||
if ($creator) {
|
||||
$creatorName = trim(($creator['first_name'] ?? '') . ' ' . ($creator['last_name'] ?? ''));
|
||||
$user['created_by_label'] = $creatorName !== '' ? $creatorName : ($creator['email'] ?? '');
|
||||
$user['created_by_uuid'] = $creator['uuid'] ?? null;
|
||||
}
|
||||
}
|
||||
$modifierId = (int) ($user['modified_by'] ?? 0);
|
||||
if ($modifierId > 0) {
|
||||
$modifier = UserService::findById($modifierId);
|
||||
if ($modifier) {
|
||||
$modifierName = trim(($modifier['first_name'] ?? '') . ' ' . ($modifier['last_name'] ?? ''));
|
||||
$user['modified_by_label'] = $modifierName !== '' ? $modifierName : ($modifier['email'] ?? '');
|
||||
$user['modified_by_uuid'] = $modifier['uuid'] ?? null;
|
||||
}
|
||||
}
|
||||
$activeChangedById = (int) ($user['active_changed_by'] ?? 0);
|
||||
if ($activeChangedById > 0) {
|
||||
$activeChanger = UserService::findById($activeChangedById);
|
||||
if ($activeChanger) {
|
||||
$activeChangerName = trim(($activeChanger['first_name'] ?? '') . ' ' . ($activeChanger['last_name'] ?? ''));
|
||||
$user['active_changed_by_label'] = $activeChangerName !== '' ? $activeChangerName : ($activeChanger['email'] ?? '');
|
||||
$user['active_changed_by_uuid'] = $activeChanger['uuid'] ?? null;
|
||||
}
|
||||
}
|
||||
$errors = [];
|
||||
$form = $user;
|
||||
// Users with tenants.update permission can see ALL tenants (to assign new tenants to users)
|
||||
$canManageTenants = PermissionService::userHas($currentUserId, PermissionService::TENANTS_UPDATE);
|
||||
$allowedTenantIds = $canManageTenants ? null : TenantScopeService::getUserTenantIds($currentUserId);
|
||||
|
||||
$tenants = TenantService::list();
|
||||
if ($allowedTenantIds) {
|
||||
$tenants = array_values(array_filter($tenants, static function (array $tenant) use ($allowedTenantIds): bool {
|
||||
$tenantId = (int) ($tenant['id'] ?? 0);
|
||||
return $tenantId > 0 && in_array($tenantId, $allowedTenantIds, true);
|
||||
}));
|
||||
} elseif (!$canManageTenants && TenantScopeService::isStrict()) {
|
||||
$tenants = [];
|
||||
}
|
||||
$selectedTenantIds = UserTenantRepository::listTenantIdsByUserId($userId);
|
||||
if ($allowedTenantIds) {
|
||||
$selectedTenantIds = array_values(array_intersect($selectedTenantIds, $allowedTenantIds));
|
||||
}
|
||||
$primaryTenantId = (int) ($user['primary_tenant_id'] ?? 0);
|
||||
if (!$primaryTenantId && count($selectedTenantIds) === 1) {
|
||||
$primaryTenantId = (int) $selectedTenantIds[0];
|
||||
$user['primary_tenant_id'] = $primaryTenantId;
|
||||
}
|
||||
$roles = RoleService::list();
|
||||
$selectedRoleIds = UserRoleRepository::listRoleIdsByUserId($userId);
|
||||
$permissionRows = RolePermissionRepository::listPermissionsWithRolesByRoleIds($selectedRoleIds);
|
||||
$departments = DepartmentService::listForUserAssignments($selectedTenantIds, []);
|
||||
$selectedDepartmentIds = UserDepartmentRepository::listDepartmentIdsByUserId($userId);
|
||||
if ($selectedDepartmentIds) {
|
||||
$departments = DepartmentService::listForUserAssignments($selectedTenantIds, $selectedDepartmentIds);
|
||||
}
|
||||
// Keep initial $isOwnAccount/$canEditUser values for view permissions.
|
||||
|
||||
if (isset($_POST['email'])) {
|
||||
if (!$canEditUser) {
|
||||
Router::redirect('error/forbidden');
|
||||
return;
|
||||
}
|
||||
$result = UserService::updateFromAdmin($userId, $_POST, $currentUserId);
|
||||
$form = $result['form'] ?? $form;
|
||||
$errors = $result['errors'] ?? [];
|
||||
$tenantIds = UserService::normalizeIdInput($_POST['tenant_ids'] ?? []);
|
||||
if ($allowedTenantIds) {
|
||||
$tenantIds = array_values(array_intersect($tenantIds, $allowedTenantIds));
|
||||
} elseif (!$canManageTenants && TenantScopeService::isStrict()) {
|
||||
$tenantIds = [];
|
||||
}
|
||||
$selectedTenantIds = $tenantIds;
|
||||
$primaryTenantId = (int) ($_POST['primary_tenant_id'] ?? 0);
|
||||
$selectedRoleIds = UserService::normalizeIdInput($_POST['role_ids'] ?? []);
|
||||
$selectedDepartmentIds = UserService::normalizeIdInput($_POST['department_ids'] ?? []);
|
||||
$departments = DepartmentService::listForUserAssignments($selectedTenantIds, $selectedDepartmentIds);
|
||||
|
||||
if ($result['ok'] ?? false) {
|
||||
if ($canEditAssignments) {
|
||||
UserService::syncTenants($userId, $tenantIds);
|
||||
UserService::syncRoles($userId, $selectedRoleIds);
|
||||
UserService::syncDepartments($userId, $selectedDepartmentIds);
|
||||
|
||||
// Update session if editing own account (tenant assignments changed)
|
||||
if ($currentUserId === $userId) {
|
||||
AuthService::loadTenantDataIntoSession($userId);
|
||||
}
|
||||
}
|
||||
if ($currentUserId === $userId && isset($form['theme'])) {
|
||||
$_SESSION['user']['theme'] = $form['theme'] ?: 'light';
|
||||
}
|
||||
if ($currentUserId === $userId && isset($form['locale'])) {
|
||||
$_SESSION['user']['locale'] = $form['locale'];
|
||||
I18n::$locale = $form['locale'];
|
||||
}
|
||||
$action = (string) ($_POST['action'] ?? 'save');
|
||||
if ($action === 'save_close') {
|
||||
Flash::success('User updated', 'admin/users', 'user_updated');
|
||||
Router::redirect('admin/users');
|
||||
} else {
|
||||
Flash::success('User updated', "admin/users/edit/{$uuid}", 'user_updated');
|
||||
Router::redirect("admin/users/edit/{$uuid}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Buffer::set('title', $isOwnAccount ? t('My account') : t('Edit user'));
|
||||
312
pages/admin/users/edit(default).phtml
Normal file
312
pages/admin/users/edit(default).phtml
Normal file
@@ -0,0 +1,312 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @var array<int, string> $errors
|
||||
* @var array $form
|
||||
* @var array $user
|
||||
*/
|
||||
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Session;
|
||||
use MintyPHP\Service\UserAvatarService;
|
||||
|
||||
$values = $form ?? $user ?? [];
|
||||
$avatarUuid = (string) ($values['uuid'] ?? '');
|
||||
$hasAvatar = $avatarUuid !== '' && UserAvatarService::hasAvatar($avatarUuid);
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
$isOwnAccount = $currentUserId > 0 && $currentUserId === (int) ($values['id'] ?? 0);
|
||||
$titleText = $isOwnAccount ? t('My account') : t('Edit user');
|
||||
$canViewUsers = can('users.view');
|
||||
$hideNavigation = $isOwnAccount && !$canViewUsers;
|
||||
|
||||
?>
|
||||
|
||||
<div class="app-details-container">
|
||||
<section>
|
||||
<?php if (!$hideNavigation): ?>
|
||||
<div class="app-breadcrumb">
|
||||
<a href="/admin">Startseite</a><i class="bi bi-chevron-right"></i><a
|
||||
href="/admin/users"><?php e(t('Users')); ?></a><i class="bi bi-chevron-right"></i><?php e($titleText); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="app-details-titlebar">
|
||||
<h1>
|
||||
<?php if (!$hideNavigation): ?>
|
||||
<a href="admin/users" title="<?php e(t('Cancel')); ?>"><i class="bi bi-arrow-left"></i></a>
|
||||
<?php endif; ?>
|
||||
<?php e($titleText); ?>
|
||||
</h1>
|
||||
<div class="app-details-titlebar-actions">
|
||||
<?php if (can('users.delete')): ?>
|
||||
<details class="dropdown">
|
||||
<summary role="button" class="outline secondary"><i class="bi bi-three-dots"></i></summary>
|
||||
<ul dir="rtl">
|
||||
<li>
|
||||
<?php if (!empty($canEditUser)): ?>
|
||||
<form method="post" action="admin/users/send-access/<?php e($values['uuid'] ?? ''); ?>"
|
||||
onsubmit="return confirm('<?php e(t('Send access email to this user?')); ?>');">
|
||||
<?php Session::getCsrfInput(); ?>
|
||||
<button type="submit" class="transparent">
|
||||
<?php e(t('Send access')); ?>
|
||||
</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</li>
|
||||
<li>
|
||||
<?php if (!empty($canEditUser)): ?>
|
||||
<form method="post" action="admin/users/forget-tokens/<?php e($values['uuid'] ?? ''); ?>"
|
||||
onsubmit="return confirm('<?php e(t('Clear all login tokens for this user?')); ?>');">
|
||||
<?php Session::getCsrfInput(); ?>
|
||||
<button type="submit" class="transparent">
|
||||
<?php e(t('Clear login tokens')); ?>
|
||||
</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</li>
|
||||
<li>
|
||||
<form method="post" action="admin/users/delete/<?php e($values['uuid'] ?? ''); ?>"
|
||||
onsubmit="return confirm('<?php e(t('Delete this user?')); ?>');">
|
||||
<?php Session::getCsrfInput(); ?>
|
||||
<button type="submit" class="transparent">
|
||||
<?php e(t('Delete')); ?>
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($canEditUser)): ?>
|
||||
<button type="submit" form="user-form" name="action" value="save" class="secondary outline">
|
||||
<?php e(t('Save')); ?>
|
||||
</button>
|
||||
<?php if (!$hideNavigation): ?>
|
||||
<button type="submit" form="user-form" name="action" value="save_close" class="primary">
|
||||
<?php e(t('Save & close')); ?>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($errors)): ?>
|
||||
<div class="app-details-errors">
|
||||
<div class="notice" data-variant="error">
|
||||
<ul>
|
||||
<?php foreach ($errors as $error): ?>
|
||||
<li><?php e($error); ?></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php
|
||||
$passwordRequired = false;
|
||||
$passwordLabel = t('New password (optional)');
|
||||
$passwordLegend = t('Password');
|
||||
$passwordConfirmLabel = t('Password (again)');
|
||||
$showTenants = !empty($canEditAssignments);
|
||||
$tenants = $tenants ?? [];
|
||||
$selectedTenantIds = $selectedTenantIds ?? [];
|
||||
$showRoles = !empty($canEditAssignments);
|
||||
$roles = $roles ?? [];
|
||||
$selectedRoleIds = $selectedRoleIds ?? [];
|
||||
$showDepartments = !empty($canEditAssignments);
|
||||
$departments = $departments ?? [];
|
||||
$selectedDepartmentIds = $selectedDepartmentIds ?? [];
|
||||
$isReadOnly = !(!empty($canEditUser));
|
||||
$canEditAssignments = $canEditAssignments ?? false;
|
||||
$permissionRows = $permissionRows ?? [];
|
||||
$showPermissions = !empty($permissionRows) && can('permissions.view');
|
||||
require __DIR__ . '/_form.phtml';
|
||||
?>
|
||||
</section>
|
||||
<aside id="app-details-aside-section">
|
||||
<div class="app-details-aside-section">
|
||||
<div class="user-avatar-block avatar-round">
|
||||
<?php if ($hasAvatar): ?>
|
||||
<a data-fslightbox="user-avatar" href="admin/users/avatar-file?uuid=<?php e($avatarUuid); ?>&size=256">
|
||||
<img class="user-avatar-image" src="admin/users/avatar-file?uuid=<?php e($avatarUuid); ?>&size=128"
|
||||
alt="<?php e(t('Profile image')); ?>">
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<?php
|
||||
$initials = strtoupper(substr((string) ($values['first_name'] ?? ''), 0, 1) . substr((string) ($values['last_name'] ?? ''), 0, 1));
|
||||
?>
|
||||
<div class="user-avatar-placeholder"><?php e($initials ?: '?'); ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<hgroup>
|
||||
<h2><?php e($values['first_name'] ?? ''); ?> <?php e($values['last_name'] ?? ''); ?></h2>
|
||||
<p><?php e($values['email'] ?? ''); ?></p>
|
||||
</hgroup>
|
||||
<hr>
|
||||
<?php if (!empty($canEditUser)): ?>
|
||||
<details name="user-avatar">
|
||||
<summary>
|
||||
<?php e(t('Upload image')); ?>
|
||||
</summary>
|
||||
<hr>
|
||||
<form class="user-avatar-form" method="post" action="admin/users/avatar/<?php e($avatarUuid); ?>"
|
||||
enctype="multipart/form-data">
|
||||
<label class="user-avatar-upload">
|
||||
<input type="file" name="avatar" accept="image/*">
|
||||
</label>
|
||||
<div class="grid">
|
||||
<button type="submit" class="primary">
|
||||
<?php e(t('Save')); ?>
|
||||
</button>
|
||||
<?php if ($hasAvatar): ?>
|
||||
<button data-tooltip-pos="top" data-tooltip="<?php e(t('Remove image')); ?>" type="submit" class="danger" formaction="admin/users/avatar-delete/<?php e($avatarUuid); ?>"
|
||||
formmethod="post">
|
||||
<i class="bi bi-trash3-fill"></i>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php Session::getCsrfInput(); ?>
|
||||
</form>
|
||||
</details>
|
||||
<?php endif; ?>
|
||||
<hr>
|
||||
<details name="user-meta" open>
|
||||
<summary><?php e(t('Status & meta')); ?></summary>
|
||||
<hr>
|
||||
<div class="grid">
|
||||
<div>
|
||||
<small><?php e(t('ID')); ?></small>
|
||||
<p>
|
||||
<span class="badge" data-variant="neutral" data-copy="true"
|
||||
data-copy-value="<?php e($values['id'] ?? ''); ?>">
|
||||
<?php e($values['id'] ?? '-'); ?>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<small><?php e(t('Status')); ?></small>
|
||||
<p>
|
||||
<?php if (!empty($values['active'])): ?>
|
||||
<span class="badge" data-variant="success"><?php e(t('Active')); ?></span>
|
||||
<?php else: ?>
|
||||
<span class="badge" data-variant="danger"><?php e(t('Inactive')); ?></span>
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<small><?php e(t('Email')); ?></small>
|
||||
<p>
|
||||
<?php if (!empty($values['email_verified_at'])): ?>
|
||||
<span class="badge" data-variant="success"><?php e(t('Verified')); ?> <?php e(dt($values['email_verified_at'])); ?></span>
|
||||
<?php else: ?>
|
||||
<span class="badge" data-variant="warning"><?php e(t('Not verified')); ?></span>
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<small>
|
||||
<?php e(t('UUID')); ?>
|
||||
</small>
|
||||
<p>
|
||||
<span class="badge" data-variant="neutral" data-copy="true"
|
||||
data-copy-value="<?php e($values['uuid'] ?? ''); ?>">
|
||||
<?php
|
||||
$uuidValue = (string) ($values['uuid'] ?? '');
|
||||
$uuidDisplay = $uuidValue !== '' ? substr($uuidValue, 0, 10) : '-';
|
||||
e($uuidDisplay);
|
||||
?>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</details>
|
||||
<hr>
|
||||
<?php
|
||||
$createdByLabel = $values['created_by_label'] ?? '';
|
||||
$createdById = $values['created_by'] ?? null;
|
||||
$createdByUuid = $values['created_by_uuid'] ?? null;
|
||||
$modifiedByLabel = $values['modified_by_label'] ?? '';
|
||||
$modifiedById = $values['modified_by'] ?? null;
|
||||
$modifiedByUuid = $values['modified_by_uuid'] ?? null;
|
||||
$activeChangedByLabel = $values['active_changed_by_label'] ?? '';
|
||||
$activeChangedById = $values['active_changed_by'] ?? null;
|
||||
$activeChangedByUuid = $values['active_changed_by_uuid'] ?? null;
|
||||
?>
|
||||
<details name="user-audit">
|
||||
<summary><?php e(t('Audit')); ?></summary>
|
||||
<hr>
|
||||
<div class="grid">
|
||||
<div>
|
||||
<small><?php e(t('Created')); ?></small>
|
||||
<p><?php e(dt($values['created'] ?? '') ?: '-'); ?></p>
|
||||
</div>
|
||||
<div>
|
||||
<small>
|
||||
<?php e(t('Created by')); ?>
|
||||
</small>
|
||||
<p>
|
||||
<?php if ($createdByLabel !== ''): ?>
|
||||
<?php if ($createdByUuid): ?>
|
||||
<a href="admin/users/edit/<?php e($createdByUuid); ?>"><?php e($createdByLabel); ?></a>
|
||||
<?php else: ?>
|
||||
<?php e($createdByLabel); ?>
|
||||
<?php endif; ?>
|
||||
<?php elseif ($createdById): ?>
|
||||
<?php e('#' . $createdById); ?>
|
||||
<?php else: ?>
|
||||
-
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid">
|
||||
<div>
|
||||
<small><?php e(t('Modified')); ?></small>
|
||||
<p><?php e(dt($values['modified'] ?? '') ?: '-'); ?></p>
|
||||
</div>
|
||||
<div>
|
||||
<small>
|
||||
<?php e(t('Modified by')); ?>
|
||||
</small>
|
||||
<p>
|
||||
<?php if ($modifiedByLabel !== ''): ?>
|
||||
<?php if ($modifiedByUuid): ?>
|
||||
<a href="admin/users/edit/<?php e($modifiedByUuid); ?>"><?php e($modifiedByLabel); ?></a>
|
||||
<?php else: ?>
|
||||
<?php e($modifiedByLabel); ?>
|
||||
<?php endif; ?>
|
||||
<?php elseif ($modifiedById): ?>
|
||||
<?php e('#' . $modifiedById); ?>
|
||||
<?php else: ?>
|
||||
-
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid">
|
||||
<div>
|
||||
<small><?php e(t('Status changed')); ?></small>
|
||||
<p><?php e(dt($values['active_changed_at'] ?? '') ?: '-'); ?></p>
|
||||
</div>
|
||||
<div>
|
||||
<small>
|
||||
<?php e(t('Status changed by')); ?>
|
||||
</small>
|
||||
<p>
|
||||
<?php if ($activeChangedByLabel !== ''): ?>
|
||||
<?php if ($activeChangedByUuid): ?>
|
||||
<a href="admin/users/edit/<?php e($activeChangedByUuid); ?>"><?php e($activeChangedByLabel); ?></a>
|
||||
<?php else: ?>
|
||||
<?php e($activeChangedByLabel); ?>
|
||||
<?php endif; ?>
|
||||
<?php elseif ($activeChangedById): ?>
|
||||
<?php e('#' . $activeChangedById); ?>
|
||||
<?php else: ?>
|
||||
-
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
49
pages/admin/users/export().php
Normal file
49
pages/admin/users/export().php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Service\UserService;
|
||||
use MintyPHP\Support\Guard;
|
||||
|
||||
Guard::requireLogin();
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
|
||||
$search = trim((string) ($_GET['search'] ?? ''));
|
||||
$order = (string) ($_GET['order'] ?? 'id');
|
||||
$dir = strtolower((string) ($_GET['dir'] ?? 'desc')) === 'asc' ? 'asc' : 'desc';
|
||||
$active = $_GET['active'] ?? 'all';
|
||||
$createdFrom = trim((string) ($_GET['created_from'] ?? ''));
|
||||
$createdTo = trim((string) ($_GET['created_to'] ?? ''));
|
||||
$tenant = trim((string) ($_GET['tenant'] ?? ''));
|
||||
$roles = $_GET['roles'] ?? '';
|
||||
$departments = $_GET['departments'] ?? '';
|
||||
|
||||
$allowedOrder = ['id', 'uuid', 'first_name', 'last_name', 'email', 'locale', 'theme', 'created', 'modified', 'active'];
|
||||
if (!in_array($order, $allowedOrder, true)) {
|
||||
$order = 'id';
|
||||
}
|
||||
|
||||
$limit = (int) ($_GET['limit'] ?? 5000);
|
||||
if ($limit < 1) {
|
||||
$limit = 5000;
|
||||
}
|
||||
if ($limit > 5000) {
|
||||
$limit = 5000;
|
||||
}
|
||||
|
||||
$result = UserService::listPaged([
|
||||
'limit' => $limit,
|
||||
'offset' => 0,
|
||||
'search' => $search,
|
||||
'order' => $order,
|
||||
'dir' => $dir,
|
||||
'active' => $active,
|
||||
'created_from' => $createdFrom,
|
||||
'created_to' => $createdTo,
|
||||
'tenant' => $tenant,
|
||||
'roles' => $roles,
|
||||
'departments' => $departments,
|
||||
'tenantUserId' => $currentUserId,
|
||||
]);
|
||||
|
||||
$exportRows = $result['rows'] ?? [];
|
||||
$exportFilename = 'users-' . date('Ymd-His') . '.csv';
|
||||
75
pages/admin/users/export(none).phtml
Normal file
75
pages/admin/users/export(none).phtml
Normal file
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
$filename = $exportFilename ?? ('users-' . date('Ymd-His') . '.csv');
|
||||
header('Content-Type: text/csv; charset=utf-8');
|
||||
header('Content-Disposition: attachment; filename="' . $filename . '"');
|
||||
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
|
||||
|
||||
$escapeValue = static function ($value, bool $allowSignedNumeric = false) {
|
||||
if ($value === null) {
|
||||
return '';
|
||||
}
|
||||
$string = (string) $value;
|
||||
if ($string !== '' && in_array($string[0], ['=', '@'], true)) {
|
||||
return "'" . $string;
|
||||
}
|
||||
if ($string !== '' && in_array($string[0], ['+', '-'], true)) {
|
||||
if ($allowSignedNumeric && preg_match('/^[+-]?[0-9\\s().-]+$/', $string)) {
|
||||
return $string;
|
||||
}
|
||||
return "'" . $string;
|
||||
}
|
||||
return $string;
|
||||
};
|
||||
|
||||
$formatTimestamp = static function ($value) {
|
||||
if ($value === null || $value === '') {
|
||||
return '';
|
||||
}
|
||||
return dt($value, 'Y-m-d H:i:s');
|
||||
};
|
||||
$joinLabels = static function ($value) {
|
||||
if (is_array($value)) {
|
||||
return implode(' | ', array_map('strval', $value));
|
||||
}
|
||||
if (is_string($value)) {
|
||||
$value = trim($value);
|
||||
if ($value === '') {
|
||||
return '';
|
||||
}
|
||||
return str_replace('||', ' | ', $value);
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
$handle = fopen('php://output', 'w');
|
||||
if ($handle === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
fputcsv($handle, ['id', 'uuid', 'first_name', 'last_name', 'email', 'phone', 'mobile', 'short_dial', 'locale', 'theme', 'active', 'tenants', 'departments', 'roles', 'created_by', 'modified_by', 'created', 'modified'], ',', '"', '\\');
|
||||
|
||||
foreach (($exportRows ?? []) as $row) {
|
||||
fputcsv($handle, [
|
||||
$escapeValue($row['id'] ?? ''),
|
||||
$escapeValue($row['uuid'] ?? ''),
|
||||
$escapeValue($row['first_name'] ?? ''),
|
||||
$escapeValue($row['last_name'] ?? ''),
|
||||
$escapeValue($row['email'] ?? ''),
|
||||
$escapeValue($row['phone'] ?? '', true),
|
||||
$escapeValue($row['mobile'] ?? '', true),
|
||||
$escapeValue($row['short_dial'] ?? '', true),
|
||||
$escapeValue($row['locale'] ?? ''),
|
||||
$escapeValue($row['theme'] ?? ''),
|
||||
(int) ($row['active'] ?? 0),
|
||||
$escapeValue($joinLabels($row['tenant_labels'] ?? [])),
|
||||
$escapeValue($joinLabels($row['department_labels'] ?? [])),
|
||||
$escapeValue($joinLabels($row['role_labels'] ?? [])),
|
||||
$escapeValue($row['created_by'] ?? ''),
|
||||
$escapeValue($row['modified_by'] ?? ''),
|
||||
$formatTimestamp($row['created'] ?? ''),
|
||||
$formatTimestamp($row['modified'] ?? ''),
|
||||
], ',', '"', '\\');
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
39
pages/admin/users/forget-tokens($id).php
Normal file
39
pages/admin/users/forget-tokens($id).php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\PermissionService;
|
||||
use MintyPHP\Service\RememberMeService;
|
||||
use MintyPHP\Service\TenantScopeService;
|
||||
use MintyPHP\Service\UserService;
|
||||
|
||||
Guard::requireLogin();
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
$canUpdateUser = PermissionService::userHas($currentUserId, PermissionService::USERS_UPDATE);
|
||||
$canUpdateSelf = PermissionService::userHas($currentUserId, PermissionService::USERS_SELF_UPDATE);
|
||||
|
||||
$uuid = trim((string) ($id ?? ''));
|
||||
$user = $uuid !== '' ? UserService::findByUuid($uuid) : null;
|
||||
if (!$user) {
|
||||
Flash::error(t('User not found'), 'admin/users', 'user_not_found');
|
||||
Router::redirect('admin/users');
|
||||
}
|
||||
|
||||
$userId = (int) ($user['id'] ?? 0);
|
||||
$isOwnAccount = $currentUserId > 0 && $currentUserId === $userId;
|
||||
$canEditUser = $isOwnAccount ? ($canUpdateUser || $canUpdateSelf) : $canUpdateUser;
|
||||
|
||||
if (!$canEditUser) {
|
||||
Router::redirect('error/forbidden');
|
||||
return;
|
||||
}
|
||||
if (!$isOwnAccount && !TenantScopeService::canAccess('users', $userId, $currentUserId)) {
|
||||
Router::redirect('error/forbidden');
|
||||
return;
|
||||
}
|
||||
|
||||
RememberMeService::forgetAllForUser($userId);
|
||||
Flash::success(t('Login tokens cleared'), "admin/users/edit/{$uuid}", 'tokens_cleared');
|
||||
Router::redirect("admin/users/edit/{$uuid}");
|
||||
48
pages/admin/users/index().php
Normal file
48
pages/admin/users/index().php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Buffer;
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Session;
|
||||
use MintyPHP\Service\DepartmentService;
|
||||
use MintyPHP\Service\RoleService;
|
||||
use MintyPHP\Service\TenantService;
|
||||
use MintyPHP\Service\PermissionService;
|
||||
use MintyPHP\Service\TenantScopeService;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermissionOrForbidden(PermissionService::USERS_VIEW);
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
if ($currentUserId > 0) {
|
||||
PermissionService::getUserPermissions($currentUserId);
|
||||
}
|
||||
$allowedTenantIds = TenantScopeService::getUserTenantIds($currentUserId);
|
||||
|
||||
Buffer::set('title', t('Users'));
|
||||
Buffer::set('grid_lang', json_encode(gridLang(), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||
$csrfKey = Session::$csrfSessionKey;
|
||||
$csrfToken = $_SESSION[$csrfKey] ?? '';
|
||||
Buffer::set(
|
||||
'grid_csrf',
|
||||
json_encode(['key' => $csrfKey, 'token' => $csrfToken], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
|
||||
);
|
||||
|
||||
$tenants = TenantService::list();
|
||||
if ($allowedTenantIds) {
|
||||
$tenants = array_values(array_filter($tenants, static function (array $tenant) use ($allowedTenantIds): bool {
|
||||
$tenantId = (int) ($tenant['id'] ?? 0);
|
||||
return $tenantId > 0 && in_array($tenantId, $allowedTenantIds, true);
|
||||
}));
|
||||
} elseif (TenantScopeService::isStrict()) {
|
||||
$tenants = [];
|
||||
}
|
||||
sortByDescription($tenants);
|
||||
|
||||
$roles = RoleService::list();
|
||||
sortByDescription($roles);
|
||||
|
||||
$departments = $allowedTenantIds ? DepartmentService::listByTenantIds($allowedTenantIds) : [];
|
||||
if (!$allowedTenantIds && !TenantScopeService::isStrict()) {
|
||||
$departments = DepartmentService::list();
|
||||
}
|
||||
sortByDescription($departments);
|
||||
389
pages/admin/users/index(default).phtml
Normal file
389
pages/admin/users/index(default).phtml
Normal file
@@ -0,0 +1,389 @@
|
||||
<?php
|
||||
use MintyPHP\Router;
|
||||
|
||||
$activeTenant = trim((string) ($_GET['tenant'] ?? ''));
|
||||
$activeRoles = array_filter(array_map('trim', explode(',', (string) ($_GET['roles'] ?? ''))));
|
||||
$activeDepartments = array_filter(array_map('trim', explode(',', (string) ($_GET['departments'] ?? ''))));
|
||||
$canDeleteUsers = can('users.delete');
|
||||
$canUpdateUsers = can('users.update');
|
||||
$canUpdateSelf = can('users.self_update');
|
||||
$currentUserUuid = (string) ($_SESSION['user']['uuid'] ?? '');
|
||||
$showTenantTabs = isset($tenants) && is_array($tenants) && count($tenants) > 1;
|
||||
?>
|
||||
<div class="app-breadcrumb">
|
||||
<a href="/admin">Startseite</a><i class="bi bi-chevron-right"></i><?php e(t('Users')); ?>
|
||||
</div>
|
||||
<div class="app-list-titlebar">
|
||||
<h1><?php e(t('Users')); ?></h1>
|
||||
<div class="app-list-titlebar-actions">
|
||||
<button type="button" class="secondary outline" data-users-bulk="activate" hidden>
|
||||
<i class="bi bi-check-circle"></i> <?php e(t('Activate')); ?>
|
||||
</button>
|
||||
<button type="button" class="secondary outline" data-users-bulk="deactivate" hidden>
|
||||
<i class="bi bi-ban"></i> <?php e(t('Deactivate')); ?>
|
||||
</button>
|
||||
<?php if ($canUpdateUsers): ?>
|
||||
<button type="button" class="secondary outline" data-users-bulk="send-access" hidden>
|
||||
<i class="bi bi-envelope"></i> <?php e(t('Send access')); ?>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<?php if ($canDeleteUsers): ?>
|
||||
<button type="button" class="danger outline" data-users-bulk="delete" hidden>
|
||||
<i class="bi bi-trash3-fill"></i> <?php e(t('Delete')); ?>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
|
||||
|
||||
<details class="dropdown" data-tooltip="<?php e(t("Actions"));?>" data-tooltip-pos="top">
|
||||
<summary role="button" class="outline secondary"><i class="bi bi-three-dots"></i></summary>
|
||||
<ul dir="rtl">
|
||||
<li dir="ltr">
|
||||
<button class="transparent" type="button" data-toolbar-toggle data-toolbar-target="#users-toolbar, #users-tabs"
|
||||
data-toolbar-text-show="<?php e(t('Show filters')); ?>"
|
||||
data-toolbar-text-hide="<?php e(t('Hide filters')); ?>">
|
||||
<span data-toolbar-label>
|
||||
<?php e(t('Hide filters')); ?>
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
<li dir="ltr">
|
||||
<button class="transparent" type="button" data-users-reset>
|
||||
<?php e(t('Reset filters')); ?>
|
||||
</button>
|
||||
</li>
|
||||
<li dir="ltr">
|
||||
<button class="transparent" type="button" data-users-export>
|
||||
<?php e(t('Export CSV')); ?>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
<?php if (can('users.create')): ?>
|
||||
<a role="button" class="primary" href="admin/users/create">
|
||||
<i class="bi bi-plus"></i> <?php e(t('Create user')); ?>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php if ($showTenantTabs): ?>
|
||||
<div class="app-list-tabs" id="users-tabs">
|
||||
<a href="admin/users" class="<?php e($activeTenant === '' ? 'tab-link is-active' : 'tab-link'); ?>">
|
||||
<?php e(t('All')); ?>
|
||||
</a>
|
||||
<?php foreach ($tenants ?? [] as $tenant): ?>
|
||||
<?php if (!empty($tenant['uuid'])): ?>
|
||||
<?php $isActive = $activeTenant === $tenant['uuid']; ?>
|
||||
<a href="admin/users?tenant=<?php e($tenant['uuid']); ?>" class="<?php e($isActive ? 'tab-link is-active' : 'tab-link'); ?>">
|
||||
<?php e($tenant['description'] ?? ''); ?>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="app-list-toolbar" id="users-toolbar">
|
||||
<input type="hidden" id="user-tenant-filter" value="">
|
||||
<label class="app-field">
|
||||
<span><?php e(t('Search')); ?></span>
|
||||
<input type="search" id="user-search" placeholder="<?php e(t('Search...')); ?>">
|
||||
</label>
|
||||
<label class="app-field">
|
||||
<span><?php e(t('Status')); ?></span>
|
||||
<select id="user-status-filter">
|
||||
<option value="all"><?php e(t('All')); ?></option>
|
||||
<option value="active"><?php e(t('Active')); ?></option>
|
||||
<option value="inactive"><?php e(t('Inactive')); ?></option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="app-field">
|
||||
<span><?php e(t('Email verified')); ?></span>
|
||||
<select id="user-email-verified-filter">
|
||||
<option value="all"><?php e(t('All')); ?></option>
|
||||
<option value="verified"><?php e(t('Verified')); ?></option>
|
||||
<option value="unverified"><?php e(t('Unverified')); ?></option>
|
||||
</select>
|
||||
</label>
|
||||
<?php multiSelectFilter('user-department-filter', 'Departments', 'Select departments', $departments ?? [], $activeDepartments); ?>
|
||||
<?php multiSelectFilter('user-role-filter', 'Roles', 'Select roles', $roles ?? [], $activeRoles); ?>
|
||||
<label class="app-field">
|
||||
<span><?php e(t('Created from')); ?></span>
|
||||
<input type="date" id="user-created-from">
|
||||
</label>
|
||||
<label class="app-field">
|
||||
<span><?php e(t('Created to')); ?></span>
|
||||
<input type="date" id="user-created-to">
|
||||
</label>
|
||||
</div>
|
||||
<div class="app-list-table">
|
||||
<div id="users-grid"></div>
|
||||
</div>
|
||||
|
||||
|
||||
<?php $gridFactoryVersion = @filemtime('web/js/core/app-grid-factory.js') ?: time(); ?>
|
||||
<script src="<?php e(asset('vendor/gridjs/gridjs.umd.js')); ?>"></script>
|
||||
<script src="<?php e(asset('vendor/gridjs/plugins/selection/selection.umd.js')); ?>"></script>
|
||||
<script type="module">
|
||||
import { initToolbarToggles } from "<?php e(asset('js/components/app-toggle-toolbar.js')); ?>";
|
||||
import { createServerGrid } from "<?php e(asset('js/core/app-grid-factory.js?v=' . $gridFactoryVersion)); ?>";
|
||||
import { initUsersListPage } from "<?php e(asset('js/pages/app-users-list.js')); ?>";
|
||||
import { bindBulkVisibility } from "<?php e(asset('js/components/app-bulk-selection.js')); ?>";
|
||||
import { buildUrl, badgeHtml } from "<?php e(asset('js/pages/app-list-utils.js')); ?>";
|
||||
import { showAsyncFlash, withLoading } from "<?php e(asset('js/components/app-async-flash.js')); ?>";
|
||||
|
||||
initToolbarToggles();
|
||||
|
||||
const appBase = "<?php e(localeBase()); ?>";
|
||||
const currentUserUuid = "<?php e($currentUserUuid); ?>";
|
||||
const canUpdateUsers = <?php e($canUpdateUsers ? 'true' : 'false'); ?>;
|
||||
const canUpdateSelf = <?php e($canUpdateSelf ? 'true' : 'false'); ?>;
|
||||
const csrf = <?php MintyPHP\Buffer::get('grid_csrf'); ?>;
|
||||
const labels = {
|
||||
active: "<?php e(t('Active')); ?>",
|
||||
inactive: "<?php e(t('Inactive')); ?>",
|
||||
bulkActivateConfirm: "<?php e(t('Activate users?')); ?>",
|
||||
bulkDeactivateConfirm: "<?php e(t('Deactivate users?')); ?>",
|
||||
bulkDeleteConfirm: "<?php e(t('Delete users?')); ?>",
|
||||
bulkSendAccessConfirm: "<?php e(t('Send access emails to selected users?')); ?>",
|
||||
primaryTenant: "<?php e(t('Primary tenant')); ?>",
|
||||
bulkMessages: {
|
||||
'activate': {
|
||||
success: "<?php e(t('%d users activated')); ?>",
|
||||
error: "<?php e(t('Failed to activate users')); ?>"
|
||||
},
|
||||
'deactivate': {
|
||||
success: "<?php e(t('%d users deactivated')); ?>",
|
||||
error: "<?php e(t('Failed to deactivate users')); ?>"
|
||||
},
|
||||
'delete': {
|
||||
success: "<?php e(t('%d users deleted')); ?>",
|
||||
error: "<?php e(t('Failed to delete users')); ?>"
|
||||
},
|
||||
'send-access': {
|
||||
success: "<?php e(t('Access emails sent to %d users')); ?>",
|
||||
partial: "<?php e(t('Access emails sent to %d users, %f failed')); ?>",
|
||||
error: "<?php e(t('Failed to send access emails')); ?>"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const buildBulkUrl = (action) => buildUrl(appBase, `admin/users/bulk/${action}`);
|
||||
|
||||
const normalizeCommaSeparated = (value) => {
|
||||
if (Array.isArray(value)) return value;
|
||||
if (typeof value === 'string') {
|
||||
return value.split(',').map((item) => item.trim()).filter(Boolean);
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
const initUsersGrid = () => {
|
||||
const rowSelection = window.gridjs?.plugins?.selection?.RowSelection;
|
||||
if (!rowSelection) {
|
||||
throw new Error('Grid.js RowSelection plugin is not available.');
|
||||
}
|
||||
const activeIndex = 6;
|
||||
const uuidIndex = 2;
|
||||
const canEditRow = (row) => {
|
||||
const uuid = row?.cells?.[uuidIndex]?.data;
|
||||
if (!uuid) return false;
|
||||
if (uuid === currentUserUuid) {
|
||||
return canUpdateUsers || canUpdateSelf;
|
||||
}
|
||||
return canUpdateUsers;
|
||||
};
|
||||
|
||||
const escapeHtml = (value) => String(value ?? '').replace(/[&<>"']/g, (char) => ({
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
}[char]));
|
||||
const buildBadgeList = (items) => {
|
||||
let list = items;
|
||||
let primary = '';
|
||||
if (items && typeof items === 'object' && !Array.isArray(items)) {
|
||||
list = Array.isArray(items.items) ? items.items : [];
|
||||
primary = String(items.primary ?? '');
|
||||
}
|
||||
if (!Array.isArray(list) || list.length === 0) return '';
|
||||
const badges = list.map((label) => {
|
||||
const isPrimary = primary !== '' && label === primary;
|
||||
const variant = isPrimary ? 'info' : 'neutral';
|
||||
const tooltip = isPrimary ? ` data-tooltip="${escapeHtml(labels.primaryTenant)}" data-tooltip-pos="top"` : '';
|
||||
return `<span class="badge" data-variant="${variant}"${tooltip}>${escapeHtml(label)}</span>`;
|
||||
}).join(' ');
|
||||
return `<div class="badge-list">${badges}</div>`;
|
||||
};
|
||||
const initialsForRow = (row) => {
|
||||
const first = row?.cells?.[3]?.data ?? '';
|
||||
const last = row?.cells?.[4]?.data ?? '';
|
||||
const parts = [first, last]
|
||||
.map((value) => String(value || '').trim())
|
||||
.filter(Boolean);
|
||||
const chars = parts.map((value) => value[0] || '').join('').toUpperCase();
|
||||
return chars || '?';
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
name: "<?php e(t('Avatar')); ?>",
|
||||
sort: false,
|
||||
formatter: (cell, row) => {
|
||||
if (!cell) {
|
||||
const initials = escapeHtml(initialsForRow(row));
|
||||
return gridjs.html(`<span class="grid-avatar grid-avatar-placeholder">${initials}</span>`);
|
||||
}
|
||||
const src = new URL(`admin/users/avatar-file?uuid=${cell}&size=64`, appBase).toString();
|
||||
const full = new URL(`admin/users/avatar-file?uuid=${cell}&size=256`, appBase).toString();
|
||||
return gridjs.html(`<a data-fslightbox="user-avatars" href="${full}"><img class="grid-avatar" src="${src}" alt="" loading="lazy"></a>`);
|
||||
}
|
||||
},
|
||||
{ name: 'UUID', hidden: true },
|
||||
{ name: "<?php e(t('First name')); ?>", sort: true },
|
||||
{ name: "<?php e(t('Last name')); ?>", sort: true },
|
||||
{ name: "<?php e(t('Email')); ?>", sort: true },
|
||||
{
|
||||
name: "<?php e(t('State')); ?>",
|
||||
sort: true,
|
||||
formatter: (cell) => cell
|
||||
? gridjs.html(`<span class="badge" data-variant="success">${labels.active}</span>`)
|
||||
: gridjs.html(`<span class="badge" data-variant="danger">${labels.inactive}</span>`)
|
||||
},
|
||||
{ name: "<?php e(t('Tenants')); ?>", sort: false, formatter: (cell) => gridjs.html(buildBadgeList(cell)) },
|
||||
{ name: "<?php e(t('Departments')); ?>", sort: false, formatter: (cell) => gridjs.html(buildBadgeList(cell)) },
|
||||
{ name: "<?php e(t('Roles')); ?>", sort: false, formatter: (cell) => gridjs.html(buildBadgeList(cell)) },
|
||||
{ name: "<?php e(t('Phone')); ?>", sort: false },
|
||||
{ name: "<?php e(t('Mobile')); ?>", sort: false },
|
||||
{ name: "<?php e(t('Short dial')); ?>", sort: false },
|
||||
{ name: "<?php e(t('Created')); ?>", sort: true, formatter: (cell) => badgeHtml(gridjs, cell) },
|
||||
{ name: "<?php e(t('Modified')); ?>", sort: true, formatter: (cell) => badgeHtml(gridjs, cell) },
|
||||
{ name: 'ID', hidden: true }
|
||||
];
|
||||
|
||||
const gridConfig = createServerGrid({
|
||||
gridjs: window.gridjs,
|
||||
container: '#users-grid',
|
||||
dataUrl: 'admin/users/data',
|
||||
appBase,
|
||||
columns,
|
||||
sortColumns: [null, null, 'first_name', 'last_name', 'email', 'active', null, null, null, null, null, null, 'created', 'modified', null],
|
||||
paginationLimit: 10,
|
||||
language: <?php MintyPHP\Buffer::get('grid_lang'); ?>,
|
||||
mapData: (data) => data.data.map((row) => [
|
||||
row.has_avatar ? row.uuid : '',
|
||||
row.uuid,
|
||||
row.first_name,
|
||||
row.last_name,
|
||||
row.email,
|
||||
row.active,
|
||||
row.tenants,
|
||||
row.departments,
|
||||
row.roles,
|
||||
row.phone,
|
||||
row.mobile,
|
||||
row.short_dial,
|
||||
row.created,
|
||||
row.modified,
|
||||
row.id
|
||||
]),
|
||||
selection: {
|
||||
enabled: true,
|
||||
id: 'selectRow',
|
||||
component: rowSelection,
|
||||
selectAllLabel: "<?php e(t('Select all')); ?>",
|
||||
props: {
|
||||
id: (row) => row.cell(uuidIndex).data
|
||||
},
|
||||
getSelectedIds: ({ container }) => {
|
||||
if (!container) return [];
|
||||
return Array.from(container.querySelectorAll('.gridjs-tr-selected'))
|
||||
.map((row) => row.getAttribute('data-uuid'))
|
||||
.filter(Boolean);
|
||||
}
|
||||
},
|
||||
search: {
|
||||
input: '#user-search',
|
||||
param: 'search',
|
||||
debounce: 250
|
||||
},
|
||||
filters: [
|
||||
{
|
||||
input: '#user-status-filter',
|
||||
param: 'active',
|
||||
default: 'all',
|
||||
normalize: (value) => (value && value !== 'all' ? value : '')
|
||||
},
|
||||
{
|
||||
input: '#user-email-verified-filter',
|
||||
param: 'email_verified',
|
||||
default: 'all',
|
||||
normalize: (value) => (value && value !== 'all' ? value : '')
|
||||
},
|
||||
{
|
||||
input: '#user-department-filter',
|
||||
param: 'departments',
|
||||
default: [],
|
||||
normalize: normalizeCommaSeparated
|
||||
},
|
||||
{
|
||||
input: '#user-role-filter',
|
||||
param: 'roles',
|
||||
default: [],
|
||||
normalize: normalizeCommaSeparated
|
||||
},
|
||||
{
|
||||
input: '#user-tenant-filter',
|
||||
param: 'tenant',
|
||||
default: '',
|
||||
normalize: (value) => value || ''
|
||||
},
|
||||
{
|
||||
input: '#user-created-from',
|
||||
param: 'created_from',
|
||||
normalize: (value) => (value ? value : '')
|
||||
},
|
||||
{
|
||||
input: '#user-created-to',
|
||||
param: 'created_to',
|
||||
normalize: (value) => (value ? value : '')
|
||||
}
|
||||
],
|
||||
urlSync: true,
|
||||
rowDataset: (row) => ({
|
||||
uuid: row?.cells?.[uuidIndex]?.data
|
||||
}),
|
||||
rowDblClick: {
|
||||
getUrl: (rowData) => {
|
||||
if (!canEditRow(rowData)) {
|
||||
return '';
|
||||
}
|
||||
const uuid = rowData?.cells?.[uuidIndex]?.data;
|
||||
return uuid ? new URL(`admin/users/edit/${uuid}`, appBase).toString() : '';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!gridConfig || !gridConfig.grid) {
|
||||
console.warn('Users grid init failed');
|
||||
return null;
|
||||
}
|
||||
|
||||
return { ...gridConfig, indices: { activeIndex, uuidIndex } };
|
||||
};
|
||||
|
||||
const gridConfig = initUsersGrid();
|
||||
initUsersListPage({
|
||||
gridConfig,
|
||||
appBase,
|
||||
csrf,
|
||||
labels,
|
||||
buildBulkUrl,
|
||||
showFlash: showAsyncFlash,
|
||||
withLoading
|
||||
});
|
||||
bindBulkVisibility({
|
||||
containerSelector: '#users-grid',
|
||||
buttonSelector: '[data-users-bulk]'
|
||||
});
|
||||
</script>
|
||||
73
pages/admin/users/send-access($id).php
Normal file
73
pages/admin/users/send-access($id).php
Normal file
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\PermissionService;
|
||||
use MintyPHP\Service\TenantScopeService;
|
||||
use MintyPHP\Service\UserService;
|
||||
use MintyPHP\Service\MailService;
|
||||
use MintyPHP\I18n;
|
||||
use MintyPHP\Http\Request;
|
||||
|
||||
Guard::requireLogin();
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
$canUpdateUser = PermissionService::userHas($currentUserId, PermissionService::USERS_UPDATE);
|
||||
$canUpdateSelf = PermissionService::userHas($currentUserId, PermissionService::USERS_SELF_UPDATE);
|
||||
|
||||
$uuid = trim((string) ($id ?? ''));
|
||||
$user = $uuid !== '' ? UserService::findByUuid($uuid) : null;
|
||||
if (!$user) {
|
||||
Flash::error(t('User not found'), 'admin/users', 'user_not_found');
|
||||
Router::redirect('admin/users');
|
||||
}
|
||||
|
||||
$userId = (int) ($user['id'] ?? 0);
|
||||
$isOwnAccount = $currentUserId > 0 && $currentUserId === $userId;
|
||||
$canEditUser = $isOwnAccount ? ($canUpdateUser || $canUpdateSelf) : $canUpdateUser;
|
||||
|
||||
if (!$canEditUser) {
|
||||
Router::redirect('error/forbidden');
|
||||
return;
|
||||
}
|
||||
if (!$isOwnAccount && !TenantScopeService::canAccess('users', $userId, $currentUserId)) {
|
||||
Router::redirect('error/forbidden');
|
||||
return;
|
||||
}
|
||||
|
||||
$locale = $user['locale'] ?? (I18n::$locale ?? I18n::$defaultLocale);
|
||||
$name = trim(($user['first_name'] ?? '') . ' ' . ($user['last_name'] ?? ''));
|
||||
$isGerman = strpos((string) $locale, 'de') === 0;
|
||||
$greeting = $isGerman ? 'Hallo' : 'Hello';
|
||||
if ($name !== '') {
|
||||
$greeting .= ' ' . $name;
|
||||
}
|
||||
$greeting .= ',';
|
||||
|
||||
$loginUrl = appUrl(Request::withLocale('login', $locale));
|
||||
$resetUrl = appUrl(Request::withLocale('password/forgot', $locale));
|
||||
$previousLocale = I18n::$locale ?? null;
|
||||
I18n::$locale = $locale;
|
||||
$subject = t('Your access details');
|
||||
I18n::$locale = $previousLocale;
|
||||
|
||||
$vars = [
|
||||
'app_name' => appTitle(),
|
||||
'app_logo_url' => appLogoUrlAbsolute(128),
|
||||
'imprint_url' => appUrl(Request::withLocale('imprint', $locale)),
|
||||
'privacy_url' => appUrl(Request::withLocale('privacy', $locale)),
|
||||
'greeting' => $greeting,
|
||||
'username' => (string) ($user['email'] ?? ''),
|
||||
'login_url' => $loginUrl,
|
||||
'reset_url' => $resetUrl,
|
||||
];
|
||||
|
||||
$result = MailService::sendTemplate('access_info', $vars, (string) $user['email'], $subject, $locale);
|
||||
if (!($result['ok'] ?? false)) {
|
||||
Flash::error(t('Access email failed'), "admin/users/edit/{$uuid}", 'access_email_failed');
|
||||
Router::redirect("admin/users/edit/{$uuid}");
|
||||
}
|
||||
|
||||
Flash::success(t('Access email sent'), "admin/users/edit/{$uuid}", 'access_email_sent');
|
||||
Router::redirect("admin/users/edit/{$uuid}");
|
||||
83
pages/admin/users/switch-tenant().php
Normal file
83
pages/admin/users/switch-tenant().php
Normal file
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Http\Request;
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Session;
|
||||
use MintyPHP\Service\UserService;
|
||||
use MintyPHP\Support\Guard;
|
||||
|
||||
Guard::requireLogin();
|
||||
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
|
||||
Router::redirect('admin');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Session::checkCsrfToken()) {
|
||||
if (Request::wantsJson()) {
|
||||
http_response_code(400);
|
||||
Router::json(['ok' => false, 'error' => 'csrf']);
|
||||
return;
|
||||
}
|
||||
Router::redirect('admin');
|
||||
return;
|
||||
}
|
||||
|
||||
$userId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
if ($userId <= 0) {
|
||||
http_response_code(401);
|
||||
if (Request::wantsJson()) {
|
||||
Router::json(['ok' => false, 'error' => 'unauthorized']);
|
||||
return;
|
||||
}
|
||||
Router::redirect('login');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get tenant_id from POST
|
||||
$tenantId = (int) ($_POST['tenant_id'] ?? 0);
|
||||
|
||||
if ($tenantId <= 0) {
|
||||
http_response_code(400);
|
||||
if (Request::wantsJson()) {
|
||||
Router::json(['ok' => false, 'error' => 'invalid_tenant_id']);
|
||||
return;
|
||||
}
|
||||
Router::redirect('admin');
|
||||
return;
|
||||
}
|
||||
|
||||
$result = UserService::setCurrentTenant($userId, $tenantId);
|
||||
if (!($result['ok'] ?? false)) {
|
||||
$error = $result['error'] ?? 'update_failed';
|
||||
http_response_code(400);
|
||||
if (Request::wantsJson()) {
|
||||
Router::json(['ok' => false, 'error' => $error]);
|
||||
return;
|
||||
}
|
||||
Router::redirect('admin');
|
||||
return;
|
||||
}
|
||||
|
||||
// Update session with new current tenant
|
||||
if (isset($_SESSION['user'])) {
|
||||
$_SESSION['user']['current_tenant_id'] = $tenantId;
|
||||
}
|
||||
|
||||
// Update session with full tenant data
|
||||
$_SESSION['current_tenant'] = $result['tenant'] ?? null;
|
||||
|
||||
// Reload available tenants and departments
|
||||
$_SESSION['available_tenants'] = UserService::getAvailableTenants($userId);
|
||||
$_SESSION['available_departments_by_tenant'] = UserService::getAvailableDepartmentsByTenant($userId);
|
||||
|
||||
if (Request::wantsJson()) {
|
||||
Router::json([
|
||||
'ok' => true,
|
||||
'tenant_id' => $tenantId,
|
||||
'tenant' => $result['tenant'] ?? null,
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
Router::redirect('admin');
|
||||
66
pages/admin/users/theme().php
Normal file
66
pages/admin/users/theme().php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Http\Request;
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Session;
|
||||
use MintyPHP\Service\UserService;
|
||||
use MintyPHP\Support\Guard;
|
||||
|
||||
Guard::requireLogin();
|
||||
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
|
||||
Router::redirect('admin');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Session::checkCsrfToken()) {
|
||||
if (Request::wantsJson()) {
|
||||
http_response_code(400);
|
||||
Router::json(['ok' => false, 'error' => 'csrf']);
|
||||
return;
|
||||
}
|
||||
Router::redirect('admin');
|
||||
return;
|
||||
}
|
||||
|
||||
$userId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
if ($userId <= 0) {
|
||||
http_response_code(401);
|
||||
if (Request::wantsJson()) {
|
||||
Router::json(['ok' => false, 'error' => 'unauthorized']);
|
||||
return;
|
||||
}
|
||||
Router::redirect('login');
|
||||
return;
|
||||
}
|
||||
|
||||
$theme = trim((string) ($_POST['theme'] ?? ''));
|
||||
if (!in_array($theme, ['light', 'dark'], true)) {
|
||||
http_response_code(400);
|
||||
if (Request::wantsJson()) {
|
||||
Router::json(['ok' => false, 'error' => 'invalid_theme']);
|
||||
return;
|
||||
}
|
||||
Router::redirect('admin');
|
||||
return;
|
||||
}
|
||||
|
||||
$updated = UserService::setTheme($userId, $theme);
|
||||
if (!$updated) {
|
||||
http_response_code(500);
|
||||
if (Request::wantsJson()) {
|
||||
Router::json(['ok' => false, 'error' => 'update_failed']);
|
||||
return;
|
||||
}
|
||||
Router::redirect('admin');
|
||||
return;
|
||||
}
|
||||
|
||||
$_SESSION['user']['theme'] = $theme;
|
||||
|
||||
if (Request::wantsJson()) {
|
||||
Router::json(['ok' => true, 'theme' => $theme]);
|
||||
return;
|
||||
}
|
||||
|
||||
Router::redirect('admin');
|
||||
Reference in New Issue
Block a user