Compare commits

...

3 Commits

Author SHA1 Message Date
Lance Pioch
da64dbb6b5 Split backup notifications into manual and scheduled settings
Backups now record whether they were initiated by a schedule task via a
new is_scheduled column, set by CreateBackupSchema through the initiate
service. The per-user setting splits into manual_backup_notifications
and scheduled_backup_notifications, both opt-in, so users can subscribe
to each kind independently. The upgrade seeding grants owners both.

The backup origin is also exposed in the client API transformer.
2026-07-17 10:39:39 -04:00
Lance Pioch
0ef479274d Default backup notifications off and remove global email switch
Backup notifications are now opt-in for everyone, owners included; the
per-user toggle is the only control and covers both the database
notification and the email.

Removes the PANEL_SEND_BACKUP_COMPLETED_NOTIFICATION env/config and its
admin settings toggle. To preserve behavior on existing installs, the
migration seeds an opt-in row for each server's owner unless that env
variable is set to false.
2026-07-17 09:30:47 -04:00
Lance Pioch
44077a1b40 Add per-user server settings for backup notifications
Adds a server_user_settings table (user_id, server_id, JSON settings with
enum-whitelisted keys) so each user with access to a server can choose
whether they receive backup notifications from it. Owners default on,
subusers default off. The global config remains a panel-wide email switch.

Toggles live on the server Settings page and the Backups list header.
2026-07-12 15:37:57 -04:00
20 changed files with 580 additions and 31 deletions

View 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;
}
}

View File

@@ -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

View File

@@ -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'))

View File

@@ -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);

View File

@@ -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 [];

View File

@@ -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));
}
}
}

View File

@@ -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',

View File

@@ -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.
*/

View 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);
}
}

View File

@@ -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
{

View File

@@ -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);

View File

@@ -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,

View File

@@ -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' => [

View 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,
],
]);
}
}

View File

@@ -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');
}
};

View File

@@ -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');
});
}
};

View File

@@ -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',

View File

@@ -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',

View File

@@ -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.',

View 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]);
});