dsfdsf
All checks were successful
Deploy / deploy (push) Successful in 59s

This commit is contained in:
2026-07-31 21:33:04 +02:00
parent 92442a57c7
commit 002108ac83
10 changed files with 348 additions and 35 deletions

View File

@@ -5,6 +5,8 @@ namespace App;
final class Auth
{
private ?UserEmailStore $emailStore = null;
public function __construct(private App $app) {}
private function pdo(): \PDO
@@ -16,6 +18,16 @@ final class Auth
return $pdo;
}
private function emailStore(): UserEmailStore
{
if ($this->emailStore === null) {
$this->emailStore = new UserEmailStore($this->pdo());
$this->emailStore->ensureSchema();
}
return $this->emailStore;
}
public function register(string $displayName, string $email, string $password): int
{
$pdo = $this->pdo();
@@ -26,18 +38,18 @@ final class Auth
throw new \InvalidArgumentException('Display-Name, E-Mail und Passwort sind erforderlich.');
}
$emailStore = $this->emailStore();
$pdo->beginTransaction();
try {
$stmt = $pdo->prepare('SELECT id FROM users WHERE email = :email LIMIT 1');
$stmt->execute(['email' => $email]);
if ($stmt->fetchColumn()) {
if ($emailStore->findUserByEmail($email, 'id')) {
throw new \RuntimeException('E-Mail ist bereits registriert.');
}
$hash = password_hash($password, PASSWORD_ARGON2ID);
$stmt = $pdo->prepare('INSERT INTO users (email, password_hash, status, created_at, updated_at) VALUES (:email, :pw, :status, NOW(), NOW())');
$stmt = $pdo->prepare('INSERT INTO users (email, email_lookup_hash, password_hash, status, created_at, updated_at) VALUES (:email, :lookup, :pw, :status, NOW(), NOW())');
$stmt->execute([
'email' => $email,
'email' => $emailStore->encrypt($email),
'lookup' => $emailStore->lookupHash($email),
'pw' => $hash,
'status' => 'pending',
]);
@@ -80,11 +92,12 @@ final class Auth
public function verifyCode(string $email, string $code): int
{
$pdo = $this->pdo();
$email = strtolower(trim($email));
$email = $this->emailStore()->normalize($email);
$hash = hash('sha256', $code);
$stmt = $pdo->prepare('SELECT u.id, u.status, t.id AS tid, t.token_hash FROM users u JOIN user_tokens t ON t.user_id = u.id AND t.type = :type WHERE u.email = :email AND (t.used_at IS NULL) AND t.expires_at > NOW() ORDER BY t.expires_at DESC LIMIT 1');
$stmt->execute(['type' => 'verify', 'email' => $email]);
$lookupHash = $this->emailStore()->lookupHash($email);
$stmt = $pdo->prepare('SELECT u.id, u.status, t.id AS tid, t.token_hash FROM users u JOIN user_tokens t ON t.user_id = u.id AND t.type = :type WHERE u.email_lookup_hash = :lookup AND (t.used_at IS NULL) AND t.expires_at > NOW() ORDER BY t.expires_at DESC LIMIT 1');
$stmt->execute(['type' => 'verify', 'lookup' => $lookupHash]);
$row = $stmt->fetch(\PDO::FETCH_ASSOC);
if (!$row || !hash_equals((string)$row['token_hash'], $hash)) {
throw new \RuntimeException('Code ist ungültig oder abgelaufen.');
@@ -109,17 +122,17 @@ final class Auth
public function createResetCode(string $email): array
{
$pdo = $this->pdo();
$email = strtolower(trim($email));
$email = $this->emailStore()->normalize($email);
$stmt = $pdo->prepare('SELECT u.id, p.display_name FROM users u LEFT JOIN user_profiles p ON p.user_id = u.id WHERE u.email = :email LIMIT 1');
$stmt->execute(['email' => $email]);
$row = $stmt->fetch(\PDO::FETCH_ASSOC);
$row = $this->emailStore()->findUserByEmail($email, 'id');
if (!$row) {
throw new \RuntimeException('E-Mail ist nicht registriert.');
}
$userId = (int)$row['id'];
$displayName = (string)($row['display_name'] ?? $email);
$stmt = $pdo->prepare('SELECT display_name FROM user_profiles WHERE user_id = :id LIMIT 1');
$stmt->execute(['id' => $userId]);
$displayName = (string)($stmt->fetchColumn() ?: $email);
$code = $this->generateCode(6);
$hash = hash('sha256', $code);
@@ -138,11 +151,12 @@ final class Auth
public function verifyResetCode(string $email, string $code): int
{
$pdo = $this->pdo();
$email = strtolower(trim($email));
$email = $this->emailStore()->normalize($email);
$hash = hash('sha256', $code);
$stmt = $pdo->prepare('SELECT u.id, t.id AS tid, t.token_hash FROM users u JOIN user_tokens t ON t.user_id = u.id AND t.type = :type WHERE u.email = :email AND (t.used_at IS NULL) AND t.expires_at > NOW() ORDER BY t.expires_at DESC LIMIT 1');
$stmt->execute(['type' => 'reset', 'email' => $email]);
$lookupHash = $this->emailStore()->lookupHash($email);
$stmt = $pdo->prepare('SELECT u.id, t.id AS tid, t.token_hash FROM users u JOIN user_tokens t ON t.user_id = u.id AND t.type = :type WHERE u.email_lookup_hash = :lookup AND (t.used_at IS NULL) AND t.expires_at > NOW() ORDER BY t.expires_at DESC LIMIT 1');
$stmt->execute(['type' => 'reset', 'lookup' => $lookupHash]);
$row = $stmt->fetch(\PDO::FETCH_ASSOC);
if (!$row || !hash_equals((string)$row['token_hash'], $hash)) {
throw new \RuntimeException('Code ist ungültig oder abgelaufen.');
@@ -191,11 +205,9 @@ final class Auth
public function login(string $email, string $password): array
{
$pdo = $this->pdo();
$email = strtolower(trim($email));
$email = $this->emailStore()->normalize($email);
$stmt = $pdo->prepare('SELECT id, password_hash, status FROM users WHERE email = :email LIMIT 1');
$stmt->execute(['email' => $email]);
$row = $stmt->fetch(\PDO::FETCH_ASSOC);
$row = $this->emailStore()->findUserByEmail($email, 'id, password_hash, status');
if (!$row) {
throw new \RuntimeException('E-Mail oder Passwort ist falsch.');
@@ -214,4 +226,23 @@ final class Auth
return ['id' => $userId, 'status' => $status];
}
public function findUserMetaByEmail(string $email): ?array
{
$row = $this->emailStore()->findUserByEmail($email, 'id, status');
if (!$row) {
return null;
}
$stmt = $this->pdo()->prepare('SELECT display_name FROM user_profiles WHERE user_id = :id LIMIT 1');
$stmt->execute(['id' => (int)$row['id']]);
$row['display_name'] = (string)($stmt->fetchColumn() ?: '');
return $row;
}
public function getEmailByUserId(int $userId): string
{
return $this->emailStore()->getEmailByUserId($userId);
}
}