M3 Mandant-Einstellungen und Rollencheck ergänzen

- zentrale Owner/Admin-Rollenpruefung in SaaS-Auth ergaenzen
- Mandant-Einstellungen fuer Preise, PayPal und Basisdaten bauen
- Settings-Flow-Test, Navigation und M3-Dokumentation aktualisieren
This commit is contained in:
2026-07-12 22:32:51 +02:00
parent 5baf6542ce
commit 19ff11c21a
9 changed files with 492 additions and 8 deletions
+210
View File
@@ -24,6 +24,40 @@ function saas_html(mixed $value): string
return htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8');
}
function saas_parse_money_cents(mixed $value): ?int
{
$normalized = trim((string)$value);
$normalized = str_replace(["\xc2\xa0", ' '], '', $normalized);
if ($normalized === '') {
return null;
}
if (str_contains($normalized, ',') && str_contains($normalized, '.')) {
$normalized = str_replace('.', '', $normalized);
}
$normalized = str_replace(',', '.', $normalized);
if (!preg_match('/^-?[0-9]+(?:\.[0-9]{1,2})?$/', $normalized)) {
return null;
}
$negative = str_starts_with($normalized, '-');
if ($negative) {
$normalized = substr($normalized, 1);
}
[$euros, $cents] = array_pad(explode('.', $normalized, 2), 2, '0');
$result = ((int)$euros * 100) + (int)str_pad(substr($cents, 0, 2), 2, '0');
return $negative ? -$result : $result;
}
function saas_format_money_cents(mixed $cents): string
{
return number_format(((int)$cents) / 100, 2, ',', '');
}
function saas_current_user(?PDO $pdo = null): ?array
{
static $cached = false;
@@ -111,6 +145,182 @@ function saas_require_login(): array
exit;
}
function saas_user_has_role(array|string $roles, ?array $user = null): bool
{
$user = $user ?? saas_current_user();
if ($user === null) {
return false;
}
$allowed = is_array($roles) ? $roles : [$roles];
return in_array((string)($user['role'] ?? ''), $allowed, true);
}
function saas_require_role(array|string $roles): array
{
$user = saas_require_login();
if (saas_user_has_role($roles, $user)) {
return $user;
}
http_response_code(403);
die('Keine Berechtigung.');
}
function saas_can_manage_tenant_settings(?array $user = null): bool
{
return saas_user_has_role(['owner', 'admin'], $user);
}
function saas_fetch_tenant_settings(PDO $pdo, int $tenantId): ?array
{
$stmt = $pdo->prepare(
'SELECT
t.id AS tenant_id,
t.slug,
t.name,
t.status,
t.timezone,
t.locale,
t.currency_code,
ts.mark_price_cents,
ts.self_entry_enabled,
ts.paypal_enabled,
ts.paypal_url_template,
ts.sheet_window_days,
ts.negative_warning_cents
FROM tenants t
LEFT JOIN tenant_settings ts ON ts.tenant_id = t.id
WHERE t.id = ?
LIMIT 1'
);
$stmt->execute([$tenantId]);
$settings = $stmt->fetch();
if ($settings === false) {
return null;
}
if ($settings['mark_price_cents'] === null) {
$pdo->prepare('INSERT INTO tenant_settings (tenant_id) VALUES (?)')->execute([$tenantId]);
return saas_fetch_tenant_settings($pdo, $tenantId);
}
return [
'tenant_id' => (int)$settings['tenant_id'],
'slug' => (string)$settings['slug'],
'name' => (string)$settings['name'],
'status' => (string)$settings['status'],
'timezone' => (string)$settings['timezone'],
'locale' => (string)$settings['locale'],
'currency_code' => (string)$settings['currency_code'],
'mark_price_cents' => (int)$settings['mark_price_cents'],
'self_entry_enabled' => (int)$settings['self_entry_enabled'],
'paypal_enabled' => (int)$settings['paypal_enabled'],
'paypal_url_template' => (string)$settings['paypal_url_template'],
'sheet_window_days' => (int)$settings['sheet_window_days'],
'negative_warning_cents' => (int)$settings['negative_warning_cents'],
];
}
function saas_update_tenant_settings(PDO $pdo, int $tenantId, array $input): array
{
$tenantName = trim((string)($input['tenant_name'] ?? ''));
$timezone = trim((string)($input['timezone'] ?? 'Europe/Berlin'));
$locale = trim((string)($input['locale'] ?? 'de-DE'));
$currencyCode = strtoupper(trim((string)($input['currency_code'] ?? 'EUR')));
$markPriceCents = saas_parse_money_cents($input['mark_price'] ?? '');
$sheetWindowDays = filter_var($input['sheet_window_days'] ?? null, FILTER_VALIDATE_INT);
$negativeWarningCents = saas_parse_money_cents($input['negative_warning'] ?? '');
$paypalUrlTemplate = trim((string)($input['paypal_url_template'] ?? ''));
$errors = [];
if (strlen($tenantName) < 3 || strlen($tenantName) > 255) {
$errors[] = 'Der Kundenname muss zwischen 3 und 255 Zeichen lang sein.';
}
if (!in_array($timezone, DateTimeZone::listIdentifiers(), true)) {
$errors[] = 'Die Zeitzone ist ungueltig.';
}
if (!preg_match('/^[a-z]{2}-[A-Z]{2}$/', $locale)) {
$errors[] = 'Die Locale muss dem Muster de-DE entsprechen.';
}
if (!preg_match('/^[A-Z]{3}$/', $currencyCode)) {
$errors[] = 'Der Waehrungscode muss aus drei Grossbuchstaben bestehen.';
}
if ($markPriceCents === null || $markPriceCents < 1 || $markPriceCents > 10000) {
$errors[] = 'Der Preis pro Strich muss zwischen 0,01 und 100,00 liegen.';
}
if ($sheetWindowDays === false || $sheetWindowDays < 1 || $sheetWindowDays > 365) {
$errors[] = 'Das Listenfenster muss zwischen 1 und 365 Tagen liegen.';
}
if ($negativeWarningCents === null || $negativeWarningCents < 0 || $negativeWarningCents > 100000) {
$errors[] = 'Die Schulden-Warnschwelle muss zwischen 0,00 und 1000,00 liegen.';
}
if (strlen($paypalUrlTemplate) > 1000) {
$errors[] = 'Der PayPal-Link ist zu lang.';
}
if ($errors !== []) {
return ['ok' => false, 'errors' => $errors];
}
$selfEntryEnabled = !empty($input['self_entry_enabled']) ? 1 : 0;
$paypalEnabled = !empty($input['paypal_enabled']) ? 1 : 0;
try {
$pdo->beginTransaction();
$stmt = $pdo->prepare(
'UPDATE tenants
SET name = ?, timezone = ?, locale = ?, currency_code = ?
WHERE id = ?'
);
$stmt->execute([$tenantName, $timezone, $locale, $currencyCode, $tenantId]);
$stmt = $pdo->prepare(
'INSERT INTO tenant_settings
(tenant_id, mark_price_cents, self_entry_enabled, paypal_enabled,
paypal_url_template, sheet_window_days, negative_warning_cents)
VALUES (?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
mark_price_cents = VALUES(mark_price_cents),
self_entry_enabled = VALUES(self_entry_enabled),
paypal_enabled = VALUES(paypal_enabled),
paypal_url_template = VALUES(paypal_url_template),
sheet_window_days = VALUES(sheet_window_days),
negative_warning_cents = VALUES(negative_warning_cents)'
);
$stmt->execute([
$tenantId,
$markPriceCents,
$selfEntryEnabled,
$paypalEnabled,
$paypalUrlTemplate,
(int)$sheetWindowDays,
-abs($negativeWarningCents),
]);
$pdo->commit();
} catch (Throwable $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
return [
'ok' => false,
'errors' => ['Die Einstellungen konnten nicht gespeichert werden.'],
];
}
return [
'ok' => true,
'errors' => [],
'settings' => saas_fetch_tenant_settings($pdo, $tenantId),
];
}
function saas_authenticate(PDO $pdo, string $email, string $password, string $tenantSlug = ''): array
{
$emailNorm = saas_email_norm($email);