Compare commits

...

18 Commits

Author SHA1 Message Date
Boy132
35ce1d34ab
Permission check fixes (#1406) 2025-05-27 19:30:30 +02:00
Boy132
17555a1d09
Make server name and server address clickable (and copyable) (#1395) 2025-05-27 19:30:07 +02:00
Lance Pioch
837121b1fb
Laravel 12.16.0 Shift (#1408)
Co-authored-by: Shift <shift@laravelshift.com>
2025-05-27 13:08:51 -04:00
Boy132
af9f2c653e
Add missing </div> to monaco editor view (#1399) 2025-05-23 06:02:29 -04:00
Boy132
c22e7456b5
Move tables & forms to resources in client area (#1388) 2025-05-22 08:41:17 +02:00
Boy132
97fb66f5d6
Use app panel for password link in AccountCreated notification (#1389) 2025-05-21 08:46:27 +02:00
Lance Pioch
51037c5c20
Laravel 12.15.0 Shift (#1390)
Co-authored-by: Shift <shift@laravelshift.com>
2025-05-20 16:32:43 -04:00
MartinOscar
23d13d9e83
Fix Mount translation (#1382) 2025-05-20 11:58:16 -04:00
Boy132
6c20426757
Put whereHas-orDoesntHave in own where (#1387) 2025-05-20 08:33:33 +02:00
Boy132
1224210668
Only include "server" subjects in activity log query (#1386) 2025-05-20 08:33:16 +02:00
Boy132
258c97bf14
Add missing auth activity logs (#1372) 2025-05-19 09:12:58 +02:00
C0D3 M4513R
7034c4d013
Fix Composer warnings (#1376)
Co-authored-by: MartinOscar <40749467+rmartinoscar@users.noreply.github.com>
2025-05-15 14:39:59 -05:00
MartinOscar
e5cba893e4
Check against 2fa backup codes too in Login (#1366)
Co-authored-by: Boy132 <Boy132@users.noreply.github.com>
2025-05-12 16:14:09 +02:00
Boy132
fd49f472c3
Remove packs folders in storage (#1367) 2025-05-12 14:30:16 +02:00
MartinOscar
c8556a4c56
Use placeholder for EditServer db_delete (#1362) 2025-05-10 00:01:58 +02:00
MartinOscar
6de6306a19
Fix GSLToken id, label & query (#1361) 2025-05-09 17:57:18 -04:00
Charles
1f8a5cdd1d
Fix font dropdown on EditProfile Page (#1360) 2025-05-09 17:42:39 -04:00
Charles
30ae860d69
Fix server notification body translation key (#1359) 2025-05-09 17:39:15 -04:00
44 changed files with 741 additions and 788 deletions

View File

@ -1,11 +0,0 @@
<?php
namespace App\Events\Auth;
use App\Models\User;
use App\Events\Event;
class DirectLogin extends Event
{
public function __construct(public User $user, public bool $remember) {}
}

View File

@ -1,16 +0,0 @@
<?php
namespace App\Events\Auth;
use App\Events\Event;
use Illuminate\Queue\SerializesModels;
class FailedPasswordReset extends Event
{
use SerializesModels;
/**
* Create a new event instance.
*/
public function __construct(public string $ip, public string $email) {}
}

View File

@ -14,8 +14,11 @@ use Filament\Facades\Filament;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\TextInput;
use Filament\Notifications\Notification;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\HtmlString;
class GSLToken extends FeatureProvider
{
@ -35,7 +38,7 @@ class GSLToken extends FeatureProvider
public function getId(): string
{
return 'gsltoken';
return 'gsl_token';
}
public function getAction(): Action
@ -44,7 +47,9 @@ class GSLToken extends FeatureProvider
$server = Filament::getTenant();
/** @var ServerVariable $serverVariable */
$serverVariable = $server->serverVariables()->where('env_variable', 'STEAM_ACC')->first();
$serverVariable = $server->serverVariables()->whereHas('variable', function (Builder $query) {
$query->where('env_variable', 'STEAM_ACC');
})->first();
return Action::make($this->getId())
->requiresConfirmation()
@ -54,7 +59,7 @@ class GSLToken extends FeatureProvider
->disabledForm(fn () => !auth()->user()->can(Permission::ACTION_STARTUP_UPDATE, $server))
->form([
Placeholder::make('info')
->label('You can either <x-filament::link href="https://steamcommunity.com/dev/managegameservers" target="_blank">generate a new one</x-filament::link> and enter it below or leave the field blank to remove it completely.'),
->label(new HtmlString(Blade::render('You can either <x-filament::link href="https://steamcommunity.com/dev/managegameservers" target="_blank">generate a new one</x-filament::link> and enter it below or leave the field blank to remove it completely.'))),
TextInput::make('gsltoken')
->label('GSL Token')
->rules([

View File

@ -79,7 +79,7 @@ class ApiKeyResource extends Resource
TextColumn::make('user.username')
->label(trans('admin/apikey.table.created_by'))
->icon('tabler-user')
->url(fn (ApiKey $apiKey) => auth()->user()->can('update user', $apiKey->user) ? EditUser::getUrl(['record' => $apiKey->user]) : null),
->url(fn (ApiKey $apiKey) => auth()->user()->can('update', $apiKey->user) ? EditUser::getUrl(['record' => $apiKey->user]) : null),
])
->actions([
DeleteAction::make(),

View File

@ -164,8 +164,10 @@ class DatabaseHostResource extends Resource
{
$query = parent::getEloquentQuery();
return $query->whereHas('nodes', function (Builder $query) {
$query->whereIn('nodes.id', auth()->user()->accessibleNodes()->pluck('id'));
})->orDoesntHave('nodes');
return $query->where(function (Builder $query) {
return $query->whereHas('nodes', function (Builder $query) {
$query->whereIn('nodes.id', auth()->user()->accessibleNodes()->pluck('id'));
})->orDoesntHave('nodes');
});
}
}

View File

@ -71,10 +71,10 @@ class DatabasesRelationManager extends RelationManager
])
->actions([
DeleteAction::make()
->authorize(fn (Database $database) => auth()->user()->can('delete database', $database)),
->authorize(fn (Database $database) => auth()->user()->can('delete', $database)),
ViewAction::make()
->color('primary')
->hidden(fn () => !auth()->user()->can('viewList database')),
->hidden(fn () => !auth()->user()->can('viewAny', Database::class)),
]);
}
}

View File

@ -76,7 +76,7 @@ class MountResource extends Resource
->badge()
->icon(fn ($state) => $state ? 'tabler-writing-off' : 'tabler-writing')
->color(fn ($state) => $state ? 'success' : 'warning')
->formatStateUsing(fn ($state) => $state ? trans('admin/mount.toggles.read_only') : trans('admin/mount.toggles.writeable')),
->formatStateUsing(fn ($state) => $state ? trans('admin/mount.toggles.read_only') : trans('admin/mount.toggles.writable')),
])
->actions([
ViewAction::make()
@ -176,8 +176,10 @@ class MountResource extends Resource
{
$query = parent::getEloquentQuery();
return $query->whereHas('nodes', function (Builder $query) {
$query->whereIn('nodes.id', auth()->user()->accessibleNodes()->pluck('id'));
})->orDoesntHave('nodes');
return $query->where(function (Builder $query) {
return $query->whereHas('nodes', function (Builder $query) {
$query->whereIn('nodes.id', auth()->user()->accessibleNodes()->pluck('id'));
})->orDoesntHave('nodes');
});
}
}

View File

@ -97,7 +97,7 @@ class AllocationsRelationManager extends RelationManager
])
->groupedBulkActions([
DeleteBulkAction::make()
->authorize(fn () => auth()->user()->can('update node')),
->authorize(fn () => auth()->user()->can('update', $this->getOwnerRecord())),
]);
}
}

View File

@ -79,8 +79,6 @@ class ServerResource extends Resource
{
$query = parent::getEloquentQuery();
return $query->whereHas('node', function (Builder $query) {
$query->whereIn('id', auth()->user()->accessibleNodes()->pluck('id'));
});
return $query->whereIn('node_id', auth()->user()->accessibleNodes()->pluck('id'));
}
}

View File

@ -144,6 +144,7 @@ class CreateServer extends CreateRecord
->relationship('user', 'username')
->searchable(['username', 'email'])
->getOptionLabelFromRecordUsing(fn (User $user) => "$user->username ($user->email)")
->createOptionAction(fn (Action $action) => $action->authorize(fn () => auth()->user()->can('create', User::class)))
->createOptionForm([
TextInput::make('username')
->label(trans('admin/user.username'))
@ -205,6 +206,7 @@ class CreateServer extends CreateRecord
->where('node_id', $get('node_id'))
->whereNull('server_id'),
)
->createOptionAction(fn (Action $action) => $action->authorize(fn (Get $get) => auth()->user()->can('create', Node::find($get('node_id')))))
->createOptionForm(function (Get $get) {
$getPage = $get;

View File

@ -686,7 +686,7 @@ class EditServer extends EditRecord
ServerResource::getMountCheckboxList($get),
]),
Tab::make(trans('admin/server.databases'))
->hidden(fn () => !auth()->user()->can('viewList database'))
->hidden(fn () => !auth()->user()->can('viewAny', Database::class))
->icon('tabler-database')
->columns(4)
->schema([
@ -710,14 +710,14 @@ class EditServer extends EditRecord
->hintAction(
Action::make('Delete')
->label(trans('filament-actions::delete.single.modal.actions.delete.label'))
->authorize(fn (Database $database) => auth()->user()->can('delete database', $database))
->authorize(fn (Database $database) => auth()->user()->can('delete', $database))
->color('danger')
->icon('tabler-trash')
->requiresConfirmation()
->modalIcon('tabler-database-x')
->modalHeading(trans('admin/server.delete_db_heading'))
->modalSubmitActionLabel(fn (Get $get) => 'Delete ' . $get('database') . '?')
->modalDescription(fn (Get $get) => trans('admin/server.delete_db') . $get('database') . '?')
->modalSubmitActionLabel(trans('filament-actions::delete.single.label'))
->modalDescription(fn (Get $get) => trans('admin/server.delete_db', ['name' => $get('database')]))
->action(function (DatabaseManagementService $databaseManagementService, $record) {
$databaseManagementService->delete($record);
$this->fillForm();
@ -763,7 +763,7 @@ class EditServer extends EditRecord
->columnSpan(4),
FormActions::make([
Action::make('createDatabase')
->authorize(fn () => auth()->user()->can('create database'))
->authorize(fn () => auth()->user()->can('create', Database::class))
->disabled(fn () => DatabaseHost::query()->count() < 1)
->label(fn () => DatabaseHost::query()->count() < 1 ? trans('admin/server.no_db_hosts') : trans('admin/server.create_database'))
->color(fn () => DatabaseHost::query()->count() < 1 ? 'danger' : 'primary')
@ -851,7 +851,7 @@ class EditServer extends EditRecord
} catch (Exception) {
Notification::make()
->title(trans('admin/server.notifications.reinstall_failed'))
->body(trans('admin/server.error_connecting', ['node' => $server->node->name]))
->body(trans('admin/server.notifications.error_connecting', ['node' => $server->node->name]))
->danger()
->send();
}
@ -900,7 +900,7 @@ class EditServer extends EditRecord
Notification::make()
->warning()
->title(trans('admin/server.notifications.server_suspension'))
->body(trans('admin/server.error_connecting', ['node' => $server->node->name]))
->body(trans('admin/server.notifications.error_connecting', ['node' => $server->node->name]))
->send();
}
}),
@ -922,7 +922,7 @@ class EditServer extends EditRecord
Notification::make()
->warning()
->title(trans('admin/server.notifications.server_suspension'))
->body(trans('admin/server.error_connecting', ['node' => $server->node->name]))
->body(trans('admin/server.notifications.error_connecting', ['node' => $server->node->name]))
->send();
}
}),
@ -987,7 +987,7 @@ class EditServer extends EditRecord
} catch (Exception) {
Notification::make()
->title(trans('admin/server.notifications.reinstall_failed'))
->body(trans('admin/server.error_connecting', ['node' => $server->node->name]))
->body(trans('admin/server.notifications.error_connecting', ['node' => $server->node->name]))
->danger()
->send();
}
@ -1065,7 +1065,7 @@ class EditServer extends EditRecord
}
})
->hidden(fn () => $canForceDelete)
->authorize(fn (Server $server) => auth()->user()->can('delete server', $server)),
->authorize(fn (Server $server) => auth()->user()->can('delete', $server)),
Actions\Action::make('ForceDelete')
->color('danger')
->label(trans('filament-actions::force-delete.single.label'))
@ -1082,7 +1082,7 @@ class EditServer extends EditRecord
}
})
->visible(fn () => $canForceDelete)
->authorize(fn (Server $server) => auth()->user()->can('delete server', $server)),
->authorize(fn (Server $server) => auth()->user()->can('delete', $server)),
Actions\Action::make('console')
->label(trans('admin/server.console'))
->icon('tabler-terminal')

View File

@ -68,13 +68,13 @@ class ListServers extends ListRecords
->searchable(),
SelectColumn::make('allocation_id')
->label(trans('admin/server.primary_allocation'))
->hidden(!auth()->user()->can('update server'))
->hidden(!auth()->user()->can('update server')) // TODO: update to policy check (fn (Server $server) --> $server is empty)
->options(fn (Server $server) => $server->allocations->mapWithKeys(fn ($allocation) => [$allocation->id => $allocation->address]))
->selectablePlaceholder(false)
->sortable(),
TextColumn::make('allocation_id_readonly')
->label(trans('admin/server.primary_allocation'))
->hidden(auth()->user()->can('update server'))
->hidden(auth()->user()->can('update server')) // TODO: update to policy check (fn (Server $server) --> $server is empty)
->state(fn (Server $server) => $server->allocation->address),
TextColumn::make('image')->hidden(),
TextColumn::make('backups_count')

View File

@ -26,7 +26,7 @@ class RotateDatabasePasswordAction extends Action
$this->icon('tabler-refresh');
$this->authorize(fn (Database $database) => auth()->user()->can('update database', $database));
$this->authorize(fn (Database $database) => auth()->user()->can('update', $database));
$this->modalHeading(trans('admin/databasehost.rotate_password'));

View File

@ -288,6 +288,8 @@ class EditProfile extends BaseEditProfile
);
Activity::event('user:api-key.create')
->actor($user)
->subject($user)
->subject($token->accessToken)
->property('identifier', $token->accessToken->identifier)
->log();
@ -383,12 +385,12 @@ class EditProfile extends BaseEditProfile
'monospace' => 'monospace', //default
];
if (!Storage::disk('public')->exists('storage/fonts')) {
Storage::disk('public')->makeDirectory('storage/fonts');
if (!Storage::disk('public')->exists('fonts')) {
Storage::disk('public')->makeDirectory('fonts');
$this->fillForm();
}
foreach (Storage::disk('public')->allFiles('storage/fonts') as $file) {
foreach (Storage::disk('public')->allFiles('fonts') as $file) {
$fileInfo = pathinfo($file);
if ($fileInfo['extension'] === 'ttf') {

View File

@ -2,8 +2,10 @@
namespace App\Filament\Pages\Auth;
use App\Events\Auth\ProvidedAuthenticationToken;
use App\Extensions\Captcha\Providers\CaptchaProvider;
use App\Extensions\OAuth\Providers\OAuthProvider;
use App\Facades\Activity;
use App\Models\User;
use Filament\Facades\Filament;
use Filament\Forms\Components\Actions;
@ -54,14 +56,37 @@ class Login extends BaseLogin
if ($token === null) {
$this->verifyTwoFactor = true;
Activity::event('auth:checkpoint')
->withRequestMetadata()
->subject($user)
->log();
return null;
}
$isValidToken = $this->google2FA->verifyKey(
$user->totp_secret,
$token,
Config::integer('panel.auth.2fa.window'),
);
$isValidToken = false;
if (strlen($token) === $this->google2FA->getOneTimePasswordLength()) {
$isValidToken = $this->google2FA->verifyKey(
$user->totp_secret,
$token,
Config::integer('panel.auth.2fa.window'),
);
if ($isValidToken) {
event(new ProvidedAuthenticationToken($user));
}
} else {
foreach ($user->recoveryTokens as $recoveryToken) {
if (password_verify($token, $recoveryToken->token)) {
$isValidToken = true;
$recoveryToken->delete();
event(new ProvidedAuthenticationToken($user, true));
break;
}
}
}
if (!$isValidToken) {
// Buffer to prevent bruteforce
@ -108,7 +133,9 @@ class Login extends BaseLogin
{
return TextInput::make('2fa')
->label(trans('auth.two-factor-code'))
->hidden(fn () => !$this->verifyTwoFactor)
->hintIcon('tabler-question-mark')
->hintIconTooltip(trans('auth.two-factor-hint'))
->visible(fn () => $this->verifyTwoFactor)
->required()
->live();
}

View File

@ -2,43 +2,27 @@
namespace App\Filament\Server\Components;
use Closure;
use Filament\Support\Concerns\EvaluatesClosures;
use Filament\Widgets\StatsOverviewWidget\Stat;
use Illuminate\Contracts\Support\Htmlable;
use Illuminate\Contracts\View\View;
class SmallStatBlock extends Stat
{
protected string|Htmlable $label;
use EvaluatesClosures;
protected $value;
protected bool|Closure $copyOnClick = false;
public function label(string|Htmlable $label): static
public function copyOnClick(bool|Closure $copyOnClick = true): static
{
$this->label = $label;
$this->copyOnClick = $copyOnClick;
return $this;
}
public function value($value): static
public function shouldCopyOnClick(): bool
{
$this->value = $value;
return $this;
}
public function getLabel(): string|Htmlable
{
return $this->label;
}
public function getValue()
{
return value($this->value);
}
public function toHtml(): string
{
return $this->render()->render();
return $this->evaluate($this->copyOnClick);
}
public function render(): View

View File

@ -1,48 +0,0 @@
<?php
namespace App\Filament\Server\Components;
use Filament\Widgets\StatsOverviewWidget\Stat;
use Illuminate\Contracts\Support\Htmlable;
use Illuminate\Contracts\View\View;
class StatBlock extends Stat
{
protected string|Htmlable $label;
protected $value;
public function label(string|Htmlable $label): static
{
$this->label = $label;
return $this;
}
public function value($value): static
{
$this->value = $value;
return $this;
}
public function getLabel(): string|Htmlable
{
return $this->label;
}
public function getValue()
{
return value($this->value);
}
public function toHtml(): string
{
return $this->render()->render();
}
public function render(): View
{
return view('filament.components.server-data-block', $this->data());
}
}

View File

@ -2,6 +2,8 @@
namespace App\Filament\Server\Resources;
use App\Filament\Admin\Resources\UserResource\Pages\EditUser;
use App\Filament\Components\Tables\Columns\DateTimeColumn;
use App\Filament\Server\Resources\ActivityResource\Pages;
use App\Models\ActivityLog;
use App\Models\Permission;
@ -9,9 +11,20 @@ use App\Models\Role;
use App\Models\Server;
use App\Models\User;
use Filament\Facades\Filament;
use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Components\DateTimePicker;
use Filament\Forms\Components\KeyValue;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\TextInput;
use Filament\Resources\Resource;
use Filament\Tables\Actions\ViewAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Query\JoinClause;
use Illuminate\Support\Arr;
use Illuminate\Support\HtmlString;
class ActivityResource extends Resource
{
@ -25,12 +38,101 @@ class ActivityResource extends Resource
protected static ?string $navigationIcon = 'tabler-stack';
public static function table(Table $table): Table
{
/** @var Server $server */
$server = Filament::getTenant();
return $table
->paginated([25, 50])
->defaultPaginationPageOption(25)
->columns([
TextColumn::make('event')
->html()
->description(fn ($state) => $state)
->icon(fn (ActivityLog $activityLog) => $activityLog->getIcon())
->formatStateUsing(fn (ActivityLog $activityLog) => $activityLog->getLabel()),
TextColumn::make('user')
->state(function (ActivityLog $activityLog) use ($server) {
if (!$activityLog->actor instanceof User) {
return $activityLog->actor_id === null ? 'System' : 'Deleted user';
}
$user = $activityLog->actor->username;
// Only show the email if the actor is the server owner/ a subuser or if the viewing user is an admin
if (auth()->user()->isAdmin() || $server->owner_id === $activityLog->actor->id || $server->subusers->where('user_id', $activityLog->actor->id)->first()) {
$user .= " ({$activityLog->actor->email})";
}
return $user;
})
->tooltip(fn (ActivityLog $activityLog) => auth()->user()->can('seeIps activityLog') ? $activityLog->ip : '')
->url(fn (ActivityLog $activityLog) => $activityLog->actor instanceof User && auth()->user()->can('update', $activityLog->actor) ? EditUser::getUrl(['record' => $activityLog->actor], panel: 'admin') : '')
->grow(false),
DateTimeColumn::make('timestamp')
->since()
->sortable()
->grow(false),
])
->defaultSort('timestamp', 'desc')
->actions([
ViewAction::make()
//->visible(fn (ActivityLog $activityLog) => $activityLog->hasAdditionalMetadata())
->form([
Placeholder::make('event')
->content(fn (ActivityLog $activityLog) => new HtmlString($activityLog->getLabel())),
TextInput::make('user')
->formatStateUsing(function (ActivityLog $activityLog) use ($server) {
if (!$activityLog->actor instanceof User) {
return $activityLog->actor_id === null ? 'System' : 'Deleted user';
}
$user = $activityLog->actor->username;
// Only show the email if the actor is the server owner/ a subuser or if the viewing user is an admin
if (auth()->user()->isAdmin() || $server->owner_id === $activityLog->actor->id || $server->subusers->where('user_id', $activityLog->actor->id)->first()) {
$user .= " ({$activityLog->actor->email})";
}
if (auth()->user()->can('seeIps activityLog')) {
$user .= " - $activityLog->ip";
}
return $user;
})
->hintAction(
Action::make('edit')
->label(trans('filament-actions::edit.single.label'))
->icon('tabler-edit')
->visible(fn (ActivityLog $activityLog) => $activityLog->actor instanceof User && auth()->user()->can('update', $activityLog->actor))
->url(fn (ActivityLog $activityLog) => EditUser::getUrl(['record' => $activityLog->actor], panel: 'admin'))
),
DateTimePicker::make('timestamp'),
KeyValue::make('properties')
->label('Metadata')
->formatStateUsing(fn ($state) => Arr::dot($state)),
]),
])
->filters([
SelectFilter::make('event')
->options(fn (Table $table) => $table->getQuery()->pluck('event', 'event')->unique()->sort())
->searchable()
->preload(),
]);
}
public static function canViewAny(): bool
{
return auth()->user()->can(Permission::ACTION_ACTIVITY_READ, Filament::getTenant());
}
public static function getEloquentQuery(): Builder
{
/** @var Server $server */
$server = Filament::getTenant();
return ActivityLog::whereHas('subjects', fn (Builder $query) => $query->where('subject_id', $server->id))
return ActivityLog::whereHas('subjects', fn (Builder $query) => $query->where('subject_id', $server->id)->where('subject_type', $server->getMorphClass()))
->whereNotIn('activity_logs.event', ActivityLog::DISABLED_EVENTS)
->when(config('activity.hide_admin_activity'), function (Builder $builder) use ($server) {
// We could do this with a query and a lot of joins, but that gets pretty
@ -51,11 +153,6 @@ class ActivityResource extends Resource
});
}
public static function canViewAny(): bool
{
return auth()->user()->can(Permission::ACTION_ACTIVITY_READ, Filament::getTenant());
}
public static function getPages(): array
{
return [

View File

@ -2,114 +2,13 @@
namespace App\Filament\Server\Resources\ActivityResource\Pages;
use App\Filament\Admin\Resources\UserResource\Pages\EditUser;
use App\Filament\Server\Resources\ActivityResource;
use App\Models\ActivityLog;
use App\Filament\Components\Tables\Columns\DateTimeColumn;
use App\Models\Server;
use App\Models\User;
use Filament\Facades\Filament;
use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Components\DateTimePicker;
use Filament\Forms\Components\KeyValue;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\TextInput;
use Filament\Resources\Pages\ListRecords;
use Filament\Tables\Actions\ViewAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;
use Illuminate\Support\Arr;
use Illuminate\Support\HtmlString;
class ListActivities extends ListRecords
{
protected static string $resource = ActivityResource::class;
public function table(Table $table): Table
{
/** @var Server $server */
$server = Filament::getTenant();
return $table
->paginated([25, 50])
->defaultPaginationPageOption(25)
->columns([
TextColumn::make('event')
->html()
->description(fn ($state) => $state)
->icon(fn (ActivityLog $activityLog) => $activityLog->getIcon())
->formatStateUsing(fn (ActivityLog $activityLog) => $activityLog->getLabel()),
TextColumn::make('user')
->state(function (ActivityLog $activityLog) use ($server) {
if (!$activityLog->actor instanceof User) {
return $activityLog->actor_id === null ? 'System' : 'Deleted user';
}
$user = $activityLog->actor->username;
// Only show the email if the actor is the server owner/ a subuser or if the viewing user is an admin
if (auth()->user()->isAdmin() || $server->owner_id === $activityLog->actor->id || $server->subusers->where('user_id', $activityLog->actor->id)->first()) {
$user .= " ({$activityLog->actor->email})";
}
return $user;
})
->tooltip(fn (ActivityLog $activityLog) => auth()->user()->can('seeIps activityLog') ? $activityLog->ip : '')
->url(fn (ActivityLog $activityLog) => $activityLog->actor instanceof User && auth()->user()->can('update user') ? EditUser::getUrl(['record' => $activityLog->actor], panel: 'admin') : '')
->grow(false),
DateTimeColumn::make('timestamp')
->since()
->sortable()
->grow(false),
])
->defaultSort('timestamp', 'desc')
->actions([
ViewAction::make()
//->visible(fn (ActivityLog $activityLog) => $activityLog->hasAdditionalMetadata())
->form([
Placeholder::make('event')
->content(fn (ActivityLog $activityLog) => new HtmlString($activityLog->getLabel())),
TextInput::make('user')
->formatStateUsing(function (ActivityLog $activityLog) use ($server) {
if (!$activityLog->actor instanceof User) {
return $activityLog->actor_id === null ? 'System' : 'Deleted user';
}
$user = $activityLog->actor->username;
// Only show the email if the actor is the server owner/ a subuser or if the viewing user is an admin
if (auth()->user()->isAdmin() || $server->owner_id === $activityLog->actor->id || $server->subusers->where('user_id', $activityLog->actor->id)->first()) {
$user .= " ({$activityLog->actor->email})";
}
if (auth()->user()->can('seeIps activityLog')) {
$user .= " - $activityLog->ip";
}
return $user;
})
->hintAction(
Action::make('edit')
->label(trans('filament-actions::edit.single.label'))
->icon('tabler-edit')
->visible(fn (ActivityLog $activityLog) => $activityLog->actor instanceof User && auth()->user()->can('update user'))
->url(fn (ActivityLog $activityLog) => EditUser::getUrl(['record' => $activityLog->actor], panel: 'admin'))
),
DateTimePicker::make('timestamp'),
KeyValue::make('properties')
->label('Metadata')
->formatStateUsing(fn ($state) => Arr::dot($state)),
]),
])
->filters([
SelectFilter::make('event')
->options(fn (Table $table) => $table->getQuery()->pluck('event', 'event')->unique()->sort())
->searchable()
->preload(),
]);
}
public function getBreadcrumbs(): array
{
return [];

View File

@ -2,12 +2,18 @@
namespace App\Filament\Server\Resources;
use App\Facades\Activity;
use App\Filament\Server\Resources\AllocationResource\Pages;
use App\Models\Allocation;
use App\Models\Permission;
use App\Models\Server;
use Filament\Facades\Filament;
use Filament\Resources\Resource;
use Filament\Tables\Actions\DetachAction;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Columns\TextInputColumn;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Model;
class AllocationResource extends Resource
@ -22,6 +28,61 @@ class AllocationResource extends Resource
protected static ?string $navigationIcon = 'tabler-network';
public static function table(Table $table): Table
{
/** @var Server $server */
$server = Filament::getTenant();
return $table
->columns([
TextColumn::make('ip')
->label('Address')
->formatStateUsing(fn (Allocation $allocation) => $allocation->alias),
TextColumn::make('alias')
->hidden(),
TextColumn::make('port'),
TextInputColumn::make('notes')
->visibleFrom('sm')
->disabled(fn () => !auth()->user()->can(Permission::ACTION_ALLOCATION_UPDATE, $server))
->label('Notes')
->placeholder('No Notes'),
IconColumn::make('primary')
->icon(fn ($state) => match ($state) {
true => 'tabler-star-filled',
default => 'tabler-star',
})
->color(fn ($state) => match ($state) {
true => 'warning',
default => 'gray',
})
->action(function (Allocation $allocation) use ($server) {
if (auth()->user()->can(PERMISSION::ACTION_ALLOCATION_UPDATE, $server)) {
return $server->update(['allocation_id' => $allocation->id]);
}
})
->default(fn (Allocation $allocation) => $allocation->id === $server->allocation_id)
->label('Primary'),
])
->actions([
DetachAction::make()
->authorize(fn () => auth()->user()->can(Permission::ACTION_ALLOCATION_DELETE, $server))
->label('Delete')
->icon('tabler-trash')
->hidden(fn (Allocation $allocation) => $allocation->id === $server->allocation_id)
->action(function (Allocation $allocation) {
Allocation::query()->where('id', $allocation->id)->update([
'notes' => null,
'server_id' => null,
]);
Activity::event('server:allocation.delete')
->subject($allocation)
->property('allocation', $allocation->toString())
->log();
}),
]);
}
// TODO: find better way handle server conflict state
public static function canAccess(): bool
{

View File

@ -4,85 +4,24 @@ namespace App\Filament\Server\Resources\AllocationResource\Pages;
use App\Facades\Activity;
use App\Filament\Server\Resources\AllocationResource;
use App\Models\Allocation;
use App\Models\Permission;
use App\Models\Server;
use App\Services\Allocations\FindAssignableAllocationService;
use Filament\Actions;
use Filament\Actions\Action;
use Filament\Facades\Filament;
use Filament\Resources\Pages\ListRecords;
use Filament\Tables\Actions\DetachAction;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Columns\TextInputColumn;
use Filament\Tables\Table;
class ListAllocations extends ListRecords
{
protected static string $resource = AllocationResource::class;
public function table(Table $table): Table
{
/** @var Server $server */
$server = Filament::getTenant();
return $table
->columns([
TextColumn::make('ip')
->label('Address')
->formatStateUsing(fn (Allocation $allocation) => $allocation->alias),
TextColumn::make('alias')
->hidden(),
TextColumn::make('port'),
TextInputColumn::make('notes')
->visibleFrom('sm')
->disabled(fn () => !auth()->user()->can(Permission::ACTION_ALLOCATION_UPDATE, $server))
->label('Notes')
->placeholder('No Notes'),
IconColumn::make('primary')
->icon(fn ($state) => match ($state) {
true => 'tabler-star-filled',
default => 'tabler-star',
})
->color(fn ($state) => match ($state) {
true => 'warning',
default => 'gray',
})
->action(function (Allocation $allocation) use ($server) {
if (auth()->user()->can(PERMISSION::ACTION_ALLOCATION_UPDATE, $server)) {
return $server->update(['allocation_id' => $allocation->id]);
}
})
->default(fn (Allocation $allocation) => $allocation->id === $server->allocation_id)
->label('Primary'),
])
->actions([
DetachAction::make()
->authorize(fn () => auth()->user()->can(Permission::ACTION_ALLOCATION_DELETE, $server))
->label('Delete')
->icon('tabler-trash')
->hidden(fn (Allocation $allocation) => $allocation->id === $server->allocation_id)
->action(function (Allocation $allocation) {
Allocation::query()->where('id', $allocation->id)->update([
'notes' => null,
'server_id' => null,
]);
Activity::event('server:allocation.delete')
->subject($allocation)
->property('allocation', $allocation->toString())
->log();
}),
]);
}
protected function getHeaderActions(): array
{
/** @var Server $server */
$server = Filament::getTenant();
return [
Actions\Action::make('addAllocation')
Action::make('addAllocation')
->authorize(fn () => auth()->user()->can(Permission::ACTION_ALLOCATION_CREATE, $server))
->label(fn () => $server->allocations()->count() >= $server->allocation_limit ? 'Allocation limit reached' : 'Add Allocation')
->hidden(fn () => !config('panel.client_features.allocations.enabled'))

View File

@ -2,13 +2,35 @@
namespace App\Filament\Server\Resources;
use App\Enums\BackupStatus;
use App\Enums\ServerState;
use App\Facades\Activity;
use App\Filament\Server\Resources\BackupResource\Pages;
use App\Http\Controllers\Api\Client\Servers\BackupController;
use App\Models\Backup;
use App\Models\Permission;
use App\Models\Server;
use App\Repositories\Daemon\DaemonBackupRepository;
use App\Services\Backups\DownloadLinkService;
use App\Filament\Components\Tables\Columns\BytesColumn;
use App\Filament\Components\Tables\Columns\DateTimeColumn;
use Filament\Facades\Filament;
use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Form;
use Filament\Notifications\Notification;
use Filament\Resources\Resource;
use Filament\Tables\Actions\Action;
use Filament\Tables\Actions\ActionGroup;
use Filament\Tables\Actions\DeleteAction;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Request;
class BackupResource extends Resource
{
@ -44,8 +66,121 @@ class BackupResource extends Resource
return null;
}
return $count >= $limit ? 'danger'
: ($count >= $limit * self::WARNING_THRESHOLD ? 'warning' : 'success');
return $count >= $limit ? 'danger' : ($count >= $limit * self::WARNING_THRESHOLD ? 'warning' : 'success');
}
public static function form(Form $form): Form
{
return $form
->schema([
TextInput::make('name')
->label('Name')
->columnSpanFull(),
TextArea::make('ignored')
->columnSpanFull()
->label('Ignored Files & Directories'),
Toggle::make('is_locked')
->label('Lock?')
->helperText('Prevents this backup from being deleted until explicitly unlocked.'),
]);
}
public static function table(Table $table): Table
{
/** @var Server $server */
$server = Filament::getTenant();
return $table
->columns([
TextColumn::make('name')
->searchable(),
BytesColumn::make('bytes')
->label('Size'),
DateTimeColumn::make('created_at')
->label('Created')
->since()
->sortable(),
TextColumn::make('status')
->label('Status')
->badge(),
IconColumn::make('is_locked')
->visibleFrom('md')
->label('Lock Status')
->trueIcon('tabler-lock')
->falseIcon('tabler-lock-open'),
])
->actions([
ActionGroup::make([
Action::make('lock')
->icon(fn (Backup $backup) => !$backup->is_locked ? 'tabler-lock' : 'tabler-lock-open')
->authorize(fn () => auth()->user()->can(Permission::ACTION_BACKUP_DELETE, $server))
->label(fn (Backup $backup) => !$backup->is_locked ? 'Lock' : 'Unlock')
->action(fn (BackupController $backupController, Backup $backup, Request $request) => $backupController->toggleLock($request, $server, $backup))
->visible(fn (Backup $backup) => $backup->status === BackupStatus::Successful),
Action::make('download')
->color('primary')
->icon('tabler-download')
->authorize(fn () => auth()->user()->can(Permission::ACTION_BACKUP_DOWNLOAD, $server))
->url(fn (DownloadLinkService $downloadLinkService, Backup $backup, Request $request) => $downloadLinkService->handle($backup, $request->user()), true)
->visible(fn (Backup $backup) => $backup->status === BackupStatus::Successful),
Action::make('restore')
->color('success')
->icon('tabler-folder-up')
->authorize(fn () => auth()->user()->can(Permission::ACTION_BACKUP_RESTORE, $server))
->form([
Placeholder::make('')
->helperText('Your server will be stopped. You will not be able to control the power state, access the file manager, or create additional backups until this process is completed.'),
Checkbox::make('truncate')
->label('Delete all files before restoring backup?'),
])
->action(function (Backup $backup, $data, DaemonBackupRepository $daemonRepository, DownloadLinkService $downloadLinkService) use ($server) {
if (!is_null($server->status)) {
return Notification::make()
->danger()
->title('Backup Restore Failed')
->body('This server is not currently in a state that allows for a backup to be restored.')
->send();
}
if (!$backup->is_successful && is_null($backup->completed_at)) { //TODO Change to Notifications
return Notification::make()
->danger()
->title('Backup Restore Failed')
->body('This backup cannot be restored at this time: not completed or failed.')
->send();
}
$log = Activity::event('server:backup.restore')
->subject($backup)
->property(['name' => $backup->name, 'truncate' => $data['truncate']]);
$log->transaction(function () use ($downloadLinkService, $daemonRepository, $backup, $server, $data) {
// If the backup is for an S3 file we need to generate a unique Download link for
// it that will allow daemon to actually access the file.
if ($backup->disk === Backup::ADAPTER_AWS_S3) {
$url = $downloadLinkService->handle($backup, auth()->user());
}
// Update the status right away for the server so that we know not to allow certain
// actions against it via the Panel API.
$server->update(['status' => ServerState::RestoringBackup]);
$daemonRepository->setServer($server)->restore($backup, $url ?? null, $data['truncate']);
});
return Notification::make()
->title('Restoring Backup')
->send();
})
->visible(fn (Backup $backup) => $backup->status === BackupStatus::Successful),
DeleteAction::make('delete')
->disabled(fn (Backup $backup) => $backup->is_locked)
->modalDescription(fn (Backup $backup) => 'Do you wish to delete, ' . $backup->name . '?')
->modalSubmitActionLabel('Delete Backup')
->action(fn (BackupController $backupController, Backup $backup, Request $request) => $backupController->delete($request, $server, $backup))
->visible(fn (Backup $backup) => $backup->status !== BackupStatus::InProgress),
]),
]);
}
// TODO: find better way handle server conflict state

View File

@ -2,165 +2,28 @@
namespace App\Filament\Server\Resources\BackupResource\Pages;
use App\Enums\BackupStatus;
use App\Enums\ServerState;
use App\Facades\Activity;
use App\Filament\Server\Resources\BackupResource;
use App\Http\Controllers\Api\Client\Servers\BackupController;
use App\Models\Backup;
use App\Models\Permission;
use App\Models\Server;
use App\Repositories\Daemon\DaemonBackupRepository;
use App\Services\Backups\DownloadLinkService;
use App\Services\Backups\InitiateBackupService;
use App\Filament\Components\Tables\Columns\BytesColumn;
use App\Filament\Components\Tables\Columns\DateTimeColumn;
use Filament\Actions;
use Filament\Actions\CreateAction;
use Filament\Facades\Filament;
use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Form;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\ListRecords;
use Filament\Tables\Actions\Action;
use Filament\Tables\Actions\ActionGroup;
use Filament\Tables\Actions\DeleteAction;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Http\Request;
use Symfony\Component\HttpKernel\Exception\HttpException;
class ListBackups extends ListRecords
{
protected static string $resource = BackupResource::class;
protected static bool $canCreateAnother = false;
public function form(Form $form): Form
{
return $form
->schema([
TextInput::make('name')
->label('Name')
->columnSpanFull(),
TextArea::make('ignored')
->columnSpanFull()
->label('Ignored Files & Directories'),
Toggle::make('is_locked')
->label('Lock?')
->helperText('Prevents this backup from being deleted until explicitly unlocked.'),
]);
}
public function table(Table $table): Table
{
/** @var Server $server */
$server = Filament::getTenant();
return $table
->columns([
TextColumn::make('name')
->searchable(),
BytesColumn::make('bytes')
->label('Size'),
DateTimeColumn::make('created_at')
->label('Created')
->since()
->sortable(),
TextColumn::make('status')
->label('Status')
->badge(),
IconColumn::make('is_locked')
->visibleFrom('md')
->label('Lock Status')
->trueIcon('tabler-lock')
->falseIcon('tabler-lock-open'),
])
->actions([
ActionGroup::make([
Action::make('lock')
->icon(fn (Backup $backup) => !$backup->is_locked ? 'tabler-lock' : 'tabler-lock-open')
->authorize(fn () => auth()->user()->can(Permission::ACTION_BACKUP_DELETE, $server))
->label(fn (Backup $backup) => !$backup->is_locked ? 'Lock' : 'Unlock')
->action(fn (BackupController $backupController, Backup $backup, Request $request) => $backupController->toggleLock($request, $server, $backup))
->visible(fn (Backup $backup) => $backup->status === BackupStatus::Successful),
Action::make('download')
->color('primary')
->icon('tabler-download')
->authorize(fn () => auth()->user()->can(Permission::ACTION_BACKUP_DOWNLOAD, $server))
->url(fn (DownloadLinkService $downloadLinkService, Backup $backup, Request $request) => $downloadLinkService->handle($backup, $request->user()), true)
->visible(fn (Backup $backup) => $backup->status === BackupStatus::Successful),
Action::make('restore')
->color('success')
->icon('tabler-folder-up')
->authorize(fn () => auth()->user()->can(Permission::ACTION_BACKUP_RESTORE, $server))
->form([
Placeholder::make('')
->helperText('Your server will be stopped. You will not be able to control the power state, access the file manager, or create additional backups until this process is completed.'),
Checkbox::make('truncate')
->label('Delete all files before restoring backup?'),
])
->action(function (Backup $backup, $data, DaemonBackupRepository $daemonRepository, DownloadLinkService $downloadLinkService) use ($server) {
if (!is_null($server->status)) {
return Notification::make()
->danger()
->title('Backup Restore Failed')
->body('This server is not currently in a state that allows for a backup to be restored.')
->send();
}
if (!$backup->is_successful && is_null($backup->completed_at)) { //TODO Change to Notifications
return Notification::make()
->danger()
->title('Backup Restore Failed')
->body('This backup cannot be restored at this time: not completed or failed.')
->send();
}
$log = Activity::event('server:backup.restore')
->subject($backup)
->property(['name' => $backup->name, 'truncate' => $data['truncate']]);
$log->transaction(function () use ($downloadLinkService, $daemonRepository, $backup, $server, $data) {
// If the backup is for an S3 file we need to generate a unique Download link for
// it that will allow daemon to actually access the file.
if ($backup->disk === Backup::ADAPTER_AWS_S3) {
$url = $downloadLinkService->handle($backup, auth()->user());
}
// Update the status right away for the server so that we know not to allow certain
// actions against it via the Panel API.
$server->update(['status' => ServerState::RestoringBackup]);
$daemonRepository->setServer($server)->restore($backup, $url ?? null, $data['truncate']);
});
return Notification::make()
->title('Restoring Backup')
->send();
})
->visible(fn (Backup $backup) => $backup->status === BackupStatus::Successful),
DeleteAction::make('delete')
->disabled(fn (Backup $backup) => $backup->is_locked)
->modalDescription(fn (Backup $backup) => 'Do you wish to delete, ' . $backup->name . '?')
->modalSubmitActionLabel('Delete Backup')
->action(fn (BackupController $backupController, Backup $backup, Request $request) => $backupController->delete($request, $server, $backup))
->visible(fn (Backup $backup) => $backup->status !== BackupStatus::InProgress),
]),
]);
}
protected function getHeaderActions(): array
{
/** @var Server $server */
$server = Filament::getTenant();
return [
Actions\CreateAction::make()
CreateAction::make()
->authorize(fn () => auth()->user()->can(Permission::ACTION_BACKUP_CREATE, $server))
->label(fn () => $server->backups()->count() >= $server->backup_limit ? 'Backup limit reached' : 'Create Backup')
->disabled(fn () => $server->backups()->count() >= $server->backup_limit)

View File

@ -2,13 +2,23 @@
namespace App\Filament\Server\Resources;
use App\Filament\Components\Forms\Actions\RotateDatabasePasswordAction;
use App\Filament\Components\Tables\Columns\DateTimeColumn;
use App\Filament\Server\Resources\DatabaseResource\Pages;
use App\Models\Database;
use App\Models\Permission;
use App\Models\Server;
use App\Services\Databases\DatabaseManagementService;
use Filament\Facades\Filament;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables\Actions\DeleteAction;
use Filament\Tables\Actions\ViewAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Model;
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
class DatabaseResource extends Resource
{
@ -42,9 +52,65 @@ class DatabaseResource extends Resource
return null;
}
return $count >= $limit
? 'danger'
: ($count >= $limit * self::WARNING_THRESHOLD ? 'warning' : 'success');
return $count >= $limit ? 'danger' : ($count >= $limit * self::WARNING_THRESHOLD ? 'warning' : 'success');
}
public static function form(Form $form): Form
{
/** @var Server $server */
$server = Filament::getTenant();
return $form
->schema([
TextInput::make('host')
->formatStateUsing(fn (Database $database) => $database->address())
->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null),
TextInput::make('database')
->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null),
TextInput::make('username')
->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null),
TextInput::make('password')
->password()->revealable()
->hidden(fn () => !auth()->user()->can(Permission::ACTION_DATABASE_VIEW_PASSWORD, $server))
->hintAction(
RotateDatabasePasswordAction::make()
->authorize(fn () => auth()->user()->can(Permission::ACTION_DATABASE_UPDATE, $server))
)
->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null)
->formatStateUsing(fn (Database $database) => $database->password),
TextInput::make('remote')
->label('Connections From'),
TextInput::make('max_connections')
->formatStateUsing(fn (Database $database) => $database->max_connections === 0 ? $database->max_connections : 'Unlimited'),
TextInput::make('jdbc')
->label('JDBC Connection String')
->password()->revealable()
->hidden(!auth()->user()->can(Permission::ACTION_DATABASE_VIEW_PASSWORD, $server))
->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null)
->columnSpanFull()
->formatStateUsing(fn (Database $database) => $database->jdbc),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('host')
->state(fn (Database $database) => $database->address())
->badge(),
TextColumn::make('database'),
TextColumn::make('username'),
TextColumn::make('remote'),
DateTimeColumn::make('created_at')
->sortable(),
])
->actions([
ViewAction::make()
->modalHeading(fn (Database $database) => 'Viewing ' . $database->database),
DeleteAction::make()
->using(fn (Database $database, DatabaseManagementService $service) => $service->delete($database)),
]);
}
// TODO: find better way handle server conflict state

View File

@ -2,12 +2,8 @@
namespace App\Filament\Server\Resources\DatabaseResource\Pages;
use App\Filament\Components\Forms\Actions\RotateDatabasePasswordAction;
use App\Filament\Components\Tables\Columns\DateTimeColumn;
use App\Filament\Server\Resources\DatabaseResource;
use App\Models\Database;
use App\Models\DatabaseHost;
use App\Models\Permission;
use App\Models\Server;
use App\Services\Databases\DatabaseManagementService;
use Filament\Actions\CreateAction;
@ -15,76 +11,12 @@ use Filament\Facades\Filament;
use Filament\Forms\Components\Grid;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Resources\Pages\ListRecords;
use Filament\Tables\Actions\DeleteAction;
use Filament\Tables\Actions\ViewAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
class ListDatabases extends ListRecords
{
protected static string $resource = DatabaseResource::class;
public function form(Form $form): Form
{
/** @var Server $server */
$server = Filament::getTenant();
return $form
->schema([
TextInput::make('host')
->formatStateUsing(fn (Database $database) => $database->address())
->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null),
TextInput::make('database')
->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null),
TextInput::make('username')
->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null),
TextInput::make('password')
->password()->revealable()
->hidden(fn () => !auth()->user()->can(Permission::ACTION_DATABASE_VIEW_PASSWORD, $server))
->hintAction(
RotateDatabasePasswordAction::make()
->authorize(fn () => auth()->user()->can(Permission::ACTION_DATABASE_UPDATE, $server))
)
->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null)
->formatStateUsing(fn (Database $database) => $database->password),
TextInput::make('remote')
->label('Connections From'),
TextInput::make('max_connections')
->formatStateUsing(fn (Database $database) => $database->max_connections === 0 ? $database->max_connections : 'Unlimited'),
TextInput::make('jdbc')
->label('JDBC Connection String')
->password()->revealable()
->hidden(!auth()->user()->can(Permission::ACTION_DATABASE_VIEW_PASSWORD, $server))
->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null)
->columnSpanFull()
->formatStateUsing(fn (Database $database) => $database->jdbc),
]);
}
public function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('host')
->state(fn (Database $database) => $database->address())
->badge(),
TextColumn::make('database'),
TextColumn::make('username'),
TextColumn::make('remote'),
DateTimeColumn::make('created_at')
->sortable(),
])
->actions([
ViewAction::make()
->modalHeading(fn (Database $database) => 'Viewing ' . $database->database),
DeleteAction::make()
->using(fn (Database $database, DatabaseManagementService $service) => $service->delete($database)),
]);
}
protected function getHeaderActions(): array
{
/** @var Server $server */

View File

@ -2,6 +2,8 @@
namespace App\Filament\Server\Resources;
use App\Facades\Activity;
use App\Filament\Components\Tables\Columns\DateTimeColumn;
use App\Filament\Server\Resources\ScheduleResource\Pages;
use App\Filament\Server\Resources\ScheduleResource\RelationManagers\TasksRelationManager;
use App\Helpers\Utilities;
@ -23,6 +25,12 @@ use Filament\Forms\Set;
use Filament\Notifications\Notification;
use Filament\Resources\Resource;
use Filament\Support\Exceptions\Halt;
use Filament\Tables\Actions\DeleteAction;
use Filament\Tables\Actions\EditAction;
use Filament\Tables\Actions\ViewAction;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Model;
class ScheduleResource extends Resource
@ -303,6 +311,44 @@ class ScheduleResource extends Resource
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('name')
->searchable(),
TextColumn::make('cron')
->state(fn (Schedule $schedule) => $schedule->cron_minute . ' ' . $schedule->cron_hour . ' ' . $schedule->cron_day_of_month . ' ' . $schedule->cron_month . ' ' . $schedule->cron_day_of_week),
TextColumn::make('status')
->state(fn (Schedule $schedule) => !$schedule->is_active ? 'Inactive' : ($schedule->is_processing ? 'Processing' : 'Active')),
IconColumn::make('only_when_online')
->boolean()
->sortable(),
DateTimeColumn::make('last_run_at')
->label('Last run')
->placeholder('Never')
->since()
->sortable(),
DateTimeColumn::make('next_run_at')
->label('Next run')
->placeholder('Never')
->since()
->sortable()
->state(fn (Schedule $schedule) => $schedule->is_active ? $schedule->next_run_at : null),
])
->actions([
ViewAction::make(),
EditAction::make(),
DeleteAction::make()
->after(function (Schedule $schedule) {
Activity::event('server:schedule.delete')
->subject($schedule)
->property('name', $schedule->name)
->log();
}),
]);
}
public static function getRelations(): array
{
return [

View File

@ -2,65 +2,19 @@
namespace App\Filament\Server\Resources\ScheduleResource\Pages;
use App\Facades\Activity;
use App\Filament\Server\Resources\ScheduleResource;
use App\Models\Schedule;
use App\Filament\Components\Tables\Columns\DateTimeColumn;
use Filament\Actions;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
use Filament\Tables\Actions\DeleteAction;
use Filament\Tables\Actions\EditAction;
use Filament\Tables\Actions\ViewAction;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
class ListSchedules extends ListRecords
{
protected static string $resource = ScheduleResource::class;
public function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('name')
->searchable(),
TextColumn::make('cron')
->state(fn (Schedule $schedule) => $schedule->cron_minute . ' ' . $schedule->cron_hour . ' ' . $schedule->cron_day_of_month . ' ' . $schedule->cron_month . ' ' . $schedule->cron_day_of_week),
TextColumn::make('status')
->state(fn (Schedule $schedule) => !$schedule->is_active ? 'Inactive' : ($schedule->is_processing ? 'Processing' : 'Active')),
IconColumn::make('only_when_online')
->boolean()
->sortable(),
DateTimeColumn::make('last_run_at')
->label('Last run')
->placeholder('Never')
->since()
->sortable(),
DateTimeColumn::make('next_run_at')
->label('Next run')
->placeholder('Never')
->since()
->sortable()
->state(fn (Schedule $schedule) => $schedule->is_active ? $schedule->next_run_at : null),
])
->actions([
ViewAction::make(),
EditAction::make(),
DeleteAction::make()
->after(function (Schedule $schedule) {
Activity::event('server:schedule.delete')
->subject($schedule)
->property('name', $schedule->name)
->log();
}),
]);
}
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make()->label('New Schedule'),
CreateAction::make()
->label('New Schedule'),
];
}

View File

@ -7,7 +7,8 @@ use App\Filament\Server\Resources\ScheduleResource;
use App\Models\Permission;
use App\Models\Schedule;
use App\Services\Schedules\ProcessScheduleService;
use Filament\Actions;
use Filament\Actions\Action;
use Filament\Actions\EditAction;
use Filament\Facades\Filament;
use Filament\Resources\Pages\ViewRecord;
@ -18,7 +19,7 @@ class ViewSchedule extends ViewRecord
protected function getHeaderActions(): array
{
return [
Actions\Action::make('runNow')
Action::make('runNow')
->authorize(fn () => auth()->user()->can(Permission::ACTION_SCHEDULE_UPDATE, Filament::getTenant()))
->label(fn (Schedule $schedule) => $schedule->tasks->count() === 0 ? 'No tasks' : ($schedule->is_processing ? 'Processing' : 'Run now'))
->color(fn (Schedule $schedule) => $schedule->tasks->count() === 0 || $schedule->is_processing ? 'warning' : 'primary')
@ -33,7 +34,7 @@ class ViewSchedule extends ViewRecord
$this->fillForm();
}),
Actions\EditAction::make(),
EditAction::make(),
];
}

View File

@ -6,8 +6,10 @@ use App\Enums\ContainerStatus;
use App\Filament\Server\Components\SmallStatBlock;
use App\Models\Server;
use Carbon\CarbonInterface;
use Filament\Notifications\Notification;
use Filament\Widgets\StatsOverviewWidget;
use Illuminate\Support\Number;
use Livewire\Attributes\On;
class ServerOverview extends StatsOverviewWidget
{
@ -19,14 +21,10 @@ class ServerOverview extends StatsOverviewWidget
{
return [
SmallStatBlock::make('Name', $this->server->name)
->extraAttributes([
'class' => 'overflow-x-auto',
]),
->copyOnClick(fn () => request()->isSecure()),
SmallStatBlock::make('Status', $this->status()),
SmallStatBlock::make('Address', $this->server->allocation->address)
->extraAttributes([
'class' => 'overflow-x-auto',
]),
->copyOnClick(fn () => request()->isSecure()),
SmallStatBlock::make('CPU', $this->cpuUsage()),
SmallStatBlock::make('Memory', $this->memoryUsage()),
SmallStatBlock::make('Disk', $this->diskUsage()),
@ -93,4 +91,16 @@ class ServerOverview extends StatsOverviewWidget
return $used . ($this->server->disk > 0 ? ' / ' . $total : ' / ∞');
}
#[On('copyClick')]
public function copyClick(string $value): void
{
$this->js("window.navigator.clipboard.writeText('{$value}');");
Notification::make()
->title('Copied to clipboard')
->body($value)
->success()
->send();
}
}

View File

@ -58,7 +58,7 @@ abstract class SubuserRequest extends ClientApiRequest
$server = $this->route()->parameter('server');
// If we are an admin or the server owner, no need to perform these checks.
if ($user->can('update server', $server) || $user->id === $server->owner_id) {
if ($user->can('update', $server) || $user->id === $server->owner_id) {
return;
}

View File

@ -4,7 +4,7 @@ namespace App\Listeners\Auth;
use App\Facades\Activity;
use Illuminate\Auth\Events\Failed;
use App\Events\Auth\DirectLogin;
use Illuminate\Auth\Events\Login;
class AuthenticationListener
{
@ -12,9 +12,10 @@ class AuthenticationListener
* Handles an authentication event by logging the user and information about
* the request.
*/
public function handle(Failed|DirectLogin $event): void
public function handle(Failed|Login $event): void
{
$activity = Activity::withRequestMetadata();
if ($event->user) {
$activity = $activity->subject($event->user);
}

View File

@ -2,22 +2,14 @@
namespace App\Listeners\Auth;
use Illuminate\Http\Request;
use App\Facades\Activity;
use Illuminate\Auth\Events\PasswordReset;
class PasswordResetListener
{
protected Request $request;
public function __construct(Request $request)
{
$this->request = $request;
}
public function handle(PasswordReset $event): void
{
Activity::event('event:password-reset')
Activity::event('auth:password-reset')
->withRequestMetadata()
->subject($event->user)
->log();

View File

@ -265,8 +265,13 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
*/
public function accessibleServers(): Builder
{
if ($this->canned('viewList server')) {
return Server::query();
if ($this->canned('viewAny', Server::class)) {
return Server::select('servers.*')
->leftJoin('subusers', 'subusers.server_id', '=', 'servers.id')
->where(function (Builder $builder) {
$builder->where('servers.owner_id', $this->id)->orWhere('subusers.user_id', $this->id)->orWhereIn('servers.node_id', $this->accessibleNodes()->pluck('id'));
})
->distinct('servers.id');
}
return $this->directAccessibleServers();
@ -278,8 +283,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
*/
public function directAccessibleServers(): Builder
{
return Server::query()
->select('servers.*')
return Server::select('servers.*')
->leftJoin('subusers', 'subusers.server_id', '=', 'servers.id')
->where(function (Builder $builder) {
$builder->where('servers.owner_id', $this->id)->orWhere('subusers.user_id', $this->id);
@ -314,12 +318,12 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
protected function checkPermission(Server $server, string $permission = ''): bool
{
if ($this->canned('update server', $server) || $server->owner_id === $this->id) {
if ($this->canned('update', $server) || $server->owner_id === $this->id) {
return true;
}
// If the user only has "view" permissions allow viewing the console
if ($permission === Permission::ACTION_WEBSOCKET_CONNECT && $this->canned('view server', $server)) {
if ($permission === Permission::ACTION_WEBSOCKET_CONNECT && $this->canned('view', $server)) {
return true;
}
@ -434,7 +438,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
public function canAccessTenant(Model $tenant): bool
{
if ($tenant instanceof Server) {
if ($this->canned('view server', $tenant) || $tenant->owner_id === $this->id) {
if ($this->canned('view', $tenant) || $tenant->owner_id === $this->id) {
return true;
}

View File

@ -30,7 +30,7 @@ class AccountCreated extends Notification implements ShouldQueue
->line('Email: ' . $notifiable->email);
if (!is_null($this->token)) {
return $message->action('Setup Your Account', Filament::getResetPasswordUrl($this->token, $notifiable));
return $message->action('Setup Your Account', Filament::getPanel('app')->getResetPasswordUrl($this->token, $notifiable));
}
return $message;

View File

@ -21,11 +21,6 @@ class ServerPolicy
return null;
}
// Make sure user can target node of the server
if (!$user->canTarget($server->node)) {
return false;
}
// Owner has full server permissions
if ($server->owner_id === $user->id) {
return true;
@ -37,6 +32,11 @@ class ServerPolicy
return true;
}
// Make sure user can target node of the server
if (!$user->canTarget($server->node)) {
return false;
}
// Return null to let default policies take over
return null;
}

View File

@ -16,8 +16,8 @@ class GetUserPermissionsService
*/
public function handle(Server $server, User $user): array
{
if ($user->isAdmin() && ($user->can('view server', $server) || $user->can('update server', $server))) {
$permissions = $user->can('update server', $server) ? ['*'] : ['websocket.connect', 'backup.read'];
if ($user->isAdmin() && ($user->can('view', $server) || $user->can('update', $server))) {
$permissions = $user->can('update', $server) ? ['*'] : ['websocket.connect', 'backup.read'];
$permissions[] = 'admin.websocket.errors';
$permissions[] = 'admin.websocket.install';

View File

@ -1,6 +1,7 @@
{
"name": "pelican-dev/panel",
"description": "The free, open-source game management panel. Supporting Minecraft, Spigot, BungeeCord, and SRCDS servers.",
"license": "AGPL-3.0-only",
"require": {
"php": "^8.2 || ^8.3 || ^8.4",
"ext-intl": "*",
@ -17,7 +18,7 @@
"doctrine/dbal": "~3.6.0",
"filament/filament": "^3.3",
"guzzlehttp/guzzle": "^7.9",
"laravel/framework": "^12.13",
"laravel/framework": "^12.16",
"laravel/helpers": "^1.7",
"laravel/sanctum": "^4.1",
"laravel/socialite": "^5.20",
@ -37,7 +38,7 @@
"spatie/laravel-data": "^4.15",
"spatie/laravel-fractal": "^6.3",
"spatie/laravel-health": "^1.34",
"spatie/laravel-permission": "^6.17",
"spatie/laravel-permission": "^6.18",
"spatie/laravel-query-builder": "^6.3",
"spatie/temporary-directory": "^2.3",
"symfony/http-client": "^7.2",
@ -50,7 +51,7 @@
"require-dev": {
"barryvdh/laravel-ide-helper": "^3.5",
"fakerphp/faker": "^1.23.1",
"larastan/larastan": "3.x-dev#5bd1c40edb43a727584081e74e9a1a2a201ea2ee",
"larastan/larastan": "^3.4",
"laravel/pail": "^1.2.2",
"laravel/pint": "^1.15.3",
"laravel/sail": "^1.41",

337
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "3819408e46a86ddb3019bf36a8f36ee9",
"content-hash": "6ae0adb5b2985ecea907ceb281a08926",
"packages": [
{
"name": "abdelhamiderrahmouni/filament-monaco-editor",
@ -1020,16 +1020,16 @@
},
{
"name": "aws/aws-sdk-php",
"version": "3.343.6",
"version": "3.343.18",
"source": {
"type": "git",
"url": "https://github.com/aws/aws-sdk-php.git",
"reference": "3746aca8cbed5f46beba850e0a480ef58e71b197"
"reference": "ae98d503173740cce23b30d2ba2737c49b0d9876"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/3746aca8cbed5f46beba850e0a480ef58e71b197",
"reference": "3746aca8cbed5f46beba850e0a480ef58e71b197",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/ae98d503173740cce23b30d2ba2737c49b0d9876",
"reference": "ae98d503173740cce23b30d2ba2737c49b0d9876",
"shasum": ""
},
"require": {
@ -1111,9 +1111,9 @@
"support": {
"forum": "https://github.com/aws/aws-sdk-php/discussions",
"issues": "https://github.com/aws/aws-sdk-php/issues",
"source": "https://github.com/aws/aws-sdk-php/tree/3.343.6"
"source": "https://github.com/aws/aws-sdk-php/tree/3.343.18"
},
"time": "2025-05-07T18:10:08+00:00"
"time": "2025-05-23T18:08:18+00:00"
},
{
"name": "aymanalhattami/filament-context-menu",
@ -2630,16 +2630,16 @@
},
{
"name": "filament/actions",
"version": "v3.3.14",
"version": "v3.3.16",
"source": {
"type": "git",
"url": "https://github.com/filamentphp/actions.git",
"reference": "08caa8dec43ebf4192dcd999cca786656a3dbc90"
"reference": "66b509aa72fa882ce91218eb743684a9350bc3fb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/filamentphp/actions/zipball/08caa8dec43ebf4192dcd999cca786656a3dbc90",
"reference": "08caa8dec43ebf4192dcd999cca786656a3dbc90",
"url": "https://api.github.com/repos/filamentphp/actions/zipball/66b509aa72fa882ce91218eb743684a9350bc3fb",
"reference": "66b509aa72fa882ce91218eb743684a9350bc3fb",
"shasum": ""
},
"require": {
@ -2679,20 +2679,20 @@
"issues": "https://github.com/filamentphp/filament/issues",
"source": "https://github.com/filamentphp/filament"
},
"time": "2025-04-30T09:16:43+00:00"
"time": "2025-05-19T07:25:24+00:00"
},
{
"name": "filament/filament",
"version": "v3.3.14",
"version": "v3.3.16",
"source": {
"type": "git",
"url": "https://github.com/filamentphp/panels.git",
"reference": "2c4783bdd973967cc2dbc2dc518c70b04839ace3"
"reference": "ed0a0109e6b2663247fcd73076c95c918bc5b412"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/filamentphp/panels/zipball/2c4783bdd973967cc2dbc2dc518c70b04839ace3",
"reference": "2c4783bdd973967cc2dbc2dc518c70b04839ace3",
"url": "https://api.github.com/repos/filamentphp/panels/zipball/ed0a0109e6b2663247fcd73076c95c918bc5b412",
"reference": "ed0a0109e6b2663247fcd73076c95c918bc5b412",
"shasum": ""
},
"require": {
@ -2744,20 +2744,20 @@
"issues": "https://github.com/filamentphp/filament/issues",
"source": "https://github.com/filamentphp/filament"
},
"time": "2025-04-30T09:16:38+00:00"
"time": "2025-05-21T08:45:13+00:00"
},
{
"name": "filament/forms",
"version": "v3.3.14",
"version": "v3.3.16",
"source": {
"type": "git",
"url": "https://github.com/filamentphp/forms.git",
"reference": "22e62dc2b4c68018e9846aadf7e8c5310d0e38cf"
"reference": "eeb18e482b1addc5fe88d57f59d6b91cb71957ff"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/filamentphp/forms/zipball/22e62dc2b4c68018e9846aadf7e8c5310d0e38cf",
"reference": "22e62dc2b4c68018e9846aadf7e8c5310d0e38cf",
"url": "https://api.github.com/repos/filamentphp/forms/zipball/eeb18e482b1addc5fe88d57f59d6b91cb71957ff",
"reference": "eeb18e482b1addc5fe88d57f59d6b91cb71957ff",
"shasum": ""
},
"require": {
@ -2800,11 +2800,11 @@
"issues": "https://github.com/filamentphp/filament/issues",
"source": "https://github.com/filamentphp/filament"
},
"time": "2025-04-30T09:16:39+00:00"
"time": "2025-05-21T08:45:29+00:00"
},
{
"name": "filament/infolists",
"version": "v3.3.14",
"version": "v3.3.16",
"source": {
"type": "git",
"url": "https://github.com/filamentphp/infolists.git",
@ -2855,16 +2855,16 @@
},
{
"name": "filament/notifications",
"version": "v3.3.14",
"version": "v3.3.16",
"source": {
"type": "git",
"url": "https://github.com/filamentphp/notifications.git",
"reference": "edf7960621b2181b4c2fc040b0712fbd5dd036ef"
"reference": "356f50e24798a6f06522bfa5123c6ffd054171d3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/filamentphp/notifications/zipball/edf7960621b2181b4c2fc040b0712fbd5dd036ef",
"reference": "edf7960621b2181b4c2fc040b0712fbd5dd036ef",
"url": "https://api.github.com/repos/filamentphp/notifications/zipball/356f50e24798a6f06522bfa5123c6ffd054171d3",
"reference": "356f50e24798a6f06522bfa5123c6ffd054171d3",
"shasum": ""
},
"require": {
@ -2903,20 +2903,20 @@
"issues": "https://github.com/filamentphp/filament/issues",
"source": "https://github.com/filamentphp/filament"
},
"time": "2025-04-23T06:39:49+00:00"
"time": "2025-05-21T08:44:14+00:00"
},
{
"name": "filament/support",
"version": "v3.3.14",
"version": "v3.3.16",
"source": {
"type": "git",
"url": "https://github.com/filamentphp/support.git",
"reference": "0ab49fdb2bc937257d6f8e1f7b97a03216a43656"
"reference": "537663fa2c5057aa236a189255b623b5124d32d8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/filamentphp/support/zipball/0ab49fdb2bc937257d6f8e1f7b97a03216a43656",
"reference": "0ab49fdb2bc937257d6f8e1f7b97a03216a43656",
"url": "https://api.github.com/repos/filamentphp/support/zipball/537663fa2c5057aa236a189255b623b5124d32d8",
"reference": "537663fa2c5057aa236a189255b623b5124d32d8",
"shasum": ""
},
"require": {
@ -2962,20 +2962,20 @@
"issues": "https://github.com/filamentphp/filament/issues",
"source": "https://github.com/filamentphp/filament"
},
"time": "2025-04-30T09:16:34+00:00"
"time": "2025-05-21T08:45:20+00:00"
},
{
"name": "filament/tables",
"version": "v3.3.14",
"version": "v3.3.16",
"source": {
"type": "git",
"url": "https://github.com/filamentphp/tables.git",
"reference": "bb5fad7306c39fdbb08d97982073114ac465bf92"
"reference": "64806e3c13caeabb23a8668a7aaf1efc8395df96"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/filamentphp/tables/zipball/bb5fad7306c39fdbb08d97982073114ac465bf92",
"reference": "bb5fad7306c39fdbb08d97982073114ac465bf92",
"url": "https://api.github.com/repos/filamentphp/tables/zipball/64806e3c13caeabb23a8668a7aaf1efc8395df96",
"reference": "64806e3c13caeabb23a8668a7aaf1efc8395df96",
"shasum": ""
},
"require": {
@ -3014,11 +3014,11 @@
"issues": "https://github.com/filamentphp/filament/issues",
"source": "https://github.com/filamentphp/filament"
},
"time": "2025-04-30T09:16:33+00:00"
"time": "2025-05-19T07:26:42+00:00"
},
{
"name": "filament/widgets",
"version": "v3.3.14",
"version": "v3.3.16",
"source": {
"type": "git",
"url": "https://github.com/filamentphp/widgets.git",
@ -3727,16 +3727,16 @@
},
{
"name": "kirschbaum-development/eloquent-power-joins",
"version": "4.2.3",
"version": "4.2.4",
"source": {
"type": "git",
"url": "https://github.com/kirschbaum-development/eloquent-power-joins.git",
"reference": "d04e06b12e5e7864c303b8a8c6045bfcd4e2c641"
"reference": "4a8012cef7abed8ac3633a180c69138a228b6c4c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/kirschbaum-development/eloquent-power-joins/zipball/d04e06b12e5e7864c303b8a8c6045bfcd4e2c641",
"reference": "d04e06b12e5e7864c303b8a8c6045bfcd4e2c641",
"url": "https://api.github.com/repos/kirschbaum-development/eloquent-power-joins/zipball/4a8012cef7abed8ac3633a180c69138a228b6c4c",
"reference": "4a8012cef7abed8ac3633a180c69138a228b6c4c",
"shasum": ""
},
"require": {
@ -3784,22 +3784,22 @@
],
"support": {
"issues": "https://github.com/kirschbaum-development/eloquent-power-joins/issues",
"source": "https://github.com/kirschbaum-development/eloquent-power-joins/tree/4.2.3"
"source": "https://github.com/kirschbaum-development/eloquent-power-joins/tree/4.2.4"
},
"time": "2025-04-01T14:41:56+00:00"
"time": "2025-05-19T14:14:41+00:00"
},
{
"name": "laravel/framework",
"version": "v12.13.0",
"version": "v12.16.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/framework.git",
"reference": "52b588bcd8efc6d01bc1493d2d67848f8065f269"
"reference": "293bb1c70224faebfd3d4328e201c37115da055f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/framework/zipball/52b588bcd8efc6d01bc1493d2d67848f8065f269",
"reference": "52b588bcd8efc6d01bc1493d2d67848f8065f269",
"url": "https://api.github.com/repos/laravel/framework/zipball/293bb1c70224faebfd3d4328e201c37115da055f",
"reference": "293bb1c70224faebfd3d4328e201c37115da055f",
"shasum": ""
},
"require": {
@ -4001,7 +4001,7 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
"time": "2025-05-07T17:29:01+00:00"
"time": "2025-05-27T15:49:44+00:00"
},
{
"name": "laravel/helpers",
@ -4246,16 +4246,16 @@
},
{
"name": "laravel/socialite",
"version": "v5.20.0",
"version": "v5.21.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/socialite.git",
"reference": "30972c12a41f71abeb418bc9ff157da8d9231519"
"reference": "d83639499ad14985c9a6a9713b70073300ce998d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/socialite/zipball/30972c12a41f71abeb418bc9ff157da8d9231519",
"reference": "30972c12a41f71abeb418bc9ff157da8d9231519",
"url": "https://api.github.com/repos/laravel/socialite/zipball/d83639499ad14985c9a6a9713b70073300ce998d",
"reference": "d83639499ad14985c9a6a9713b70073300ce998d",
"shasum": ""
},
"require": {
@ -4314,7 +4314,7 @@
"issues": "https://github.com/laravel/socialite/issues",
"source": "https://github.com/laravel/socialite"
},
"time": "2025-04-21T14:21:34+00:00"
"time": "2025-05-19T12:56:37+00:00"
},
{
"name": "laravel/tinker",
@ -6160,31 +6160,31 @@
},
{
"name": "nunomaduro/termwind",
"version": "v2.3.0",
"version": "v2.3.1",
"source": {
"type": "git",
"url": "https://github.com/nunomaduro/termwind.git",
"reference": "52915afe6a1044e8b9cee1bcff836fb63acf9cda"
"reference": "dfa08f390e509967a15c22493dc0bac5733d9123"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nunomaduro/termwind/zipball/52915afe6a1044e8b9cee1bcff836fb63acf9cda",
"reference": "52915afe6a1044e8b9cee1bcff836fb63acf9cda",
"url": "https://api.github.com/repos/nunomaduro/termwind/zipball/dfa08f390e509967a15c22493dc0bac5733d9123",
"reference": "dfa08f390e509967a15c22493dc0bac5733d9123",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"php": "^8.2",
"symfony/console": "^7.1.8"
"symfony/console": "^7.2.6"
},
"require-dev": {
"illuminate/console": "^11.33.2",
"laravel/pint": "^1.18.2",
"illuminate/console": "^11.44.7",
"laravel/pint": "^1.22.0",
"mockery/mockery": "^1.6.12",
"pestphp/pest": "^2.36.0",
"phpstan/phpstan": "^1.12.11",
"phpstan/phpstan-strict-rules": "^1.6.1",
"symfony/var-dumper": "^7.1.8",
"pestphp/pest": "^2.36.0 || ^3.8.2",
"phpstan/phpstan": "^1.12.25",
"phpstan/phpstan-strict-rules": "^1.6.2",
"symfony/var-dumper": "^7.2.6",
"thecodingmachine/phpstan-strict-rules": "^1.0.0"
},
"type": "library",
@ -6227,7 +6227,7 @@
],
"support": {
"issues": "https://github.com/nunomaduro/termwind/issues",
"source": "https://github.com/nunomaduro/termwind/tree/v2.3.0"
"source": "https://github.com/nunomaduro/termwind/tree/v2.3.1"
},
"funding": [
{
@ -6243,7 +6243,7 @@
"type": "github"
}
],
"time": "2024-11-21T10:39:51+00:00"
"time": "2025-05-08T08:14:37+00:00"
},
{
"name": "openspout/openspout",
@ -8002,16 +8002,16 @@
},
{
"name": "secondnetwork/blade-tabler-icons",
"version": "v3.31.0",
"version": "v3.33.0",
"source": {
"type": "git",
"url": "https://github.com/secondnetwork/blade-tabler-icons.git",
"reference": "bf069bbdb2a63aa8eaeb10e6fd993464e9340a31"
"reference": "523b3ede493ce9f8cbe3a5bdce21769976a80b28"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/secondnetwork/blade-tabler-icons/zipball/bf069bbdb2a63aa8eaeb10e6fd993464e9340a31",
"reference": "bf069bbdb2a63aa8eaeb10e6fd993464e9340a31",
"url": "https://api.github.com/repos/secondnetwork/blade-tabler-icons/zipball/523b3ede493ce9f8cbe3a5bdce21769976a80b28",
"reference": "523b3ede493ce9f8cbe3a5bdce21769976a80b28",
"shasum": ""
},
"require": {
@ -8054,9 +8054,9 @@
],
"support": {
"issues": "https://github.com/secondnetwork/blade-tabler-icons/issues",
"source": "https://github.com/secondnetwork/blade-tabler-icons/tree/v3.31.0"
"source": "https://github.com/secondnetwork/blade-tabler-icons/tree/v3.33.0"
},
"time": "2025-03-06T09:24:43+00:00"
"time": "2025-05-17T06:28:48+00:00"
},
{
"name": "socialiteproviders/authentik",
@ -8705,16 +8705,16 @@
},
{
"name": "spatie/laravel-health",
"version": "1.34.1",
"version": "1.34.2",
"source": {
"type": "git",
"url": "https://github.com/spatie/laravel-health.git",
"reference": "283d7d5dffeeff6452c9a182f466c327fec6db3b"
"reference": "e7d9d6e0807a9de46f544c76d54badc19af36ddc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/laravel-health/zipball/283d7d5dffeeff6452c9a182f466c327fec6db3b",
"reference": "283d7d5dffeeff6452c9a182f466c327fec6db3b",
"url": "https://api.github.com/repos/spatie/laravel-health/zipball/e7d9d6e0807a9de46f544c76d54badc19af36ddc",
"reference": "e7d9d6e0807a9de46f544c76d54badc19af36ddc",
"shasum": ""
},
"require": {
@ -8786,7 +8786,7 @@
"spatie"
],
"support": {
"source": "https://github.com/spatie/laravel-health/tree/1.34.1"
"source": "https://github.com/spatie/laravel-health/tree/1.34.2"
},
"funding": [
{
@ -8794,7 +8794,7 @@
"type": "github"
}
],
"time": "2025-04-17T06:34:01+00:00"
"time": "2025-05-20T08:31:02+00:00"
},
{
"name": "spatie/laravel-package-tools",
@ -8859,16 +8859,16 @@
},
{
"name": "spatie/laravel-permission",
"version": "6.17.0",
"version": "6.18.0",
"source": {
"type": "git",
"url": "https://github.com/spatie/laravel-permission.git",
"reference": "02ada8f638b643713fa2fb543384738e27346ddb"
"reference": "3c05f04d12275dfbe462c8b4aae3290e586c2dde"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/laravel-permission/zipball/02ada8f638b643713fa2fb543384738e27346ddb",
"reference": "02ada8f638b643713fa2fb543384738e27346ddb",
"url": "https://api.github.com/repos/spatie/laravel-permission/zipball/3c05f04d12275dfbe462c8b4aae3290e586c2dde",
"reference": "3c05f04d12275dfbe462c8b4aae3290e586c2dde",
"shasum": ""
},
"require": {
@ -8930,7 +8930,7 @@
],
"support": {
"issues": "https://github.com/spatie/laravel-permission/issues",
"source": "https://github.com/spatie/laravel-permission/tree/6.17.0"
"source": "https://github.com/spatie/laravel-permission/tree/6.18.0"
},
"funding": [
{
@ -8938,7 +8938,7 @@
"type": "github"
}
],
"time": "2025-04-08T15:06:14+00:00"
"time": "2025-05-14T03:32:23+00:00"
},
{
"name": "spatie/laravel-query-builder",
@ -9450,16 +9450,16 @@
},
{
"name": "symfony/deprecation-contracts",
"version": "v3.5.1",
"version": "v3.6.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/deprecation-contracts.git",
"reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6"
"reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6",
"reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6",
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62",
"reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62",
"shasum": ""
},
"require": {
@ -9472,7 +9472,7 @@
"name": "symfony/contracts"
},
"branch-alias": {
"dev-main": "3.5-dev"
"dev-main": "3.6-dev"
}
},
"autoload": {
@ -9497,7 +9497,7 @@
"description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.1"
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0"
},
"funding": [
{
@ -9513,7 +9513,7 @@
"type": "tidelift"
}
],
"time": "2024-09-25T14:20:29+00:00"
"time": "2024-09-25T14:21:43+00:00"
},
{
"name": "symfony/error-handler",
@ -9672,16 +9672,16 @@
},
{
"name": "symfony/event-dispatcher-contracts",
"version": "v3.5.1",
"version": "v3.6.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/event-dispatcher-contracts.git",
"reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f"
"reference": "59eb412e93815df44f05f342958efa9f46b1e586"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/7642f5e970b672283b7823222ae8ef8bbc160b9f",
"reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f",
"url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/59eb412e93815df44f05f342958efa9f46b1e586",
"reference": "59eb412e93815df44f05f342958efa9f46b1e586",
"shasum": ""
},
"require": {
@ -9695,7 +9695,7 @@
"name": "symfony/contracts"
},
"branch-alias": {
"dev-main": "3.5-dev"
"dev-main": "3.6-dev"
}
},
"autoload": {
@ -9728,7 +9728,7 @@
"standards"
],
"support": {
"source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.5.1"
"source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.6.0"
},
"funding": [
{
@ -9744,7 +9744,7 @@
"type": "tidelift"
}
],
"time": "2024-09-25T14:20:29+00:00"
"time": "2024-09-25T14:21:43+00:00"
},
{
"name": "symfony/finder",
@ -9976,16 +9976,16 @@
},
{
"name": "symfony/http-client-contracts",
"version": "v3.5.2",
"version": "v3.6.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-client-contracts.git",
"reference": "ee8d807ab20fcb51267fdace50fbe3494c31e645"
"reference": "75d7043853a42837e68111812f4d964b01e5101c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/ee8d807ab20fcb51267fdace50fbe3494c31e645",
"reference": "ee8d807ab20fcb51267fdace50fbe3494c31e645",
"url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/75d7043853a42837e68111812f4d964b01e5101c",
"reference": "75d7043853a42837e68111812f4d964b01e5101c",
"shasum": ""
},
"require": {
@ -9998,7 +9998,7 @@
"name": "symfony/contracts"
},
"branch-alias": {
"dev-main": "3.5-dev"
"dev-main": "3.6-dev"
}
},
"autoload": {
@ -10034,7 +10034,7 @@
"standards"
],
"support": {
"source": "https://github.com/symfony/http-client-contracts/tree/v3.5.2"
"source": "https://github.com/symfony/http-client-contracts/tree/v3.6.0"
},
"funding": [
{
@ -10050,7 +10050,7 @@
"type": "tidelift"
}
],
"time": "2024-12-07T08:49:48+00:00"
"time": "2025-04-29T11:18:49+00:00"
},
{
"name": "symfony/http-foundation",
@ -11328,16 +11328,16 @@
},
{
"name": "symfony/service-contracts",
"version": "v3.5.1",
"version": "v3.6.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/service-contracts.git",
"reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0"
"reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/service-contracts/zipball/e53260aabf78fb3d63f8d79d69ece59f80d5eda0",
"reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0",
"url": "https://api.github.com/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4",
"reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4",
"shasum": ""
},
"require": {
@ -11355,7 +11355,7 @@
"name": "symfony/contracts"
},
"branch-alias": {
"dev-main": "3.5-dev"
"dev-main": "3.6-dev"
}
},
"autoload": {
@ -11391,7 +11391,7 @@
"standards"
],
"support": {
"source": "https://github.com/symfony/service-contracts/tree/v3.5.1"
"source": "https://github.com/symfony/service-contracts/tree/v3.6.0"
},
"funding": [
{
@ -11407,7 +11407,7 @@
"type": "tidelift"
}
],
"time": "2024-09-25T14:20:29+00:00"
"time": "2025-04-25T09:37:31+00:00"
},
{
"name": "symfony/string",
@ -11593,16 +11593,16 @@
},
{
"name": "symfony/translation-contracts",
"version": "v3.5.1",
"version": "v3.6.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/translation-contracts.git",
"reference": "4667ff3bd513750603a09c8dedbea942487fb07c"
"reference": "df210c7a2573f1913b2d17cc95f90f53a73d8f7d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/translation-contracts/zipball/4667ff3bd513750603a09c8dedbea942487fb07c",
"reference": "4667ff3bd513750603a09c8dedbea942487fb07c",
"url": "https://api.github.com/repos/symfony/translation-contracts/zipball/df210c7a2573f1913b2d17cc95f90f53a73d8f7d",
"reference": "df210c7a2573f1913b2d17cc95f90f53a73d8f7d",
"shasum": ""
},
"require": {
@ -11615,7 +11615,7 @@
"name": "symfony/contracts"
},
"branch-alias": {
"dev-main": "3.5-dev"
"dev-main": "3.6-dev"
}
},
"autoload": {
@ -11651,7 +11651,7 @@
"standards"
],
"support": {
"source": "https://github.com/symfony/translation-contracts/tree/v3.5.1"
"source": "https://github.com/symfony/translation-contracts/tree/v3.6.0"
},
"funding": [
{
@ -11667,7 +11667,7 @@
"type": "tidelift"
}
],
"time": "2024-09-25T14:20:29+00:00"
"time": "2024-09-27T08:32:26+00:00"
},
{
"name": "symfony/uid",
@ -12965,16 +12965,16 @@
},
{
"name": "larastan/larastan",
"version": "3.x-dev",
"version": "v3.4.0",
"source": {
"type": "git",
"url": "https://github.com/larastan/larastan.git",
"reference": "5bd1c40edb43a727584081e74e9a1a2a201ea2ee"
"reference": "1042fa0c2ee490bb6da7381f3323f7292ad68222"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/larastan/larastan/zipball/5bd1c40edb43a727584081e74e9a1a2a201ea2ee",
"reference": "5bd1c40edb43a727584081e74e9a1a2a201ea2ee",
"url": "https://api.github.com/repos/larastan/larastan/zipball/1042fa0c2ee490bb6da7381f3323f7292ad68222",
"reference": "1042fa0c2ee490bb6da7381f3323f7292ad68222",
"shasum": ""
},
"require": {
@ -13003,7 +13003,6 @@
"suggest": {
"orchestra/testbench": "Using Larastan for analysing a package needs Testbench"
},
"default-branch": true,
"type": "phpstan-extension",
"extra": {
"phpstan": {
@ -13043,7 +13042,7 @@
],
"support": {
"issues": "https://github.com/larastan/larastan/issues",
"source": "https://github.com/larastan/larastan/tree/3.x"
"source": "https://github.com/larastan/larastan/tree/v3.4.0"
},
"funding": [
{
@ -13051,7 +13050,7 @@
"type": "github"
}
],
"time": "2025-04-24T07:26:41+00:00"
"time": "2025-04-22T09:44:59+00:00"
},
{
"name": "laravel/pail",
@ -13133,16 +13132,16 @@
},
{
"name": "laravel/pint",
"version": "v1.22.0",
"version": "v1.22.1",
"source": {
"type": "git",
"url": "https://github.com/laravel/pint.git",
"reference": "7ddfaa6523a675fae5c4123ee38fc6bfb8ee4f36"
"reference": "941d1927c5ca420c22710e98420287169c7bcaf7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/pint/zipball/7ddfaa6523a675fae5c4123ee38fc6bfb8ee4f36",
"reference": "7ddfaa6523a675fae5c4123ee38fc6bfb8ee4f36",
"url": "https://api.github.com/repos/laravel/pint/zipball/941d1927c5ca420c22710e98420287169c7bcaf7",
"reference": "941d1927c5ca420c22710e98420287169c7bcaf7",
"shasum": ""
},
"require": {
@ -13154,11 +13153,11 @@
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.75.0",
"illuminate/view": "^11.44.2",
"larastan/larastan": "^3.3.1",
"illuminate/view": "^11.44.7",
"larastan/larastan": "^3.4.0",
"laravel-zero/framework": "^11.36.1",
"mockery/mockery": "^1.6.12",
"nunomaduro/termwind": "^2.3",
"nunomaduro/termwind": "^2.3.1",
"pestphp/pest": "^2.36.0"
},
"bin": [
@ -13195,20 +13194,20 @@
"issues": "https://github.com/laravel/pint/issues",
"source": "https://github.com/laravel/pint"
},
"time": "2025-04-08T22:11:45+00:00"
"time": "2025-05-08T08:38:12+00:00"
},
{
"name": "laravel/sail",
"version": "v1.42.0",
"version": "v1.43.1",
"source": {
"type": "git",
"url": "https://github.com/laravel/sail.git",
"reference": "2edaaf77f3c07a4099965bb3d7dfee16e801c0f6"
"reference": "3e7d899232a8c5e3ea4fc6dee7525ad583887e72"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/sail/zipball/2edaaf77f3c07a4099965bb3d7dfee16e801c0f6",
"reference": "2edaaf77f3c07a4099965bb3d7dfee16e801c0f6",
"url": "https://api.github.com/repos/laravel/sail/zipball/3e7d899232a8c5e3ea4fc6dee7525ad583887e72",
"reference": "3e7d899232a8c5e3ea4fc6dee7525ad583887e72",
"shasum": ""
},
"require": {
@ -13258,7 +13257,7 @@
"issues": "https://github.com/laravel/sail/issues",
"source": "https://github.com/laravel/sail"
},
"time": "2025-04-29T14:26:46+00:00"
"time": "2025-05-19T13:19:21+00:00"
},
{
"name": "mockery/mockery",
@ -14017,16 +14016,16 @@
},
{
"name": "phpstan/phpstan",
"version": "2.1.14",
"version": "2.1.17",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan.git",
"reference": "8f2e03099cac24ff3b379864d171c5acbfc6b9a2"
"reference": "89b5ef665716fa2a52ecd2633f21007a6a349053"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/8f2e03099cac24ff3b379864d171c5acbfc6b9a2",
"reference": "8f2e03099cac24ff3b379864d171c5acbfc6b9a2",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/89b5ef665716fa2a52ecd2633f21007a6a349053",
"reference": "89b5ef665716fa2a52ecd2633f21007a6a349053",
"shasum": ""
},
"require": {
@ -14071,7 +14070,7 @@
"type": "github"
}
],
"time": "2025-05-02T15:32:28+00:00"
"time": "2025-05-21T20:55:28+00:00"
},
{
"name": "phpunit/php-code-coverage",
@ -14874,23 +14873,23 @@
},
{
"name": "sebastian/environment",
"version": "7.2.0",
"version": "7.2.1",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/environment.git",
"reference": "855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5"
"reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5",
"reference": "855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5",
"url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/a5c75038693ad2e8d4b6c15ba2403532647830c4",
"reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4",
"shasum": ""
},
"require": {
"php": ">=8.2"
},
"require-dev": {
"phpunit/phpunit": "^11.0"
"phpunit/phpunit": "^11.3"
},
"suggest": {
"ext-posix": "*"
@ -14926,15 +14925,27 @@
"support": {
"issues": "https://github.com/sebastianbergmann/environment/issues",
"security": "https://github.com/sebastianbergmann/environment/security/policy",
"source": "https://github.com/sebastianbergmann/environment/tree/7.2.0"
"source": "https://github.com/sebastianbergmann/environment/tree/7.2.1"
},
"funding": [
{
"url": "https://github.com/sebastianbergmann",
"type": "github"
},
{
"url": "https://liberapay.com/sebastianbergmann",
"type": "liberapay"
},
{
"url": "https://thanks.dev/u/gh/sebastianbergmann",
"type": "thanks_dev"
},
{
"url": "https://tidelift.com/funding/github/packagist/sebastian/environment",
"type": "tidelift"
}
],
"time": "2024-07-03T04:54:44+00:00"
"time": "2025-05-21T11:55:47+00:00"
},
{
"name": "sebastian/exporter",
@ -15425,16 +15436,16 @@
},
{
"name": "spatie/backtrace",
"version": "1.7.3",
"version": "1.7.4",
"source": {
"type": "git",
"url": "https://github.com/spatie/backtrace.git",
"reference": "80ae5fabe2a1e3bc7df5c7ca5d91b094dc314b20"
"reference": "cd37a49fce7137359ac30ecc44ef3e16404cccbe"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/backtrace/zipball/80ae5fabe2a1e3bc7df5c7ca5d91b094dc314b20",
"reference": "80ae5fabe2a1e3bc7df5c7ca5d91b094dc314b20",
"url": "https://api.github.com/repos/spatie/backtrace/zipball/cd37a49fce7137359ac30ecc44ef3e16404cccbe",
"reference": "cd37a49fce7137359ac30ecc44ef3e16404cccbe",
"shasum": ""
},
"require": {
@ -15472,7 +15483,7 @@
"spatie"
],
"support": {
"source": "https://github.com/spatie/backtrace/tree/1.7.3"
"source": "https://github.com/spatie/backtrace/tree/1.7.4"
},
"funding": [
{
@ -15484,7 +15495,7 @@
"type": "other"
}
],
"time": "2025-05-07T07:20:00+00:00"
"time": "2025-05-08T15:41:09+00:00"
},
{
"name": "spatie/error-solutions",
@ -15967,9 +15978,7 @@
],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": {
"larastan/larastan": 20
},
"stability-flags": {},
"prefer-stable": true,
"prefer-lowest": false,
"platform": {
@ -15980,9 +15989,9 @@
"ext-pdo": "*",
"ext-zip": "*"
},
"platform-dev": [],
"platform-dev": {},
"platform-overrides": {
"php": "8.2"
},
"plugin-api-version": "2.3.0"
"plugin-api-version": "2.6.0"
}

View File

@ -86,7 +86,7 @@ return [
'allocations' => 'Allocations',
'databases' => 'Databases',
'no_databases' => 'No Databases exist for this Server',
'delete_db' => 'Are you sure you want to delete',
'delete_db' => 'Are you sure you want to delete :name ?',
'delete_db_heading' => 'Delete Database?',
'backups' => 'Backups',
'egg' => 'Egg',

View File

@ -16,6 +16,7 @@ return [
'failed' => 'These credentials do not match our records.',
'failed-two-factor' => 'Incorrect 2FA Code',
'two-factor-code' => 'Two Factor Code',
'two-factor-hint' => 'You may use backup codes if you lost access to your device.',
'password' => 'The provided password is incorrect.',
'throttle' => 'Too many login attempts. Please try again in :seconds seconds.',
'2fa_must_be_enabled' => 'The administrator has required that 2-Factor Authentication must be enabled for your account in order to use the Panel.',

View File

@ -1,10 +0,0 @@
<div class="fi-wi-stats-overview-stat relative rounded-lg bg-white p-4 shadow-sm ring-1 ring-gray-950/5 dark:bg-gray-900 dark:ring-white/10">
<div class="grid grid-flow-row">
<span class="text-sm font-medium text-gray-500 dark:text-gray-400">
{{ $getLabel() }}
</span>
<div class="text-xl font-semibold text-gray-950 dark:text-white">
{{ $getValue() }}
</div>
</div>
</div>

View File

@ -1,9 +1,14 @@
<div
class="grid grid-flow-row w-full p-3 rounded-lg bg-white shadow-sm overflow-hidden ring-1 ring-gray-950/5 dark:bg-gray-900 dark:ring-white/10">
<div class="fi-small-stat-block grid grid-flow-row w-full p-3 rounded-lg bg-white shadow-sm overflow-hidden overflow-x-auto ring-1 ring-gray-950/5 dark:bg-gray-900 dark:ring-white/10">
@if ($shouldCopyOnClick())
<span class="cursor-pointer" wire:click="copyClick('{{ $getValue() }}')">
@else
<span>
@endif
<span class="text-md font-medium text-gray-500 dark:text-gray-400">
{{ $getLabel() }}
</span>
<span class="text-md font-semibold">{{ $getValue() }}</span>
<span class="text-md font-semibold">
{{ $getValue() }}
</span>
</span>
</div>

View File

@ -176,5 +176,5 @@
x-text="monacoPlaceholderText"></div>
</div>
</div>
</div>
</x-dynamic-component>