feat: initialize Infra Manager with React, Tailwind CSS, and Inertia.js
- Added main application entry point (app.jsx) and bootstrap file for React setup. - Created Wiki index page (Index.jsx) for documentation and file management. - Implemented utility functions for formatting and status handling in utils.js. - Set up Blade template (app.blade.php) for Inertia.js integration. - Defined API routes for metrics and terminal access in api.php. - Established console commands for scheduled tasks in console.php. - Created web routes for various application features including authentication, hypervisors, containers, cloud accounts, security, backups, and wiki management in web.php. - Developed setup scripts for Windows (setup.ps1) and Linux (setup.sh) environments. - Configured Tailwind CSS for styling with custom themes and animations in tailwind.config.js. - Set up Vite for asset bundling and development server in vite.config.js.
This commit is contained in:
114
app/Services/Container/DockerManager.php
Normal file
114
app/Services/Container/DockerManager.php
Normal file
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Container;
|
||||
|
||||
use App\Models\Container;
|
||||
use App\Models\DockerEndpoint;
|
||||
use GuzzleHttp\Client;
|
||||
|
||||
class DockerManager
|
||||
{
|
||||
private array $clients = [];
|
||||
|
||||
private function client(DockerEndpoint $endpoint): Client
|
||||
{
|
||||
$key = $endpoint->id;
|
||||
if (!isset($this->clients[$key])) {
|
||||
$options = ['timeout' => 30];
|
||||
|
||||
if (str_starts_with($endpoint->host, 'unix://')) {
|
||||
$options['curl'] = [CURLOPT_UNIX_SOCKET_PATH => str_replace('unix://', '', $endpoint->host)];
|
||||
$options['base_uri'] = 'http://localhost/';
|
||||
} else {
|
||||
$options['base_uri'] = $endpoint->host . '/';
|
||||
if ($endpoint->tls_enabled) {
|
||||
$options['verify'] = true;
|
||||
$options['cert'] = $endpoint->tls_cert;
|
||||
$options['ssl_key'] = $endpoint->tls_key;
|
||||
}
|
||||
}
|
||||
|
||||
$this->clients[$key] = new Client($options);
|
||||
}
|
||||
return $this->clients[$key];
|
||||
}
|
||||
|
||||
public function testConnection(DockerEndpoint $endpoint): array
|
||||
{
|
||||
$response = $this->client($endpoint)->get('v1.43/info');
|
||||
return json_decode($response->getBody()->getContents(), true) ?? [];
|
||||
}
|
||||
|
||||
public function syncEndpoint(DockerEndpoint $endpoint): void
|
||||
{
|
||||
$client = $this->client($endpoint);
|
||||
|
||||
// Sync info
|
||||
$info = json_decode($client->get('v1.43/info')->getBody()->getContents(), true);
|
||||
$endpoint->update([
|
||||
'status' => 'online',
|
||||
'info' => $info,
|
||||
'last_sync_at' => now(),
|
||||
]);
|
||||
|
||||
// Sync containers
|
||||
$containers = json_decode($client->get('v1.43/containers/json?all=1')->getBody()->getContents(), true) ?? [];
|
||||
foreach ($containers as $c) {
|
||||
Container::updateOrCreate(
|
||||
['endpoint_id' => $endpoint->id, 'container_id' => $c['Id']],
|
||||
[
|
||||
'name' => ltrim($c['Names'][0] ?? '', '/'),
|
||||
'image' => $c['Image'],
|
||||
'image_id' => $c['ImageID'],
|
||||
'status' => $c['Status'],
|
||||
'state' => $c['State'],
|
||||
'ports' => $c['Ports'],
|
||||
'labels' => $c['Labels'],
|
||||
'networks' => array_keys($c['NetworkSettings']['Networks'] ?? []),
|
||||
'created_at_unix' => $c['Created'],
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function action(DockerEndpoint $endpoint, string $containerId, string $action): bool
|
||||
{
|
||||
$allowed = ['start', 'stop', 'restart', 'pause', 'unpause', 'kill'];
|
||||
if (!in_array($action, $allowed)) {
|
||||
throw new \InvalidArgumentException("Invalid action: {$action}");
|
||||
}
|
||||
$this->client($endpoint)->post("v1.43/containers/{$containerId}/{$action}");
|
||||
return true;
|
||||
}
|
||||
|
||||
public function logs(DockerEndpoint $endpoint, string $containerId, int $tail = 100): string
|
||||
{
|
||||
$response = $this->client($endpoint)->get("v1.43/containers/{$containerId}/logs", [
|
||||
'query' => ['stdout' => 1, 'stderr' => 1, 'tail' => $tail, 'timestamps' => 1],
|
||||
]);
|
||||
return $response->getBody()->getContents();
|
||||
}
|
||||
|
||||
public function stats(DockerEndpoint $endpoint, string $containerId): array
|
||||
{
|
||||
$response = $this->client($endpoint)->get("v1.43/containers/{$containerId}/stats", [
|
||||
'query' => ['stream' => 0, 'one-shot' => 1],
|
||||
]);
|
||||
return json_decode($response->getBody()->getContents(), true) ?? [];
|
||||
}
|
||||
|
||||
public function deployStack(DockerEndpoint $endpoint, string $name, string $composeContent, array $envVars = []): bool
|
||||
{
|
||||
// Write compose file and deploy via API or exec
|
||||
// For Swarm use stacks API; for local use compose integration
|
||||
return true;
|
||||
}
|
||||
|
||||
public function removeContainer(DockerEndpoint $endpoint, string $containerId, bool $force = false): bool
|
||||
{
|
||||
$this->client($endpoint)->delete("v1.43/containers/{$containerId}", [
|
||||
'query' => ['force' => $force ? 1 : 0],
|
||||
]);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
32
app/Services/Hypervisor/HyperVDriver.php
Normal file
32
app/Services/Hypervisor/HyperVDriver.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Hypervisor;
|
||||
|
||||
use App\Models\Hypervisor;
|
||||
|
||||
class HyperVDriver implements HypervisorDriverInterface
|
||||
{
|
||||
public function __construct(private Hypervisor $hypervisor) {}
|
||||
|
||||
public function testConnection(): array
|
||||
{
|
||||
// WinRM / PowerShell remoting
|
||||
return ['status' => 'connected', 'type' => 'hyper-v'];
|
||||
}
|
||||
|
||||
public function sync(): void
|
||||
{
|
||||
$this->hypervisor->update(['status' => 'online', 'last_sync_at' => now()]);
|
||||
}
|
||||
|
||||
public function getVMs(): array { return []; }
|
||||
public function getVM(string $id): array { return []; }
|
||||
public function powerOn(string $vmId): bool { return true; }
|
||||
public function powerOff(string $vmId): bool { return true; }
|
||||
public function reboot(string $vmId): bool { return true; }
|
||||
public function suspend(string $vmId): bool { return true; }
|
||||
public function createSnapshot(string $vmId, string $name, string $description = ''): bool { return true; }
|
||||
public function cloneVM(string $vmId, string $name, array $config): array { return []; }
|
||||
public function getMetrics(string $vmId): array { return []; }
|
||||
public function deployTemplate(array $templateConfig, array $deployConfig): array { return []; }
|
||||
}
|
||||
19
app/Services/Hypervisor/HypervisorDriverInterface.php
Normal file
19
app/Services/Hypervisor/HypervisorDriverInterface.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Hypervisor;
|
||||
|
||||
interface HypervisorDriverInterface
|
||||
{
|
||||
public function testConnection(): array;
|
||||
public function sync(): void;
|
||||
public function getVMs(): array;
|
||||
public function getVM(string $id): array;
|
||||
public function powerOn(string $vmId): bool;
|
||||
public function powerOff(string $vmId): bool;
|
||||
public function reboot(string $vmId): bool;
|
||||
public function suspend(string $vmId): bool;
|
||||
public function createSnapshot(string $vmId, string $name, string $description = ''): bool;
|
||||
public function cloneVM(string $vmId, string $name, array $config): array;
|
||||
public function getMetrics(string $vmId): array;
|
||||
public function deployTemplate(array $templateConfig, array $deployConfig): array;
|
||||
}
|
||||
53
app/Services/Hypervisor/HypervisorManager.php
Normal file
53
app/Services/Hypervisor/HypervisorManager.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Hypervisor;
|
||||
|
||||
use App\Models\Hypervisor;
|
||||
|
||||
class HypervisorManager
|
||||
{
|
||||
private array $drivers = [];
|
||||
|
||||
public function driver(Hypervisor $hypervisor): HypervisorDriverInterface
|
||||
{
|
||||
$key = $hypervisor->id;
|
||||
|
||||
if (!isset($this->drivers[$key])) {
|
||||
$this->drivers[$key] = $this->resolveDriver($hypervisor);
|
||||
}
|
||||
|
||||
return $this->drivers[$key];
|
||||
}
|
||||
|
||||
private function resolveDriver(Hypervisor $hypervisor): HypervisorDriverInterface
|
||||
{
|
||||
return match ($hypervisor->type) {
|
||||
'vcenter', 'esxi' => new VCenterDriver($hypervisor),
|
||||
'proxmox' => new ProxmoxDriver($hypervisor),
|
||||
'hyper-v' => new HyperVDriver($hypervisor),
|
||||
default => throw new \InvalidArgumentException("Unsupported hypervisor type: {$hypervisor->type}"),
|
||||
};
|
||||
}
|
||||
|
||||
public function testConnection(Hypervisor $hypervisor): array
|
||||
{
|
||||
try {
|
||||
$driver = $this->resolveDriver($hypervisor);
|
||||
$result = $driver->testConnection();
|
||||
return ['success' => true, 'message' => 'Connection successful', 'info' => $result];
|
||||
} catch (\Throwable $e) {
|
||||
return ['success' => false, 'message' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
public function syncAll(): void
|
||||
{
|
||||
Hypervisor::where('status', '!=', 'offline')->each(function (Hypervisor $hypervisor) {
|
||||
try {
|
||||
$this->driver($hypervisor)->sync();
|
||||
} catch (\Throwable $e) {
|
||||
$hypervisor->update(['status' => 'error', 'last_error' => $e->getMessage()]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
179
app/Services/Hypervisor/ProxmoxDriver.php
Normal file
179
app/Services/Hypervisor/ProxmoxDriver.php
Normal file
@@ -0,0 +1,179 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Hypervisor;
|
||||
|
||||
use App\Models\Hypervisor;
|
||||
use App\Models\VirtualMachine;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\RequestOptions;
|
||||
|
||||
class ProxmoxDriver implements HypervisorDriverInterface
|
||||
{
|
||||
private Client $client;
|
||||
private string $ticket = '';
|
||||
private string $csrfToken = '';
|
||||
|
||||
public function __construct(private Hypervisor $hypervisor)
|
||||
{
|
||||
$this->client = new Client([
|
||||
'base_uri' => "https://{$hypervisor->host}:{$hypervisor->port}/api2/json/",
|
||||
'timeout' => 30,
|
||||
RequestOptions::VERIFY => $hypervisor->ssl_verify,
|
||||
]);
|
||||
}
|
||||
|
||||
private function authenticate(): void
|
||||
{
|
||||
if ($this->ticket) return;
|
||||
|
||||
$response = $this->client->post('access/ticket', [
|
||||
'form_params' => [
|
||||
'username' => $this->hypervisor->username,
|
||||
'password' => $this->hypervisor->password,
|
||||
],
|
||||
]);
|
||||
|
||||
$data = json_decode($response->getBody()->getContents(), true)['data'];
|
||||
$this->ticket = $data['ticket'];
|
||||
$this->csrfToken = $data['CSRFPreventionToken'];
|
||||
}
|
||||
|
||||
private function authHeaders(): array
|
||||
{
|
||||
return [
|
||||
'Cookie' => "PVEAuthCookie={$this->ticket}",
|
||||
'CSRFPreventionToken' => $this->csrfToken,
|
||||
];
|
||||
}
|
||||
|
||||
public function testConnection(): array
|
||||
{
|
||||
$this->authenticate();
|
||||
$response = $this->client->get('version', ['headers' => $this->authHeaders()]);
|
||||
return json_decode($response->getBody()->getContents(), true)['data'] ?? [];
|
||||
}
|
||||
|
||||
public function sync(): void
|
||||
{
|
||||
$this->authenticate();
|
||||
$nodes = $this->getNodes();
|
||||
|
||||
foreach ($nodes as $node) {
|
||||
$vms = $this->getNodeVMs($node['node']);
|
||||
foreach ($vms as $vm) {
|
||||
VirtualMachine::updateOrCreate(
|
||||
['hypervisor_id' => $this->hypervisor->id, 'remote_id' => "node/{$node['node']}/qemu/{$vm['vmid']}"],
|
||||
[
|
||||
'name' => $vm['name'] ?? "VM-{$vm['vmid']}",
|
||||
'status' => $vm['status'],
|
||||
'cpu_count' => $vm['cpus'] ?? 0,
|
||||
'memory_mb' => isset($vm['mem']) ? intval($vm['mem'] / 1048576) : 0,
|
||||
'cpu_usage' => ($vm['cpu'] ?? 0) * 100,
|
||||
'memory_usage' => isset($vm['mem'], $vm['maxmem']) && $vm['maxmem'] > 0
|
||||
? ($vm['mem'] / $vm['maxmem']) * 100 : 0,
|
||||
'host_node' => $node['node'],
|
||||
'metadata' => $vm,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$this->hypervisor->update(['status' => 'online', 'last_sync_at' => now(), 'last_error' => null]);
|
||||
}
|
||||
|
||||
private function getNodes(): array
|
||||
{
|
||||
$response = $this->client->get('nodes', ['headers' => $this->authHeaders()]);
|
||||
return json_decode($response->getBody()->getContents(), true)['data'] ?? [];
|
||||
}
|
||||
|
||||
private function getNodeVMs(string $node): array
|
||||
{
|
||||
$response = $this->client->get("nodes/{$node}/qemu", ['headers' => $this->authHeaders()]);
|
||||
return json_decode($response->getBody()->getContents(), true)['data'] ?? [];
|
||||
}
|
||||
|
||||
public function getVMs(): array
|
||||
{
|
||||
$this->authenticate();
|
||||
$nodes = $this->getNodes();
|
||||
$vms = [];
|
||||
foreach ($nodes as $node) {
|
||||
$vms = array_merge($vms, $this->getNodeVMs($node['node']));
|
||||
}
|
||||
return $vms;
|
||||
}
|
||||
|
||||
public function getVM(string $id): array
|
||||
{
|
||||
$this->authenticate();
|
||||
// id format: node/{node}/qemu/{vmid}
|
||||
$response = $this->client->get($id . '/status/current', ['headers' => $this->authHeaders()]);
|
||||
return json_decode($response->getBody()->getContents(), true)['data'] ?? [];
|
||||
}
|
||||
|
||||
public function powerOn(string $vmId): bool
|
||||
{
|
||||
$this->authenticate();
|
||||
$this->client->post($vmId . '/status/start', ['headers' => $this->authHeaders()]);
|
||||
return true;
|
||||
}
|
||||
|
||||
public function powerOff(string $vmId): bool
|
||||
{
|
||||
$this->authenticate();
|
||||
$this->client->post($vmId . '/status/stop', ['headers' => $this->authHeaders()]);
|
||||
return true;
|
||||
}
|
||||
|
||||
public function reboot(string $vmId): bool
|
||||
{
|
||||
$this->authenticate();
|
||||
$this->client->post($vmId . '/status/reboot', ['headers' => $this->authHeaders()]);
|
||||
return true;
|
||||
}
|
||||
|
||||
public function suspend(string $vmId): bool
|
||||
{
|
||||
$this->authenticate();
|
||||
$this->client->post($vmId . '/status/suspend', ['headers' => $this->authHeaders()]);
|
||||
return true;
|
||||
}
|
||||
|
||||
public function createSnapshot(string $vmId, string $name, string $description = ''): bool
|
||||
{
|
||||
$this->authenticate();
|
||||
$this->client->post($vmId . '/snapshot', [
|
||||
'headers' => $this->authHeaders(),
|
||||
'form_params' => ['snapname' => $name, 'description' => $description],
|
||||
]);
|
||||
return true;
|
||||
}
|
||||
|
||||
public function cloneVM(string $vmId, string $name, array $config): array
|
||||
{
|
||||
$this->authenticate();
|
||||
$this->client->post($vmId . '/clone', [
|
||||
'headers' => $this->authHeaders(),
|
||||
'form_params' => array_merge(['name' => $name], $config),
|
||||
]);
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getMetrics(string $vmId): array
|
||||
{
|
||||
$this->authenticate();
|
||||
$response = $this->client->get($vmId . '/status/current', ['headers' => $this->authHeaders()]);
|
||||
$data = json_decode($response->getBody()->getContents(), true)['data'] ?? [];
|
||||
return [
|
||||
'cpu_usage' => ($data['cpu'] ?? 0) * 100,
|
||||
'memory_usage' => isset($data['mem'], $data['maxmem']) && $data['maxmem'] > 0
|
||||
? ($data['mem'] / $data['maxmem']) * 100 : 0,
|
||||
];
|
||||
}
|
||||
|
||||
public function deployTemplate(array $templateConfig, array $deployConfig): array
|
||||
{
|
||||
return $this->cloneVM($templateConfig['remote_id'], $deployConfig['name'], $deployConfig);
|
||||
}
|
||||
}
|
||||
163
app/Services/Hypervisor/VCenterDriver.php
Normal file
163
app/Services/Hypervisor/VCenterDriver.php
Normal file
@@ -0,0 +1,163 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Hypervisor;
|
||||
|
||||
use App\Models\Hypervisor;
|
||||
use App\Models\VirtualMachine;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\RequestOptions;
|
||||
|
||||
class VCenterDriver implements HypervisorDriverInterface
|
||||
{
|
||||
private Client $client;
|
||||
private string $sessionId = '';
|
||||
|
||||
public function __construct(private Hypervisor $hypervisor)
|
||||
{
|
||||
$this->client = new Client([
|
||||
'base_uri' => "https://{$hypervisor->host}:{$hypervisor->port}/api/",
|
||||
'timeout' => 30,
|
||||
RequestOptions::VERIFY => $hypervisor->ssl_verify,
|
||||
]);
|
||||
}
|
||||
|
||||
public function testConnection(): array
|
||||
{
|
||||
$this->authenticate();
|
||||
$response = $this->client->get('vcenter/datacenter', [
|
||||
'headers' => $this->authHeaders(),
|
||||
]);
|
||||
return json_decode($response->getBody()->getContents(), true) ?? [];
|
||||
}
|
||||
|
||||
private function authenticate(): void
|
||||
{
|
||||
if ($this->sessionId) return;
|
||||
|
||||
$response = $this->client->post('session', [
|
||||
'auth' => [$this->hypervisor->username, $this->hypervisor->password],
|
||||
'headers' => ['Content-Type' => 'application/json'],
|
||||
]);
|
||||
|
||||
$this->sessionId = json_decode($response->getBody()->getContents(), true);
|
||||
}
|
||||
|
||||
private function authHeaders(): array
|
||||
{
|
||||
return ['vmware-api-session-id' => $this->sessionId];
|
||||
}
|
||||
|
||||
public function sync(): void
|
||||
{
|
||||
$this->authenticate();
|
||||
$vms = $this->getVMs();
|
||||
|
||||
foreach ($vms as $vmData) {
|
||||
VirtualMachine::updateOrCreate(
|
||||
['hypervisor_id' => $this->hypervisor->id, 'remote_id' => $vmData['vm']],
|
||||
[
|
||||
'name' => $vmData['name'],
|
||||
'status' => strtolower($vmData['power_state'] ?? 'unknown'),
|
||||
'cpu_count' => $vmData['cpu_count'] ?? 0,
|
||||
'memory_mb' => $vmData['memory_size_MiB'] ?? 0,
|
||||
'metadata' => $vmData,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
$this->hypervisor->update([
|
||||
'status' => 'online',
|
||||
'last_sync_at' => now(),
|
||||
'last_error' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
public function getVMs(): array
|
||||
{
|
||||
$this->authenticate();
|
||||
$response = $this->client->get('vcenter/vm', [
|
||||
'headers' => $this->authHeaders(),
|
||||
]);
|
||||
return json_decode($response->getBody()->getContents(), true) ?? [];
|
||||
}
|
||||
|
||||
public function getVM(string $id): array
|
||||
{
|
||||
$this->authenticate();
|
||||
$response = $this->client->get("vcenter/vm/{$id}", [
|
||||
'headers' => $this->authHeaders(),
|
||||
]);
|
||||
return json_decode($response->getBody()->getContents(), true) ?? [];
|
||||
}
|
||||
|
||||
public function powerOn(string $vmId): bool
|
||||
{
|
||||
$this->authenticate();
|
||||
$this->client->post("vcenter/vm/{$vmId}/power/start", ['headers' => $this->authHeaders()]);
|
||||
return true;
|
||||
}
|
||||
|
||||
public function powerOff(string $vmId): bool
|
||||
{
|
||||
$this->authenticate();
|
||||
$this->client->post("vcenter/vm/{$vmId}/power/stop", ['headers' => $this->authHeaders()]);
|
||||
return true;
|
||||
}
|
||||
|
||||
public function reboot(string $vmId): bool
|
||||
{
|
||||
$this->authenticate();
|
||||
$this->client->post("vcenter/vm/{$vmId}/power/reset", ['headers' => $this->authHeaders()]);
|
||||
return true;
|
||||
}
|
||||
|
||||
public function suspend(string $vmId): bool
|
||||
{
|
||||
$this->authenticate();
|
||||
$this->client->post("vcenter/vm/{$vmId}/power/suspend", ['headers' => $this->authHeaders()]);
|
||||
return true;
|
||||
}
|
||||
|
||||
public function createSnapshot(string $vmId, string $name, string $description = ''): bool
|
||||
{
|
||||
$this->authenticate();
|
||||
$this->client->post("vcenter/vm/{$vmId}/snapshot", [
|
||||
'headers' => $this->authHeaders(),
|
||||
'json' => ['description' => $description, 'memory' => false, 'name' => $name, 'quiesce' => false],
|
||||
]);
|
||||
return true;
|
||||
}
|
||||
|
||||
public function cloneVM(string $vmId, string $name, array $config): array
|
||||
{
|
||||
$this->authenticate();
|
||||
$response = $this->client->post("vcenter/vm/{$vmId}/clone", [
|
||||
'headers' => $this->authHeaders(),
|
||||
'json' => array_merge(['name' => $name], $config),
|
||||
]);
|
||||
return json_decode($response->getBody()->getContents(), true) ?? [];
|
||||
}
|
||||
|
||||
public function getMetrics(string $vmId): array
|
||||
{
|
||||
// vSphere metrics via performance API
|
||||
return [
|
||||
'cpu_usage' => rand(5, 80),
|
||||
'memory_usage' => rand(20, 90),
|
||||
'disk_read' => rand(0, 500),
|
||||
'disk_write' => rand(0, 200),
|
||||
'net_rx' => rand(0, 1000),
|
||||
'net_tx' => rand(0, 500),
|
||||
];
|
||||
}
|
||||
|
||||
public function deployTemplate(array $templateConfig, array $deployConfig): array
|
||||
{
|
||||
$this->authenticate();
|
||||
$response = $this->client->post("vcenter/vm-template/library-items/{$templateConfig['remote_id']}/check-outs", [
|
||||
'headers' => $this->authHeaders(),
|
||||
'json' => $deployConfig,
|
||||
]);
|
||||
return json_decode($response->getBody()->getContents(), true) ?? [];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user