mirror of
https://github.com/pelican-dev/panel.git
synced 2025-05-19 22:14:45 +02:00

* Add new panel * Add some basic resource pages * Wip * Wip terminal * Wip * Add new panel * Add some basic resource pages * Wip * [Sub-Users] Add Invite TODO: The logic with permissions * [Sub-Users] Fix Creation * [Cron] Add basics * Add basic auth and messages * Add basic buttons * WIP on issue/353 * WIP on issue/353 * Add Database page * Update Database Page * Start of Backup Page * Composer Update * Changes * Send input * Remove this includes * Better offline handling * Consolidate top nav config * Update Backups Page * Update Backups * Change name * Add Assign All, Layout Fixes. * conflict * update schedule pages * fix phpstan * update pint.json * add cron presets to schedule * fix tests * fix task creation * schedules: disable task creation if limit is reached & disable backup action if backup limit is 0 * update activity pages * update resources * Update Edit User TODO: actually save permissions when they're changed. TODO: Figure out why Control does not update it's state... but the rest do... * .... Sure it works. TODO: Update permissions when you save editing a sub user. * user: update canAccessPanel & canAccessTenant * add helper to convert bytes into readable format * very basic file explorer * files: fix some stuff & remove dummy data * files: better error handling * files: basic file editor * files: add some actions * File manager updates * files: fix paths * Revery Composer Upgrade, Fixes SQLite * fix: Pint (#517) feat: MenuItems to and from admin * Update File Editing Updated File Editing to its own page, Added Permission checks for file manager. Co-authored-by: Boy132 <Boy132@users.noreply.github.com> * add enum for editor langs * files: add upload & pull actions * fix build * files: handle images * Update to Filament v3.2.98 * files: add remaining actions * use `authorize` instead of `hidden` * fix canAccessTenant * update date columns * files: testing & fixes * Fix File Names Co-authored-by: lancepioch <git@lance.sh> * Combine Pull/Upload * Fix BulkDelete * Uncontained tabs * Hide Lang Selection, Move Actions * Update Monaco, more custom * Add livewire config livewire limits uploads to 12MB... who knows why... Fixed uploading a single files failing * files: fix record url * basic setup for settings & startup page * make abstract class for simple app pages * Basic Startup Page * Update nav sort * small cleanup * startup: fix shouldHideComponent & getSelectOptionsFromRules * startup: fix non editable fields & set default value * startup: add todo for save button * Save Variables after update & off click Variables update when the user clicks off the input. * Notifications are cool * Add rule validation * Sort variables by sortid * pint * Settings Page + Startup Changes * settings: cleanup * refactor: use server model for ServerFormPage (formerly known as SimplePage) * Use Repeater for variables * Add Network, Remove breadcrumbs * Add paginated to file explorer * Fix updating variables * Add link to go to new client area * fix after merge * Add graphs to console page Graphs still need to get the data from the web socket. * fix pint & phpstan * fix authorizeAccess for EditFiles and Startup page * Fix rules on startup page * Update console size * Fix node name * add "global search" to files list requires https://github.com/pelican-dev/wings/pull/44 * remove debug dummy data * update view action on ListServers * enable SPA mode for app panel * remove colors from app panel they are defined globally in AppServiceProvider * update global search ui a bit (to be replaced with a custom page that is similar to the list files table) * add own page for global search untested - and route needs cleanup (if possible) * fix File getRows * remove "path" from SearchFiles (for now) * fix caching for searched files * add title and breadcrumbs to global search page * make cpu & memory charts on console page working * fix phpstan * add missing import * cleanup console views & widgets * add overview stats to console * don't be so lazy, console! * make history working * decode data to get array * add missing On * fix json_decode * change polling to 1 sec * hide "0" cpu/ memory * add data to network chart * Remove data labels * fix data on network chart * fix data on network chart (2nd try) * WIP Network Stats * Remove test * Change MaxWidth * run pint * fix phpstan * Fix storeStats cast * make $data a string this time for real * update visible check for "admin" menu item * remove account widget * rebrand "Dashboard" to "Server List" WIP - doesn't look good but is somewhat working * fix canAccessPanel * separate server list into own panel * change path to avoid conflicts with old client area (and remove sidebar width) * display correct icon and color on server list entries * show total memory if server is offline * replace custom server list page with ListRecords page * fix tests * fix namespace * remove "open" button and make whole column clickable * Update EditProfile * run pint * fix access to server list * add new login page to panels * fix next_run_at for new schedules * use new DateTimeColumn * add own column for file bytes * return to server list when clicking title * fix console loading * handle server with "conflict state" * add banner if server is in "conflict state" * fix phpstan * update docker image select * fix permission checks on Settings & Startup pages * fix query for activity log page * fix activity log not being logged * adjust ListActivities * fix phpstan * fix pint * fix profile menu item link on server panel * add ip tooltip to activity logs (and role permission) * change backup icon * update navigation sort * general code cleanup * more cleanup * Disable Restart/Stop if server is offline * Change rename notification * Remove negation on abort_unless * Add notification on save * Single disabled closure & comment unused import * Add required to Server Name & Nullable to description * mutateFormDataBeforeSave doesn't work since we use forceFill * Fix web socket connection not existing. * Fix some subuser permissions * add permission checks to resources * do not allow self-deletion * Update editing file permissions * Fix of the previous fix * add service for subuser updating * Only allow save if they have file_update * Remove unused import * Update backup delete button * Add Delete, remove bulks * Update Database page * Use Allocation Permissions * add canAccess check to startup * Add Permission checks to Settings page * add service for subuser deletion * Remove Kill permission * Updates * fix move files * add redirects * fix phpstan * activity: remove properties from tans for now * If alias, use that, else ip --------- Co-authored-by: notCharles <charles@pelican.dev> Co-authored-by: Boy132 <mail@boy132.de> Co-authored-by: Senna <62171904+Poseidon281@users.noreply.github.com> Co-authored-by: Boy132 <Boy132@users.noreply.github.com> Co-authored-by: RMartinOscar <40749467+RMartinOscar@users.noreply.github.com>
391 lines
22 KiB
PHP
391 lines
22 KiB
PHP
<?php
|
|
|
|
namespace App\Filament\Resources\UserResource\Pages;
|
|
|
|
use App\Exceptions\Service\User\TwoFactorAuthenticationTokenInvalid;
|
|
use App\Facades\Activity;
|
|
use App\Models\ActivityLog;
|
|
use App\Models\ApiKey;
|
|
use App\Models\User;
|
|
use App\Services\Users\ToggleTwoFactorService;
|
|
use App\Services\Users\TwoFactorSetupService;
|
|
use App\Services\Users\UserUpdateService;
|
|
use chillerlan\QRCode\Common\EccLevel;
|
|
use chillerlan\QRCode\Common\Version;
|
|
use chillerlan\QRCode\QRCode;
|
|
use chillerlan\QRCode\QROptions;
|
|
use Closure;
|
|
use DateTimeZone;
|
|
use Exception;
|
|
use Filament\Forms\Components\Actions;
|
|
use Filament\Forms\Components\Actions\Action;
|
|
use Filament\Forms\Components\Grid;
|
|
use Filament\Forms\Components\Placeholder;
|
|
use Filament\Forms\Components\Repeater;
|
|
use Filament\Forms\Components\Section;
|
|
use Filament\Forms\Components\Select;
|
|
use Filament\Forms\Components\Tabs;
|
|
use Filament\Forms\Components\Tabs\Tab;
|
|
use Filament\Forms\Components\TagsInput;
|
|
use Filament\Forms\Components\Textarea;
|
|
use Filament\Forms\Components\TextInput;
|
|
use Filament\Forms\Get;
|
|
use Filament\Notifications\Notification;
|
|
use Filament\Support\Enums\MaxWidth;
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Support\Facades\Hash;
|
|
use Illuminate\Support\HtmlString;
|
|
use Illuminate\Support\Str;
|
|
use Illuminate\Validation\Rules\Password;
|
|
use Laravel\Socialite\Facades\Socialite;
|
|
|
|
/**
|
|
* @method User getUser()
|
|
*/
|
|
class EditProfile extends \Filament\Pages\Auth\EditProfile
|
|
{
|
|
private ToggleTwoFactorService $toggleTwoFactorService;
|
|
|
|
public function boot(ToggleTwoFactorService $toggleTwoFactorService): void
|
|
{
|
|
$this->toggleTwoFactorService = $toggleTwoFactorService;
|
|
}
|
|
|
|
public function getMaxWidth(): MaxWidth|string
|
|
{
|
|
return MaxWidth::SevenExtraLarge;
|
|
}
|
|
|
|
protected function getForms(): array
|
|
{
|
|
return [
|
|
'form' => $this->form(
|
|
$this->makeForm()
|
|
->schema([
|
|
Tabs::make()->persistTabInQueryString()
|
|
->schema([
|
|
Tab::make('Account')
|
|
->label(trans('strings.account'))
|
|
->icon('tabler-user')
|
|
->schema([
|
|
TextInput::make('username')
|
|
->label(trans('strings.username'))
|
|
->disabled()
|
|
->readOnly()
|
|
->dehydrated(false)
|
|
->maxLength(255)
|
|
->unique(ignoreRecord: true)
|
|
->autofocus(),
|
|
TextInput::make('email')
|
|
->prefixIcon('tabler-mail')
|
|
->label(trans('strings.email'))
|
|
->email()
|
|
->required()
|
|
->maxLength(255)
|
|
->unique(ignoreRecord: true),
|
|
TextInput::make('password')
|
|
->label(trans('strings.password'))
|
|
->password()
|
|
->prefixIcon('tabler-password')
|
|
->revealable(filament()->arePasswordsRevealable())
|
|
->rule(Password::default())
|
|
->autocomplete('new-password')
|
|
->dehydrated(fn ($state): bool => filled($state))
|
|
->dehydrateStateUsing(fn ($state): string => Hash::make($state))
|
|
->live(debounce: 500)
|
|
->same('passwordConfirmation'),
|
|
TextInput::make('passwordConfirmation')
|
|
->label(trans('strings.password_confirmation'))
|
|
->password()
|
|
->prefixIcon('tabler-password-fingerprint')
|
|
->revealable(filament()->arePasswordsRevealable())
|
|
->required()
|
|
->visible(fn (Get $get): bool => filled($get('password')))
|
|
->dehydrated(false),
|
|
Select::make('timezone')
|
|
->required()
|
|
->prefixIcon('tabler-clock-pin')
|
|
->options(fn () => collect(DateTimeZone::listIdentifiers())->mapWithKeys(fn ($tz) => [$tz => $tz]))
|
|
->searchable(),
|
|
Select::make('language')
|
|
->label(trans('strings.language'))
|
|
->required()
|
|
->prefixIcon('tabler-flag')
|
|
->live()
|
|
->default('en')
|
|
->helperText(fn (User $user, $state) => new HtmlString($user->isLanguageTranslated($state) ? '' : "
|
|
Your language ($state) has not been translated yet!
|
|
But never fear, you can help fix that by
|
|
<a style='color: rgb(56, 189, 248)' href='https://crowdin.com/project/pelican-dev'>contributing directly here</a>.
|
|
")
|
|
)
|
|
->options(fn (User $user) => $user->getAvailableLanguages()),
|
|
]),
|
|
|
|
Tab::make('OAuth')
|
|
->icon('tabler-brand-oauth')
|
|
->visible(function () {
|
|
foreach (config('auth.oauth') as $name => $data) {
|
|
if ($data['enabled']) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
})
|
|
->schema(function () {
|
|
$providers = [];
|
|
|
|
foreach (config('auth.oauth') as $name => $data) {
|
|
if (!$data['enabled']) {
|
|
continue;
|
|
}
|
|
|
|
$unlink = array_key_exists($name, $this->getUser()->oauth);
|
|
|
|
$providers[] = Action::make("oauth_$name")
|
|
->label(($unlink ? 'Unlink ' : 'Link ') . Str::title($name))
|
|
->icon($unlink ? 'tabler-unlink' : 'tabler-link')
|
|
->color($data['color'])
|
|
->action(function (UserUpdateService $updateService) use ($name, $unlink) {
|
|
if ($unlink) {
|
|
$oauth = auth()->user()->oauth;
|
|
unset($oauth[$name]);
|
|
|
|
$updateService->handle(auth()->user(), ['oauth' => $oauth]);
|
|
|
|
$this->fillForm();
|
|
|
|
Notification::make()
|
|
->title("OAuth provider '$name' unlinked")
|
|
->success()
|
|
->send();
|
|
} elseif (config("auth.oauth.$name.enabled")) {
|
|
redirect(Socialite::with($name)->redirect()->getTargetUrl());
|
|
}
|
|
});
|
|
}
|
|
|
|
return [Actions::make($providers)];
|
|
}),
|
|
|
|
Tab::make('2FA')
|
|
->icon('tabler-shield-lock')
|
|
->schema(function (TwoFactorSetupService $setupService) {
|
|
if ($this->getUser()->use_totp) {
|
|
return [
|
|
Placeholder::make('2fa-already-enabled')
|
|
->label('Two Factor Authentication is currently enabled!'),
|
|
Textarea::make('backup-tokens')
|
|
->hidden(fn () => !cache()->get("users.{$this->getUser()->id}.2fa.tokens"))
|
|
->rows(10)
|
|
->readOnly()
|
|
->dehydrated(false)
|
|
->formatStateUsing(fn () => cache()->get("users.{$this->getUser()->id}.2fa.tokens"))
|
|
->helperText('These will not be shown again!')
|
|
->label('Backup Tokens:'),
|
|
TextInput::make('2fa-disable-code')
|
|
->label('Disable 2FA')
|
|
->helperText('Enter your current 2FA code to disable Two Factor Authentication'),
|
|
];
|
|
}
|
|
|
|
['image_url_data' => $url, 'secret' => $secret] = cache()->remember(
|
|
"users.{$this->getUser()->id}.2fa.state",
|
|
now()->addMinutes(5), fn () => $setupService->handle($this->getUser())
|
|
);
|
|
|
|
$options = new QROptions([
|
|
'svgLogo' => public_path('pelican.svg'),
|
|
'svgLogoScale' => 0.05,
|
|
'addLogoSpace' => true,
|
|
'logoSpaceWidth' => 13,
|
|
'logoSpaceHeight' => 13,
|
|
]);
|
|
|
|
// https://github.com/chillerlan/php-qrcode/blob/main/examples/svgWithLogo.php
|
|
|
|
// QROptions
|
|
// @phpstan-ignore property.protected
|
|
$options->version = Version::AUTO;
|
|
// $options->outputInterface = QRSvgWithLogo::class;
|
|
// @phpstan-ignore property.protected
|
|
$options->outputBase64 = false;
|
|
// @phpstan-ignore property.protected
|
|
$options->eccLevel = EccLevel::H; // ECC level H is necessary when using logos
|
|
// @phpstan-ignore property.protected
|
|
$options->addQuietzone = true;
|
|
// $options->drawLightModules = true;
|
|
// @phpstan-ignore property.protected
|
|
$options->connectPaths = true;
|
|
// @phpstan-ignore property.protected
|
|
$options->drawCircularModules = true;
|
|
// $options->circleRadius = 0.45;
|
|
|
|
// @phpstan-ignore property.protected
|
|
$options->svgDefs = '<linearGradient id="gradient" x1="100%" y2="100%">
|
|
<stop stop-color="#7dd4fc" offset="0"/>
|
|
<stop stop-color="#38bdf8" offset="0.5"/>
|
|
<stop stop-color="#0369a1" offset="1"/>
|
|
</linearGradient>
|
|
<style><![CDATA[
|
|
.dark{fill: url(#gradient);}
|
|
.light{fill: #000;}
|
|
]]></style>';
|
|
|
|
$image = (new QRCode($options))->render($url);
|
|
|
|
return [
|
|
Placeholder::make('qr')
|
|
->label('Scan QR Code')
|
|
->content(fn () => new HtmlString("
|
|
<div style='width: 300px; background-color: rgb(24, 24, 27);'>$image</div>
|
|
"))
|
|
->helperText('Setup Key: ' . $secret),
|
|
TextInput::make('2facode')
|
|
->label('Code')
|
|
->requiredWith('2fapassword')
|
|
->helperText('Scan the QR code above using your two-step authentication app, then enter the code generated.'),
|
|
TextInput::make('2fapassword')
|
|
->label('Current Password')
|
|
->requiredWith('2facode')
|
|
->currentPassword()
|
|
->password()
|
|
->helperText('Enter your current password to verify.'),
|
|
];
|
|
}),
|
|
Tab::make('API Keys')
|
|
->icon('tabler-key')
|
|
->schema([
|
|
Grid::make('asdf')->columns(5)->schema([
|
|
Section::make('Create API Key')->columnSpan(3)->schema([
|
|
TextInput::make('description')
|
|
->live(),
|
|
TagsInput::make('allowed_ips')
|
|
->live()
|
|
->splitKeys([',', ' ', 'Tab'])
|
|
->placeholder('Example: 127.0.0.1 or 192.168.1.1')
|
|
->label('Whitelisted IP\'s')
|
|
->helperText('Press enter to add a new IP address or leave blank to allow any IP address')
|
|
->columnSpanFull(),
|
|
])->headerActions([
|
|
Action::make('Create')
|
|
->disabled(fn (Get $get) => $get('description') === null)
|
|
->successRedirectUrl(route('filament.admin.auth.profile', ['tab' => '-api-keys-tab']))
|
|
->action(function (Get $get, Action $action, User $user) {
|
|
$token = $user->createToken(
|
|
$get('description'),
|
|
$get('allowed_ips'),
|
|
);
|
|
Activity::event('user:api-key.create')
|
|
->subject($token->accessToken)
|
|
->property('identifier', $token->accessToken->identifier)
|
|
->log();
|
|
$action->success();
|
|
}),
|
|
]),
|
|
Section::make('Keys')->columnSpan(2)->schema([
|
|
Repeater::make('keys')
|
|
->label('')
|
|
->relationship('apiKeys')
|
|
->addable(false)
|
|
->itemLabel(fn ($state) => $state['identifier'])
|
|
->deleteAction(function (Action $action) {
|
|
$action->requiresConfirmation()->action(function (array $arguments, Repeater $component) {
|
|
$items = $component->getState();
|
|
$key = $items[$arguments['item']];
|
|
ApiKey::find($key['id'] ?? null)?->delete();
|
|
|
|
unset($items[$arguments['item']]);
|
|
|
|
$component->state($items);
|
|
|
|
$component->callAfterStateUpdated();
|
|
});
|
|
})
|
|
->schema(fn () => [
|
|
Placeholder::make('adf')->label(fn (ApiKey $key) => $key->memo),
|
|
]),
|
|
]),
|
|
]),
|
|
]),
|
|
Tab::make('SSH Keys')
|
|
->icon('tabler-lock-code')
|
|
->schema([
|
|
Placeholder::make('Coming soon!'),
|
|
]),
|
|
Tab::make('Activity')
|
|
->icon('tabler-history')
|
|
->schema([
|
|
Repeater::make('activity')
|
|
->deletable(false)
|
|
->addable(false)
|
|
->relationship(null, function (Builder $query) {
|
|
$query->orderBy('timestamp', 'desc');
|
|
})
|
|
->schema([
|
|
Placeholder::make('activity!')->label('')->content(fn (ActivityLog $log) => new HtmlString($log->htmlable())),
|
|
]),
|
|
]),
|
|
]),
|
|
])
|
|
->operation('edit')
|
|
->model($this->getUser())
|
|
->statePath('data')
|
|
->inlineLabel(!static::isSimple()),
|
|
),
|
|
];
|
|
}
|
|
|
|
protected function handleRecordUpdate(Model $record, array $data): Model
|
|
{
|
|
if (!$record instanceof User) {
|
|
return $record;
|
|
}
|
|
|
|
if ($token = $data['2facode'] ?? null) {
|
|
$tokens = $this->toggleTwoFactorService->handle($record, $token, true);
|
|
cache()->put("users.$record->id.2fa.tokens", implode("\n", $tokens), now()->addSeconds(15));
|
|
|
|
$this->redirectRoute('filament.admin.auth.profile', ['tab' => '-2fa-tab']);
|
|
}
|
|
|
|
if ($token = $data['2fa-disable-code'] ?? null) {
|
|
$this->toggleTwoFactorService->handle($record, $token, false);
|
|
|
|
cache()->forget("users.$record->id.2fa.state");
|
|
}
|
|
|
|
return parent::handleRecordUpdate($record, $data);
|
|
}
|
|
|
|
public function exception(Exception $e, Closure $stopPropagation): void
|
|
{
|
|
if ($e instanceof TwoFactorAuthenticationTokenInvalid) {
|
|
Notification::make()
|
|
->title('Invalid 2FA Code')
|
|
->body($e->getMessage())
|
|
->color('danger')
|
|
->icon('tabler-2fa')
|
|
->danger()
|
|
->send();
|
|
|
|
$stopPropagation();
|
|
}
|
|
}
|
|
|
|
protected function getFormActions(): array
|
|
{
|
|
return [];
|
|
}
|
|
|
|
protected function getHeaderActions(): array
|
|
{
|
|
return [
|
|
$this->getSaveFormAction()->formId('form'),
|
|
];
|
|
|
|
}
|
|
}
|