app->pdo(); if (!$pdo) { throw new \RuntimeException('Database connection not available.'); } return $pdo; } public function register(string $displayName, string $email, string $password): int { $pdo = $this->pdo(); $email = strtolower(trim($email)); $displayName = trim($displayName); if ($displayName === '' || $email === '' || $password === '') { throw new \InvalidArgumentException('Display-Name, E-Mail und Passwort sind erforderlich.'); } $pdo->beginTransaction(); try { $stmt = $pdo->prepare('SELECT id FROM users WHERE email = :email LIMIT 1'); $stmt->execute(['email' => $email]); if ($stmt->fetchColumn()) { 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->execute([ 'email' => $email, 'pw' => $hash, 'status' => 'pending', ]); $userId = (int)$pdo->lastInsertId(); $stmt = $pdo->prepare('INSERT INTO user_profiles (user_id, display_name, share_level, children_visibility, created_at, updated_at) VALUES (:uid, :name, :share, :childvis, NOW(), NOW())'); $stmt->execute([ 'uid' => $userId, 'name' => $displayName, 'share' => 'basic', 'childvis' => 'hidden', ]); $pdo->commit(); return $userId; } catch (\Throwable $e) { $pdo->rollBack(); throw $e; } } public function createVerifyCode(int $userId, string $email): string { $pdo = $this->pdo(); $code = $this->generateCode(6); $hash = hash('sha256', $code); $pdo->prepare('DELETE FROM user_tokens WHERE user_id = :uid AND type = :t')->execute(['uid' => $userId, 't' => 'verify']); $stmt = $pdo->prepare('INSERT INTO user_tokens (user_id, type, code, token_hash, expires_at, created_at) VALUES (:uid, :type, :code, :hash, DATE_ADD(NOW(), INTERVAL 48 HOUR), NOW())'); $stmt->execute([ 'uid' => $userId, 'type' => 'verify', 'code' => $code, 'hash' => $hash, ]); return $code; } public function verifyCode(string $email, string $code): int { $pdo = $this->pdo(); $email = strtolower(trim($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]); $row = $stmt->fetch(\PDO::FETCH_ASSOC); if (!$row || !hash_equals((string)$row['token_hash'], $hash)) { throw new \RuntimeException('Code ist ungültig oder abgelaufen.'); } $userId = (int)$row['id']; $tid = (int)$row['tid']; $pdo->beginTransaction(); try { $pdo->prepare('UPDATE user_tokens SET used_at = NOW() WHERE id = :id')->execute(['id' => $tid]); $pdo->prepare('UPDATE users SET status = :st, email_verified_at = NOW() WHERE id = :id')->execute(['st' => 'active', 'id' => $userId]); $pdo->commit(); } catch (\Throwable $e) { $pdo->rollBack(); throw $e; } return $userId; } public function createResetCode(string $email): array { $pdo = $this->pdo(); $email = strtolower(trim($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); if (!$row) { throw new \RuntimeException('E-Mail ist nicht registriert.'); } $userId = (int)$row['id']; $displayName = (string)($row['display_name'] ?? $email); $code = $this->generateCode(6); $hash = hash('sha256', $code); $pdo->prepare('DELETE FROM user_tokens WHERE user_id = :uid AND type = :t')->execute(['uid' => $userId, 't' => 'reset']); $stmt = $pdo->prepare('INSERT INTO user_tokens (user_id, type, code, token_hash, expires_at, created_at) VALUES (:uid, :type, :code, :hash, DATE_ADD(NOW(), INTERVAL 2 HOUR), NOW())'); $stmt->execute([ 'uid' => $userId, 'type' => 'reset', 'code' => $code, 'hash' => $hash, ]); return ['user_id' => $userId, 'code' => $code, 'display_name' => $displayName]; } public function verifyResetCode(string $email, string $code): int { $pdo = $this->pdo(); $email = strtolower(trim($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]); $row = $stmt->fetch(\PDO::FETCH_ASSOC); if (!$row || !hash_equals((string)$row['token_hash'], $hash)) { throw new \RuntimeException('Code ist ungültig oder abgelaufen.'); } $userId = (int)$row['id']; $tid = (int)$row['tid']; $pdo->beginTransaction(); try { $pdo->prepare('UPDATE user_tokens SET used_at = NOW() WHERE id = :id')->execute(['id' => $tid]); $pdo->commit(); } catch (\Throwable $e) { $pdo->rollBack(); throw $e; } return $userId; } public function resetPassword(int $userId, string $password): void { $pdo = $this->pdo(); if ($password === '' || strlen($password) < 8) { throw new \InvalidArgumentException('Passwort muss mindestens 8 Zeichen haben.'); } $hash = password_hash($password, PASSWORD_ARGON2ID); $stmt = $pdo->prepare('UPDATE users SET password_hash = :pw, status = :status, updated_at = NOW() WHERE id = :id'); $stmt->execute([ 'pw' => $hash, 'status' => 'active', 'id' => $userId, ]); } private function generateCode(int $len = 6): string { $chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; $out = ''; for ($i = 0; $i < $len; $i++) { $out .= $chars[random_int(0, strlen($chars) - 1)]; } return $out; } public function login(string $email, string $password): array { $pdo = $this->pdo(); $email = strtolower(trim($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); if (!$row) { throw new \RuntimeException('E-Mail oder Passwort ist falsch.'); } if (!password_verify($password, (string)$row['password_hash'])) { throw new \RuntimeException('E-Mail oder Passwort ist falsch.'); } $userId = (int)$row['id']; $status = (string)$row['status']; if ($status === 'active') { $upd = $pdo->prepare('UPDATE users SET last_login_at = NOW() WHERE id = :id'); $upd->execute(['id' => $userId]); } return ['id' => $userId, 'status' => $status]; } }