M5: Hinweise auf tenant-scoped Notices umziehen, Mitgliederzugang auf Rollen umstellen
Hinweise: - Neue Tabelle notices (tenant-scoped, Soft-Delete via deleted_at) loest die global unscoped kl_hinweise als aktive Datenquelle ab; Migration uebernimmt einmalig aktuell gueltige kl_hinweise-Eintraege fuer den Default-Mandanten. kl_hinweise bleibt als Golden-Master-Referenz stehen. - hinweise.php und der Banner in header.php sind tenant-scoped umgestellt. Mitgliederverwaltung: - mitarbeiterverwalten.php verwaltet jetzt participants (tenant-scoped) statt der global unscoped kl_Mitarbeiter-Tabelle als primaere Quelle. Das behebt nebenbei ein Mandanten-Datenleck: jeder SaaS-Mandant mit Owner/Admin-Rolle haette zuvor die komplette Default-Mandanten- Mitgliederliste sehen und bearbeiten koennen. - Fuer den Default-Mandanten bleibt Dual-Write nach kl_Mitarbeiter bestehen, damit stricheintragen.php/einzahlung.php weiter funktionieren; andere Mandanten werden rein participant-nativ verwaltet. - Die Legacy-Administrator-Checkbox ist raus. Stattdessen kann ein Admin je Mitglied unabhaengig von Name/E-Mail einen Login-Zugang mit Rolle (member/treasurer/admin) gewaehren oder entziehen (saas_grant_participant_access / saas_revoke_participant_access). Einladung laeuft ueber den bestehenden Passwort-Reset-Mechanismus, Entzug setzt die Mitgliedschaft auf revoked statt sie zu loeschen. - Kompletter Flow live getestet: anlegen, Zugang gewaehren, Einladungsmail, Passwort setzen, Login, Rollenschutz, Zugang entziehen, Login-Sperre. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -848,6 +848,132 @@ function saas_authenticate(PDO $pdo, string $email, string $password, string $te
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Roles an admin can hand out through the member management UI. "owner" is
|
||||
* deliberately excluded here; it is only ever set during registration.
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
function saas_grantable_roles(): array
|
||||
{
|
||||
return ['member', 'treasurer', 'admin'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Grants (or updates) tenant access for an existing participant: creates the
|
||||
* login account if needed, links it to the participant, and (re)activates
|
||||
* the tenant membership with the given role. The participant's name/email
|
||||
* stay the notification identity regardless of login state.
|
||||
*
|
||||
* @return array{ok: bool, errors: list<string>, token?: string, user_email?: string}
|
||||
*/
|
||||
function saas_grant_participant_access(PDO $pdo, int $tenantId, int $participantId, string $role): array
|
||||
{
|
||||
if (!in_array($role, saas_grantable_roles(), true)) {
|
||||
return ['ok' => false, 'errors' => ['Ungültige Rolle.']];
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare(
|
||||
'SELECT id, display_name, email, email_norm
|
||||
FROM participants
|
||||
WHERE id = ? AND tenant_id = ?'
|
||||
);
|
||||
$stmt->execute([$participantId, $tenantId]);
|
||||
$participant = $stmt->fetch();
|
||||
if ($participant === false) {
|
||||
return ['ok' => false, 'errors' => ['Teilnehmer wurde nicht gefunden.']];
|
||||
}
|
||||
|
||||
$emailNorm = (string)($participant['email_norm'] ?? '');
|
||||
if ($emailNorm === '' || !filter_var($participant['email'], FILTER_VALIDATE_EMAIL)) {
|
||||
return ['ok' => false, 'errors' => ['Für den Zugang wird eine gültige E-Mail-Adresse benötigt.']];
|
||||
}
|
||||
|
||||
try {
|
||||
$pdo->beginTransaction();
|
||||
|
||||
$stmt = $pdo->prepare('SELECT id, status FROM users WHERE email_norm = ? LIMIT 1');
|
||||
$stmt->execute([$emailNorm]);
|
||||
$user = $stmt->fetch();
|
||||
|
||||
if ($user === false) {
|
||||
$stmt = $pdo->prepare(
|
||||
'INSERT INTO users (email, email_norm, display_name, status) VALUES (?, ?, ?, ?)'
|
||||
);
|
||||
$stmt->execute([$participant['email'], $emailNorm, $participant['display_name'], 'active']);
|
||||
$userId = (int)$pdo->lastInsertId();
|
||||
} elseif ((string)$user['status'] !== 'active') {
|
||||
$pdo->rollBack();
|
||||
|
||||
return ['ok' => false, 'errors' => ['Dieses Benutzerkonto ist nicht aktiv.']];
|
||||
} else {
|
||||
$userId = (int)$user['id'];
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare(
|
||||
'INSERT INTO tenant_memberships (tenant_id, user_id, role, status, invited_at, joined_at)
|
||||
VALUES (?, ?, ?, ?, NOW(), NOW())
|
||||
ON DUPLICATE KEY UPDATE
|
||||
role = VALUES(role),
|
||||
status = VALUES(status),
|
||||
invited_at = COALESCE(tenant_memberships.invited_at, VALUES(invited_at)),
|
||||
joined_at = COALESCE(tenant_memberships.joined_at, VALUES(joined_at))'
|
||||
);
|
||||
$stmt->execute([$tenantId, $userId, $role, 'active']);
|
||||
|
||||
$pdo->prepare('UPDATE participants SET user_id = ? WHERE id = ? AND tenant_id = ?')
|
||||
->execute([$userId, $participantId, $tenantId]);
|
||||
|
||||
$pdo->commit();
|
||||
} catch (Throwable $e) {
|
||||
if ($pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
|
||||
return ['ok' => false, 'errors' => ['Der Zugang konnte nicht angelegt werden.']];
|
||||
}
|
||||
|
||||
$token = saas_create_auth_token($pdo, $userId, 'password_reset', $tenantId, 1440);
|
||||
|
||||
return [
|
||||
'ok' => true,
|
||||
'errors' => [],
|
||||
'token' => $token['token'],
|
||||
'user_email' => (string)$participant['email'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Revokes tenant access without touching the participant's name/email or
|
||||
* deleting the membership row, so the grant can be reinstated later and the
|
||||
* history stays auditable. Owners cannot be revoked here.
|
||||
*
|
||||
* @return array{ok: bool, errors: list<string>}
|
||||
*/
|
||||
function saas_revoke_participant_access(PDO $pdo, int $tenantId, int $participantId): array
|
||||
{
|
||||
$stmt = $pdo->prepare('SELECT user_id FROM participants WHERE id = ? AND tenant_id = ?');
|
||||
$stmt->execute([$participantId, $tenantId]);
|
||||
$participant = $stmt->fetch();
|
||||
if ($participant === false || $participant['user_id'] === null) {
|
||||
return ['ok' => true, 'errors' => []];
|
||||
}
|
||||
|
||||
$userId = (int)$participant['user_id'];
|
||||
|
||||
$stmt = $pdo->prepare('SELECT role FROM tenant_memberships WHERE tenant_id = ? AND user_id = ?');
|
||||
$stmt->execute([$tenantId, $userId]);
|
||||
$membership = $stmt->fetch();
|
||||
if ($membership !== false && (string)$membership['role'] === 'owner') {
|
||||
return ['ok' => false, 'errors' => ['Dem Inhaber kann der Zugang nicht entzogen werden.']];
|
||||
}
|
||||
|
||||
$pdo->prepare("UPDATE tenant_memberships SET status = 'revoked' WHERE tenant_id = ? AND user_id = ?")
|
||||
->execute([$tenantId, $userId]);
|
||||
|
||||
return ['ok' => true, 'errors' => []];
|
||||
}
|
||||
|
||||
function saas_register_tenant_owner(PDO $pdo, array $input): array
|
||||
{
|
||||
$tenantName = trim((string)($input['tenant_name'] ?? ''));
|
||||
|
||||
Reference in New Issue
Block a user