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>
68 lines
2.5 KiB
PHP
68 lines
2.5 KiB
PHP
<?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)]);
|
|
}
|
|
}
|