ADconsole v1.0.0
First public release. - docs: document the audit log, user offboarding, CSV export and the "require a reason for destructive actions" setting across README, docs/ and CLAUDE/AGENTS.md; note retention for panel_audit - docs: fill the publishing placeholders (repo URL, security contact, issue-template links) and add a semantic-versioning policy - move agent/contributor context into CLAUDE/ (AGENTS.md + a README); root CLAUDE.md is now a thin @-import pointer - add a favicon (shield-lock, matching the login screen) to the login page and the app layout - CHANGELOG: promote [Unreleased] to [1.0.0]; keep the earlier internal build as [0.1.0] Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UfuvfwexztdS7LDpGuKsuK
This commit is contained in:
commit
1126d96271
174 files changed
+27113
No files matched your search
@@ -0,0 +1,582 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\PanelAttribute;
|
||||
use App\Models\PanelSetting;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* Ustawienia samego panelu (nie danych z AD), trzymane w bazie:
|
||||
* - ogólne (domyślne zaznaczenia resetu hasła, polityka i generator haseł,
|
||||
* klucz szyfrujący) -> tabela `panel_settings`, jeden wiersz 'panel'
|
||||
* (klucz -> wartość JSON),
|
||||
* - definicje atrybutów AD (kolumny listy + pola profilu) -> tabela
|
||||
* `panel_attributes`, po jednym wierszu na atrybut, kolejność wg `position`.
|
||||
*/
|
||||
class PanelSettingsService
|
||||
{
|
||||
/**
|
||||
* Kategorie (zakładki) w oknie "Podgląd / Edycja" użytkownika, do których
|
||||
* przypisywane są poszczególne atrybuty. Klucz = identyfikator zapisywany
|
||||
* w konfiguracji, wartość = etykieta wyświetlana w interfejsie.
|
||||
*/
|
||||
public const ATTRIBUTE_CATEGORIES = [
|
||||
'general' => 'Ogólne',
|
||||
'org' => 'Organizacja',
|
||||
'address' => 'Adres i lokalizacja',
|
||||
'hr' => 'Kadry i chmura',
|
||||
'account' => 'Konto AD',
|
||||
];
|
||||
|
||||
/**
|
||||
* Pozycje "na stałe": sAMAccountName (identyfikuje wiersze) i DN (ścieżka
|
||||
* obiektu). Nie można ich usunąć ani zmienić ich klucza / atrybutu AD.
|
||||
* Etykieta, kategoria i widoczność jako kolumna są konfigurowalne, ale typ
|
||||
* jest wymuszony ("systemowy") - takich pozycji panel nie edytuje.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
public const SYSTEM_ATTRIBUTE_KEYS = ['login', 'distinguishedName'];
|
||||
|
||||
/**
|
||||
* Kolumny wyliczane przez panel (nie są to atrybuty AD): status konta
|
||||
* oraz wygasanie hasła. W zakładce "Atrybuty" można nimi zarządzać jak
|
||||
* kolumnami (kolejność, widoczność), ale nie da się ich edytować, usunąć
|
||||
* ani zmienić ich klucza / typu.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
public const COMPUTED_ATTRIBUTE_KEYS = ['status', 'password_expiry'];
|
||||
|
||||
/**
|
||||
* Pozycje "na stałe", ale będące realnymi atrybutami AD: klucza ani nazwy
|
||||
* atrybutu AD nie da się zmienić i nie można ich usunąć, natomiast kategorię
|
||||
* ustawia się normalnie. Wartość mapy to kanoniczna nazwa atrybutu AD
|
||||
* (zapis / odczyt ma dedykowaną obsługę w kontrolerze - accountExpires to
|
||||
* Windows FileTime, nie zwykły string). Typ takiej pozycji jest wymuszony.
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
public const FIXED_ATTRIBUTE_KEYS = ['account_expiry' => 'accountExpires'];
|
||||
|
||||
/**
|
||||
* Wymuszony typ pozycji "na stałe" (patrz FIXED_ATTRIBUTE_KEYS).
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
public const FIXED_ATTRIBUTE_TYPES = ['account_expiry' => 'date'];
|
||||
|
||||
/**
|
||||
* Typ atrybutu w oknie "Podgląd / Edycja". Zastępuje dawne osobne flagi
|
||||
* "edytowalny" i "szyfrowany" - jeden wybór z listy w zakładce "Atrybuty".
|
||||
*
|
||||
* Edytowalne: text | number | date | encrypted
|
||||
* Nieedytowalne: system | account_status | password_expiry
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
public const ATTRIBUTE_TYPE_LABELS = [
|
||||
'text' => 'Tekst',
|
||||
'number' => 'Liczba',
|
||||
'date' => 'Data',
|
||||
'encrypted' => 'Tekst szyfrowany',
|
||||
'system' => 'Systemowy',
|
||||
'account_status' => 'Status konta',
|
||||
'password_expiry' => 'Wygaśnięcie hasła',
|
||||
];
|
||||
|
||||
/** Typy, które użytkownik może wybrać dla zwykłego atrybutu AD. */
|
||||
public const EDITABLE_ATTRIBUTE_TYPES = ['text', 'number', 'date', 'encrypted'];
|
||||
|
||||
/** Wszystkie dopuszczalne wartości typu. */
|
||||
public const ATTRIBUTE_TYPES = [
|
||||
'text', 'number', 'date', 'encrypted', 'system', 'account_status', 'password_expiry',
|
||||
];
|
||||
|
||||
/** Klucz wiersza w tabeli `panel_settings`, pod którym zapisujemy całą strukturę. */
|
||||
protected const STORE_KEY = 'panel';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Domyślna, wbudowana konfiguracja panelu.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function defaults(): array
|
||||
{
|
||||
return [
|
||||
'reset_password' => [
|
||||
'must_change_password' => true,
|
||||
'unlock_account' => true,
|
||||
],
|
||||
'password_policy' => [
|
||||
// Maksymalny wiek hasła w dniach (domyślne AD = 42).
|
||||
'max_age_days' => 42,
|
||||
// Generator haseł w panelu (nie zmienia polityki w AD).
|
||||
'generator_length' => 16,
|
||||
'generator_symbols' => true,
|
||||
],
|
||||
'encryption' => [
|
||||
// Klucz (hasło) do odszyfrowania / zaszyfrowania atrybutów
|
||||
// oznaczonych jako "szyfrowany". Puste = szyfrowanie wyłączone.
|
||||
'key' => '',
|
||||
],
|
||||
'audit' => [
|
||||
// Wymuś podanie powodu przy akcjach niszczących (wyłącz / usuń /
|
||||
// przenieś / offboarding). Wpis w dzienniku powstaje zawsze.
|
||||
'require_reason' => false,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Czy panel wymaga podania powodu przy akcjach niszczących.
|
||||
*/
|
||||
public function requireReason(): bool
|
||||
{
|
||||
return (bool) ($this->all()['audit']['require_reason'] ?? false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maksymalny wiek hasła w dniach (0 = brak polityki wygasania).
|
||||
*/
|
||||
public function passwordMaxAgeDays(): int
|
||||
{
|
||||
$value = (int) ($this->all()['password_policy']['max_age_days'] ?? 42);
|
||||
|
||||
return max(0, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ustawienia generatora haseł (tylko w panelu - nie dotyka polityki AD).
|
||||
*
|
||||
* @return array{length:int,symbols:bool}
|
||||
*/
|
||||
public function passwordGenerator(): array
|
||||
{
|
||||
$policy = $this->all()['password_policy'] ?? [];
|
||||
|
||||
return [
|
||||
'length' => max(8, min(128, (int) ($policy['generator_length'] ?? 16))),
|
||||
'symbols' => (bool) ($policy['generator_symbols'] ?? true),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Klucz szyfrujący atrybuty (pusty = szyfrowanie wyłączone).
|
||||
*/
|
||||
public function encryptionKey(): string
|
||||
{
|
||||
return trim((string) ($this->all()['encryption']['key'] ?? ''));
|
||||
}
|
||||
|
||||
/**
|
||||
* Wbudowana lista atrybutów widocznych w panelu. Stanowi punkt wyjścia
|
||||
* dla konfiguracji użytkownika (zakładka "Atrybuty").
|
||||
*
|
||||
* @return list<array<string, mixed>>
|
||||
*/
|
||||
protected function defaultAttributes(): array
|
||||
{
|
||||
return [
|
||||
// --- Tożsamość (domyślnie widoczne kolumny listy) --------------
|
||||
['key' => 'login', 'label' => 'sAMAccountName', 'ldap' => 'sAMAccountName', 'category' => 'account', 'type' => 'system', 'column' => true],
|
||||
['key' => 'displayName', 'label' => 'Imię i Nazwisko', 'ldap' => 'displayName', 'category' => 'general', 'type' => 'text', 'column' => true],
|
||||
['key' => 'email', 'label' => 'Adres e-mail', 'ldap' => 'mail', 'category' => 'general', 'type' => 'text', 'column' => true],
|
||||
|
||||
// --- Ogólne -----------------------------------------------------
|
||||
['key' => 'firstName', 'label' => 'Imię', 'ldap' => 'givenName', 'category' => 'general', 'type' => 'text', 'column' => false],
|
||||
['key' => 'lastName', 'label' => 'Nazwisko', 'ldap' => 'sn', 'category' => 'general', 'type' => 'text', 'column' => false],
|
||||
['key' => 'telephoneNumber', 'label' => 'Telefon stacjonarny', 'ldap' => 'telephoneNumber', 'category' => 'general', 'type' => 'text', 'column' => false],
|
||||
['key' => 'mobile', 'label' => 'Telefon komórkowy', 'ldap' => 'mobile', 'category' => 'general', 'type' => 'text', 'column' => false],
|
||||
|
||||
// --- Organizacja ---------------------------------------------------
|
||||
['key' => 'title', 'label' => 'Stanowisko', 'ldap' => 'title', 'category' => 'org', 'type' => 'text', 'column' => false],
|
||||
['key' => 'department', 'label' => 'Dział / Departament', 'ldap' => 'department', 'category' => 'org', 'type' => 'text', 'column' => false],
|
||||
['key' => 'company', 'label' => 'Firma / Spółka', 'ldap' => 'company', 'category' => 'org', 'type' => 'text', 'column' => false],
|
||||
['key' => 'manager', 'label' => 'Kierownik', 'ldap' => 'manager', 'category' => 'org', 'type' => 'text', 'column' => false],
|
||||
|
||||
// --- Adres i lokalizacja ----------------------------------------
|
||||
['key' => 'co', 'label' => 'Kraj (co)', 'ldap' => 'co', 'category' => 'address', 'type' => 'text', 'column' => false],
|
||||
['key' => 'st', 'label' => 'Województwo (st)', 'ldap' => 'st', 'category' => 'address', 'type' => 'text', 'column' => false],
|
||||
['key' => 'l', 'label' => 'Miasto (l)', 'ldap' => 'l', 'category' => 'address', 'type' => 'text', 'column' => false],
|
||||
['key' => 'postalCode', 'label' => 'Kod pocztowy', 'ldap' => 'postalCode', 'category' => 'address', 'type' => 'text', 'column' => false],
|
||||
['key' => 'streetAddress', 'label' => 'Ulica i numer domu', 'ldap' => 'streetAddress', 'category' => 'address', 'type' => 'text', 'column' => false],
|
||||
['key' => 'roomNumber', 'label' => 'Numer pokoju / biurka', 'ldap' => 'roomNumber', 'category' => 'address', 'type' => 'text', 'column' => false],
|
||||
['key' => 'physicalDeliveryOfficeName', 'label' => 'Główna lokalizacja', 'ldap' => 'physicalDeliveryOfficeName', 'category' => 'address', 'type' => 'text', 'column' => false],
|
||||
|
||||
// --- Kadry i chmura -------------------------------------------------
|
||||
['key' => 'employeeID', 'label' => 'ID pracownika (Snipe-IT)', 'ldap' => 'employeeID', 'category' => 'hr', 'type' => 'text', 'column' => false],
|
||||
['key' => 'employeeNumber', 'label' => 'Kod pracownika (PIN)', 'ldap' => 'employeeNumber', 'category' => 'hr', 'type' => 'text', 'column' => false],
|
||||
['key' => 'microsoft', 'label' => 'Microsoft Cloud', 'ldap' => 'msDS-cloudExtensionAttribute1', 'category' => 'hr', 'type' => 'encrypted', 'column' => false],
|
||||
['key' => 'google', 'label' => 'Google Cloud', 'ldap' => 'msDS-cloudExtensionAttribute2', 'category' => 'hr', 'type' => 'encrypted', 'column' => false],
|
||||
|
||||
// --- Konto AD ----------------------------------------------------
|
||||
['key' => 'distinguishedName', 'label' => 'Ścieżka (DN)', 'ldap' => 'distinguishedName', 'category' => 'account', 'type' => 'system', 'column' => false],
|
||||
['key' => 'account_expiry', 'label' => 'Wygasanie konta', 'ldap' => 'accountExpires', 'category' => 'account', 'type' => 'date', 'column' => false],
|
||||
|
||||
// --- Kolumny wyliczane (nie są atrybutami AD) ---------------------
|
||||
['key' => 'status', 'label' => 'Status konta', 'ldap' => 'status', 'category' => 'account', 'type' => 'account_status', 'column' => true],
|
||||
['key' => 'password_expiry', 'label' => 'Wygasanie hasła', 'ldap' => 'password_expiry', 'category' => 'account', 'type' => 'password_expiry', 'column' => true],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Ustawienia ogólne (bez definicji atrybutów - te ma `attributes()`).
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function all(): array
|
||||
{
|
||||
return array_replace_recursive($this->defaults(), $this->readStored());
|
||||
}
|
||||
|
||||
/**
|
||||
* Surowy wiersz 'panel' z tabeli `panel_settings` (bez scalania z
|
||||
* domyślnymi). Gdy tabeli jeszcze nie ma (np. przed migracją) - pusto.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function readStored(): array
|
||||
{
|
||||
try {
|
||||
$value = PanelSetting::query()->find(self::STORE_KEY)?->value;
|
||||
|
||||
return is_array($value) ? $value : [];
|
||||
} catch (\Throwable) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public function save(array $settings): void
|
||||
{
|
||||
PanelSetting::query()->updateOrCreate(
|
||||
['key' => self::STORE_KEY],
|
||||
['value' => $settings],
|
||||
);
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Atrybuty / kolumny
|
||||
// =====================================================================
|
||||
|
||||
/**
|
||||
* Klucze, których nie wolno usunąć (systemowe + wyliczane).
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
protected function lockedKeys(): array
|
||||
{
|
||||
return array_merge(
|
||||
self::SYSTEM_ATTRIBUTE_KEYS,
|
||||
self::COMPUTED_ATTRIBUTE_KEYS,
|
||||
array_keys(self::FIXED_ATTRIBUTE_KEYS),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Znormalizowana lista wszystkich zdefiniowanych atrybutów, z gwarancją
|
||||
* obecności pozycji systemowych i wyliczanych oraz bez duplikatów kluczy.
|
||||
*
|
||||
* @return list<array{key:string,label:string,ldap:string,category:string,type:string,input:string,editable:bool,column:bool,system:bool,computed:bool,fixed:bool,encrypted:bool}>
|
||||
*/
|
||||
public function attributes(): array
|
||||
{
|
||||
// Zapisana konfiguracja z tabeli `panel_attributes`; brak wierszy =
|
||||
// użytkownik jeszcze nic nie zmieniał -> wbudowana lista domyślna.
|
||||
$list = $this->storedAttributeRows() ?: $this->defaultAttributes();
|
||||
|
||||
$normalized = [];
|
||||
$seen = [];
|
||||
|
||||
foreach ($list as $item) {
|
||||
if (! is_array($item)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$attr = $this->normalizeAttribute($item);
|
||||
|
||||
if ($attr['key'] === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$lower = strtolower($attr['key']);
|
||||
if (isset($seen[$lower])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$seen[$lower] = true;
|
||||
$normalized[] = $attr;
|
||||
}
|
||||
|
||||
// Pozycje systemowe i wyliczane muszą istnieć zawsze - odtwarzamy brakujące.
|
||||
$lockedKeys = $this->lockedKeys();
|
||||
foreach ($this->defaultAttributes() as $default) {
|
||||
if (! in_array($default['key'], $lockedKeys, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($seen[strtolower($default['key'])])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$normalized[] = $this->normalizeAttribute($default);
|
||||
$seen[strtolower($default['key'])] = true;
|
||||
}
|
||||
|
||||
return $normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wiersze z tabeli `panel_attributes` w kolejności `position`, sprowadzone
|
||||
* do kształtu przyjmowanego przez normalizeAttribute(). Brak tabeli
|
||||
* (np. przed migracją) traktujemy jak brak konfiguracji.
|
||||
*
|
||||
* @return list<array<string, mixed>>
|
||||
*/
|
||||
protected function storedAttributeRows(): array
|
||||
{
|
||||
try {
|
||||
return PanelAttribute::query()
|
||||
->orderBy('position')
|
||||
->orderBy('id')
|
||||
->get()
|
||||
->map(fn (PanelAttribute $a) => [
|
||||
'key' => $a->key,
|
||||
'label' => $a->label,
|
||||
'ldap' => $a->ldap,
|
||||
'category' => $a->category,
|
||||
'type' => $a->type,
|
||||
'regex' => $a->regex,
|
||||
'required' => $a->required,
|
||||
'help' => $a->help,
|
||||
'column' => $a->is_column,
|
||||
])
|
||||
->all();
|
||||
} catch (\Throwable) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Zapisuje przesłaną listę atrybutów, wymuszając reguły dla pozycji
|
||||
* systemowych i wyliczanych oraz odtwarzając te, które ktoś próbował usunąć.
|
||||
*
|
||||
* @param iterable<array<string, mixed>> $attributes
|
||||
*/
|
||||
public function saveAttributes(iterable $attributes): void
|
||||
{
|
||||
$lockedDefaults = [];
|
||||
foreach ($this->defaultAttributes() as $default) {
|
||||
if (in_array($default['key'], $this->lockedKeys(), true)) {
|
||||
$lockedDefaults[$default['key']] = $this->normalizeAttribute($default);
|
||||
}
|
||||
}
|
||||
|
||||
$clean = [];
|
||||
$seen = [];
|
||||
|
||||
foreach ($attributes as $item) {
|
||||
if (! is_array($item)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$attr = $this->normalizeAttribute($item);
|
||||
|
||||
if ($attr['key'] === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$lower = strtolower($attr['key']);
|
||||
if (isset($seen[$lower])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($lockedDefaults[$attr['key']])) {
|
||||
$def = $lockedDefaults[$attr['key']];
|
||||
// Typ, editable/encrypted oraz ldap dla kolumn wyliczanych /
|
||||
// "na stałe" wymusza już normalizeAttribute po kluczu. Zostaje
|
||||
// przywrócenie nazwy atrybutu AD dla pozycji systemowych oraz
|
||||
// stałej kategorii dla kolumn wyliczanych.
|
||||
$attr['ldap'] = $def['ldap'];
|
||||
if ($def['computed']) {
|
||||
$attr['category'] = $def['category'];
|
||||
}
|
||||
}
|
||||
|
||||
$seen[$lower] = true;
|
||||
$clean[] = $attr;
|
||||
}
|
||||
|
||||
foreach ($lockedDefaults as $key => $def) {
|
||||
if (isset($seen[strtolower($key)])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$clean[] = $def;
|
||||
}
|
||||
|
||||
// Nadpisujemy całą tabelę - lista jest krótka, a to najprościej trzyma
|
||||
// kolejność (position) i usunięcia w zgodzie z wynikiem powyżej.
|
||||
DB::transaction(function () use ($clean) {
|
||||
PanelAttribute::query()->delete();
|
||||
|
||||
$now = now();
|
||||
$rows = [];
|
||||
|
||||
foreach (array_values($clean) as $i => $attr) {
|
||||
$rows[] = [
|
||||
'key' => $attr['key'],
|
||||
'label' => $attr['label'],
|
||||
'ldap' => $attr['ldap'],
|
||||
'category' => $attr['category'],
|
||||
'type' => $attr['type'],
|
||||
'regex' => $attr['regex'] !== '' ? $attr['regex'] : null,
|
||||
'required' => $attr['required'],
|
||||
'help' => $attr['help'],
|
||||
'is_column' => $attr['column'],
|
||||
'position' => $i,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
];
|
||||
}
|
||||
|
||||
if ($rows !== []) {
|
||||
PanelAttribute::query()->insert($rows);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Atrybuty / kolumny wyliczane oznaczone do wyświetlenia jako kolumna
|
||||
* listy użytkowników - w kolejności zapisanej w konfiguracji.
|
||||
*
|
||||
* @return list<array<string, mixed>>
|
||||
*/
|
||||
public function attributeColumns(): array
|
||||
{
|
||||
return array_values(array_filter($this->attributes(), static fn ($a) => $a['column']));
|
||||
}
|
||||
|
||||
/**
|
||||
* Atrybuty pogrupowane wg kategorii (zakładek podglądu użytkownika).
|
||||
* Pozycje wyliczane są pomijane - nie są polami profilu.
|
||||
*
|
||||
* @return array<string, list<array<string, mixed>>>
|
||||
*/
|
||||
public function attributesByCategory(): array
|
||||
{
|
||||
$grouped = [];
|
||||
|
||||
foreach (array_keys(self::ATTRIBUTE_CATEGORIES) as $catId) {
|
||||
$grouped[$catId] = [];
|
||||
}
|
||||
|
||||
foreach ($this->attributes() as $attr) {
|
||||
if ($attr['computed']) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$grouped[$attr['category']][] = $attr;
|
||||
}
|
||||
|
||||
return $grouped;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wyłącznie atrybuty edytowalne (bez wyliczanych), pogrupowane wg kategorii.
|
||||
* Zwraca tylko te kategorie, które mają co najmniej jedno pole - to jest
|
||||
* dokładnie zestaw pól pokazywanych w oknie "Podgląd / Edycja".
|
||||
*
|
||||
* @return array<string, list<array<string, mixed>>>
|
||||
*/
|
||||
public function editableAttributesByCategory(): array
|
||||
{
|
||||
$grouped = [];
|
||||
|
||||
foreach ($this->attributes() as $attr) {
|
||||
if ($attr['computed'] || ! $attr['editable']) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$grouped[$attr['category']][] = $attr;
|
||||
}
|
||||
|
||||
$ordered = [];
|
||||
foreach (array_keys(self::ATTRIBUTE_CATEGORIES) as $catId) {
|
||||
if (! empty($grouped[$catId])) {
|
||||
$ordered[$catId] = $grouped[$catId];
|
||||
}
|
||||
}
|
||||
|
||||
return $ordered;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function categories(): array
|
||||
{
|
||||
return self::ATTRIBUTE_CATEGORIES;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $a
|
||||
* @return array{key:string,label:string,ldap:string,category:string,type:string,input:string,editable:bool,column:bool,system:bool,computed:bool,fixed:bool,encrypted:bool}
|
||||
*/
|
||||
protected function normalizeAttribute(array $a): array
|
||||
{
|
||||
$key = preg_replace('/[^A-Za-z0-9_-]/', '', (string) ($a['key'] ?? ''));
|
||||
$ldap = trim((string) ($a['ldap'] ?? ''));
|
||||
$category = (string) ($a['category'] ?? 'general');
|
||||
|
||||
$computed = in_array($key, self::COMPUTED_ATTRIBUTE_KEYS, true);
|
||||
$system = in_array($key, self::SYSTEM_ATTRIBUTE_KEYS, true);
|
||||
$fixedLdap = self::FIXED_ATTRIBUTE_KEYS[$key] ?? null;
|
||||
|
||||
// Typ pozycji wbudowanych jest wymuszony; dla zwykłych atrybutów
|
||||
// pochodzi z wyboru użytkownika (lista w zakładce "Atrybuty") i musi
|
||||
// być jednym z typów edytowalnych.
|
||||
if ($system) {
|
||||
$type = 'system';
|
||||
} elseif ($computed) {
|
||||
$type = $key === 'status' ? 'account_status' : 'password_expiry';
|
||||
} elseif ($fixedLdap !== null) {
|
||||
$type = self::FIXED_ATTRIBUTE_TYPES[$key] ?? 'text';
|
||||
} else {
|
||||
$type = (string) ($a['type'] ?? 'text');
|
||||
if (! in_array($type, self::EDITABLE_ATTRIBUTE_TYPES, true)) {
|
||||
$type = 'text';
|
||||
}
|
||||
}
|
||||
|
||||
$editable = in_array($type, self::EDITABLE_ATTRIBUTE_TYPES, true);
|
||||
|
||||
return [
|
||||
'key' => $key,
|
||||
'label' => trim((string) ($a['label'] ?? '')) ?: $key,
|
||||
// Klucz i atrybut AD pozycji "na stałe" są niezmienne; kolumny
|
||||
// wyliczane nie są atrybutami AD.
|
||||
'ldap' => $fixedLdap ?? ($computed ? $key : ($ldap !== '' ? $ldap : $key)),
|
||||
'category' => array_key_exists($category, self::ATTRIBUTE_CATEGORIES) ? $category : 'general',
|
||||
'type' => $type,
|
||||
// Rodzaj pola formularza w oknie "Podgląd / Edycja".
|
||||
'input' => in_array($type, ['number', 'date'], true) ? $type : 'text',
|
||||
'editable' => $editable,
|
||||
// Reguły walidacji pola profilu - tylko dla atrybutów edytowalnych.
|
||||
'regex' => $editable ? trim((string) ($a['regex'] ?? '')) : '',
|
||||
'required' => $editable ? filter_var($a['required'] ?? false, FILTER_VALIDATE_BOOL) : false,
|
||||
'help' => $editable ? trim((string) ($a['help'] ?? '')) : '',
|
||||
'column' => filter_var($a['column'] ?? false, FILTER_VALIDATE_BOOL),
|
||||
'system' => $system,
|
||||
'computed' => $computed,
|
||||
'fixed' => $fixedLdap !== null,
|
||||
'encrypted' => $type === 'encrypted',
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user