M3 Registrierung und Login-Grundlage ergänzen
- SaaS-Auth-Helper und PDO-Datenbankzugriff einführen - Registrierung, Login, Logout und Kontoansicht ergänzen - Auth-Migration, Flow-Check und Smoke-Abdeckung aktualisieren
This commit is contained in:
@@ -0,0 +1,340 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/bootstrap.php';
|
||||
require_once __DIR__ . '/database.php';
|
||||
|
||||
function saas_email_norm(string $email): string
|
||||
{
|
||||
return strtolower(trim($email));
|
||||
}
|
||||
|
||||
function saas_slugify(string $value): string
|
||||
{
|
||||
$slug = strtolower(trim($value));
|
||||
$slug = preg_replace('/[^a-z0-9]+/', '-', $slug) ?? '';
|
||||
$slug = trim($slug, '-');
|
||||
|
||||
return $slug;
|
||||
}
|
||||
|
||||
function saas_html(mixed $value): string
|
||||
{
|
||||
return htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
function saas_current_user(?PDO $pdo = null): ?array
|
||||
{
|
||||
static $cached = false;
|
||||
static $user = null;
|
||||
|
||||
app_start_session();
|
||||
|
||||
if ($cached) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
$cached = true;
|
||||
|
||||
$userId = isset($_SESSION['saas_user_id']) ? (int)$_SESSION['saas_user_id'] : 0;
|
||||
$tenantId = isset($_SESSION['saas_tenant_id']) ? (int)$_SESSION['saas_tenant_id'] : 0;
|
||||
if ($userId <= 0 || $tenantId <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$pdo = $pdo ?? app_db_pdo();
|
||||
$stmt = $pdo->prepare(
|
||||
'SELECT
|
||||
u.id AS user_id,
|
||||
u.email,
|
||||
u.email_norm,
|
||||
u.display_name,
|
||||
u.status AS user_status,
|
||||
t.id AS tenant_id,
|
||||
t.slug AS tenant_slug,
|
||||
t.name AS tenant_name,
|
||||
t.status AS tenant_status,
|
||||
tm.role,
|
||||
tm.status AS membership_status
|
||||
FROM users u
|
||||
JOIN tenant_memberships tm ON tm.user_id = u.id
|
||||
JOIN tenants t ON t.id = tm.tenant_id
|
||||
WHERE u.id = ?
|
||||
AND t.id = ?
|
||||
AND u.status = ?
|
||||
AND tm.status = ?
|
||||
AND t.status = ?
|
||||
LIMIT 1'
|
||||
);
|
||||
$stmt->execute([$userId, $tenantId, 'active', 'active', 'active']);
|
||||
$row = $stmt->fetch();
|
||||
} catch (Throwable $e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($row === false) {
|
||||
unset($_SESSION['saas_user_id'], $_SESSION['saas_tenant_id'], $_SESSION['saas_role']);
|
||||
return null;
|
||||
}
|
||||
|
||||
$user = $row;
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
function saas_session_login(array $identity): void
|
||||
{
|
||||
app_start_session();
|
||||
session_regenerate_id(true);
|
||||
|
||||
$_SESSION['saas_user_id'] = (int)$identity['user_id'];
|
||||
$_SESSION['saas_tenant_id'] = (int)$identity['tenant_id'];
|
||||
$_SESSION['saas_role'] = (string)$identity['role'];
|
||||
}
|
||||
|
||||
function saas_logout(): void
|
||||
{
|
||||
app_start_session();
|
||||
unset($_SESSION['saas_user_id'], $_SESSION['saas_tenant_id'], $_SESSION['saas_role']);
|
||||
}
|
||||
|
||||
function saas_require_login(): array
|
||||
{
|
||||
$user = saas_current_user();
|
||||
if ($user !== null) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
header('Location: login.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
function saas_authenticate(PDO $pdo, string $email, string $password, string $tenantSlug = ''): array
|
||||
{
|
||||
$emailNorm = saas_email_norm($email);
|
||||
if (!filter_var($emailNorm, FILTER_VALIDATE_EMAIL) || $password === '') {
|
||||
return [
|
||||
'ok' => false,
|
||||
'errors' => ['E-Mail oder Passwort ist ungueltig.'],
|
||||
];
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare(
|
||||
'SELECT id, email, email_norm, display_name, password_hash, status
|
||||
FROM users
|
||||
WHERE email_norm = ?
|
||||
LIMIT 1'
|
||||
);
|
||||
$stmt->execute([$emailNorm]);
|
||||
$user = $stmt->fetch();
|
||||
|
||||
if ($user === false
|
||||
|| (string)$user['status'] !== 'active'
|
||||
|| empty($user['password_hash'])
|
||||
|| !password_verify($password, (string)$user['password_hash'])
|
||||
) {
|
||||
return [
|
||||
'ok' => false,
|
||||
'errors' => ['E-Mail oder Passwort ist ungueltig.'],
|
||||
];
|
||||
}
|
||||
|
||||
$params = [(int)$user['id'], 'active', 'active'];
|
||||
$tenantFilter = '';
|
||||
if (trim($tenantSlug) !== '') {
|
||||
$tenantFilter = ' AND t.slug = ?';
|
||||
$params[] = saas_slugify($tenantSlug);
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare(
|
||||
'SELECT
|
||||
t.id AS tenant_id,
|
||||
t.slug AS tenant_slug,
|
||||
t.name AS tenant_name,
|
||||
tm.role
|
||||
FROM tenant_memberships tm
|
||||
JOIN tenants t ON t.id = tm.tenant_id
|
||||
WHERE tm.user_id = ?
|
||||
AND tm.status = ?
|
||||
AND t.status = ?' . $tenantFilter . '
|
||||
ORDER BY
|
||||
CASE tm.role
|
||||
WHEN \'owner\' THEN 1
|
||||
WHEN \'admin\' THEN 2
|
||||
ELSE 3
|
||||
END,
|
||||
t.name
|
||||
LIMIT 1'
|
||||
);
|
||||
$stmt->execute($params);
|
||||
$membership = $stmt->fetch();
|
||||
|
||||
if ($membership === false) {
|
||||
return [
|
||||
'ok' => false,
|
||||
'errors' => ['Fuer dieses Konto ist kein aktiver Mandant verfuegbar.'],
|
||||
];
|
||||
}
|
||||
|
||||
$pdo->prepare('UPDATE users SET last_login_at = NOW() WHERE id = ?')->execute([(int)$user['id']]);
|
||||
|
||||
return [
|
||||
'ok' => true,
|
||||
'identity' => [
|
||||
'user_id' => (int)$user['id'],
|
||||
'email' => (string)$user['email'],
|
||||
'email_norm' => (string)$user['email_norm'],
|
||||
'display_name' => (string)$user['display_name'],
|
||||
'tenant_id' => (int)$membership['tenant_id'],
|
||||
'tenant_slug' => (string)$membership['tenant_slug'],
|
||||
'tenant_name' => (string)$membership['tenant_name'],
|
||||
'role' => (string)$membership['role'],
|
||||
],
|
||||
'errors' => [],
|
||||
];
|
||||
}
|
||||
|
||||
function saas_register_tenant_owner(PDO $pdo, array $input): array
|
||||
{
|
||||
$tenantName = trim((string)($input['tenant_name'] ?? ''));
|
||||
$tenantSlug = saas_slugify((string)($input['tenant_slug'] ?? ''));
|
||||
if ($tenantSlug === '') {
|
||||
$tenantSlug = saas_slugify($tenantName);
|
||||
}
|
||||
|
||||
$displayName = trim((string)($input['display_name'] ?? ''));
|
||||
$email = trim((string)($input['email'] ?? ''));
|
||||
$emailNorm = saas_email_norm($email);
|
||||
$password = (string)($input['password'] ?? '');
|
||||
$passwordConfirm = (string)($input['password_confirm'] ?? '');
|
||||
|
||||
$errors = [];
|
||||
if (strlen($tenantName) < 3 || strlen($tenantName) > 255) {
|
||||
$errors[] = 'Der Kundenname muss zwischen 3 und 255 Zeichen lang sein.';
|
||||
}
|
||||
if (!preg_match('/^[a-z0-9][a-z0-9-]{1,98}[a-z0-9]$/', $tenantSlug)) {
|
||||
$errors[] = 'Das Kundenkuerzel darf nur Kleinbuchstaben, Zahlen und Bindestriche enthalten.';
|
||||
}
|
||||
if (strlen($displayName) < 2 || strlen($displayName) > 255) {
|
||||
$errors[] = 'Der Name muss zwischen 2 und 255 Zeichen lang sein.';
|
||||
}
|
||||
if (!filter_var($emailNorm, FILTER_VALIDATE_EMAIL)) {
|
||||
$errors[] = 'Bitte eine gueltige E-Mail-Adresse eingeben.';
|
||||
}
|
||||
if (strlen($password) < 8) {
|
||||
$errors[] = 'Das Passwort muss mindestens 8 Zeichen lang sein.';
|
||||
}
|
||||
if ($password !== $passwordConfirm) {
|
||||
$errors[] = 'Die Passwort-Wiederholung stimmt nicht.';
|
||||
}
|
||||
|
||||
if ($errors !== []) {
|
||||
return ['ok' => false, 'errors' => $errors];
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare('SELECT id, password_hash, status FROM users WHERE email_norm = ? LIMIT 1');
|
||||
$stmt->execute([$emailNorm]);
|
||||
$existingUser = $stmt->fetch();
|
||||
if ($existingUser !== false && (string)$existingUser['status'] !== 'active') {
|
||||
return [
|
||||
'ok' => false,
|
||||
'errors' => ['Dieses Benutzerkonto ist nicht aktiv.'],
|
||||
];
|
||||
}
|
||||
if ($existingUser !== false
|
||||
&& !empty($existingUser['password_hash'])
|
||||
&& !password_verify($password, (string)$existingUser['password_hash'])
|
||||
) {
|
||||
return [
|
||||
'ok' => false,
|
||||
'errors' => ['Diese E-Mail ist bereits vergeben. Bitte mit dem bestehenden Passwort anmelden.'],
|
||||
];
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare('SELECT COUNT(*) FROM tenants WHERE slug = ?');
|
||||
$stmt->execute([$tenantSlug]);
|
||||
if ((int)$stmt->fetchColumn() > 0) {
|
||||
return [
|
||||
'ok' => false,
|
||||
'errors' => ['Dieses Kundenkuerzel ist bereits vergeben.'],
|
||||
];
|
||||
}
|
||||
|
||||
$passwordHash = password_hash($password, PASSWORD_DEFAULT);
|
||||
|
||||
try {
|
||||
$pdo->beginTransaction();
|
||||
|
||||
$stmt = $pdo->prepare(
|
||||
'INSERT INTO tenants (slug, name, status, timezone, locale, currency_code)
|
||||
VALUES (?, ?, ?, ?, ?, ?)'
|
||||
);
|
||||
$stmt->execute([$tenantSlug, $tenantName, 'active', 'Europe/Berlin', 'de-DE', 'EUR']);
|
||||
$tenantId = (int)$pdo->lastInsertId();
|
||||
|
||||
$stmt = $pdo->prepare(
|
||||
'INSERT INTO tenant_settings
|
||||
(tenant_id, mark_price_cents, self_entry_enabled, paypal_enabled, paypal_url_template)
|
||||
VALUES (?, ?, ?, ?, ?)'
|
||||
);
|
||||
$stmt->execute([$tenantId, 20, 1, 0, '']);
|
||||
|
||||
if ($existingUser === false) {
|
||||
$stmt = $pdo->prepare(
|
||||
'INSERT INTO users (email, email_norm, display_name, password_hash, status)
|
||||
VALUES (?, ?, ?, ?, ?)'
|
||||
);
|
||||
$stmt->execute([$email, $emailNorm, $displayName, $passwordHash, 'active']);
|
||||
$userId = (int)$pdo->lastInsertId();
|
||||
} else {
|
||||
$userId = (int)$existingUser['id'];
|
||||
$stmt = $pdo->prepare(
|
||||
'UPDATE users
|
||||
SET email = ?, display_name = ?, password_hash = ?, status = ?
|
||||
WHERE id = ?'
|
||||
);
|
||||
$stmt->execute([$email, $displayName, $passwordHash, 'active', $userId]);
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare(
|
||||
'INSERT INTO tenant_memberships (tenant_id, user_id, role, status, joined_at)
|
||||
VALUES (?, ?, ?, ?, NOW())'
|
||||
);
|
||||
$stmt->execute([$tenantId, $userId, 'owner', 'active']);
|
||||
|
||||
$stmt = $pdo->prepare(
|
||||
'INSERT INTO participants
|
||||
(tenant_id, user_id, display_name, email, email_norm, active)
|
||||
VALUES (?, ?, ?, ?, ?, ?)'
|
||||
);
|
||||
$stmt->execute([$tenantId, $userId, $displayName, $email, $emailNorm, 1]);
|
||||
|
||||
$pdo->commit();
|
||||
} catch (Throwable $e) {
|
||||
if ($pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
|
||||
return [
|
||||
'ok' => false,
|
||||
'errors' => ['Die Registrierung konnte nicht gespeichert werden.'],
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'ok' => true,
|
||||
'identity' => [
|
||||
'user_id' => $userId,
|
||||
'email' => $email,
|
||||
'email_norm' => $emailNorm,
|
||||
'display_name' => $displayName,
|
||||
'tenant_id' => $tenantId,
|
||||
'tenant_slug' => $tenantSlug,
|
||||
'tenant_name' => $tenantName,
|
||||
'role' => 'owner',
|
||||
],
|
||||
'errors' => [],
|
||||
];
|
||||
}
|
||||
Reference in New Issue
Block a user