Admin: Add ability to mange access tokens and manually create tokens

This commit is contained in:
weeb 2025-06-10 23:06:01 +02:00
parent b412ad3fca
commit d9177b9435
5 changed files with 172 additions and 6 deletions

View File

@ -0,0 +1,58 @@
<?php
namespace App\Livewire;
use App\Services\SynapseService;
use Livewire\Component;
use Illuminate\Support\Facades\Http;
class RegistrationToken extends Component
{
public $tokens = [];
public $registrationToken;
protected SynapseService $synapseService;
public function boot(SynapseService $synapseService)
{
$this->synapseService = $synapseService;
}
public function mount()
{
$this->fetchTokens();
}
public function fetchTokens()
{
$this->tokens = $this->synapseService->fetchTokens();
}
public function revokeToken(string $token)
{
if ($this->synapseService->revokeToken($token)) {
$this->fetchTokens();
}
}
public function createToken()
{
$this->registrationToken = $this->synapseService->createRegistrationToken();
// Open modal
$this->modal('registration-token')->show();
}
public function resetToken()
{
$this->registrationToken = null;
$this->fetchTokens();
}
public function render()
{
return view('livewire.registration-token');
}
}

View File

@ -9,15 +9,24 @@ use Illuminate\Support\Facades\Log;
class SynapseService class SynapseService
{ {
protected string $AUTH_TOKEN;
protected string $SYNAPSE_ENDPOINT;
public function __construct()
{
$this->AUTH_TOKEN = Settings::get('synapse_access_token', '');
$this->SYNAPSE_ENDPOINT = Settings::get('synapse_endpoint', '');
}
/**
* Create registration token
*/
public function createRegistrationToken(): ?string public function createRegistrationToken(): ?string
{ {
$token = Settings::get('synapse_access_token', '');
$endpoint = Settings::get('synapse_endpoint', '');
$response = Http::withHeaders([ $response = Http::withHeaders([
'Authorization' => "Bearer $token", 'Authorization' => "Bearer $this->AUTH_TOKEN",
'Content-Type' => 'application/json' 'Content-Type' => 'application/json'
])->post($endpoint . '/_synapse/admin/v1/registration_tokens/new', [ ])->post("$this->SYNAPSE_ENDPOINT/_synapse/admin/v1/registration_tokens/new", [
'uses_allowed' => 1, 'uses_allowed' => 1,
'length' => 12, 'length' => 12,
'expiry_time' => now()->addWeek()->valueOf(), 'expiry_time' => now()->addWeek()->valueOf(),
@ -34,4 +43,34 @@ class SynapseService
return null; return null;
} }
/**
* Fetch all active registration tokens from Synapse
*/
public function fetchTokens(): ?array
{
$response = Http::withHeaders([
'Authorization' => "Bearer $this->AUTH_TOKEN",
'Content-Type' => 'application/json'
])->get("$this->SYNAPSE_ENDPOINT/_synapse/admin/v1/registration_tokens?valid=true");
if ($response->successful()) {
return $response->json()['registration_tokens'] ?? [];
}
return null;
}
/**
* Revoke registration token
*/
public function revokeToken(string $token): ?bool
{
$response = Http::withHeaders([
'Authorization' => "Bearer $this->AUTH_TOKEN",
'Content-Type' => 'application/json'
])->delete("$this->SYNAPSE_ENDPOINT/_synapse/admin/v1/registration_tokens/$token");
return $response->successful();
}
} }

View File

@ -10,6 +10,7 @@
<flux:navlist variant="outline"> <flux:navlist variant="outline">
<flux:navlist.group :heading="__('Platform')" class="grid"> <flux:navlist.group :heading="__('Platform')" class="grid">
<flux:navlist.item icon="document-text" :href="route('dashboard')" :current="request()->routeIs('dashboard')" wire:navigate>{{ __('Applications') }}</flux:navlist.item> <flux:navlist.item icon="document-text" :href="route('dashboard')" :current="request()->routeIs('dashboard')" wire:navigate>{{ __('Applications') }}</flux:navlist.item>
<flux:navlist.item icon="key" :href="route('registration.tokens')" :current="request()->routeIs('registration.tokens')" wire:navigate>{{ __('Tokens') }}</flux:navlist.item>
<flux:navlist.item icon="wrench-screwdriver" :href="route('settings.synapse')" :current="request()->routeIs('settings.synapse')" wire:navigate>{{ __('Synapse') }}</flux:navlist.item> <flux:navlist.item icon="wrench-screwdriver" :href="route('settings.synapse')" :current="request()->routeIs('settings.synapse')" wire:navigate>{{ __('Synapse') }}</flux:navlist.item>
</flux:navlist.group> </flux:navlist.group>
</flux:navlist> </flux:navlist>

View File

@ -0,0 +1,66 @@
<div class="flex items-start max-md:flex-col">
<div class="flex-1 self-stretch max-md:pt-6">
<div class="relative mb-6 w-full">
<flux:heading size="xl" level="1">{{ __('Registration Tokens') }}</flux:heading>
<flux:subheading size="lg" class="mb-6">{{ __('Manage matrix registration tokens') }}
</flux:subheading>
<flux:separator variant="subtle" />
</div>
<div class="flex justify-end pb-5">
<flux:button wire:click="createToken">Create token</flux:button>
</div>
<flux:modal name="registration-token" wire:close="resetToken" class="md:w-96">
<div class="space-y-6">
<div>
<flux:heading size="lg">Registration token</flux:heading>
</div>
<flux:input class="mb-2" icon="key" value="{{ $registrationToken }}" readonly copyable />
</div>
</flux:modal>
<div class="bg-white dark:bg-zinc-900 shadow-md rounded-lg overflow-hidden">
<table class="min-w-full text-sm text-left text-zinc-700 dark:text-zinc-300">
<thead class="bg-zinc-100 dark:bg-zinc-950 text-xs uppercase text-zinc-500 dark:text-zinc-400">
<tr>
<th class="px-4 py-2 text-left hidden md:block">Token</th>
<th class="px-4 py-2">Uses allowed</th>
<th class="px-4 py-2">Pending</th>
<th class="px-4 py-2">Completed</th>
<th class="px-4 py-2">Expiry time</th>
<th class="px-4 py-2">Actions</th>
</tr>
</thead>
<tbody>
@forelse ($tokens as $token)
<tr class="border-b dark:border-zinc-700 hover:bg-zinc-50 dark:hover:bg-zinc-950">
<td class="px-4 py-4 font-mono text-sm hidden md:block align-middle">{{ $token['token'] }}
</td>
<td class="px-4 py-2">
{{ $token['uses_allowed'] }}
</td>
<td class="px-4 py-2">{{ $token['pending'] }}</td>
<td class="px-4 py-2 text-sm">{{ $token['completed'] }}</td>
<td class="px-4 py-2">
{{ $token['expiry_time'] ? Carbon\Carbon::createFromTimestampMs($token['expiry_time'])->toDateTimeString() : 'Never' }}
</td>
<td>
<flux:button class="cursor-pointer" size="sm"
wire:click="revokeToken('{{ (string) $token['token'] }}')">
Revoke
</flux:button>
</td>
</tr>
@empty
<tr>
<td colspan="6" class="text-center py-4 text-gray-500">No tokens found.</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
</div>

View File

@ -1,5 +1,6 @@
<?php <?php
use App\Livewire\RegistrationToken;
use App\Livewire\Settings\Appearance; use App\Livewire\Settings\Appearance;
use App\Livewire\Settings\Password; use App\Livewire\Settings\Password;
use App\Livewire\Settings\Profile; use App\Livewire\Settings\Profile;
@ -13,13 +14,14 @@ Route::view('dashboard', 'dashboard')
->middleware(['auth', 'verified', App\Http\Middleware\IsAdmin::class]) ->middleware(['auth', 'verified', App\Http\Middleware\IsAdmin::class])
->name('dashboard'); ->name('dashboard');
Route::middleware(['auth'])->group(function () { Route::middleware(['auth', App\Http\Middleware\IsAdmin::class])->group(function () {
Route::redirect('settings', 'settings/profile'); Route::redirect('settings', 'settings/profile');
Route::get('settings/profile', Profile::class)->name('settings.profile'); Route::get('settings/profile', Profile::class)->name('settings.profile');
Route::get('settings/password', Password::class)->name('settings.password'); Route::get('settings/password', Password::class)->name('settings.password');
Route::get('settings/appearance', Appearance::class)->name('settings.appearance'); Route::get('settings/appearance', Appearance::class)->name('settings.appearance');
Route::get('settings/synapse', Synapse::class)->name('settings.synapse'); Route::get('settings/synapse', Synapse::class)->name('settings.synapse');
Route::get('registration-tokens', RegistrationToken::class)->name('registration.tokens');
}); });
require __DIR__ . '/auth.php'; require __DIR__ . '/auth.php';