Add SSH Keys to Profile (#1478)

This commit is contained in:
Boy132 2025-07-06 22:51:45 +02:00 committed by GitHub
parent 23ddded61e
commit 556551b4f3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 163 additions and 14 deletions

View File

@ -8,7 +8,9 @@ use App\Facades\Activity;
use App\Models\ActivityLog;
use App\Models\ApiKey;
use App\Models\User;
use App\Models\UserSSHKey;
use App\Services\Helpers\LanguageService;
use App\Services\Ssh\KeyCreationService;
use App\Services\Users\ToggleTwoFactorService;
use App\Services\Users\TwoFactorSetupService;
use App\Services\Users\UserUpdateService;
@ -19,6 +21,7 @@ use chillerlan\QRCode\Common\Version;
use chillerlan\QRCode\QRCode;
use chillerlan\QRCode\QROptions;
use DateTimeZone;
use Exception;
use Filament\Actions\Action as HeaderAction;
use Filament\Actions\ActionGroup;
use Filament\Forms\Components\Actions;
@ -276,7 +279,7 @@ class EditProfile extends BaseEditProfile
->icon('tabler-key')
->schema([
Grid::make('name')->columns(5)->schema([
Section::make(trans('profile.create_key'))->columnSpan(3)->schema([
Section::make(trans('profile.create_api_key'))->columnSpan(3)->schema([
TextInput::make('description')
->label(trans('profile.description'))
->live(),
@ -288,9 +291,9 @@ class EditProfile extends BaseEditProfile
->helperText(trans('profile.allowed_ips_help'))
->columnSpanFull(),
])->headerActions([
Action::make('Create')
Action::make('create')
->label(trans('filament-actions::create.single.modal.actions.create.label'))
->disabled(fn (Get $get) => $get('description') === null)
->disabled(fn (Get $get) => empty($get('description')))
->successRedirectUrl(self::getUrl(['tab' => '-api-keys-tab'], panel: 'app'))
->action(function (Get $get, Action $action, User $user) {
$token = $user->createToken(
@ -306,7 +309,7 @@ class EditProfile extends BaseEditProfile
->log();
Notification::make()
->title(trans('profile.key_created'))
->title(trans('profile.api_key_created'))
->body($token->accessToken->identifier . $token->plainTextToken)
->persistent()
->success()
@ -315,17 +318,28 @@ class EditProfile extends BaseEditProfile
$action->success();
}),
]),
Section::make(trans('profile.keys'))->label(trans('profile.keys'))->columnSpan(2)->schema([
Repeater::make('keys')
->label('')
Section::make(trans('profile.api_keys'))->columnSpan(2)->schema([
Repeater::make('api_keys')
->hiddenLabel()
->relationship('apiKeys')
->addable(false)
->itemLabel(fn ($state) => $state['identifier'])
->deleteAction(function (Action $action) {
$action->requiresConfirmation()->action(function (array $arguments, Repeater $component) {
$action->requiresConfirmation()->action(function (array $arguments, Repeater $component, User $user) {
$items = $component->getState();
$key = $items[$arguments['item']];
ApiKey::find($key['id'] ?? null)?->delete();
$apiKey = ApiKey::find($key['id'] ?? null);
if ($apiKey->exists()) {
$apiKey->delete();
Activity::event('user:api-key.delete')
->actor($user)
->subject($user)
->subject($apiKey)
->property('identifier', $apiKey->identifier)
->log();
}
unset($items[$arguments['item']]);
@ -335,7 +349,8 @@ class EditProfile extends BaseEditProfile
});
})
->schema(fn () => [
Placeholder::make('adf')->label(fn (ApiKey $key) => $key->memo),
Placeholder::make('memo')
->label(fn (ApiKey $key) => $key->memo),
]),
]),
]),
@ -343,7 +358,87 @@ class EditProfile extends BaseEditProfile
Tab::make(trans('profile.tabs.ssh_keys'))
->icon('tabler-lock-code')
->hidden(),
->schema([
Grid::make('name')->columns(5)->schema([
Section::make(trans('profile.create_ssh_key'))->columnSpan(3)->schema([
TextInput::make('name')
->label(trans('profile.name'))
->live(),
Textarea::make('public_key')
->label(trans('profile.public_key'))
->autosize()
->live(),
])->headerActions([
Action::make('create')
->label(trans('filament-actions::create.single.modal.actions.create.label'))
->disabled(fn (Get $get) => empty($get('name')) || empty($get('public_key')))
->successRedirectUrl(self::getUrl(['tab' => '-ssh-keys-tab'], panel: 'app'))
->action(function (Get $get, Action $action, User $user, KeyCreationService $service) {
try {
$sshKey = $service->handle($user, $get('name'), $get('public_key'));
Activity::event('user:ssh-key.create')
->actor($user)
->subject($user)
->subject($sshKey)
->property('fingerprint', $sshKey->fingerprint)
->log();
Notification::make()
->title(trans('profile.ssh_key_created'))
->body("SHA256:{$sshKey->fingerprint}")
->success()
->send();
$action->success();
} catch (Exception $exception) {
Notification::make()
->title(trans('profile.could_not_create_ssh_key'))
->body($exception->getMessage())
->danger()
->send();
$action->failure();
}
}),
]),
Section::make(trans('profile.ssh_keys'))->columnSpan(2)->schema([
Repeater::make('ssh_keys')
->hiddenLabel()
->relationship('sshKeys')
->addable(false)
->itemLabel(fn ($state) => $state['name'])
->deleteAction(function (Action $action) {
$action->requiresConfirmation()->action(function (array $arguments, Repeater $component, User $user) {
$items = $component->getState();
$key = $items[$arguments['item']];
$sshKey = UserSSHKey::find($key['id'] ?? null);
if ($sshKey->exists()) {
$sshKey->delete();
Activity::event('user:ssh-key.delete')
->actor($user)
->subject($user)
->subject($sshKey)
->property('fingerprint', $sshKey->fingerprint)
->log();
}
unset($items[$arguments['item']]);
$component->state($items);
$component->callAfterStateUpdated();
});
})
->schema(fn () => [
Placeholder::make('fingerprint')
->label(fn (UserSSHKey $key) => "SHA256:{$key->fingerprint}"),
]),
]),
]),
]),
Tab::make(trans('profile.tabs.activity'))
->icon('tabler-history')

View File

@ -0,0 +1,48 @@
<?php
namespace App\Services\Ssh;
use App\Models\User;
use App\Models\UserSSHKey;
use Exception;
use phpseclib3\Crypt\DSA;
use phpseclib3\Crypt\PublicKeyLoader;
use phpseclib3\Crypt\RSA;
use phpseclib3\Exception\NoKeyLoadedException;
class KeyCreationService
{
/**
* @throws Exception
*/
public function handle(User $user, string $name, string $publicKey): UserSSHKey
{
try {
$key = PublicKeyLoader::loadPublicKey($publicKey);
} catch (NoKeyLoadedException) {
throw new Exception('The public key provided is not valid');
}
if ($key instanceof DSA) {
throw new Exception('DSA keys are not supported');
}
if ($key instanceof RSA && $key->getLength() < 2048) {
throw new Exception('RSA keys must be at least 2048 bytes in length');
}
$fingerprint = $key->getFingerprint('sha256');
if ($user->sshKeys()->where('fingerprint', $fingerprint)->exists()) {
throw new Exception('The public key provided already exists on your account');
}
/** @var UserSSHKey $sshKey */
$sshKey = $user->sshKeys()->create([
'name' => $name,
'public_key' => $key->toString('PKCS8'),
'fingerprint' => $fingerprint,
]);
return $sshKey;
}
}

View File

@ -33,12 +33,18 @@ return [
'backup_codes' => 'Backup Codes',
'disable_2fa' => 'Disable 2FA',
'disable_2fa_help' => 'Enter your current 2FA code to disable Two Factor Authentication',
'keys' => 'Keys',
'create_key' => 'Create API Key',
'key_created' => 'Key Created',
'api_keys' => 'API Keys',
'create_api_key' => 'Create API Key',
'api_key_created' => 'API Key Created',
'description' => 'Description',
'allowed_ips' => 'Allowed IPs',
'allowed_ips_help' => 'Press enter to add a new IP address or leave blank to allow any IP address',
'ssh_keys' => 'SSH Keys',
'create_ssh_key' => 'Create SSH Key',
'ssh_key_created' => 'SSH Key Created',
'name' => 'Name',
'public_key' => 'Public Key',
'could_not_create_ssh_key' => 'Could not create ssh key',
'dashboard' => 'Dashboard',
'dashboard_layout' => 'Dashboard Layout',
'console' => 'Console',