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,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/bootstrap.php';
|
||||||
|
|
||||||
|
function app_db_pdo(): PDO
|
||||||
|
{
|
||||||
|
static $pdo = null;
|
||||||
|
|
||||||
|
if ($pdo instanceof PDO) {
|
||||||
|
return $pdo;
|
||||||
|
}
|
||||||
|
|
||||||
|
$host = app_env('DB_HOST', '');
|
||||||
|
$database = app_env('DB_NAME', '');
|
||||||
|
$user = app_env('DB_USER', '');
|
||||||
|
$password = app_env('DB_PASS', '');
|
||||||
|
$port = app_env('DB_PORT', '3306');
|
||||||
|
|
||||||
|
if ($host === '' || $database === '' || $user === '') {
|
||||||
|
throw new RuntimeException('Database environment is incomplete.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$dsn = sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $host, $port, $database);
|
||||||
|
$pdo = new PDO($dsn, $user, $password ?? '', [
|
||||||
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||||
|
PDO::ATTR_EMULATE_PREPARES => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $pdo;
|
||||||
|
}
|
||||||
@@ -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' => [],
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -34,9 +34,6 @@ CREATE TABLE IF NOT EXISTS users (
|
|||||||
display_name VARCHAR(255) NOT NULL,
|
display_name VARCHAR(255) NOT NULL,
|
||||||
password_hash VARCHAR(255) NULL,
|
password_hash VARCHAR(255) NULL,
|
||||||
status VARCHAR(30) NOT NULL DEFAULT 'active',
|
status VARCHAR(30) NOT NULL DEFAULT 'active',
|
||||||
email_verified_at DATETIME NULL,
|
|
||||||
mfa_enabled TINYINT(1) NOT NULL DEFAULT 0,
|
|
||||||
disabled_at DATETIME NULL,
|
|
||||||
last_login_at DATETIME NULL,
|
last_login_at DATETIME NULL,
|
||||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
@@ -79,9 +76,7 @@ CREATE TABLE IF NOT EXISTS participants (
|
|||||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
UNIQUE KEY uq_participants_tenant_legacy (tenant_id, legacy_mitarbeiter_id),
|
UNIQUE KEY uq_participants_tenant_legacy (tenant_id, legacy_mitarbeiter_id),
|
||||||
UNIQUE KEY uq_participants_tenant_email_norm (tenant_id, email_norm),
|
UNIQUE KEY uq_participants_tenant_email_norm (tenant_id, email_norm),
|
||||||
UNIQUE KEY uq_participants_tenant_user (tenant_id, user_id),
|
|
||||||
KEY idx_participants_tenant_active_name (tenant_id, active, display_name),
|
KEY idx_participants_tenant_active_name (tenant_id, active, display_name),
|
||||||
KEY idx_participants_tenant_paypal_name (tenant_id, paypal_name),
|
|
||||||
KEY idx_participants_user (user_id),
|
KEY idx_participants_user (user_id),
|
||||||
CONSTRAINT fk_participants_tenant
|
CONSTRAINT fk_participants_tenant
|
||||||
FOREIGN KEY (tenant_id) REFERENCES tenants(id)
|
FOREIGN KEY (tenant_id) REFERENCES tenants(id)
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
SET @schema_name = DATABASE();
|
||||||
|
|
||||||
|
SET @sql = (
|
||||||
|
SELECT IF(
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = @schema_name
|
||||||
|
AND TABLE_NAME = 'users'
|
||||||
|
AND COLUMN_NAME = 'email_verified_at'
|
||||||
|
),
|
||||||
|
'SELECT 1',
|
||||||
|
'ALTER TABLE users ADD COLUMN email_verified_at DATETIME NULL AFTER status'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @sql = (
|
||||||
|
SELECT IF(
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = @schema_name
|
||||||
|
AND TABLE_NAME = 'users'
|
||||||
|
AND COLUMN_NAME = 'mfa_enabled'
|
||||||
|
),
|
||||||
|
'SELECT 1',
|
||||||
|
'ALTER TABLE users ADD COLUMN mfa_enabled TINYINT(1) NOT NULL DEFAULT 0 AFTER email_verified_at'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @sql = (
|
||||||
|
SELECT IF(
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = @schema_name
|
||||||
|
AND TABLE_NAME = 'users'
|
||||||
|
AND COLUMN_NAME = 'disabled_at'
|
||||||
|
),
|
||||||
|
'SELECT 1',
|
||||||
|
'ALTER TABLE users ADD COLUMN disabled_at DATETIME NULL AFTER mfa_enabled'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @sql = (
|
||||||
|
SELECT IF(
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM INFORMATION_SCHEMA.STATISTICS
|
||||||
|
WHERE TABLE_SCHEMA = @schema_name
|
||||||
|
AND TABLE_NAME = 'participants'
|
||||||
|
AND INDEX_NAME = 'uq_participants_tenant_user'
|
||||||
|
),
|
||||||
|
'SELECT 1',
|
||||||
|
'ALTER TABLE participants ADD UNIQUE KEY uq_participants_tenant_user (tenant_id, user_id)'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @sql = (
|
||||||
|
SELECT IF(
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM INFORMATION_SCHEMA.STATISTICS
|
||||||
|
WHERE TABLE_SCHEMA = @schema_name
|
||||||
|
AND TABLE_NAME = 'participants'
|
||||||
|
AND INDEX_NAME = 'idx_participants_tenant_paypal_name'
|
||||||
|
),
|
||||||
|
'SELECT 1',
|
||||||
|
'ALTER TABLE participants ADD KEY idx_participants_tenant_paypal_name (tenant_id, paypal_name)'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
@@ -140,7 +140,8 @@ Die Skripte erwarten die bekannten Dev-Umgebungsvariablen `DB_HOST`, `DB_NAME`,
|
|||||||
eine PayPal-Alias-Dublette und hinterlaesst keine Datei in `var/uploads`.
|
eine PayPal-Alias-Dublette und hinterlaesst keine Datei in `var/uploads`.
|
||||||
- CSV-Upload negative Tests: Nicht-CSV-Dateien werden abgewiesen.
|
- CSV-Upload negative Tests: Nicht-CSV-Dateien werden abgewiesen.
|
||||||
- Golden Master weiterhin gruen mit 104 Assertions.
|
- Golden Master weiterhin gruen mit 104 Assertions.
|
||||||
- HTTP-Smoke weiterhin gruen mit 14 sicheren Seiten.
|
- HTTP-Smoke weiterhin gruen mit 16 sicheren Seiten inklusive Login und
|
||||||
|
Registrierung.
|
||||||
|
|
||||||
## Bewusste Grenzen
|
## Bewusste Grenzen
|
||||||
|
|
||||||
@@ -167,7 +168,8 @@ M3 startet mit additiven SaaS-Tabellen und einer Default-Tenant-Abbildung. Die
|
|||||||
Legacy-Seiten sollen dabei weiterhin ueber die bestehenden Tabellen laufen, bis
|
Legacy-Seiten sollen dabei weiterhin ueber die bestehenden Tabellen laufen, bis
|
||||||
der fachliche Kern in M4/M5 schrittweise auf das Zielmodell umgestellt wird.
|
der fachliche Kern in M4/M5 schrittweise auf das Zielmodell umgestellt wird.
|
||||||
|
|
||||||
Naechster Startpunkt:
|
M3 wurde inzwischen gestartet. Umgesetzte Details stehen in
|
||||||
|
`docs/m3-saas-basis-vorbereitung.md`. Der urspruengliche Startpunkt war:
|
||||||
|
|
||||||
1. `tenants`, `tenant_settings`, `users`, `tenant_memberships` und
|
1. `tenants`, `tenant_settings`, `users`, `tenant_memberships` und
|
||||||
`participants` als neue Migration anlegen.
|
`participants` als neue Migration anlegen.
|
||||||
|
|||||||
@@ -26,10 +26,11 @@ das neue Modell umgestellt.
|
|||||||
|
|
||||||
## Umgesetzter erster Schritt
|
## Umgesetzter erster Schritt
|
||||||
|
|
||||||
Umgesetzte Migration:
|
Umgesetzte Migrationen:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
database/migrations/0002_saas_identity_tenants.sql
|
database/migrations/0002_saas_identity_tenants.sql
|
||||||
|
database/migrations/0003_saas_auth_account_fields.sql
|
||||||
```
|
```
|
||||||
|
|
||||||
Tabellen:
|
Tabellen:
|
||||||
@@ -45,6 +46,7 @@ Ergaenzende Skripte:
|
|||||||
```text
|
```text
|
||||||
scripts/backfill-default-tenant.php
|
scripts/backfill-default-tenant.php
|
||||||
scripts/check-m3-saas-basis.php
|
scripts/check-m3-saas-basis.php
|
||||||
|
scripts/check-m3-auth-flow.php
|
||||||
```
|
```
|
||||||
|
|
||||||
Wichtige Regeln:
|
Wichtige Regeln:
|
||||||
@@ -104,6 +106,39 @@ Ausgefuehrter Dev-Stand:
|
|||||||
`tenant_memberships` angelegt.
|
`tenant_memberships` angelegt.
|
||||||
- Ein zweiter Backfill-Lauf blieb idempotent.
|
- Ein zweiter Backfill-Lauf blieb idempotent.
|
||||||
|
|
||||||
|
## Registrierung und Login
|
||||||
|
|
||||||
|
Umgesetzte Dateien:
|
||||||
|
|
||||||
|
```text
|
||||||
|
app/database.php
|
||||||
|
app/saas-auth.php
|
||||||
|
login.php
|
||||||
|
register.php
|
||||||
|
konto.php
|
||||||
|
logout.php
|
||||||
|
```
|
||||||
|
|
||||||
|
Umgesetzter Umfang:
|
||||||
|
|
||||||
|
- Registrierung legt Tenant, Owner-User, Default-Settings, Membership und
|
||||||
|
Owner-Participant in einer Transaktion an.
|
||||||
|
- Login prueft `users.password_hash`, aktive Membership und aktiven Tenant.
|
||||||
|
- Logout beendet die neue PHP-Login-Session.
|
||||||
|
- `konto.php` zeigt den aktuellen SaaS-Kontext fuer den angemeldeten User.
|
||||||
|
- `functions.php` akzeptiert eine SaaS-Login-Session als erste Identitaetsquelle,
|
||||||
|
laesst `DEV_AUTH_EMAIL` und `AUTH_USER` aber als Legacy-Fallback bestehen.
|
||||||
|
- `footer.php` ist fuer nicht angemeldete Public-Seiten sicher und zeigt Links
|
||||||
|
zu Login und Registrierung.
|
||||||
|
|
||||||
|
Noch offen im M3-Auth-Scope:
|
||||||
|
|
||||||
|
- Passwort-Reset.
|
||||||
|
- E-Mail-Verifikation.
|
||||||
|
- Tenant-Aufloesung ueber Subdomain oder Custom Domain.
|
||||||
|
- Zentrale Rollenpruefung fuer neue SaaS-Seiten.
|
||||||
|
- Owner-/Admin-Seite fuer Tenant-Grundeinstellungen.
|
||||||
|
|
||||||
## Erste Akzeptanzkriterien
|
## Erste Akzeptanzkriterien
|
||||||
|
|
||||||
- Migrationen laufen mehrfach ohne Fehler: erfuellt.
|
- Migrationen laufen mehrfach ohne Fehler: erfuellt.
|
||||||
@@ -113,6 +148,8 @@ Ausgefuehrter Dev-Stand:
|
|||||||
- Jeder bestehende `kl_Mitarbeiter` hat genau einen `participant`: erfuellt.
|
- Jeder bestehende `kl_Mitarbeiter` hat genau einen `participant`: erfuellt.
|
||||||
- `participants.legacy_mitarbeiter_id` ist gesetzt: erfuellt.
|
- `participants.legacy_mitarbeiter_id` ist gesetzt: erfuellt.
|
||||||
- Admins werden als tenant-scoped Rolle abgebildet: erfuellt.
|
- Admins werden als tenant-scoped Rolle abgebildet: erfuellt.
|
||||||
|
- Registrierung und Login funktionieren fuer einen neuen Test-Tenant:
|
||||||
|
erfuellt.
|
||||||
- Golden-Master und HTTP-Smoke bleiben gruen: erfuellt.
|
- Golden-Master und HTTP-Smoke bleiben gruen: erfuellt.
|
||||||
|
|
||||||
## Risiken
|
## Risiken
|
||||||
@@ -128,8 +165,12 @@ Ausgefuehrter Dev-Stand:
|
|||||||
## Empfohlene Reihenfolge
|
## Empfohlene Reihenfolge
|
||||||
|
|
||||||
1. Migration `0002_saas_identity_tenants.sql` erstellen: erledigt.
|
1. Migration `0002_saas_identity_tenants.sql` erstellen: erledigt.
|
||||||
2. `scripts/backfill-default-tenant.php` erstellen: erledigt.
|
2. Migration `0003_saas_auth_account_fields.sql` erstellen: erledigt.
|
||||||
3. Backfill gegen die Dev-Datenbank ausfuehren: erledigt.
|
3. `scripts/backfill-default-tenant.php` erstellen: erledigt.
|
||||||
4. Kontrollskript fuer Tenant/Participant/Role-Counts schreiben: erledigt.
|
4. Backfill gegen die Dev-Datenbank ausfuehren: erledigt.
|
||||||
5. Golden-Master und HTTP-Smoke ausfuehren: erledigt.
|
5. Kontrollskript fuer Tenant/Participant/Role-Counts schreiben: erledigt.
|
||||||
6. Danach Login-/Registrierungsrouten planen: naechster Schritt.
|
6. Login-/Registrierungsrouten bauen: erledigt.
|
||||||
|
7. Auth-Flow-Kontrollskript schreiben: erledigt.
|
||||||
|
8. Golden-Master und HTTP-Smoke ausfuehren: erledigt.
|
||||||
|
9. Passwort-Reset, E-Mail-Verifikation und zentrale Rollenpruefung planen:
|
||||||
|
naechster Schritt.
|
||||||
|
|||||||
@@ -417,8 +417,10 @@ Schritte:
|
|||||||
- Default-Tenant fuer den aktuellen Bestand anlegen: erledigt.
|
- Default-Tenant fuer den aktuellen Bestand anlegen: erledigt.
|
||||||
- Bestehende `kl_Mitarbeiter` idempotent in `participants` spiegeln:
|
- Bestehende `kl_Mitarbeiter` idempotent in `participants` spiegeln:
|
||||||
erledigt.
|
erledigt.
|
||||||
- Registrierung: Tenant + Owner-User + Default-Settings erzeugen.
|
- Registrierung: Tenant + Owner-User + Default-Settings erzeugen: erster
|
||||||
- Login, Logout, Passwort-Reset und E-Mail-Verifikation bauen.
|
Flow erledigt.
|
||||||
|
- Login und Logout bauen: erster Flow erledigt.
|
||||||
|
- Passwort-Reset und E-Mail-Verifikation bauen.
|
||||||
- Tenant-Aufloesung definieren.
|
- Tenant-Aufloesung definieren.
|
||||||
- Rollenpruefung zentralisieren.
|
- Rollenpruefung zentralisieren.
|
||||||
- Erste Admin-/Owner-Seite fuer Grundeinstellungen.
|
- Erste Admin-/Owner-Seite fuer Grundeinstellungen.
|
||||||
|
|||||||
+14
@@ -33,6 +33,20 @@ if(checkKaffeelisteAdmin($conn, $mailadress)){
|
|||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
<li><a href="faq.php">FAQ Kaffeeliste</a></li>
|
<li><a href="faq.php">FAQ Kaffeeliste</a></li>
|
||||||
|
<?php
|
||||||
|
$saasNavUser = function_exists('saas_current_user') ? saas_current_user() : null;
|
||||||
|
if ($saasNavUser !== null) {
|
||||||
|
?>
|
||||||
|
<li><a href="konto.php">Kundenkonto</a></li>
|
||||||
|
<li><a href="logout.php">Logout</a></li>
|
||||||
|
<?php
|
||||||
|
} else {
|
||||||
|
?>
|
||||||
|
<li><a href="login.php">Login</a></li>
|
||||||
|
<li><a href="register.php">Registrierung</a></li>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
?>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
|||||||
+31
-8
@@ -1,21 +1,33 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
include_once "config.php";
|
include_once "config.php";
|
||||||
|
require_once __DIR__ . "/app/saas-auth.php";
|
||||||
|
|
||||||
$devAuthEmail = getenv('DEV_AUTH_EMAIL') ?: '';
|
$mailadress = '';
|
||||||
if ($devAuthEmail !== '') {
|
$kennung = '';
|
||||||
$mailadress = strtolower(trim($devAuthEmail));
|
|
||||||
|
$saasUser = saas_current_user();
|
||||||
|
if ($saasUser !== null) {
|
||||||
|
$mailadress = strtolower(trim((string)$saasUser['email']));
|
||||||
$kennung = $mailadress;
|
$kennung = $mailadress;
|
||||||
} else {
|
} else {
|
||||||
$kennungtemp = $_SERVER['AUTH_USER'];
|
$devAuthEmail = getenv('DEV_AUTH_EMAIL') ?: '';
|
||||||
$teile = explode("\\", $kennungtemp);
|
if ($devAuthEmail !== '') {
|
||||||
$kennung = $teile[1];
|
$mailadress = strtolower(trim($devAuthEmail));
|
||||||
|
$kennung = $mailadress;
|
||||||
|
} elseif (!empty($_SERVER['AUTH_USER'])) {
|
||||||
|
$kennungtemp = $_SERVER['AUTH_USER'];
|
||||||
|
$teile = explode("\\", $kennungtemp);
|
||||||
|
$kennung = $teile[1] ?? $kennungtemp;
|
||||||
|
|
||||||
include_once "functionsLDAP.php";
|
include_once "functionsLDAP.php";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getUserName($conn, $mail){
|
function getUserName($conn, $mail){
|
||||||
|
if (trim((string)$mail) === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
$sqlMitglieder = "SELECT Name FROM kl_Mitarbeiter WHERE Email like '" . strtolower($mail) . "' ";
|
$sqlMitglieder = "SELECT Name FROM kl_Mitarbeiter WHERE Email like '" . strtolower($mail) . "' ";
|
||||||
$stmtMitglieder = sqlsrv_query($conn, $sqlMitglieder);
|
$stmtMitglieder = sqlsrv_query($conn, $sqlMitglieder);
|
||||||
@@ -27,6 +39,9 @@ function getUserName($conn, $mail){
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getUserId($conn, $mail){
|
function getUserId($conn, $mail){
|
||||||
|
if (trim((string)$mail) === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
$sqlMitglieder = "SELECT MitarbeiterID FROM kl_Mitarbeiter WHERE Email like '" . strtolower($mail) . "' ";
|
$sqlMitglieder = "SELECT MitarbeiterID FROM kl_Mitarbeiter WHERE Email like '" . strtolower($mail) . "' ";
|
||||||
$stmtMitglieder = sqlsrv_query($conn, $sqlMitglieder);
|
$stmtMitglieder = sqlsrv_query($conn, $sqlMitglieder);
|
||||||
@@ -41,6 +56,10 @@ function getUserId($conn, $mail){
|
|||||||
|
|
||||||
function checkKaffeelisteAccess($conn, $mail)
|
function checkKaffeelisteAccess($conn, $mail)
|
||||||
{
|
{
|
||||||
|
if (trim((string)$mail) === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Mitarbeiter aus der Datenbank abrufen und nach Namen sortieren
|
// Mitarbeiter aus der Datenbank abrufen und nach Namen sortieren
|
||||||
$sqlMitarbeiter = "SELECT MitarbeiterID FROM kl_Mitarbeiter WHERE Email like '" . strtolower($mail) . "' AND aktiv='1'";
|
$sqlMitarbeiter = "SELECT MitarbeiterID FROM kl_Mitarbeiter WHERE Email like '" . strtolower($mail) . "' AND aktiv='1'";
|
||||||
|
|
||||||
@@ -57,6 +76,10 @@ function checkKaffeelisteAccess($conn, $mail)
|
|||||||
|
|
||||||
function checkKaffeelisteAdmin($conn, $mail)
|
function checkKaffeelisteAdmin($conn, $mail)
|
||||||
{
|
{
|
||||||
|
if (trim((string)$mail) === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Mitarbeiter aus der Datenbank abrufen und nach Namen sortieren
|
// Mitarbeiter aus der Datenbank abrufen und nach Namen sortieren
|
||||||
$sqlMitarbeiter = "SELECT MitarbeiterID FROM kl_Mitarbeiter WHERE Email like '" . strtolower($mail) . "' AND admin='1'";
|
$sqlMitarbeiter = "SELECT MitarbeiterID FROM kl_Mitarbeiter WHERE Email like '" . strtolower($mail) . "' AND admin='1'";
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once __DIR__ . '/functions.php';
|
||||||
|
|
||||||
|
$user = saas_require_login();
|
||||||
|
|
||||||
|
include 'header.php';
|
||||||
|
include 'headerline.php';
|
||||||
|
include 'nav.php';
|
||||||
|
?>
|
||||||
|
|
||||||
|
<section id="banner">
|
||||||
|
<div class="content">
|
||||||
|
<h2>Kundenkonto</h2>
|
||||||
|
|
||||||
|
<?php if (isset($_GET['registered'])): ?>
|
||||||
|
<div class="hint-box success"><p>Das Kundenkonto wurde angelegt.</p></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<td><?php echo saas_html($user['display_name']); ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>E-Mail</th>
|
||||||
|
<td><?php echo saas_html($user['email']); ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Kunde</th>
|
||||||
|
<td><?php echo saas_html($user['tenant_name']); ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Kundenkuerzel</th>
|
||||||
|
<td><?php echo saas_html($user['tenant_slug']); ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Rolle</th>
|
||||||
|
<td><?php echo saas_html($user['role']); ?></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<form method="post" action="logout.php">
|
||||||
|
<?php echo app_csrf_field(); ?>
|
||||||
|
<button type="submit">Abmelden</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<?php include 'footer.php'; ?>
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once __DIR__ . '/functions.php';
|
||||||
|
|
||||||
|
$pdo = app_db_pdo();
|
||||||
|
$errors = [];
|
||||||
|
$email = trim((string)($_POST['email'] ?? ''));
|
||||||
|
$tenantSlug = trim((string)($_POST['tenant_slug'] ?? ''));
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
app_require_csrf();
|
||||||
|
|
||||||
|
$result = saas_authenticate(
|
||||||
|
$pdo,
|
||||||
|
$email,
|
||||||
|
(string)($_POST['password'] ?? ''),
|
||||||
|
$tenantSlug
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($result['ok']) {
|
||||||
|
saas_session_login($result['identity']);
|
||||||
|
header('Location: konto.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$errors = $result['errors'];
|
||||||
|
}
|
||||||
|
|
||||||
|
include 'header.php';
|
||||||
|
include 'headerline.php';
|
||||||
|
include 'nav.php';
|
||||||
|
?>
|
||||||
|
|
||||||
|
<section id="banner">
|
||||||
|
<div class="content">
|
||||||
|
<h2>Login</h2>
|
||||||
|
|
||||||
|
<?php if (isset($_GET['logged_out'])): ?>
|
||||||
|
<div class="hint-box success"><p>Du wurdest abgemeldet.</p></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($errors !== []): ?>
|
||||||
|
<div class="hint-box error">
|
||||||
|
<?php foreach ($errors as $error): ?>
|
||||||
|
<p><?php echo saas_html($error); ?></p>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<form method="post" action="login.php">
|
||||||
|
<?php echo app_csrf_field(); ?>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6 col-12-small">
|
||||||
|
<label for="email">E-Mail</label>
|
||||||
|
<input type="email" name="email" id="email" value="<?php echo saas_html($email); ?>" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 col-12-small">
|
||||||
|
<label for="password">Passwort</label>
|
||||||
|
<input type="password" name="password" id="password" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 col-12-small">
|
||||||
|
<label for="tenant_slug">Kundenkuerzel</label>
|
||||||
|
<input type="text" name="tenant_slug" id="tenant_slug" value="<?php echo saas_html($tenantSlug); ?>" placeholder="optional">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ul class="actions">
|
||||||
|
<li><button type="submit">Einloggen</button></li>
|
||||||
|
<li><a href="register.php" class="button alt">Registrieren</a></li>
|
||||||
|
</ul>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<?php include 'footer.php'; ?>
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once __DIR__ . '/functions.php';
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
app_require_csrf();
|
||||||
|
saas_logout();
|
||||||
|
header('Location: login.php?logged_out=1');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
include 'header.php';
|
||||||
|
include 'headerline.php';
|
||||||
|
include 'nav.php';
|
||||||
|
?>
|
||||||
|
|
||||||
|
<section id="banner">
|
||||||
|
<div class="content">
|
||||||
|
<h2>Abmelden</h2>
|
||||||
|
<form method="post" action="logout.php">
|
||||||
|
<?php echo app_csrf_field(); ?>
|
||||||
|
<button type="submit">Abmelden</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<?php include 'footer.php'; ?>
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once __DIR__ . '/functions.php';
|
||||||
|
|
||||||
|
$pdo = app_db_pdo();
|
||||||
|
$errors = [];
|
||||||
|
$values = [
|
||||||
|
'tenant_name' => trim((string)($_POST['tenant_name'] ?? '')),
|
||||||
|
'tenant_slug' => trim((string)($_POST['tenant_slug'] ?? '')),
|
||||||
|
'display_name' => trim((string)($_POST['display_name'] ?? '')),
|
||||||
|
'email' => trim((string)($_POST['email'] ?? '')),
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
app_require_csrf();
|
||||||
|
|
||||||
|
$result = saas_register_tenant_owner($pdo, [
|
||||||
|
'tenant_name' => $values['tenant_name'],
|
||||||
|
'tenant_slug' => $values['tenant_slug'],
|
||||||
|
'display_name' => $values['display_name'],
|
||||||
|
'email' => $values['email'],
|
||||||
|
'password' => (string)($_POST['password'] ?? ''),
|
||||||
|
'password_confirm' => (string)($_POST['password_confirm'] ?? ''),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($result['ok']) {
|
||||||
|
saas_session_login($result['identity']);
|
||||||
|
header('Location: konto.php?registered=1');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$errors = $result['errors'];
|
||||||
|
}
|
||||||
|
|
||||||
|
include 'header.php';
|
||||||
|
include 'headerline.php';
|
||||||
|
include 'nav.php';
|
||||||
|
?>
|
||||||
|
|
||||||
|
<section id="banner">
|
||||||
|
<div class="content">
|
||||||
|
<h2>Registrierung</h2>
|
||||||
|
|
||||||
|
<?php if ($errors !== []): ?>
|
||||||
|
<div class="hint-box error">
|
||||||
|
<?php foreach ($errors as $error): ?>
|
||||||
|
<p><?php echo saas_html($error); ?></p>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<form method="post" action="register.php">
|
||||||
|
<?php echo app_csrf_field(); ?>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6 col-12-small">
|
||||||
|
<label for="tenant_name">Kundenname</label>
|
||||||
|
<input type="text" name="tenant_name" id="tenant_name" value="<?php echo saas_html($values['tenant_name']); ?>" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 col-12-small">
|
||||||
|
<label for="tenant_slug">Kundenkuerzel</label>
|
||||||
|
<input type="text" name="tenant_slug" id="tenant_slug" value="<?php echo saas_html($values['tenant_slug']); ?>" placeholder="kaffeeliste-team" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 col-12-small">
|
||||||
|
<label for="display_name">Dein Name</label>
|
||||||
|
<input type="text" name="display_name" id="display_name" value="<?php echo saas_html($values['display_name']); ?>" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 col-12-small">
|
||||||
|
<label for="email">E-Mail</label>
|
||||||
|
<input type="email" name="email" id="email" value="<?php echo saas_html($values['email']); ?>" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 col-12-small">
|
||||||
|
<label for="password">Passwort</label>
|
||||||
|
<input type="password" name="password" id="password" minlength="8" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 col-12-small">
|
||||||
|
<label for="password_confirm">Passwort wiederholen</label>
|
||||||
|
<input type="password" name="password_confirm" id="password_confirm" minlength="8" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ul class="actions">
|
||||||
|
<li><button type="submit">Kundenkonto anlegen</button></li>
|
||||||
|
<li><a href="login.php" class="button alt">Zum Login</a></li>
|
||||||
|
</ul>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<?php include 'footer.php'; ?>
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require __DIR__ . '/dev-db.php';
|
||||||
|
require __DIR__ . '/../app/saas-auth.php';
|
||||||
|
|
||||||
|
function auth_check_assert(string $label, bool $condition, array &$failures, int &$passes): void
|
||||||
|
{
|
||||||
|
if ($condition) {
|
||||||
|
$passes++;
|
||||||
|
echo "PASS {$label}\n";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$failures[] = $label;
|
||||||
|
echo "FAIL {$label}\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
function auth_check_cleanup(PDO $pdo, string $slug, string $emailNorm): void
|
||||||
|
{
|
||||||
|
$stmt = $pdo->prepare('SELECT id FROM tenants WHERE slug = ?');
|
||||||
|
$stmt->execute([$slug]);
|
||||||
|
$tenantId = $stmt->fetchColumn();
|
||||||
|
|
||||||
|
if ($tenantId !== false) {
|
||||||
|
$tenantId = (int)$tenantId;
|
||||||
|
$pdo->prepare('DELETE FROM participants WHERE tenant_id = ?')->execute([$tenantId]);
|
||||||
|
$pdo->prepare('DELETE FROM tenant_memberships WHERE tenant_id = ?')->execute([$tenantId]);
|
||||||
|
$pdo->prepare('DELETE FROM tenant_settings WHERE tenant_id = ?')->execute([$tenantId]);
|
||||||
|
$pdo->prepare('DELETE FROM tenants WHERE id = ?')->execute([$tenantId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo->prepare('DELETE FROM users WHERE email_norm = ?')->execute([$emailNorm]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = dev_pdo();
|
||||||
|
dev_apply_schema($pdo);
|
||||||
|
|
||||||
|
$slug = 'm3-auth-flow-check';
|
||||||
|
$email = 'm3-auth-flow-check@test.local';
|
||||||
|
$emailNorm = saas_email_norm($email);
|
||||||
|
$password = 'M3-auth-flow-123';
|
||||||
|
$failures = [];
|
||||||
|
$passes = 0;
|
||||||
|
|
||||||
|
auth_check_cleanup($pdo, $slug, $emailNorm);
|
||||||
|
|
||||||
|
$result = saas_register_tenant_owner($pdo, [
|
||||||
|
'tenant_name' => 'M3 Auth Flow Check',
|
||||||
|
'tenant_slug' => $slug,
|
||||||
|
'display_name' => 'M3 Auth Check',
|
||||||
|
'email' => $email,
|
||||||
|
'password' => $password,
|
||||||
|
'password_confirm' => $password,
|
||||||
|
]);
|
||||||
|
|
||||||
|
auth_check_assert('registration succeeds', $result['ok'] === true, $failures, $passes);
|
||||||
|
|
||||||
|
if ($result['ok']) {
|
||||||
|
$tenantId = (int)$result['identity']['tenant_id'];
|
||||||
|
$userId = (int)$result['identity']['user_id'];
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare('SELECT COUNT(*) FROM tenants WHERE id = ? AND slug = ?');
|
||||||
|
$stmt->execute([$tenantId, $slug]);
|
||||||
|
auth_check_assert('tenant is persisted', (int)$stmt->fetchColumn() === 1, $failures, $passes);
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare('SELECT COUNT(*) FROM tenant_settings WHERE tenant_id = ?');
|
||||||
|
$stmt->execute([$tenantId]);
|
||||||
|
auth_check_assert('tenant settings are persisted', (int)$stmt->fetchColumn() === 1, $failures, $passes);
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare('SELECT COUNT(*) FROM tenant_memberships WHERE tenant_id = ? AND user_id = ? AND role = ?');
|
||||||
|
$stmt->execute([$tenantId, $userId, 'owner']);
|
||||||
|
auth_check_assert('owner membership is persisted', (int)$stmt->fetchColumn() === 1, $failures, $passes);
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare('SELECT COUNT(*) FROM participants WHERE tenant_id = ? AND user_id = ? AND email_norm = ?');
|
||||||
|
$stmt->execute([$tenantId, $userId, $emailNorm]);
|
||||||
|
auth_check_assert('owner participant is persisted', (int)$stmt->fetchColumn() === 1, $failures, $passes);
|
||||||
|
}
|
||||||
|
|
||||||
|
$login = saas_authenticate($pdo, $email, $password, $slug);
|
||||||
|
auth_check_assert('login succeeds', $login['ok'] === true, $failures, $passes);
|
||||||
|
if ($login['ok']) {
|
||||||
|
auth_check_assert('login returns owner role', $login['identity']['role'] === 'owner', $failures, $passes);
|
||||||
|
auth_check_assert('login returns tenant slug', $login['identity']['tenant_slug'] === $slug, $failures, $passes);
|
||||||
|
}
|
||||||
|
|
||||||
|
$badLogin = saas_authenticate($pdo, $email, 'wrong-password', $slug);
|
||||||
|
auth_check_assert('wrong password is rejected', $badLogin['ok'] === false, $failures, $passes);
|
||||||
|
|
||||||
|
auth_check_cleanup($pdo, $slug, $emailNorm);
|
||||||
|
|
||||||
|
if ($failures !== []) {
|
||||||
|
echo "\nM3 auth flow check failed with " . count($failures) . " failure(s):\n";
|
||||||
|
foreach ($failures as $failure) {
|
||||||
|
echo "- {$failure}\n";
|
||||||
|
}
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\nM3 auth flow check passed with {$passes} assertions.\n";
|
||||||
+10
-1
@@ -51,6 +51,16 @@ $checks = [
|
|||||||
'path' => 'faq.php',
|
'path' => 'faq.php',
|
||||||
'contains' => ['FAQ - Kaffeeliste'],
|
'contains' => ['FAQ - Kaffeeliste'],
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'label' => 'Login',
|
||||||
|
'path' => 'login.php',
|
||||||
|
'contains' => ['Login', 'Registrieren'],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'label' => 'Registrierung',
|
||||||
|
'path' => 'register.php',
|
||||||
|
'contains' => ['Registrierung', 'Kundenkonto anlegen'],
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'label' => 'Namensanpassung',
|
'label' => 'Namensanpassung',
|
||||||
'path' => 'namenanpassen.php',
|
'path' => 'namenanpassen.php',
|
||||||
@@ -217,4 +227,3 @@ if ($failures !== []) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
echo "\nHTTP smoke passed with {$passes} checked pages.\n";
|
echo "\nHTTP smoke passed with {$passes} checked pages.\n";
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user