requireSuper(); $first = trim($input['user_first_name'] ?? ''); $last = trim($input['user_last_name'] ?? ''); $email = trim($input['user_email'] ?? ''); $name = trim($input['user_name'] ?? ''); $role = $input['user_role'] ?? ''; if (!$first || !$last || strlen($first) > 128 || strlen($last) > 128 || !filter_var($email, FILTER_VALIDATE_EMAIL) || strlen($email) > 100 || !preg_match('/\A[A-Za-z0-9_.-]{3,64}\z/', $name) || !in_array($role, ['User', 'Guest', 'Administrator', TenantAccess::SUPER], true)) throw new AppException('Enter a valid username, first/last name, email and role.'); $db = $this->connect(); $stmt = $db->prepare('SELECT user_serial FROM users WHERE user_name IN (?,?) OR user_email IN (?,?)'); $stmt->execute([$name, $email, $email, $name]); if ($stmt->fetch()) throw new AppException('An account already uses those details. Use the existing account or resend its invitation.', 409); $db->beginTransaction(); try { $stmt = $db->prepare("INSERT INTO users(user_name,user_first_name,user_last_name,user_email,user_password_hash,user_role,user_status,user_change_password,user_creator) VALUES(?,?,?,?,?,?,'active',1,?)"); $stmt->execute([$name, $first, $last, $email, password_hash(bin2hex(random_bytes(24)), PASSWORD_DEFAULT), $role, $_SESSION[self::USER_NAME] ?? '']); $id = (int) $db->lastInsertId(); Audit::required($db, (int) ($_SESSION[self::USER_SERIAL] ?? 0), 'created account ' . json_encode(['user' => $id, 'name' => $name, 'role' => $role])); $db->commit(); } catch (Throwable $e) { if ($db->inTransaction()) $db->rollBack();throw $e; } $warning = ''; try { if (!(new AuthService())->requestReset($email)) $warning = 'Account created, but the invitation is rate limited. Open the user summary to resend it later.'; } catch (Throwable $e) { error_log('Invitation delivery failed: ' . $e->getMessage()); $warning = 'Account created, but its invitation could not be sent. Open the user summary to resend it after checking email settings.'; } return ['ok' => true, 'user_serial' => $id, 'message' => 'Account created.', 'warning' => $warning]; } }