add composer-unused, comprehensive docs, and project restructure
- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks - Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists - Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services - Add Microsoft OIDC SSO, API token management, and user lifecycle features - Add swagger-ui vendor integration and OpenAPI spec - Add production Docker setup and bin/ scripts - Update composer dependencies, config, templates, and frontend assets throughout Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -15,7 +15,7 @@
|
||||
* @var array $roles
|
||||
* @var array $selectedRoleIds
|
||||
* @var bool $showDepartments
|
||||
* @var array $departments
|
||||
* @var array $departmentOptionsByTenant
|
||||
* @var array $selectedDepartmentIds
|
||||
* @var bool $isReadOnly
|
||||
* @var bool $showPermissions
|
||||
@@ -24,6 +24,19 @@
|
||||
* @var array $passwordResets
|
||||
* @var bool $showRememberTokens
|
||||
* @var array $rememberTokens
|
||||
* @var bool $showCustomFieldsTab
|
||||
* @var bool $canEditCustomFieldValues
|
||||
* @var array $customFieldDefinitionsByTenant
|
||||
* @var array $customFieldValueMap
|
||||
* @var array $customFieldPostedValues
|
||||
* @var bool $showDangerZone
|
||||
* @var string|null $dangerZoneDeleteFormId
|
||||
* @var string|null $dangerZoneWarning
|
||||
* @var string|null $dangerZoneActionLabel
|
||||
* @var bool $showTokenDangerAction
|
||||
* @var string|null $tokenDangerFormId
|
||||
* @var string|null $tokenDangerWarning
|
||||
* @var string|null $tokenDangerActionLabel
|
||||
*/
|
||||
|
||||
use MintyPHP\Session;
|
||||
@@ -45,7 +58,7 @@ $showRoles = $showRoles ?? false;
|
||||
$roles = $roles ?? [];
|
||||
$selectedRoleIds = $selectedRoleIds ?? [];
|
||||
$showDepartments = $showDepartments ?? false;
|
||||
$departments = $departments ?? [];
|
||||
$departmentOptionsByTenant = $departmentOptionsByTenant ?? [];
|
||||
$selectedDepartmentIds = $selectedDepartmentIds ?? [];
|
||||
$primaryTenantId = (int) ($values['primary_tenant_id'] ?? 0);
|
||||
if (!$primaryTenantId && count($selectedTenantIds) === 1) {
|
||||
@@ -58,34 +71,69 @@ $passwordResets = $passwordResets ?? [];
|
||||
$showPasswordResets = $showPasswordResets ?? false;
|
||||
$rememberTokens = $rememberTokens ?? [];
|
||||
$showRememberTokens = $showRememberTokens ?? false;
|
||||
$apiTokens = $apiTokens ?? [];
|
||||
$showApiTokens = $showApiTokens ?? false;
|
||||
$canManageApiTokens = $canManageApiTokens ?? false;
|
||||
$showCustomFieldsTab = (bool) $showCustomFieldsTab;
|
||||
$canEditCustomFieldValues = (bool) $canEditCustomFieldValues;
|
||||
$customFieldDefinitionsByTenant = (array) $customFieldDefinitionsByTenant;
|
||||
$customFieldValueMap = (array) $customFieldValueMap;
|
||||
$customFieldPostedValues = (array) $customFieldPostedValues;
|
||||
$customFieldScalarPosted = is_array($customFieldPostedValues['custom_field_values'] ?? null)
|
||||
? $customFieldPostedValues['custom_field_values']
|
||||
: [];
|
||||
$customFieldMultiPosted = is_array($customFieldPostedValues['custom_field_values_multi'] ?? null)
|
||||
? $customFieldPostedValues['custom_field_values_multi']
|
||||
: [];
|
||||
$showDangerZone = (bool) ($showDangerZone ?? false);
|
||||
$dangerZoneDeleteFormId = (string) ($dangerZoneDeleteFormId ?? '');
|
||||
$dangerZoneWarning = (string) ($dangerZoneWarning ?? t('This action cannot be undone.'));
|
||||
$dangerZoneActionLabel = (string) ($dangerZoneActionLabel ?? t('Delete'));
|
||||
$showTokenDangerAction = (bool) ($showTokenDangerAction ?? false);
|
||||
$tokenDangerFormId = (string) ($tokenDangerFormId ?? '');
|
||||
$tokenDangerWarning = (string) ($tokenDangerWarning ?? t('This action cannot be undone.'));
|
||||
$tokenDangerActionLabel = (string) ($tokenDangerActionLabel ?? t('Clear login tokens'));
|
||||
$minLength = UserService::passwordMinLength();
|
||||
$requiredAttr = $passwordRequired ? 'required' : '';
|
||||
$readonlyAttr = $isReadOnly ? 'readonly' : '';
|
||||
$disabledAttr = $isReadOnly ? 'disabled' : '';
|
||||
$dataDisabledAttr = $isReadOnly ? 'data-disabled="true"' : '';
|
||||
$visibilityDisabled = $isReadOnly || $isOwnAccount;
|
||||
$visibilityDisabledAttr = $visibilityDisabled ? 'disabled' : '';
|
||||
$visibilityHint = $isOwnAccount
|
||||
? t('You cannot deactivate your own account')
|
||||
: t('Inactive entries are hidden in lists and selections.');
|
||||
$locales = defined('APP_LOCALES') ? APP_LOCALES : [I18n::$defaultLocale];
|
||||
$primaryTenantDisabled = $isReadOnly || count($selectedTenantIds) === 1;
|
||||
$primaryTenantDisabledAttr = $primaryTenantDisabled ? 'disabled' : '';
|
||||
$showOrganization = $showTenants || $showDepartments || $showRoles;
|
||||
$showOrganizationTab = $showOrganization || $showPermissions;
|
||||
$customFieldInputsDisabled = $isReadOnly || !$canEditCustomFieldValues;
|
||||
$customFieldInputsDisabledAttr = $customFieldInputsDisabled ? 'disabled' : '';
|
||||
$customFieldTenantMap = [];
|
||||
foreach ($tenants as $tenant) {
|
||||
$tenantId = (int) ($tenant['id'] ?? 0);
|
||||
if ($tenantId <= 0 || !in_array($tenantId, $selectedTenantIds, true)) {
|
||||
continue;
|
||||
}
|
||||
$customFieldTenantMap[$tenantId] = $tenant;
|
||||
}
|
||||
|
||||
?>
|
||||
<form id="<?php e($formId); ?>" method="post">
|
||||
<form id="<?php e($formId); ?>" method="post" data-details-storage="admin-user-form-details-v1">
|
||||
<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="profile" data-tab-default><?php e(t('Master data')); ?></button>
|
||||
<?php if ($showOrganizationTab): ?>
|
||||
<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 if ($showCustomFieldsTab): ?>
|
||||
<button type="button" data-tab="custom_fields"><?php e(t('Custom fields')); ?></button>
|
||||
<?php endif; ?>
|
||||
<?php if ($showPasswordResets): ?>
|
||||
<button type="button" data-tab="password-resets"><?php e(t('Password resets')); ?></button>
|
||||
<?php endif; ?>
|
||||
<?php if ($showRememberTokens): ?>
|
||||
<button type="button" data-tab="login-tokens"><?php e(t('Login tokens')); ?></button>
|
||||
<button type="button" data-tab="security"><?php e(t('Security')); ?></button>
|
||||
<button type="button" data-tab="visibility"><?php e(t('Visibility')); ?></button>
|
||||
<?php if ($showDangerZone || $showTokenDangerAction): ?>
|
||||
<button type="button" data-tab="danger"><?php e(t('Danger zone')); ?></button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
@@ -109,7 +157,7 @@ $showOrganization = $showTenants || $showDepartments || $showRoles;
|
||||
</label>
|
||||
|
||||
<hr>
|
||||
<details name="profile-data" open>
|
||||
<details name="profile-data" data-details-key="profile-data" <?php e($detailsOpenAll ? 'open' : ''); ?>>
|
||||
<summary><?php e(t('Profile')); ?></summary>
|
||||
<hr>
|
||||
<label for="job_title">
|
||||
@@ -124,7 +172,7 @@ $showOrganization = $showTenants || $showDepartments || $showRoles;
|
||||
</label>
|
||||
</details>
|
||||
<hr>
|
||||
<details name="preferences" open>
|
||||
<details name="preferences" data-details-key="preferences" <?php e($detailsOpenAll ? 'open' : ''); ?>>
|
||||
<summary><?php e(t('Preferences')); ?></summary>
|
||||
<hr>
|
||||
<div class="grid">
|
||||
@@ -168,104 +216,8 @@ $showOrganization = $showTenants || $showDepartments || $showRoles;
|
||||
</label>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<div data-tab-panel="access">
|
||||
|
||||
<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; ?>
|
||||
</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>
|
||||
<hr>
|
||||
<details name="profile-contact-data" data-details-key="profile-contact-data" <?php e($detailsOpenAll ? 'open' : ''); ?>>
|
||||
<summary><?php e(t('Contact')); ?></summary>
|
||||
<hr>
|
||||
<div class="grid">
|
||||
@@ -287,7 +239,7 @@ $showOrganization = $showTenants || $showDepartments || $showRoles;
|
||||
</div>
|
||||
</details>
|
||||
<hr>
|
||||
<details name="address-data" open>
|
||||
<details name="profile-address-data" data-details-key="profile-address-data" <?php e($detailsOpenAll ? 'open' : ''); ?>>
|
||||
<summary><?php e(t('Address')); ?></summary>
|
||||
<hr>
|
||||
<label for="address">
|
||||
@@ -316,7 +268,7 @@ $showOrganization = $showTenants || $showDepartments || $showRoles;
|
||||
</div>
|
||||
</details>
|
||||
<hr>
|
||||
<details name="employment-data" open>
|
||||
<details name="profile-employment-data" data-details-key="profile-employment-data" <?php e($detailsOpenAll ? 'open' : ''); ?>>
|
||||
<summary><?php e(t('Employment')); ?></summary>
|
||||
<hr>
|
||||
<label for="hire_date">
|
||||
@@ -325,154 +277,598 @@ $showOrganization = $showTenants || $showDepartments || $showRoles;
|
||||
</label>
|
||||
</details>
|
||||
</div>
|
||||
<?php if ($showPermissions): ?>
|
||||
<div data-tab-panel="permissions">
|
||||
<div class="overflow-auto white-space-nowrap">
|
||||
<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>
|
||||
|
||||
<div data-tab-panel="security">
|
||||
<details name="security-password" data-details-key="security-password" 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; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<li data-rule="match"><?php e(t('Passwords must match')); ?></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if ($showPasswordResets): ?>
|
||||
<div data-tab-panel="password-resets">
|
||||
<div class="overflow-auto white-space-nowrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?php e(t('Requested at')); ?></th>
|
||||
<th><?php e(t('Expires at')); ?></th>
|
||||
<th><?php e(t('Used at')); ?></th>
|
||||
<th><?php e(t('Attempts')); ?></th>
|
||||
<th><?php e(t('Status')); ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($passwordResets as $reset): ?>
|
||||
<?php
|
||||
$expiresAt = (string) ($reset['expires_at'] ?? '');
|
||||
$usedAt = (string) ($reset['used_at'] ?? '');
|
||||
$status = t('Active');
|
||||
$statusVariant = 'success';
|
||||
if ($usedAt !== '') {
|
||||
$status = t('Used');
|
||||
$statusVariant = 'neutral';
|
||||
} elseif ($expiresAt !== '') {
|
||||
try {
|
||||
$expired = (new \DateTimeImmutable($expiresAt)) <= new \DateTimeImmutable('now');
|
||||
if ($expired) {
|
||||
$status = t('Expired');
|
||||
$statusVariant = 'warning';
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// fallback: keep Active
|
||||
}
|
||||
}
|
||||
?>
|
||||
</details>
|
||||
<?php if ($showPasswordResets): ?>
|
||||
<hr>
|
||||
<details name="security-password-resets" data-details-key="security-password-resets">
|
||||
<summary><?php e(t('Password resets')); ?></summary>
|
||||
<hr>
|
||||
<div class="overflow-auto white-space-nowrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td><?php e(dt($reset['created'] ?? '')); ?></td>
|
||||
<td><?php e($expiresAt !== '' ? dt($expiresAt) : '-'); ?></td>
|
||||
<td><?php e($usedAt !== '' ? dt($usedAt) : '-'); ?></td>
|
||||
<td><?php e((string) ($reset['attempts'] ?? 0)); ?></td>
|
||||
<td>
|
||||
<span class="badge" data-variant="<?php e($statusVariant); ?>">
|
||||
<?php e($status); ?>
|
||||
</span>
|
||||
</td>
|
||||
<th><?php e(t('Requested at')); ?></th>
|
||||
<th><?php e(t('Expires at')); ?></th>
|
||||
<th><?php e(t('Used at')); ?></th>
|
||||
<th><?php e(t('Attempts')); ?></th>
|
||||
<th><?php e(t('Status')); ?></th>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if ($showRememberTokens): ?>
|
||||
<div data-tab-panel="login-tokens">
|
||||
<div class="overflow-auto white-space-nowrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?php e(t('Created at')); ?></th>
|
||||
<th><?php e(t('Expires at')); ?></th>
|
||||
<th><?php e(t('Last used')); ?></th>
|
||||
<th><?php e(t('Status')); ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($rememberTokens as $token): ?>
|
||||
<?php
|
||||
$expiresAt = (string) ($token['expires_at'] ?? '');
|
||||
$expiredByAdminAt = (string) ($token['expired_by_admin_at'] ?? '');
|
||||
$lastUsed = (string) ($token['last_used'] ?? '');
|
||||
$status = t('Active');
|
||||
$statusVariant = 'success';
|
||||
if ($expiresAt !== '') {
|
||||
try {
|
||||
$expired = (new \DateTimeImmutable($expiresAt)) <= new \DateTimeImmutable('now');
|
||||
if ($expired) {
|
||||
if ($expiredByAdminAt !== '') {
|
||||
$status = t('Expired by admin');
|
||||
$statusVariant = 'warning';
|
||||
} else {
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($passwordResets as $reset): ?>
|
||||
<?php
|
||||
$expiresAt = (string) ($reset['expires_at'] ?? '');
|
||||
$usedAt = (string) ($reset['used_at'] ?? '');
|
||||
$status = t('Active');
|
||||
$statusVariant = 'success';
|
||||
if ($usedAt !== '') {
|
||||
$status = t('Used');
|
||||
$statusVariant = 'neutral';
|
||||
} elseif ($expiresAt !== '') {
|
||||
try {
|
||||
$expired = (new \DateTimeImmutable($expiresAt)) <= new \DateTimeImmutable('now');
|
||||
if ($expired) {
|
||||
$status = t('Expired');
|
||||
$statusVariant = 'warning';
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// fallback: keep Active
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// fallback: keep Active
|
||||
}
|
||||
}
|
||||
?>
|
||||
?>
|
||||
<tr>
|
||||
<td><?php e(dt($reset['created'] ?? '')); ?></td>
|
||||
<td><?php e($expiresAt !== '' ? dt($expiresAt) : '-'); ?></td>
|
||||
<td><?php e($usedAt !== '' ? dt($usedAt) : '-'); ?></td>
|
||||
<td><?php e((string) ($reset['attempts'] ?? 0)); ?></td>
|
||||
<td>
|
||||
<span class="badge" data-variant="<?php e($statusVariant); ?>">
|
||||
<?php e($status); ?>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</details>
|
||||
<?php endif; ?>
|
||||
<?php if ($showRememberTokens): ?>
|
||||
<hr>
|
||||
<details name="security-login-tokens" data-details-key="security-login-tokens">
|
||||
<summary><?php e(t('Login tokens')); ?></summary>
|
||||
<hr>
|
||||
<div class="overflow-auto white-space-nowrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td><?php e(dt($token['created'] ?? '')); ?></td>
|
||||
<td><?php e($expiresAt !== '' ? dt($expiresAt) : '-'); ?></td>
|
||||
<td><?php e($lastUsed !== '' ? dt($lastUsed) : '-'); ?></td>
|
||||
<td>
|
||||
<span class="badge" data-variant="<?php e($statusVariant); ?>">
|
||||
<?php e($status); ?>
|
||||
</span>
|
||||
</td>
|
||||
<th><?php e(t('Created at')); ?></th>
|
||||
<th><?php e(t('Expires at')); ?></th>
|
||||
<th><?php e(t('Last used')); ?></th>
|
||||
<th><?php e(t('Status')); ?></th>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($rememberTokens as $token): ?>
|
||||
<?php
|
||||
$expiresAt = (string) ($token['expires_at'] ?? '');
|
||||
$expiredByAdminAt = (string) ($token['expired_by_admin_at'] ?? '');
|
||||
$lastUsed = (string) ($token['last_used'] ?? '');
|
||||
$status = t('Active');
|
||||
$statusVariant = 'success';
|
||||
if ($expiresAt !== '') {
|
||||
try {
|
||||
$expired = (new \DateTimeImmutable($expiresAt)) <= new \DateTimeImmutable('now');
|
||||
if ($expired) {
|
||||
if ($expiredByAdminAt !== '') {
|
||||
$status = t('Expired by admin');
|
||||
$statusVariant = 'warning';
|
||||
} else {
|
||||
$status = t('Expired');
|
||||
$statusVariant = 'warning';
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// fallback: keep Active
|
||||
}
|
||||
}
|
||||
?>
|
||||
<tr>
|
||||
<td><?php e(dt($token['created'] ?? '')); ?></td>
|
||||
<td><?php e($expiresAt !== '' ? dt($expiresAt) : '-'); ?></td>
|
||||
<td><?php e($lastUsed !== '' ? dt($lastUsed) : '-'); ?></td>
|
||||
<td>
|
||||
<span class="badge" data-variant="<?php e($statusVariant); ?>">
|
||||
<?php e($status); ?>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</details>
|
||||
<?php endif; ?>
|
||||
<?php if ($showApiTokens): ?>
|
||||
<hr>
|
||||
<?php
|
||||
$apiTokenData = $_SESSION['api_token_created'] ?? null;
|
||||
$apiTokenCreated = null;
|
||||
if (is_array($apiTokenData) && ($apiTokenData['uuid'] ?? '') === ($values['uuid'] ?? '')) {
|
||||
$apiTokenCreated = $apiTokenData['token'];
|
||||
unset($_SESSION['api_token_created']);
|
||||
}
|
||||
?>
|
||||
<details name="security-api-tokens" data-details-key="security-api-tokens"<?php if ($apiTokenCreated): ?> open<?php endif; ?>>
|
||||
<summary><?php e(t('API tokens')); ?></summary>
|
||||
<hr>
|
||||
<?php if ($apiTokenCreated): ?>
|
||||
<div class="app-flash" data-variant="info" style="margin-bottom: 1rem;">
|
||||
<p><strong><?php e(t('New API token (copy now — shown only once):')); ?></strong></p>
|
||||
<span class="badge" data-variant="neutral" data-copy="true"
|
||||
data-copy-value="<?php e($apiTokenCreated); ?>"
|
||||
style="font-family: var(--pico-font-family-monospace); font-size: 0.85rem; word-break: break-all;">
|
||||
<?php e($apiTokenCreated); ?>
|
||||
</span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if ($canManageApiTokens): ?>
|
||||
<div>
|
||||
<label>
|
||||
<span><?php e(t('Token name')); ?></span>
|
||||
</label>
|
||||
<div role="group" class="white-space-nowrap">
|
||||
<input type="text" name="token_name" id="api-token-name"
|
||||
form="api-token-create-form" required
|
||||
placeholder="<?php e(t('e.g. CI Pipeline')); ?>">
|
||||
<button type="submit"
|
||||
form="api-token-create-form"
|
||||
><?php e(t('Create API token')); ?></button>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($apiTokens)): ?>
|
||||
<div class="overflow-auto white-space-nowrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?php e(t('Name')); ?></th>
|
||||
<th><?php e(t('Prefix')); ?></th>
|
||||
<th><?php e(t('Created at')); ?></th>
|
||||
<th><?php e(t('Last used')); ?></th>
|
||||
<th><?php e(t('Status')); ?></th>
|
||||
<?php if ($canManageApiTokens): ?>
|
||||
<th></th>
|
||||
<?php endif; ?>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($apiTokens as $apiToken): ?>
|
||||
<?php
|
||||
$isRevoked = !empty($apiToken['revoked_at']);
|
||||
$isExpired = false;
|
||||
$expiresAtApi = (string) ($apiToken['expires_at'] ?? '');
|
||||
if (!$isRevoked && $expiresAtApi !== '') {
|
||||
try {
|
||||
$isExpired = (new \DateTimeImmutable($expiresAtApi)) <= new \DateTimeImmutable('now');
|
||||
} catch (\Exception $e) {}
|
||||
}
|
||||
$statusLabel = t('Active');
|
||||
$statusVariant = 'success';
|
||||
if ($isRevoked) {
|
||||
$statusLabel = t('Revoked');
|
||||
$statusVariant = 'danger';
|
||||
} elseif ($isExpired) {
|
||||
$statusLabel = t('Expired');
|
||||
$statusVariant = 'warning';
|
||||
}
|
||||
?>
|
||||
<tr>
|
||||
<td><?php e($apiToken['name'] ?? ''); ?></td>
|
||||
<td><code><?php e(substr($apiToken['selector'] ?? '', 0, 8) . '...'); ?></code></td>
|
||||
<td><?php e(dt($apiToken['created'] ?? '')); ?></td>
|
||||
<td><?php e(!empty($apiToken['last_used_at']) ? dt($apiToken['last_used_at']) : '-'); ?></td>
|
||||
<td>
|
||||
<span class="badge" data-variant="<?php e($statusVariant); ?>">
|
||||
<?php e($statusLabel); ?>
|
||||
</span>
|
||||
</td>
|
||||
<?php if ($canManageApiTokens): ?>
|
||||
<td>
|
||||
<?php if (!$isRevoked): ?>
|
||||
<button type="submit"
|
||||
name="token_id"
|
||||
value="<?php e((string) ($apiToken['id'] ?? '')); ?>"
|
||||
class="outline"
|
||||
style="--pico-color: var(--app-danger);"
|
||||
formaction="<?php e(lurl("admin/users/api-token-revoke/{$values['uuid']}")); ?>"
|
||||
onclick="return confirm('<?php e(t('Revoke this API token?')); ?>');"
|
||||
><?php e(t('Revoke')); ?></button>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<?php endif; ?>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if (empty($apiTokens) && !$canManageApiTokens): ?>
|
||||
<p class="secondary"><?php e(t('No API tokens')); ?></p>
|
||||
<?php endif; ?>
|
||||
</details>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if ($showOrganizationTab): ?>
|
||||
<div data-tab-panel="organization">
|
||||
<?php if ($showOrganization): ?>
|
||||
<?php if ($showTenants): ?>
|
||||
<details name="assignments-tenants" data-details-key="assignments-tenants" open>
|
||||
<summary><?php e(t('Tenants')); ?></summary>
|
||||
<hr>
|
||||
<?php multiSelectForm('user-tenants', 'tenant_ids', 'Assigned tenants', 'Select tenants', $tenants, $selectedTenantIds, $isReadOnly); ?>
|
||||
<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>
|
||||
</details>
|
||||
<hr>
|
||||
<?php endif; ?>
|
||||
<?php if ($showDepartments): ?>
|
||||
<details name="assignments-departments" data-details-key="assignments-departments">
|
||||
<summary><?php e(t('Departments')); ?></summary>
|
||||
<hr>
|
||||
<?php
|
||||
$selectedTenantMap = [];
|
||||
foreach ($tenants as $tenant) {
|
||||
$tenantId = (int) ($tenant['id'] ?? 0);
|
||||
if ($tenantId <= 0 || !in_array($tenantId, $selectedTenantIds, true)) {
|
||||
continue;
|
||||
}
|
||||
$selectedTenantMap[$tenantId] = $tenant;
|
||||
}
|
||||
?>
|
||||
<?php if (!$selectedTenantMap): ?>
|
||||
<p class="muted"><?php e(t('Assign tenants first to select departments')); ?></p>
|
||||
<?php else: ?>
|
||||
<?php foreach ($selectedTenantMap as $tenantId => $tenant): ?>
|
||||
<?php $tenantDepartments = $departmentOptionsByTenant[$tenantId] ?? []; ?>
|
||||
<fieldset>
|
||||
<legend>
|
||||
<small><?php e(t('Departments for tenant')); ?>: <?php e($tenant['description'] ?? ''); ?></small>
|
||||
</legend>
|
||||
<?php if ($tenantDepartments): ?>
|
||||
<?php
|
||||
multiSelectForm(
|
||||
'user-departments-tenant-' . $tenantId,
|
||||
'department_ids',
|
||||
'Assigned departments',
|
||||
'Select departments',
|
||||
$tenantDepartments,
|
||||
$selectedDepartmentIds,
|
||||
$isReadOnly
|
||||
);
|
||||
?>
|
||||
<?php else: ?>
|
||||
<p class="muted"><?php e(t('No departments available for this tenant')); ?></p>
|
||||
<?php endif; ?>
|
||||
</fieldset>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</details>
|
||||
<?php if ($showRoles): ?><hr><?php endif; ?>
|
||||
<?php endif; ?>
|
||||
<?php if ($showRoles): ?>
|
||||
<details name="assignments-roles" data-details-key="assignments-roles">
|
||||
<summary><?php e(t('Roles')); ?></summary>
|
||||
<hr>
|
||||
<?php multiSelectForm('user-roles', 'role_ids', 'Assigned roles', 'Select roles', $roles, $selectedRoleIds, $isReadOnly); ?>
|
||||
</details>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
<?php if ($showPermissions): ?>
|
||||
<?php if ($showOrganization): ?><hr><?php endif; ?>
|
||||
<details name="assignments-permissions" data-details-key="assignments-permissions">
|
||||
<summary><?php e(t('Permissions')); ?></summary>
|
||||
<hr>
|
||||
<div class="overflow-auto white-space-nowrap">
|
||||
<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>
|
||||
</details>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($showCustomFieldsTab): ?>
|
||||
<div data-tab-panel="custom_fields">
|
||||
<?php if (!$customFieldTenantMap): ?>
|
||||
<p class="muted"><?php e(t('Assign tenants first to edit custom fields')); ?></p>
|
||||
<?php else: ?>
|
||||
<?php $hasRenderedCustomFields = false; ?>
|
||||
<?php foreach ($customFieldTenantMap as $tenantId => $tenant): ?>
|
||||
<?php $tenantDefinitions = $customFieldDefinitionsByTenant[$tenantId] ?? []; ?>
|
||||
<details name="custom-fields-tenant" data-details-key="custom-fields-tenant-<?php e((string) $tenantId); ?>">
|
||||
<summary>
|
||||
<?php e((string) ($tenant['description'] ?? t('Custom fields for tenant'))); ?>
|
||||
</summary>
|
||||
<hr>
|
||||
<?php if (!$tenantDefinitions): ?>
|
||||
<p class="muted"><?php e(t('No custom fields configured for this tenant')); ?></p>
|
||||
<?php else: ?>
|
||||
<div>
|
||||
<?php foreach ($tenantDefinitions as $definition): ?>
|
||||
<?php
|
||||
$definitionId = (int) ($definition['id'] ?? 0);
|
||||
$definitionType = strtolower((string) ($definition['type'] ?? 'text'));
|
||||
$definitionLabel = (string) ($definition['label'] ?? '');
|
||||
$definitionOptions = is_array($definition['options'] ?? null) ? $definition['options'] : [];
|
||||
if ($definitionId <= 0 || $definitionLabel === '') {
|
||||
continue;
|
||||
}
|
||||
$hasRenderedCustomFields = true;
|
||||
$currentValue = $customFieldValueMap[$definitionId] ?? [];
|
||||
$scalarPostedKey = (string) $definitionId;
|
||||
$scalarPosted = array_key_exists($scalarPostedKey, $customFieldScalarPosted)
|
||||
? $customFieldScalarPosted[$scalarPostedKey]
|
||||
: null;
|
||||
$multiPostedRaw = $customFieldMultiPosted[$scalarPostedKey] ?? null;
|
||||
$multiPosted = is_array($multiPostedRaw)
|
||||
? array_values(array_unique(array_map('intval', $multiPostedRaw)))
|
||||
: [];
|
||||
$multiPosted = array_values(array_filter($multiPosted, static fn (int $id): bool => $id > 0));
|
||||
$currentOptionIds = is_array($currentValue['option_ids'] ?? null)
|
||||
? array_values(array_unique(array_map('intval', $currentValue['option_ids'])))
|
||||
: [];
|
||||
$currentOptionIds = array_values(array_filter($currentOptionIds, static fn (int $id): bool => $id > 0));
|
||||
$valueText = array_key_exists('value_text', $currentValue) ? (string) ($currentValue['value_text'] ?? '') : '';
|
||||
$valueBool = array_key_exists('value_bool', $currentValue) && $currentValue['value_bool'] !== null
|
||||
? (string) ((int) $currentValue['value_bool'])
|
||||
: '';
|
||||
$valueDate = array_key_exists('value_date', $currentValue) ? (string) ($currentValue['value_date'] ?? '') : '';
|
||||
$valueOptionId = array_key_exists('option_id', $currentValue) && $currentValue['option_id'] !== null
|
||||
? (int) $currentValue['option_id']
|
||||
: 0;
|
||||
?>
|
||||
<div>
|
||||
<?php if (in_array($definitionType, ['text', 'textarea'], true)): ?>
|
||||
<?php $fieldValue = $scalarPosted !== null ? trim((string) $scalarPosted) : $valueText; ?>
|
||||
<label for="custom-field-<?php e((string) $definitionId); ?>">
|
||||
<span>
|
||||
<?php e($definitionLabel); ?>
|
||||
</span>
|
||||
<?php if ($definitionType === 'textarea'): ?>
|
||||
<textarea
|
||||
id="custom-field-<?php e((string) $definitionId); ?>"
|
||||
name="custom_field_values[<?php e((string) $definitionId); ?>]"
|
||||
rows="4"
|
||||
<?php e($customFieldInputsDisabledAttr); ?>
|
||||
><?php e($fieldValue); ?></textarea>
|
||||
<?php else: ?>
|
||||
<input
|
||||
id="custom-field-<?php e((string) $definitionId); ?>"
|
||||
type="text"
|
||||
name="custom_field_values[<?php e((string) $definitionId); ?>]"
|
||||
value="<?php e($fieldValue); ?>"
|
||||
<?php e($customFieldInputsDisabledAttr); ?>
|
||||
>
|
||||
<?php endif; ?>
|
||||
</label>
|
||||
<?php elseif ($definitionType === 'date'): ?>
|
||||
<?php $fieldValue = $scalarPosted !== null ? trim((string) $scalarPosted) : $valueDate; ?>
|
||||
<label for="custom-field-<?php e((string) $definitionId); ?>">
|
||||
<span>
|
||||
<?php e($definitionLabel); ?>
|
||||
</span>
|
||||
<input
|
||||
id="custom-field-<?php e((string) $definitionId); ?>"
|
||||
type="date"
|
||||
name="custom_field_values[<?php e((string) $definitionId); ?>]"
|
||||
value="<?php e($fieldValue); ?>"
|
||||
<?php e($customFieldInputsDisabledAttr); ?>
|
||||
>
|
||||
</label>
|
||||
<?php elseif ($definitionType === 'boolean'): ?>
|
||||
<?php $fieldValue = $scalarPosted !== null ? strtolower(trim((string) $scalarPosted)) : $valueBool; ?>
|
||||
<label for="custom-field-<?php e((string) $definitionId); ?>">
|
||||
<span>
|
||||
<?php e($definitionLabel); ?>
|
||||
</span>
|
||||
<select
|
||||
id="custom-field-<?php e((string) $definitionId); ?>"
|
||||
name="custom_field_values[<?php e((string) $definitionId); ?>]"
|
||||
<?php e($customFieldInputsDisabledAttr); ?>
|
||||
>
|
||||
<option value=""><?php e(t('Please select')); ?></option>
|
||||
<option value="1" <?php e($fieldValue === '1' || $fieldValue === 'true' ? 'selected' : ''); ?>>
|
||||
<?php e(t('Yes')); ?>
|
||||
</option>
|
||||
<option value="0" <?php e($fieldValue === '0' || $fieldValue === 'false' ? 'selected' : ''); ?>>
|
||||
<?php e(t('No')); ?>
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
<?php elseif ($definitionType === 'select'): ?>
|
||||
<?php
|
||||
$fieldValue = $scalarPosted !== null ? (int) $scalarPosted : $valueOptionId;
|
||||
?>
|
||||
<label for="custom-field-<?php e((string) $definitionId); ?>">
|
||||
<span>
|
||||
<?php e($definitionLabel); ?>
|
||||
</span>
|
||||
<select
|
||||
id="custom-field-<?php e((string) $definitionId); ?>"
|
||||
name="custom_field_values[<?php e((string) $definitionId); ?>]"
|
||||
<?php e($customFieldInputsDisabledAttr); ?>
|
||||
>
|
||||
<option value=""><?php e(t('Please select')); ?></option>
|
||||
<?php foreach ($definitionOptions as $option): ?>
|
||||
<?php
|
||||
$optionId = (int) ($option['id'] ?? 0);
|
||||
if ($optionId <= 0) {
|
||||
continue;
|
||||
}
|
||||
?>
|
||||
<option value="<?php e((string) $optionId); ?>" <?php e($fieldValue === $optionId ? 'selected' : ''); ?>>
|
||||
<?php e((string) ($option['label'] ?? '')); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</label>
|
||||
<?php elseif ($definitionType === 'multiselect'): ?>
|
||||
<?php
|
||||
$selectedOptions = $multiPostedRaw !== null ? $multiPosted : $currentOptionIds;
|
||||
multiSelectForm(
|
||||
'user-custom-field-' . $definitionId,
|
||||
'custom_field_values_multi[' . $definitionId . ']',
|
||||
$definitionLabel,
|
||||
'Select options',
|
||||
$definitionOptions,
|
||||
$selectedOptions,
|
||||
$customFieldInputsDisabled,
|
||||
['label', 'description']
|
||||
);
|
||||
?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</details>
|
||||
<hr>
|
||||
<?php endforeach; ?>
|
||||
<?php if (!$hasRenderedCustomFields): ?>
|
||||
<p class="muted"><?php e(t('No custom fields configured for selected tenants')); ?></p>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div data-tab-panel="visibility">
|
||||
<?php
|
||||
$statusFieldName = 'active';
|
||||
$statusFieldId = 'user-active';
|
||||
$statusFieldValue = (string) (($values['active'] ?? 1) ? '1' : '0');
|
||||
$statusFieldOptions = [
|
||||
'1' => t('Active'),
|
||||
'0' => t('Inactive'),
|
||||
];
|
||||
$statusFieldHint = $visibilityHint;
|
||||
$statusFieldDisabledAttr = $visibilityDisabledAttr;
|
||||
require templatePath('partials/app-visibility-status-field.phtml');
|
||||
?>
|
||||
</div>
|
||||
<?php if ($showDangerZone || $showTokenDangerAction): ?>
|
||||
<div data-tab-panel="danger">
|
||||
<?php if ($showTokenDangerAction && $tokenDangerFormId !== ''): ?>
|
||||
<fieldset>
|
||||
<legend><small><?php e(t('Login tokens')); ?></small></legend>
|
||||
<p class="muted"><?php e($tokenDangerWarning); ?></p>
|
||||
<button type="submit" class="danger" form="<?php e($tokenDangerFormId); ?>">
|
||||
<?php e($tokenDangerActionLabel); ?>
|
||||
</button>
|
||||
</fieldset>
|
||||
<?php endif; ?>
|
||||
<?php if ($showDangerZone && $dangerZoneDeleteFormId !== ''): ?>
|
||||
<?php
|
||||
$dangerFormId = $dangerZoneDeleteFormId;
|
||||
$dangerLegend = t('Delete');
|
||||
$dangerWarning = $dangerZoneWarning;
|
||||
$dangerButtonLabel = $dangerZoneActionLabel;
|
||||
require templatePath('partials/app-danger-zone-delete-field.phtml');
|
||||
?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
58
pages/admin/users/access-pdf($id).php
Normal file
58
pages/admin/users/access-pdf($id).php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Service\Access\PermissionService;
|
||||
use MintyPHP\Service\Tenant\TenantScopeService;
|
||||
use MintyPHP\Service\User\UserAccessPdfService;
|
||||
use MintyPHP\Service\User\UserService;
|
||||
use MintyPHP\Router;
|
||||
|
||||
define('MINTY_ALLOW_OUTPUT', true);
|
||||
|
||||
// Hard gate: user must be logged in and explicitly allowed to generate access PDFs.
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermissionOrForbidden(PermissionService::USERS_ACCESS_PDF);
|
||||
|
||||
// Support both route param and POSTed uuid (POST avoids exposing uuid in URL/history).
|
||||
$uuid = trim((string) ($id ?? ''));
|
||||
if ($uuid === '' && strtoupper((string) ($_SERVER['REQUEST_METHOD'] ?? 'GET')) === 'POST') {
|
||||
$uuid = trim((string) ($_POST['uuid'] ?? ''));
|
||||
}
|
||||
$user = $uuid !== '' ? UserService::findByUuid($uuid) : null;
|
||||
if (!$user) {
|
||||
http_response_code(404);
|
||||
return;
|
||||
}
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
$userId = (int) ($user['id'] ?? 0);
|
||||
// Enforce tenant scope on the target user before rendering any sensitive document.
|
||||
if ($userId <= 0 || !TenantScopeService::canAccess('users', $userId, $currentUserId)) {
|
||||
Router::redirect('error/forbidden');
|
||||
return;
|
||||
}
|
||||
|
||||
$pdf = UserAccessPdfService::renderUserAccessPdf($user);
|
||||
if ($pdf === '') {
|
||||
// Keep error generic; service handles concrete renderer/asset failures internally.
|
||||
http_response_code(500);
|
||||
return;
|
||||
}
|
||||
|
||||
$locale = strtolower((string) (\MintyPHP\I18n::$locale ?? \MintyPHP\I18n::$defaultLocale));
|
||||
$filename = str_starts_with($locale, 'de') ? 'zugangsdaten.pdf' : 'access-details.pdf';
|
||||
|
||||
header('Content-Type: application/pdf');
|
||||
header('Content-Disposition: inline; filename="' . $filename . '"');
|
||||
header('Cache-Control: private, no-store, max-age=0');
|
||||
header('Pragma: no-cache');
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
header('Content-Length: ' . strlen($pdf));
|
||||
|
||||
$output = fopen('php://output', 'wb');
|
||||
if ($output === false) {
|
||||
http_response_code(500);
|
||||
return;
|
||||
}
|
||||
fwrite($output, $pdf);
|
||||
fclose($output);
|
||||
100
pages/admin/users/access-pdf-bulk().php
Normal file
100
pages/admin/users/access-pdf-bulk().php
Normal file
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\Access\PermissionService;
|
||||
use MintyPHP\Service\Tenant\TenantScopeService;
|
||||
use MintyPHP\Service\User\UserAccessPdfService;
|
||||
use MintyPHP\Service\User\UserService;
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
|
||||
define('MINTY_ALLOW_OUTPUT', true);
|
||||
|
||||
// Hard gate: user must be logged in and explicitly allowed to generate access PDFs.
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermissionOrForbidden(PermissionService::USERS_ACCESS_PDF);
|
||||
|
||||
if (strtoupper((string) ($_SERVER['REQUEST_METHOD'] ?? 'GET')) !== 'POST') {
|
||||
Router::redirect('admin/users');
|
||||
return;
|
||||
}
|
||||
|
||||
$raw = $_POST['uuids'] ?? '';
|
||||
$uuids = [];
|
||||
if (is_array($raw)) {
|
||||
$uuids = $raw;
|
||||
} elseif (is_string($raw)) {
|
||||
$uuids = explode(',', $raw);
|
||||
}
|
||||
$uuids = array_values(array_unique(array_filter(array_map('trim', $uuids))));
|
||||
if (!$uuids) {
|
||||
Flash::error(t('No valid users selected'), 'admin/users', 'users_access_pdf_no_selection');
|
||||
Router::redirect('admin/users');
|
||||
return;
|
||||
}
|
||||
|
||||
if (count($uuids) > 100) {
|
||||
// Server-side hard limit to keep synchronous ZIP generation predictable.
|
||||
Flash::error(t('Too many users selected'), 'admin/users', 'users_access_pdf_too_many');
|
||||
Router::redirect('admin/users');
|
||||
return;
|
||||
}
|
||||
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
$users = [];
|
||||
foreach ($uuids as $uuid) {
|
||||
$user = UserService::findByUuid($uuid);
|
||||
if (!$user) {
|
||||
continue;
|
||||
}
|
||||
$userId = (int) ($user['id'] ?? 0);
|
||||
if ($userId <= 0) {
|
||||
continue;
|
||||
}
|
||||
// Scope check per user prevents cross-tenant exports in mixed selections.
|
||||
if (!TenantScopeService::canAccess('users', $userId, $currentUserId)) {
|
||||
continue;
|
||||
}
|
||||
$users[] = $user;
|
||||
}
|
||||
|
||||
if (!$users) {
|
||||
Flash::error(t('No valid users selected'), 'admin/users', 'users_access_pdf_not_allowed');
|
||||
Router::redirect('admin/users');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!class_exists(ZipArchive::class)) {
|
||||
// Explicit runtime dependency check for environments missing ext-zip.
|
||||
Flash::error(t('ZIP support missing (ext-zip)'), 'admin/users', 'users_access_pdf_zip_missing');
|
||||
Router::redirect('admin/users');
|
||||
return;
|
||||
}
|
||||
|
||||
$zip = UserAccessPdfService::renderUsersAccessZip($users);
|
||||
$zipPath = (string) ($zip['path'] ?? '');
|
||||
$zipFilename = trim((string) ($zip['filename'] ?? ''));
|
||||
$zipCount = (int) ($zip['count'] ?? 0);
|
||||
|
||||
if ($zipPath === '' || !is_file($zipPath) || $zipCount <= 0) {
|
||||
Flash::error(t('Failed to generate access PDF'), 'admin/users', 'users_access_pdf_failed');
|
||||
Router::redirect('admin/users');
|
||||
return;
|
||||
}
|
||||
|
||||
if ($zipFilename === '') {
|
||||
$zipFilename = 'user-invitations-' . gmdate('Ymd-His') . '.zip';
|
||||
}
|
||||
|
||||
try {
|
||||
header('Content-Type: application/zip');
|
||||
header('Content-Disposition: attachment; filename="' . $zipFilename . '"');
|
||||
header('Cache-Control: private, no-store, max-age=0');
|
||||
header('Pragma: no-cache');
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
header('Content-Length: ' . filesize($zipPath));
|
||||
readfile($zipPath);
|
||||
} finally {
|
||||
// Always delete temporary archive, including partial-failure paths.
|
||||
@unlink($zipPath);
|
||||
}
|
||||
38
pages/admin/users/api-token-create($id).php
Normal file
38
pages/admin/users/api-token-create($id).php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\Access\PermissionService;
|
||||
use MintyPHP\Service\Auth\ApiTokenService;
|
||||
use MintyPHP\Service\User\UserService;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermissionOrForbidden(PermissionService::API_TOKENS_MANAGE);
|
||||
|
||||
$uuid = trim((string) ($id ?? ''));
|
||||
$user = $uuid !== '' ? UserService::findByUuid($uuid) : null;
|
||||
if (!$user) {
|
||||
Router::redirect('admin/users');
|
||||
}
|
||||
|
||||
$userId = (int) ($user['id'] ?? 0);
|
||||
$currentUserId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
$name = trim((string) ($_POST['token_name'] ?? ''));
|
||||
|
||||
if ($name === '') {
|
||||
Flash::error(t('Token name is required'), "admin/users/edit/{$uuid}", 'api_token_error');
|
||||
Router::redirect("admin/users/edit/{$uuid}");
|
||||
}
|
||||
|
||||
$result = ApiTokenService::create($userId, $name, null, null, $currentUserId);
|
||||
|
||||
if ($result['ok'] ?? false) {
|
||||
$_SESSION['api_token_created'] = ['token' => $result['token'], 'uuid' => $uuid];
|
||||
Flash::success(t('API token created. Copy it now — it will not be shown again.'), "admin/users/edit/{$uuid}", 'api_token_created');
|
||||
} else {
|
||||
$error = $result['error'] ?? 'create_failed';
|
||||
Flash::error(t('Could not create API token: %s', $error), "admin/users/edit/{$uuid}", 'api_token_error');
|
||||
}
|
||||
|
||||
Router::redirect("admin/users/edit/{$uuid}");
|
||||
26
pages/admin/users/api-token-revoke($id).php
Normal file
26
pages/admin/users/api-token-revoke($id).php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\Access\PermissionService;
|
||||
use MintyPHP\Service\Auth\ApiTokenService;
|
||||
use MintyPHP\Service\User\UserService;
|
||||
|
||||
Guard::requireLogin();
|
||||
Guard::requirePermissionOrForbidden(PermissionService::API_TOKENS_MANAGE);
|
||||
|
||||
$uuid = trim((string) ($id ?? ''));
|
||||
$user = $uuid !== '' ? UserService::findByUuid($uuid) : null;
|
||||
if (!$user) {
|
||||
Router::redirect('admin/users');
|
||||
}
|
||||
|
||||
$tokenId = (int) ($_POST['token_id'] ?? 0);
|
||||
|
||||
if ($tokenId > 0) {
|
||||
ApiTokenService::revoke($tokenId);
|
||||
Flash::success(t('API token revoked'), "admin/users/edit/{$uuid}", 'api_token_revoked');
|
||||
}
|
||||
|
||||
Router::redirect("admin/users/edit/{$uuid}");
|
||||
@@ -10,14 +10,14 @@ use MintyPHP\Service\Tenant\TenantScopeService;
|
||||
Guard::requireLogin();
|
||||
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
|
||||
Router::redirect('admin/users');
|
||||
Router::redirect('admin');
|
||||
return;
|
||||
}
|
||||
|
||||
$uuid = trim((string) ($id ?? ''));
|
||||
if (!UserAvatarService::isValidUuid($uuid)) {
|
||||
Flash::error('User not found', 'admin/users', 'user_not_found');
|
||||
Router::redirect('admin/users');
|
||||
Flash::error('User not found', 'admin', 'user_not_found');
|
||||
Router::redirect('admin');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,14 +10,14 @@ use MintyPHP\Service\Tenant\TenantScopeService;
|
||||
Guard::requireLogin();
|
||||
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
|
||||
Router::redirect('admin/users');
|
||||
Router::redirect('admin');
|
||||
return;
|
||||
}
|
||||
|
||||
$uuid = trim((string) ($id ?? ''));
|
||||
if (!UserAvatarService::isValidUuid($uuid)) {
|
||||
Flash::error('User not found', 'admin/users', 'user_not_found');
|
||||
Router::redirect('admin/users');
|
||||
Flash::error('User not found', 'admin', 'user_not_found');
|
||||
Router::redirect('admin');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Http\Request;
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Service\Access\PermissionService;
|
||||
use MintyPHP\Service\User\UserService;
|
||||
use MintyPHP\Service\User\UserAccessTemplateService;
|
||||
use MintyPHP\Service\Mail\MailService;
|
||||
use MintyPHP\I18n;
|
||||
|
||||
Guard::requireLogin();
|
||||
|
||||
@@ -92,32 +91,10 @@ function handleSendAccess(array $uuids, int $currentUserId): void
|
||||
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,
|
||||
];
|
||||
$context = UserAccessTemplateService::buildContext($user);
|
||||
$locale = (string) ($context['locale'] ?? '');
|
||||
$subject = (string) ($context['subject'] ?? t('Your access details'));
|
||||
$vars = is_array($context['vars'] ?? null) ? $context['vars'] : [];
|
||||
|
||||
$result = MailService::sendTemplate('access_info', $vars, (string) $user['email'], $subject, $locale);
|
||||
if ($result['ok'] ?? false) {
|
||||
|
||||
@@ -11,6 +11,7 @@ use MintyPHP\Service\Tenant\TenantScopeService;
|
||||
use MintyPHP\Service\Tenant\TenantService;
|
||||
use MintyPHP\Service\Access\RoleService;
|
||||
use MintyPHP\Service\Org\DepartmentService;
|
||||
use MintyPHP\Service\CustomField\UserCustomFieldValueService;
|
||||
use MintyPHP\Service\Settings\SettingService;
|
||||
|
||||
Guard::requirePermissionOrForbidden(PermissionService::USERS_CREATE);
|
||||
@@ -28,10 +29,18 @@ if ($allowedTenantIds) {
|
||||
$tenants = [];
|
||||
}
|
||||
$roles = RoleService::listActive();
|
||||
$departments = $allowedTenantIds ? DepartmentService::listByTenantIds($allowedTenantIds) : [];
|
||||
if (!$allowedTenantIds && !TenantScopeService::isStrict()) {
|
||||
$departments = DepartmentService::list();
|
||||
}
|
||||
$selectedTenantIds = [];
|
||||
$selectedRoleIds = [];
|
||||
$selectedDepartmentIds = [];
|
||||
$departmentOptionsByTenant = [];
|
||||
$canEditCustomFieldValues = PermissionService::userHas($currentUserId, PermissionService::CUSTOM_FIELDS_EDIT_VALUES)
|
||||
&& PermissionService::userHas($currentUserId, PermissionService::USERS_UPDATE_ASSIGNMENTS);
|
||||
$customFieldDefinitionsByTenant = [];
|
||||
$customFieldValueMap = [];
|
||||
$customFieldPostedValues = [
|
||||
'custom_field_values' => [],
|
||||
'custom_field_values_multi' => [],
|
||||
];
|
||||
|
||||
$errors = [];
|
||||
$form = [
|
||||
@@ -66,18 +75,75 @@ if (isset($_POST['email'])) {
|
||||
if (TenantScopeService::isStrict() && !$tenantIds && !$defaultTenantId) {
|
||||
$errors[] = t('Please select at least one tenant');
|
||||
}
|
||||
$selectedTenantIds = $tenantIds;
|
||||
$selectedRoleIds = UserService::normalizeIdInput($input['role_ids'] ?? []);
|
||||
$selectedDepartmentIds = UserService::normalizeIdInput($input['department_ids'] ?? []);
|
||||
$departmentOptionsByTenant = DepartmentService::groupActiveByTenantIds($selectedTenantIds);
|
||||
$customFieldDefinitionsByTenant = UserCustomFieldValueService::buildDefinitionsByTenant($selectedTenantIds);
|
||||
$customFieldPostedValues = [
|
||||
'custom_field_values' => is_array($_POST['custom_field_values'] ?? null) ? $_POST['custom_field_values'] : [],
|
||||
'custom_field_values_multi' => is_array($_POST['custom_field_values_multi'] ?? null) ? $_POST['custom_field_values_multi'] : [],
|
||||
];
|
||||
if ($canEditCustomFieldValues) {
|
||||
$customFieldValidationResult = UserCustomFieldValueService::validateForTenants(
|
||||
$selectedTenantIds,
|
||||
$_POST,
|
||||
true
|
||||
);
|
||||
if (!($customFieldValidationResult['ok'] ?? false)) {
|
||||
$errors = array_merge($errors, $customFieldValidationResult['errors'] ?? []);
|
||||
foreach (array_keys($form) as $fieldKey) {
|
||||
if (!array_key_exists($fieldKey, $input)) {
|
||||
continue;
|
||||
}
|
||||
$fieldValue = $input[$fieldKey];
|
||||
if (is_scalar($fieldValue)) {
|
||||
$form[$fieldKey] = trim((string) $fieldValue);
|
||||
}
|
||||
}
|
||||
$form['active'] = array_key_exists('active', $input) ? 1 : 0;
|
||||
}
|
||||
}
|
||||
$input['tenant_ids'] = $tenantIds;
|
||||
$result = UserService::createFromAdmin($input, $currentUserId);
|
||||
$form = $result['form'] ?? $form;
|
||||
$errors = array_merge($errors, $result['errors'] ?? []);
|
||||
$input['role_ids'] = $selectedRoleIds;
|
||||
$input['department_ids'] = $selectedDepartmentIds;
|
||||
$result = ['ok' => false];
|
||||
if (!$errors) {
|
||||
$result = UserService::createFromAdmin($input, $currentUserId);
|
||||
$form = $result['form'] ?? $form;
|
||||
$errors = array_merge($errors, $result['errors'] ?? []);
|
||||
}
|
||||
|
||||
if (($result['ok'] ?? false) && !$errors) {
|
||||
$createdUuid = (string) ($result['uuid'] ?? '');
|
||||
if ($canEditCustomFieldValues && $createdUuid !== '') {
|
||||
$createdUser = UserService::findByUuid($createdUuid);
|
||||
$createdUserId = (int) ($createdUser['id'] ?? 0);
|
||||
if ($createdUserId > 0) {
|
||||
$customFieldSyncResult = UserCustomFieldValueService::syncForUser(
|
||||
$createdUserId,
|
||||
$selectedTenantIds,
|
||||
$_POST,
|
||||
true
|
||||
);
|
||||
if (!($customFieldSyncResult['ok'] ?? false)) {
|
||||
$syncErrors = $customFieldSyncResult['errors'] ?? [];
|
||||
if ($syncErrors) {
|
||||
Flash::error(
|
||||
implode(' | ', $syncErrors),
|
||||
"admin/users/edit/{$createdUuid}",
|
||||
'user_custom_field_sync_failed'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$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'] ?? '');
|
||||
$uuid = $createdUuid;
|
||||
if ($uuid !== '') {
|
||||
$target = "admin/users/edit/{$uuid}";
|
||||
Flash::success('User created', $target, 'user_created');
|
||||
@@ -90,4 +156,11 @@ if (isset($_POST['email'])) {
|
||||
}
|
||||
}
|
||||
|
||||
if (!$departmentOptionsByTenant && $selectedTenantIds) {
|
||||
$departmentOptionsByTenant = DepartmentService::groupActiveByTenantIds($selectedTenantIds);
|
||||
}
|
||||
if (!$customFieldDefinitionsByTenant && $selectedTenantIds) {
|
||||
$customFieldDefinitionsByTenant = UserCustomFieldValueService::buildDefinitionsByTenant($selectedTenantIds);
|
||||
}
|
||||
|
||||
Buffer::set('title', t('Create user'));
|
||||
|
||||
@@ -18,21 +18,29 @@
|
||||
['label' => t('Create user')],
|
||||
];
|
||||
require templatePath('partials/app-breadcrumb.phtml');
|
||||
$titlebar = [
|
||||
'title' => t('Create user'),
|
||||
'backHref' => 'admin/users',
|
||||
'backTitle' => t('Cancel'),
|
||||
'actions' => [
|
||||
[
|
||||
'form' => 'user-form',
|
||||
'name' => 'action',
|
||||
'value' => 'create',
|
||||
'class' => 'secondary outline',
|
||||
'label' => t('Create'),
|
||||
],
|
||||
[
|
||||
'form' => 'user-form',
|
||||
'name' => 'action',
|
||||
'value' => 'create_close',
|
||||
'class' => 'primary',
|
||||
'label' => t('Create & close'),
|
||||
],
|
||||
],
|
||||
];
|
||||
require templatePath('partials/app-details-titlebar.phtml');
|
||||
?>
|
||||
<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>
|
||||
@@ -49,6 +57,20 @@
|
||||
$passwordLabel = t('Password');
|
||||
$passwordLegend = t('Password');
|
||||
$passwordConfirmLabel = t('Password (again)');
|
||||
$showTenants = can('users.update_assignments');
|
||||
$showRoles = $showTenants;
|
||||
$showDepartments = $showTenants;
|
||||
$tenants = $tenants ?? [];
|
||||
$roles = $roles ?? [];
|
||||
$selectedTenantIds = $selectedTenantIds ?? [];
|
||||
$selectedRoleIds = $selectedRoleIds ?? [];
|
||||
$selectedDepartmentIds = $selectedDepartmentIds ?? [];
|
||||
$departmentOptionsByTenant = $departmentOptionsByTenant ?? [];
|
||||
$showCustomFieldsTab = true;
|
||||
$canEditCustomFieldValues = !empty($canEditCustomFieldValues);
|
||||
$customFieldDefinitionsByTenant = $customFieldDefinitionsByTenant ?? [];
|
||||
$customFieldValueMap = $customFieldValueMap ?? [];
|
||||
$customFieldPostedValues = $customFieldPostedValues ?? [];
|
||||
require __DIR__ . '/_form.phtml';
|
||||
?>
|
||||
</section>
|
||||
|
||||
@@ -5,6 +5,7 @@ use MintyPHP\Support\Flash;
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\I18n;
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\DB;
|
||||
use MintyPHP\Repository\Access\UserRoleRepository;
|
||||
use MintyPHP\Repository\Tenant\UserTenantRepository;
|
||||
use MintyPHP\Repository\Org\UserDepartmentRepository;
|
||||
@@ -12,6 +13,7 @@ use MintyPHP\Repository\Access\RolePermissionRepository;
|
||||
use MintyPHP\Repository\Auth\PasswordResetRepository;
|
||||
use MintyPHP\Repository\Auth\RememberTokenRepository;
|
||||
use MintyPHP\Service\Auth\AuthService;
|
||||
use MintyPHP\Service\CustomField\UserCustomFieldValueService;
|
||||
use MintyPHP\Service\Org\DepartmentService;
|
||||
use MintyPHP\Service\Access\RoleService;
|
||||
use MintyPHP\Service\Tenant\TenantService;
|
||||
@@ -103,11 +105,26 @@ if (!$primaryTenantId && count($selectedTenantIds) === 1) {
|
||||
$roles = RoleService::listActive();
|
||||
$selectedRoleIds = UserRoleRepository::listRoleIdsByUserId($userId);
|
||||
$permissionRows = RolePermissionRepository::listPermissionsWithRolesByRoleIds($selectedRoleIds);
|
||||
$departments = DepartmentService::listForUserAssignments($selectedTenantIds, []);
|
||||
$selectedDepartmentIds = UserDepartmentRepository::listDepartmentIdsByUserId($userId);
|
||||
if ($selectedDepartmentIds) {
|
||||
$departments = DepartmentService::listForUserAssignments($selectedTenantIds, $selectedDepartmentIds);
|
||||
$departmentOptionsByTenant = DepartmentService::groupActiveByTenantIds($selectedTenantIds);
|
||||
$canEditCustomFieldValues = $canEditUser
|
||||
&& (PermissionService::userHas($currentUserId, PermissionService::CUSTOM_FIELDS_EDIT_VALUES)
|
||||
|| ($isOwnAccount && $canUpdateSelf));
|
||||
$customFieldDefinitionsByTenant = UserCustomFieldValueService::buildDefinitionsByTenant($selectedTenantIds);
|
||||
$customFieldDefinitionIds = [];
|
||||
foreach ($customFieldDefinitionsByTenant as $tenantDefinitions) {
|
||||
foreach ($tenantDefinitions as $definition) {
|
||||
$definitionId = (int) ($definition['id'] ?? 0);
|
||||
if ($definitionId > 0) {
|
||||
$customFieldDefinitionIds[] = $definitionId;
|
||||
}
|
||||
}
|
||||
}
|
||||
$customFieldValueMap = UserCustomFieldValueService::buildUserValueMap($userId, $customFieldDefinitionIds);
|
||||
$customFieldPostedValues = [
|
||||
'custom_field_values' => [],
|
||||
'custom_field_values_multi' => [],
|
||||
];
|
||||
$passwordResets = [];
|
||||
if ($canViewUsers || $canEditUser) {
|
||||
$passwordResets = PasswordResetRepository::listByUserId($userId, 25);
|
||||
@@ -116,6 +133,13 @@ $rememberTokens = [];
|
||||
if ($canViewUsers || $canEditUser) {
|
||||
$rememberTokens = RememberTokenRepository::listByUserId($userId, 25);
|
||||
}
|
||||
$apiTokens = [];
|
||||
$canManageApiTokens = $canEditUser
|
||||
&& PermissionService::userHas($currentUserId, PermissionService::API_TOKENS_MANAGE);
|
||||
if ($canViewUsers || $canEditUser) {
|
||||
$apiTokens = \MintyPHP\Service\Auth\ApiTokenService::listForUser($userId);
|
||||
}
|
||||
$showApiTokens = !empty($apiTokens) || $canManageApiTokens;
|
||||
// Keep initial $isOwnAccount/$canEditUser values for view permissions.
|
||||
|
||||
if (isset($_POST['email'])) {
|
||||
@@ -127,28 +151,77 @@ if (isset($_POST['email'])) {
|
||||
$form = $result['form'] ?? $form;
|
||||
$errors = $result['errors'] ?? [];
|
||||
$tenantIds = UserService::normalizeIdInput($_POST['tenant_ids'] ?? []);
|
||||
if ($allowedTenantIds) {
|
||||
$existingTenantIds = UserTenantRepository::listTenantIdsByUserId($userId);
|
||||
$tenantIdsForSync = $tenantIds;
|
||||
if ($canEditAssignments && $allowedTenantIds) {
|
||||
$tenantIds = array_values(array_intersect($tenantIds, $allowedTenantIds));
|
||||
} elseif (!$canManageTenants && TenantScopeService::isStrict()) {
|
||||
$tenantIdsForSync = TenantScopeService::mergeTenantIdsPreservingOutOfScope(
|
||||
$tenantIds,
|
||||
$existingTenantIds,
|
||||
$allowedTenantIds
|
||||
);
|
||||
} elseif ($canEditAssignments && !$canManageTenants && TenantScopeService::isStrict()) {
|
||||
$tenantIds = [];
|
||||
$tenantIdsForSync = $existingTenantIds;
|
||||
}
|
||||
if (!$canEditAssignments) {
|
||||
$tenantIds = UserTenantRepository::listTenantIdsByUserId($userId);
|
||||
if ($allowedTenantIds) {
|
||||
$tenantIds = array_values(array_intersect($tenantIds, $allowedTenantIds));
|
||||
}
|
||||
$tenantIdsForSync = $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);
|
||||
$departmentOptionsByTenant = DepartmentService::groupActiveByTenantIds($selectedTenantIds);
|
||||
$customFieldDefinitionsByTenant = UserCustomFieldValueService::buildDefinitionsByTenant($selectedTenantIds);
|
||||
$customFieldDefinitionIds = [];
|
||||
foreach ($customFieldDefinitionsByTenant as $tenantDefinitions) {
|
||||
foreach ($tenantDefinitions as $definition) {
|
||||
$definitionId = (int) ($definition['id'] ?? 0);
|
||||
if ($definitionId > 0) {
|
||||
$customFieldDefinitionIds[] = $definitionId;
|
||||
}
|
||||
}
|
||||
}
|
||||
$customFieldValueMap = UserCustomFieldValueService::buildUserValueMap($userId, $customFieldDefinitionIds);
|
||||
$customFieldPostedValues = [
|
||||
'custom_field_values' => is_array($_POST['custom_field_values'] ?? null) ? $_POST['custom_field_values'] : [],
|
||||
'custom_field_values_multi' => is_array($_POST['custom_field_values_multi'] ?? null) ? $_POST['custom_field_values_multi'] : [],
|
||||
];
|
||||
|
||||
if ($result['ok'] ?? false) {
|
||||
if ($canEditAssignments) {
|
||||
UserService::syncTenants($userId, $tenantIds);
|
||||
UserService::syncRoles($userId, $selectedRoleIds);
|
||||
UserService::syncDepartments($userId, $selectedDepartmentIds);
|
||||
$db = DB::handle();
|
||||
$db->begin_transaction();
|
||||
try {
|
||||
UserService::syncTenants($userId, $tenantIdsForSync, false);
|
||||
UserService::syncRoles($userId, $selectedRoleIds, false);
|
||||
UserService::syncDepartments($userId, $selectedDepartmentIds, false);
|
||||
UserService::bumpAuthzVersion($userId);
|
||||
$db->commit();
|
||||
} catch (\Throwable $exception) {
|
||||
$db->rollback();
|
||||
$errors[] = t('User assignments can not be updated');
|
||||
}
|
||||
|
||||
// Update session if editing own account (tenant assignments changed)
|
||||
if ($currentUserId === $userId) {
|
||||
if ($currentUserId === $userId && !$errors) {
|
||||
AuthService::loadTenantDataIntoSession($userId);
|
||||
}
|
||||
}
|
||||
$customFieldSyncResult = UserCustomFieldValueService::syncForUser(
|
||||
$userId,
|
||||
$selectedTenantIds,
|
||||
$_POST,
|
||||
$canEditCustomFieldValues
|
||||
);
|
||||
if (!($customFieldSyncResult['ok'] ?? false)) {
|
||||
$errors = array_merge($errors, $customFieldSyncResult['errors'] ?? []);
|
||||
$customFieldValueMap = UserCustomFieldValueService::buildUserValueMap($userId, $customFieldDefinitionIds);
|
||||
}
|
||||
if ($currentUserId === $userId && isset($form['theme'])) {
|
||||
$themes = appThemes();
|
||||
$themeValue = strtolower(trim((string) ($form['theme'] ?? '')));
|
||||
@@ -158,13 +231,15 @@ if (isset($_POST['email'])) {
|
||||
$_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}");
|
||||
if (!$errors) {
|
||||
$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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,8 +20,26 @@ $canViewUsers = can('users.view');
|
||||
$canViewAddressBook = can('address_book.view');
|
||||
$canViewUserMeta = can('users.view_meta');
|
||||
$canViewUserAudit = can('users.view_audit');
|
||||
$hasDetailsActions = $canViewAddressBook || !empty($canEditUser) || can('users.delete');
|
||||
$canAccessPdf = can('users.access_pdf');
|
||||
$canEditUser = !empty($canEditUser);
|
||||
$canDeleteUser = can('users.delete') && !$isOwnAccount;
|
||||
$hideNavigation = $isOwnAccount && !$canViewUsers;
|
||||
$lastLoginProvider = strtolower((string) ($values['last_login_provider'] ?? ($user['last_login_provider'] ?? '')));
|
||||
$lastLoginAt = (string) ($values['last_login_at'] ?? ($user['last_login_at'] ?? ''));
|
||||
$lastLoginProviderLabel = '-';
|
||||
$lastLoginProviderVariant = 'neutral';
|
||||
if ($lastLoginAt !== '') {
|
||||
if ($lastLoginProvider === 'microsoft') {
|
||||
$lastLoginProviderLabel = t('Microsoft');
|
||||
$lastLoginProviderVariant = 'primary';
|
||||
} elseif ($lastLoginProvider === 'local') {
|
||||
$lastLoginProviderLabel = t('Local');
|
||||
$lastLoginProviderVariant = 'secondary';
|
||||
} else {
|
||||
$lastLoginProviderLabel = t('Unknown');
|
||||
$lastLoginProviderVariant = 'warning';
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
|
||||
@@ -37,72 +55,63 @@ $hideNavigation = $isOwnAccount && !$canViewUsers;
|
||||
require templatePath('partials/app-breadcrumb.phtml');
|
||||
?>
|
||||
<?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 ($hasDetailsActions): ?>
|
||||
<details class="dropdown">
|
||||
<summary role="button" class="outline secondary"><i class="bi bi-three-dots"></i></summary>
|
||||
<ul dir="rtl">
|
||||
<?php if ($canViewAddressBook): ?>
|
||||
<li>
|
||||
<a role="button" class="transparent" href="<?php e(lurl('address-book/view/' . ($values['uuid'] ?? ''))); ?>">
|
||||
<?php e(t('View in address book')); ?>
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
<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
|
||||
$asideActions = [];
|
||||
if ($canViewAddressBook) {
|
||||
$asideActions[] = [
|
||||
'type' => 'link',
|
||||
'href' => lurl('address-book/view/' . ($values['uuid'] ?? '')),
|
||||
'label' => t('View in address book'),
|
||||
];
|
||||
}
|
||||
if ($canEditUser) {
|
||||
$asideActions[] = [
|
||||
'type' => 'form',
|
||||
'method' => 'post',
|
||||
'action' => 'admin/users/send-access/' . ($values['uuid'] ?? ''),
|
||||
'confirm' => t('Send access email to this user?'),
|
||||
'label' => t('Send access'),
|
||||
];
|
||||
}
|
||||
if ($canAccessPdf) {
|
||||
$asideActions[] = [
|
||||
'type' => 'form',
|
||||
'method' => 'post',
|
||||
'action' => 'admin/users/access-pdf',
|
||||
'label' => t('Access PDF'),
|
||||
'target' => '_blank',
|
||||
'fields' => [
|
||||
'uuid' => (string) ($values['uuid'] ?? ''),
|
||||
],
|
||||
];
|
||||
}
|
||||
$titlebarActions = [];
|
||||
if ($canEditUser) {
|
||||
$titlebarActions[] = [
|
||||
'form' => 'user-form',
|
||||
'name' => 'action',
|
||||
'value' => 'save',
|
||||
'class' => 'secondary outline',
|
||||
'label' => t('Save'),
|
||||
];
|
||||
if (!$hideNavigation) {
|
||||
$titlebarActions[] = [
|
||||
'form' => 'user-form',
|
||||
'name' => 'action',
|
||||
'value' => 'save_close',
|
||||
'class' => 'primary',
|
||||
'label' => t('Save & close'),
|
||||
];
|
||||
}
|
||||
}
|
||||
$titlebar = [
|
||||
'title' => $titleText,
|
||||
'backHref' => $hideNavigation ? '' : 'admin/users',
|
||||
'backTitle' => t('Cancel'),
|
||||
'actions' => $titlebarActions,
|
||||
];
|
||||
require templatePath('partials/app-details-titlebar.phtml');
|
||||
?>
|
||||
|
||||
<?php if (!empty($errors)): ?>
|
||||
<div class="app-details-errors">
|
||||
@@ -128,9 +137,9 @@ $hideNavigation = $isOwnAccount && !$canViewUsers;
|
||||
$roles = $roles ?? [];
|
||||
$selectedRoleIds = $selectedRoleIds ?? [];
|
||||
$showDepartments = !empty($canEditAssignments);
|
||||
$departments = $departments ?? [];
|
||||
$departmentOptionsByTenant = $departmentOptionsByTenant ?? [];
|
||||
$selectedDepartmentIds = $selectedDepartmentIds ?? [];
|
||||
$isReadOnly = !(!empty($canEditUser));
|
||||
$isReadOnly = !$canEditUser;
|
||||
$canEditAssignments = $canEditAssignments ?? false;
|
||||
$permissionRows = $permissionRows ?? [];
|
||||
$showPermissions = !empty($permissionRows) && can('permissions.view');
|
||||
@@ -138,8 +147,49 @@ $hideNavigation = $isOwnAccount && !$canViewUsers;
|
||||
$showPasswordResets = !empty($passwordResets);
|
||||
$rememberTokens = $rememberTokens ?? [];
|
||||
$showRememberTokens = !empty($rememberTokens);
|
||||
$showDangerZone = $canDeleteUser;
|
||||
$dangerZoneDeleteFormId = $showDangerZone ? 'user-delete-form' : '';
|
||||
$dangerZoneWarning = t('This will permanently delete this user.');
|
||||
$dangerZoneActionLabel = t('Delete user');
|
||||
$showTokenDangerAction = $canEditUser;
|
||||
$tokenDangerFormId = $showTokenDangerAction ? 'user-forget-tokens-form' : '';
|
||||
$tokenDangerWarning = t('This will mark all remember-me tokens as expired. Users will need to sign in again.');
|
||||
$tokenDangerActionLabel = t('Clear login tokens');
|
||||
$showCustomFieldsTab = true;
|
||||
$canEditCustomFieldValues = !empty($canEditCustomFieldValues);
|
||||
$customFieldDefinitionsByTenant = $customFieldDefinitionsByTenant ?? [];
|
||||
$customFieldValueMap = $customFieldValueMap ?? [];
|
||||
$customFieldPostedValues = $customFieldPostedValues ?? [];
|
||||
require __DIR__ . '/_form.phtml';
|
||||
?>
|
||||
<?php if ($canManageApiTokens ?? false): ?>
|
||||
<form id="api-token-create-form" method="post"
|
||||
action="admin/users/api-token-create/<?php e($values['uuid'] ?? ''); ?>" hidden>
|
||||
<?php Session::getCsrfInput(); ?>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
<?php if ($showTokenDangerAction): ?>
|
||||
<form
|
||||
id="user-forget-tokens-form"
|
||||
method="post"
|
||||
action="admin/users/forget-tokens/<?php e($values['uuid'] ?? ''); ?>"
|
||||
hidden
|
||||
onsubmit="return confirm('<?php e(t('Clear all login tokens for this user?')); ?>');"
|
||||
>
|
||||
<?php Session::getCsrfInput(); ?>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
<?php if ($showDangerZone): ?>
|
||||
<form
|
||||
id="user-delete-form"
|
||||
method="post"
|
||||
action="admin/users/delete/<?php e($values['uuid'] ?? ''); ?>"
|
||||
hidden
|
||||
onsubmit="return confirm('<?php e(t('Delete this user?')); ?>');"
|
||||
>
|
||||
<?php Session::getCsrfInput(); ?>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
<aside id="app-details-aside-section">
|
||||
<div class="app-details-aside-section">
|
||||
@@ -161,7 +211,7 @@ $hideNavigation = $isOwnAccount && !$canViewUsers;
|
||||
<p><?php e($values['email'] ?? ''); ?></p>
|
||||
</hgroup>
|
||||
<hr>
|
||||
<?php if (!empty($canEditUser)): ?>
|
||||
<?php if ($canEditUser): ?>
|
||||
<details name="user-avatar">
|
||||
<summary>
|
||||
<?php e(t('Upload image')); ?>
|
||||
@@ -186,10 +236,15 @@ $hideNavigation = $isOwnAccount && !$canViewUsers;
|
||||
<?php Session::getCsrfInput(); ?>
|
||||
</form>
|
||||
</details>
|
||||
<hr>
|
||||
<?php endif; ?>
|
||||
<hr>
|
||||
<?php
|
||||
if ($asideActions) {
|
||||
require templatePath('partials/app-details-aside-actions.phtml');
|
||||
}
|
||||
?>
|
||||
<?php if ($canViewUserMeta): ?>
|
||||
<details name="user-meta" open>
|
||||
<details name="user-meta">
|
||||
<summary><?php e(t('Status & meta')); ?></summary>
|
||||
<hr>
|
||||
<div class="grid">
|
||||
@@ -223,6 +278,14 @@ $hideNavigation = $isOwnAccount && !$canViewUsers;
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<small><?php e(t('Last login via')); ?></small>
|
||||
<p>
|
||||
<span class="badge" data-variant="<?php e($lastLoginProviderVariant); ?>">
|
||||
<?php e($lastLoginProviderLabel); ?>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<small>
|
||||
<?php e(t('UUID')); ?>
|
||||
@@ -242,93 +305,22 @@ $hideNavigation = $isOwnAccount && !$canViewUsers;
|
||||
<hr>
|
||||
<?php endif; ?>
|
||||
<?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;
|
||||
$asideAuditDetailsName = 'user-audit';
|
||||
$asideAuditCreated = (string) ($values['created'] ?? '');
|
||||
$asideAuditCreatedByLabel = (string) ($values['created_by_label'] ?? '');
|
||||
$asideAuditCreatedById = $values['created_by'] ?? null;
|
||||
$asideAuditCreatedByUuid = (string) ($values['created_by_uuid'] ?? '');
|
||||
$asideAuditModified = (string) ($values['modified'] ?? '');
|
||||
$asideAuditModifiedByLabel = (string) ($values['modified_by_label'] ?? '');
|
||||
$asideAuditModifiedById = $values['modified_by'] ?? null;
|
||||
$asideAuditModifiedByUuid = (string) ($values['modified_by_uuid'] ?? '');
|
||||
$asideAuditStatusChanged = (string) ($values['active_changed_at'] ?? '');
|
||||
$asideAuditStatusChangedByLabel = (string) ($values['active_changed_by_label'] ?? '');
|
||||
$asideAuditStatusChangedById = $values['active_changed_by'] ?? null;
|
||||
$asideAuditStatusChangedByUuid = (string) ($values['active_changed_by_uuid'] ?? '');
|
||||
?>
|
||||
<?php if ($canViewUserAudit): ?>
|
||||
<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>
|
||||
<?php require templatePath('partials/app-details-aside-audit.phtml'); ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
15
pages/admin/users/index($slug).php
Normal file
15
pages/admin/users/index($slug).php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Http\Request;
|
||||
|
||||
Guard::requireLogin();
|
||||
|
||||
$slug = trim((string) ($slug ?? ''));
|
||||
if ($slug === '') {
|
||||
require __DIR__ . '/index().php';
|
||||
return;
|
||||
}
|
||||
|
||||
Router::redirect('error/not_found?url=' . urlencode(Request::pathWithQuery()));
|
||||
@@ -1,11 +1,13 @@
|
||||
<?php
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Session;
|
||||
|
||||
$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');
|
||||
$canAccessPdf = can('users.access_pdf');
|
||||
$canUpdateSelf = can('users.self_update');
|
||||
$currentUserUuid = (string) ($_SESSION['user']['uuid'] ?? '');
|
||||
$showTenantTabs = isset($tenants) && is_array($tenants) && count($tenants) > 1;
|
||||
@@ -31,6 +33,11 @@ require templatePath('partials/app-breadcrumb.phtml');
|
||||
<i class="bi bi-envelope"></i> <?php e(t('Send access')); ?>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<?php if ($canAccessPdf): ?>
|
||||
<button type="button" class="secondary outline" data-users-bulk="access-pdf" hidden>
|
||||
<i class="bi bi-file-earmark-pdf"></i> <?php e(t('Download access PDFs')); ?>
|
||||
</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')); ?>
|
||||
@@ -163,16 +170,22 @@ require templatePath('partials/app-breadcrumb.phtml');
|
||||
<div class="app-list-table">
|
||||
<div id="users-grid"></div>
|
||||
</div>
|
||||
<?php if ($canAccessPdf): ?>
|
||||
<form id="users-access-pdf-bulk-form" method="post" action="admin/users/access-pdf-bulk" hidden>
|
||||
<?php Session::getCsrfInput(); ?>
|
||||
<input type="hidden" name="uuids" value="">
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
|
||||
|
||||
<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 { createServerGrid } from "<?php e(assetVersion('js/core/app-grid-factory.js')); ?>";
|
||||
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, getAppBase, escapeHtml, buildBadgeList } from "<?php e(asset('js/pages/app-list-utils.js')); ?>";
|
||||
import { showAsyncFlash, withLoading } from "<?php e(asset('js/components/app-async-flash.js')); ?>";
|
||||
import { initUsersListPage } from "<?php e(assetVersion('js/pages/app-users-list.js')); ?>";
|
||||
import { bindBulkVisibility } from "<?php e(assetVersion('js/components/app-bulk-selection.js')); ?>";
|
||||
import { buildUrl, badgeHtml, getAppBase, escapeHtml, buildBadgeList } from "<?php e(assetVersion('js/pages/app-list-utils.js')); ?>";
|
||||
import { showAsyncFlash, withLoading } from "<?php e(assetVersion('js/components/app-async-flash.js')); ?>";
|
||||
const appBase = getAppBase();
|
||||
const currentUserUuid = "<?php e($currentUserUuid); ?>";
|
||||
const canUpdateUsers = <?php e($canUpdateUsers ? 'true' : 'false'); ?>;
|
||||
@@ -186,6 +199,7 @@ require templatePath('partials/app-breadcrumb.phtml');
|
||||
bulkDeactivateConfirm: "<?php e(t('Deactivate users?')); ?>",
|
||||
bulkDeleteConfirm: "<?php e(t('Delete users?')); ?>",
|
||||
bulkSendAccessConfirm: "<?php e(t('Send access emails to selected users?')); ?>",
|
||||
bulkAccessPdfConfirm: "<?php e(t('Generate access PDFs for selected users?')); ?>",
|
||||
primaryTenant: "<?php e(t('Primary tenant')); ?>",
|
||||
bulkMessages: {
|
||||
'activate': {
|
||||
@@ -414,6 +428,9 @@ require templatePath('partials/app-breadcrumb.phtml');
|
||||
csrf,
|
||||
labels,
|
||||
buildBulkUrl,
|
||||
bulkDownloadForms: {
|
||||
'access-pdf': '#users-access-pdf-bulk-form'
|
||||
},
|
||||
showFlash: showAsyncFlash,
|
||||
withLoading
|
||||
});
|
||||
@@ -421,4 +438,4 @@ require templatePath('partials/app-breadcrumb.phtml');
|
||||
containerSelector: '#users-grid',
|
||||
buttonSelector: '[data-users-bulk]'
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -6,9 +6,8 @@ use MintyPHP\Router;
|
||||
use MintyPHP\Service\Access\PermissionService;
|
||||
use MintyPHP\Service\Tenant\TenantScopeService;
|
||||
use MintyPHP\Service\User\UserService;
|
||||
use MintyPHP\Service\User\UserAccessTemplateService;
|
||||
use MintyPHP\Service\Mail\MailService;
|
||||
use MintyPHP\I18n;
|
||||
use MintyPHP\Http\Request;
|
||||
|
||||
Guard::requireLogin();
|
||||
|
||||
@@ -36,32 +35,10 @@ if (!$isOwnAccount && !TenantScopeService::canAccess('users', $userId, $currentU
|
||||
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,
|
||||
];
|
||||
$context = UserAccessTemplateService::buildContext($user);
|
||||
$locale = (string) ($context['locale'] ?? '');
|
||||
$subject = (string) ($context['subject'] ?? t('Your access details'));
|
||||
$vars = is_array($context['vars'] ?? null) ? $context['vars'] : [];
|
||||
|
||||
$result = MailService::sendTemplate('access_info', $vars, (string) $user['email'], $subject, $locale);
|
||||
if (!($result['ok'] ?? false)) {
|
||||
|
||||
@@ -5,6 +5,7 @@ use MintyPHP\Router;
|
||||
use MintyPHP\Session;
|
||||
use MintyPHP\Service\User\UserService;
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Support\Flash;
|
||||
|
||||
Guard::requireLogin();
|
||||
|
||||
@@ -34,6 +35,17 @@ if ($userId <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!allowUserTheme()) {
|
||||
http_response_code(403);
|
||||
if (Request::wantsJson()) {
|
||||
Router::json(['ok' => false, 'error' => 'theme_disabled']);
|
||||
return;
|
||||
}
|
||||
Flash::error(t('Theme change is disabled for this tenant'), 'admin', 'theme_disabled');
|
||||
Router::redirect('admin');
|
||||
return;
|
||||
}
|
||||
|
||||
$theme = strtolower(trim((string) ($_POST['theme'] ?? '')));
|
||||
$themes = appThemes();
|
||||
if ($theme === '' || !isset($themes[$theme])) {
|
||||
|
||||
Reference in New Issue
Block a user