Initial
EK-DOS-WEB bauen und ausrollen / frontend (push) Failing after 22s
EK-DOS-WEB bauen und ausrollen / backend (push) Failing after 27s
EK-DOS-WEB bauen und ausrollen / deploy (push) Skipped

This commit is contained in:
Kyle Müller
2026-09-06 13:25:44 +02:00
commit 7d82e807fc
71 changed files with 10601 additions and 0 deletions
+250
View File
@@ -0,0 +1,250 @@
#!/usr/bin/env php
<?php
declare(strict_types=1);
/**
* Wartungs-CLI.
*
* php bin/ekdos migrate Schema anlegen oder fortschreiben
* php bin/ekdos user:create Benutzer anlegen (Passwort wird abgefragt)
* php bin/ekdos user:list Benutzer auflisten
* php bin/ekdos user:password <name> Passwort zuruecksetzen
* php bin/ekdos cache:flush n8n-Cache leeren
* php bin/ekdos check Postgres, Redis und n8n pruefen
*
* Der erste Administrator wird mit user:create angelegt. Es gibt bewusst keinen
* Bootstrap-Benutzer aus der Umgebung: ein Passwort in einer .env ist ein
* Passwort, das niemand mehr aendert.
*/
use Ekdos\Auth\AuthService;
use Ekdos\Bootstrap\Container;
use Ekdos\N8n\Cache;
use Ekdos\N8n\Client;
use Ekdos\N8n\Endpoints;
use Ekdos\Support\Config;
use Ekdos\Users\Role;
use Ekdos\Users\UserRepository;
require __DIR__ . '/../vendor/autoload.php';
$projectRoot = dirname(__DIR__);
$command = $argv[1] ?? 'help';
function out(string $line = ''): void
{
fwrite(STDOUT, $line . PHP_EOL);
}
function fail(string $line): never
{
fwrite(STDERR, $line . PHP_EOL);
exit(1);
}
/** Liest eine Eingabe ohne Bildschirmausgabe. */
function readSecret(string $prompt): string
{
fwrite(STDERR, $prompt);
if (DIRECTORY_SEPARATOR === '/' && function_exists('shell_exec')) {
shell_exec('stty -echo 2>/dev/null');
}
$value = trim((string) fgets(STDIN));
if (DIRECTORY_SEPARATOR === '/' && function_exists('shell_exec')) {
shell_exec('stty echo 2>/dev/null');
}
fwrite(STDERR, PHP_EOL);
return $value;
}
function ask(string $prompt, string $fallback = ''): string
{
fwrite(STDERR, $prompt);
$value = trim((string) fgets(STDIN));
return $value !== '' ? $value : $fallback;
}
$container = Container::build($projectRoot);
switch ($command) {
case 'migrate':
/** @var PDO $db */
$db = $container->get(PDO::class);
$db->exec('create table if not exists schema_migrations (version text primary key, applied_at timestamptz not null default now())');
$applied = $db->query('select version from schema_migrations')->fetchAll(PDO::FETCH_COLUMN);
$files = glob($projectRoot . '/migrations/*.sql') ?: [];
sort($files);
$ran = 0;
foreach ($files as $file) {
$version = basename($file, '.sql');
if (in_array($version, $applied, true)) {
continue;
}
$sql = file_get_contents($file);
if ($sql === false) {
fail('Migration nicht lesbar: ' . $file);
}
$db->beginTransaction();
try {
$db->exec($sql);
$statement = $db->prepare('insert into schema_migrations (version) values (:version)');
$statement->execute(['version' => $version]);
$db->commit();
} catch (Throwable $exception) {
$db->rollBack();
fail('Migration ' . $version . ' fehlgeschlagen: ' . $exception->getMessage());
}
out(' angewendet: ' . $version);
$ran++;
}
out($ran === 0 ? 'Das Schema ist aktuell.' : $ran . ' Migration(en) angewendet.');
break;
case 'user:create':
/** @var UserRepository $users */
$users = $container->get(UserRepository::class);
$username = ask('Benutzername: ');
$displayName = ask('Anzeigename: ', $username);
$roleInput = ask('Rolle [admin|inhaber|buero] (admin): ', 'admin');
$role = Role::tryFrom($roleInput) ?? fail('Unbekannte Rolle: ' . $roleInput);
if ($username === '') {
fail('Der Benutzername darf nicht leer sein.');
}
if ($users->usernameTaken($username)) {
fail('Dieser Benutzername ist bereits vergeben.');
}
$password = readSecret('Passwort: ');
if (($problem = AuthService::rejectWeakPassword($password)) !== null) {
fail($problem);
}
if ($password !== readSecret('Passwort wiederholen: ')) {
fail('Die Passwörter stimmen nicht überein.');
}
$user = $users->create($username, $displayName, $role, AuthService::hash($password));
$users->audit(null, 'CLI', 'user.create', $user->id, $user->username, ['role' => $role->value]);
out('Angelegt: ' . $user->username . ' (' . $user->role->label() . ')');
break;
case 'user:list':
/** @var UserRepository $users */
$users = $container->get(UserRepository::class);
$rows = $users->all();
if ($rows === []) {
out('Es ist noch kein Benutzer angelegt. Anlegen mit: php bin/ekdos user:create');
break;
}
out(sprintf('%-22s %-24s %-14s %-8s %s', 'BENUTZER', 'NAME', 'ROLLE', 'AKTIV', 'LETZTE ANMELDUNG'));
foreach ($rows as $user) {
out(sprintf(
'%-22s %-24s %-14s %-8s %s',
$user->username,
$user->displayName,
$user->role->value,
$user->isActive ? 'ja' : 'nein',
$user->lastLoginAt ?? '',
));
}
break;
case 'user:password':
/** @var UserRepository $users */
$users = $container->get(UserRepository::class);
$username = $argv[2] ?? fail('Aufruf: php bin/ekdos user:password <benutzername>');
$user = $users->findByUsernameWithHash($username) ?? fail('Unbekannter Benutzer: ' . $username);
$password = readSecret('Neues Passwort für ' . $user->username . ': ');
if (($problem = AuthService::rejectWeakPassword($password)) !== null) {
fail($problem);
}
$users->update($user->id, ['password_hash' => AuthService::hash($password)]);
$container->get(\Ekdos\Auth\SessionStore::class)->destroyAllFor($user->id);
$users->audit(null, 'CLI', 'user.password', $user->id, $user->username);
out('Passwort geändert. Offene Sitzungen dieses Benutzers wurden beendet.');
break;
case 'cache:flush':
/** @var Cache $cache */
$cache = $container->get(Cache::class);
out($cache->flushAll() . ' zwischengespeicherte n8n-Antworten verworfen.');
break;
case 'check':
/** @var Config $config */
$config = $container->get(Config::class);
$problems = 0;
try {
$container->get(PDO::class)->query('select 1');
out(' Postgres erreichbar');
} catch (Throwable $exception) {
out(' Postgres FEHLER: ' . $exception->getMessage());
$problems++;
}
try {
$container->get(Redis::class)->ping();
out(' Redis erreichbar');
} catch (Throwable $exception) {
out(' Redis FEHLER: ' . $exception->getMessage());
$problems++;
}
/** @var Endpoints $endpoints */
$endpoints = $container->get(Endpoints::class);
/** @var Client $n8n */
$n8n = $container->get(Client::class);
$reply = $n8n->get($endpoints->openTickets());
if ($reply->status === 0) {
out(' n8n FEHLER: nicht erreichbar unter ' . $endpoints->openTickets());
$problems++;
} else {
out(' n8n HTTP ' . $reply->status . ' von ' . $endpoints->openTickets());
}
foreach (['N8N_CUSTOMER_KEY', 'N8N_INVOICE_SYNC_SECRET'] as $secret) {
if (!$config->has($secret)) {
out(' Hinweis ' . $secret . ' ist nicht gesetzt; betroffene Funktionen antworten mit 503.');
}
}
exit($problems === 0 ? 0 : 1);
default:
out('EK-DOS-WEB Wartungs-CLI');
out();
out(' php bin/ekdos migrate Schema anlegen oder fortschreiben');
out(' php bin/ekdos user:create Benutzer anlegen');
out(' php bin/ekdos user:list Benutzer auflisten');
out(' php bin/ekdos user:password <name> Passwort zurücksetzen');
out(' php bin/ekdos cache:flush n8n-Cache leeren');
out(' php bin/ekdos check Postgres, Redis und n8n prüfen');
}