Change setting base
This commit is contained in:
@@ -54,6 +54,7 @@ jobs:
|
|||||||
--exclude=".git/***" \
|
--exclude=".git/***" \
|
||||||
--exclude=".gitea/***" \
|
--exclude=".gitea/***" \
|
||||||
--exclude=".ci_config_deploy/***" \
|
--exclude=".ci_config_deploy/***" \
|
||||||
|
--exclude="data/***" \
|
||||||
--exclude="docs/***" \
|
--exclude="docs/***" \
|
||||||
--exclude="temp/***" \
|
--exclude="temp/***" \
|
||||||
--exclude="CopyToKeycloak/***" \
|
--exclude="CopyToKeycloak/***" \
|
||||||
@@ -136,6 +137,7 @@ jobs:
|
|||||||
--exclude=".git/***" \
|
--exclude=".git/***" \
|
||||||
--exclude=".gitea/***" \
|
--exclude=".gitea/***" \
|
||||||
--exclude=".ci_config_deploy/***" \
|
--exclude=".ci_config_deploy/***" \
|
||||||
|
--exclude="data/***" \
|
||||||
--exclude="docs/***" \
|
--exclude="docs/***" \
|
||||||
--exclude="temp/***" \
|
--exclude="temp/***" \
|
||||||
--exclude="CopyToKeycloak/***" \
|
--exclude="CopyToKeycloak/***" \
|
||||||
|
|||||||
8
config/base_db.php
Normal file
8
config/base_db.php
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'enabled' => false,
|
||||||
|
'db' => [],
|
||||||
|
];
|
||||||
8
config/prod/base_db.php
Normal file
8
config/prod/base_db.php
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'enabled' => true,
|
||||||
|
'db' => require __DIR__ . '/db_settings_basic.php',
|
||||||
|
];
|
||||||
53
config/prod/db_settings_basic.php
Normal file
53
config/prod/db_settings_basic.php
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
$driver = 'pgsql';
|
||||||
|
// $driver = 'mysql';
|
||||||
|
// $driver = 'sqlite';
|
||||||
|
|
||||||
|
$pgsql = [
|
||||||
|
'driver' => 'pgsql',
|
||||||
|
'host' => 'db_nexus',
|
||||||
|
'port' => 5432,
|
||||||
|
'dbname' => 'nexus',
|
||||||
|
'connect_timeout' => 5,
|
||||||
|
'schema' => 'public',
|
||||||
|
'user' => 'nexusbasis',
|
||||||
|
'password' => 'j4v6YV6tcszzKOf3YBTV',
|
||||||
|
'options' => [
|
||||||
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$mysql = [
|
||||||
|
'driver' => 'mysql',
|
||||||
|
'host' => 'localhost',
|
||||||
|
'port' => 3306,
|
||||||
|
'dbname' => 'd0453540',
|
||||||
|
'charset' => 'utf8mb4',
|
||||||
|
'user' => 'd0453540',
|
||||||
|
'password' => 'P6jGRrSaX8QSiBMEJBL7',
|
||||||
|
'options' => [
|
||||||
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||||
|
PDO::ATTR_EMULATE_PREPARES => false,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$sqlite = [
|
||||||
|
'driver' => 'sqlite',
|
||||||
|
'path' => __DIR__ . '/../../var/app.sqlite',
|
||||||
|
'options' => [
|
||||||
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
return match ($driver) {
|
||||||
|
'pgsql' => $pgsql,
|
||||||
|
'mysql' => $mysql,
|
||||||
|
'sqlite' => $sqlite,
|
||||||
|
default => throw new RuntimeException('Unsupported DB driver in db_settings_basic.php: ' . $driver),
|
||||||
|
};
|
||||||
8
config/staging/base_db.php
Normal file
8
config/staging/base_db.php
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'enabled' => true,
|
||||||
|
'db' => require __DIR__ . '/db_settings_basic.php',
|
||||||
|
];
|
||||||
53
config/staging/db_settings_basic.php
Normal file
53
config/staging/db_settings_basic.php
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
$driver = 'pgsql';
|
||||||
|
// $driver = 'mysql';
|
||||||
|
// $driver = 'sqlite';
|
||||||
|
|
||||||
|
$pgsql = [
|
||||||
|
'driver' => 'pgsql',
|
||||||
|
'host' => 'staging_db_nexus',
|
||||||
|
'port' => 5432,
|
||||||
|
'dbname' => 'nexus_staging',
|
||||||
|
'connect_timeout' => 5,
|
||||||
|
'schema' => 'public',
|
||||||
|
'user' => 'nexusbasisstg',
|
||||||
|
'password' => '8HHtFt9ON6RkmwIS0c7U',
|
||||||
|
'options' => [
|
||||||
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$mysql = [
|
||||||
|
'driver' => 'mysql',
|
||||||
|
'host' => 'localhost',
|
||||||
|
'port' => 3306,
|
||||||
|
'dbname' => 'd0453540',
|
||||||
|
'charset' => 'utf8mb4',
|
||||||
|
'user' => 'd0453540',
|
||||||
|
'password' => 'P6jGRrSaX8QSiBMEJBL7',
|
||||||
|
'options' => [
|
||||||
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||||
|
PDO::ATTR_EMULATE_PREPARES => false,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$sqlite = [
|
||||||
|
'driver' => 'sqlite',
|
||||||
|
'path' => __DIR__ . '/../../var/app.sqlite',
|
||||||
|
'options' => [
|
||||||
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
return match ($driver) {
|
||||||
|
'pgsql' => $pgsql,
|
||||||
|
'mysql' => $mysql,
|
||||||
|
'sqlite' => $sqlite,
|
||||||
|
default => throw new RuntimeException('Unsupported DB driver in db_settings_basic.php: ' . $driver),
|
||||||
|
};
|
||||||
@@ -55,6 +55,7 @@ Aktuell gelten folgende Regeln:
|
|||||||
- Standardfelder wie Name, E-Mail, Telefon, Titel und Ort koennen fuer LDAP vorbereitet oder synchronisiert werden.
|
- Standardfelder wie Name, E-Mail, Telefon, Titel und Ort koennen fuer LDAP vorbereitet oder synchronisiert werden.
|
||||||
- `Geburtsdatum` bleibt derzeit lokal gespeichert.
|
- `Geburtsdatum` bleibt derzeit lokal gespeichert.
|
||||||
- nicht jedes Profilfeld wird automatisch in LDAP geschrieben.
|
- nicht jedes Profilfeld wird automatisch in LDAP geschrieben.
|
||||||
|
- Desktop-bezogene Nutzereinstellungen werden in einer eigenen Datenbanktabelle gespeichert.
|
||||||
|
|
||||||
## Desktop Type und Skins
|
## Desktop Type und Skins
|
||||||
|
|
||||||
|
|||||||
@@ -80,7 +80,8 @@ Stand dieser Datei:
|
|||||||
- Startmenue mit `User Setting Bereich`, `Funktion-Bereich` und `Auswahlbereich`
|
- Startmenue mit `User Setting Bereich`, `Funktion-Bereich` und `Auswahlbereich`
|
||||||
- rechter `Infobereich` mit benutzerbezogener Sichtbarkeit
|
- rechter `Infobereich` mit benutzerbezogener Sichtbarkeit
|
||||||
- `User Self Management` als Setup-App
|
- `User Self Management` als Setup-App
|
||||||
- lokale Benutzereinstellungen fuer Desktop-Skin, App-Auswahl, Infobereich und Profildaten
|
- Benutzereinstellungen fuer Desktop-Skin, App-Auswahl, Infobereich und Profildaten in einer eigenen Datenbanktabelle mit `_user_data`-Suffix
|
||||||
|
- vorhandene JSON-Dateien dienen nur noch als Fallback oder Uebergang fuer lokale Entwicklung und Altbestaende
|
||||||
- LDAP-Synchronisierung fuer Standardfelder wie Name, E-Mail, Telefon, Titel und Ort
|
- LDAP-Synchronisierung fuer Standardfelder wie Name, E-Mail, Telefon, Titel und Ort
|
||||||
- `Geburtsdatum` aktuell bewusst nur lokal gespeichert
|
- `Geburtsdatum` aktuell bewusst nur lokal gespeichert
|
||||||
- Keycloak bleibt das Auth-System
|
- Keycloak bleibt das Auth-System
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ Verbindlich ist:
|
|||||||
- Apps als bereitgestellte Systemfunktionen denken, nicht nur als sichtbare Fenster
|
- Apps als bereitgestellte Systemfunktionen denken, nicht nur als sichtbare Fenster
|
||||||
- Unterschiede zwischen `App`, `Infobereich` und `Widget-Bereich` sauber trennen
|
- Unterschiede zwischen `App`, `Infobereich` und `Widget-Bereich` sauber trennen
|
||||||
- lokale Persistenz und spaetere Backend-Synchronisierung getrennt vorbereiten
|
- lokale Persistenz und spaetere Backend-Synchronisierung getrennt vorbereiten
|
||||||
|
- Nutzerdaten und Desktop-Preferences bevorzugt datenbankbasiert speichern, nicht dateibasiert
|
||||||
- `README.md`-Dateien in Teilbereichen aktuell halten
|
- `README.md`-Dateien in Teilbereichen aktuell halten
|
||||||
- zentrale Doku bei jeder relevanten Struktur- oder Begriffsanpassung mitpflegen
|
- zentrale Doku bei jeder relevanten Struktur- oder Begriffsanpassung mitpflegen
|
||||||
|
|
||||||
@@ -69,3 +70,4 @@ Bei jeder groesseren Aenderung ist zu pruefen:
|
|||||||
- die Schriftfarbe von Desktop-Icons passt sich automatisch an den Hintergrund an
|
- die Schriftfarbe von Desktop-Icons passt sich automatisch an den Hintergrund an
|
||||||
- Benutzerdaten sollen spaeter sauber in LDAP oder Keycloak geschrieben werden
|
- Benutzerdaten sollen spaeter sauber in LDAP oder Keycloak geschrieben werden
|
||||||
- Standard-Fensterinhalte sollen ein gemeinsames Design nutzen
|
- Standard-Fensterinhalte sollen ein gemeinsames Design nutzen
|
||||||
|
- Desktop-Nutzereinstellungen liegen in einer eigenen Tabelle mit erkennbarem `_user_data`-Suffix
|
||||||
|
|||||||
@@ -192,8 +192,35 @@ body[data-skin="apple"] .tray-skin-button {
|
|||||||
|
|
||||||
body[data-skin="apple"] .widget-card {
|
body[data-skin="apple"] .widget-card {
|
||||||
border-radius: 22px;
|
border-radius: 22px;
|
||||||
background: rgba(255, 255, 255, 0.16);
|
color: #0f172a;
|
||||||
border-color: rgba(255, 255, 255, 0.22);
|
background: linear-gradient(180deg, rgba(255, 255, 255, 0.74), rgba(244, 247, 252, 0.68));
|
||||||
|
border-color: rgba(255, 255, 255, 0.42);
|
||||||
|
box-shadow: 0 22px 48px rgba(17, 24, 39, 0.16);
|
||||||
|
backdrop-filter: blur(34px) saturate(1.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-skin="apple"] .widget-card h3,
|
||||||
|
body[data-skin="apple"] .widget-card p {
|
||||||
|
color: #0f172a;
|
||||||
|
text-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-skin="apple"] .widget-card-header p {
|
||||||
|
color: #475569;
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-skin="apple"] .widget-card-action {
|
||||||
|
background: rgba(51, 65, 85, 0.92);
|
||||||
|
color: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-skin="apple"] .widget-card-action:hover {
|
||||||
|
background: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-skin="apple"] .widget-badge {
|
||||||
|
background: rgba(51, 65, 85, 0.72);
|
||||||
|
color: #f8fafc;
|
||||||
}
|
}
|
||||||
|
|
||||||
body[data-skin="apple"] .desktop-icon {
|
body[data-skin="apple"] .desktop-icon {
|
||||||
|
|||||||
189
src/App/DesktopUserPreferenceRepository.php
Normal file
189
src/App/DesktopUserPreferenceRepository.php
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App;
|
||||||
|
|
||||||
|
final class DesktopUserPreferenceRepository
|
||||||
|
{
|
||||||
|
private const TABLE_NAME = 'desktop_preferences_user_data';
|
||||||
|
|
||||||
|
private ?\PDO $pdo = null;
|
||||||
|
private bool $schemaEnsured = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $config
|
||||||
|
*/
|
||||||
|
public function __construct(private readonly array $config)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function available(): bool
|
||||||
|
{
|
||||||
|
return $this->pdo() instanceof \PDO;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>|null
|
||||||
|
*/
|
||||||
|
public function read(string $userScope): ?array
|
||||||
|
{
|
||||||
|
$pdo = $this->pdo();
|
||||||
|
if (!$pdo instanceof \PDO || $userScope === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->ensureSchema($pdo);
|
||||||
|
|
||||||
|
$statement = $pdo->prepare(
|
||||||
|
'SELECT settings_json
|
||||||
|
FROM ' . self::TABLE_NAME . '
|
||||||
|
WHERE user_scope = :user_scope
|
||||||
|
LIMIT 1'
|
||||||
|
);
|
||||||
|
$statement->execute(['user_scope' => $userScope]);
|
||||||
|
$raw = $statement->fetchColumn();
|
||||||
|
|
||||||
|
if (!is_string($raw) || trim($raw) === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$decoded = json_decode($raw, true);
|
||||||
|
|
||||||
|
return is_array($decoded) ? $decoded : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $settings
|
||||||
|
* @param array<string, mixed> $authUser
|
||||||
|
*/
|
||||||
|
public function write(string $userScope, array $settings, array $authUser): void
|
||||||
|
{
|
||||||
|
$pdo = $this->pdo();
|
||||||
|
if (!$pdo instanceof \PDO || $userScope === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->ensureSchema($pdo);
|
||||||
|
|
||||||
|
$payload = [
|
||||||
|
'user_scope' => $userScope,
|
||||||
|
'username' => $this->limit((string) ($authUser['username'] ?? ''), 191),
|
||||||
|
'subject_id' => $this->limit((string) ($authUser['sub'] ?? ''), 191),
|
||||||
|
'settings_json' => json_encode($settings, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT),
|
||||||
|
];
|
||||||
|
|
||||||
|
$driver = strtolower((string) $pdo->getAttribute(\PDO::ATTR_DRIVER_NAME));
|
||||||
|
if ($driver === 'mysql') {
|
||||||
|
$statement = $pdo->prepare(
|
||||||
|
'INSERT INTO ' . self::TABLE_NAME . ' (user_scope, username, subject_id, settings_json)
|
||||||
|
VALUES (:user_scope, :username, :subject_id, :settings_json)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
username = VALUES(username),
|
||||||
|
subject_id = VALUES(subject_id),
|
||||||
|
settings_json = VALUES(settings_json),
|
||||||
|
updated_at = CURRENT_TIMESTAMP'
|
||||||
|
);
|
||||||
|
$statement->execute($payload);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$updated = $pdo->prepare(
|
||||||
|
'UPDATE ' . self::TABLE_NAME . '
|
||||||
|
SET username = :username,
|
||||||
|
subject_id = :subject_id,
|
||||||
|
settings_json = :settings_json,
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE user_scope = :user_scope'
|
||||||
|
);
|
||||||
|
$updated->execute($payload);
|
||||||
|
|
||||||
|
if ($updated->rowCount() > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$insert = $pdo->prepare(
|
||||||
|
'INSERT INTO ' . self::TABLE_NAME . ' (user_scope, username, subject_id, settings_json, created_at, updated_at)
|
||||||
|
VALUES (:user_scope, :username, :subject_id, :settings_json, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)'
|
||||||
|
);
|
||||||
|
$insert->execute($payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function pdo(): ?\PDO
|
||||||
|
{
|
||||||
|
if ($this->pdo instanceof \PDO) {
|
||||||
|
return $this->pdo;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($this->config['enabled']) || !is_array($this->config['db'] ?? null) || $this->config['db'] === []) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->pdo = PdoFactory::createFromArray($this->config['db']);
|
||||||
|
} catch (\Throwable $exception) {
|
||||||
|
error_log('[desktop-user-preferences] DB unavailable: ' . $exception->getMessage());
|
||||||
|
$this->pdo = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->pdo;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function ensureSchema(\PDO $pdo): void
|
||||||
|
{
|
||||||
|
if ($this->schemaEnsured) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$driver = strtolower((string) $pdo->getAttribute(\PDO::ATTR_DRIVER_NAME));
|
||||||
|
$sql = match ($driver) {
|
||||||
|
'pgsql' => <<<SQL
|
||||||
|
CREATE TABLE IF NOT EXISTS desktop_preferences_user_data (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
user_scope TEXT NOT NULL UNIQUE,
|
||||||
|
username TEXT NULL,
|
||||||
|
subject_id TEXT NULL,
|
||||||
|
settings_json TEXT NOT NULL DEFAULT '{}',
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
)
|
||||||
|
SQL,
|
||||||
|
'sqlite' => <<<SQL
|
||||||
|
CREATE TABLE IF NOT EXISTS desktop_preferences_user_data (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_scope TEXT NOT NULL UNIQUE,
|
||||||
|
username TEXT NULL,
|
||||||
|
subject_id TEXT NULL,
|
||||||
|
settings_json TEXT NOT NULL DEFAULT '{}',
|
||||||
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
SQL,
|
||||||
|
default => <<<SQL
|
||||||
|
CREATE TABLE IF NOT EXISTS desktop_preferences_user_data (
|
||||||
|
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
user_scope VARCHAR(191) NOT NULL UNIQUE,
|
||||||
|
username VARCHAR(191) NULL,
|
||||||
|
subject_id VARCHAR(191) NULL,
|
||||||
|
settings_json LONGTEXT NOT NULL,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||||
|
SQL,
|
||||||
|
};
|
||||||
|
|
||||||
|
$pdo->exec($sql);
|
||||||
|
$this->schemaEnsured = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function limit(string $value, int $maxLength): string
|
||||||
|
{
|
||||||
|
$trimmed = trim($value);
|
||||||
|
|
||||||
|
if (function_exists('mb_substr')) {
|
||||||
|
return mb_substr($trimmed, 0, $maxLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
return substr($trimmed, 0, $maxLength);
|
||||||
|
}
|
||||||
|
}
|
||||||
107
src/App/PdoFactory.php
Normal file
107
src/App/PdoFactory.php
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App;
|
||||||
|
|
||||||
|
final class PdoFactory
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $db
|
||||||
|
*/
|
||||||
|
public static function createFromArray(array $db): \PDO
|
||||||
|
{
|
||||||
|
$driver = (string) ($db['driver'] ?? '');
|
||||||
|
if ($driver === '') {
|
||||||
|
throw new \RuntimeException('DB config missing "driver".');
|
||||||
|
}
|
||||||
|
|
||||||
|
$dsn = match ($driver) {
|
||||||
|
'mysql' => self::buildMysqlDsn($db),
|
||||||
|
'pgsql' => self::buildPgsqlDsn($db),
|
||||||
|
'sqlite' => self::buildSqliteDsn($db),
|
||||||
|
default => throw new \RuntimeException('Unsupported PDO driver: ' . $driver),
|
||||||
|
};
|
||||||
|
|
||||||
|
$pdo = new \PDO(
|
||||||
|
$dsn,
|
||||||
|
(string) ($db['user'] ?? ''),
|
||||||
|
(string) ($db['password'] ?? ''),
|
||||||
|
(array) ($db['options'] ?? [])
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($driver === 'pgsql' && !empty($db['schema'])) {
|
||||||
|
$schema = preg_replace('/[^a-zA-Z0-9_]/', '', (string) $db['schema']);
|
||||||
|
if ($schema !== null && $schema !== '') {
|
||||||
|
$pdo->exec('SET search_path TO ' . $schema);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $pdo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $db
|
||||||
|
*/
|
||||||
|
private static function buildMysqlDsn(array $db): string
|
||||||
|
{
|
||||||
|
if (empty($db['dbname'])) {
|
||||||
|
throw new \RuntimeException('MySQL config missing "dbname".');
|
||||||
|
}
|
||||||
|
|
||||||
|
$charset = (string) ($db['charset'] ?? 'utf8mb4');
|
||||||
|
|
||||||
|
if (!empty($db['unix_socket'])) {
|
||||||
|
return sprintf(
|
||||||
|
'mysql:unix_socket=%s;dbname=%s;charset=%s',
|
||||||
|
(string) $db['unix_socket'],
|
||||||
|
(string) $db['dbname'],
|
||||||
|
$charset
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sprintf(
|
||||||
|
'mysql:host=%s;port=%d;dbname=%s;charset=%s',
|
||||||
|
(string) ($db['host'] ?? 'localhost'),
|
||||||
|
(int) ($db['port'] ?? 3306),
|
||||||
|
(string) $db['dbname'],
|
||||||
|
$charset
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $db
|
||||||
|
*/
|
||||||
|
private static function buildPgsqlDsn(array $db): string
|
||||||
|
{
|
||||||
|
if (empty($db['dbname'])) {
|
||||||
|
throw new \RuntimeException('PostgreSQL config missing "dbname".');
|
||||||
|
}
|
||||||
|
|
||||||
|
$dsn = sprintf(
|
||||||
|
'pgsql:host=%s;port=%d;dbname=%s',
|
||||||
|
(string) ($db['host'] ?? 'localhost'),
|
||||||
|
(int) ($db['port'] ?? 5432),
|
||||||
|
(string) $db['dbname']
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isset($db['connect_timeout'])) {
|
||||||
|
$dsn .= ';connect_timeout=' . max(1, (int) $db['connect_timeout']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $dsn;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $db
|
||||||
|
*/
|
||||||
|
private static function buildSqliteDsn(array $db): string
|
||||||
|
{
|
||||||
|
$path = (string) ($db['path'] ?? '');
|
||||||
|
if ($path === '') {
|
||||||
|
throw new \RuntimeException('SQLite config missing "path".');
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'sqlite:' . $path;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -37,7 +37,7 @@ final class UserSelfManagementService
|
|||||||
public function load(array $authUser, array $visibleApps, array $widgets, array $availableSkins): array
|
public function load(array $authUser, array $visibleApps, array $widgets, array $availableSkins): array
|
||||||
{
|
{
|
||||||
return $this->normalizeSettings(
|
return $this->normalizeSettings(
|
||||||
$this->store($this->storageScope($authUser))->read(),
|
$this->readRawSettings($authUser),
|
||||||
$authUser,
|
$authUser,
|
||||||
$visibleApps,
|
$visibleApps,
|
||||||
$widgets,
|
$widgets,
|
||||||
@@ -60,7 +60,7 @@ final class UserSelfManagementService
|
|||||||
$normalized = $this->normalizeSettings($merged, $authUser, $visibleApps, $widgets, $availableSkins);
|
$normalized = $this->normalizeSettings($merged, $authUser, $visibleApps, $widgets, $availableSkins);
|
||||||
$normalized['updated_at'] = gmdate(DATE_ATOM);
|
$normalized['updated_at'] = gmdate(DATE_ATOM);
|
||||||
|
|
||||||
$this->store($this->storageScope($authUser))->write($normalized);
|
$this->writeRawSettings($normalized, $authUser);
|
||||||
|
|
||||||
return $normalized;
|
return $normalized;
|
||||||
}
|
}
|
||||||
@@ -142,7 +142,7 @@ final class UserSelfManagementService
|
|||||||
'meta' => [
|
'meta' => [
|
||||||
'storage_scope' => $this->storageScope($authUser),
|
'storage_scope' => $this->storageScope($authUser),
|
||||||
'authenticated' => ($authUser['sub'] ?? '') !== '' || ($authUser['username'] ?? '') !== '',
|
'authenticated' => ($authUser['sub'] ?? '') !== '' || ($authUser['username'] ?? '') !== '',
|
||||||
'sync_mode' => 'local-profile',
|
'sync_mode' => $this->repository()->available() ? 'base-db-user-data' : 'local-fallback',
|
||||||
'sync_targets' => [
|
'sync_targets' => [
|
||||||
'ldap' => 'planned',
|
'ldap' => 'planned',
|
||||||
'keycloak' => 'planned',
|
'keycloak' => 'planned',
|
||||||
@@ -162,11 +162,26 @@ final class UserSelfManagementService
|
|||||||
return $sanitized !== null && $sanitized !== '' ? $sanitized : 'guest';
|
return $sanitized !== null && $sanitized !== '' ? $sanitized : 'guest';
|
||||||
}
|
}
|
||||||
|
|
||||||
private function store(string $scope): JsonStore
|
private function fileStore(string $scope): JsonStore
|
||||||
{
|
{
|
||||||
return new JsonStore($this->projectRoot . '/data/user-self-management/users/' . $scope . '.json');
|
return new JsonStore($this->projectRoot . '/data/user-self-management/users/' . $scope . '.json');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function repository(): DesktopUserPreferenceRepository
|
||||||
|
{
|
||||||
|
static $repository = null;
|
||||||
|
|
||||||
|
if ($repository instanceof DesktopUserPreferenceRepository) {
|
||||||
|
return $repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
$repository = new DesktopUserPreferenceRepository(
|
||||||
|
ConfigLoader::load($this->projectRoot, 'base_db')
|
||||||
|
);
|
||||||
|
|
||||||
|
return $repository;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<string, mixed> $settings
|
* @param array<string, mixed> $settings
|
||||||
* @param array<string, mixed> $authUser
|
* @param array<string, mixed> $authUser
|
||||||
@@ -236,6 +251,49 @@ final class UserSelfManagementService
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $authUser
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private function readRawSettings(array $authUser): array
|
||||||
|
{
|
||||||
|
$scope = $this->storageScope($authUser);
|
||||||
|
$repository = $this->repository();
|
||||||
|
|
||||||
|
if ($repository->available()) {
|
||||||
|
$stored = $repository->read($scope);
|
||||||
|
if (is_array($stored)) {
|
||||||
|
return $stored;
|
||||||
|
}
|
||||||
|
|
||||||
|
$legacy = $this->fileStore($scope)->read();
|
||||||
|
if ($legacy !== []) {
|
||||||
|
$repository->write($scope, $legacy, $authUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $legacy;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->fileStore($scope)->read();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $settings
|
||||||
|
* @param array<string, mixed> $authUser
|
||||||
|
*/
|
||||||
|
private function writeRawSettings(array $settings, array $authUser): void
|
||||||
|
{
|
||||||
|
$scope = $this->storageScope($authUser);
|
||||||
|
$repository = $this->repository();
|
||||||
|
|
||||||
|
if ($repository->available()) {
|
||||||
|
$repository->write($scope, $settings, $authUser);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->fileStore($scope)->write($settings);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param mixed $selected
|
* @param mixed $selected
|
||||||
* @param array<int, string> $allowedIds
|
* @param array<int, string> $allowedIds
|
||||||
|
|||||||
Reference in New Issue
Block a user