Add the central site registry and remote updates
Every installation now reports address and version to sites.kant-it.de every 15 minutes and picks up update orders there (ADR 123). Reports and answers are HMAC-signed with a per-installation key sent on first contact; an order can only install a published release with checksum. The next request after the file swap runs the migrations, so a remote update finishes without anyone in the admin. The platform itself lives in zentrale/ (plain PHP + SQLite, not part of the release archive): overview with outdated, silent and unassigned installations, single and bulk updates, per-site detail and history. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
32cb77b72e
commit
723680b7de
39 files changed
+2355
-27
No files matched your search
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Zentrale;
|
||||
|
||||
/**
|
||||
* Anmeldung: Name und Passwort, gehasht mit password_hash() (bcrypt/argon2 je nach PHP).
|
||||
*
|
||||
* Nach fünf Fehlversuchen in 15 Minuten nimmt diese IP-Adresse eine Viertelstunde lang keine
|
||||
* Anmeldung mehr an.
|
||||
*/
|
||||
final class Konten
|
||||
{
|
||||
private const VERSUCHE = 5;
|
||||
private const SPERRE_SEKUNDEN = 900;
|
||||
|
||||
public function __construct(private \PDO $db)
|
||||
{
|
||||
}
|
||||
|
||||
public function gibtEs(): bool
|
||||
{
|
||||
return (int) $this->db->query('SELECT COUNT(*) FROM konto')->fetchColumn() > 0;
|
||||
}
|
||||
|
||||
public function anlegen(string $name, string $passwort): void
|
||||
{
|
||||
$this->db->prepare('INSERT INTO konto (name, passwort, erstellt) VALUES (?, ?, ?)')
|
||||
->execute([$name, password_hash($passwort, PASSWORD_DEFAULT), Datenbank::jetzt()]);
|
||||
}
|
||||
|
||||
public function gesperrt(string $ip): bool
|
||||
{
|
||||
$abfrage = $this->db->prepare('SELECT COUNT(*) FROM anmeldeversuch WHERE ip = ? AND zeit > ?');
|
||||
$abfrage->execute([$ip, gmdate('Y-m-d\TH:i:s\Z', time() - self::SPERRE_SEKUNDEN)]);
|
||||
|
||||
return (int) $abfrage->fetchColumn() >= self::VERSUCHE;
|
||||
}
|
||||
|
||||
/** @return array{id: int, name: string}|null */
|
||||
public function pruefen(string $name, string $passwort, string $ip): ?array
|
||||
{
|
||||
$abfrage = $this->db->prepare('SELECT id, name, passwort FROM konto WHERE name = ?');
|
||||
$abfrage->execute([$name]);
|
||||
$konto = $abfrage->fetch() ?: null;
|
||||
// Auch ohne Konto einmal hashen: Die Antwortzeit verrät sonst, welche Namen es gibt.
|
||||
$hash = null !== $konto ? (string) $konto['passwort'] : password_hash('attrappe', PASSWORD_DEFAULT);
|
||||
if (!password_verify($passwort, $hash) || null === $konto) {
|
||||
$this->fehlversuch($ip);
|
||||
|
||||
return null;
|
||||
}
|
||||
if (password_needs_rehash($hash, PASSWORD_DEFAULT)) {
|
||||
$this->db->prepare('UPDATE konto SET passwort = ? WHERE id = ?')->execute([password_hash($passwort, PASSWORD_DEFAULT), $konto['id']]);
|
||||
}
|
||||
$this->db->prepare('DELETE FROM anmeldeversuch WHERE ip = ?')->execute([$ip]);
|
||||
|
||||
return ['id' => (int) $konto['id'], 'name' => (string) $konto['name']];
|
||||
}
|
||||
|
||||
public function fehlversuch(string $ip): void
|
||||
{
|
||||
$this->db->prepare('INSERT INTO anmeldeversuch (ip, zeit) VALUES (?, ?)')->execute([$ip, Datenbank::jetzt()]);
|
||||
$this->db->prepare('DELETE FROM anmeldeversuch WHERE zeit < ?')->execute([gmdate('Y-m-d\TH:i:s\Z', time() - 86400)]);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user