96 lines
2.9 KiB
PHP
96 lines
2.9 KiB
PHP
<?php
|
|
|
|
namespace App\Livewire\Admin;
|
|
|
|
use App\Models\Instance;
|
|
use App\Provisioning\Jobs\IssueInstanceAdminAccess;
|
|
use App\Services\Wireguard\ConfigHandoff;
|
|
use Illuminate\Support\Facades\Hash;
|
|
use Illuminate\Support\Facades\RateLimiter;
|
|
use Illuminate\Support\Str;
|
|
use LivewireUI\Modal\ModalComponent;
|
|
|
|
/**
|
|
* Administrator access to a customer's Nextcloud.
|
|
*
|
|
* Not impersonation: that borrows a portal session. This resets our managed
|
|
* admin account inside the customer's installation and hands the credentials
|
|
* over once — the only thing stock Nextcloud offers, and honest about what it
|
|
* does rather than pretending to be a passwordless jump.
|
|
*
|
|
* The operator's own password is required, every time. Taking control of a
|
|
* customer's installation is not something an unattended browser should be able
|
|
* to do on its own.
|
|
*/
|
|
class InstanceAdminAccess extends ModalComponent
|
|
{
|
|
public string $uuid;
|
|
|
|
public string $subdomain = '';
|
|
|
|
public string $password = '';
|
|
|
|
/** Opaque handle; the credentials never enter the component snapshot. */
|
|
public ?string $token = null;
|
|
|
|
public bool $waiting = false;
|
|
|
|
public function mount(string $uuid): void
|
|
{
|
|
$this->authorize('instances.adminlogin');
|
|
|
|
$instance = Instance::query()->where('uuid', $uuid)->firstOrFail();
|
|
$this->uuid = $uuid;
|
|
$this->subdomain = $instance->subdomain;
|
|
}
|
|
|
|
public function hydrate(): void
|
|
{
|
|
$this->authorize('instances.adminlogin');
|
|
}
|
|
|
|
public function request(): void
|
|
{
|
|
$this->authorize('instances.adminlogin');
|
|
$this->resetErrorBag('password');
|
|
|
|
$key = 'instance-admin:'.auth()->id();
|
|
if (RateLimiter::tooManyAttempts($key, 5)) {
|
|
$this->addError('password', __('instances.too_many_attempts', ['seconds' => RateLimiter::availableIn($key)]));
|
|
|
|
return;
|
|
}
|
|
|
|
if (! Hash::check($this->password, auth()->user()->password)) {
|
|
RateLimiter::hit($key, 300);
|
|
$this->addError('password', __('instances.wrong_password'));
|
|
|
|
return;
|
|
}
|
|
|
|
RateLimiter::clear($key);
|
|
$this->reset('password');
|
|
|
|
// The reset runs on the provisioning worker — it owns the tunnel. The
|
|
// token is where it will leave the result.
|
|
$this->token = Str::random(40);
|
|
$this->waiting = true;
|
|
|
|
IssueInstanceAdminAccess::dispatch($this->uuid, $this->token, (int) auth()->id());
|
|
}
|
|
|
|
public function render()
|
|
{
|
|
$payload = null;
|
|
if ($this->token !== null) {
|
|
$raw = ConfigHandoff::get($this->token);
|
|
$payload = $raw === null ? null : json_decode($raw, true);
|
|
}
|
|
|
|
return view('livewire.admin.instance-admin-access', [
|
|
'credentials' => isset($payload['password']) ? $payload : null,
|
|
'failed' => isset($payload['error']) ? $payload['error'] : null,
|
|
]);
|
|
}
|
|
}
|