Added ability to send registrar notifications, fixed #166

Also fixed msg_producer (again)
This commit is contained in:
Pinga 2025-02-12 01:02:47 +02:00
parent 6968bfafa2
commit 5711546f78
5 changed files with 244 additions and 17 deletions

View file

@ -37,13 +37,13 @@ class RedisPool {
*/
public function initialize(int $size = 10): void {
for ($i = 0; $i < $size; $i++) {
// Create a coroutine for each connection.
Swoole\Coroutine\run(function () {
go(function () {
$redis = new Redis();
if (!$redis->connect($this->host, $this->port)) {
throw new Exception("Failed to connect to Redis at {$this->host}:{$this->port}");
}
$this->pool->push($redis);
echo "Added Redis connection to pool\n"; // Debugging log
});
}
}
@ -52,7 +52,7 @@ class RedisPool {
* Get a Redis connection from the pool.
* Optionally, you can add a timeout to avoid indefinite blocking.
*/
public function get(float $timeout = 1.0): Redis {
public function get(float $timeout = 2.0): Redis {
$conn = $this->pool->pop($timeout);
if (!$conn) {
throw new Exception("No available Redis connection in pool");
@ -63,13 +63,13 @@ class RedisPool {
/**
* Return a Redis connection back to the pool.
*/
public function put(Redis $redis): void {
$this->pool->push($redis);
public function put(?Redis $redis): void {
if ($redis && $redis->isConnected()) {
$this->pool->push($redis);
}
}
}
// Global RedisPool instance
$redisPool = new RedisPool('127.0.0.1', 6379, 10);
}
// Create the Swoole HTTP server
$server = new Server("127.0.0.1", 8250);
@ -80,24 +80,28 @@ $server->set([
'log_file' => '/var/log/namingo/msg_producer.log',
'log_level' => SWOOLE_LOG_INFO,
'worker_num' => swoole_cpu_num() * 2,
'pid_file' => '/var/run/msg_producer.pid'
'pid_file' => '/var/run/msg_producer.pid',
'enable_coroutine' => true
]);
/**
* Instead of initializing the Redis pool in the "start" event (which runs in the master process),
* we initialize it in the "workerStart" event so that it runs in a coroutine-enabled worker process.
*/
$server->on("workerStart", function () use ($redisPool, $logger) {
$server->on("workerStart", function ($server, $workerId) use (&$logger) {
try {
$redisPool->initialize(10);
$logger->info("Redis pool initialized in worker process");
$server->redisPool = new RedisPool('127.0.0.1', 6379, 10); // Store in server object
$server->redisPool->initialize(10);
$logger->info("Redis pool initialized in worker process {$workerId}");
} catch (Exception $e) {
$logger->error("Failed to initialize Redis pool: " . $e->getMessage());
$logger->error("Worker {$workerId}: Failed to initialize Redis pool - " . $e->getMessage());
}
});
// Handle incoming requests
$server->on("request", function (Request $request, Response $response) use ($redisPool, $logger) {
$server->on("request", function (Request $request, Response $response) use ($server, $logger) {
$redisPool = $server->redisPool ?? null;
if (!$redisPool) {
$logger->error("Redis pool not initialized");
$response->status(500);

View file

@ -1467,4 +1467,129 @@ class RegistrarsController extends Controller
Auth::leaveImpersonation();
}
public function notifyRegistrars(Request $request, Response $response)
{
if ($_SESSION["auth_roles"] != 0) {
return $response->withHeader('Location', '/dashboard')->withStatus(302);
}
if ($request->getMethod() === 'POST') {
// Retrieve POST data
$data = $request->getParsedBody();
$db = $this->container->get('db');
// Ensure registrars array exists and is not empty
if (!isset($data['registrars']) || empty($data['registrars'])) {
$this->container->get('flash')->addMessage('error', 'No registrars selected');
return $response->withHeader('Location', '/registrars/notify')->withStatus(302);
}
$registrars = $data['registrars']; // Array of registrar IDs
$subject = isset($data['subject']) && is_string($data['subject']) ? trim($data['subject']) : 'No subject';
$message = isset($data['message']) && is_string($data['message']) ? trim($data['message']) : 'No message';
// Enforce length limits
$subjectMaxLength = 255;
$messageMaxLength = 5000;
if (strlen($subject) > $subjectMaxLength) {
$subject = substr($subject, 0, $subjectMaxLength);
}
if (strlen($message) > $messageMaxLength) {
$message = substr($message, 0, $messageMaxLength);
}
// Escape HTML to prevent XSS if displaying in HTML later
$subject = htmlspecialchars($subject, ENT_QUOTES, 'UTF-8');
$message = htmlspecialchars($message, ENT_QUOTES, 'UTF-8');
$url = 'http://127.0.0.1:8250';
// Retrieve registrar names from database
$registrarNames = [];
$registrarEmails = [];
$placeholders = implode(',', array_fill(0, count($registrars), '?'));
$rows = $db->select(
"SELECT id, name, email FROM registrar WHERE id IN ($placeholders)",
$registrars
);
foreach ($rows as $row) {
$registrarNames[$row['id']] = $row['name'];
$registrarEmails[$row['id']] = $row['email'];
}
$notifiedRegistrars = [];
foreach ($registrars as $registrarId) {
$data = [
'type' => 'sendmail',
'toEmail' => $registrarEmails[$registrarId] ?? null,
'subject' => $subject,
'body' => $message
];
$jsonData = json_encode($data);
$options = [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_POSTFIELDS => $jsonData,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Content-Length: ' . strlen($jsonData)
],
];
$curl = curl_init($url);
curl_setopt_array($curl, $options);
$curlResponse = curl_exec($curl);
if ($curlResponse === false) {
$this->container->get('flash')->addMessage('error', 'cURL Error: ' . curl_error($curl));
curl_close($curl);
return $response->withHeader('Location', '/registrars/notify')->withStatus(302);
} else {
$notifiedRegistrars[] = $registrarNames[$registrarId] ?? "Registrar ID: $registrarId";
}
curl_close($curl);
}
// Create success message with registrar names
$successMessage = "Notification sent to: " . implode(', ', $notifiedRegistrars);
$this->container->get('flash')->addMessage('success', $successMessage);
return $response->withHeader('Location', '/registrars/notify')->withStatus(302);
} else {
// Prepare the view
$db = $this->container->get('db');
$uri = $request->getUri()->getPath();
// Get all registrars
$registrars = $db->select("SELECT id, clid, name, email, abuse_email FROM registrar");
// Fetch last login for each registrar
foreach ($registrars as &$registrar) {
// Get the latest user_id associated with the registrar
$user_id = $db->selectValue("SELECT user_id FROM registrar_users WHERE registrar_id = ? ORDER BY user_id DESC LIMIT 1", [$registrar['id']]);
// Fetch last login time if user_id exists
if ($user_id) {
$last_login = $db->selectValue("SELECT last_login FROM users WHERE id = ?", [$user_id]);
$registrar['last_login'] = ($last_login && is_numeric($last_login)) ? date('Y-m-d H:i:s', $last_login) : null;
} else {
$registrar['last_login'] = null;
}
}
// Default view for GET requests or if POST data is not set
return view($response,'admin/registrars/notifyRegistrars.twig', [
'registrars' => $registrars,
'currentUri' => $uri,
]);
}
}
}

View file

@ -0,0 +1,93 @@
{% extends "layouts/app.twig" %}
{% block title %}{{ __('Notify Registrars') }}{% endblock %}
{% block content %}
<div class="page-wrapper">
<!-- Page header -->
<div class="page-header d-print-none">
<div class="container-xl">
<div class="row g-2 align-items-center">
<div class="col">
<!-- Page pre-title -->
<div class="page-pretitle">
{{ __('Overview') }}
</div>
<h2 class="page-title">
{{ __('Notify Registrars') }}
</h2>
</div>
</div>
</div>
</div>
<!-- Page body -->
<div class="page-body">
<div class="container-xl">
<div class="col-12">
{% include 'partials/flash.twig' %}
<div class="card">
<div class="card-header">
<h3 class="card-title">{{ __('Send Registrar Notification') }}</h3>
</div>
<div class="card-body">
<form method="post" action="/registrars/notify">
{{ csrf.field | raw }}
<!-- Select Registrars -->
<div class="mb-3">
<label class="form-label"><strong>{{ __('Select Registrars') }}</strong></label>
<div class="card p-2">
<div class="d-flex justify-content-between mb-2">
<button type="button" class="btn btn-outline-primary btn-sm" id="selectAll">{{ __('Select All') }}</button>
<button type="button" class="btn btn-outline-secondary btn-sm" id="deselectAll">{{ __('Deselect All') }}</button>
</div>
<div class="row">
{% for registrar in registrars %}
<div class="col-md-6">
<div class="form-check">
<input class="form-check-input registrar-checkbox" type="checkbox" name="registrars[]" value="{{ registrar.id }}" id="registrar{{ registrar.id }}">
<label class="form-check-label" for="registrar{{ registrar.id }}">
<strong>{{ registrar.name }}</strong> ({{ registrar.clid }})
<br><small class="text-muted">{{ registrar.email }}</small>
<br><small class="text-muted">{{ __('Last Login:') }} {{ registrar.last_login is not null ? registrar.last_login : 'Not available' }}</small>
</label>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
<!-- Subject Field -->
<div class="mb-3">
<label class="form-label">{{ __('Subject') }}</label>
<input type="text" name="subject" class="form-control" placeholder="{{ __('Enter subject') }}" required>
</div>
<!-- Message Field -->
<div class="mb-3">
<label class="form-label">{{ __('Message') }}</label>
<textarea name="message" class="form-control" rows="4" placeholder="{{ __('Type your message...') }}" required></textarea>
</div>
<!-- Submit Button -->
<div class="text-end">
<button type="submit" class="btn btn-primary">{{ __('Send Notification') }}</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{% include 'partials/footer.twig' %}
</div>
<script>
document.getElementById("selectAll").addEventListener("click", function() {
document.querySelectorAll(".registrar-checkbox").forEach(el => el.checked = true);
});
document.getElementById("deselectAll").addEventListener("click", function() {
document.querySelectorAll(".registrar-checkbox").forEach(el => el.checked = false);
});
</script>
{% endblock %}

View file

@ -6,7 +6,7 @@
<meta http-equiv="X-UA-Compatible" content="ie=edge"/>
<title>{% block title %}{% endblock %} | Namingo</title>
<!-- CSS files -->
{% if route_is('domains') or route_is('applications') or route_is('contacts') or route_is('hosts') or route_is('epphistory') or route_is('registrars') or route_is('transactions') or route_is('overview') or route_is('reports') or route_is('transfers') or route_is('users') or is_current_url('ticketview') or route_is('poll') or route_is('log') or route_is('invoices') or route_is('registry/tlds') or route_is('profile') %}{% include 'partials/css-tables.twig' %}{% else %}{% include 'partials/css.twig' %}{% endif %}
{% if route_is('domains') or route_is('applications') or route_is('contacts') or route_is('hosts') or route_is('epphistory') or is_current_url('registrars') or route_is('transactions') or route_is('overview') or route_is('reports') or route_is('transfers') or route_is('users') or is_current_url('ticketview') or route_is('poll') or route_is('log') or route_is('invoices') or route_is('registry/tlds') or route_is('profile') %}{% include 'partials/css-tables.twig' %}{% else %}{% include 'partials/css.twig' %}{% endif %}
</head>
<body{% if screen_mode == 'dark' %} data-bs-theme="dark"{% endif %}>
<div class="page">
@ -147,7 +147,7 @@
</a>
</div>
</li>
{% if roles == 0 %}<li {{ is_current_url('registrars') or is_current_url('listUsers') or is_current_url('transferRegistrar') or is_current_url('createUser') or is_current_url('registrarcreate') or 'user/update/' in currentUri or 'registrar/' in currentUri ? 'class="nav-item dropdown active"' : 'class="nav-item dropdown"' }}>
{% if roles == 0 %}<li {{ is_current_url('registrars') or is_current_url('listUsers') or is_current_url('transferRegistrar') or is_current_url('createUser') or is_current_url('registrarcreate') or is_current_url('notifyRegistrars') or 'user/update/' in currentUri or 'registrar/' in currentUri ? 'class="nav-item dropdown active"' : 'class="nav-item dropdown"' }}>
<a class="nav-link dropdown-toggle" href="#" data-bs-toggle="dropdown" data-bs-auto-close="outside" role="button" aria-expanded="false">
<span class="nav-link-icon d-md-none d-lg-inline-block"><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M12 13a3 3 0 1 0 0 -6a3 3 0 0 0 0 6z"></path><path d="M12 3c7.2 0 9 1.8 9 9s-1.8 9 -9 9s-9 -1.8 -9 -9s1.8 -9 9 -9z"></path><path d="M6 20.05v-.05a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4v.05"></path></svg>
</span>
@ -163,6 +163,10 @@
{{ __('Create Registrar') }}
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="{{route('notifyRegistrars')}}">
{{ __('Notify Registrars') }}
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="{{route('listUsers')}}">
{{ __('List Users') }}
</a>
@ -275,7 +279,7 @@
{% include 'partials/js-hosts.twig' %}
{% elseif route_is('epphistory') %}
{% include 'partials/js-logs.twig' %}
{% elseif route_is('registrars') %}
{% elseif is_current_url('registrars') %}
{% include 'partials/js-registrars.twig' %}
{% elseif route_is('transactions') %}
{% include 'partials/js-transactions.twig' %}

View file

@ -100,6 +100,7 @@ $app->group('', function ($route) {
$route->map(['GET', 'POST'], '/registrar/process', RegistrarsController::class . ':transferRegistrarProcess')->setName('transferRegistrarProcess');
$route->get('/registrar/impersonate/{registrar}', RegistrarsController::class . ':impersonateRegistrar')->setName('impersonateRegistrar');
$route->get('/leave_impersonation', RegistrarsController::class . ':leave_impersonation')->setName('leave_impersonation');
$route->map(['GET', 'POST'], '/registrars/notify', RegistrarsController::class .':notifyRegistrars')->setName('notifyRegistrars');
$route->get('/users', UsersController::class .':listUsers')->setName('listUsers');
$route->map(['GET', 'POST'], '/user/create', UsersController::class . ':createUser')->setName('createUser');