yxxx
This commit is contained in:
@@ -39,7 +39,7 @@ final class Auth
|
||||
$stmt->execute([
|
||||
'email' => $email,
|
||||
'pw' => $hash,
|
||||
'status' => 'active',
|
||||
'status' => 'pending',
|
||||
]);
|
||||
$userId = (int)$pdo->lastInsertId();
|
||||
|
||||
@@ -59,7 +59,64 @@ final class Auth
|
||||
}
|
||||
}
|
||||
|
||||
public function login(string $email, string $password): int
|
||||
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;
|
||||
}
|
||||
|
||||
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));
|
||||
@@ -71,18 +128,18 @@ final class Auth
|
||||
if (!$row) {
|
||||
throw new \RuntimeException('E-Mail oder Passwort ist falsch.');
|
||||
}
|
||||
if ($row['status'] !== 'active') {
|
||||
throw new \RuntimeException('Account ist nicht aktiv.');
|
||||
}
|
||||
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'];
|
||||
|
||||
$upd = $pdo->prepare('UPDATE users SET last_login_at = NOW() WHERE id = :id');
|
||||
$upd->execute(['id' => $userId]);
|
||||
if ($status === 'active') {
|
||||
$upd = $pdo->prepare('UPDATE users SET last_login_at = NOW() WHERE id = :id');
|
||||
$upd->execute(['id' => $userId]);
|
||||
}
|
||||
|
||||
return $userId;
|
||||
return ['id' => $userId, 'status' => $status];
|
||||
}
|
||||
}
|
||||
|
||||
174
src/App/Mailer.php
Normal file
174
src/App/Mailer.php
Normal file
@@ -0,0 +1,174 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App;
|
||||
|
||||
final class Mailer
|
||||
{
|
||||
public function __construct(private App $app) {}
|
||||
|
||||
private function templates(): array
|
||||
{
|
||||
$env = $this->app->config()->env;
|
||||
$root = __DIR__ . '/../../config/emailtemplates.php';
|
||||
$envPath = __DIR__ . "/../../config/{$env}/emailtemplates.php";
|
||||
$file = is_file($root) ? $root : $envPath;
|
||||
$emailtemplates = [];
|
||||
if (is_file($file)) {
|
||||
/** @noinspection PhpIncludeInspection */
|
||||
$emailtemplates = include $file;
|
||||
}
|
||||
return is_array($emailtemplates ?? null) ? $emailtemplates : [];
|
||||
}
|
||||
|
||||
private function renderTemplate(string $key, array $vars): array
|
||||
{
|
||||
$templates = $this->templates();
|
||||
$id = $templates[$key] ?? $key;
|
||||
|
||||
$apiBase = getenv('EMAILTEMPLATE_API_BASE') ?: '';
|
||||
$apiToken = getenv('EMAILTEMPLATE_API_TOKEN') ?: '';
|
||||
|
||||
if ($apiBase && $apiToken) {
|
||||
$payload = [
|
||||
'template' => $id,
|
||||
'placeholders' => $vars,
|
||||
];
|
||||
$payload['token'] = $apiToken;
|
||||
$ctx = stream_context_create([
|
||||
'http' => [
|
||||
'method' => 'POST',
|
||||
'header' => "Content-Type: application/json\r\n",
|
||||
'timeout' => 15,
|
||||
'content' => json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||||
],
|
||||
]);
|
||||
$resp = @file_get_contents($apiBase, false, $ctx);
|
||||
if ($resp !== false) {
|
||||
$decoded = json_decode($resp, true);
|
||||
if (is_array($decoded) && !empty($decoded['ok']) && !empty($decoded['html'])) {
|
||||
return [
|
||||
'subject' => $decoded['subject'] ?? 'Papakind',
|
||||
'html' => $decoded['html'],
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: einfacher Text
|
||||
$subject = 'Papa-Kind-Treff';
|
||||
$body = $id;
|
||||
foreach ($vars as $k => $v) {
|
||||
$body = str_replace(['{' . $k . '}', '{{' . $k . '}}'], (string)$v, $body);
|
||||
}
|
||||
return [
|
||||
'subject' => $subject,
|
||||
'html' => nl2br(htmlspecialchars($body, ENT_QUOTES)),
|
||||
];
|
||||
}
|
||||
|
||||
public function sendTemplate(string $templateKey, string $to, array $vars = []): void
|
||||
{
|
||||
if (!filter_var($to, FILTER_VALIDATE_EMAIL)) {
|
||||
throw new \InvalidArgumentException('Invalid recipient email.');
|
||||
}
|
||||
|
||||
$tpl = $this->renderTemplate($templateKey, $vars);
|
||||
$subject = $tpl['subject'] ?? 'Papa-Kind-Treff';
|
||||
$html = $tpl['html'] ?? '';
|
||||
|
||||
$transport = getenv('MAIL_TRANSPORT') ?: 'mail';
|
||||
$fromEmail = getenv('MAIL_FROM') ?: 'no-reply@' . $this->app->config()->primaryDomain;
|
||||
$fromName = getenv('MAIL_FROM_NAME') ?: 'Papa-Kind-Treff';
|
||||
|
||||
if ($transport === 'smtp') {
|
||||
$this->sendSmtp($to, $subject, $html, $fromEmail, $fromName);
|
||||
} else {
|
||||
$this->sendMailFn($to, $subject, $html, $fromEmail, $fromName);
|
||||
}
|
||||
}
|
||||
|
||||
private function sendMailFn(string $to, string $subject, string $html, string $from, string $fromName): void
|
||||
{
|
||||
$headers = [];
|
||||
if ($from) {
|
||||
$headers[] = 'From: ' . sprintf('"%s" <%s>', addslashes($fromName), $from);
|
||||
}
|
||||
$headers[] = 'Content-Type: text/html; charset=utf-8';
|
||||
if (!@mail($to, $subject, $html, implode("\r\n", $headers))) {
|
||||
throw new \RuntimeException('mail() transport failed');
|
||||
}
|
||||
}
|
||||
|
||||
private function sendSmtp(string $to, string $subject, string $html, string $from, string $fromName): void
|
||||
{
|
||||
$host = getenv('SMTP_HOST') ?: '';
|
||||
$port = (int)(getenv('SMTP_PORT') ?: 587);
|
||||
$user = getenv('SMTP_USER') ?: '';
|
||||
$pass = getenv('SMTP_PASS') ?: '';
|
||||
$secure = strtolower(getenv('SMTP_SECURE') ?: 'tls'); // tls|ssl|none
|
||||
|
||||
if (!$host) {
|
||||
$this->sendMailFn($to, $subject, $html, $from, $fromName);
|
||||
return;
|
||||
}
|
||||
|
||||
$proto = ($secure === 'ssl') ? 'ssl://' : '';
|
||||
$fp = @stream_socket_client($proto . $host . ':' . $port, $errno, $errstr, 15, STREAM_CLIENT_CONNECT);
|
||||
if (!$fp) {
|
||||
$this->sendMailFn($to, $subject, $html, $from, $fromName);
|
||||
return;
|
||||
}
|
||||
stream_set_timeout($fp, 15);
|
||||
|
||||
$read = function () use ($fp) {
|
||||
return fgets($fp, 515);
|
||||
};
|
||||
$write = function (string $cmd) use ($fp) {
|
||||
fwrite($fp, $cmd . "\r\n");
|
||||
};
|
||||
|
||||
$read();
|
||||
$write('EHLO ' . $this->app->config()->primaryDomain);
|
||||
$read();
|
||||
|
||||
if ($secure === 'tls') {
|
||||
$write('STARTTLS');
|
||||
$read();
|
||||
if (!stream_socket_enable_crypto($fp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
|
||||
fclose($fp);
|
||||
$this->sendMailFn($to, $subject, $html, $from, $fromName);
|
||||
return;
|
||||
}
|
||||
$write('EHLO ' . $this->app->config()->primaryDomain);
|
||||
$read();
|
||||
}
|
||||
|
||||
if ($user !== '') {
|
||||
$write('AUTH LOGIN');
|
||||
$read();
|
||||
$write(base64_encode($user));
|
||||
$read();
|
||||
$write(base64_encode($pass));
|
||||
$read();
|
||||
}
|
||||
|
||||
$write('MAIL FROM: <' . $from . '>');
|
||||
$read();
|
||||
$write('RCPT TO: <' . $to . '>');
|
||||
$read();
|
||||
$write('DATA');
|
||||
$read();
|
||||
|
||||
$msg = "From: {$fromName} <{$from}>\r\n";
|
||||
$msg .= "To: <{$to}>\r\n";
|
||||
$msg .= "Subject: {$subject}\r\n";
|
||||
$msg .= "MIME-Version: 1.0\r\n";
|
||||
$msg .= "Content-Type: text/html; charset=utf-8\r\n\r\n";
|
||||
$msg .= $html . "\r\n.\r\n";
|
||||
$write($msg);
|
||||
$read();
|
||||
$write('QUIT');
|
||||
fclose($fp);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user