mirror of
https://github.com/pelican-dev/panel.git
synced 2026-07-17 20:53:56 +03:00
Compare commits
3 Commits
boy132/ran
...
server-use
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da64dbb6b5 | ||
|
|
0ef479274d | ||
|
|
44077a1b40 |
31
app/Enums/ServerUserSettingKey.php
Normal file
31
app/Enums/ServerUserSettingKey.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum ServerUserSettingKey: string
|
||||
{
|
||||
case ManualBackupNotifications = 'manual_backup_notifications';
|
||||
case ScheduledBackupNotifications = 'scheduled_backup_notifications';
|
||||
|
||||
/**
|
||||
* The default value for users without an explicit setting.
|
||||
*/
|
||||
public function getDefaultValue(): bool
|
||||
{
|
||||
return match ($this) {
|
||||
self::ManualBackupNotifications, self::ScheduledBackupNotifications => false,
|
||||
};
|
||||
}
|
||||
|
||||
/** @return array<string, bool> */
|
||||
public static function getDefaultSettings(): array
|
||||
{
|
||||
$default = [];
|
||||
|
||||
foreach (self::cases() as $key) {
|
||||
$default[$key->value] = $key->getDefaultValue();
|
||||
}
|
||||
|
||||
return $default;
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ final class CreateBackupSchema extends TaskSchema
|
||||
|
||||
public function runTask(Task $task): void
|
||||
{
|
||||
$this->initiateService->setIgnoredFiles(explode(PHP_EOL, $task->payload))->handle($task->server, null, true);
|
||||
$this->initiateService->setIsScheduled(true)->setIgnoredFiles(explode(PHP_EOL, $task->payload))->handle($task->server, null, true);
|
||||
}
|
||||
|
||||
public function canCreate(Schedule $schedule): bool
|
||||
|
||||
@@ -689,16 +689,6 @@ class Settings extends Page implements HasSchemas
|
||||
->formatStateUsing(fn ($state): bool => (bool) $state)
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('PANEL_SEND_REINSTALL_NOTIFICATION', (bool) $state))
|
||||
->default(env('PANEL_SEND_REINSTALL_NOTIFICATION', config('panel.email.send_reinstall_notification'))),
|
||||
Toggle::make('PANEL_SEND_BACKUP_COMPLETED_NOTIFICATION')
|
||||
->label(trans('admin/setting.misc.mail_notifications.backup_completed'))
|
||||
->onIcon(TablerIcon::Check)
|
||||
->offIcon(TablerIcon::X)
|
||||
->onColor('success')
|
||||
->offColor('danger')
|
||||
->live()
|
||||
->formatStateUsing(fn ($state): bool => (bool) $state)
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('PANEL_SEND_BACKUP_COMPLETED_NOTIFICATION', (bool) $state))
|
||||
->default(env('PANEL_SEND_BACKUP_COMPLETED_NOTIFICATION', config('panel.email.send_backup_completed_notification'))),
|
||||
]),
|
||||
Section::make(trans('admin/setting.misc.connections.title'))
|
||||
->description(trans('admin/setting.misc.connections.helper'))
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Filament\Server\Pages;
|
||||
|
||||
use App\Enums\ServerUserSettingKey;
|
||||
use App\Enums\SubuserPermission;
|
||||
use App\Enums\TablerIcon;
|
||||
use App\Facades\Activity;
|
||||
@@ -14,6 +15,7 @@ use Exception;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Infolists\Components\TextEntry;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Schemas\Components\Fieldset;
|
||||
@@ -254,9 +256,44 @@ class Settings extends ServerFormPage
|
||||
TextEntry::make('files_info')
|
||||
->label(trans('server/setting.reinstall.body2')),
|
||||
]),
|
||||
Section::make(trans('server/setting.notifications.title'))
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
Toggle::make(ServerUserSettingKey::ManualBackupNotifications->value)
|
||||
->label(trans('server/setting.notifications.backup_manual.label'))
|
||||
->helperText(trans('server/setting.notifications.backup_manual.helper'))
|
||||
->live()
|
||||
->afterStateUpdated(fn ($state, Server $server) => $this->updateNotificationSetting(ServerUserSettingKey::ManualBackupNotifications, (bool) $state, $server)),
|
||||
Toggle::make(ServerUserSettingKey::ScheduledBackupNotifications->value)
|
||||
->label(trans('server/setting.notifications.backup_scheduled.label'))
|
||||
->helperText(trans('server/setting.notifications.backup_scheduled.helper'))
|
||||
->live()
|
||||
->afterStateUpdated(fn ($state, Server $server) => $this->updateNotificationSetting(ServerUserSettingKey::ScheduledBackupNotifications, (bool) $state, $server)),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function fillForm(): void
|
||||
{
|
||||
$data = $this->getRecord()->attributesToArray();
|
||||
|
||||
foreach (ServerUserSettingKey::cases() as $key) {
|
||||
$data[$key->value] = (bool) user()?->getServerSetting($this->getRecord(), $key);
|
||||
}
|
||||
|
||||
$this->form->fill($data);
|
||||
}
|
||||
|
||||
public function updateNotificationSetting(ServerUserSettingKey $key, bool $state, Server $server): void
|
||||
{
|
||||
user()?->updateServerSetting($server, $key, $state);
|
||||
|
||||
Notification::make()
|
||||
->title(trans('server/setting.notifications.saved'))
|
||||
->success()
|
||||
->send();
|
||||
}
|
||||
|
||||
public function updateName(string $name, Server $server): void
|
||||
{
|
||||
abort_unless(user()?->can(SubuserPermission::SettingsRename, $server), 403);
|
||||
|
||||
@@ -2,9 +2,16 @@
|
||||
|
||||
namespace App\Filament\Server\Resources\Backups\Pages;
|
||||
|
||||
use App\Enums\ServerUserSettingKey;
|
||||
use App\Enums\TablerIcon;
|
||||
use App\Filament\Server\Resources\Backups\BackupResource;
|
||||
use App\Models\Server;
|
||||
use App\Traits\Filament\CanCustomizeHeaderActions;
|
||||
use App\Traits\Filament\CanCustomizeHeaderWidgets;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListBackups extends ListRecords
|
||||
@@ -14,6 +21,39 @@ class ListBackups extends ListRecords
|
||||
|
||||
protected static string $resource = BackupResource::class;
|
||||
|
||||
/** @return Action[] */
|
||||
protected function getDefaultHeaderActions(): array
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
return [
|
||||
Action::make('notification_settings')
|
||||
->label(trans('server/backup.notifications.action'))
|
||||
->icon(TablerIcon::Bell)
|
||||
->schema([
|
||||
Toggle::make(ServerUserSettingKey::ManualBackupNotifications->value)
|
||||
->label(trans('server/backup.notifications.toggle_manual'))
|
||||
->helperText(trans('server/backup.notifications.helper_manual'))
|
||||
->default(fn () => (bool) user()?->getServerSetting($server, ServerUserSettingKey::ManualBackupNotifications)),
|
||||
Toggle::make(ServerUserSettingKey::ScheduledBackupNotifications->value)
|
||||
->label(trans('server/backup.notifications.toggle_scheduled'))
|
||||
->helperText(trans('server/backup.notifications.helper_scheduled'))
|
||||
->default(fn () => (bool) user()?->getServerSetting($server, ServerUserSettingKey::ScheduledBackupNotifications)),
|
||||
])
|
||||
->action(function (array $data) use ($server) {
|
||||
foreach (ServerUserSettingKey::cases() as $key) {
|
||||
user()?->updateServerSetting($server, $key, (bool) $data[$key->value]);
|
||||
}
|
||||
|
||||
Notification::make()
|
||||
->title(trans('server/backup.notifications.saved'))
|
||||
->success()
|
||||
->send();
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
public function getBreadcrumbs(): array
|
||||
{
|
||||
return [];
|
||||
|
||||
@@ -2,8 +2,11 @@
|
||||
|
||||
namespace App\Listeners\Server;
|
||||
|
||||
use App\Enums\ServerUserSettingKey;
|
||||
use App\Events\Server\BackupCompleted;
|
||||
use App\Filament\Server\Resources\Backups\Pages\ListBackups;
|
||||
use App\Models\ServerUserSettings;
|
||||
use App\Models\User;
|
||||
use App\Notifications\BackupCompleted as BackupCompletedNotification;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Notifications\Notification;
|
||||
@@ -12,26 +15,47 @@ class BackupCompletedListener
|
||||
{
|
||||
public function handle(BackupCompleted $event): void
|
||||
{
|
||||
$event->backup->loadMissing('server');
|
||||
$event->backup->server->loadMissing('user');
|
||||
$event->backup->loadMissing(['server.user', 'server.subusers.user']);
|
||||
|
||||
$locale = $event->backup->server->user->language ?? 'en';
|
||||
$server = $event->backup->server;
|
||||
|
||||
Notification::make()
|
||||
->status($event->backup->is_successful ? 'success' : 'danger')
|
||||
->title(trans('notifications.backup_' . ($event->backup->is_successful ? 'completed' : 'failed'), locale: $locale))
|
||||
->body(trans('notifications.backup_body', ['name' => $event->backup->name, 'server' => $event->backup->server->name], $locale))
|
||||
->actions([
|
||||
Action::make('exclude_view')
|
||||
->button()
|
||||
->label(trans('notifications.view_backups', locale: $locale))
|
||||
->markAsRead()
|
||||
->url(fn () => ListBackups::getUrl(panel: 'server', tenant: $event->backup->server)),
|
||||
])
|
||||
->sendToDatabase($event->backup->server->user);
|
||||
$candidates = collect([$server->user])
|
||||
->concat($server->subusers->map->user)
|
||||
->filter()
|
||||
->unique('id');
|
||||
|
||||
if (config()->get('panel.email.send_backup_completed_notification', true)) {
|
||||
$event->backup->server->user->notify(new BackupCompletedNotification($event->backup));
|
||||
$settings = ServerUserSettings::query()
|
||||
->where('server_id', $server->id)
|
||||
->whereIn('user_id', $candidates->pluck('id'))
|
||||
->get()
|
||||
->keyBy('user_id');
|
||||
|
||||
// Users only receive notifications if they opted in for this kind of backup.
|
||||
$key = $event->backup->is_scheduled ? ServerUserSettingKey::ScheduledBackupNotifications : ServerUserSettingKey::ManualBackupNotifications;
|
||||
|
||||
$recipients = $candidates->filter(function (User $user) use ($settings, $key) {
|
||||
$userSettings = $settings->get($user->id)->settings ?? [];
|
||||
|
||||
return (bool) ($userSettings[$key->value] ?? $key->getDefaultValue());
|
||||
});
|
||||
|
||||
foreach ($recipients as $user) {
|
||||
$locale = $user->language ?? 'en';
|
||||
|
||||
Notification::make()
|
||||
->status($event->backup->is_successful ? 'success' : 'danger')
|
||||
->title(trans('notifications.backup_' . ($event->backup->is_successful ? 'completed' : 'failed'), locale: $locale))
|
||||
->body(trans('notifications.backup_body', ['name' => $event->backup->name, 'server' => $server->name], $locale))
|
||||
->actions([
|
||||
Action::make('exclude_view')
|
||||
->button()
|
||||
->label(trans('notifications.view_backups', locale: $locale))
|
||||
->markAsRead()
|
||||
->url(fn () => ListBackups::getUrl(panel: 'server', tenant: $server)),
|
||||
])
|
||||
->sendToDatabase($user);
|
||||
|
||||
$user->notify(new BackupCompletedNotification($event->backup));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ use Illuminate\Database\Query\Builder;
|
||||
* @property bool $is_successful
|
||||
* @property string|null $upload_id
|
||||
* @property bool $is_locked
|
||||
* @property bool $is_scheduled
|
||||
* @property-read Server $server
|
||||
* @property-read BackupStatus $status
|
||||
*
|
||||
@@ -69,6 +70,7 @@ class Backup extends Model implements Validatable
|
||||
protected $attributes = [
|
||||
'is_successful' => false,
|
||||
'is_locked' => false,
|
||||
'is_scheduled' => false,
|
||||
'checksum' => null,
|
||||
'bytes' => 0,
|
||||
'upload_id' => null,
|
||||
@@ -82,6 +84,7 @@ class Backup extends Model implements Validatable
|
||||
'uuid' => ['required', 'uuid'],
|
||||
'is_successful' => ['boolean'],
|
||||
'is_locked' => ['boolean'],
|
||||
'is_scheduled' => ['boolean'],
|
||||
'name' => ['required', 'string'],
|
||||
'ignored_files' => ['array'],
|
||||
'backup_host_id' => ['required', 'integer', 'exists:backup_hosts,id'],
|
||||
@@ -96,6 +99,7 @@ class Backup extends Model implements Validatable
|
||||
'id' => 'int',
|
||||
'is_successful' => 'bool',
|
||||
'is_locked' => 'bool',
|
||||
'is_scheduled' => 'bool',
|
||||
'ignored_files' => 'array',
|
||||
'bytes' => 'int',
|
||||
'completed_at' => 'immutable_datetime',
|
||||
|
||||
@@ -90,6 +90,8 @@ use Psr\Http\Message\ResponseInterface;
|
||||
* @property-read int|null $server_variables_count
|
||||
* @property-read Collection<int, Subuser> $subusers
|
||||
* @property-read int|null $subusers_count
|
||||
* @property-read Collection<int, ServerUserSettings> $userSettings
|
||||
* @property-read int|null $user_settings_count
|
||||
* @property-read ServerTransfer|null $transfer
|
||||
* @property-read User $user
|
||||
* @property-read Collection<int, EggVariable> $variables
|
||||
@@ -271,6 +273,16 @@ class Server extends Model implements HasAvatar, Validatable
|
||||
return $this->hasMany(Subuser::class, 'server_id', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the per-user settings associated with a server.
|
||||
*
|
||||
* @return HasMany<ServerUserSettings, $this>
|
||||
*/
|
||||
public function userSettings(): HasMany
|
||||
{
|
||||
return $this->hasMany(ServerUserSettings::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the default allocation for a server.
|
||||
*/
|
||||
|
||||
84
app/Models/ServerUserSettings.php
Normal file
84
app/Models/ServerUserSettings.php
Normal file
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Contracts\Validatable;
|
||||
use App\Traits\HasValidation;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property int $user_id
|
||||
* @property int $server_id
|
||||
* @property array<string, string|int|bool>|null $settings
|
||||
* @property Carbon|null $created_at
|
||||
* @property Carbon|null $updated_at
|
||||
* @property-read Server $server
|
||||
* @property-read User $user
|
||||
*
|
||||
* @method static \Database\Factories\ServerUserSettingsFactory factory($count = null, $state = [])
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|ServerUserSettings newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|ServerUserSettings newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|ServerUserSettings query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|ServerUserSettings whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|ServerUserSettings whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|ServerUserSettings whereServerId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|ServerUserSettings whereSettings($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|ServerUserSettings whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|ServerUserSettings whereUserId($value)
|
||||
*/
|
||||
class ServerUserSettings extends Model implements Validatable
|
||||
{
|
||||
use HasFactory;
|
||||
use HasValidation;
|
||||
|
||||
/**
|
||||
* The resource name for this model when it is transformed into an
|
||||
* API representation using fractal.
|
||||
*/
|
||||
public const RESOURCE_NAME = 'server_user_settings';
|
||||
|
||||
protected $table = 'server_user_settings';
|
||||
|
||||
/**
|
||||
* Fields that are not mass assignable.
|
||||
*/
|
||||
protected $guarded = ['id', 'created_at', 'updated_at'];
|
||||
|
||||
/** @var array<array-key, string[]> */
|
||||
public static array $validationRules = [
|
||||
'user_id' => ['required', 'numeric', 'exists:users,id'],
|
||||
'server_id' => ['required', 'numeric', 'exists:servers,id'],
|
||||
'settings' => ['nullable', 'array'],
|
||||
'settings.manual_backup_notifications' => ['boolean'],
|
||||
'settings.scheduled_backup_notifications' => ['boolean'],
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'user_id' => 'int',
|
||||
'server_id' => 'int',
|
||||
'settings' => 'array',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the server these settings are associated with.
|
||||
*/
|
||||
public function server(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Server::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the user these settings are associated with.
|
||||
*/
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ namespace App\Models;
|
||||
|
||||
use App\Contracts\Validatable;
|
||||
use App\Enums\CustomizationKey;
|
||||
use App\Enums\ServerUserSettingKey;
|
||||
use App\Enums\SubuserPermission;
|
||||
use App\Events\User\Deleting;
|
||||
use App\Exceptions\DisplayException;
|
||||
@@ -86,6 +87,8 @@ use Spatie\Permission\Traits\HasRoles;
|
||||
* @property-read int|null $sub_servers_count
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, Subuser> $subusers
|
||||
* @property-read int|null $subusers_count
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, ServerUserSettings> $serverSettings
|
||||
* @property-read int|null $server_settings_count
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, ApiKey> $tokens
|
||||
* @property-read int|null $tokens_count
|
||||
*
|
||||
@@ -351,6 +354,30 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
|
||||
return $this->belongsToMany(Server::class, 'subusers');
|
||||
}
|
||||
|
||||
/** @return HasMany<ServerUserSettings, $this> */
|
||||
public function serverSettings(): HasMany
|
||||
{
|
||||
return $this->hasMany(ServerUserSettings::class);
|
||||
}
|
||||
|
||||
public function getServerSetting(Server $server, ServerUserSettingKey $key): string|int|bool
|
||||
{
|
||||
$settings = $this->serverSettings()->where('server_id', $server->id)->first()->settings ?? [];
|
||||
|
||||
return $settings[$key->value] ?? $key->getDefaultValue();
|
||||
}
|
||||
|
||||
public function updateServerSetting(Server $server, ServerUserSettingKey $key, string|int|bool $value): void
|
||||
{
|
||||
$row = $this->serverSettings()->firstOrNew(['server_id' => $server->id]);
|
||||
|
||||
$settings = $row->settings ?? [];
|
||||
$settings[$key->value] = $value;
|
||||
|
||||
$row->settings = array_intersect_key($settings, ServerUserSettingKey::getDefaultSettings());
|
||||
$row->save();
|
||||
}
|
||||
|
||||
/** @return ($key is null ? array<string, string|int|bool> : string|int|bool) */
|
||||
public function getCustomization(?CustomizationKey $key = null): array|string|int|bool|null
|
||||
{
|
||||
|
||||
@@ -21,6 +21,8 @@ class InitiateBackupService
|
||||
|
||||
private bool $isLocked = false;
|
||||
|
||||
private bool $isScheduled = false;
|
||||
|
||||
/**
|
||||
* InitiateBackupService constructor.
|
||||
*/
|
||||
@@ -41,6 +43,16 @@ class InitiateBackupService
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set if the backup was initiated by a schedule rather than a user.
|
||||
*/
|
||||
public function setIsScheduled(bool $isScheduled): self
|
||||
{
|
||||
$this->isScheduled = $isScheduled;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the files to be ignored by this backup.
|
||||
*
|
||||
@@ -125,6 +137,7 @@ class InitiateBackupService
|
||||
'ignored_files' => array_values($this->ignoredFiles ?? []),
|
||||
'backup_host_id' => $backupHost->id,
|
||||
'is_locked' => $this->isLocked,
|
||||
'is_scheduled' => $this->isScheduled,
|
||||
]);
|
||||
|
||||
$schema->createBackup($backup);
|
||||
|
||||
@@ -20,6 +20,7 @@ class BackupTransformer extends BaseClientTransformer
|
||||
'uuid' => $backup->uuid,
|
||||
'is_successful' => $backup->is_successful,
|
||||
'is_locked' => $backup->is_locked,
|
||||
'is_scheduled' => $backup->is_scheduled,
|
||||
'name' => $backup->name,
|
||||
'ignored_files' => $backup->ignored_files,
|
||||
'checksum' => $backup->checksum,
|
||||
|
||||
@@ -55,8 +55,6 @@ return [
|
||||
'send_install_notification' => env('PANEL_SEND_INSTALL_NOTIFICATION', true),
|
||||
// Should an email be sent to a server owner whenever their server is reinstalled?
|
||||
'send_reinstall_notification' => env('PANEL_SEND_REINSTALL_NOTIFICATION', true),
|
||||
// Should an email be sent to a server owner whenever a backup is completed?
|
||||
'send_backup_completed_notification' => env('PANEL_SEND_BACKUP_COMPLETED_NOTIFICATION', true),
|
||||
],
|
||||
|
||||
'filament' => [
|
||||
|
||||
56
database/Factories/ServerUserSettingsFactory.php
Normal file
56
database/Factories/ServerUserSettingsFactory.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Enums\ServerUserSettingKey;
|
||||
use App\Models\ServerUserSettings;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
class ServerUserSettingsFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* The name of the factory's corresponding model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $model = ServerUserSettings::class;
|
||||
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'settings' => [
|
||||
ServerUserSettingKey::ManualBackupNotifications->value => true,
|
||||
ServerUserSettingKey::ScheduledBackupNotifications->value => true,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate the user has opted in to all backup notifications.
|
||||
*/
|
||||
public function optedIn(): static
|
||||
{
|
||||
return $this->state([
|
||||
'settings' => [
|
||||
ServerUserSettingKey::ManualBackupNotifications->value => true,
|
||||
ServerUserSettingKey::ScheduledBackupNotifications->value => true,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate the user has opted out of all backup notifications.
|
||||
*/
|
||||
public function optedOut(): static
|
||||
{
|
||||
return $this->state([
|
||||
'settings' => [
|
||||
ServerUserSettingKey::ManualBackupNotifications->value => false,
|
||||
ServerUserSettingKey::ScheduledBackupNotifications->value => false,
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('server_user_settings', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedInteger('user_id');
|
||||
$table->unsignedInteger('server_id');
|
||||
$table->json('settings')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->foreign('user_id')->references('id')->on('users')->cascadeOnDelete();
|
||||
$table->foreign('server_id')->references('id')->on('servers')->cascadeOnDelete();
|
||||
$table->unique(['user_id', 'server_id']);
|
||||
});
|
||||
|
||||
// Backup notifications now default off for everyone. Owners on existing
|
||||
// installs previously always received them, so seed an opt-in row for each
|
||||
// server's owner unless the panel-wide email switch was already disabled.
|
||||
if (!filter_var(env('PANEL_SEND_BACKUP_COMPLETED_NOTIFICATION', true), FILTER_VALIDATE_BOOL)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$now = now();
|
||||
|
||||
DB::table('servers')->select(['id', 'owner_id'])->orderBy('id')->chunkById(250, function ($servers) use ($now) {
|
||||
DB::table('server_user_settings')->insert($servers->map(fn ($server) => [
|
||||
'user_id' => $server->owner_id,
|
||||
'server_id' => $server->id,
|
||||
'settings' => json_encode(['manual_backup_notifications' => true, 'scheduled_backup_notifications' => true]),
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
])->all());
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('server_user_settings');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('backups', function (Blueprint $table) {
|
||||
$table->boolean('is_scheduled')->default(false)->after('is_locked');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('backups', function (Blueprint $table) {
|
||||
$table->dropColumn('is_scheduled');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -121,7 +121,6 @@ return [
|
||||
'removed_from_server' => 'Removed from Server',
|
||||
'server_installed' => 'Server Installed',
|
||||
'server_reinstalled' => 'Server Reinstalled',
|
||||
'backup_completed' => 'Backup Completed',
|
||||
],
|
||||
'connections' => [
|
||||
'title' => 'Connections',
|
||||
|
||||
@@ -12,6 +12,14 @@ return [
|
||||
'successful' => 'Successful',
|
||||
'failed' => 'Failed',
|
||||
],
|
||||
'notifications' => [
|
||||
'action' => 'Notification Settings',
|
||||
'toggle_manual' => 'Manual Backup Notifications',
|
||||
'helper_manual' => 'Receive a notification and email when a manually created backup for this server completes or fails.',
|
||||
'toggle_scheduled' => 'Scheduled Backup Notifications',
|
||||
'helper_scheduled' => 'Receive a notification and email when a scheduled backup for this server completes or fails.',
|
||||
'saved' => 'Notification Settings Updated',
|
||||
],
|
||||
'actions' => [
|
||||
'create' => [
|
||||
'title' => 'Create Backup',
|
||||
|
||||
@@ -41,6 +41,18 @@ return [
|
||||
'password_body' => 'Your SFTP password is the same as the password you use to access this panel.',
|
||||
],
|
||||
],
|
||||
'notifications' => [
|
||||
'title' => 'Notifications',
|
||||
'saved' => 'Notification Settings Updated',
|
||||
'backup_manual' => [
|
||||
'label' => 'Manual Backup Notifications',
|
||||
'helper' => 'Receive a notification and email when a manually created backup for this server completes or fails.',
|
||||
],
|
||||
'backup_scheduled' => [
|
||||
'label' => 'Scheduled Backup Notifications',
|
||||
'helper' => 'Receive a notification and email when a scheduled backup for this server completes or fails.',
|
||||
],
|
||||
],
|
||||
'reinstall' => [
|
||||
'title' => 'Reinstall Server',
|
||||
'body' => 'Reinstalling your server will stop it, and then re-run the installation script that initially set it up.',
|
||||
|
||||
143
tests/Feature/BackupNotificationSettingsTest.php
Normal file
143
tests/Feature/BackupNotificationSettingsTest.php
Normal file
@@ -0,0 +1,143 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\ServerUserSettingKey;
|
||||
use App\Enums\SubuserPermission;
|
||||
use App\Events\Server\BackupCompleted;
|
||||
use App\Listeners\Server\BackupCompletedListener;
|
||||
use App\Models\Backup;
|
||||
use App\Models\BackupHost;
|
||||
use App\Models\Server;
|
||||
use App\Models\ServerUserSettings;
|
||||
use App\Models\User;
|
||||
use App\Notifications\BackupCompleted as BackupCompletedNotification;
|
||||
use Filament\Notifications\DatabaseNotification;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
|
||||
function runBackupCompletedListener(Server $server, bool $scheduled = false): void
|
||||
{
|
||||
/** @var Backup $backup */
|
||||
$backup = Backup::factory()->create([
|
||||
'server_id' => $server->id,
|
||||
'backup_host_id' => BackupHost::factory()->create()->id,
|
||||
'is_scheduled' => $scheduled,
|
||||
]);
|
||||
|
||||
(new BackupCompletedListener())->handle(new BackupCompleted($backup));
|
||||
}
|
||||
|
||||
it('does not notify anyone by default', function () {
|
||||
Notification::fake();
|
||||
|
||||
[$subuser, $server] = generateTestAccount([SubuserPermission::WebsocketConnect]);
|
||||
|
||||
runBackupCompletedListener($server);
|
||||
runBackupCompletedListener($server, scheduled: true);
|
||||
|
||||
Notification::assertNotSentTo($server->user, DatabaseNotification::class);
|
||||
Notification::assertNotSentTo($server->user, BackupCompletedNotification::class);
|
||||
Notification::assertNotSentTo($subuser, DatabaseNotification::class);
|
||||
Notification::assertNotSentTo($subuser, BackupCompletedNotification::class);
|
||||
});
|
||||
|
||||
it('notifies owners who opted in to manual backups only for manual backups', function () {
|
||||
Notification::fake();
|
||||
|
||||
[$owner, $server] = generateTestAccount();
|
||||
|
||||
$owner->updateServerSetting($server, ServerUserSettingKey::ManualBackupNotifications, true);
|
||||
|
||||
runBackupCompletedListener($server, scheduled: true);
|
||||
|
||||
Notification::assertNotSentTo($owner, DatabaseNotification::class);
|
||||
Notification::assertNotSentTo($owner, BackupCompletedNotification::class);
|
||||
|
||||
runBackupCompletedListener($server);
|
||||
|
||||
Notification::assertSentTo($owner, DatabaseNotification::class);
|
||||
Notification::assertSentTo($owner, BackupCompletedNotification::class);
|
||||
});
|
||||
|
||||
it('notifies owners who opted in to scheduled backups only for scheduled backups', function () {
|
||||
Notification::fake();
|
||||
|
||||
[$owner, $server] = generateTestAccount();
|
||||
|
||||
$owner->updateServerSetting($server, ServerUserSettingKey::ScheduledBackupNotifications, true);
|
||||
|
||||
runBackupCompletedListener($server);
|
||||
|
||||
Notification::assertNotSentTo($owner, DatabaseNotification::class);
|
||||
Notification::assertNotSentTo($owner, BackupCompletedNotification::class);
|
||||
|
||||
runBackupCompletedListener($server, scheduled: true);
|
||||
|
||||
Notification::assertSentTo($owner, DatabaseNotification::class);
|
||||
Notification::assertSentTo($owner, BackupCompletedNotification::class);
|
||||
});
|
||||
|
||||
it('notifies subusers who opted in', function () {
|
||||
Notification::fake();
|
||||
|
||||
[$subuser, $server] = generateTestAccount([SubuserPermission::WebsocketConnect]);
|
||||
|
||||
$subuser->updateServerSetting($server, ServerUserSettingKey::ManualBackupNotifications, true);
|
||||
|
||||
runBackupCompletedListener($server);
|
||||
|
||||
Notification::assertSentTo($subuser, DatabaseNotification::class);
|
||||
Notification::assertSentTo($subuser, BackupCompletedNotification::class);
|
||||
});
|
||||
|
||||
it('does not notify users who opted back out', function () {
|
||||
Notification::fake();
|
||||
|
||||
[$owner, $server] = generateTestAccount();
|
||||
|
||||
$owner->updateServerSetting($server, ServerUserSettingKey::ManualBackupNotifications, true);
|
||||
$owner->updateServerSetting($server, ServerUserSettingKey::ManualBackupNotifications, false);
|
||||
|
||||
runBackupCompletedListener($server);
|
||||
|
||||
Notification::assertNotSentTo($owner, DatabaseNotification::class);
|
||||
Notification::assertNotSentTo($owner, BackupCompletedNotification::class);
|
||||
});
|
||||
|
||||
it('defaults backup notifications off for everyone', function () {
|
||||
[$owner, $server] = generateTestAccount();
|
||||
|
||||
/** @var User $subuser */
|
||||
$subuser = User::factory()->create();
|
||||
|
||||
foreach (ServerUserSettingKey::cases() as $key) {
|
||||
expect($owner->getServerSetting($server, $key))->toBeFalse()
|
||||
->and($subuser->getServerSetting($server, $key))->toBeFalse();
|
||||
}
|
||||
});
|
||||
|
||||
it('updates a single settings row per user and server', function () {
|
||||
[$owner, $server] = generateTestAccount();
|
||||
|
||||
$owner->updateServerSetting($server, ServerUserSettingKey::ManualBackupNotifications, false);
|
||||
$owner->updateServerSetting($server, ServerUserSettingKey::ManualBackupNotifications, true);
|
||||
$owner->updateServerSetting($server, ServerUserSettingKey::ScheduledBackupNotifications, true);
|
||||
|
||||
expect(ServerUserSettings::query()->where('user_id', $owner->id)->where('server_id', $server->id)->count())->toBe(1)
|
||||
->and($owner->getServerSetting($server, ServerUserSettingKey::ManualBackupNotifications))->toBeTrue()
|
||||
->and($owner->getServerSetting($server, ServerUserSettingKey::ScheduledBackupNotifications))->toBeTrue();
|
||||
});
|
||||
|
||||
it('drops unknown keys when updating settings', function () {
|
||||
[$owner, $server] = generateTestAccount();
|
||||
|
||||
ServerUserSettings::factory()->create([
|
||||
'user_id' => $owner->id,
|
||||
'server_id' => $server->id,
|
||||
'settings' => ['unknown_key' => 'value'],
|
||||
]);
|
||||
|
||||
$owner->updateServerSetting($server, ServerUserSettingKey::ManualBackupNotifications, true);
|
||||
|
||||
$settings = ServerUserSettings::query()->where('user_id', $owner->id)->where('server_id', $server->id)->value('settings');
|
||||
|
||||
expect($settings)->toBe([ServerUserSettingKey::ManualBackupNotifications->value => true]);
|
||||
});
|
||||
Reference in New Issue
Block a user