M8: Security-Headers, Rate-Limits und Audit-Log
Erste drei Bausteine der Haertung: - Security-Headers (X-Content-Type-Options, X-Frame-Options, Referrer- Policy, Permissions-Policy, HSTS bei HTTPS) laufen automatisch ueber app_send_security_headers() am Ende von app/bootstrap.php fuer jede dynamische Seite; landing.php war als einzige Seite ganz ohne PHP und bekam einen minimalen Bootstrap-Aufruf. Bewusst kein CSP, da die bestehenden Templates durchgaengig auf Inline-style-Attribute setzen. - DB-gestuetzte Rate-Limits (neue Tabelle rate_limit_attempts) fuer Login (10/15min je E-Mail, 20/15min je IP), Registrierung (5/h je IP) und Passwort-Reset-Anfrage (5/h je E-Mail, 10/h je IP); bei ausgereiztem Reset-Limit erscheint dieselbe generische Meldung wie im Erfolgsfall, um kein Konto-Enumeration-Signal zu geben. - Zentrales Audit-Log (neue Tabelle audit_log) fuer Mitgliederverwaltung, Zugangsvergabe/-entzug, Storno, Mandant-Einstellungen, Hinweise, CSV-Import, Jahresbonus-Verteilung und Live-Mailversand; sichtbar fuer Owner/Admin auf mandant-einstellungen.php. Live getestet: Rate-Limit greift nach 10 Fehlversuchen, Audit-Log-Eintrag mit korrekten Metadaten und Nutzernamen ueber einen isolierten Test- Mandanten geprueft. Alle Regressionstests weiterhin gruen (26/26 Smoke, 104 Golden-Master-Assertions). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/bootstrap.php';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Records one admin/security-relevant action. Keep $action as a short,
|
||||||
|
* stable slug (e.g. "participant.access_granted") so entries stay
|
||||||
|
* filterable; put anything variable in $metadata.
|
||||||
|
*/
|
||||||
|
function app_audit_log(
|
||||||
|
PDO $pdo,
|
||||||
|
?int $tenantId,
|
||||||
|
?int $actorUserId,
|
||||||
|
string $action,
|
||||||
|
string $subjectType,
|
||||||
|
?int $subjectId,
|
||||||
|
array $metadata = []
|
||||||
|
): void {
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
'INSERT INTO audit_log (tenant_id, actor_user_id, action, subject_type, subject_id, metadata_json, ip)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?)'
|
||||||
|
);
|
||||||
|
$stmt->execute([
|
||||||
|
$tenantId,
|
||||||
|
$actorUserId,
|
||||||
|
$action,
|
||||||
|
$subjectType,
|
||||||
|
$subjectId,
|
||||||
|
$metadata !== [] ? json_encode($metadata, JSON_UNESCAPED_UNICODE) : null,
|
||||||
|
$_SERVER['REMOTE_ADDR'] ?? null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<array{id: int, actor_user_id: ?int, actor_name: ?string, action: string, subject_type: string, subject_id: ?int, metadata_json: ?string, ip: ?string, created_at: string}>
|
||||||
|
*/
|
||||||
|
function app_fetch_audit_log(PDO $pdo, int $tenantId, int $limit = 100): array
|
||||||
|
{
|
||||||
|
$limit = max(1, min($limit, 500));
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
"SELECT a.id, a.actor_user_id, u.display_name AS actor_name, a.action, a.subject_type, a.subject_id, a.metadata_json, a.ip, a.created_at
|
||||||
|
FROM audit_log a
|
||||||
|
LEFT JOIN users u ON u.id = a.actor_user_id
|
||||||
|
WHERE a.tenant_id = ?
|
||||||
|
ORDER BY a.created_at DESC, a.id DESC
|
||||||
|
LIMIT {$limit}"
|
||||||
|
);
|
||||||
|
$stmt->execute([$tenantId]);
|
||||||
|
|
||||||
|
return $stmt->fetchAll();
|
||||||
|
}
|
||||||
@@ -27,6 +27,29 @@ function app_is_https(): bool
|
|||||||
|| (($_SERVER['SERVER_PORT'] ?? null) === '443');
|
|| (($_SERVER['SERVER_PORT'] ?? null) === '443');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends baseline security headers on every dynamic request. Deliberately
|
||||||
|
* does not set a Content-Security-Policy: the existing templates rely on
|
||||||
|
* inline style="" attributes throughout (banners, table cells, etc.), and
|
||||||
|
* locking that down would need a broader template pass. Runs once per
|
||||||
|
* request via the auto-invocation at the bottom of this file.
|
||||||
|
*/
|
||||||
|
function app_send_security_headers(): void
|
||||||
|
{
|
||||||
|
if (PHP_SAPI === 'cli' || headers_sent()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
header('X-Content-Type-Options: nosniff');
|
||||||
|
header('X-Frame-Options: DENY');
|
||||||
|
header('Referrer-Policy: strict-origin-when-cross-origin');
|
||||||
|
header('Permissions-Policy: geolocation=(), microphone=(), camera=()');
|
||||||
|
|
||||||
|
if (app_is_https()) {
|
||||||
|
header('Strict-Transport-Security: max-age=31536000; includeSubDomains');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function app_start_session(): void
|
function app_start_session(): void
|
||||||
{
|
{
|
||||||
if (PHP_SAPI === 'cli' || session_status() === PHP_SESSION_ACTIVE) {
|
if (PHP_SAPI === 'cli' || session_status() === PHP_SESSION_ACTIVE) {
|
||||||
@@ -102,3 +125,5 @@ function app_require_csrf(): void
|
|||||||
die('CSRF validation failed.');
|
die('CSRF validation failed.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
app_send_security_headers();
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/bootstrap.php';
|
||||||
|
|
||||||
|
function app_client_ip(): string
|
||||||
|
{
|
||||||
|
return (string)($_SERVER['REMOTE_ADDR'] ?? 'unknown');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Records an attempt in the given bucket and reports whether the caller is
|
||||||
|
* still within the allowed rate. Call before doing the sensitive work (auth
|
||||||
|
* check, mail dispatch, account creation); if this returns false, show a
|
||||||
|
* generic "too many attempts" message without processing the request, so a
|
||||||
|
* bucket also can't be used to enumerate valid accounts by timing.
|
||||||
|
*/
|
||||||
|
function app_rate_limit_check(PDO $pdo, string $bucket, int $maxAttempts, int $windowSeconds): bool
|
||||||
|
{
|
||||||
|
$pdo->prepare('INSERT INTO rate_limit_attempts (bucket) VALUES (?)')->execute([$bucket]);
|
||||||
|
|
||||||
|
// Opportunistic cleanup so the table doesn't grow unbounded; scoped to
|
||||||
|
// this bucket to keep each call cheap.
|
||||||
|
$pdo->prepare('DELETE FROM rate_limit_attempts WHERE bucket = ? AND created_at < DATE_SUB(NOW(), INTERVAL ? SECOND)')
|
||||||
|
->execute([$bucket, $windowSeconds]);
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
'SELECT COUNT(*) FROM rate_limit_attempts WHERE bucket = ? AND created_at >= DATE_SUB(NOW(), INTERVAL ? SECOND)'
|
||||||
|
);
|
||||||
|
$stmt->execute([$bucket, $windowSeconds]);
|
||||||
|
|
||||||
|
return (int)$stmt->fetchColumn() <= $maxAttempts;
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
include "functions.php";
|
include "functions.php";
|
||||||
require_once __DIR__ . "/app/ledger.php";
|
require_once __DIR__ . "/app/ledger.php";
|
||||||
require_once __DIR__ . "/app/imports.php";
|
require_once __DIR__ . "/app/imports.php";
|
||||||
|
require_once __DIR__ . "/app/audit.php";
|
||||||
app_require_csrf();
|
app_require_csrf();
|
||||||
include "header.php";
|
include "header.php";
|
||||||
include "headerline.php";
|
include "headerline.php";
|
||||||
@@ -180,6 +181,7 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
|
|||||||
$batchId = (int)($_POST['batch_id'] ?? 0);
|
$batchId = (int)($_POST['batch_id'] ?? 0);
|
||||||
try {
|
try {
|
||||||
$ergebnis = imports_commit_batch($pdo, $tenantId, $batchId);
|
$ergebnis = imports_commit_batch($pdo, $tenantId, $batchId);
|
||||||
|
app_audit_log($pdo, $tenantId, $saasUser['user_id'] ?? null, 'csv_import.committed', 'payment_import_batch', $batchId, ['imported' => $ergebnis['imported']]);
|
||||||
$meldung = $ergebnis['imported'] . ' Einzahlungen wurden importiert.';
|
$meldung = $ergebnis['imported'] . ' Einzahlungen wurden importiert.';
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
$fehler = $e->getMessage();
|
$fehler = $e->getMessage();
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS rate_limit_attempts (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
bucket VARCHAR(191) NOT NULL,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
KEY idx_rate_limit_attempts_bucket_created (bucket, created_at)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS audit_log (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
tenant_id INT NULL,
|
||||||
|
actor_user_id INT NULL,
|
||||||
|
action VARCHAR(60) NOT NULL,
|
||||||
|
subject_type VARCHAR(60) NOT NULL,
|
||||||
|
subject_id INT NULL,
|
||||||
|
metadata_json TEXT NULL,
|
||||||
|
ip VARCHAR(45) NULL,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
KEY idx_audit_log_tenant_created (tenant_id, created_at),
|
||||||
|
KEY idx_audit_log_actor_user (actor_user_id),
|
||||||
|
KEY idx_audit_log_subject (subject_type, subject_id),
|
||||||
|
CONSTRAINT fk_audit_log_tenant
|
||||||
|
FOREIGN KEY (tenant_id) REFERENCES tenants(id)
|
||||||
|
ON DELETE CASCADE,
|
||||||
|
CONSTRAINT fk_audit_log_actor_user
|
||||||
|
FOREIGN KEY (actor_user_id) REFERENCES users(id)
|
||||||
|
ON DELETE SET NULL
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
# M8 Härtung, Datenschutz und Betrieb
|
||||||
|
|
||||||
|
Stand: 2026-07-15
|
||||||
|
|
||||||
|
M8 macht die SaaS-App für den mehrmandantenfähigen Betrieb sicherer und
|
||||||
|
nachvollziehbarer: Security-Headers, Rate-Limits, ein zentrales Audit-Log,
|
||||||
|
sowie (folgend) Mandanten-Isolation, Rollenmatrix, Datenexport und
|
||||||
|
Löschung/Anonymisierung.
|
||||||
|
|
||||||
|
## Security-Headers
|
||||||
|
|
||||||
|
Umgesetzte Dateien:
|
||||||
|
|
||||||
|
```text
|
||||||
|
app/bootstrap.php
|
||||||
|
landing.php
|
||||||
|
```
|
||||||
|
|
||||||
|
- `app_send_security_headers()` setzt `X-Content-Type-Options: nosniff`,
|
||||||
|
`X-Frame-Options: DENY`, `Referrer-Policy: strict-origin-when-cross-origin`,
|
||||||
|
`Permissions-Policy` (Geolocation/Mikrofon/Kamera aus) sowie `Strict-
|
||||||
|
Transport-Security` bei HTTPS. Der Aufruf steht am Ende von
|
||||||
|
`app/bootstrap.php`, das transitiv von jeder dynamischen Seite geladen
|
||||||
|
wird, und läuft damit automatisch einmal pro Request.
|
||||||
|
- Bewusst **kein** Content-Security-Policy-Header: Die bestehenden
|
||||||
|
Templates nutzen durchgängig `style=""`-Inline-Attribute (Banner,
|
||||||
|
Tabellenzellen, Status-Einfärbung). Ein CSP-Lockdown würde diese
|
||||||
|
brechen und bräuchte einen eigenen Template-Durchgang – als offener
|
||||||
|
Punkt vermerkt, nicht in diesem Schritt umgesetzt.
|
||||||
|
- `landing.php` war die einzige Seite ganz ohne PHP (reines HTML). Ein
|
||||||
|
minimaler `require_once app/bootstrap.php`-Aufruf am Dateianfang sorgt
|
||||||
|
dafür, dass auch sie die Header bekommt, ohne das Markup zu verändern.
|
||||||
|
|
||||||
|
## Rate-Limits
|
||||||
|
|
||||||
|
Umgesetzte Dateien:
|
||||||
|
|
||||||
|
```text
|
||||||
|
database/migrations/0010_saas_rate_limits.sql
|
||||||
|
app/rate-limit.php
|
||||||
|
login.php
|
||||||
|
register.php
|
||||||
|
passwort-vergessen.php
|
||||||
|
```
|
||||||
|
|
||||||
|
- Neue Tabelle `rate_limit_attempts` (Bucket + Zeitstempel) mit
|
||||||
|
`app_rate_limit_check()`: schreibt einen Versuch, räumt alte Einträge
|
||||||
|
für denselben Bucket auf und meldet, ob das Limit noch eingehalten ist.
|
||||||
|
- Login: 10 Versuche / 15 Minuten je E-Mail-Adresse **und** 20 Versuche /
|
||||||
|
15 Minuten je IP (beide müssen greifen, damit ein einzelnes Konto nicht
|
||||||
|
gezielt von verteilten IPs aus brute-forced werden kann und eine IP
|
||||||
|
nicht viele Konten gleichzeitig durchprobieren kann).
|
||||||
|
- Registrierung: 5 Versuche / Stunde je IP (gegen Spam-Registrierungen).
|
||||||
|
- Passwort-Reset-Anfrage: 5 / Stunde je E-Mail, 10 / Stunde je IP. Bei
|
||||||
|
überschrittenem Limit erscheint bewusst dieselbe generische
|
||||||
|
Erfolgsmeldung wie im echten Erfolgsfall, damit ein ausgereiztes Limit
|
||||||
|
nicht zusätzlich verrät, ob ein Konto existiert.
|
||||||
|
- Live getestet: 11 aufeinanderfolgende Fehlversuche gegen `login.php` –
|
||||||
|
die ersten 10 zeigen „ungültig“, der 11. wird korrekt mit „Zu viele
|
||||||
|
Anmeldeversuche“ abgewiesen.
|
||||||
|
|
||||||
|
## Audit-Log
|
||||||
|
|
||||||
|
Umgesetzte Dateien:
|
||||||
|
|
||||||
|
```text
|
||||||
|
database/migrations/0011_saas_audit_log.sql
|
||||||
|
app/audit.php
|
||||||
|
mitarbeiterverwalten.php
|
||||||
|
letzteneintraege.php
|
||||||
|
mandant-einstellungen.php
|
||||||
|
hinweise.php
|
||||||
|
csvupload.php
|
||||||
|
jahresauswertung.php
|
||||||
|
mailversenden.php
|
||||||
|
```
|
||||||
|
|
||||||
|
- Neue Tabelle `audit_log` (Mandant, ausführender Nutzer, Aktion,
|
||||||
|
betroffener Datensatz, Metadaten als JSON, IP, Zeitpunkt) gemäß
|
||||||
|
Kernschema aus dem Umstrukturierungsplan.
|
||||||
|
- `app_audit_log()` schreibt einen Eintrag, `app_fetch_audit_log()` liest
|
||||||
|
die letzten N Einträge eines Mandanten.
|
||||||
|
- Protokollierte Aktionen: Mitglied anlegen/bearbeiten/(de)aktivieren,
|
||||||
|
Zugang gewähren/entziehen, Storno von Einzahlung/Strich, Mandant-
|
||||||
|
Einstellungen ändern, Hinweis anlegen/löschen, CSV-Import bestätigen,
|
||||||
|
Jahresbonus-Verteilung, Live-Mailversand.
|
||||||
|
- Sichtbar für Owner/Admin auf `mandant-einstellungen.php` unter
|
||||||
|
„Protokoll" (letzte 50 Einträge, mit Namen des ausführenden Nutzers).
|
||||||
|
- Live getestet: Hinweis anlegen erzeugt einen Log-Eintrag mit korrekten
|
||||||
|
Metadaten; über einen eigens angelegten Test-Mandanten geprüft, dass
|
||||||
|
eine Einstellungsänderung im UI-Protokoll mit korrektem Nutzernamen
|
||||||
|
erscheint. Testdaten anschließend entfernt.
|
||||||
|
|
||||||
|
## Prüfstatus
|
||||||
|
|
||||||
|
- Golden Master: grün mit 104 Assertions.
|
||||||
|
- M4 Ledger-Migration: grün mit 73 Assertions.
|
||||||
|
- M3 Settings-Flow: grün mit 13 Assertions.
|
||||||
|
- HTTP-Smoke: grün mit 26 geprüften Seiten.
|
||||||
|
|
||||||
|
## Noch offen in M8
|
||||||
|
|
||||||
|
- Content-Security-Policy (braucht Template-Bereinigung der Inline-Styles).
|
||||||
|
- Mandanten-Isolation automatisiert testen.
|
||||||
|
- Rollenmatrix automatisiert testen.
|
||||||
|
- Datenexport pro Mandant.
|
||||||
|
- Lösch-/Anonymisierungsprozess für Teilnehmer und Kunden.
|
||||||
|
- Backup-/Restore-Prozess und Monitoring/Fehlerlogging dokumentieren.
|
||||||
@@ -308,7 +308,7 @@ Nicht tun:
|
|||||||
| M5 | App-Kern | Weit fortgeschritten: Kernseiten lesen und schreiben tenant-sicher gegen das Ledger, inklusive Hinweise und rollenbasiertem Zugang; offen ist ein eigener Zahlungs-Screen |
|
| M5 | App-Kern | Weit fortgeschritten: Kernseiten lesen und schreiben tenant-sicher gegen das Ledger, inklusive Hinweise und rollenbasiertem Zugang; offen ist ein eigener Zahlungs-Screen |
|
||||||
| M6 | Betriebsflows | Abgeschlossen: Import, Export, Mail und Jahresabschluss sind auditierbar |
|
| M6 | Betriebsflows | Abgeschlossen: Import, Export, Mail und Jahresabschluss sind auditierbar |
|
||||||
| M7 | Landingpage | Public-Seite und Auth-Seiten sind im gemeinsamen Stil nutzbar; spätere Ausbaustufen folgen |
|
| M7 | Landingpage | Public-Seite und Auth-Seiten sind im gemeinsamen Stil nutzbar; spätere Ausbaustufen folgen |
|
||||||
| M8 | Härtung | Betrieb, Datenschutz, Monitoring und Isolation sind geprüft |
|
| M8 | Härtung | Gestartet: Security-Headers, Rate-Limits und Audit-Log stehen; Isolation/Rollenmatrix-Tests, Datenexport, Löschung und Betrieb offen |
|
||||||
| M9 | Cutover | Produktivumstellung ist vorbereitet und Legacy ist read-only |
|
| M9 | Cutover | Produktivumstellung ist vorbereitet und Legacy ist read-only |
|
||||||
|
|
||||||
### M0: Planungs- und Sicherheitsbaseline
|
### M0: Planungs- und Sicherheitsbaseline
|
||||||
@@ -643,16 +643,22 @@ Schritte:
|
|||||||
|
|
||||||
- Backups und Restore-Prozess definieren.
|
- Backups und Restore-Prozess definieren.
|
||||||
- Monitoring und Fehlerlogging einrichten.
|
- Monitoring und Fehlerlogging einrichten.
|
||||||
- Audit-Log für Admin-Aktionen prüfen.
|
- Audit-Log für Admin-Aktionen prüfen: erledigt. Neue Tabelle `audit_log`,
|
||||||
|
protokolliert Mitgliederverwaltung, Storno, Mandant-Einstellungen,
|
||||||
|
Hinweise, CSV-Import, Jahresbonus und Live-Mailversand; sichtbar für
|
||||||
|
Owner/Admin auf `mandant-einstellungen.php`.
|
||||||
- Datenexport pro Tenant.
|
- Datenexport pro Tenant.
|
||||||
- Lösch-/Anonymisierungsprozess für Teilnehmer und Kunden.
|
- Lösch-/Anonymisierungsprozess für Teilnehmer und Kunden.
|
||||||
- Rate-Limits und Security Headers.
|
- Rate-Limits und Security Headers: erledigt. Globale Security-Headers
|
||||||
|
über `app/bootstrap.php` (ohne CSP, siehe `docs/m8-haertung.md`),
|
||||||
|
DB-gestützte Rate-Limits für Login, Registrierung und Passwort-Reset.
|
||||||
- Mandanten-Isolation testen.
|
- Mandanten-Isolation testen.
|
||||||
- Rollenmatrix testen.
|
- Rollenmatrix testen.
|
||||||
|
|
||||||
Ergebnis:
|
Ergebnis:
|
||||||
|
|
||||||
- SaaS ist betrieblich und datenschutzseitig belastbarer.
|
- SaaS ist betrieblich und datenschutzseitig belastbarer.
|
||||||
|
- Stand: gestartet. Dokumentation: `docs/m8-haertung.md`.
|
||||||
|
|
||||||
Abhängigkeiten:
|
Abhängigkeiten:
|
||||||
|
|
||||||
|
|||||||
+7
-4
@@ -3,6 +3,7 @@
|
|||||||
include "functions.php";
|
include "functions.php";
|
||||||
require_once __DIR__ . "/app/ledger.php";
|
require_once __DIR__ . "/app/ledger.php";
|
||||||
require_once __DIR__ . "/app/notices.php";
|
require_once __DIR__ . "/app/notices.php";
|
||||||
|
require_once __DIR__ . "/app/audit.php";
|
||||||
app_require_csrf();
|
app_require_csrf();
|
||||||
include "header.php";
|
include "header.php";
|
||||||
include "headerline.php";
|
include "headerline.php";
|
||||||
@@ -45,10 +46,12 @@ if($hasAccess){
|
|||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
$aktion = $_POST['aktion'] ?? 'speichern';
|
$aktion = $_POST['aktion'] ?? 'speichern';
|
||||||
|
|
||||||
|
$actorUserId = $saasUser !== null ? (int)$saasUser['user_id'] : null;
|
||||||
|
|
||||||
if ($aktion === 'loeschen') {
|
if ($aktion === 'loeschen') {
|
||||||
$id = (int)($_POST['id'] ?? 0);
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
if ($id > 0) {
|
if ($id > 0 && notices_soft_delete($pdo, $tenantId, $id)) {
|
||||||
notices_soft_delete($pdo, $tenantId, $id);
|
app_audit_log($pdo, $tenantId, $actorUserId, 'notice.deleted', 'notice', $id);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$nachricht = $_POST['nachricht'] ?? '';
|
$nachricht = $_POST['nachricht'] ?? '';
|
||||||
@@ -57,8 +60,8 @@ if($hasAccess){
|
|||||||
|
|
||||||
if ($dt) {
|
if ($dt) {
|
||||||
$gueltig_bis_sql = $dt->format('Y-m-d H:i:s'); // z.B. "2025-09-03 14:00:00"
|
$gueltig_bis_sql = $dt->format('Y-m-d H:i:s'); // z.B. "2025-09-03 14:00:00"
|
||||||
$createdByUserId = $saasUser !== null ? (int)$saasUser['user_id'] : null;
|
notices_create($pdo, $tenantId, (string)$nachricht, $gueltig_bis_sql, $actorUserId);
|
||||||
notices_create($pdo, $tenantId, (string)$nachricht, $gueltig_bis_sql, $createdByUserId);
|
app_audit_log($pdo, $tenantId, $actorUserId, 'notice.created', 'notice', null, ['valid_until' => $gueltig_bis_sql]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
include "functions.php";
|
include "functions.php";
|
||||||
require_once __DIR__ . "/app/ledger.php";
|
require_once __DIR__ . "/app/ledger.php";
|
||||||
require_once __DIR__ . "/app/saas-mail.php";
|
require_once __DIR__ . "/app/saas-mail.php";
|
||||||
|
require_once __DIR__ . "/app/audit.php";
|
||||||
app_require_csrf();
|
app_require_csrf();
|
||||||
include "header.php";
|
include "header.php";
|
||||||
include "headerline.php";
|
include "headerline.php";
|
||||||
@@ -130,6 +131,7 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
|
|||||||
|
|
||||||
if (!$dryRun) {
|
if (!$dryRun) {
|
||||||
$pdo->commit();
|
$pdo->commit();
|
||||||
|
app_audit_log($pdo, $tenantId, $createdByUserId, 'year_end_bonus.distributed', 'tenant', $tenantId, ['jahr' => $jahr, 'gesamtbetrag_cents' => $totalCents, 'empfaenger' => count($ergebnisse)]);
|
||||||
}
|
}
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
if ($pdo->inTransaction()) {
|
if ($pdo->inTransaction()) {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
<?php require_once __DIR__ . '/app/bootstrap.php'; ?>
|
||||||
<!DOCTYPE HTML>
|
<!DOCTYPE HTML>
|
||||||
<html lang="de">
|
<html lang="de">
|
||||||
<head>
|
<head>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
include "functions.php";
|
include "functions.php";
|
||||||
require_once __DIR__ . "/app/ledger.php";
|
require_once __DIR__ . "/app/ledger.php";
|
||||||
|
require_once __DIR__ . "/app/audit.php";
|
||||||
app_require_csrf();
|
app_require_csrf();
|
||||||
include "header.php";
|
include "header.php";
|
||||||
include "headerline.php";
|
include "headerline.php";
|
||||||
@@ -66,6 +67,7 @@ if ($_SERVER["REQUEST_METHOD"] == "POST" && isset($_POST["aktion"]) && $_POST["a
|
|||||||
$ergebnis = loescheEinzahlung($einzahlungID, $pdo, $tenantId);
|
$ergebnis = loescheEinzahlung($einzahlungID, $pdo, $tenantId);
|
||||||
|
|
||||||
if ($ergebnis === true) {
|
if ($ergebnis === true) {
|
||||||
|
app_audit_log($pdo, $tenantId, $saasUser['user_id'] ?? null, 'payment.voided', 'kl_Einzahlungen', $einzahlungID);
|
||||||
echo "Einzahlung erfolgreich gelöscht.";
|
echo "Einzahlung erfolgreich gelöscht.";
|
||||||
} else {
|
} else {
|
||||||
echo "Fehler: $ergebnis";
|
echo "Fehler: $ergebnis";
|
||||||
@@ -157,6 +159,7 @@ if ($_SERVER["REQUEST_METHOD"] == "POST" && isset($_POST["aktion"]) && $_POST["a
|
|||||||
$ergebnis = loescheStrichEintrag($strichID, $pdo, $tenantId);
|
$ergebnis = loescheStrichEintrag($strichID, $pdo, $tenantId);
|
||||||
|
|
||||||
if ($ergebnis === true) {
|
if ($ergebnis === true) {
|
||||||
|
app_audit_log($pdo, $tenantId, $saasUser['user_id'] ?? null, 'consumption.voided', 'kl_Kaffeeverbrauch', $strichID);
|
||||||
echo "Strich-Eintrag erfolgreich gelöscht.";
|
echo "Strich-Eintrag erfolgreich gelöscht.";
|
||||||
} else {
|
} else {
|
||||||
echo "Fehler: $ergebnis";
|
echo "Fehler: $ergebnis";
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
require_once __DIR__ . '/functions.php';
|
require_once __DIR__ . '/functions.php';
|
||||||
|
require_once __DIR__ . '/app/rate-limit.php';
|
||||||
|
|
||||||
$pdo = app_db_pdo();
|
$pdo = app_db_pdo();
|
||||||
$errors = [];
|
$errors = [];
|
||||||
@@ -10,26 +11,33 @@ $tenantSlug = trim((string)($_POST['tenant_slug'] ?? ''));
|
|||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
app_require_csrf();
|
app_require_csrf();
|
||||||
|
|
||||||
$result = saas_authenticate(
|
$emailBucketOk = app_rate_limit_check($pdo, 'login_email:' . saas_email_norm($email), 10, 900);
|
||||||
$pdo,
|
$ipBucketOk = app_rate_limit_check($pdo, 'login_ip:' . app_client_ip(), 20, 900);
|
||||||
$email,
|
|
||||||
(string)($_POST['password'] ?? ''),
|
|
||||||
$tenantSlug
|
|
||||||
);
|
|
||||||
|
|
||||||
if ($result['ok'] && !empty($result['needs_tenant_selection'])) {
|
if (!$emailBucketOk || !$ipBucketOk) {
|
||||||
saas_start_pending_tenant_selection((int)$result['user_id']);
|
$errors = ['Zu viele Anmeldeversuche. Bitte versuche es in einigen Minuten erneut.'];
|
||||||
header('Location: mandant-auswahl.php');
|
} else {
|
||||||
exit;
|
$result = saas_authenticate(
|
||||||
|
$pdo,
|
||||||
|
$email,
|
||||||
|
(string)($_POST['password'] ?? ''),
|
||||||
|
$tenantSlug
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($result['ok'] && !empty($result['needs_tenant_selection'])) {
|
||||||
|
saas_start_pending_tenant_selection((int)$result['user_id']);
|
||||||
|
header('Location: mandant-auswahl.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($result['ok']) {
|
||||||
|
saas_session_login($result['identity']);
|
||||||
|
header('Location: konto.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$errors = $result['errors'];
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($result['ok']) {
|
|
||||||
saas_session_login($result['identity']);
|
|
||||||
header('Location: konto.php');
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
$errors = $result['errors'];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
?>
|
?>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
include "functions.php";
|
include "functions.php";
|
||||||
require_once __DIR__ . "/app/ledger.php";
|
require_once __DIR__ . "/app/ledger.php";
|
||||||
require_once __DIR__ . "/app/saas-mail.php";
|
require_once __DIR__ . "/app/saas-mail.php";
|
||||||
|
require_once __DIR__ . "/app/audit.php";
|
||||||
app_require_csrf();
|
app_require_csrf();
|
||||||
include "header.php";
|
include "header.php";
|
||||||
include "headerline.php";
|
include "headerline.php";
|
||||||
@@ -85,6 +86,10 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
|
|||||||
$ergebnisse[] = ['name' => $person['display_name'], 'email' => $email, 'status' => 'failed'];
|
$ergebnisse[] = ['name' => $person['display_name'], 'email' => $email, 'status' => 'failed'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!$dryRun) {
|
||||||
|
app_audit_log($pdo, $tenantId, $createdByUserId, 'mail_broadcast.sent', 'tenant', $tenantId, ['empfaenger' => count($ergebnisse)]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$log = saas_fetch_outbound_email_log($pdo, $tenantId, 20);
|
$log = saas_fetch_outbound_email_log($pdo, $tenantId, 20);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
require_once __DIR__ . '/functions.php';
|
require_once __DIR__ . '/functions.php';
|
||||||
|
require_once __DIR__ . '/app/audit.php';
|
||||||
|
|
||||||
$pdo = app_db_pdo();
|
$pdo = app_db_pdo();
|
||||||
$user = saas_require_login();
|
$user = saas_require_login();
|
||||||
@@ -16,6 +17,7 @@ if ($canManageSettings) {
|
|||||||
if ($result['ok']) {
|
if ($result['ok']) {
|
||||||
$saved = true;
|
$saved = true;
|
||||||
$settings = $result['settings'];
|
$settings = $result['settings'];
|
||||||
|
app_audit_log($pdo, (int)$user['tenant_id'], (int)$user['user_id'], 'tenant_settings.updated', 'tenant', (int)$user['tenant_id']);
|
||||||
} else {
|
} else {
|
||||||
$errors = $result['errors'];
|
$errors = $result['errors'];
|
||||||
$settings = [
|
$settings = [
|
||||||
@@ -34,6 +36,8 @@ if ($canManageSettings) {
|
|||||||
} else {
|
} else {
|
||||||
$settings = saas_fetch_tenant_settings($pdo, (int)$user['tenant_id']);
|
$settings = saas_fetch_tenant_settings($pdo, (int)$user['tenant_id']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$auditLog = app_fetch_audit_log($pdo, (int)$user['tenant_id'], 50);
|
||||||
}
|
}
|
||||||
|
|
||||||
include 'header.php';
|
include 'header.php';
|
||||||
@@ -112,6 +116,23 @@ include 'nav.php';
|
|||||||
<li><a href="konto.php" class="button alt">Zurück</a></li>
|
<li><a href="konto.php" class="button alt">Zurück</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<h3>Protokoll</h3>
|
||||||
|
<p>Die letzten Admin-Aktionen für diesen Mandanten.</p>
|
||||||
|
<table>
|
||||||
|
<tr><th>Datum</th><th>Wer</th><th>Aktion</th><th>Betrifft</th></tr>
|
||||||
|
<?php if ($auditLog === []): ?>
|
||||||
|
<tr><td colspan="4">Noch keine protokollierten Aktionen.</td></tr>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php foreach ($auditLog as $eintrag): ?>
|
||||||
|
<tr>
|
||||||
|
<td><?php echo saas_html($eintrag['created_at']); ?></td>
|
||||||
|
<td><?php echo saas_html($eintrag['actor_name'] ?? '—'); ?></td>
|
||||||
|
<td><?php echo saas_html($eintrag['action']); ?></td>
|
||||||
|
<td><?php echo saas_html($eintrag['subject_type']); ?><?php echo $eintrag['subject_id'] !== null ? ' #' . (int)$eintrag['subject_id'] : ''; ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</table>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
include "functions.php";
|
include "functions.php";
|
||||||
require_once __DIR__ . "/app/ledger.php";
|
require_once __DIR__ . "/app/ledger.php";
|
||||||
require_once __DIR__ . "/app/saas-mail.php";
|
require_once __DIR__ . "/app/saas-mail.php";
|
||||||
|
require_once __DIR__ . "/app/audit.php";
|
||||||
app_require_csrf();
|
app_require_csrf();
|
||||||
include "header.php";
|
include "header.php";
|
||||||
include "headerline.php";
|
include "headerline.php";
|
||||||
@@ -43,6 +44,7 @@ if($hasAccess){
|
|||||||
$fehler = null;
|
$fehler = null;
|
||||||
$einladungslink = null;
|
$einladungslink = null;
|
||||||
$bearbeitenId = null;
|
$bearbeitenId = null;
|
||||||
|
$actorUserId = $saasUser['user_id'] ?? null;
|
||||||
|
|
||||||
if ($_SERVER["REQUEST_METHOD"] == "POST") {
|
if ($_SERVER["REQUEST_METHOD"] == "POST") {
|
||||||
$aktion = $_POST["aktion"] ?? '';
|
$aktion = $_POST["aktion"] ?? '';
|
||||||
@@ -59,7 +61,8 @@ if($hasAccess){
|
|||||||
$fehler = 'Bitte einen Namen und eine gültige E-Mail-Adresse angeben.';
|
$fehler = 'Bitte einen Namen und eine gültige E-Mail-Adresse angeben.';
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
ledger_create_participant($pdo, $tenantId, $name, $email, $paypalname, $aktiv);
|
$neueId = ledger_create_participant($pdo, $tenantId, $name, $email, $paypalname, $aktiv);
|
||||||
|
app_audit_log($pdo, $tenantId, $actorUserId, 'participant.created', 'participant', $neueId, ['name' => $name, 'email' => $email]);
|
||||||
$meldung = 'Mitglied wurde angelegt.';
|
$meldung = 'Mitglied wurde angelegt.';
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
$fehler = 'Das Mitglied konnte nicht angelegt werden (E-Mail eventuell schon vergeben).';
|
$fehler = 'Das Mitglied konnte nicht angelegt werden (E-Mail eventuell schon vergeben).';
|
||||||
@@ -75,6 +78,7 @@ if($hasAccess){
|
|||||||
if ($name === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
if ($name === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||||
$fehler = 'Bitte einen Namen und eine gültige E-Mail-Adresse angeben.';
|
$fehler = 'Bitte einen Namen und eine gültige E-Mail-Adresse angeben.';
|
||||||
} elseif (ledger_update_participant($pdo, $tenantId, $participantId, $name, $email, $paypalname, $aktiv)) {
|
} elseif (ledger_update_participant($pdo, $tenantId, $participantId, $name, $email, $paypalname, $aktiv)) {
|
||||||
|
app_audit_log($pdo, $tenantId, $actorUserId, 'participant.updated', 'participant', $participantId, ['name' => $name, 'email' => $email]);
|
||||||
$meldung = 'Mitglied wurde gespeichert.';
|
$meldung = 'Mitglied wurde gespeichert.';
|
||||||
} else {
|
} else {
|
||||||
$fehler = 'Das Mitglied konnte nicht gespeichert werden.';
|
$fehler = 'Das Mitglied konnte nicht gespeichert werden.';
|
||||||
@@ -82,6 +86,7 @@ if($hasAccess){
|
|||||||
} elseif ($aktion === 'aktivieren' || $aktion === 'deaktivieren') {
|
} elseif ($aktion === 'aktivieren' || $aktion === 'deaktivieren') {
|
||||||
$participantId = (int)$_POST["mitgliedID"];
|
$participantId = (int)$_POST["mitgliedID"];
|
||||||
if (ledger_set_participant_active($pdo, $tenantId, $participantId, $aktion === 'aktivieren')) {
|
if (ledger_set_participant_active($pdo, $tenantId, $participantId, $aktion === 'aktivieren')) {
|
||||||
|
app_audit_log($pdo, $tenantId, $actorUserId, $aktion === 'aktivieren' ? 'participant.activated' : 'participant.deactivated', 'participant', $participantId);
|
||||||
$meldung = $aktion === 'aktivieren' ? 'Mitglied wurde aktiviert.' : 'Mitglied wurde deaktiviert.';
|
$meldung = $aktion === 'aktivieren' ? 'Mitglied wurde aktiviert.' : 'Mitglied wurde deaktiviert.';
|
||||||
} else {
|
} else {
|
||||||
$fehler = 'Der Status konnte nicht geändert werden.';
|
$fehler = 'Der Status konnte nicht geändert werden.';
|
||||||
@@ -99,6 +104,7 @@ if($hasAccess){
|
|||||||
$rolle,
|
$rolle,
|
||||||
$ergebnis['token']
|
$ergebnis['token']
|
||||||
);
|
);
|
||||||
|
app_audit_log($pdo, $tenantId, $actorUserId, 'participant.access_granted', 'participant', $participantId, ['role' => $rolle]);
|
||||||
$meldung = 'Zugang wurde gewährt, Einladung wurde verschickt.';
|
$meldung = 'Zugang wurde gewährt, Einladung wurde verschickt.';
|
||||||
if (saas_should_show_auth_links()) {
|
if (saas_should_show_auth_links()) {
|
||||||
$einladungslink = saas_app_url('passwort-zuruecksetzen.php?token=' . urlencode($ergebnis['token']));
|
$einladungslink = saas_app_url('passwort-zuruecksetzen.php?token=' . urlencode($ergebnis['token']));
|
||||||
@@ -111,6 +117,7 @@ if($hasAccess){
|
|||||||
|
|
||||||
$ergebnis = saas_revoke_participant_access($pdo, $tenantId, $participantId);
|
$ergebnis = saas_revoke_participant_access($pdo, $tenantId, $participantId);
|
||||||
if ($ergebnis['ok']) {
|
if ($ergebnis['ok']) {
|
||||||
|
app_audit_log($pdo, $tenantId, $actorUserId, 'participant.access_revoked', 'participant', $participantId);
|
||||||
$meldung = 'Zugang wurde entzogen.';
|
$meldung = 'Zugang wurde entzogen.';
|
||||||
} else {
|
} else {
|
||||||
$fehler = implode(' ', $ergebnis['errors']);
|
$fehler = implode(' ', $ergebnis['errors']);
|
||||||
|
|||||||
+21
-10
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
require_once __DIR__ . '/functions.php';
|
require_once __DIR__ . '/functions.php';
|
||||||
require_once __DIR__ . '/app/saas-mail.php';
|
require_once __DIR__ . '/app/saas-mail.php';
|
||||||
|
require_once __DIR__ . '/app/rate-limit.php';
|
||||||
|
|
||||||
$pdo = app_db_pdo();
|
$pdo = app_db_pdo();
|
||||||
$email = trim((string)($_POST['email'] ?? ''));
|
$email = trim((string)($_POST['email'] ?? ''));
|
||||||
@@ -12,18 +13,28 @@ $resetLink = null;
|
|||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
app_require_csrf();
|
app_require_csrf();
|
||||||
$result = saas_request_password_reset($pdo, $email, $tenantSlug);
|
|
||||||
|
|
||||||
if ($result['ok']) {
|
$emailBucketOk = app_rate_limit_check($pdo, 'pwreset_email:' . saas_email_norm($email), 5, 3600);
|
||||||
$message = $result['message'];
|
$ipBucketOk = app_rate_limit_check($pdo, 'pwreset_ip:' . app_client_ip(), 10, 3600);
|
||||||
if (saas_should_show_auth_links() && !empty($result['token'])) {
|
|
||||||
$resetLink = 'passwort-zuruecksetzen.php?token=' . urlencode((string)$result['token']);
|
if (!$emailBucketOk || !$ipBucketOk) {
|
||||||
}
|
// Gleiche generische Meldung wie im Erfolgsfall, damit ein
|
||||||
if (!empty($result['token'])) {
|
// ausgereiztes Limit nicht verrät, ob das Konto existiert.
|
||||||
saas_send_password_reset_mail($email, (string)$result['token']);
|
$message = 'Wenn ein passendes Konto existiert, wurde ein Link vorbereitet.';
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
$errors = $result['errors'];
|
$result = saas_request_password_reset($pdo, $email, $tenantSlug);
|
||||||
|
|
||||||
|
if ($result['ok']) {
|
||||||
|
$message = $result['message'];
|
||||||
|
if (saas_should_show_auth_links() && !empty($result['token'])) {
|
||||||
|
$resetLink = 'passwort-zuruecksetzen.php?token=' . urlencode((string)$result['token']);
|
||||||
|
}
|
||||||
|
if (!empty($result['token'])) {
|
||||||
|
saas_send_password_reset_mail($email, (string)$result['token']);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$errors = $result['errors'];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+24
-19
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
require_once __DIR__ . '/functions.php';
|
require_once __DIR__ . '/functions.php';
|
||||||
require_once __DIR__ . '/app/saas-mail.php';
|
require_once __DIR__ . '/app/saas-mail.php';
|
||||||
|
require_once __DIR__ . '/app/rate-limit.php';
|
||||||
|
|
||||||
$pdo = app_db_pdo();
|
$pdo = app_db_pdo();
|
||||||
$errors = [];
|
$errors = [];
|
||||||
@@ -15,28 +16,32 @@ $values = [
|
|||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
app_require_csrf();
|
app_require_csrf();
|
||||||
|
|
||||||
$result = saas_register_tenant_owner($pdo, [
|
if (!app_rate_limit_check($pdo, 'register_ip:' . app_client_ip(), 5, 3600)) {
|
||||||
'tenant_name' => $values['tenant_name'],
|
$errors = ['Zu viele Registrierungsversuche. Bitte versuche es später erneut.'];
|
||||||
'tenant_slug' => $values['tenant_slug'],
|
} else {
|
||||||
'display_name' => $values['display_name'],
|
$result = saas_register_tenant_owner($pdo, [
|
||||||
'email' => $values['email'],
|
'tenant_name' => $values['tenant_name'],
|
||||||
'password' => (string)($_POST['password'] ?? ''),
|
'tenant_slug' => $values['tenant_slug'],
|
||||||
'password_confirm' => (string)($_POST['password_confirm'] ?? ''),
|
'display_name' => $values['display_name'],
|
||||||
]);
|
'email' => $values['email'],
|
||||||
|
'password' => (string)($_POST['password'] ?? ''),
|
||||||
|
'password_confirm' => (string)($_POST['password_confirm'] ?? ''),
|
||||||
|
]);
|
||||||
|
|
||||||
if ($result['ok']) {
|
if ($result['ok']) {
|
||||||
if (!empty($result['email_verification_token'])) {
|
if (!empty($result['email_verification_token'])) {
|
||||||
saas_send_email_verification_mail($values['email'], (string)$result['email_verification_token']);
|
saas_send_email_verification_mail($values['email'], (string)$result['email_verification_token']);
|
||||||
|
}
|
||||||
|
saas_session_login($result['identity']);
|
||||||
|
if (saas_should_show_auth_links() && !empty($result['email_verification_token'])) {
|
||||||
|
$_SESSION['saas_dev_email_verification_token'] = (string)$result['email_verification_token'];
|
||||||
|
}
|
||||||
|
header('Location: konto.php?registered=1');
|
||||||
|
exit;
|
||||||
}
|
}
|
||||||
saas_session_login($result['identity']);
|
|
||||||
if (saas_should_show_auth_links() && !empty($result['email_verification_token'])) {
|
$errors = $result['errors'];
|
||||||
$_SESSION['saas_dev_email_verification_token'] = (string)$result['email_verification_token'];
|
|
||||||
}
|
|
||||||
header('Location: konto.php?registered=1');
|
|
||||||
exit;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$errors = $result['errors'];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
?>
|
?>
|
||||||
|
|||||||
Reference in New Issue
Block a user