WIP notifications and other refactors (#88)

* WIP notifications and other refactors
- refactor notification channels
- send notifications on events related to the servers and sites
- delete server log files on server deletion
- add telegram notification channel
- add new icons
- cache configs and icons on installation and updates
- new navbar for dark mode and settings

* discord channel

* build assets

* pint
This commit is contained in:
Saeed Vaziry 2024-01-07 09:54:08 +01:00 committed by GitHub
parent f06b8f7d20
commit e997d0deea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
72 changed files with 1153 additions and 480 deletions

View File

@ -26,8 +26,8 @@ REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=smtp
MAIL_HOST=mailhog
MAIL_PORT=1025
MAIL_HOST=
MAIL_PORT=
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null

View File

@ -26,8 +26,8 @@ REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=smtp
MAIL_HOST=mailhog
MAIL_PORT=1025
MAIL_HOST=
MAIL_PORT=
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null

View File

@ -26,8 +26,8 @@ REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=smtp
MAIL_HOST=mailhog
MAIL_PORT=1025
MAIL_HOST=
MAIL_PORT=
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null

View File

@ -21,16 +21,25 @@ public function add(User $user, array $input): void
'label' => $input['label'],
]);
$this->validateType($channel, $input);
$channel->data = $channel->provider()->data($input);
$channel->data = $channel->provider()->createData($input);
$channel->save();
if (! $channel->provider()->connect()) {
$channel->delete();
if ($channel->provider === \App\Enums\NotificationChannel::EMAIL) {
throw ValidationException::withMessages([
'email' => __('Could not connect! Make sure you configured `.env` file correctly.'),
]);
}
throw ValidationException::withMessages([
'provider' => __('Could not connect'),
]);
}
$channel->connected = true;
$channel->save();
}
/**
@ -49,7 +58,7 @@ protected function validate(array $input): void
*/
protected function validateType(NotificationChannel $channel, array $input): void
{
Validator::make($input, $channel->provider()->validationRules())
Validator::make($input, $channel->provider()->createRules($input))
->validate();
}
}

View File

@ -2,9 +2,17 @@
namespace App\Contracts;
use Illuminate\Notifications\Messages\MailMessage;
interface Notification
{
public function subject(): string;
public function rawText(): string;
public function message(bool $mail = false): mixed;
public function toMail(object $notifiable): MailMessage;
public function toSlack(object $notifiable): string;
public function toDiscord(object $notifiable): string;
public function toTelegram(object $notifiable): string;
}

View File

@ -4,11 +4,13 @@
interface NotificationChannel
{
public function validationRules(): array;
public function createRules(array $input): array;
public function data(array $input): array;
public function createData(array $input): array;
public function data(): array;
public function connect(): bool;
public function sendMessage(string $subject, string $text): void;
public function send(object $notifiable, Notification $notification): void;
}

View File

@ -11,4 +11,6 @@ final class NotificationChannel extends Enum
const SLACK = 'slack';
const DISCORD = 'discord';
const TELEGRAM = 'telegram';
}

17
app/Facades/Notifier.php Normal file
View File

@ -0,0 +1,17 @@
<?php
namespace App\Facades;
use App\Contracts\Notification;
use Illuminate\Support\Facades\Facade;
/**
* @method static void send(object $notifiable, Notification $notification)
*/
class Notifier extends Facade
{
protected static function getFacadeAccessor(): string
{
return 'notifier';
}
}

19
app/Helpers/Notifier.php Normal file
View File

@ -0,0 +1,19 @@
<?php
namespace App\Helpers;
use App\Contracts\Notification;
use App\Models\NotificationChannel;
class Notifier
{
/**
* In the future we can send notifications based on the notifiable instance
* For example, If it was a server then we will send the channels specified by that server
* For now, we will send all channels.
*/
public function send(object $notifiable, Notification $notification): void
{
NotificationChannel::notifyAll($notification);
}
}

View File

@ -3,8 +3,11 @@
namespace App\Http\Controllers;
use App\Exceptions\SourceControlIsNotConnected;
use App\Facades\Notifier;
use App\Models\GitHook;
use App\Notifications\SourceControlDisconnected;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Throwable;
class GitHookController extends Controller
@ -25,7 +28,7 @@ public function __invoke(Request $request)
try {
$gitHook->site->deploy();
} catch (SourceControlIsNotConnected) {
// TODO: send notification
Notifier::send($gitHook->sourceControl, new SourceControlDisconnected($gitHook->sourceControl));
} catch (Throwable $e) {
Log::error('git-hook-exception', (array) $e);
}

View File

@ -15,6 +15,10 @@ class AddChannel extends Component
public string $email;
public string $bot_token;
public string $chat_id;
public function add(): void
{
app(\App\Actions\NotificationChannels\AddChannel::class)->add(

View File

@ -3,8 +3,10 @@
namespace App\Jobs\Server;
use App\Events\Broadcast;
use App\Facades\Notifier;
use App\Jobs\Job;
use App\Models\Server;
use App\Notifications\ServerDisconnected;
use Throwable;
class CheckConnection extends Job
@ -39,7 +41,7 @@ public function failed(): void
{
$this->server->status = 'disconnected';
$this->server->save();
/** @todo notify */
Notifier::send($this->server, new ServerDisconnected($this->server));
event(
new Broadcast('server-status-failed', [
'server' => $this->server,

View File

@ -1,47 +0,0 @@
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Queue\SerializesModels;
class NotificationChannelMessage extends Mailable implements ShouldQueue
{
use Queueable, SerializesModels;
/**
* @var mixed
*/
public $text;
/**
* Create a new message instance.
*
* @return void
*/
public function __construct($subject, $text)
{
$this->subject = $subject;
$this->text = $text;
}
/**
* Build the message.
*
* @return $this
*/
public function build()
{
if ($this->text instanceof MailMessage) {
return $this->markdown('vendor.notifications.email', $this->text->data());
}
return $this->markdown('emails.notification-channel-message', [
'subject' => $this->subject,
'text' => $this->text,
]);
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class NotificationMail extends Mailable
{
use Queueable, SerializesModels;
public string $text;
public function __construct(string $subject, string $text)
{
$this->subject = $subject;
$this->text = $text;
}
public function build(): self
{
return $this->html($this->text);
}
}

View File

@ -2,19 +2,21 @@
namespace App\Models;
use App\Contracts\Notification;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Notifications\Notifiable;
/**
* @property string $provider
* @property string $label
* @property array $data
* @property bool $connected
* @property bool $is_default
* @property User $user
* @property int $id
* @property string provider
* @property array data
* @property string label
* @property bool connected
*/
class NotificationChannel extends AbstractModel
{
use HasFactory;
use Notifiable;
protected $fillable = [
'provider',
@ -25,15 +27,24 @@ class NotificationChannel extends AbstractModel
];
protected $casts = [
'data' => 'json',
'project_id' => 'integer',
'data' => 'array',
'connected' => 'boolean',
'is_default' => 'boolean',
];
public function provider(): \App\Contracts\NotificationChannel
{
$provider = config('core.notification_channels_providers_class')[$this->provider];
$class = config('core.notification_channels_providers_class')[$this->provider];
return new $provider($this);
return new $class($this);
}
public static function notifyAll(Notification $notification): void
{
$channels = self::all();
foreach ($channels as $channel) {
$channel->notify($notification);
}
}
}

View File

@ -4,15 +4,18 @@
use App\Contracts\ServerType;
use App\Enums\ServerStatus;
use App\Facades\Notifier;
use App\Facades\SSH;
use App\Jobs\Installation\Upgrade;
use App\Jobs\Server\CheckConnection;
use App\Jobs\Server\RebootServer;
use App\Notifications\ServerInstallationStarted;
use App\Support\Testing\SSHFake;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Filesystem\FilesystemAdapter;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
@ -110,7 +113,9 @@ public static function boot(): void
$site->delete();
});
$server->provider()->delete();
$server->logs()->delete();
$server->logs()->each(function (ServerLog $log) {
$log->delete();
});
$server->services()->delete();
$server->databases()->delete();
$server->databaseUsers()->delete();
@ -239,7 +244,7 @@ public function getServiceByUnit($unit): ?Service
public function install(): void
{
$this->type()->install();
// $this->team->notify(new ServerInstallationStarted($this));
Notifier::send($this, new ServerInstallationStarted($this));
}
public function ssh(?string $user = null): \App\Helpers\SSH|SSHFake
@ -343,7 +348,7 @@ public function sshKey(): array
];
}
/** @var \Illuminate\Filesystem\FilesystemAdapter $storageDisk */
/** @var FilesystemAdapter $storageDisk */
$storageDisk = Storage::disk(config('core.key_pairs_disk'));
return [

View File

@ -34,6 +34,17 @@ class ServerLog extends AbstractModel
'site_id' => 'integer',
];
public static function boot(): void
{
parent::boot();
static::deleting(function (ServerLog $log) {
if (Storage::disk($log->disk)->exists($log->name)) {
Storage::disk($log->disk)->delete($log->name);
}
});
}
public function getRouteKey(): string
{
return 'log';

View File

@ -8,10 +8,13 @@
use App\Enums\SslStatus;
use App\Events\Broadcast;
use App\Exceptions\SourceControlIsNotConnected;
use App\Facades\Notifier;
use App\Jobs\Site\ChangePHPVersion;
use App\Jobs\Site\Deploy;
use App\Jobs\Site\DeployEnv;
use App\Jobs\Site\UpdateBranch;
use App\Notifications\SiteInstallationFailed;
use App\Notifications\SiteInstallationSucceed;
use Exception;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
@ -406,7 +409,7 @@ public function installationFinished(): void
'site' => $this,
])
);
/** @todo notify */
Notifier::send($this, new SiteInstallationSucceed($this));
}
/**
@ -422,7 +425,7 @@ public function installationFailed(Throwable $e): void
'site' => $this,
])
);
/** @todo notify */
Notifier::send($this, new SiteInstallationFailed($this));
Log::error('install-site-error', [
'error' => (string) $e,
]);

View File

@ -0,0 +1,13 @@
<?php
namespace App\NotificationChannels;
use App\Contracts\NotificationChannel as NotificationChannelInterface;
use App\Models\NotificationChannel;
abstract class AbstractNotificationChannel implements NotificationChannelInterface
{
public function __construct(protected NotificationChannel $notificationChannel)
{
}
}

View File

@ -1,16 +0,0 @@
<?php
namespace App\NotificationChannels;
use App\Contracts\NotificationChannel as NotificationChannelContract;
use App\Models\NotificationChannel;
abstract class AbstractProvider implements NotificationChannelContract
{
protected NotificationChannel $notificationChannel;
public function __construct(NotificationChannel $notificationChannel)
{
$this->notificationChannel = $notificationChannel;
}
}

View File

@ -2,21 +2,34 @@
namespace App\NotificationChannels;
use App\Contracts\Notification;
use Illuminate\Support\Facades\Http;
class Discord extends AbstractProvider
class Discord extends AbstractNotificationChannel
{
public function validationRules(): array
public function channel(): string
{
return 'discord';
}
public function createRules(array $input): array
{
return [
'webhook_url' => 'required|url',
];
}
public function data(array $input): array
public function createData(array $input): array
{
return [
'webhook_url' => $input['webhook_url'],
'webhook_url' => $input['webhook_url'] ?? '',
];
}
public function data(): array
{
return [
'webhook_url' => $this->notificationChannel->data['webhook_url'] ?? '',
];
}
@ -24,35 +37,37 @@ public function connect(): bool
{
$connect = $this->checkConnection(
__('Congratulations! 🎉'),
__("You've connected your Discord to Vito")."\n".
__("You've connected your Discord to :app", ['app' => config('app.name')])."\n".
__('Manage your notification channels')."\n".
route('notification-channels')
);
if (! $connect) {
$this->notificationChannel->delete();
return false;
}
$this->notificationChannel->connected = true;
$this->notificationChannel->save();
return true;
}
public function sendMessage(string $subject, string $text): void
{
dispatch(function () use ($subject, $text) {
$data = $this->notificationChannel->data;
Http::post($data['webhook_url'], [
'content' => '*'.$subject.'*'."\n".$text,
]);
});
}
private function checkConnection(string $subject, string $text): bool
{
$data = $this->notificationChannel->data;
$connect = Http::post($data['webhook_url'], [
$connect = Http::post($this->data()['webhook_url'], [
'content' => '*'.$subject.'*'."\n".$text,
]);
return $connect->ok();
}
public function send(object $notifiable, Notification $notification): void
{
$data = $this->notificationChannel->data;
Http::post($data['webhook_url'], [
'content' => $notification->toSlack($notifiable),
]);
}
}

View File

@ -2,36 +2,56 @@
namespace App\NotificationChannels;
use App\Mail\NotificationChannelMessage;
use App\Contracts\Notification;
use App\Mail\NotificationMail;
use App\Models\NotificationChannel;
use Illuminate\Support\Facades\Mail;
use Throwable;
class Email extends AbstractProvider
class Email extends AbstractNotificationChannel
{
public function validationRules(): array
public function createRules(array $input): array
{
return [
'email' => 'required|email',
];
}
public function data(array $input): array
public function createData(array $input): array
{
return [
'email' => $input['email'],
];
}
public function data(): array
{
return [
'email' => $this->notificationChannel->data['email'] ?? '',
];
}
public function connect(): bool
{
$this->notificationChannel->connected = true;
$this->notificationChannel->save();
try {
Mail::to($this->data()['email'])->send(
new NotificationMail('Test VitoDeploy', 'This is a test email!')
);
} catch (Throwable) {
return false;
}
return true;
}
public function sendMessage(string $subject, mixed $text): void
public function send(object $notifiable, Notification $notification): void
{
$data = $this->notificationChannel->data;
Mail::to($data['email'])->send(new NotificationChannelMessage($subject, $text));
/** @var NotificationChannel $notifiable */
$this->notificationChannel = $notifiable;
$message = $notification->toMail($notifiable);
Mail::to($this->data()['email'])->send(
new NotificationMail($message->subject, $message->render())
);
}
}

View File

@ -2,21 +2,34 @@
namespace App\NotificationChannels;
use App\Contracts\Notification;
use Illuminate\Support\Facades\Http;
class Slack extends AbstractProvider
class Slack extends AbstractNotificationChannel
{
public function validationRules(): array
public function channel(): string
{
return 'slack';
}
public function createRules(array $input): array
{
return [
'webhook_url' => 'required|url',
];
}
public function data(array $input): array
public function createData(array $input): array
{
return [
'webhook_url' => $input['webhook_url'],
'webhook_url' => $input['webhook_url'] ?? '',
];
}
public function data(): array
{
return [
'webhook_url' => $this->notificationChannel->data['webhook_url'] ?? '',
];
}
@ -24,35 +37,37 @@ public function connect(): bool
{
$connect = $this->checkConnection(
__('Congratulations! 🎉'),
__("You've connected your Slack to Vito")."\n".
__("You've connected your Slack to :app", ['app' => config('app.name')])."\n".
__('Manage your notification channels')."\n".
route('notification-channels')
);
if (! $connect) {
$this->notificationChannel->delete();
return false;
}
$this->notificationChannel->connected = true;
$this->notificationChannel->save();
return true;
}
public function sendMessage(string $subject, string $text): void
{
dispatch(function () use ($subject, $text) {
$data = $this->notificationChannel->data;
Http::post($data['webhook_url'], [
'text' => '*'.$subject.'*'."\n".$text,
]);
});
}
private function checkConnection(string $subject, string $text): bool
{
$data = $this->notificationChannel->data;
$connect = Http::post($data['webhook_url'], [
$connect = Http::post($this->data()['webhook_url'], [
'text' => '*'.$subject.'*'."\n".$text,
]);
return $connect->ok();
}
public function send(object $notifiable, Notification $notification): void
{
$data = $this->notificationChannel->data;
Http::post($data['webhook_url'], [
'text' => $notification->toSlack($notifiable),
]);
}
}

View File

@ -0,0 +1,67 @@
<?php
namespace App\NotificationChannels;
use App\Contracts\Notification;
use Illuminate\Support\Facades\Http;
use Throwable;
class Telegram extends AbstractNotificationChannel
{
protected string $apiUrl = 'https://api.telegram.org/bot';
public function channel(): string
{
return 'telegram';
}
public function createRules(array $input): array
{
return [
'bot_token' => 'required|string',
'chat_id' => 'required',
];
}
public function createData(array $input): array
{
return [
'bot_token' => $input['bot_token'],
'chat_id' => $input['chat_id'],
];
}
public function data(): array
{
return [
'bot_token' => $this->notificationChannel->data['bot_token'] ?? '',
'chat_id' => $this->notificationChannel->data['chat_id'] ?? '',
];
}
public function connect(): bool
{
try {
$this->sendToTelegram(__('Connected!'));
} catch (Throwable) {
return false;
}
return true;
}
public function send(object $notifiable, Notification $notification): void
{
$this->sendToTelegram($notification->toTelegram($notifiable));
}
private function sendToTelegram(string $text): void
{
Http::post($this->apiUrl.$this->data()['bot_token'].'/sendMessage', [
'chat_id' => $this->data()['chat_id'],
'text' => $text,
'parse_mode' => 'markdown',
'disable_web_page_preview' => true,
]);
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace App\Notifications;
use App\Contracts\Notification as NotificationInterface;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
use Illuminate\Queue\SerializesModels;
abstract class AbstractNotification extends Notification implements NotificationInterface, ShouldQueue
{
use Queueable, SerializesModels;
public function toMail(object $notifiable): MailMessage
{
return (new MailMessage())
->line($this->rawText());
}
public function toSlack(object $notifiable): string
{
return $this->rawText();
}
public function toDiscord(object $notifiable): string
{
return $this->rawText();
}
public function toTelegram(object $notifiable): string
{
return $this->rawText();
}
}

View File

@ -2,12 +2,11 @@
namespace App\Notifications;
use App\Contracts\Notification;
use App\Models\Server;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Messages\MailMessage;
class FailedToDeleteServerFromProvider implements Notification
class FailedToDeleteServerFromProvider extends AbstractNotification
{
use Queueable;
@ -18,26 +17,18 @@ public function __construct(Server $server)
$this->server = $server;
}
public function subject(): string
public function rawText(): string
{
return __('Failed to delete the server from the provider!');
}
public function message(bool $mail = false): mixed
{
if ($mail) {
return $this->mail();
}
return __("We couldn't delete [:server] \nfrom :provider \nPlease check your provider and delete it manually", [
'server' => $this->server->name,
'provider' => $this->server->provider,
]);
}
public function mail(): MailMessage
public function toMail(object $notifiable): MailMessage
{
return (new MailMessage)
->subject(__('Failed to delete the server from the provider!'))
->line("We couldn't delete [".$this->server->name.'] from '.$this->server->provider)
->line('Please check your provider and delete it manually');
}

View File

@ -1,26 +0,0 @@
<?php
namespace App\Notifications;
use App\Contracts\Notification;
use App\Models\Ssl;
class SSLExpirationAlert implements Notification
{
protected Ssl $ssl;
public function __construct(Ssl $ssl)
{
$this->ssl = $ssl;
}
public function subject(): string
{
return __('SSL expiring soon!');
}
public function message(bool $mail = false): string
{
return $this->ssl->site->domain."'s ".__('SSL is expiring on').' '.$this->ssl->expires_at->format('Y-m-d');
}
}

View File

@ -2,11 +2,10 @@
namespace App\Notifications;
use App\Contracts\Notification;
use App\Models\Server;
use Illuminate\Notifications\Messages\MailMessage;
class ServerDisconnected implements Notification
class ServerDisconnected extends AbstractNotification
{
protected Server $server;
@ -15,25 +14,17 @@ public function __construct(Server $server)
$this->server = $server;
}
public function subject(): string
public function rawText(): string
{
return __('Server disconnected!');
}
public function message(bool $mail = false): mixed
{
if ($mail) {
return $this->mail();
}
return __("We've disconnected from your server [:server]", [
'server' => $this->server->name,
]);
}
public function mail(): MailMessage
public function toMail(object $notifiable): MailMessage
{
return (new MailMessage)
->subject(__('Server disconnected!'))
->line("We've disconnected from your server [".$this->server->name.'].')
->line('Please check your sever is online and make sure that has our public keys in it');
}

View File

@ -2,11 +2,10 @@
namespace App\Notifications;
use App\Contracts\Notification;
use App\Models\Server;
use Illuminate\Notifications\Messages\MailMessage;
class ServerInstallationFailed implements Notification
class ServerInstallationFailed extends AbstractNotification
{
protected Server $server;
@ -15,26 +14,18 @@ public function __construct(Server $server)
$this->server = $server;
}
public function subject(): string
public function rawText(): string
{
return __('Server installation failed!');
}
public function message(bool $mail = false): mixed
{
if ($mail) {
return $this->mail();
}
return __("Installation failed for server [:server] \nCheck your server's logs \n:logs", [
'server' => $this->server->name,
'logs' => url('/servers/'.$this->server->id.'/logs'),
]);
}
private function mail(): MailMessage
public function toMail(object $notifiable): MailMessage
{
return (new MailMessage)
->subject(__('Server installation failed!'))
->line('Your server ['.$this->server->name.'] installation has been failed.')
->line('Check your server logs')
->action('View Logs', url('/servers/'.$this->server->id.'/logs'));

View File

@ -2,11 +2,10 @@
namespace App\Notifications;
use App\Contracts\Notification;
use App\Models\Server;
use Illuminate\Notifications\Messages\MailMessage;
class ServerInstallationStarted implements Notification
class ServerInstallationStarted extends AbstractNotification
{
protected Server $server;
@ -15,26 +14,18 @@ public function __construct(Server $server)
$this->server = $server;
}
public function subject(): string
public function rawText(): string
{
return __('Server installation started!');
}
public function message(bool $mail = false): mixed
{
if ($mail) {
return $this->mail();
}
return __("Installation started for server [:server]\nThis may take several minutes depending on many things like your server's internet speed.\nAs soon as it finishes, We will notify you through this channel.\nYou can check the progress live on your dashboard.\n:progress", [
'server' => $this->server->name,
'progress' => url('/servers/'.$this->server->id),
]);
}
public function mail(): MailMessage
public function toMail(object $notifiable): MailMessage
{
return (new MailMessage)
->subject(__('Server installation started!'))
->line('Your server\'s ['.$this->server->name.'] installation has been started.')
->line("This may take several minutes depending on many things like your server's internet speed.")
->line('As soon as it finishes, We will notify you through this channel.')

View File

@ -2,11 +2,10 @@
namespace App\Notifications;
use App\Contracts\Notification;
use App\Models\Server;
use Illuminate\Notifications\Messages\MailMessage;
class ServerInstallationSucceed implements Notification
class ServerInstallationSucceed extends AbstractNotification
{
protected Server $server;
@ -20,14 +19,10 @@ public function subject(): string
return __('Server installation succeed!');
}
public function message(bool $mail = false): mixed
public function rawText(): string
{
$this->server->refresh();
if ($mail) {
return $this->mail();
}
return __("Installation succeed for server [:server] \nServer IP: :ip \nUser: :user\nPassword: :password\n:link", [
'server' => $this->server->name,
'ip' => $this->server->ip,
@ -37,11 +32,12 @@ public function message(bool $mail = false): mixed
]);
}
public function mail(): MailMessage
public function toMail(object $notifiable): MailMessage
{
$this->server->refresh();
return (new MailMessage)
->subject(__('Server installation succeed!'))
->line('Your server ['.$this->server->name.'] has been installed successfully.')
->line('Server IP: '.$this->server->ip)
->line('User: '.$this->server->authentication['user'])

View File

@ -0,0 +1,30 @@
<?php
namespace App\Notifications;
use App\Models\Site;
use Illuminate\Notifications\Messages\MailMessage;
class SiteInstallationFailed extends AbstractNotification
{
public function __construct(protected Site $site)
{
}
public function rawText(): string
{
return __("Installation failed for site [:site] \nCheck your server's logs \n:logs", [
'site' => $this->site->domain,
'logs' => url('/servers/'.$this->site->server_id.'/logs'),
]);
}
public function toMail(object $notifiable): MailMessage
{
return (new MailMessage)
->subject(__('Site installation failed!'))
->line('Your site\'s ['.$this->site->domain.'] installation has been failed.')
->line('Check your server logs')
->action('View Logs', url('/servers/'.$this->site->server_id.'/logs'));
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Notifications;
use App\Models\Site;
use Illuminate\Notifications\Messages\MailMessage;
class SiteInstallationSucceed extends AbstractNotification
{
public function __construct(protected Site $site)
{
}
public function rawText(): string
{
return __('Installation succeed for site [:site]', [
'site' => $this->site->domain,
]);
}
public function toMail(object $notifiable): MailMessage
{
return (new MailMessage)
->subject(__('Site installation succeed!'))
->line('Your site\'s ['.$this->site->domain.'] installation has been installed.')
->line('Check your site')
->action('View Site', url('/servers/'.$this->site->server_id.'/sites/'.$this->site->id));
}
}

View File

@ -2,41 +2,26 @@
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use App\Models\SourceControl;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class SourceControlDisconnected extends Notification implements ShouldQueue
class SourceControlDisconnected extends AbstractNotification
{
use Queueable;
protected string $sourceControl;
public function __construct(string $sourceControl)
public function __construct(protected SourceControl $sourceControl)
{
$this->sourceControl = $sourceControl;
}
public function via(): array
public function rawText(): string
{
return ['mail'];
return __('Source control [:sourceControl] has been disconnected from Vito', [
'sourceControl' => $this->sourceControl->profile,
]);
}
public function toMail(): MailMessage
public function toMail(object $notifiable): MailMessage
{
return (new MailMessage)
->subject('Lost connection to your '.$this->sourceControl)
->line("We've lost connection to your $this->sourceControl account.")
->line("We'll not able to do any deployments until you reconnect.")
->line("To reconnect your $this->sourceControl account please click on the bellow button.")
->action('Reconnect', url('/source-controls'));
}
public function toArray(): array
{
return [
//
];
->subject(__('Source control disconnected!'))
->line($this->rawText());
}
}

View File

@ -2,6 +2,7 @@
namespace App\Providers;
use App\Helpers\Notifier;
use App\Helpers\SSH;
use App\Support\SocialiteProviders\DropboxProvider;
use Illuminate\Contracts\Container\BindingResolutionException;
@ -29,6 +30,9 @@ public function boot(): void
$this->app->bind('ssh', function () {
return new SSH;
});
$this->app->bind('notifier', function () {
return new Notifier;
});
$this->extendSocialite();
}

View File

@ -4,10 +4,14 @@
use App\Enums\OperatingSystem;
use App\Exceptions\CouldNotConnectToProvider;
use App\Facades\Notifier;
use App\Notifications\FailedToDeleteServerFromProvider;
use Aws\Ec2\Ec2Client;
use Aws\EC2InstanceConnect\EC2InstanceConnectClient;
use Exception;
use Illuminate\Filesystem\FilesystemAdapter;
use Illuminate\Support\Facades\Storage;
use Throwable;
class AWS extends AbstractProvider
{
@ -125,9 +129,8 @@ public function delete(): void
$this->ec2Client->terminateInstances([
'InstanceIds' => [$this->server->provider_data['instance_id']],
]);
} catch (Exception) {
/** @todo notify */
// $this->server->team->notify(new FailedToDeleteServerFromProvider($this->server));
} catch (Throwable) {
Notifier::send($this->server, new FailedToDeleteServerFromProvider($this->server));
}
}
}
@ -164,7 +167,7 @@ private function createKeyPair(): void
$result = $this->ec2Client->createKeyPair([
'KeyName' => $keyName,
]);
/** @var \Illuminate\Filesystem\FilesystemAdapter $storageDisk */
/** @var FilesystemAdapter $storageDisk */
$storageDisk = Storage::disk(config('core.key_pairs_disk'));
$storageDisk->put((string) $this->server->id, $result['KeyMaterial']);
generate_public_key(

View File

@ -4,6 +4,8 @@
use App\Exceptions\CouldNotConnectToProvider;
use App\Exceptions\ServerProviderError;
use App\Facades\Notifier;
use App\Notifications\FailedToDeleteServerFromProvider;
use Exception;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
@ -148,10 +150,9 @@ public function delete(): void
$delete = Http::withToken($this->server->serverProvider->credentials['token'])
->delete($this->apiUrl.'/droplets/'.$this->server->provider_data['droplet_id']);
/** @todo notify */
// if (! $delete->ok()) {
// $this->server->team->notify(new FailedToDeleteServerFromProvider($this->server));
// }
if (! $delete->ok()) {
Notifier::send($this->server, new FailedToDeleteServerFromProvider($this->server));
}
}
}
}

View File

@ -4,6 +4,8 @@
use App\Exceptions\CouldNotConnectToProvider;
use App\Exceptions\ServerProviderError;
use App\Facades\Notifier;
use App\Notifications\FailedToDeleteServerFromProvider;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Http;
@ -122,10 +124,9 @@ public function delete(): void
$delete = Http::withToken($this->server->serverProvider->credentials['token'])
->delete($this->apiUrl.'/servers/'.$this->server->provider_data['hetzner_id']);
/** @todo notify */
// if (! $delete->ok()) {
// $this->server->team->notify(new FailedToDeleteServerFromProvider($this->server));
// }
if (! $delete->ok()) {
Notifier::send($this->server, new FailedToDeleteServerFromProvider($this->server));
}
}
// delete key

View File

@ -4,6 +4,8 @@
use App\Exceptions\CouldNotConnectToProvider;
use App\Exceptions\ServerProviderError;
use App\Facades\Notifier;
use App\Notifications\FailedToDeleteServerFromProvider;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
@ -131,10 +133,9 @@ public function delete(): void
$delete = Http::withToken($this->server->serverProvider->credentials['token'])
->delete($this->apiUrl.'/linode/instances/'.$this->server->provider_data['linode_id']);
/** @todo notify */
// if (! $delete->ok()) {
// $this->server->team->notify(new FailedToDeleteServerFromProvider($this->server));
// }
if (! $delete->ok()) {
Notifier::send($this->server, new FailedToDeleteServerFromProvider($this->server));
}
}
}
}

View File

@ -4,6 +4,8 @@
use App\Exceptions\CouldNotConnectToProvider;
use App\Exceptions\ServerProviderError;
use App\Facades\Notifier;
use App\Notifications\FailedToDeleteServerFromProvider;
use Exception;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
@ -144,10 +146,9 @@ public function delete(): void
$delete = Http::withToken($this->server->serverProvider->credentials['token'])
->delete($this->apiUrl.'/instances/'.$this->server->provider_data['instance_id']);
/** @todo notify */
// if (! $delete->ok()) {
// $this->server->team->notify(new FailedToDeleteServerFromProvider($this->server));
// }
if (! $delete->ok()) {
Notifier::send($this->server, new FailedToDeleteServerFromProvider($this->server));
}
}
}
}

View File

@ -3,9 +3,12 @@
namespace App\ServerTypes;
use App\Events\Broadcast;
use App\Facades\Notifier;
use App\Jobs\Installation\Initialize;
use App\Jobs\Installation\InstallRequirements;
use App\Jobs\Installation\Upgrade;
use App\Notifications\ServerInstallationFailed;
use App\Notifications\ServerInstallationSucceed;
use Illuminate\Support\Facades\Bus;
use Illuminate\Support\Facades\Log;
use Throwable;
@ -66,7 +69,7 @@ public function install(): void
'server' => $this->server,
])
);
/** @todo notify */
Notifier::send($this->server, new ServerInstallationSucceed($this->server));
};
Bus::chain($jobs)
@ -79,7 +82,7 @@ public function install(): void
'server' => $this->server,
])
);
/** @todo notify */
Notifier::send($this->server, new ServerInstallationFailed($this->server));
Log::error('server-installation-error', [
'error' => (string) $e,
]);

View File

@ -4,12 +4,15 @@
use App\Enums\ServerStatus;
use App\Events\Broadcast;
use App\Facades\Notifier;
use App\Jobs\Installation\Initialize;
use App\Jobs\Installation\InstallCertbot;
use App\Jobs\Installation\InstallComposer;
use App\Jobs\Installation\InstallNodejs;
use App\Jobs\Installation\InstallRequirements;
use App\Jobs\Installation\Upgrade;
use App\Notifications\ServerInstallationFailed;
use App\Notifications\ServerInstallationSucceed;
use Illuminate\Support\Facades\Bus;
use Illuminate\Support\Facades\Log;
use Throwable;
@ -88,7 +91,7 @@ public function install(): void
'server' => $this->server,
])
);
/** @todo notify */
Notifier::send($this->server, new ServerInstallationSucceed($this->server));
};
Bus::chain($jobs)
@ -101,7 +104,7 @@ public function install(): void
'server' => $this->server,
])
);
/** @todo notify */
Notifier::send($this->server, new ServerInstallationFailed($this->server));
Log::error('server-installation-error', [
'error' => (string) $e,
]);

View File

@ -22,11 +22,6 @@ public function delete(): void
dispatch(new DeleteSite($this->site))->onConnection('ssh');
}
public function install(): void
{
// TODO: Implement install() method.
}
protected function progress(int $percentage): Closure
{
return function () use ($percentage) {

View File

@ -1,5 +1,6 @@
<?php
use Illuminate\Contracts\Database\Query\Expression;
use Illuminate\Support\Str;
function random_color(): string
@ -63,3 +64,10 @@ function date_with_timezone($date, $timezone): string
return $dt->format('Y-m-d H:i:s');
}
function cast_to_json(array $json): Illuminate\Database\Query\Expression|Expression
{
$json = addslashes(json_encode($json));
return DB::raw("CAST('{$json}' AS JSON)");
}

View File

@ -6,18 +6,22 @@
"license": "MIT",
"require": {
"php": "^8.1",
"ext-ftp": "*",
"aws/aws-sdk-php": "^3.158",
"bensampo/laravel-enum": "^6.3",
"blade-ui-kit/blade-heroicons": "^2.2",
"davidhsianturi/blade-bootstrap-icons": "^1.4",
"guzzlehttp/guzzle": "^7.2",
"khatabwedaa/blade-css-icons": "^1.3",
"laravel/fortify": "^1.17",
"laravel/framework": "^10.0",
"laravel/sanctum": "^3.2",
"laravel/socialite": "^5.2",
"laravel/tinker": "^2.8",
"livewire/livewire": "^2.12",
"phpseclib/phpseclib": "~3.0",
"opcodesio/log-viewer": "^2.5",
"ext-ftp": "*"
"owenvoke/blade-fontawesome": "^2.5",
"phpseclib/phpseclib": "~3.0"
},
"require-dev": {
"fakerphp/faker": "^1.9.1",

475
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": "bffd27708153bfce7c9dc34b975700de",
"content-hash": "b3f98cafe7fcc5d3ce67ad09a8f66661",
"packages": [
{
"name": "aws/aws-crt-php",
@ -300,6 +300,156 @@
],
"time": "2023-11-15T15:39:24+00:00"
},
{
"name": "blade-ui-kit/blade-heroicons",
"version": "2.2.1",
"source": {
"type": "git",
"url": "https://github.com/blade-ui-kit/blade-heroicons.git",
"reference": "bcf4be8f6bbde0bb4c23f2e3fb189b88dec1580a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/blade-ui-kit/blade-heroicons/zipball/bcf4be8f6bbde0bb4c23f2e3fb189b88dec1580a",
"reference": "bcf4be8f6bbde0bb4c23f2e3fb189b88dec1580a",
"shasum": ""
},
"require": {
"blade-ui-kit/blade-icons": "^1.1",
"illuminate/support": "^9.0|^10.0",
"php": "^8.0"
},
"require-dev": {
"orchestra/testbench": "^7.0|^8.0",
"phpunit/phpunit": "^9.0"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"BladeUI\\Heroicons\\BladeHeroiconsServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"BladeUI\\Heroicons\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Dries Vints",
"homepage": "https://driesvints.com"
}
],
"description": "A package to easily make use of Heroicons in your Laravel Blade views.",
"homepage": "https://github.com/blade-ui-kit/blade-heroicons",
"keywords": [
"Heroicons",
"blade",
"laravel"
],
"support": {
"issues": "https://github.com/blade-ui-kit/blade-heroicons/issues",
"source": "https://github.com/blade-ui-kit/blade-heroicons/tree/2.2.1"
},
"funding": [
{
"url": "https://github.com/sponsors/driesvints",
"type": "github"
},
{
"url": "https://www.paypal.com/paypalme/driesvints",
"type": "paypal"
}
],
"time": "2023-12-18T20:44:03+00:00"
},
{
"name": "blade-ui-kit/blade-icons",
"version": "1.5.3",
"source": {
"type": "git",
"url": "https://github.com/blade-ui-kit/blade-icons.git",
"reference": "b5e6603218e2347ac81cb780bc6f71c8c3b31f5b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/blade-ui-kit/blade-icons/zipball/b5e6603218e2347ac81cb780bc6f71c8c3b31f5b",
"reference": "b5e6603218e2347ac81cb780bc6f71c8c3b31f5b",
"shasum": ""
},
"require": {
"illuminate/contracts": "^8.0|^9.0|^10.0",
"illuminate/filesystem": "^8.0|^9.0|^10.0",
"illuminate/support": "^8.0|^9.0|^10.0",
"illuminate/view": "^8.0|^9.0|^10.0",
"php": "^7.4|^8.0",
"symfony/console": "^5.3|^6.0",
"symfony/finder": "^5.3|^6.0"
},
"require-dev": {
"mockery/mockery": "^1.3",
"orchestra/testbench": "^6.0|^7.0|^8.0",
"phpunit/phpunit": "^9.0"
},
"bin": [
"bin/blade-icons-generate"
],
"type": "library",
"extra": {
"laravel": {
"providers": [
"BladeUI\\Icons\\BladeIconsServiceProvider"
]
}
},
"autoload": {
"files": [
"src/helpers.php"
],
"psr-4": {
"BladeUI\\Icons\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Dries Vints",
"homepage": "https://driesvints.com"
}
],
"description": "A package to easily make use of icons in your Laravel Blade views.",
"homepage": "https://github.com/blade-ui-kit/blade-icons",
"keywords": [
"blade",
"icons",
"laravel",
"svg"
],
"support": {
"issues": "https://github.com/blade-ui-kit/blade-icons/issues",
"source": "https://github.com/blade-ui-kit/blade-icons"
},
"funding": [
{
"url": "https://github.com/sponsors/driesvints",
"type": "github"
},
{
"url": "https://www.paypal.com/paypalme/driesvints",
"type": "paypal"
}
],
"time": "2023-10-18T10:50:13+00:00"
},
{
"name": "brick/math",
"version": "0.11.0",
@ -618,6 +768,67 @@
},
"time": "2023-08-25T16:18:39+00:00"
},
{
"name": "davidhsianturi/blade-bootstrap-icons",
"version": "v1.4.0",
"source": {
"type": "git",
"url": "https://github.com/davidhsianturi/blade-bootstrap-icons.git",
"reference": "255040a0058683dd5a0fd36dfa0857a91a95137f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/davidhsianturi/blade-bootstrap-icons/zipball/255040a0058683dd5a0fd36dfa0857a91a95137f",
"reference": "255040a0058683dd5a0fd36dfa0857a91a95137f",
"shasum": ""
},
"require": {
"blade-ui-kit/blade-icons": "^1.0",
"illuminate/support": "^8.0|^9.0|^10.0",
"php": "^7.4|^8.0"
},
"require-dev": {
"orchestra/testbench": "^6.0|^7.0|^8.0",
"phpunit/phpunit": "^9.0"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Davidhsianturi\\BladeBootstrapIcons\\BladeBootstrapIconsServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Davidhsianturi\\BladeBootstrapIcons\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "David H. Sianturi",
"email": "davidhsianturi@gmail.com",
"homepage": "https://davidhsianturi.com",
"role": "Developer"
}
],
"description": "A package to easily make use of Bootstrap Icons in your Laravel Blade views.",
"homepage": "https://github.com/davidhsianturi/blade-bootstrap-icons",
"keywords": [
"Bootstrap Icons",
"blade",
"laravel"
],
"support": {
"issues": "https://github.com/davidhsianturi/blade-bootstrap-icons/issues",
"source": "https://github.com/davidhsianturi/blade-bootstrap-icons/tree/v1.4.0"
},
"time": "2023-03-17T14:49:47+00:00"
},
{
"name": "dflydev/dot-access-data",
"version": "v3.0.2",
@ -1533,6 +1744,67 @@
],
"time": "2023-12-03T19:50:20+00:00"
},
{
"name": "khatabwedaa/blade-css-icons",
"version": "1.3.0",
"source": {
"type": "git",
"url": "https://github.com/khatabwedaa/blade-css-icons.git",
"reference": "a022e9a0057d9ce4f99728647fb139808c6134d8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/khatabwedaa/blade-css-icons/zipball/a022e9a0057d9ce4f99728647fb139808c6134d8",
"reference": "a022e9a0057d9ce4f99728647fb139808c6134d8",
"shasum": ""
},
"require": {
"blade-ui-kit/blade-icons": "^1.1",
"illuminate/support": "^9.0|^10",
"php": "^8.0"
},
"require-dev": {
"orchestra/testbench": "^7.0|^8.0",
"phpunit/phpunit": "^9.0"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Khatabwedaa\\BladeCssIcons\\BladeCssIconsServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Khatabwedaa\\BladeCssIcons\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Khatab Wedaa",
"email": "khatabwedaa@gmail.com",
"homepage": "https://twitter.com/khatabwedaa",
"role": "Developer"
}
],
"description": "A package to easily make use of Css.gg in your Laravel Blade views.",
"homepage": "https://github.com/khatabwedaa/blade-css-icons",
"keywords": [
"Css.gg",
"blade",
"laravel"
],
"support": {
"issues": "https://github.com/khatabwedaa/blade-css-icons/issues",
"source": "https://github.com/khatabwedaa/blade-css-icons/tree/1.3.0"
},
"time": "2023-02-04T14:04:11+00:00"
},
{
"name": "laminas/laminas-code",
"version": "4.13.0",
@ -3382,6 +3654,68 @@
],
"time": "2023-09-03T08:22:57+00:00"
},
{
"name": "owenvoke/blade-fontawesome",
"version": "v2.5.1",
"source": {
"type": "git",
"url": "https://github.com/owenvoke/blade-fontawesome.git",
"reference": "b3eac80b0f2f1b70083d4acea0da49350f88856e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/owenvoke/blade-fontawesome/zipball/b3eac80b0f2f1b70083d4acea0da49350f88856e",
"reference": "b3eac80b0f2f1b70083d4acea0da49350f88856e",
"shasum": ""
},
"require": {
"blade-ui-kit/blade-icons": "^1.5",
"illuminate/support": "^10.34",
"php": "^8.1",
"thecodingmachine/safe": "^2.5"
},
"require-dev": {
"laravel/pint": "^1.13",
"orchestra/testbench": "^8.12",
"pestphp/pest": "^2.26",
"phpstan/phpstan": "^1.10",
"symfony/var-dumper": "^6.3",
"thecodingmachine/phpstan-safe-rule": "^1.2"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"OwenVoke\\BladeFontAwesome\\BladeFontAwesomeServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"OwenVoke\\BladeFontAwesome\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "A package to easily make use of Font Awesome in your Laravel Blade views",
"support": {
"issues": "https://github.com/owenvoke/blade-fontawesome/issues",
"source": "https://github.com/owenvoke/blade-fontawesome/tree/v2.5.1"
},
"funding": [
{
"url": "https://ecologi.com/owenvoke?gift-trees",
"type": "custom"
},
{
"url": "https://github.com/owenvoke",
"type": "github"
}
],
"time": "2023-12-12T09:07:03+00:00"
},
{
"name": "paragonie/constant_time_encoding",
"version": "v2.6.3",
@ -6710,6 +7044,145 @@
],
"time": "2023-12-28T19:16:56+00:00"
},
{
"name": "thecodingmachine/safe",
"version": "v2.5.0",
"source": {
"type": "git",
"url": "https://github.com/thecodingmachine/safe.git",
"reference": "3115ecd6b4391662b4931daac4eba6b07a2ac1f0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thecodingmachine/safe/zipball/3115ecd6b4391662b4931daac4eba6b07a2ac1f0",
"reference": "3115ecd6b4391662b4931daac4eba6b07a2ac1f0",
"shasum": ""
},
"require": {
"php": "^8.0"
},
"require-dev": {
"phpstan/phpstan": "^1.5",
"phpunit/phpunit": "^9.5",
"squizlabs/php_codesniffer": "^3.2",
"thecodingmachine/phpstan-strict-rules": "^1.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.2.x-dev"
}
},
"autoload": {
"files": [
"deprecated/apc.php",
"deprecated/array.php",
"deprecated/datetime.php",
"deprecated/libevent.php",
"deprecated/misc.php",
"deprecated/password.php",
"deprecated/mssql.php",
"deprecated/stats.php",
"deprecated/strings.php",
"lib/special_cases.php",
"deprecated/mysqli.php",
"generated/apache.php",
"generated/apcu.php",
"generated/array.php",
"generated/bzip2.php",
"generated/calendar.php",
"generated/classobj.php",
"generated/com.php",
"generated/cubrid.php",
"generated/curl.php",
"generated/datetime.php",
"generated/dir.php",
"generated/eio.php",
"generated/errorfunc.php",
"generated/exec.php",
"generated/fileinfo.php",
"generated/filesystem.php",
"generated/filter.php",
"generated/fpm.php",
"generated/ftp.php",
"generated/funchand.php",
"generated/gettext.php",
"generated/gmp.php",
"generated/gnupg.php",
"generated/hash.php",
"generated/ibase.php",
"generated/ibmDb2.php",
"generated/iconv.php",
"generated/image.php",
"generated/imap.php",
"generated/info.php",
"generated/inotify.php",
"generated/json.php",
"generated/ldap.php",
"generated/libxml.php",
"generated/lzf.php",
"generated/mailparse.php",
"generated/mbstring.php",
"generated/misc.php",
"generated/mysql.php",
"generated/network.php",
"generated/oci8.php",
"generated/opcache.php",
"generated/openssl.php",
"generated/outcontrol.php",
"generated/pcntl.php",
"generated/pcre.php",
"generated/pgsql.php",
"generated/posix.php",
"generated/ps.php",
"generated/pspell.php",
"generated/readline.php",
"generated/rpminfo.php",
"generated/rrd.php",
"generated/sem.php",
"generated/session.php",
"generated/shmop.php",
"generated/sockets.php",
"generated/sodium.php",
"generated/solr.php",
"generated/spl.php",
"generated/sqlsrv.php",
"generated/ssdeep.php",
"generated/ssh2.php",
"generated/stream.php",
"generated/strings.php",
"generated/swoole.php",
"generated/uodbc.php",
"generated/uopz.php",
"generated/url.php",
"generated/var.php",
"generated/xdiff.php",
"generated/xml.php",
"generated/xmlrpc.php",
"generated/yaml.php",
"generated/yaz.php",
"generated/zip.php",
"generated/zlib.php"
],
"classmap": [
"lib/DateTime.php",
"lib/DateTimeImmutable.php",
"lib/Exceptions/",
"deprecated/Exceptions/",
"generated/Exceptions/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "PHP core functions that throw exceptions instead of returning FALSE on error",
"support": {
"issues": "https://github.com/thecodingmachine/safe/issues",
"source": "https://github.com/thecodingmachine/safe/tree/v2.5.0"
},
"time": "2023-04-05T11:54:14+00:00"
},
{
"name": "tijsverkoyen/css-to-inline-styles",
"version": "v2.2.7",

View File

@ -13,6 +13,7 @@
use App\NotificationChannels\Discord;
use App\NotificationChannels\Email;
use App\NotificationChannels\Slack;
use App\NotificationChannels\Telegram;
use App\ServerProviders\AWS;
use App\ServerProviders\DigitalOcean;
use App\ServerProviders\Hetzner;
@ -343,11 +344,13 @@
\App\Enums\NotificationChannel::SLACK,
\App\Enums\NotificationChannel::DISCORD,
\App\Enums\NotificationChannel::EMAIL,
\App\Enums\NotificationChannel::TELEGRAM,
],
'notification_channels_providers_class' => [
\App\Enums\NotificationChannel::SLACK => Slack::class,
\App\Enums\NotificationChannel::DISCORD => Discord::class,
\App\Enums\NotificationChannel::EMAIL => Email::class,
\App\Enums\NotificationChannel::TELEGRAM => Telegram::class,
],
/*

View File

@ -233,6 +233,11 @@ chmod +x /home/${V_USERNAME}/${V_DOMAIN}/update.sh
# cleanup
chown -R ${V_USERNAME}:${V_USERNAME} /home/${V_USERNAME}
# cache
php artisan config:cache
php artisan icons:cache
# print info
echo "🎉 Congratulations!"
echo "✅ SSH User: ${V_USERNAME}"
echo "✅ SSH Password: ${V_PASSWORD}"

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
{
"resources/css/app.css": {
"file": "assets/app-f482c864.css",
"file": "assets/app-8b808e33.css",
"isEntry": true,
"src": "resources/css/app.css"
},

View File

@ -1 +1 @@
<a {{ $attributes->merge(['class' => 'block w-full px-4 py-2 text-left text-sm leading-5 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 focus:outline-none focus:bg-gray-100 dark:focus:bg-gray-800 transition duration-150 ease-in-out flex items-center justify-start']) }}>{{ $slot }}</a>
<a {{ $attributes->merge(['class' => 'block w-full px-4 py-2 text-left text-sm leading-5 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none focus:bg-gray-100 dark:focus:bg-gray-700 transition duration-150 ease-in-out flex items-center justify-start']) }}>{{ $slot }}</a>

View File

@ -1,4 +1,4 @@
@props(['align' => 'right', 'width' => '48', 'contentClasses' => 'py-1 bg-white dark:bg-gray-700'])
@props(['align' => 'right', 'width' => '48', 'contentClasses' => 'py-1 bg-white dark:bg-gray-800'])
@php
switch ($align) {

View File

@ -1,5 +1,5 @@
@props(['server'])
<!DOCTYPE html>
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
@ -32,7 +32,7 @@
</head>
<body class="font-sans antialiased bg-gray-100 dark:bg-gray-900 dark:text-gray-300 min-h-screen min-w-max"
x-data="" x-cloak>
x-data="" x-cloak>
<div class="flex min-h-screen">
<div
class="left-0 top-0 min-h-screen w-64 flex-none bg-gray-800 dark:bg-gray-800/50 p-3 dark:border-r-2 dark:border-gray-800">
@ -58,9 +58,9 @@ class="left-0 top-0 min-h-screen w-64 flex-none bg-gray-800 dark:bg-gray-800/50
<div class="mt-3 space-y-1">
<x-sidebar-link :href="route('servers.show', ['server' => $server])" :active="request()->routeIs('servers.show')">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="w-6 h-6">
stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round"
d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" />
d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" />
</svg>
<span class="ml-2 text-gray-50">{{ __('Overview') }}</span>
</x-sidebar-link>
@ -68,9 +68,9 @@ class="left-0 top-0 min-h-screen w-64 flex-none bg-gray-800 dark:bg-gray-800/50
@if ($server->webserver())
<x-sidebar-link :href="route('servers.sites', ['server' => $server])" :active="request()->routeIs('servers.sites') || request()->is('servers/*/sites/*')">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round"
d="M12 21a9.004 9.004 0 008.716-6.747M12 21a9.004 9.004 0 01-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 017.843 4.582M12 3a8.997 8.997 0 00-7.843 4.582m15.686 0A11.953 11.953 0 0112 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0121 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0112 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 013 12c0-1.605.42-3.113 1.157-4.418" />
d="M12 21a9.004 9.004 0 008.716-6.747M12 21a9.004 9.004 0 01-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 017.843 4.582M12 3a8.997 8.997 0 00-7.843 4.582m15.686 0A11.953 11.953 0 0112 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0121 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0112 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 013 12c0-1.605.42-3.113 1.157-4.418" />
</svg>
<span class="ml-2 text-gray-50">{{ __('Sites') }}</span>
</x-sidebar-link>
@ -79,9 +79,9 @@ class="left-0 top-0 min-h-screen w-64 flex-none bg-gray-800 dark:bg-gray-800/50
<x-sidebar-link :href="route('servers.databases', ['server' => $server])" :active="request()->routeIs('servers.databases') ||
request()->routeIs('servers.databases.backups')">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round"
d="M20.25 6.375c0 2.278-3.694 4.125-8.25 4.125S3.75 8.653 3.75 6.375m16.5 0c0-2.278-3.694-4.125-8.25-4.125S3.75 4.097 3.75 6.375m16.5 0v11.25c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125V6.375m16.5 0v3.75m-16.5-3.75v3.75m16.5 0v3.75C20.25 16.153 16.556 18 12 18s-8.25-1.847-8.25-4.125v-3.75m16.5 0c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125" />
d="M20.25 6.375c0 2.278-3.694 4.125-8.25 4.125S3.75 8.653 3.75 6.375m16.5 0c0-2.278-3.694-4.125-8.25-4.125S3.75 4.097 3.75 6.375m16.5 0v11.25c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125V6.375m16.5 0v3.75m-16.5-3.75v3.75m16.5 0v3.75C20.25 16.153 16.556 18 12 18s-8.25-1.847-8.25-4.125v-3.75m16.5 0c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125" />
</svg>
<span class="ml-2 text-gray-50">{{ __('Databases') }}</span>
</x-sidebar-link>
@ -89,9 +89,9 @@ class="left-0 top-0 min-h-screen w-64 flex-none bg-gray-800 dark:bg-gray-800/50
@if ($server->php())
<x-sidebar-link :href="route('servers.php', ['server' => $server])" :active="request()->routeIs('servers.php')">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round"
d="M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5" />
d="M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5" />
</svg>
<span class="ml-2 text-gray-50">{{ __('PHP') }}</span>
</x-sidebar-link>
@ -99,55 +99,53 @@ class="left-0 top-0 min-h-screen w-64 flex-none bg-gray-800 dark:bg-gray-800/50
@if ($server->firewall())
<x-sidebar-link :href="route('servers.firewall', ['server' => $server])" :active="request()->routeIs('servers.firewall')">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round"
d="M15.362 5.214A8.252 8.252 0 0112 21 8.25 8.25 0 016.038 7.048 8.287 8.287 0 009 9.6a8.983 8.983 0 013.361-6.867 8.21 8.21 0 003 2.48z" />
d="M15.362 5.214A8.252 8.252 0 0112 21 8.25 8.25 0 016.038 7.048 8.287 8.287 0 009 9.6a8.983 8.983 0 013.361-6.867 8.21 8.21 0 003 2.48z" />
<path stroke-linecap="round" stroke-linejoin="round"
d="M12 18a3.75 3.75 0 00.495-7.467 5.99 5.99 0 00-1.925 3.546 5.974 5.974 0 01-2.133-1A3.75 3.75 0 0012 18z" />
d="M12 18a3.75 3.75 0 00.495-7.467 5.99 5.99 0 00-1.925 3.546 5.974 5.974 0 01-2.133-1A3.75 3.75 0 0012 18z" />
</svg>
<span class="ml-2 text-gray-50">{{ __('Firewall') }}</span>
</x-sidebar-link>
@endif
<x-sidebar-link :href="route('servers.cronjobs', ['server' => $server])" :active="request()->routeIs('servers.cronjobs')">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round"
d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" /> </svg>
<span class="ml-2 text-gray-50">{{ __('Cronjobs') }}</span>
</x-sidebar-link>
<x-sidebar-link :href="route('servers.ssh-keys', ['server' => $server])" :active="request()->routeIs('servers.ssh-keys')">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round"
d="M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z" />
d="M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z" />
</svg>
<span class="ml-2 text-gray-50">{{ __('SSH Keys') }}</span>
</x-sidebar-link>
<x-sidebar-link :href="route('servers.services', ['server' => $server])" :active="request()->routeIs('servers.services')">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round"
d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z" />
d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z" />
<path stroke-linecap="round" stroke-linejoin="round"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /> </svg>
<span class="ml-2 text-gray-50">{{ __('Services') }}</span>
</x-sidebar-link>
@endif
<x-sidebar-link :href="route('servers.settings', ['server' => $server])" :active="request()->routeIs('servers.settings')">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round"
d="M11.42 15.17L17.25 21A2.652 2.652 0 0021 17.25l-5.877-5.877M11.42 15.17l2.496-3.03c.317-.384.74-.626 1.208-.766M11.42 15.17l-4.655 5.653a2.548 2.548 0 11-3.586-3.586l6.837-5.63m5.108-.233c.55-.164 1.163-.188 1.743-.14a4.5 4.5 0 004.486-6.336l-3.276 3.277a3.004 3.004 0 01-2.25-2.25l3.276-3.276a4.5 4.5 0 00-6.336 4.486c.091 1.076-.071 2.264-.904 2.95l-.102.085m-1.745 1.437L5.909 7.5H4.5L2.25 3.75l1.5-1.5L7.5 4.5v1.409l4.26 4.26m-1.745 1.437l1.745-1.437m6.615 8.206L15.75 15.75M4.867 19.125h.008v.008h-.008v-.008z" />
d="M11.42 15.17L17.25 21A2.652 2.652 0 0021 17.25l-5.877-5.877M11.42 15.17l2.496-3.03c.317-.384.74-.626 1.208-.766M11.42 15.17l-4.655 5.653a2.548 2.548 0 11-3.586-3.586l6.837-5.63m5.108-.233c.55-.164 1.163-.188 1.743-.14a4.5 4.5 0 004.486-6.336l-3.276 3.277a3.004 3.004 0 01-2.25-2.25l3.276-3.276a4.5 4.5 0 00-6.336 4.486c.091 1.076-.071 2.264-.904 2.95l-.102.085m-1.745 1.437L5.909 7.5H4.5L2.25 3.75l1.5-1.5L7.5 4.5v1.409l4.26 4.26m-1.745 1.437l1.745-1.437m6.615 8.206L15.75 15.75M4.867 19.125h.008v.008h-.008v-.008z" />
</svg>
<span class="ml-2 text-gray-50">{{ __('Settings') }}</span>
</x-sidebar-link>
<x-sidebar-link :href="route('servers.logs', ['server' => $server])" :active="request()->routeIs('servers.logs')">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round"
d="M6.429 9.75L2.25 12l4.179 2.25m0-4.5l5.571 3 5.571-3m-11.142 0L2.25 7.5 12 2.25l9.75 5.25-4.179 2.25m0 0L21.75 12l-4.179 2.25m0 0l4.179 2.25L12 21.75 2.25 16.5l4.179-2.25m11.142 0l-5.571 3-5.571-3" />
d="M6.429 9.75L2.25 12l4.179 2.25m0-4.5l5.571 3 5.571-3m-11.142 0L2.25 7.5 12 2.25l9.75 5.25-4.179 2.25m0 0L21.75 12l-4.179 2.25m0 0l4.179 2.25L12 21.75 2.25 16.5l4.179-2.25m11.142 0l-5.571 3-5.571-3" />
</svg>
<span class="ml-2 text-gray-50">{{ __('Logs') }}</span>
</x-sidebar-link>
@ -164,48 +162,7 @@ class="min-h-screen w-64 flex-none border-r border-gray-200 bg-white dark:border
@endif
<div class="flex min-h-screen flex-grow flex-col">
<nav class="h-16 border-b border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900">
<!-- Primary Navigation Menu -->
<div class="mx-auto max-w-full px-4 sm:px-6 lg:px-8">
<div class="flex h-16 justify-end">
<div class="flex items-center justify-center">
{{-- Search --}}
</div>
@include('layouts.partials.color-scheme')
<div class="ml-6 flex items-center">
<div class="relative ml-5">
<x-dropdown align="right" width="48">
<x-slot name="trigger">
<div class="flex cursor-pointer items-center justify-between">
{{ auth()->user()->name }}
<svg class="ml-2 -mr-0.5 h-4 w-4" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clip-rule="evenodd" />
</svg>
</div>
</x-slot>
<x-slot name="content">
<x-dropdown-link :href="route('profile')">Profile</x-dropdown-link>
<div class="border-t border-gray-100 dark:border-gray-700"></div>
<div class="border-t border-gray-100 dark:border-gray-700"></div>
<form method="POST" action="{{ route('logout') }}">
@csrf
<x-dropdown-link :href="route('logout')"
onclick="event.preventDefault(); this.closest('form').submit();">
{{ __('Log Out') }}
</x-dropdown-link>
</form>
</x-slot>
</x-dropdown>
</div>
</div>
</div>
</div>
</nav>
@include('layouts.navigation')
<!-- Page Heading -->
@if (isset($header))
@ -244,7 +201,7 @@ class="min-h-screen w-64 flex-none border-r border-gray-200 bg-white dark:border
// On page load or when changing themes, best to add inline in `head` to avoid FOUC
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia(
'(prefers-color-scheme: dark)').matches)) {
'(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark')
} else {
document.documentElement.classList.remove('dark')

View File

@ -1,61 +1,16 @@
<nav x-data="{ open: false }" class="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
<!-- Primary Navigation Menu -->
<div class="max-w-7xl mx-auto px-6">
<div class="flex justify-between h-16">
<nav x-data="{ open: false }" class="h-16 border-b border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900">
<div class="mx-auto max-w-full px-4 sm:px-6 lg:px-8">
<div class="flex items-center justify-between h-16">
<div class="flex">
<!-- Logo -->
<div class="shrink-0 flex items-center">
<x-application-logo class="block h-9 w-auto fill-current text-gray-800 dark:text-gray-200 rounded-lg" />
</div>
<div class="flex items-center">
<div class="mr-3">
@include('layouts.partials.color-scheme')
</div>
<!-- Navigation Links -->
<div class="hidden space-x-8 sm:-my-px sm:ml-10 sm:flex">
<x-nav-link :href="route('servers')" :active="request()->routeIs('servers') || request()->is('servers/*')">
{{ __('Servers') }}
</x-nav-link>
</div>
</div>
<!-- Settings Dropdown -->
<livewire:user-dropdown />
<!-- Hamburger -->
<div class="-mr-2 flex items-center sm:hidden">
<button @click="open = ! open" class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 dark:text-gray-500 hover:text-gray-500 dark:hover:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-900 focus:outline-none focus:bg-gray-100 dark:focus:bg-gray-900 focus:text-gray-500 dark:focus:text-gray-400 transition duration-150 ease-in-out">
<svg class="h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24">
<path :class="{'hidden': open, 'inline-flex': ! open }" class="inline-flex" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
<path :class="{'hidden': ! open, 'inline-flex': open }" class="hidden" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
</div>
<!-- Responsive Navigation Menu -->
<div :class="{'block': open, 'hidden': ! open}" class="hidden sm:hidden">
<!-- Responsive Settings Options -->
<div class="pt-4 pb-1 border-t border-gray-200 dark:border-gray-600">
<div class="px-4">
<div class="font-medium text-base text-gray-800 dark:text-gray-200">{{ Auth::user()->name }}</div>
<div class="font-medium text-sm text-gray-500">{{ Auth::user()->email }}</div>
</div>
<div class="mt-3 space-y-1">
<x-responsive-nav-link :href="route('profile')">
{{ __('Profile') }}
</x-responsive-nav-link>
<x-responsive-nav-link :href="route('profile')">
{{ __('Settings') }}
</x-responsive-nav-link>
<!-- Authentication -->
<form method="POST" action="{{ route('logout') }}">
@csrf
<x-responsive-nav-link :href="route('logout')" onclick="event.preventDefault(); this.closest('form').submit();">
{{ __('Log Out') }}
</x-responsive-nav-link>
</form>
<livewire:user-dropdown />
</div>
</div>
</div>

View File

@ -1,37 +1,46 @@
<div class="flex items-center" x-data="{
isDarkMode: localStorage.theme,
toggleTheme() {
localStorage.theme = this.isDarkMode === 'dark' ? 'light' : 'dark';
if (localStorage.theme === 'dark') {
<div class="flex items-center text-gray-600 dark:text-gray-300" x-data="{
theme: localStorage.theme,
isDark() {
if (this.theme === 'dark') {
return true
}
console.log(window.matchMedia('(prefers-color-scheme: dark)'))
return this.theme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches;
},
changeTheme(theme) {
this.theme = theme;
localStorage.theme = theme;
this.updateDocument();
},
updateDocument() {
if (this.isDark()) {
document.documentElement.classList.add('dark')
} else {
document.documentElement.classList.remove('dark')
}
this.isDarkMode = localStorage.theme
}
}" x-on:click="toggleTheme()">
}" x-init="updateDocument()">
<div class="flex items-center">
<div class="flex items-center justify-end">
<button id="theme-toggle" type="button" class="text-sm p-2"
:class="isDarkMode === 'dark' ? 'text-gray-300 border-gray-300' :
'text-gray-800 border-gray-800'">
<svg x-show="isDarkMode !== 'dark'" id="theme-toggle-dark-icon" class="w-5 h-5"
fill="currentColor" viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg">
<path
d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z">
</path>
</svg>
<svg x-show="isDarkMode === 'dark'" id="theme-toggle-light-icon" class="w-5 h-5"
fill="currentColor" viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg">
<path
d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"
fillRule="evenodd" clipRule="evenodd"></path>
</svg>
</button>
<x-dropdown>
<x-slot name="trigger">
<button type="button" class="flex items-center">
<x-heroicon-o-moon x-show="isDark()" class="h-7 w-7" />
<x-heroicon-o-sun x-show="!isDark()" class="h-7 w-7" />
</button>
</x-slot>
<x-slot name="content">
<x-dropdown-link class="cursor-pointer" x-on:click="changeTheme('dark')">
<x-heroicon-o-moon class="h-5 w-5 mr-2" x-bind:class="theme === 'dark' ? 'text-primary-600' : ''" /> {{ __("Dark") }}
</x-dropdown-link>
<x-dropdown-link class="cursor-pointer" x-on:click="changeTheme('light')">
<x-heroicon-o-sun class="h-5 w-5 mr-2" x-bind:class="theme === 'light' ? 'text-primary-600' : ''" /> {{ __("Light") }}
</x-dropdown-link>
<x-dropdown-link class="cursor-pointer" x-on:click="changeTheme('system')">
<x-heroicon-o-computer-desktop class="h-5 w-5 mr-2" x-bind:class="theme === 'system' ? 'text-primary-600' : ''" /> {{ __("System") }}
</x-dropdown-link>
</x-slot>
</x-dropdown>
</div>
</div>
</div>

View File

@ -52,6 +52,24 @@
</div>
@endif
@if($provider == \App\Enums\NotificationChannel::TELEGRAM)
<div class="mt-6">
<x-input-label for="bot_token" :value="__('Bot Token')" />
<x-text-input wire:model.defer="bot_token" id="bot_token" name="bot_token" type="text" class="mt-1 w-full" />
@error('bot_token')
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
<div class="mt-6">
<x-input-label for="chat_id" :value="__('Chat ID')" />
<x-text-input wire:model.defer="chat_id" id="chat_id" name="chat_id" type="text" class="mt-1 w-full" />
@error('chat_id')
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
@endif
<div class="mt-6 flex justify-end">
<x-secondary-button type="button" x-on:click="$dispatch('close')">
{{ __('Cancel') }}

View File

@ -10,8 +10,8 @@
@if(count($channels) > 0)
@foreach($channels as $channel)
<x-item-card>
<div class="flex-none">
<img src="{{ asset('static/images/' . $channel->provider . '.svg') }}" class="h-10 w-10" alt="">
<div class="flex-none text-gray-600 dark:text-gray-300">
@include('livewire.notification-channels.icons.' . $channel->provider)
</div>
<div class="ml-3 flex flex-grow flex-col items-start justify-center">
<span class="mb-1">{{ $channel->label }}</span>

View File

@ -0,0 +1 @@
<x-bi-discord class="h-10 w-10" />

View File

@ -0,0 +1 @@
<x-bi-envelope class="h-10 w-10" />

View File

@ -0,0 +1 @@
<x-bi-slack class="h-10 w-10" />

View File

@ -0,0 +1 @@
<x-bi-telegram class="h-10 w-10" />

View File

@ -0,0 +1 @@
<x-fab-bitbucket class="w-10 h-10" />

View File

@ -0,0 +1 @@
<x-fab-github class="w-10 h-10" />

View File

@ -0,0 +1 @@
<x-fab-gitlab class="w-10 h-10" />

View File

@ -1,12 +0,0 @@
<svg class="w-10 h-10" viewBox="0 0 256 231" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
<defs>
<linearGradient x1="108.63338%" y1="13.818022%" x2="46.9265964%" y2="78.7761408%" id="linearGradient-1">
<stop stop-color="#0052CC" offset="18%"></stop>
<stop stop-color="#2684FF" offset="100%"></stop>
</linearGradient>
</defs>
<g fill="none">
<polygon points="101.272088 152.561281 154.720712 152.561281 167.622105 77.2417255 87.0600784 77.2417255"></polygon>
<path d="M8.30813067,0.000683498206 C5.88502974,-0.0305685468 3.57212663,1.01125669 1.98985644,2.84669011 C0.407586248,4.68212353 -0.282086001,7.12328571 0.105843921,9.51533612 L34.9245512,220.888266 C35.8200362,226.227525 40.4199456,230.153012 45.8335925,230.197861 L212.873162,230.197861 C216.936516,230.250159 220.425595,227.319332 221.075449,223.30794 L255.894156,9.55634756 C256.282086,7.16429714 255.592414,4.72313497 254.010144,2.88770154 C252.427873,1.05226812 250.11497,0.0104428869 247.691869,0.0416949319 L8.30813067,0.000683498206 Z M154.924006,152.768274 L101.609142,152.768274 L87.1731177,77.3482475 L167.842608,77.3482475 L154.924006,152.768274 Z" fill="#2684FF"></path>
<path d="M244.610824,77.2417255 L167.693776,77.2417255 L154.78548,152.601582 L101.513151,152.601582 L38.6108235,227.264801 C40.6045494,228.988786 43.1464609,229.94745 45.7820986,229.969396 L212.729383,229.969396 C216.789495,230.021652 220.275791,227.093164 220.925126,223.084972 L244.610824,77.2417255 Z" fill="url(#linearGradient-1)"></path>
</g> </svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -1,3 +0,0 @@
<svg class="fill-current w-10 h-10" viewBox="0 0 256 250" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
<g><path d="M128.00106,0 C57.3172926,0 0,57.3066942 0,128.00106 C0,184.555281 36.6761997,232.535542 87.534937,249.460899 C93.9320223,250.645779 96.280588,246.684165 96.280588,243.303333 C96.280588,240.251045 96.1618878,230.167899 96.106777,219.472176 C60.4967585,227.215235 52.9826207,204.369712 52.9826207,204.369712 C47.1599584,189.574598 38.770408,185.640538 38.770408,185.640538 C27.1568785,177.696113 39.6458206,177.859325 39.6458206,177.859325 C52.4993419,178.762293 59.267365,191.04987 59.267365,191.04987 C70.6837675,210.618423 89.2115753,204.961093 96.5158685,201.690482 C97.6647155,193.417512 100.981959,187.77078 104.642583,184.574357 C76.211799,181.33766 46.324819,170.362144 46.324819,121.315702 C46.324819,107.340889 51.3250588,95.9223682 59.5132437,86.9583937 C58.1842268,83.7344152 53.8029229,70.715562 60.7532354,53.0843636 C60.7532354,53.0843636 71.5019501,49.6441813 95.9626412,66.2049595 C106.172967,63.368876 117.123047,61.9465949 128.00106,61.8978432 C138.879073,61.9465949 149.837632,63.368876 160.067033,66.2049595 C184.49805,49.6441813 195.231926,53.0843636 195.231926,53.0843636 C202.199197,70.715562 197.815773,83.7344152 196.486756,86.9583937 C204.694018,95.9223682 209.660343,107.340889 209.660343,121.315702 C209.660343,170.478725 179.716133,181.303747 151.213281,184.472614 C155.80443,188.444828 159.895342,196.234518 159.895342,208.176593 C159.895342,225.303317 159.746968,239.087361 159.746968,243.303333 C159.746968,246.709601 162.05102,250.70089 168.53925,249.443941 C219.370432,232.499507 256,184.536204 256,128.00106 C256,57.3066942 198.691187,0 128.00106,0 Z M47.9405593,182.340212 C47.6586465,182.976105 46.6581745,183.166873 45.7467277,182.730227 C44.8183235,182.312656 44.2968914,181.445722 44.5978808,180.80771 C44.8734344,180.152739 45.876026,179.97045 46.8023103,180.409216 C47.7328342,180.826786 48.2627451,181.702199 47.9405593,182.340212 Z M54.2367892,187.958254 C53.6263318,188.524199 52.4329723,188.261363 51.6232682,187.366874 C50.7860088,186.474504 50.6291553,185.281144 51.2480912,184.70672 C51.8776254,184.140775 53.0349512,184.405731 53.8743302,185.298101 C54.7115892,186.201069 54.8748019,187.38595 54.2367892,187.958254 Z M58.5562413,195.146347 C57.7719732,195.691096 56.4895886,195.180261 55.6968417,194.042013 C54.9125733,192.903764 54.9125733,191.538713 55.713799,190.991845 C56.5086651,190.444977 57.7719732,190.936735 58.5753181,192.066505 C59.3574669,193.22383 59.3574669,194.58888 58.5562413,195.146347 Z M65.8613592,203.471174 C65.1597571,204.244846 63.6654083,204.03712 62.5716717,202.981538 C61.4524999,201.94927 61.1409122,200.484596 61.8446341,199.710926 C62.5547146,198.935137 64.0575422,199.15346 65.1597571,200.200564 C66.2704506,201.230712 66.6095936,202.705984 65.8613592,203.471174 Z M75.3025151,206.281542 C74.9930474,207.284134 73.553809,207.739857 72.1039724,207.313809 C70.6562556,206.875043 69.7087748,205.700761 70.0012857,204.687571 C70.302275,203.678621 71.7478721,203.20382 73.2083069,203.659543 C74.6539041,204.09619 75.6035048,205.261994 75.3025151,206.281542 Z M86.046947,207.473627 C86.0829806,208.529209 84.8535871,209.404622 83.3316829,209.4237 C81.8013,209.457614 80.563428,208.603398 80.5464708,207.564772 C80.5464708,206.498591 81.7483088,205.631657 83.2786917,205.606221 C84.8005962,205.576546 86.046947,206.424403 86.046947,207.473627 Z M96.6021471,207.069023 C96.7844366,208.099171 95.7267341,209.156872 94.215428,209.438785 C92.7295577,209.710099 91.3539086,209.074206 91.1652603,208.052538 C90.9808515,206.996955 92.0576306,205.939253 93.5413813,205.66582 C95.054807,205.402984 96.4092596,206.021919 96.6021471,207.069023 Z" fill="current"></path></g>
</svg>

Before

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -1,10 +0,0 @@
<svg class="w-10 h-10" viewBox="0 0 256 236" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
<g>
<path d="M128.07485,236.074667 L128.07485,236.074667 L175.17885,91.1043048 L80.9708495,91.1043048 L128.07485,236.074667 L128.07485,236.074667 Z" fill="#E24329"></path>
<path d="M128.07485,236.074423 L80.9708495,91.104061 L14.9557638,91.104061 L128.07485,236.074423 L128.07485,236.074423 Z" fill="#FC6D26"></path>
<path d="M14.9558857,91.1044267 L14.9558857,91.1044267 L0.641828571,135.159589 C-0.663771429,139.17757 0.766171429,143.57955 4.18438095,146.06275 L128.074971,236.074789 L14.9558857,91.1044267 L14.9558857,91.1044267 Z" fill="#FCA326"></path>
<path d="M14.9558857,91.1045486 L80.9709714,91.1045486 L52.6000762,3.79026286 C51.1408762,-0.703146667 44.7847619,-0.701927619 43.3255619,3.79026286 L14.9558857,91.1045486 L14.9558857,91.1045486 Z" fill="#E24329"></path>
<path d="M128.07485,236.074423 L175.17885,91.104061 L241.193935,91.104061 L128.07485,236.074423 L128.07485,236.074423 Z" fill="#FC6D26"></path>
<path d="M241.193935,91.1044267 L241.193935,91.1044267 L255.507992,135.159589 C256.813592,139.17757 255.38365,143.57955 251.96544,146.06275 L128.07485,236.074789 L241.193935,91.1044267 L241.193935,91.1044267 Z" fill="#FCA326"></path>
<path d="M241.193935,91.1045486 L175.17885,91.1045486 L203.549745,3.79026286 C205.008945,-0.703146667 211.365059,-0.701927619 212.824259,3.79026286 L241.193935,91.1045486 L241.193935,91.1045486 Z" fill="#E24329"></path>
</g> </svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -10,8 +10,8 @@
@if(count($sourceControls) > 0)
@foreach($sourceControls as $sourceControl)
<x-item-card>
<div class="flex-none">
<img src="{{ asset('static/images/' . $sourceControl->provider . '.svg') }}" class="h-10 w-10" alt="">
<div class="flex-none text-gray-600 dark:text-gray-300">
@include('livewire.source-controls.icons.' . $sourceControl->provider . '-icon')
</div>
<div class="ml-3 flex flex-grow flex-col items-start justify-center">
<span class="mb-1">{{ $sourceControl->profile }}</span>

View File

@ -1,27 +1,22 @@
<div class="hidden sm:flex sm:items-center sm:ml-6">
<div class="flex items-center text-gray-600 dark:text-gray-300">
<x-dropdown align="right" width="48">
<x-slot name="trigger">
<button class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 dark:text-gray-400 bg-white dark:bg-gray-800 hover:text-gray-700 dark:hover:text-gray-300 focus:outline-none transition ease-in-out duration-150">
<div>{{ Auth::user()->name }}</div>
<div class="ml-1">
<svg class="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
</div>
<button class="flex items-center">
<x-heroicon-o-cog-6-tooth class="w-7 h-7" />
</button>
</x-slot>
<x-slot name="content">
<x-dropdown-link :href="route('profile')">
{{ __('Profile') }}
</x-dropdown-link>
<x-dropdown-link :href="route('profile')">{{ __('Profile') }}</x-dropdown-link>
<x-dropdown-link :href="route('projects')">{{ __('Projects') }}</x-dropdown-link>
<x-dropdown-link :href="route('server-providers')">{{ __('Server Providers') }}</x-dropdown-link>
<x-dropdown-link :href="route('source-controls')">{{ __('Source Controls') }}</x-dropdown-link>
<x-dropdown-link :href="route('storage-providers')">{{ __('Storage Providers') }}</x-dropdown-link>
<x-dropdown-link :href="route('notification-channels')">{{ __('Notification Channels') }}</x-dropdown-link>
<x-dropdown-link :href="route('ssh-keys')">{{ __('SSH Keys') }}</x-dropdown-link>
<!-- Authentication -->
<form method="POST" action="{{ route('logout') }}">
@csrf
<x-dropdown-link :href="route('logout')" onclick="event.preventDefault(); this.closest('form').submit();">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 mr-1">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15M12 9l-3 3m0 0l3 3m-3-3h12.75" />
</svg>
{{ __('Log Out') }}
</x-dropdown-link>
</form>

View File

@ -23,8 +23,17 @@ public function test_add_email_channel(): void
Livewire::test(AddChannel::class)
->set('provider', NotificationChannel::EMAIL)
->set('email', 'email@example.com')
->set('label', 'Email')
->call('add')
->assertSuccessful();
$this->assertDatabaseHas('notification_channels', [
'provider' => NotificationChannel::EMAIL,
'data' => cast_to_json([
'email' => 'email@example.com',
]),
'connected' => 1,
]);
}
public function test_add_slack_channel(): void
@ -36,9 +45,17 @@ public function test_add_slack_channel(): void
Livewire::test(AddChannel::class)
->set('provider', NotificationChannel::SLACK)
->set('label', 'Slack')
->set('webhook_url', $this->faker->url)
->set('webhook_url', 'https://hooks.slack.com/services/123/token')
->call('add')
->assertSuccessful();
$this->assertDatabaseHas('notification_channels', [
'provider' => NotificationChannel::SLACK,
'data' => cast_to_json([
'webhook_url' => 'https://hooks.slack.com/services/123/token',
]),
'connected' => 1,
]);
}
public function test_add_discord_channel(): void
@ -49,10 +66,42 @@ public function test_add_discord_channel(): void
Livewire::test(AddChannel::class)
->set('provider', NotificationChannel::DISCORD)
->set('label', 'Slack')
->set('webhook_url', $this->faker->url)
->set('label', 'Discord')
->set('webhook_url', 'https://discord.com/api/webhooks/123/token')
->call('add')
->assertSuccessful();
$this->assertDatabaseHas('notification_channels', [
'provider' => NotificationChannel::DISCORD,
'data' => cast_to_json([
'webhook_url' => 'https://discord.com/api/webhooks/123/token',
]),
'connected' => 1,
]);
}
public function test_add_telegram_channel(): void
{
$this->actingAs($this->user);
Http::fake();
Livewire::test(AddChannel::class)
->set('provider', NotificationChannel::TELEGRAM)
->set('label', 'Telegram')
->set('bot_token', 'token')
->set('chat_id', '123')
->call('add')
->assertSuccessful();
$this->assertDatabaseHas('notification_channels', [
'provider' => NotificationChannel::TELEGRAM,
'data' => cast_to_json([
'chat_id' => '123',
'bot_token' => 'token',
]),
'connected' => 1,
]);
}
public function test_see_channels_list(): void

View File

@ -10,6 +10,7 @@ php artisan config:clear
php artisan cache:clear
php artisan config:cache
php artisan icons:cache
sudo supervisorctl restart worker:*