false, 'error' => 'rate_limited']; } $emailNorm = self::normalizeEmail($email); if ($emailNorm === '' || !filter_var($emailNorm, FILTER_VALIDATE_EMAIL)) { AuthAttempts::record('register_fail', $username ?: $email); return ['ok' => false, 'error' => 'invalid_email']; } if (strlen($password) < 10) { AuthAttempts::record('register_fail', $username ?: $email); return ['ok' => false, 'error' => 'weak_password']; } $username = trim($username); if ($username === '') { $username = strstr($emailNorm, '@', true) ?: $emailNorm; } $pdo = Database::pdo(); $st = $pdo->prepare('SELECT id FROM users WHERE email_normalized = ? OR username = ? LIMIT 1'); $st->execute([$emailNorm, $username]); if ($st->fetchColumn()) { AuthAttempts::record('register_fail', $username); return ['ok' => false, 'error' => 'already_registered']; } $hash = password_hash($password, PASSWORD_DEFAULT); $pdo->prepare( 'INSERT INTO users (username, password_hash, role, email_normalized, status) VALUES (?, ?, ?, ?, ?)' )->execute([ $username, $hash, 'viewer', $emailNorm, 'pending', ]); $userId = (int) $pdo->lastInsertId(); $token = bin2hex(random_bytes(32)); $tokenHash = hash('sha256', $token); $expires = date('Y-m-d H:i:s', time() + self::TOKEN_TTL_S); $pdo->prepare( 'INSERT INTO auth_tokens (user_id, purpose, token_hash, email_normalized, expires_at, created_at) VALUES (?, ?, ?, ?, ?, ?)' )->execute([$userId, 'verify_email', $tokenHash, $emailNorm, $expires, date('Y-m-d H:i:s')]); $base = rtrim((string) cfg('base_path', ''), '/'); $verifyUrl = 'https://apps.f0xx.org' . $base . '/verify-email?token=' . urlencode($token); if (!AuthMailer::sendVerifyEmail($emailNorm, $verifyUrl)) { error_log('AuthRegistration: verify mail failed for ' . $emailNorm); } AuthAttempts::record('register_ok', $username); return ['ok' => true]; } /** @return array{ok:bool,error?:string} */ public static function verifyEmailToken(string $token): array { self::ensureSchema(); $token = trim($token); if ($token === '') { return ['ok' => false, 'error' => 'missing_token']; } $hash = hash('sha256', $token); $pdo = Database::pdo(); $st = $pdo->prepare( "SELECT * FROM auth_tokens WHERE token_hash = ? AND purpose = 'verify_email' AND used_at IS NULL LIMIT 1" ); $st->execute([$hash]); $row = $st->fetch(PDO::FETCH_ASSOC); if (!$row) { return ['ok' => false, 'error' => 'invalid_token']; } $exp = strtotime((string) ($row['expires_at'] ?? '')); if ($exp !== false && $exp < time()) { return ['ok' => false, 'error' => 'expired_token']; } $userId = (int) ($row['user_id'] ?? 0); $now = date('Y-m-d H:i:s'); $pdo->prepare('UPDATE auth_tokens SET used_at = ? WHERE id = ?')->execute([$now, (int) $row['id']]); $pdo->prepare( "UPDATE users SET email_verified_at = ?, status = 'active' WHERE id = ?" )->execute([$now, $userId]); return ['ok' => true]; } private static function normalizeEmail(string $email): string { return strtolower(trim($email)); } }