From 470274279b0516280c05d20623298843fa4087de Mon Sep 17 00:00:00 2001 From: Saeed Vaziry Date: Sun, 25 Feb 2024 17:16:11 +0100 Subject: [PATCH 01/43] drop mysql and redis and use sqlite for Vito itself to optimize the resources --- .env.example | 22 ------- .env.prod | 25 +------- .env.testing | 27 -------- .github/workflows/code-style.yml | 3 - .github/workflows/tests.yml | 21 +------ Dockerfile | 17 +----- config/database.php | 5 +- ..._15_090830_create_firewall_rules_table.php | 2 +- ..._21_210213_update_firewall_rules_table.php | 4 +- docker/standalone/start.sh | 16 +---- install/install.sh | 28 +-------- phpunit.xml | 4 +- tests/Feature/NotificationChannelsTest.php | 61 ++++++++++--------- tests/Feature/ServerKeysTest.php | 8 ++- 14 files changed, 53 insertions(+), 190 deletions(-) delete mode 100755 .env.testing diff --git a/.env.example b/.env.example index bf21e04..239fb2f 100755 --- a/.env.example +++ b/.env.example @@ -4,26 +4,7 @@ APP_KEY= APP_DEBUG=true APP_URL=http://vito.test -LOG_CHANNEL=stack -LOG_LEVEL=debug - -DB_CONNECTION=mysql -DB_HOST=127.0.0.1 -DB_PORT=3306 -DB_DATABASE=vito -DB_USERNAME=root -DB_PASSWORD= - -BROADCAST_DRIVER=null -CACHE_DRIVER=file FILESYSTEM_DRIVER=local -QUEUE_CONNECTION=default -SESSION_DRIVER=database -SESSION_LIFETIME=120 - -REDIS_HOST=127.0.0.1 -REDIS_PASSWORD=null -REDIS_PORT=6379 MAIL_MAILER=smtp MAIL_HOST= @@ -33,6 +14,3 @@ MAIL_PASSWORD=null MAIL_ENCRYPTION=null MAIL_FROM_ADDRESS=null MAIL_FROM_NAME="${APP_NAME}" - -SSH_PUBLIC_KEY_NAME=ssh-public.key -SSH_PRIVATE_KEY_NAME=ssh-private.pem diff --git a/.env.prod b/.env.prod index 8b39156..b18d155 100755 --- a/.env.prod +++ b/.env.prod @@ -1,29 +1,9 @@ APP_NAME=Vito APP_ENV=production APP_KEY= -APP_DEBUG=false -APP_URL= +APP_URL=http://vito.test -LOG_CHANNEL=stack -LOG_LEVEL=debug - -DB_CONNECTION=mysql -DB_HOST=127.0.0.1 -DB_PORT=3306 -DB_DATABASE= -DB_USERNAME= -DB_PASSWORD= - -BROADCAST_DRIVER=null -CACHE_DRIVER=file FILESYSTEM_DRIVER=local -QUEUE_CONNECTION=default -SESSION_DRIVER=database -SESSION_LIFETIME=120 - -REDIS_HOST=127.0.0.1 -REDIS_PASSWORD=null -REDIS_PORT=6379 MAIL_MAILER=smtp MAIL_HOST= @@ -33,6 +13,3 @@ MAIL_PASSWORD=null MAIL_ENCRYPTION=null MAIL_FROM_ADDRESS=null MAIL_FROM_NAME="${APP_NAME}" - -SSH_PUBLIC_KEY_NAME=ssh-public.key -SSH_PRIVATE_KEY_NAME=ssh-private.pem diff --git a/.env.testing b/.env.testing deleted file mode 100755 index caee8cd..0000000 --- a/.env.testing +++ /dev/null @@ -1,27 +0,0 @@ -APP_NAME=Vito -APP_ENV=local -APP_KEY=base64:d9kZW60V4lFEw2SPn6UiJ0cfi04v80EWP0GZ6kzoxNg= -APP_DEBUG=true -APP_URL=http://localhost:2080 - -LOG_CHANNEL=stack -LOG_LEVEL=debug - -DB_CONNECTION=mysql -DB_HOST=127.0.0.1 -DB_PORT=3306 -DB_DATABASE=vito_test -DB_USERNAME=root -DB_PASSWORD= - -BROADCAST_DRIVER=null -CACHE_DRIVER=array -FILESYSTEM_DRIVER=local -QUEUE_CONNECTION=database -SESSION_DRIVER=array -SESSION_LIFETIME=120 - -MAIL_MAILER=array - -SSH_PUBLIC_KEY_NAME=ssh-public.key -SSH_PRIVATE_KEY_NAME=ssh-private.pem diff --git a/.github/workflows/code-style.yml b/.github/workflows/code-style.yml index 7a77dbb..6920a07 100644 --- a/.github/workflows/code-style.yml +++ b/.github/workflows/code-style.yml @@ -1,10 +1,7 @@ name: code-style on: - push: pull_request: - schedule: - - cron: '0 0 * * *' jobs: code-style: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 42b2f12..cab6151 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -2,26 +2,14 @@ name: tests on: push: + branches: + - 1.x pull_request: - schedule: - - cron: '0 0 * * *' jobs: tests: runs-on: ubuntu-22.04 - services: - mysql: - image: mysql - env: - MYSQL_DATABASE: test_db - MYSQL_USER: user - MYSQL_PASSWORD: password - MYSQL_ROOT_PASSWORD: rootpassword - ports: - - 3306:3306 - options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 - strategy: fail-fast: true matrix: @@ -49,8 +37,3 @@ jobs: - name: Run test suite run: php artisan test - env: - DB_HOST: 127.0.0.1 - DB_DATABASE: test_db - DB_USERNAME: user - DB_PASSWORD: password diff --git a/Dockerfile b/Dockerfile index ec01afe..d3becdf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,9 +4,6 @@ WORKDIR /var/www/html ENV DEBIAN_FRONTEND noninteractive -RUN echo "mysql-server mysql-server/root_password password password" | debconf-set-selections -RUN echo "mysql-server mysql-server/root_password_again password password" | debconf-set-selections - # upgrade RUN apt clean && apt update && apt update && apt upgrade -y && apt autoremove -y @@ -22,23 +19,13 @@ RUN apt update \ python2 dnsutils librsvg2-bin fswatch wget \ && add-apt-repository ppa:ondrej/php -y \ && apt update \ - && apt install -y php8.1 php8.1-fpm php8.1-mbstring php8.1-mysql php8.1-mcrypt php8.1-gd php8.1-xml \ + && apt install -y php8.1 php8.1-fpm php8.1-mbstring php8.1-mcrypt php8.1-gd php8.1-xml \ php8.1-curl php8.1-gettext php8.1-zip php8.1-bcmath php8.1-soap php8.1-redis COPY docker/standalone/php.ini /etc/php/8.1/cli/conf.d/99-vito.ini # composer RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer -# mysql -RUN wget -c https://dev.mysql.com/get/mysql-apt-config_0.8.22-1_all.deb \ - && mkdir -p /etc/apt/keyrings \ - && apt clean \ - && apt update \ - && dpkg -i mysql-apt-config_0.8.22-1_all.deb \ - && apt install mysql-server -y - -RUN service mysql stop - # app COPY . /var/www/html RUN rm -rf /var/www/html/vendor @@ -60,6 +47,6 @@ COPY docker/standalone/supervisord.conf /etc/supervisor/conf.d/supervisord.conf COPY docker/standalone/start.sh /start.sh RUN chmod +x /start.sh -EXPOSE 80 3306 +EXPOSE 80 CMD ["/start.sh"] diff --git a/config/database.php b/config/database.php index 833bcc8..03641e9 100644 --- a/config/database.php +++ b/config/database.php @@ -15,7 +15,7 @@ | */ - 'default' => env('DB_CONNECTION', 'mysql'), + 'default' => env('DB_CONNECTION', 'sqlite'), /* |-------------------------------------------------------------------------- @@ -37,8 +37,7 @@ 'sqlite' => [ 'driver' => 'sqlite', - 'url' => env('DATABASE_URL'), - 'database' => env('DB_DATABASE', database_path('database.sqlite')), + 'database' => storage_path(env('DB_DATABASE', 'database.sqlite')), 'prefix' => '', 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), ], diff --git a/database/migrations/2021_07_15_090830_create_firewall_rules_table.php b/database/migrations/2021_07_15_090830_create_firewall_rules_table.php index ac13e58..92020a8 100755 --- a/database/migrations/2021_07_15_090830_create_firewall_rules_table.php +++ b/database/migrations/2021_07_15_090830_create_firewall_rules_table.php @@ -15,7 +15,7 @@ public function up(): void $table->string('protocol'); $table->integer('port'); $table->ipAddress('source')->default('0.0.0.0'); - $table->tinyInteger('mask')->default(0); + $table->string('mask')->nullable(); $table->text('note')->nullable(); $table->string('status')->default('creating'); $table->timestamps(); diff --git a/database/migrations/2023_07_21_210213_update_firewall_rules_table.php b/database/migrations/2023_07_21_210213_update_firewall_rules_table.php index 5ac104a..2e31c43 100644 --- a/database/migrations/2023_07_21_210213_update_firewall_rules_table.php +++ b/database/migrations/2023_07_21_210213_update_firewall_rules_table.php @@ -7,7 +7,9 @@ { public function up(): void { - DB::statement('ALTER TABLE firewall_rules MODIFY mask varchar(10) null'); + if (DB::getDriverName() === 'mysql') { + DB::statement('ALTER TABLE firewall_rules MODIFY mask varchar(10) null'); + } } public function down(): void diff --git a/docker/standalone/start.sh b/docker/standalone/start.sh index 9c9ee93..bba6759 100644 --- a/docker/standalone/start.sh +++ b/docker/standalone/start.sh @@ -8,19 +8,7 @@ PASSWORD=${PASSWORD:-"password"} # Check if the flag file does not exist, indicating a first run if [ ! -f "$INIT_FLAG" ]; then - echo "First run of the container. Initializing MySQL..." - - # Start MySQL temporarily - service mysql start - - # Wait for MySQL to start up completely (may need to adjust the sleep duration) - sleep 3 - - # Create Database - mysql -u root -p`echo password` -e "CREATE DATABASE IF NOT EXISTS vito CHARACTER SET utf8 COLLATE utf8_general_ci;" - - # Change Password - mysql -u root -p`echo password` -e "ALTER USER 'root'@'localhost' IDENTIFIED BY '$DB_PASSWORD'; FLUSH PRIVILEGES;" + echo "Initializing..." # Generate SSH keys openssl genpkey -algorithm RSA -out /var/www/html/storage/ssh-private.pem @@ -31,8 +19,6 @@ if [ ! -f "$INIT_FLAG" ]; then touch "$INIT_FLAG" fi -service mysql start - service php8.1-fpm start service nginx start diff --git a/install/install.sh b/install/install.sh index b616eb8..c86e060 100644 --- a/install/install.sh +++ b/install/install.sh @@ -100,14 +100,11 @@ if ! echo "${V_NGINX_CONFIG}" | tee /etc/nginx/nginx.conf; then fi service nginx start -# redis -apt install redis-server -y - # php export V_PHP_VERSION="8.1" add-apt-repository ppa:ondrej/php -y apt update -apt install -y php${V_PHP_VERSION} php${V_PHP_VERSION}-fpm php${V_PHP_VERSION}-mbstring php${V_PHP_VERSION}-mysql php${V_PHP_VERSION}-mcrypt php${V_PHP_VERSION}-gd php${V_PHP_VERSION}-xml php${V_PHP_VERSION}-curl php${V_PHP_VERSION}-gettext php${V_PHP_VERSION}-zip php${V_PHP_VERSION}-bcmath php${V_PHP_VERSION}-soap php${V_PHP_VERSION}-redis +apt install -y php${V_PHP_VERSION} php${V_PHP_VERSION}-fpm php${V_PHP_VERSION}-mbstring php${V_PHP_VERSION}-mcrypt php${V_PHP_VERSION}-gd php${V_PHP_VERSION}-xml php${V_PHP_VERSION}-curl php${V_PHP_VERSION}-gettext php${V_PHP_VERSION}-zip php${V_PHP_VERSION}-bcmath php${V_PHP_VERSION}-soap php${V_PHP_VERSION}-redis if ! sed -i "s/www-data/${V_USERNAME}/g" /etc/php/${V_PHP_VERSION}/fpm/pool.d/www.conf; then echo 'Error installing PHP' && exit 1 fi @@ -120,23 +117,6 @@ service php${V_PHP_VERSION}-fpm restart curl -sS https://getcomposer.org/installer -o composer-setup.php php composer-setup.php --install-dir=/usr/local/bin --filename=composer -# database -export V_MARIADB_VERSION="10.3" -export V_DB_USER="vito" -export V_DB_NAME="vito" -export V_DB_PASS=$(openssl rand -base64 12) -wget -c https://dev.mysql.com/get/mysql-apt-config_0.8.22-1_all.deb -dpkg -i mysql-apt-config_0.8.22-1_all.deb -apt update -apt install mysql-server -y -service mysql enable -service mysql start -mysql -e "CREATE DATABASE IF NOT EXISTS ${V_DB_NAME} CHARACTER SET utf8 COLLATE utf8_general_ci" -mysql -e "CREATE USER IF NOT EXISTS '${V_DB_USER}'@'localhost' IDENTIFIED BY '${V_DB_PASS}'" -mysql -e "FLUSH PRIVILEGES" -mysql -e "GRANT ALL PRIVILEGES ON ${V_DB_NAME}.* TO '${V_DB_USER}'@'localhost'" -mysql -e "FLUSH PRIVILEGES" - # setup website export V_SSL=${V_SSL:-1} export COMPOSER_ALLOW_SUPERUSER=1 @@ -196,9 +176,6 @@ else export V_URL="http://${V_DOMAIN}" fi sed -i "s|APP_URL=.*|APP_URL=${V_URL}|" /home/${V_USERNAME}/${V_DOMAIN}/.env -sed -i "s|DB_DATABASE=.*|DB_DATABASE=${V_DB_NAME}|" /home/${V_USERNAME}/${V_DOMAIN}/.env -sed -i "s|DB_USERNAME=.*|DB_USERNAME=${V_DB_USER}|" /home/${V_USERNAME}/${V_DOMAIN}/.env -sed -i "s|DB_PASSWORD=.*|DB_PASSWORD=${V_DB_PASS}|" /home/${V_USERNAME}/${V_DOMAIN}/.env php artisan key:generate php artisan storage:link php artisan migrate --force @@ -249,9 +226,6 @@ php artisan icons:cache echo "🎉 Congratulations!" echo "✅ SSH User: ${V_USERNAME}" echo "✅ SSH Password: ${V_PASSWORD}" -echo "✅ DB Name: ${V_DB_NAME}" -echo "✅ DB Username: ${V_DB_USER}" -echo "✅ DB Password: ${V_DB_PASS}" echo "✅ Admin Email: ${V_ADMIN_EMAIL}" echo "✅ Admin Password: ${V_ADMIN_PASSWORD}" echo "✅ URL: http://${V_DOMAIN}" diff --git a/phpunit.xml b/phpunit.xml index 66fee18..2d8b792 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -11,12 +11,12 @@ + + - - diff --git a/tests/Feature/NotificationChannelsTest.php b/tests/Feature/NotificationChannelsTest.php index 50a3d76..c75c932 100644 --- a/tests/Feature/NotificationChannelsTest.php +++ b/tests/Feature/NotificationChannelsTest.php @@ -27,13 +27,13 @@ public function test_add_email_channel(): void ->call('add') ->assertSuccessful(); - $this->assertDatabaseHas('notification_channels', [ - 'provider' => NotificationChannel::EMAIL, - 'data' => cast_to_json([ - 'email' => 'email@example.com', - ]), - 'connected' => 1, - ]); + /** @var \App\Models\NotificationChannel $channel */ + $channel = \App\Models\NotificationChannel::query() + ->where('provider', NotificationChannel::EMAIL) + ->first(); + + $this->assertEquals('email@example.com', $channel->data['email']); + $this->assertTrue($channel->connected); } public function test_add_slack_channel(): void @@ -49,13 +49,13 @@ public function test_add_slack_channel(): void ->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, - ]); + /** @var \App\Models\NotificationChannel $channel */ + $channel = \App\Models\NotificationChannel::query() + ->where('provider', NotificationChannel::SLACK) + ->first(); + + $this->assertEquals('https://hooks.slack.com/services/123/token', $channel->data['webhook_url']); + $this->assertTrue($channel->connected); } public function test_add_discord_channel(): void @@ -71,15 +71,18 @@ public function test_add_discord_channel(): void ->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, - ]); + /** @var \App\Models\NotificationChannel $channel */ + $channel = \App\Models\NotificationChannel::query() + ->where('provider', NotificationChannel::DISCORD) + ->first(); + + $this->assertEquals('https://discord.com/api/webhooks/123/token', $channel->data['webhook_url']); + $this->assertTrue($channel->connected); } + /* + * @TODO fix json comparison + */ public function test_add_telegram_channel(): void { $this->actingAs($this->user); @@ -94,14 +97,14 @@ public function test_add_telegram_channel(): void ->call('add') ->assertSuccessful(); - $this->assertDatabaseHas('notification_channels', [ - 'provider' => NotificationChannel::TELEGRAM, - 'data' => cast_to_json([ - 'chat_id' => '123', - 'bot_token' => 'token', - ]), - 'connected' => 1, - ]); + /** @var \App\Models\NotificationChannel $channel */ + $channel = \App\Models\NotificationChannel::query() + ->where('provider', NotificationChannel::TELEGRAM) + ->first(); + + $this->assertEquals('123', $channel->data['chat_id']); + $this->assertEquals('token', $channel->data['bot_token']); + $this->assertTrue($channel->connected); } public function test_see_channels_list(): void diff --git a/tests/Feature/ServerKeysTest.php b/tests/Feature/ServerKeysTest.php index 726b4de..0e4ce9c 100644 --- a/tests/Feature/ServerKeysTest.php +++ b/tests/Feature/ServerKeysTest.php @@ -28,7 +28,9 @@ public function test_see_server_keys() 'public_key' => 'public-key-content', ]); - $this->server->sshKeys()->attach($sshKey); + $this->server->sshKeys()->attach($sshKey, [ + 'status' => SshKeyStatus::ADDED, + ]); Livewire::test(ServerKeysList::class, ['server' => $this->server]) ->assertSeeText('My first key'); @@ -46,7 +48,9 @@ public function test_delete_ssh_key() 'public_key' => 'public-key-content', ]); - $this->server->sshKeys()->attach($sshKey); + $this->server->sshKeys()->attach($sshKey, [ + 'status' => SshKeyStatus::ADDED, + ]); Livewire::test(ServerKeysList::class, ['server' => $this->server]) ->set('deleteId', $sshKey->id) From 5b2c419e917fc36cf297b7f83d4e881e90954de2 Mon Sep 17 00:00:00 2001 From: Saeed Vaziry Date: Sun, 25 Feb 2024 17:25:25 +0100 Subject: [PATCH 02/43] create sqlite database for tests --- .github/workflows/tests.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index cab6151..72cd786 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -35,5 +35,8 @@ jobs: if: steps.composer-cache.outputs.cache-hit != 'true' run: composer install --prefer-dist --no-progress --no-suggest + - name: Create sqlite database + run: touch storage/database-test.sqlite + - name: Run test suite run: php artisan test From b2083fc6b2d9cc4c47b35d284143fccd717dfa09 Mon Sep 17 00:00:00 2001 From: Saeed Vaziry <61919774+saeedvaziry@users.noreply.github.com> Date: Wed, 6 Mar 2024 17:02:59 +0100 Subject: [PATCH 03/43] Migrate to HTMX (#114) Dropped Livewire Added HTMX Added Blade code lint Drop Mysql and Redis Migrate to SQLite --- .prettierrc | 15 + README.md | 1 - app/Actions/CronJob/CreateCronJob.php | 28 +- app/Actions/CronJob/DeleteCronJob.php | 24 + app/Actions/Database/CreateBackup.php | 20 +- app/Actions/Database/CreateDatabase.php | 6 +- app/Actions/Database/CreateDatabaseUser.php | 14 +- app/Actions/Database/DeleteDatabase.php | 15 + app/Actions/Database/DeleteDatabaseUser.php | 15 + app/Actions/Database/LinkUser.php | 45 +- app/Actions/FirewallRule/CreateRule.php | 16 +- app/Actions/FirewallRule/DeleteRule.php | 28 + app/Actions/PHP/ChangeDefaultCli.php | 29 + app/Actions/PHP/GetPHPIni.php | 32 + app/Actions/PHP/InstallNewPHP.php | 4 +- app/Actions/PHP/InstallPHPExtension.php | 29 +- app/Actions/PHP/UninstallPHP.php | 19 +- app/Actions/PHP/UpdatePHPIni.php | 28 +- app/Actions/Projects/DeleteProject.php | 5 +- app/Actions/Projects/UpdateProject.php | 2 +- app/Actions/Queue/CreateQueue.php | 2 +- app/Actions/SSL/CreateSSL.php | 2 +- app/Actions/Server/CheckConnection.php | 30 + app/Actions/Server/CreateServer.php | 2 +- app/Actions/Server/EditServer.php | 2 +- app/Actions/Server/RebootServer.php | 24 + app/Actions/Site/ChangePHPVersion.php | 30 - app/Actions/Site/UpdateEnv.php | 8 +- app/Actions/Site/UpdateSourceControl.php | 35 -- .../StorageProvider/CreateStorageProvider.php | 11 +- app/Exceptions/SSHCommandError.php | 10 + app/Facades/Toast.php | 19 + app/Helpers/HtmxResponse.php | 34 ++ app/Helpers/SSH.php | 24 +- app/Helpers/Toast.php | 9 +- .../Controllers/ApplicationController.php | 104 ++++ app/Http/Controllers/CronjobController.php | 29 +- .../Controllers/DatabaseBackupController.php | 79 +++ app/Http/Controllers/DatabaseController.php | 36 +- .../Controllers/DatabaseUserController.php | 54 ++ app/Http/Controllers/FirewallController.php | 29 +- app/Http/Controllers/PHPController.php | 72 ++- app/Http/Controllers/ProjectController.php | 29 - app/Http/Controllers/QueueController.php | 52 ++ app/Http/Controllers/SSHKeyController.php | 50 +- app/Http/Controllers/SSLController.php | 43 ++ app/Http/Controllers/ServerController.php | 57 +- app/Http/Controllers/ServerLogController.php | 29 + .../Controllers/ServerSettingController.php | 45 +- app/Http/Controllers/ServiceController.php | 34 +- .../NotificationChannelController.php | 45 ++ .../Settings/ProfileController.php | 43 ++ .../Settings/ProjectController.php | 79 +++ .../Controllers/Settings/SSHKeyController.php | 45 ++ .../Settings/ServerProviderController.php | 48 ++ .../Settings/SourceControlController.php | 47 ++ .../Settings/StorageProviderController.php | 48 ++ app/Http/Controllers/SiteController.php | 58 +- app/Http/Controllers/SiteLogController.php | 18 + .../Controllers/SiteSettingController.php | 65 +++ app/Http/Kernel.php | 2 + .../Livewire/Application/AutoDeployment.php | 60 -- .../Livewire/Application/ChangeBranch.php | 35 -- app/Http/Livewire/Application/Deploy.php | 40 -- .../Livewire/Application/DeploymentScript.php | 38 -- .../Livewire/Application/DeploymentsList.php | 34 -- app/Http/Livewire/Application/Env.php | 37 -- app/Http/Livewire/Application/LaravelApp.php | 20 - app/Http/Livewire/Application/PhpApp.php | 20 - app/Http/Livewire/Application/PhpBlankApp.php | 19 - .../Livewire/Application/WordpressApp.php | 20 - app/Http/Livewire/Broadcast.php | 21 - app/Http/Livewire/Cronjobs/CreateCronjob.php | 34 -- app/Http/Livewire/Cronjobs/CronjobsList.php | 35 -- .../Databases/DatabaseBackupFiles.php | 67 --- .../Livewire/Databases/DatabaseBackups.php | 81 --- app/Http/Livewire/Databases/DatabaseList.php | 66 --- .../Livewire/Databases/DatabaseUserList.php | 98 ---- .../Livewire/Firewall/CreateFirewallRule.php | 40 -- .../Livewire/Firewall/FirewallRulesList.php | 37 -- .../NotificationChannels/AddChannel.php | 38 -- .../NotificationChannels/ChannelsList.php | 37 -- app/Http/Livewire/Php/DefaultCli.php | 30 - app/Http/Livewire/Php/InstalledVersions.php | 117 ---- .../Profile/TwoFactorAuthentication.php | 14 - app/Http/Livewire/Profile/UpdatePassword.php | 32 - .../Profile/UpdateProfileInformation.php | 54 -- app/Http/Livewire/Projects/CreateProject.php | 37 -- app/Http/Livewire/Projects/EditProject.php | 37 -- app/Http/Livewire/Projects/ProjectsList.php | 42 -- app/Http/Livewire/Queues/CreateQueue.php | 36 -- app/Http/Livewire/Queues/QueuesList.php | 57 -- app/Http/Livewire/ServerLogs/LogsList.php | 58 -- .../ServerProviders/ConnectProvider.php | 40 -- .../ServerProviders/ProvidersList.php | 37 -- .../ServerSettings/CheckConnection.php | 24 - .../Livewire/ServerSettings/EditServer.php | 40 -- .../Livewire/ServerSettings/RebootServer.php | 24 - .../Livewire/ServerSettings/ServerDetails.php | 20 - .../Livewire/ServerSshKeys/AddExistingKey.php | 33 -- app/Http/Livewire/ServerSshKeys/AddNewKey.php | 36 -- .../Livewire/ServerSshKeys/ServerKeysList.php | 40 -- app/Http/Livewire/Servers/CreateServer.php | 64 -- app/Http/Livewire/Servers/DeleteServer.php | 29 - app/Http/Livewire/Servers/ServerStatus.php | 20 - app/Http/Livewire/Servers/ServersList.php | 24 - app/Http/Livewire/Servers/ShowServer.php | 31 - .../Livewire/Services/InstallPHPMyAdmin.php | 31 - app/Http/Livewire/Services/ServicesList.php | 63 -- app/Http/Livewire/Sites/ChangePhpVersion.php | 41 -- app/Http/Livewire/Sites/CreateSite.php | 47 -- app/Http/Livewire/Sites/DeleteSite.php | 27 - app/Http/Livewire/Sites/ShowSite.php | 20 - app/Http/Livewire/Sites/SiteStatus.php | 20 - app/Http/Livewire/Sites/SitesList.php | 38 -- .../Sites/UpdateSourceControlProvider.php | 33 -- app/Http/Livewire/Sites/UpdateVHost.php | 41 -- app/Http/Livewire/SourceControls/Connect.php | 38 -- .../SourceControls/SourceControlsList.php | 37 -- app/Http/Livewire/SshKeys/AddKey.php | 31 - app/Http/Livewire/SshKeys/KeysList.php | 37 -- app/Http/Livewire/Ssl/CreateSsl.php | 35 -- app/Http/Livewire/Ssl/SslsList.php | 46 -- .../StorageProviders/ConnectProvider.php | 44 -- .../StorageProviders/ProvidersList.php | 37 -- app/Http/Livewire/UserDropdown.php | 18 - app/Http/Middleware/HandleSSHErrors.php | 30 + app/Http/Requests/Auth/LoginRequest.php | 85 --- app/Http/Requests/ProfileUpdateRequest.php | 23 - app/Jobs/Backup/RestoreDatabase.php | 12 - app/Jobs/Backup/RunBackup.php | 12 - app/Jobs/CronJob/AddToServer.php | 48 -- app/Jobs/CronJob/RemoveFromServer.php | 46 -- app/Jobs/Database/CreateOnServer.php | 35 -- app/Jobs/Database/DeleteFromServer.php | 37 -- app/Jobs/DatabaseUser/CreateOnServer.php | 48 -- app/Jobs/DatabaseUser/DeleteFromServer.php | 40 -- app/Jobs/DatabaseUser/LinkUser.php | 40 -- app/Jobs/DatabaseUser/UnlinkUser.php | 39 -- app/Jobs/Firewall/AddToServer.php | 48 -- app/Jobs/Firewall/RemoveFromServer.php | 48 -- app/Jobs/Installation/InstallPHPMyAdmin.php | 3 + app/Jobs/Installation/UninstallPHPMyAdmin.php | 3 + app/Jobs/PHP/SetDefaultCli.php | 46 -- app/Jobs/Server/CheckConnection.php | 51 -- app/Jobs/Server/RebootServer.php | 45 -- app/Jobs/Site/ChangePHPVersion.php | 43 -- app/Models/CronJob.php | 19 +- app/Models/Database.php | 21 - app/Models/DatabaseUser.php | 40 -- app/Models/FirewallRule.php | 15 - app/Models/Server.php | 14 +- app/Models/Site.php | 10 +- app/Providers/AppServiceProvider.php | 8 + app/ServiceHandlers/PHP.php | 12 +- app/SiteTypes/Wordpress.php | 17 +- app/Support/helpers.php | 48 +- app/Traits/HasCustomPaginationView.php | 15 - app/Traits/HasToast.php | 13 - app/Traits/RefreshComponentOnBroadcast.php | 20 - app/ValidationRules/CronRule.php | 9 +- composer.json | 1 - composer.lock | 77 +-- config/livewire.php | 159 ----- ..._095440_update_storage_providers_table.php | 8 +- database/seeders/DatabaseSeeder.php | 6 + install/install.sh | 1 + package-lock.json | 132 +++++ package.json | 8 +- public/build/assets/app-5db42bc3.css | 1 + public/build/assets/app-887de6f7.css | 1 - public/build/assets/app-c41c626e.js | 21 + public/build/assets/app-e6b0cd9c.js | 101 ---- public/build/manifest.json | 4 +- resources/commands/php/get-php-ini.sh | 4 +- resources/js/app.js | 49 +- .../application/auto-deployment.blade.php | 37 ++ .../views/application/change-branch.blade.php | 39 ++ resources/views/application/deploy.blade.php | 14 + .../application/deployment-script.blade.php | 35 ++ .../application/deployments-list.blade.php | 69 +++ resources/views/application/env.blade.php | 43 ++ .../views/application/laravel-app.blade.php | 1 + .../partials/deployment-status.blade.php | 8 +- resources/views/application/php-app.blade.php | 56 ++ .../views/application/php-blank-app.blade.php | 1 + .../views/application/wordpress-app.blade.php | 10 + .../views/auth/confirm-password.blade.php | 20 +- .../views/auth/forgot-password.blade.php | 18 +- resources/views/auth/login.blade.php | 70 ++- resources/views/auth/reset-password.blade.php | 39 +- .../views/auth/two-factor-challenge.blade.php | 36 +- .../components/application-logo.blade.php | 6 +- .../components/auth-session-status.blade.php | 6 +- .../views/components/card-header.blade.php | 8 +- resources/views/components/card.blade.php | 18 +- .../views/components/confirm-modal.blade.php | 18 - .../components/confirmation-modal.blade.php | 32 + .../views/components/console-view.blade.php | 4 +- .../views/components/container.blade.php | 2 +- .../views/components/danger-button.blade.php | 4 +- resources/views/components/datetime.blade.php | 4 +- .../views/components/dropdown-link.blade.php | 6 +- resources/views/components/dropdown.blade.php | 66 ++- .../components/htmx-error-handler.blade.php | 48 ++ .../views/components/icon-button.blade.php | 13 +- .../views/components/input-error.blade.php | 6 +- .../views/components/input-help.blade.php | 6 +- .../views/components/input-label.blade.php | 8 +- .../views/components/item-card.blade.php | 4 +- resources/views/components/live.blade.php | 14 + resources/views/components/modal.blade.php | 70 +-- resources/views/components/nav-link.blade.php | 13 +- .../views/components/primary-button.blade.php | 13 +- .../components/responsive-nav-link.blade.php | 13 +- .../components/secondary-button.blade.php | 23 +- .../secondary-sidebar-link.blade.php | 13 +- resources/views/components/section.blade.php | 18 +- .../views/components/select-input.blade.php | 7 +- .../components/server-provider-item.blade.php | 18 +- .../views/components/sidebar-link.blade.php | 13 +- .../views/components/simple-card.blade.php | 4 +- .../views/components/site-type-item.blade.php | 16 +- resources/views/components/status.blade.php | 6 +- resources/views/components/table.blade.php | 4 +- resources/views/components/td.blade.php | 4 +- .../views/components/text-input.blade.php | 8 +- resources/views/components/textarea.blade.php | 9 +- resources/views/components/th.blade.php | 4 +- resources/views/components/toast.blade.php | 15 +- .../views/components/user-dropdown.blade.php | 42 ++ resources/views/cronjobs/index.blade.php | 2 +- .../partrials/create-cronjob.blade.php | 116 ++++ .../partrials/cronjobs-list.blade.php | 52 ++ .../partrials}/status.blade.php | 8 +- resources/views/daemons/index.blade.php | 1 - resources/views/databases/backups.blade.php | 2 +- resources/views/databases/index.blade.php | 6 +- .../partials/backup-file-status.blade.php | 20 +- .../partials/backup-status.blade.php | 8 +- .../partials/create-backup-modal.blade.php | 105 ++++ .../partials/create-database-modal.blade.php | 109 ++++ .../create-database-user-modal.blade.php | 84 +++ .../partials/database-backup-files.blade.php | 94 +++ .../partials/database-backups.blade.php | 61 ++ .../partials/database-list.blade.php | 59 ++ .../partials/database-status.blade.php | 11 +- .../partials/database-user-list.blade.php | 84 +++ .../database-user-password-modal.blade.php | 45 ++ .../partials/database-user-status.blade.php | 11 +- .../link-database-user-modal.blade.php | 52 ++ .../partials/restore-backup-modal.blade.php | 41 ++ resources/views/firewall/index.blade.php | 2 +- .../partials/create-firewall-rule.blade.php | 92 +++ .../partials/firewall-rules-list.blade.php | 65 +++ .../firewall/partials/status.blade.php | 8 +- resources/views/layouts/app.blade.php | 548 +++++++++++------- resources/views/layouts/guest.blade.php | 27 +- resources/views/layouts/navigation.blade.php | 12 +- .../layouts/partials/color-scheme.blade.php | 67 ++- .../views/layouts/partials/favicon.blade.php | 34 +- .../layouts/partials/project-select.blade.php | 72 ++- .../layouts/partials/server-select.blade.php | 70 ++- .../layouts/partials/site-select.blade.php | 68 ++- resources/views/layouts/profile.blade.php | 145 ++++- resources/views/layouts/server.blade.php | 35 +- resources/views/layouts/site.blade.php | 164 ++++-- .../application/auto-deployment.blade.php | 32 - .../application/change-branch.blade.php | 31 - .../livewire/application/deploy.blade.php | 5 - .../application/deployment-script.blade.php | 31 - .../application/deployments-list.blade.php | 51 -- .../views/livewire/application/env.blade.php | 35 -- .../application/laravel-app.blade.php | 1 - .../livewire/application/php-app.blade.php | 62 -- .../application/php-blank-app.blade.php | 1 - .../application/wordpress-app.blade.php | 6 - resources/views/livewire/broadcast.blade.php | 1 - .../cronjobs/create-cronjob.blade.php | 69 --- .../livewire/cronjobs/cronjobs-list.blade.php | 45 -- .../databases/database-backup-files.blade.php | 74 --- .../databases/database-backups.blade.php | 49 -- .../databases/database-list.blade.php | 46 -- .../databases/database-user-list.blade.php | 56 -- .../partials/create-backup-modal.blade.php | 78 --- .../partials/create-database-modal.blade.php | 68 --- .../create-database-user-modal.blade.php | 51 -- .../database-user-password-modal.blade.php | 23 - .../delete-backup-file-modal.blade.php | 6 - .../partials/delete-backup-modal.blade.php | 6 - .../partials/delete-database-modal.blade.php | 6 - .../delete-database-user-modal.blade.php | 6 - .../link-database-user-modal.blade.php | 28 - .../partials/restore-backup-modal.blade.php | 30 - .../firewall/create-firewall-rule.blade.php | 72 --- .../firewall/firewall-rules-list.blade.php | 52 -- .../add-channel.blade.php | 84 --- .../livewire/partials/pagination.blade.php | 43 -- .../livewire/php/installed-versions.blade.php | 63 -- .../php/partials/install-extension.blade.php | 36 -- .../php/partials/install-new-php.blade.php | 18 - .../php/partials/uninstall-php.blade.php | 6 - .../php/partials/update-php-ini.blade.php | 25 - .../profile/update-password.blade.php | 45 -- .../update-profile-information.blade.php | 68 --- .../projects/create-project.blade.php | 31 - .../livewire/projects/edit-project.blade.php | 31 - .../livewire/projects/projects-list.blade.php | 38 -- .../livewire/queues/create-queue.blade.php | 75 --- .../livewire/queues/queues-list.blade.php | 54 -- .../livewire/server-logs/logs-list.blade.php | 43 -- .../connect-provider.blade.php | 74 --- .../check-connection.blade.php | 3 - .../server-settings/edit-server.blade.php | 38 -- .../server-settings/reboot-server.blade.php | 3 - .../add-existing-key.blade.php | 36 -- .../server-ssh-keys/add-new-key.blade.php | 39 -- .../server-keys-list.blade.php | 50 -- .../livewire/servers/create-server.blade.php | 172 ------ .../servers/partials/installing.blade.php | 16 - .../servers/partials/public-key.blade.php | 26 - .../partials/server-overview.blade.php | 41 -- .../livewire/servers/servers-list.blade.php | 42 -- .../livewire/servers/show-server.blade.php | 16 - .../services/install-phpmyadmin.blade.php | 33 -- .../livewire/services/services-list.blade.php | 71 --- .../sites/change-php-version.blade.php | 33 -- .../livewire/sites/create-site.blade.php | 45 -- .../partials/create/fields/branch.blade.php | 7 - .../partials/create/fields/composer.blade.php | 6 - .../create/fields/php-version.blade.php | 17 - .../create/fields/repository.blade.php | 7 - .../create/fields/source-control.blade.php | 17 - .../create/fields/web-directory.blade.php | 10 - .../sites/partials/create/laravel.blade.php | 11 - .../sites/partials/create/php-blank.blade.php | 3 - .../sites/partials/create/php.blade.php | 11 - .../sites/partials/create/wordpress.blade.php | 63 -- .../sites/partials/installing.blade.php | 13 - .../views/livewire/sites/show-site.blade.php | 15 - .../views/livewire/sites/sites-list.blade.php | 42 -- .../update-source-control-provider.blade.php | 27 - .../livewire/sites/update-v-host.blade.php | 34 -- .../source-controls/connect.blade.php | 65 --- .../icons/bitbucket-icon.blade.php | 1 - .../icons/github-icon.blade.php | 1 - .../icons/gitlab-icon.blade.php | 1 - .../views/livewire/ssh-keys/add-key.blade.php | 39 -- .../views/livewire/ssl/create-ssl.blade.php | 54 -- .../views/livewire/ssl/ssls-list.blade.php | 54 -- .../connect-provider.blade.php | 120 ---- .../providers-list.blade.php | 51 -- .../views/livewire/user-dropdown.blade.php | 25 - resources/views/php/index.blade.php | 6 +- .../partials}/default-cli.blade.php | 23 +- .../php/partials/install-extension.blade.php | 49 ++ .../php/partials/install-new-php.blade.php | 22 + .../php/partials/installed-versions.blade.php | 90 +++ .../php/partials/update-php-ini.blade.php | 39 ++ resources/views/profile/index.blade.php | 9 - .../index.blade.php} | 2 +- .../queues/partials/create-queue.blade.php | 110 ++++ .../queues/partials/queues-list.blade.php | 74 +++ .../queues/partials/status.blade.php | 26 +- .../index.blade.php} | 2 +- .../server-logs/partials/logs-list.blade.php | 70 +++ .../views/server-settings/index.blade.php | 40 +- .../partials/check-connection.blade.php | 11 + .../partials/edit-server.blade.php | 71 +++ .../partials/reboot-server.blade.php | 11 + .../partials}/server-details.blade.php | 38 +- .../views/server-ssh-keys/index.blade.php | 2 +- .../partials/add-existing-key.blade.php | 46 ++ .../partials/add-new-key.blade.php | 53 ++ .../partials/server-keys-list.blade.php | 58 ++ .../partials/status.blade.php | 8 +- resources/views/servers/create.blade.php | 2 +- resources/views/servers/index.blade.php | 2 +- .../servers/partials/create-server.blade.php | 251 ++++++++ .../partials}/delete-server.blade.php | 7 +- .../partials/installation-failed.blade.php | 6 +- .../servers/partials/installing.blade.php | 22 + .../servers/partials/public-key.blade.php | 45 ++ .../partials/server-overview.blade.php | 88 +++ .../partials}/server-status.blade.php | 11 +- .../servers/partials/servers-list.blade.php | 48 ++ .../servers/partials/show-server.blade.php | 19 + .../servers/partials/status.blade.php | 11 +- resources/views/servers/show.blade.php | 2 +- resources/views/services/index.blade.php | 2 +- .../services/partials/services-list.blade.php | 65 +++ .../services/partials/status.blade.php | 26 +- .../notification-channels/index.blade.php | 2 +- .../partials/add-channel.blade.php | 114 ++++ .../partials}/channels-list.blade.php | 28 +- .../partials/delete-channel.blade.php | 18 + .../partials}/icons/discord.blade.php | 0 .../partials}/icons/email.blade.php | 0 .../partials}/icons/slack.blade.php | 0 .../partials}/icons/telegram.blade.php | 0 .../views/settings/profile/index.blade.php | 9 + .../two-factor-authentication.blade.php | 28 +- .../partials/update-password.blade.php | 69 +++ .../update-profile-information.blade.php | 80 +++ .../{ => settings}/projects/index.blade.php | 2 +- .../partials/create-project.blade.php | 40 ++ .../partials/delete-project.blade.php | 20 + .../projects/partials/edit-project.blade.php | 47 ++ .../projects/partials/projects-list.blade.php | 40 ++ .../server-providers}/index.blade.php | 2 +- .../partials/connect-provider.blade.php | 97 ++++ .../partials/delete-provider.blade.php | 18 + .../partials}/providers-list.blade.php | 30 +- .../source-controls/index.blade.php | 2 +- .../partials/connect.blade.php | 91 +++ .../partials/delete-source-control.blade.php | 18 + .../partials/icons/bitbucket-icon.blade.php | 1 + .../partials/icons/github-icon.blade.php | 1 + .../partials/icons/gitlab-icon.blade.php | 1 + .../partials}/source-controls-list.blade.php | 26 +- .../{ => settings}/ssh-keys/index.blade.php | 2 +- .../ssh-keys/partials/add-key.blade.php | 50 ++ .../partials/delete-ssh-key.blade.php | 18 + .../ssh-keys/partials}/keys-list.blade.php | 26 +- .../storage-providers}/index.blade.php | 2 +- .../partials/connect-provider.blade.php | 156 +++++ .../delete-storage-provider.blade.php | 18 + .../partials/providers-list.blade.php | 64 ++ .../index.blade.php} | 2 +- resources/views/site-settings/index.blade.php | 20 + .../partials/change-php-version.blade.php | 36 ++ .../partials}/delete-site.blade.php | 7 +- .../partials/update-v-host.blade.php | 40 ++ resources/views/sites/application.blade.php | 11 - resources/views/sites/create.blade.php | 2 +- resources/views/sites/index.blade.php | 2 +- .../sites/partials/create-site.blade.php | 80 +++ .../partials/create/fields/branch.blade.php | 15 + .../partials/create/fields/composer.blade.php | 14 + .../create/fields/php-version.blade.php | 14 + .../create/fields/repository.blade.php | 15 + .../create/fields/source-control.blade.php | 23 + .../create/fields/web-directory.blade.php | 17 + .../sites/partials/create/laravel.blade.php | 11 + .../sites/partials/create/php-blank.blade.php | 3 + .../views/sites/partials/create/php.blade.php | 11 + .../sites/partials/create/wordpress.blade.php | 114 ++++ .../partials/installation-failed.blade.php | 10 +- .../views/sites/partials/installing.blade.php | 19 + .../views/sites/partials/show-site.blade.php | 17 + .../sites/partials/site-overview.blade.php | 12 +- .../partials}/site-status.blade.php | 11 +- .../views/sites/partials/sites-list.blade.php | 48 ++ .../sites/partials/status.blade.php | 11 +- resources/views/sites/settings.blade.php | 20 - resources/views/sites/show.blade.php | 2 +- .../ssl.blade.php => ssls/index.blade.php} | 2 +- .../views/ssls/partials/create-ssl.blade.php | 78 +++ .../views/ssls/partials/ssls-list.blade.php | 61 ++ .../ssl => ssls}/partials/status.blade.php | 11 +- routes/server.php | 135 +++++ routes/settings.php | 66 +++ routes/web.php | 50 +- tests/Feature/ApplicationTest.php | 53 +- tests/Feature/Auth/PasswordUpdateTest.php | 40 -- tests/Feature/CronjobTest.php | 35 +- tests/Feature/DatabaseBackupTest.php | 26 +- tests/Feature/DatabaseTest.php | 36 +- tests/Feature/DatabaseUserTest.php | 39 +- tests/Feature/FirewallTest.php | 50 +- tests/Feature/LogsTest.php | 5 - tests/Feature/NotificationChannelsTest.php | 77 +-- tests/Feature/PHP.php | 110 ---- tests/Feature/PHPTest.php | 132 +++++ tests/Feature/ProfileTest.php | 44 +- tests/Feature/ProjectsTest.php | 44 +- tests/Feature/QueuesTest.php | 50 +- tests/Feature/ServerKeysTest.php | 29 +- tests/Feature/ServerProvidersTest.php | 33 +- tests/Feature/ServerTest.php | 32 +- tests/Feature/ServicesTest.php | 61 +- tests/Feature/SitesTest.php | 124 ++-- tests/Feature/SourceControlsTest.php | 31 +- tests/Feature/SshKeysTest.php | 34 +- tests/Feature/SslTest.php | 36 +- tests/Feature/StorageProvidersTest.php | 33 +- 486 files changed, 8609 insertions(+), 8707 deletions(-) create mode 100644 .prettierrc create mode 100755 app/Actions/CronJob/DeleteCronJob.php create mode 100755 app/Actions/Database/DeleteDatabase.php create mode 100755 app/Actions/Database/DeleteDatabaseUser.php create mode 100755 app/Actions/FirewallRule/DeleteRule.php create mode 100644 app/Actions/PHP/ChangeDefaultCli.php create mode 100644 app/Actions/PHP/GetPHPIni.php create mode 100644 app/Actions/Server/CheckConnection.php create mode 100644 app/Actions/Server/RebootServer.php delete mode 100755 app/Actions/Site/ChangePHPVersion.php delete mode 100755 app/Actions/Site/UpdateSourceControl.php create mode 100755 app/Exceptions/SSHCommandError.php create mode 100644 app/Facades/Toast.php create mode 100644 app/Helpers/HtmxResponse.php create mode 100644 app/Http/Controllers/ApplicationController.php create mode 100644 app/Http/Controllers/DatabaseBackupController.php create mode 100644 app/Http/Controllers/DatabaseUserController.php delete mode 100644 app/Http/Controllers/ProjectController.php create mode 100644 app/Http/Controllers/QueueController.php create mode 100644 app/Http/Controllers/SSLController.php create mode 100644 app/Http/Controllers/ServerLogController.php create mode 100644 app/Http/Controllers/Settings/NotificationChannelController.php create mode 100644 app/Http/Controllers/Settings/ProfileController.php create mode 100644 app/Http/Controllers/Settings/ProjectController.php create mode 100644 app/Http/Controllers/Settings/SSHKeyController.php create mode 100644 app/Http/Controllers/Settings/ServerProviderController.php create mode 100644 app/Http/Controllers/Settings/SourceControlController.php create mode 100644 app/Http/Controllers/Settings/StorageProviderController.php create mode 100644 app/Http/Controllers/SiteLogController.php create mode 100644 app/Http/Controllers/SiteSettingController.php delete mode 100644 app/Http/Livewire/Application/AutoDeployment.php delete mode 100644 app/Http/Livewire/Application/ChangeBranch.php delete mode 100644 app/Http/Livewire/Application/Deploy.php delete mode 100644 app/Http/Livewire/Application/DeploymentScript.php delete mode 100644 app/Http/Livewire/Application/DeploymentsList.php delete mode 100644 app/Http/Livewire/Application/Env.php delete mode 100644 app/Http/Livewire/Application/LaravelApp.php delete mode 100644 app/Http/Livewire/Application/PhpApp.php delete mode 100644 app/Http/Livewire/Application/PhpBlankApp.php delete mode 100644 app/Http/Livewire/Application/WordpressApp.php delete mode 100644 app/Http/Livewire/Broadcast.php delete mode 100644 app/Http/Livewire/Cronjobs/CreateCronjob.php delete mode 100644 app/Http/Livewire/Cronjobs/CronjobsList.php delete mode 100644 app/Http/Livewire/Databases/DatabaseBackupFiles.php delete mode 100644 app/Http/Livewire/Databases/DatabaseBackups.php delete mode 100644 app/Http/Livewire/Databases/DatabaseList.php delete mode 100644 app/Http/Livewire/Databases/DatabaseUserList.php delete mode 100644 app/Http/Livewire/Firewall/CreateFirewallRule.php delete mode 100644 app/Http/Livewire/Firewall/FirewallRulesList.php delete mode 100644 app/Http/Livewire/NotificationChannels/AddChannel.php delete mode 100644 app/Http/Livewire/NotificationChannels/ChannelsList.php delete mode 100644 app/Http/Livewire/Php/DefaultCli.php delete mode 100644 app/Http/Livewire/Php/InstalledVersions.php delete mode 100644 app/Http/Livewire/Profile/TwoFactorAuthentication.php delete mode 100644 app/Http/Livewire/Profile/UpdatePassword.php delete mode 100644 app/Http/Livewire/Profile/UpdateProfileInformation.php delete mode 100644 app/Http/Livewire/Projects/CreateProject.php delete mode 100644 app/Http/Livewire/Projects/EditProject.php delete mode 100644 app/Http/Livewire/Projects/ProjectsList.php delete mode 100644 app/Http/Livewire/Queues/CreateQueue.php delete mode 100644 app/Http/Livewire/Queues/QueuesList.php delete mode 100644 app/Http/Livewire/ServerLogs/LogsList.php delete mode 100644 app/Http/Livewire/ServerProviders/ConnectProvider.php delete mode 100644 app/Http/Livewire/ServerProviders/ProvidersList.php delete mode 100644 app/Http/Livewire/ServerSettings/CheckConnection.php delete mode 100644 app/Http/Livewire/ServerSettings/EditServer.php delete mode 100644 app/Http/Livewire/ServerSettings/RebootServer.php delete mode 100644 app/Http/Livewire/ServerSettings/ServerDetails.php delete mode 100644 app/Http/Livewire/ServerSshKeys/AddExistingKey.php delete mode 100644 app/Http/Livewire/ServerSshKeys/AddNewKey.php delete mode 100644 app/Http/Livewire/ServerSshKeys/ServerKeysList.php delete mode 100644 app/Http/Livewire/Servers/CreateServer.php delete mode 100644 app/Http/Livewire/Servers/DeleteServer.php delete mode 100644 app/Http/Livewire/Servers/ServerStatus.php delete mode 100644 app/Http/Livewire/Servers/ServersList.php delete mode 100644 app/Http/Livewire/Servers/ShowServer.php delete mode 100644 app/Http/Livewire/Services/InstallPHPMyAdmin.php delete mode 100644 app/Http/Livewire/Services/ServicesList.php delete mode 100644 app/Http/Livewire/Sites/ChangePhpVersion.php delete mode 100644 app/Http/Livewire/Sites/CreateSite.php delete mode 100644 app/Http/Livewire/Sites/DeleteSite.php delete mode 100644 app/Http/Livewire/Sites/ShowSite.php delete mode 100644 app/Http/Livewire/Sites/SiteStatus.php delete mode 100644 app/Http/Livewire/Sites/SitesList.php delete mode 100644 app/Http/Livewire/Sites/UpdateSourceControlProvider.php delete mode 100644 app/Http/Livewire/Sites/UpdateVHost.php delete mode 100644 app/Http/Livewire/SourceControls/Connect.php delete mode 100644 app/Http/Livewire/SourceControls/SourceControlsList.php delete mode 100644 app/Http/Livewire/SshKeys/AddKey.php delete mode 100644 app/Http/Livewire/SshKeys/KeysList.php delete mode 100644 app/Http/Livewire/Ssl/CreateSsl.php delete mode 100644 app/Http/Livewire/Ssl/SslsList.php delete mode 100644 app/Http/Livewire/StorageProviders/ConnectProvider.php delete mode 100644 app/Http/Livewire/StorageProviders/ProvidersList.php delete mode 100644 app/Http/Livewire/UserDropdown.php create mode 100644 app/Http/Middleware/HandleSSHErrors.php delete mode 100644 app/Http/Requests/Auth/LoginRequest.php delete mode 100644 app/Http/Requests/ProfileUpdateRequest.php delete mode 100644 app/Jobs/CronJob/AddToServer.php delete mode 100644 app/Jobs/CronJob/RemoveFromServer.php delete mode 100644 app/Jobs/Database/CreateOnServer.php delete mode 100644 app/Jobs/Database/DeleteFromServer.php delete mode 100644 app/Jobs/DatabaseUser/CreateOnServer.php delete mode 100644 app/Jobs/DatabaseUser/DeleteFromServer.php delete mode 100644 app/Jobs/DatabaseUser/LinkUser.php delete mode 100644 app/Jobs/DatabaseUser/UnlinkUser.php delete mode 100644 app/Jobs/Firewall/AddToServer.php delete mode 100644 app/Jobs/Firewall/RemoveFromServer.php delete mode 100644 app/Jobs/PHP/SetDefaultCli.php delete mode 100644 app/Jobs/Server/CheckConnection.php delete mode 100644 app/Jobs/Server/RebootServer.php delete mode 100644 app/Jobs/Site/ChangePHPVersion.php delete mode 100644 app/Traits/HasCustomPaginationView.php delete mode 100644 app/Traits/HasToast.php delete mode 100644 app/Traits/RefreshComponentOnBroadcast.php delete mode 100644 config/livewire.php create mode 100644 public/build/assets/app-5db42bc3.css delete mode 100644 public/build/assets/app-887de6f7.css create mode 100644 public/build/assets/app-c41c626e.js delete mode 100644 public/build/assets/app-e6b0cd9c.js create mode 100644 resources/views/application/auto-deployment.blade.php create mode 100644 resources/views/application/change-branch.blade.php create mode 100644 resources/views/application/deploy.blade.php create mode 100644 resources/views/application/deployment-script.blade.php create mode 100644 resources/views/application/deployments-list.blade.php create mode 100644 resources/views/application/env.blade.php create mode 100644 resources/views/application/laravel-app.blade.php rename resources/views/{livewire => }/application/partials/deployment-status.blade.php (53%) create mode 100644 resources/views/application/php-app.blade.php create mode 100644 resources/views/application/php-blank-app.blade.php create mode 100644 resources/views/application/wordpress-app.blade.php delete mode 100644 resources/views/components/confirm-modal.blade.php create mode 100644 resources/views/components/confirmation-modal.blade.php create mode 100644 resources/views/components/htmx-error-handler.blade.php create mode 100644 resources/views/components/live.blade.php create mode 100644 resources/views/components/user-dropdown.blade.php create mode 100644 resources/views/cronjobs/partrials/create-cronjob.blade.php create mode 100644 resources/views/cronjobs/partrials/cronjobs-list.blade.php rename resources/views/{livewire/server-ssh-keys/partials => cronjobs/partrials}/status.blade.php (54%) rename resources/views/{livewire => }/databases/partials/backup-file-status.blade.php (52%) rename resources/views/{livewire => }/databases/partials/backup-status.blade.php (55%) create mode 100644 resources/views/databases/partials/create-backup-modal.blade.php create mode 100644 resources/views/databases/partials/create-database-modal.blade.php create mode 100644 resources/views/databases/partials/create-database-user-modal.blade.php create mode 100644 resources/views/databases/partials/database-backup-files.blade.php create mode 100644 resources/views/databases/partials/database-backups.blade.php create mode 100644 resources/views/databases/partials/database-list.blade.php rename resources/views/{livewire => }/databases/partials/database-status.blade.php (54%) create mode 100644 resources/views/databases/partials/database-user-list.blade.php create mode 100644 resources/views/databases/partials/database-user-password-modal.blade.php rename resources/views/{livewire => }/databases/partials/database-user-status.blade.php (52%) create mode 100644 resources/views/databases/partials/link-database-user-modal.blade.php create mode 100644 resources/views/databases/partials/restore-backup-modal.blade.php create mode 100644 resources/views/firewall/partials/create-firewall-rule.blade.php create mode 100644 resources/views/firewall/partials/firewall-rules-list.blade.php rename resources/views/{livewire => }/firewall/partials/status.blade.php (52%) delete mode 100644 resources/views/livewire/application/auto-deployment.blade.php delete mode 100644 resources/views/livewire/application/change-branch.blade.php delete mode 100644 resources/views/livewire/application/deploy.blade.php delete mode 100644 resources/views/livewire/application/deployment-script.blade.php delete mode 100644 resources/views/livewire/application/deployments-list.blade.php delete mode 100644 resources/views/livewire/application/env.blade.php delete mode 100644 resources/views/livewire/application/laravel-app.blade.php delete mode 100644 resources/views/livewire/application/php-app.blade.php delete mode 100644 resources/views/livewire/application/php-blank-app.blade.php delete mode 100644 resources/views/livewire/application/wordpress-app.blade.php delete mode 100644 resources/views/livewire/broadcast.blade.php delete mode 100644 resources/views/livewire/cronjobs/create-cronjob.blade.php delete mode 100644 resources/views/livewire/cronjobs/cronjobs-list.blade.php delete mode 100644 resources/views/livewire/databases/database-backup-files.blade.php delete mode 100644 resources/views/livewire/databases/database-backups.blade.php delete mode 100644 resources/views/livewire/databases/database-list.blade.php delete mode 100644 resources/views/livewire/databases/database-user-list.blade.php delete mode 100644 resources/views/livewire/databases/partials/create-backup-modal.blade.php delete mode 100644 resources/views/livewire/databases/partials/create-database-modal.blade.php delete mode 100644 resources/views/livewire/databases/partials/create-database-user-modal.blade.php delete mode 100644 resources/views/livewire/databases/partials/database-user-password-modal.blade.php delete mode 100644 resources/views/livewire/databases/partials/delete-backup-file-modal.blade.php delete mode 100644 resources/views/livewire/databases/partials/delete-backup-modal.blade.php delete mode 100644 resources/views/livewire/databases/partials/delete-database-modal.blade.php delete mode 100644 resources/views/livewire/databases/partials/delete-database-user-modal.blade.php delete mode 100644 resources/views/livewire/databases/partials/link-database-user-modal.blade.php delete mode 100644 resources/views/livewire/databases/partials/restore-backup-modal.blade.php delete mode 100644 resources/views/livewire/firewall/create-firewall-rule.blade.php delete mode 100644 resources/views/livewire/firewall/firewall-rules-list.blade.php delete mode 100644 resources/views/livewire/notification-channels/add-channel.blade.php delete mode 100644 resources/views/livewire/partials/pagination.blade.php delete mode 100644 resources/views/livewire/php/installed-versions.blade.php delete mode 100644 resources/views/livewire/php/partials/install-extension.blade.php delete mode 100644 resources/views/livewire/php/partials/install-new-php.blade.php delete mode 100644 resources/views/livewire/php/partials/uninstall-php.blade.php delete mode 100644 resources/views/livewire/php/partials/update-php-ini.blade.php delete mode 100644 resources/views/livewire/profile/update-password.blade.php delete mode 100644 resources/views/livewire/profile/update-profile-information.blade.php delete mode 100644 resources/views/livewire/projects/create-project.blade.php delete mode 100644 resources/views/livewire/projects/edit-project.blade.php delete mode 100644 resources/views/livewire/projects/projects-list.blade.php delete mode 100644 resources/views/livewire/queues/create-queue.blade.php delete mode 100644 resources/views/livewire/queues/queues-list.blade.php delete mode 100644 resources/views/livewire/server-logs/logs-list.blade.php delete mode 100644 resources/views/livewire/server-providers/connect-provider.blade.php delete mode 100644 resources/views/livewire/server-settings/check-connection.blade.php delete mode 100644 resources/views/livewire/server-settings/edit-server.blade.php delete mode 100644 resources/views/livewire/server-settings/reboot-server.blade.php delete mode 100644 resources/views/livewire/server-ssh-keys/add-existing-key.blade.php delete mode 100644 resources/views/livewire/server-ssh-keys/add-new-key.blade.php delete mode 100644 resources/views/livewire/server-ssh-keys/server-keys-list.blade.php delete mode 100644 resources/views/livewire/servers/create-server.blade.php delete mode 100644 resources/views/livewire/servers/partials/installing.blade.php delete mode 100644 resources/views/livewire/servers/partials/public-key.blade.php delete mode 100644 resources/views/livewire/servers/partials/server-overview.blade.php delete mode 100644 resources/views/livewire/servers/servers-list.blade.php delete mode 100644 resources/views/livewire/servers/show-server.blade.php delete mode 100644 resources/views/livewire/services/install-phpmyadmin.blade.php delete mode 100644 resources/views/livewire/services/services-list.blade.php delete mode 100644 resources/views/livewire/sites/change-php-version.blade.php delete mode 100644 resources/views/livewire/sites/create-site.blade.php delete mode 100644 resources/views/livewire/sites/partials/create/fields/branch.blade.php delete mode 100644 resources/views/livewire/sites/partials/create/fields/composer.blade.php delete mode 100644 resources/views/livewire/sites/partials/create/fields/php-version.blade.php delete mode 100644 resources/views/livewire/sites/partials/create/fields/repository.blade.php delete mode 100644 resources/views/livewire/sites/partials/create/fields/source-control.blade.php delete mode 100644 resources/views/livewire/sites/partials/create/fields/web-directory.blade.php delete mode 100644 resources/views/livewire/sites/partials/create/laravel.blade.php delete mode 100644 resources/views/livewire/sites/partials/create/php-blank.blade.php delete mode 100644 resources/views/livewire/sites/partials/create/php.blade.php delete mode 100644 resources/views/livewire/sites/partials/create/wordpress.blade.php delete mode 100644 resources/views/livewire/sites/partials/installing.blade.php delete mode 100644 resources/views/livewire/sites/show-site.blade.php delete mode 100644 resources/views/livewire/sites/sites-list.blade.php delete mode 100644 resources/views/livewire/sites/update-source-control-provider.blade.php delete mode 100644 resources/views/livewire/sites/update-v-host.blade.php delete mode 100644 resources/views/livewire/source-controls/connect.blade.php delete mode 100644 resources/views/livewire/source-controls/icons/bitbucket-icon.blade.php delete mode 100644 resources/views/livewire/source-controls/icons/github-icon.blade.php delete mode 100644 resources/views/livewire/source-controls/icons/gitlab-icon.blade.php delete mode 100644 resources/views/livewire/ssh-keys/add-key.blade.php delete mode 100644 resources/views/livewire/ssl/create-ssl.blade.php delete mode 100644 resources/views/livewire/ssl/ssls-list.blade.php delete mode 100644 resources/views/livewire/storage-providers/connect-provider.blade.php delete mode 100644 resources/views/livewire/storage-providers/providers-list.blade.php delete mode 100644 resources/views/livewire/user-dropdown.blade.php rename resources/views/{livewire/php => php/partials}/default-cli.blade.php (55%) create mode 100644 resources/views/php/partials/install-extension.blade.php create mode 100644 resources/views/php/partials/install-new-php.blade.php create mode 100644 resources/views/php/partials/installed-versions.blade.php create mode 100644 resources/views/php/partials/update-php-ini.blade.php delete mode 100644 resources/views/profile/index.blade.php rename resources/views/{sites/queues.blade.php => queues/index.blade.php} (67%) create mode 100644 resources/views/queues/partials/create-queue.blade.php create mode 100644 resources/views/queues/partials/queues-list.blade.php rename resources/views/{livewire => }/queues/partials/status.blade.php (55%) rename resources/views/{servers/logs.blade.php => server-logs/index.blade.php} (68%) create mode 100644 resources/views/server-logs/partials/logs-list.blade.php create mode 100644 resources/views/server-settings/partials/check-connection.blade.php create mode 100644 resources/views/server-settings/partials/edit-server.blade.php create mode 100644 resources/views/server-settings/partials/reboot-server.blade.php rename resources/views/{livewire/server-settings => server-settings/partials}/server-details.blade.php (57%) create mode 100644 resources/views/server-ssh-keys/partials/add-existing-key.blade.php create mode 100644 resources/views/server-ssh-keys/partials/add-new-key.blade.php create mode 100644 resources/views/server-ssh-keys/partials/server-keys-list.blade.php rename resources/views/{livewire/cronjobs => server-ssh-keys}/partials/status.blade.php (55%) create mode 100644 resources/views/servers/partials/create-server.blade.php rename resources/views/{livewire/servers => servers/partials}/delete-server.blade.php (62%) rename resources/views/{livewire => }/servers/partials/installation-failed.blade.php (72%) create mode 100644 resources/views/servers/partials/installing.blade.php create mode 100644 resources/views/servers/partials/public-key.blade.php create mode 100644 resources/views/servers/partials/server-overview.blade.php rename resources/views/{livewire/servers => servers/partials}/server-status.blade.php (55%) create mode 100644 resources/views/servers/partials/servers-list.blade.php create mode 100644 resources/views/servers/partials/show-server.blade.php rename resources/views/{livewire => }/servers/partials/status.blade.php (53%) create mode 100644 resources/views/services/partials/services-list.blade.php rename resources/views/{livewire => }/services/partials/status.blade.php (53%) rename resources/views/{ => settings}/notification-channels/index.blade.php (61%) create mode 100644 resources/views/settings/notification-channels/partials/add-channel.blade.php rename resources/views/{livewire/notification-channels => settings/notification-channels/partials}/channels-list.blade.php (58%) create mode 100644 resources/views/settings/notification-channels/partials/delete-channel.blade.php rename resources/views/{livewire/notification-channels => settings/notification-channels/partials}/icons/discord.blade.php (100%) rename resources/views/{livewire/notification-channels => settings/notification-channels/partials}/icons/email.blade.php (100%) rename resources/views/{livewire/notification-channels => settings/notification-channels/partials}/icons/slack.blade.php (100%) rename resources/views/{livewire/notification-channels => settings/notification-channels/partials}/icons/telegram.blade.php (100%) create mode 100644 resources/views/settings/profile/index.blade.php rename resources/views/{livewire/profile => settings/profile/partials}/two-factor-authentication.blade.php (60%) create mode 100644 resources/views/settings/profile/partials/update-password.blade.php create mode 100644 resources/views/settings/profile/partials/update-profile-information.blade.php rename resources/views/{ => settings}/projects/index.blade.php (63%) create mode 100644 resources/views/settings/projects/partials/create-project.blade.php create mode 100644 resources/views/settings/projects/partials/delete-project.blade.php create mode 100644 resources/views/settings/projects/partials/edit-project.blade.php create mode 100644 resources/views/settings/projects/partials/projects-list.blade.php rename resources/views/{storage-providers => settings/server-providers}/index.blade.php (62%) create mode 100644 resources/views/settings/server-providers/partials/connect-provider.blade.php create mode 100644 resources/views/settings/server-providers/partials/delete-provider.blade.php rename resources/views/{livewire/server-providers => settings/server-providers/partials}/providers-list.blade.php (59%) rename resources/views/{ => settings}/source-controls/index.blade.php (59%) create mode 100644 resources/views/settings/source-controls/partials/connect.blade.php create mode 100644 resources/views/settings/source-controls/partials/delete-source-control.blade.php create mode 100644 resources/views/settings/source-controls/partials/icons/bitbucket-icon.blade.php create mode 100644 resources/views/settings/source-controls/partials/icons/github-icon.blade.php create mode 100644 resources/views/settings/source-controls/partials/icons/gitlab-icon.blade.php rename resources/views/{livewire/source-controls => settings/source-controls/partials}/source-controls-list.blade.php (62%) rename resources/views/{ => settings}/ssh-keys/index.blade.php (65%) create mode 100644 resources/views/settings/ssh-keys/partials/add-key.blade.php create mode 100644 resources/views/settings/ssh-keys/partials/delete-ssh-key.blade.php rename resources/views/{livewire/ssh-keys => settings/ssh-keys/partials}/keys-list.blade.php (61%) rename resources/views/{server-providers => settings/storage-providers}/index.blade.php (61%) create mode 100644 resources/views/settings/storage-providers/partials/connect-provider.blade.php create mode 100644 resources/views/settings/storage-providers/partials/delete-storage-provider.blade.php create mode 100644 resources/views/settings/storage-providers/partials/providers-list.blade.php rename resources/views/{sites/logs.blade.php => site-logs/index.blade.php} (52%) create mode 100644 resources/views/site-settings/index.blade.php create mode 100644 resources/views/site-settings/partials/change-php-version.blade.php rename resources/views/{livewire/sites => site-settings/partials}/delete-site.blade.php (58%) create mode 100644 resources/views/site-settings/partials/update-v-host.blade.php delete mode 100644 resources/views/sites/application.blade.php create mode 100644 resources/views/sites/partials/create-site.blade.php create mode 100644 resources/views/sites/partials/create/fields/branch.blade.php create mode 100644 resources/views/sites/partials/create/fields/composer.blade.php create mode 100644 resources/views/sites/partials/create/fields/php-version.blade.php create mode 100644 resources/views/sites/partials/create/fields/repository.blade.php create mode 100644 resources/views/sites/partials/create/fields/source-control.blade.php create mode 100644 resources/views/sites/partials/create/fields/web-directory.blade.php create mode 100644 resources/views/sites/partials/create/laravel.blade.php create mode 100644 resources/views/sites/partials/create/php-blank.blade.php create mode 100644 resources/views/sites/partials/create/php.blade.php create mode 100644 resources/views/sites/partials/create/wordpress.blade.php rename resources/views/{livewire => }/sites/partials/installation-failed.blade.php (51%) create mode 100644 resources/views/sites/partials/installing.blade.php create mode 100644 resources/views/sites/partials/show-site.blade.php rename resources/views/{livewire => }/sites/partials/site-overview.blade.php (76%) rename resources/views/{livewire/sites => sites/partials}/site-status.blade.php (56%) create mode 100644 resources/views/sites/partials/sites-list.blade.php rename resources/views/{livewire => }/sites/partials/status.blade.php (54%) delete mode 100644 resources/views/sites/settings.blade.php rename resources/views/{sites/ssl.blade.php => ssls/index.blade.php} (69%) create mode 100644 resources/views/ssls/partials/create-ssl.blade.php create mode 100644 resources/views/ssls/partials/ssls-list.blade.php rename resources/views/{livewire/ssl => ssls}/partials/status.blade.php (56%) create mode 100644 routes/server.php create mode 100644 routes/settings.php delete mode 100644 tests/Feature/Auth/PasswordUpdateTest.php delete mode 100644 tests/Feature/PHP.php create mode 100644 tests/Feature/PHPTest.php diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..a2ddf73 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,15 @@ +{ + "plugins": ["prettier-plugin-blade", "prettier-plugin-tailwindcss"], + "overrides": [ + { + "files": ["*.blade.php"], + "options": { + "parser": "blade", + "printWidth": 120, + "htmlWhitespaceSensitivity": "ignore", + "tabWidth": 4, + "quoteProps": "consistent" + } + } + ] +} diff --git a/README.md b/README.md index 02c1f4b..abbabf4 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,6 @@ ## Credits - Laravel - Tailwindcss -- Livewire - Alpinejs - Vite - Laravel Enum by BenSampo diff --git a/app/Actions/CronJob/CreateCronJob.php b/app/Actions/CronJob/CreateCronJob.php index ff5262a..05fc131 100755 --- a/app/Actions/CronJob/CreateCronJob.php +++ b/app/Actions/CronJob/CreateCronJob.php @@ -5,12 +5,17 @@ use App\Enums\CronjobStatus; use App\Models\CronJob; use App\Models\Server; +use App\SSHCommands\CronJob\UpdateCronJobsCommand; use App\ValidationRules\CronRule; use Illuminate\Support\Facades\Validator; use Illuminate\Validation\ValidationException; +use Throwable; class CreateCronJob { + /** + * @throws Throwable + */ public function create(Server $server, array $input): void { $this->validate($input); @@ -19,11 +24,17 @@ public function create(Server $server, array $input): void 'server_id' => $server->id, 'user' => $input['user'], 'command' => $input['command'], - 'frequency' => $input['frequency'], + 'frequency' => $input['frequency'] == 'custom' ? $input['custom'] : $input['frequency'], 'status' => CronjobStatus::CREATING, ]); $cronJob->save(); - $cronJob->addToServer(); + + $server->ssh()->exec( + new UpdateCronJobsCommand($cronJob->user, CronJob::crontab($server, $cronJob->user)), + 'update-crontab' + ); + $cronJob->status = CronjobStatus::READY; + $cronJob->save(); } /** @@ -41,8 +52,17 @@ private function validate(array $input): void ], 'frequency' => [ 'required', - new CronRule(), + new CronRule(acceptCustom: true), ], - ])->validateWithBag('createCronJob'); + ])->validate(); + + if ($input['frequency'] == 'custom') { + Validator::make($input, [ + 'custom' => [ + 'required', + new CronRule(), + ], + ])->validate(); + } } } diff --git a/app/Actions/CronJob/DeleteCronJob.php b/app/Actions/CronJob/DeleteCronJob.php new file mode 100755 index 0000000..b579f85 --- /dev/null +++ b/app/Actions/CronJob/DeleteCronJob.php @@ -0,0 +1,24 @@ +user; + $cronJob->delete(); + $server->ssh()->exec( + new UpdateCronJobsCommand($user, CronJob::crontab($server, $user)), + 'update-crontab' + ); + } +} diff --git a/app/Actions/Database/CreateBackup.php b/app/Actions/Database/CreateBackup.php index 3acbd01..c31038f 100644 --- a/app/Actions/Database/CreateBackup.php +++ b/app/Actions/Database/CreateBackup.php @@ -24,10 +24,10 @@ public function create($type, Server $server, array $input): Backup $backup = new Backup([ 'type' => $type, 'server_id' => $server->id, - 'database_id' => $input['database'] ?? null, - 'storage_id' => $input['storage'], - 'interval' => $input['interval'] == 'custom' ? $input['custom'] : $input['interval'], - 'keep_backups' => $input['keep'], + 'database_id' => $input['backup_database'] ?? null, + 'storage_id' => $input['backup_storage'], + 'interval' => $input['backup_interval'] == 'custom' ? $input['backup_custom'] : $input['backup_interval'], + 'keep_backups' => $input['backup_keep'], 'status' => BackupStatus::RUNNING, ]); $backup->save(); @@ -43,16 +43,16 @@ public function create($type, Server $server, array $input): Backup private function validate($type, Server $server, array $input): void { $rules = [ - 'storage' => [ + 'backup_storage' => [ 'required', Rule::exists('storage_providers', 'id'), ], - 'keep' => [ + 'backup_keep' => [ 'required', 'numeric', 'min:1', ], - 'interval' => [ + 'backup_interval' => [ 'required', Rule::in([ '0 * * * *', @@ -63,13 +63,13 @@ private function validate($type, Server $server, array $input): void ]), ], ]; - if ($input['interval'] == 'custom') { - $rules['custom'] = [ + if ($input['backup_interval'] == 'custom') { + $rules['backup_custom'] = [ 'required', ]; } if ($type === 'database') { - $rules['database'] = [ + $rules['backup_database'] = [ 'required', Rule::exists('databases', 'id') ->where('server_id', $server->id) diff --git a/app/Actions/Database/CreateDatabase.php b/app/Actions/Database/CreateDatabase.php index 7136035..58e3fd9 100755 --- a/app/Actions/Database/CreateDatabase.php +++ b/app/Actions/Database/CreateDatabase.php @@ -2,6 +2,7 @@ namespace App\Actions\Database; +use App\Enums\DatabaseStatus; use App\Models\Database; use App\Models\Server; use Illuminate\Support\Facades\Validator; @@ -22,7 +23,10 @@ public function create(Server $server, array $input): Database 'name' => $input['name'], ]); $database->save(); - $database->createOnServer(); + + $server->database()->handler()->create($database->name); + $database->status = DatabaseStatus::READY; + $database->save(); return $database; } diff --git a/app/Actions/Database/CreateDatabaseUser.php b/app/Actions/Database/CreateDatabaseUser.php index 0521c45..d22345e 100755 --- a/app/Actions/Database/CreateDatabaseUser.php +++ b/app/Actions/Database/CreateDatabaseUser.php @@ -2,6 +2,7 @@ namespace App\Actions\Database; +use App\Enums\DatabaseUserStatus; use App\Models\DatabaseUser; use App\Models\Server; use Illuminate\Support\Facades\Validator; @@ -25,7 +26,18 @@ public function create(Server $server, array $input, array $links = []): Databas 'databases' => $links, ]); $databaseUser->save(); - $databaseUser->createOnServer(); + + $server->database()->handler()->createUser( + $databaseUser->username, + $databaseUser->password, + $databaseUser->host + ); + $databaseUser->status = DatabaseUserStatus::READY; + $databaseUser->save(); + + if (count($databaseUser->databases) > 0) { + app(LinkUser::class)->link($databaseUser, $databaseUser->databases); + } return $databaseUser; } diff --git a/app/Actions/Database/DeleteDatabase.php b/app/Actions/Database/DeleteDatabase.php new file mode 100755 index 0000000..c8d49f8 --- /dev/null +++ b/app/Actions/Database/DeleteDatabase.php @@ -0,0 +1,15 @@ +database()->handler()->delete($database->name); + $database->delete(); + } +} diff --git a/app/Actions/Database/DeleteDatabaseUser.php b/app/Actions/Database/DeleteDatabaseUser.php new file mode 100755 index 0000000..8b70433 --- /dev/null +++ b/app/Actions/Database/DeleteDatabaseUser.php @@ -0,0 +1,15 @@ +database()->handler()->deleteUser($databaseUser->username, $databaseUser->host); + $databaseUser->delete(); + } +} diff --git a/app/Actions/Database/LinkUser.php b/app/Actions/Database/LinkUser.php index c6a9bcf..166afc2 100755 --- a/app/Actions/Database/LinkUser.php +++ b/app/Actions/Database/LinkUser.php @@ -4,6 +4,9 @@ use App\Models\Database; use App\Models\DatabaseUser; +use App\Models\Server; +use Illuminate\Support\Facades\Validator; +use Illuminate\Validation\Rule; use Illuminate\Validation\ValidationException; class LinkUser @@ -11,20 +14,50 @@ class LinkUser /** * @throws ValidationException */ - public function link(DatabaseUser $databaseUser, array $databases): void + public function link(DatabaseUser $databaseUser, array $input): void { + if (! isset($input['databases']) || ! is_array($input['databases'])) { + $input['databases'] = []; + } + + $this->validate($databaseUser->server, $input); + $dbs = Database::query() ->where('server_id', $databaseUser->server_id) - ->whereIn('name', $databases) + ->whereIn('name', $input['databases']) ->count(); - if (count($databases) !== $dbs) { + if (count($input['databases']) !== $dbs) { throw ValidationException::withMessages(['databases' => __('Databases not found!')]) ->errorBag('linkUser'); } - $databaseUser->databases = $databases; - $databaseUser->unlinkUser(); - $databaseUser->linkUser(); + $databaseUser->databases = $input['databases']; + + // Unlink the user from all databases + $databaseUser->server->database()->handler()->unlink( + $databaseUser->username, + $databaseUser->host + ); + + // Link the user to the selected databases + $databaseUser->server->database()->handler()->link( + $databaseUser->username, + $databaseUser->host, + $databaseUser->databases + ); + $databaseUser->save(); } + + private function validate(Server $server, array $input): void + { + $rules = [ + 'databases.*' => [ + 'required', + Rule::exists('databases', 'name')->where('server_id', $server->id), + ], + ]; + + Validator::make($input, $rules)->validate(); + } } diff --git a/app/Actions/FirewallRule/CreateRule.php b/app/Actions/FirewallRule/CreateRule.php index 135f352..01670e9 100755 --- a/app/Actions/FirewallRule/CreateRule.php +++ b/app/Actions/FirewallRule/CreateRule.php @@ -24,7 +24,19 @@ public function create(Server $server, array $input): FirewallRule 'status' => FirewallRuleStatus::CREATING, ]); $rule->save(); - $rule->addToServer(); + + $server->firewall() + ->handler() + ->addRule( + $rule->type, + $rule->real_protocol, + $rule->port, + $rule->source, + $rule->mask + ); + + $rule->status = FirewallRuleStatus::READY; + $rule->save(); return $rule; } @@ -56,6 +68,6 @@ private function validate(Server $server, array $input): void 'mask' => [ 'numeric', ], - ])->validateWithBag('createRule'); + ])->validate(); } } diff --git a/app/Actions/FirewallRule/DeleteRule.php b/app/Actions/FirewallRule/DeleteRule.php new file mode 100755 index 0000000..d9addf8 --- /dev/null +++ b/app/Actions/FirewallRule/DeleteRule.php @@ -0,0 +1,28 @@ +status = FirewallRuleStatus::DELETING; + $rule->save(); + + $server->firewall() + ->handler() + ->removeRule( + $rule->type, + $rule->real_protocol, + $rule->port, + $rule->source, + $rule->mask + ); + + $rule->delete(); + } +} diff --git a/app/Actions/PHP/ChangeDefaultCli.php b/app/Actions/PHP/ChangeDefaultCli.php new file mode 100644 index 0000000..ff60476 --- /dev/null +++ b/app/Actions/PHP/ChangeDefaultCli.php @@ -0,0 +1,29 @@ +validate($server, $input); + $service = $server->php($input['version']); + $service->handler()->setDefaultCli(); + $server->defaultService('php')->update(['is_default' => 0]); + $service->update(['is_default' => 1]); + $service->update(['status' => ServiceStatus::READY]); + } + + public function validate(Server $server, array $input): void + { + if (! isset($input['version']) || ! in_array($input['version'], $server->installedPHPVersions())) { + throw ValidationException::withMessages( + ['version' => __('This version is not installed')] + ); + } + } +} diff --git a/app/Actions/PHP/GetPHPIni.php b/app/Actions/PHP/GetPHPIni.php new file mode 100644 index 0000000..1500ae4 --- /dev/null +++ b/app/Actions/PHP/GetPHPIni.php @@ -0,0 +1,32 @@ +validate($server, $input); + + try { + return $server->ssh()->exec(new GetPHPIniCommand($input['version'])); + } catch (\Throwable $e) { + throw ValidationException::withMessages( + ['ini' => $e->getMessage()] + ); + } + } + + public function validate(Server $server, array $input): void + { + if (! isset($input['version']) || ! in_array($input['version'], $server->installedPHPVersions())) { + throw ValidationException::withMessages( + ['version' => __('This version is not installed')] + ); + } + } +} diff --git a/app/Actions/PHP/InstallNewPHP.php b/app/Actions/PHP/InstallNewPHP.php index fe75a05..0609f94 100755 --- a/app/Actions/PHP/InstallNewPHP.php +++ b/app/Actions/PHP/InstallNewPHP.php @@ -41,12 +41,12 @@ private function validate(Server $server, array $input): void 'required', Rule::in(config('core.php_versions')), ], - ])->validateWithBag('installPHP'); + ])->validate(); if (in_array($input['version'], $server->installedPHPVersions())) { throw ValidationException::withMessages( ['version' => __('This version is already installed')] - )->errorBag('installPHP'); + ); } } } diff --git a/app/Actions/PHP/InstallPHPExtension.php b/app/Actions/PHP/InstallPHPExtension.php index f310fa3..52eb189 100755 --- a/app/Actions/PHP/InstallPHPExtension.php +++ b/app/Actions/PHP/InstallPHPExtension.php @@ -2,8 +2,10 @@ namespace App\Actions\PHP; +use App\Models\Server; use App\Models\Service; use Illuminate\Support\Facades\Validator; +use Illuminate\Validation\Rule; use Illuminate\Validation\ValidationException; class InstallPHPExtension @@ -11,16 +13,18 @@ class InstallPHPExtension /** * @throws ValidationException */ - public function handle(Service $service, array $input): Service + public function install(Server $server, array $input): Service { + $this->validate($server, $input); + + /** @var Service $service */ + $service = $server->php($input['version']); $typeData = $service->type_data; $typeData['extensions'] = $typeData['extensions'] ?? []; $service->type_data = $typeData; $service->save(); - $this->validate($service, $input); - - $service->handler()->installExtension($input['name']); + $service->handler()->installExtension($input['extension']); return $service; } @@ -28,18 +32,25 @@ public function handle(Service $service, array $input): Service /** * @throws ValidationException */ - private function validate(Service $service, array $input): void + private function validate(Server $server, array $input): void { Validator::make($input, [ - 'name' => [ + 'extension' => [ 'required', 'in:'.implode(',', config('core.php_extensions')), ], - ])->validateWithBag('installPHPExtension'); + 'version' => [ + 'required', + Rule::in($server->installedPHPVersions()), + ], + ])->validate(); - if (in_array($input['name'], $service->type_data['extensions'])) { + /** @var Service $service */ + $service = $server->php($input['version']); + + if (in_array($input['extension'], $service->type_data['extensions'])) { throw ValidationException::withMessages( - ['name' => __('This extension already installed')] + ['extension' => __('This extension already installed')] )->errorBag('installPHPExtension'); } } diff --git a/app/Actions/PHP/UninstallPHP.php b/app/Actions/PHP/UninstallPHP.php index 2596545..55b77df 100755 --- a/app/Actions/PHP/UninstallPHP.php +++ b/app/Actions/PHP/UninstallPHP.php @@ -4,16 +4,17 @@ use App\Models\Server; use App\Models\Service; +use Illuminate\Support\Facades\Validator; use Illuminate\Validation\ValidationException; class UninstallPHP { - public function uninstall(Server $server, string $version): void + public function uninstall(Server $server, array $input): void { - $this->validate($server, $version); + $this->validate($server, $input); /** @var Service $php */ - $php = $server->services()->where('type', 'php')->where('version', $version)->first(); + $php = $server->php($input['version']); $php->uninstall(); } @@ -21,17 +22,19 @@ public function uninstall(Server $server, string $version): void /** * @throws ValidationException */ - private function validate(Server $server, string $version): void + private function validate(Server $server, array $input): void { - $php = $server->services()->where('type', 'php')->where('version', $version)->first(); + Validator::make($input, [ + 'version' => 'required|string', + ])->validate(); - if (! $php) { + if (! in_array($input['version'], $server->installedPHPVersions())) { throw ValidationException::withMessages( - ['version' => __('This version has not been installed yet!')] + ['version' => __('This version is not installed')] ); } - $hasSite = $server->sites()->where('php_version', $version)->first(); + $hasSite = $server->sites()->where('php_version', $input['version'])->first(); if ($hasSite) { throw ValidationException::withMessages( ['version' => __('Cannot uninstall this version because some sites are using it!')] diff --git a/app/Actions/PHP/UpdatePHPIni.php b/app/Actions/PHP/UpdatePHPIni.php index fd9392b..2f253d9 100755 --- a/app/Actions/PHP/UpdatePHPIni.php +++ b/app/Actions/PHP/UpdatePHPIni.php @@ -2,8 +2,9 @@ namespace App\Actions\PHP; -use App\Models\Service; +use App\Models\Server; use Illuminate\Support\Facades\Storage; +use Illuminate\Support\Facades\Validator; use Illuminate\Support\Str; use Illuminate\Validation\ValidationException; use Throwable; @@ -13,14 +14,18 @@ class UpdatePHPIni /** * @throws ValidationException */ - public function update(Service $service, string $ini): void + public function update(Server $server, array $input): void { + $this->validate($server, $input); + + $service = $server->php($input['version']); + $tmpName = Str::random(10).strtotime('now'); try { /** @var \Illuminate\Filesystem\FilesystemAdapter $storageDisk */ $storageDisk = Storage::disk('local'); - $storageDisk->put($tmpName, $ini); + $storageDisk->put($tmpName, $input['ini']); $service->server->ssh('root')->upload( $storageDisk->path($tmpName), "/etc/php/$service->version/cli/php.ini" @@ -42,4 +47,21 @@ private function deleteTempFile(string $name): void Storage::disk('local')->delete($name); } } + + public function validate(Server $server, array $input): void + { + Validator::make($input, [ + 'ini' => [ + 'required', + 'string', + ], + 'version' => 'required|string', + ])->validate(); + + if (! in_array($input['version'], $server->installedPHPVersions())) { + throw ValidationException::withMessages( + ['version' => __('This version is not installed')] + ); + } + } } diff --git a/app/Actions/Projects/DeleteProject.php b/app/Actions/Projects/DeleteProject.php index 05b4507..aa4e53d 100644 --- a/app/Actions/Projects/DeleteProject.php +++ b/app/Actions/Projects/DeleteProject.php @@ -8,11 +8,8 @@ class DeleteProject { - public function delete(User $user, int $projectId): void + public function delete(User $user, Project $project): void { - /** @var Project $project */ - $project = $user->projects()->findOrFail($projectId); - if ($user->projects()->count() === 1) { throw ValidationException::withMessages([ 'project' => __('Cannot delete the last project.'), diff --git a/app/Actions/Projects/UpdateProject.php b/app/Actions/Projects/UpdateProject.php index 0d5ecdd..132cf25 100644 --- a/app/Actions/Projects/UpdateProject.php +++ b/app/Actions/Projects/UpdateProject.php @@ -26,7 +26,7 @@ private function validate(Project $project, array $input): void 'required', 'string', 'max:255', - Rule::unique('projects')->ignore($project->id), + Rule::unique('projects')->where('user_id', $project->user_id)->ignore($project->id), ], ])->validate(); } diff --git a/app/Actions/Queue/CreateQueue.php b/app/Actions/Queue/CreateQueue.php index 42b15ce..32182b3 100644 --- a/app/Actions/Queue/CreateQueue.php +++ b/app/Actions/Queue/CreateQueue.php @@ -60,6 +60,6 @@ protected function validate(array $input): void ], ]; - Validator::make($input, $rules)->validateWithBag('createQueue'); + Validator::make($input, $rules)->validate(); } } diff --git a/app/Actions/SSL/CreateSSL.php b/app/Actions/SSL/CreateSSL.php index 18076da..5197f0e 100644 --- a/app/Actions/SSL/CreateSSL.php +++ b/app/Actions/SSL/CreateSSL.php @@ -42,6 +42,6 @@ protected function validate(array $input): void $rules['private'] = 'required'; } - Validator::make($input, $rules)->validateWithBag('createSSL'); + Validator::make($input, $rules)->validate(); } } diff --git a/app/Actions/Server/CheckConnection.php b/app/Actions/Server/CheckConnection.php new file mode 100644 index 0000000..444d5c1 --- /dev/null +++ b/app/Actions/Server/CheckConnection.php @@ -0,0 +1,30 @@ +status; + try { + $server->ssh()->connect(); + $server->refresh(); + if ($status == 'disconnected') { + $server->status = 'ready'; + $server->save(); + } + } catch (Throwable) { + $server->status = 'disconnected'; + $server->save(); + Notifier::send($server, new ServerDisconnected($server)); + } + + return $server; + } +} diff --git a/app/Actions/Server/CreateServer.php b/app/Actions/Server/CreateServer.php index c1aabdb..c64d8c5 100755 --- a/app/Actions/Server/CreateServer.php +++ b/app/Actions/Server/CreateServer.php @@ -30,7 +30,7 @@ public function create(User $creator, array $input): Server 'user_id' => $creator->id, 'name' => $input['name'], 'ssh_user' => config('core.server_providers_default_user')[$input['provider']][$input['os']], - 'ip' => $input['ip'], + 'ip' => $input['ip'] ?? '', 'port' => $input['port'] ?? 22, 'os' => $input['os'], 'type' => $input['type'], diff --git a/app/Actions/Server/EditServer.php b/app/Actions/Server/EditServer.php index 85187fe..8e929de 100755 --- a/app/Actions/Server/EditServer.php +++ b/app/Actions/Server/EditServer.php @@ -35,7 +35,7 @@ public function edit(Server $server, array $input): Server $server->save(); if ($checkConnection) { - $server->checkConnection(); + return $server->checkConnection(); } return $server; diff --git a/app/Actions/Server/RebootServer.php b/app/Actions/Server/RebootServer.php new file mode 100644 index 0000000..20ba2cc --- /dev/null +++ b/app/Actions/Server/RebootServer.php @@ -0,0 +1,24 @@ +ssh()->exec(new RebootCommand(), 'reboot'); + $server->status = ServerStatus::DISCONNECTED; + $server->save(); + } catch (Throwable) { + $server = $server->checkConnection(); + } + + return $server; + } +} diff --git a/app/Actions/Site/ChangePHPVersion.php b/app/Actions/Site/ChangePHPVersion.php deleted file mode 100755 index 94bc56a..0000000 --- a/app/Actions/Site/ChangePHPVersion.php +++ /dev/null @@ -1,30 +0,0 @@ -validate($site, $input); - - $site->changePHPVersion($input['php_version']); - } - - /** - * @throws ValidationException - */ - protected function validate(Site $site, array $input): void - { - Validator::make($input, [ - 'php_version' => 'required|in:'.implode(',', $site->server->installedPHPVersions()), - ])->validateWithBag('changePHPVersion'); - } -} diff --git a/app/Actions/Site/UpdateEnv.php b/app/Actions/Site/UpdateEnv.php index 9df9566..1538fb5 100644 --- a/app/Actions/Site/UpdateEnv.php +++ b/app/Actions/Site/UpdateEnv.php @@ -3,6 +3,7 @@ namespace App\Actions\Site; use App\Models\Site; +use App\SSHCommands\System\EditFileCommand; class UpdateEnv { @@ -13,6 +14,11 @@ public function update(Site $site, array $input): void $site->type_data = $typeData; $site->save(); - $site->deployEnv(); + $site->server->ssh()->exec( + new EditFileCommand( + $site->path.'/.env', + $site->env + ) + ); } } diff --git a/app/Actions/Site/UpdateSourceControl.php b/app/Actions/Site/UpdateSourceControl.php deleted file mode 100755 index cd06da3..0000000 --- a/app/Actions/Site/UpdateSourceControl.php +++ /dev/null @@ -1,35 +0,0 @@ -validate($input); - - $site->source_control_id = $input['source_control']; - $site->save(); - } - - /** - * @throws ValidationException - */ - protected function validate(array $input): void - { - Validator::make($input, [ - 'source_control' => [ - 'required', - Rule::exists('source_controls', 'id'), - ], - ])->validate(); - } -} diff --git a/app/Actions/StorageProvider/CreateStorageProvider.php b/app/Actions/StorageProvider/CreateStorageProvider.php index 6c8520b..0f8ba73 100644 --- a/app/Actions/StorageProvider/CreateStorageProvider.php +++ b/app/Actions/StorageProvider/CreateStorageProvider.php @@ -27,11 +27,18 @@ public function create(User $user, array $input): void $storageProvider->credentials = $storageProvider->provider()->credentialData($input); - if (! $storageProvider->provider()->connect()) { + try { + if (! $storageProvider->provider()->connect()) { + throw ValidationException::withMessages([ + 'provider' => __("Couldn't connect to the provider"), + ]); + } + } catch (\Throwable $e) { throw ValidationException::withMessages([ - 'provider' => __("Couldn't connect to the provider"), + 'provider' => $e->getMessage(), ]); } + $storageProvider->save(); } diff --git a/app/Exceptions/SSHCommandError.php b/app/Exceptions/SSHCommandError.php new file mode 100755 index 0000000..5965ed2 --- /dev/null +++ b/app/Exceptions/SSHCommandError.php @@ -0,0 +1,10 @@ +header('HX-Redirect', $redirect); + + return $this; + } + + public function back(): self + { + return $this->redirect(back()->getTargetUrl()); + } + + public function refresh(): self + { + $this->header('HX-Refresh', true); + + return $this; + } + + public function location(string $location): self + { + $this->header('HX-Location', $location); + + return $this; + } +} diff --git a/app/Helpers/SSH.php b/app/Helpers/SSH.php index 07c0892..75b4f9c 100755 --- a/app/Helpers/SSH.php +++ b/app/Helpers/SSH.php @@ -4,6 +4,8 @@ use App\Contracts\SSHCommand; use App\Exceptions\SSHAuthenticationError; +use App\Exceptions\SSHCommandError; +use App\Exceptions\SSHConnectionError; use App\Models\Server; use App\Models\ServerLog; use Exception; @@ -99,20 +101,28 @@ public function exec(string|array|SSHCommand $commands, string $log = '', ?int $ $this->log = null; } - if (! $this->connection) { - $this->connect(); + try { + if (! $this->connection) { + $this->connect(); + } + } catch (Throwable $e) { + throw new SSHConnectionError($e->getMessage()); } if (! is_array($commands)) { $commands = [$commands]; } - $result = ''; - foreach ($commands as $command) { - $result .= $this->executeCommand($command); - } + try { + $result = ''; + foreach ($commands as $command) { + $result .= $this->executeCommand($command); + } - return $result; + return $result; + } catch (Throwable $e) { + throw new SSHCommandError($e->getMessage()); + } } /** diff --git a/app/Helpers/Toast.php b/app/Helpers/Toast.php index 18d8ba4..c15fa70 100644 --- a/app/Helpers/Toast.php +++ b/app/Helpers/Toast.php @@ -2,14 +2,8 @@ namespace App\Helpers; -use Livewire\Component; - class Toast { - public function __construct(public Component $component) - { - } - public function success(string $message): void { $this->toast('success', $message); @@ -32,6 +26,7 @@ public function info(string $message): void private function toast(string $type, string $message): void { - $this->component->dispatch('toast', type: $type, message: $message); + session()->flash('toast.type', $type); + session()->flash('toast.message', $message); } } diff --git a/app/Http/Controllers/ApplicationController.php b/app/Http/Controllers/ApplicationController.php new file mode 100644 index 0000000..222570a --- /dev/null +++ b/app/Http/Controllers/ApplicationController.php @@ -0,0 +1,104 @@ +deploy(); + + Toast::success('Deployment started!'); + } catch (SourceControlIsNotConnected $e) { + Toast::error($e->getMessage()); + + return htmx()->redirect(route('source-controls')); + } + + return htmx()->back(); + } + + public function showDeploymentLog(Server $server, Site $site, Deployment $deployment): RedirectResponse + { + return back()->with('content', $deployment->log->content); + } + + public function updateDeploymentScript(Server $server, Site $site, Request $request): RedirectResponse + { + app(UpdateDeploymentScript::class)->update($site, $request->input()); + + Toast::success('Deployment script updated!'); + + return back(); + } + + public function updateBranch(Server $server, Site $site, Request $request): RedirectResponse + { + app(UpdateBranch::class)->update($site, $request->input()); + + Toast::success('Branch updated!'); + + return back(); + } + + public function getEnv(Server $server, Site $site): RedirectResponse + { + return back()->with('env', $site->getEnv()); + } + + public function updateEnv(Server $server, Site $site, Request $request): RedirectResponse + { + app(UpdateEnv::class)->update($site, $request->input()); + + Toast::success('Env updated!'); + + return back(); + } + + public function enableAutoDeployment(Server $server, Site $site): RedirectResponse + { + if (! $site->auto_deployment) { + try { + $site->enableAutoDeployment(); + + $site->refresh(); + + Toast::success('Auto deployment has been enabled.'); + } catch (SourceControlIsNotConnected) { + Toast::error('Source control is not connected. Check site\'s settings.'); + } + } + + return back(); + } + + public function disableAutoDeployment(Server $server, Site $site): RedirectResponse + { + if ($site->auto_deployment) { + try { + $site->disableAutoDeployment(); + + $site->refresh(); + + Toast::success('Auto deployment has been disabled.'); + } catch (SourceControlIsNotConnected) { + Toast::error('Source control is not connected. Check site\'s settings.'); + } + } + + return back(); + } +} diff --git a/app/Http/Controllers/CronjobController.php b/app/Http/Controllers/CronjobController.php index f9b4b57..2c55c3c 100644 --- a/app/Http/Controllers/CronjobController.php +++ b/app/Http/Controllers/CronjobController.php @@ -2,14 +2,41 @@ namespace App\Http\Controllers; +use App\Actions\CronJob\CreateCronJob; +use App\Actions\CronJob\DeleteCronJob; +use App\Facades\Toast; +use App\Helpers\HtmxResponse; +use App\Models\CronJob; use App\Models\Server; +use Illuminate\Contracts\View\View; +use Illuminate\Http\RedirectResponse; +use Illuminate\Http\Request; class CronjobController extends Controller { - public function index(Server $server) + public function index(Server $server): View { return view('cronjobs.index', [ 'server' => $server, + 'cronjobs' => $server->cronJobs, ]); } + + public function store(Server $server, Request $request): HtmxResponse + { + app(CreateCronJob::class)->create($server, $request->input()); + + Toast::success('Cronjob created successfully.'); + + return htmx()->back(); + } + + public function destroy(Server $server, CronJob $cronJob): RedirectResponse + { + app(DeleteCronJob::class)->delete($server, $cronJob); + + Toast::success('Cronjob deleted successfully.'); + + return back(); + } } diff --git a/app/Http/Controllers/DatabaseBackupController.php b/app/Http/Controllers/DatabaseBackupController.php new file mode 100644 index 0000000..b18f944 --- /dev/null +++ b/app/Http/Controllers/DatabaseBackupController.php @@ -0,0 +1,79 @@ + $server, + 'databases' => $server->databases, + 'backup' => $backup, + 'files' => $backup->files()->orderByDesc('id')->simplePaginate(10), + ]); + } + + public function run(Server $server, Backup $backup): RedirectResponse + { + $backup->run(); + + Toast::success('Backup is running.'); + + return back(); + } + + public function store(Server $server, Request $request): HtmxResponse + { + app(CreateBackup::class)->create('database', $server, $request->input()); + + Toast::success('Backup created successfully.'); + + return htmx()->back(); + } + + public function destroy(Server $server, Backup $backup): RedirectResponse + { + $backup->delete(); + + Toast::success('Backup deleted successfully.'); + + return back(); + } + + public function restore(Server $server, Backup $backup, BackupFile $backupFile, Request $request): HtmxResponse + { + $this->validate($request, [ + 'database' => 'required|exists:databases,id', + ]); + + /** @var Database $database */ + $database = Database::query()->findOrFail($request->input('database')); + + $backupFile->restore($database); + + Toast::success('Backup restored successfully.'); + + return htmx()->back(); + } + + public function destroyFile(Server $server, Backup $backup, BackupFile $backupFile): RedirectResponse + { + $backupFile->delete(); + + Toast::success('Backup file deleted successfully.'); + + return back(); + } +} diff --git a/app/Http/Controllers/DatabaseController.php b/app/Http/Controllers/DatabaseController.php index e00818f..dafd84f 100644 --- a/app/Http/Controllers/DatabaseController.php +++ b/app/Http/Controllers/DatabaseController.php @@ -2,9 +2,16 @@ namespace App\Http\Controllers; -use App\Models\Backup; +use App\Actions\Database\CreateDatabase; +use App\Actions\Database\CreateDatabaseUser; +use App\Actions\Database\DeleteDatabase; +use App\Facades\Toast; +use App\Helpers\HtmxResponse; +use App\Models\Database; use App\Models\Server; use Illuminate\Contracts\View\View; +use Illuminate\Http\RedirectResponse; +use Illuminate\Http\Request; class DatabaseController extends Controller { @@ -12,14 +19,31 @@ public function index(Server $server): View { return view('databases.index', [ 'server' => $server, + 'databases' => $server->databases, + 'databaseUsers' => $server->databaseUsers, + 'backups' => $server->backups, ]); } - public function backups(Server $server, Backup $backup): View + public function store(Server $server, Request $request): HtmxResponse { - return view('databases.backups', [ - 'server' => $server, - 'backup' => $backup, - ]); + $database = app(CreateDatabase::class)->create($server, $request->input()); + + if ($request->input('user')) { + app(CreateDatabaseUser::class)->create($server, $request->input(), [$database->name]); + } + + Toast::success('Database created successfully.'); + + return htmx()->back(); + } + + public function destroy(Server $server, Database $database): RedirectResponse + { + app(DeleteDatabase::class)->delete($server, $database); + + Toast::success('Database deleted successfully.'); + + return back(); } } diff --git a/app/Http/Controllers/DatabaseUserController.php b/app/Http/Controllers/DatabaseUserController.php new file mode 100644 index 0000000..945a908 --- /dev/null +++ b/app/Http/Controllers/DatabaseUserController.php @@ -0,0 +1,54 @@ +create($server, $request->input()); + + if ($request->input('user')) { + app(CreateDatabaseUser::class)->create($server, $request->input(), [$database->name]); + } + + Toast::success('User created successfully.'); + + return htmx()->back(); + } + + public function destroy(Server $server, DatabaseUser $databaseUser): RedirectResponse + { + app(DeleteDatabaseUser::class)->delete($server, $databaseUser); + + Toast::success('User deleted successfully.'); + + return back(); + } + + public function password(Server $server, DatabaseUser $databaseUser): RedirectResponse + { + return back()->with([ + 'password' => $databaseUser->password, + ]); + } + + public function link(Server $server, DatabaseUser $databaseUser, Request $request): HtmxResponse + { + app(LinkUser::class)->link($databaseUser, $request->input()); + + Toast::success('Database linked successfully.'); + + return htmx()->back(); + } +} diff --git a/app/Http/Controllers/FirewallController.php b/app/Http/Controllers/FirewallController.php index 4afe79f..218bb85 100644 --- a/app/Http/Controllers/FirewallController.php +++ b/app/Http/Controllers/FirewallController.php @@ -2,14 +2,41 @@ namespace App\Http\Controllers; +use App\Actions\FirewallRule\CreateRule; +use App\Actions\FirewallRule\DeleteRule; +use App\Facades\Toast; +use App\Helpers\HtmxResponse; +use App\Models\FirewallRule; use App\Models\Server; +use Illuminate\Contracts\View\View; +use Illuminate\Http\RedirectResponse; +use Illuminate\Http\Request; class FirewallController extends Controller { - public function index(Server $server) + public function index(Server $server): View { return view('firewall.index', [ 'server' => $server, + 'rules' => $server->firewallRules, ]); } + + public function store(Server $server, Request $request): HtmxResponse + { + app(CreateRule::class)->create($server, $request->input()); + + Toast::success('Firewall rule created!'); + + return htmx()->back(); + } + + public function destroy(Server $server, FirewallRule $firewallRule): RedirectResponse + { + app(DeleteRule::class)->delete($server, $firewallRule); + + Toast::success('Firewall rule deleted!'); + + return back(); + } } diff --git a/app/Http/Controllers/PHPController.php b/app/Http/Controllers/PHPController.php index b9850e3..aa016b8 100644 --- a/app/Http/Controllers/PHPController.php +++ b/app/Http/Controllers/PHPController.php @@ -2,14 +2,84 @@ namespace App\Http\Controllers; +use App\Actions\PHP\ChangeDefaultCli; +use App\Actions\PHP\GetPHPIni; +use App\Actions\PHP\InstallNewPHP; +use App\Actions\PHP\InstallPHPExtension; +use App\Actions\PHP\UninstallPHP; +use App\Actions\PHP\UpdatePHPIni; +use App\Facades\Toast; +use App\Helpers\HtmxResponse; use App\Models\Server; +use Illuminate\Contracts\View\View; +use Illuminate\Http\RedirectResponse; +use Illuminate\Http\Request; +use Illuminate\Validation\ValidationException; class PHPController extends Controller { - public function index(Server $server) + public function index(Server $server): View { return view('php.index', [ 'server' => $server, + 'phps' => $server->services()->where('type', 'php')->get(), + 'defaultPHP' => $server->defaultService('php'), ]); } + + public function install(Server $server, Request $request): HtmxResponse + { + try { + app(InstallNewPHP::class)->install($server, $request->input()); + + Toast::success('PHP is being installed!'); + } catch (ValidationException $e) { + Toast::error($e->getMessage()); + } + + return htmx()->back(); + } + + public function installExtension(Server $server, Request $request): HtmxResponse + { + app(InstallPHPExtension::class)->install($server, $request->input()); + + Toast::success('PHP extension is being installed! Check the logs'); + + return htmx()->back(); + } + + public function defaultCli(Server $server, Request $request): HtmxResponse + { + app(ChangeDefaultCli::class)->change($server, $request->input()); + + Toast::success('Default PHP CLI is being changed!'); + + return htmx()->back(); + } + + public function getIni(Server $server, Request $request): RedirectResponse + { + $ini = app(GetPHPIni::class)->getIni($server, $request->input()); + + return back()->with('ini', $ini); + } + + public function updateIni(Server $server, Request $request): RedirectResponse + { + app(UpdatePHPIni::class)->update($server, $request->input()); + + Toast::success('PHP ini updated!'); + + return back(); + } + + public function uninstall(Server $server, Request $request): RedirectResponse + { + app(UninstallPHP::class)->uninstall($server, $request->input()); + + Toast::success('PHP is being uninstalled!'); + + return back(); + } } diff --git a/app/Http/Controllers/ProjectController.php b/app/Http/Controllers/ProjectController.php deleted file mode 100644 index ed47101..0000000 --- a/app/Http/Controllers/ProjectController.php +++ /dev/null @@ -1,29 +0,0 @@ -user(); - - /** @var Project $project */ - $project = $user->projects()->findOrFail($projectId); - - $user->current_project_id = $project->id; - $user->save(); - - return redirect()->route('servers'); - } -} diff --git a/app/Http/Controllers/QueueController.php b/app/Http/Controllers/QueueController.php new file mode 100644 index 0000000..e21e4c6 --- /dev/null +++ b/app/Http/Controllers/QueueController.php @@ -0,0 +1,52 @@ + $server, + 'site' => $site, + 'queues' => $site->queues, + ]); + } + + public function store(Server $server, Site $site, Request $request): HtmxResponse + { + app(CreateQueue::class)->create($site, $request->input()); + + Toast::success('Queue is being created.'); + + return htmx()->back(); + } + + public function action(Server $server, Site $site, Queue $queue, string $action): HtmxResponse + { + $queue->{$action}(); + + Toast::success('Queue is about to '.$action); + + return htmx()->back(); + } + + public function destroy(Server $server, Site $site, Queue $queue): RedirectResponse + { + $queue->remove(); + + Toast::success('Queue is being deleted.'); + + return back(); + } +} diff --git a/app/Http/Controllers/SSHKeyController.php b/app/Http/Controllers/SSHKeyController.php index a463945..c06a4b6 100644 --- a/app/Http/Controllers/SSHKeyController.php +++ b/app/Http/Controllers/SSHKeyController.php @@ -2,14 +2,62 @@ namespace App\Http\Controllers; +use App\Actions\SshKey\CreateSshKey; +use App\Facades\Toast; +use App\Helpers\HtmxResponse; use App\Models\Server; +use App\Models\SshKey; +use Illuminate\Contracts\View\View; +use Illuminate\Http\RedirectResponse; +use Illuminate\Http\Request; class SSHKeyController extends Controller { - public function index(Server $server) + public function index(Server $server): View { return view('server-ssh-keys.index', [ 'server' => $server, + 'keys' => $server->sshKeys, ]); } + + public function store(Server $server, Request $request): HtmxResponse + { + /** @var \App\Models\SshKey $key */ + $key = app(CreateSshKey::class)->create( + $request->user(), + $request->input() + ); + + $key->deployTo($server); + + Toast::success('SSH Key added and being deployed to the server.'); + + return htmx()->back(); + } + + public function destroy(Server $server, SshKey $sshKey): RedirectResponse + { + $sshKey->deleteFrom($server); + + Toast::success('SSH Key is being deleted.'); + + return back(); + } + + public function deploy(Server $server, Request $request): HtmxResponse + { + $this->validate($request, [ + 'key_id' => 'required|exists:ssh_keys,id', + ]); + + /** @var SshKey $sshKey */ + $sshKey = SshKey::query()->findOrFail($request->input('key_id')); + + $sshKey->deployTo($server); + + Toast::success('SSH Key is being deployed to the server.'); + + return htmx()->back(); + } } diff --git a/app/Http/Controllers/SSLController.php b/app/Http/Controllers/SSLController.php new file mode 100644 index 0000000..aa37783 --- /dev/null +++ b/app/Http/Controllers/SSLController.php @@ -0,0 +1,43 @@ + $server, + 'site' => $site, + 'ssls' => $site->ssls, + ]); + } + + public function store(Server $server, Site $site, Request $request): HtmxResponse + { + app(CreateSSL::class)->create($site, $request->input()); + + Toast::success('SSL certificate is being created.'); + + return htmx()->back(); + } + + public function destroy(Server $server, Site $site, Ssl $ssl): RedirectResponse + { + $ssl->remove(); + + Toast::success('SSL certificate is being deleted.'); + + return back(); + } +} diff --git a/app/Http/Controllers/ServerController.php b/app/Http/Controllers/ServerController.php index a16ffbf..8892642 100644 --- a/app/Http/Controllers/ServerController.php +++ b/app/Http/Controllers/ServerController.php @@ -2,27 +2,68 @@ namespace App\Http\Controllers; +use App\Actions\Server\CreateServer; +use App\Facades\Toast; +use App\Helpers\HtmxResponse; use App\Models\Server; +use App\Models\ServerProvider; +use App\Models\User; +use Illuminate\Contracts\View\View; +use Illuminate\Http\RedirectResponse; +use Illuminate\Http\Request; +use Throwable; class ServerController extends Controller { - public function index() + public function index(): View { - return view('servers.index'); + /** @var User $user */ + $user = auth()->user(); + $servers = $user->currentProject->servers()->orderByDesc('created_at')->get(); + + return view('servers.index', compact('servers')); } - public function create() + public function create(Request $request): View { - return view('servers.create'); + $provider = $request->query('provider', old('provider', \App\Enums\ServerProvider::CUSTOM)); + $serverProviders = ServerProvider::query()->where('provider', $provider)->get(); + + return view('servers.create', [ + 'serverProviders' => $serverProviders, + 'provider' => $provider, + ]); } - public function show(Server $server) + /** + * @throws Throwable + */ + public function store(Request $request): HtmxResponse { - return view('servers.show', compact('server')); + $server = app(CreateServer::class)->create( + $request->user(), + $request->input() + ); + + Toast::success('Server created successfully.'); + + return htmx()->redirect(route('servers.show', ['server' => $server])); } - public function logs(Server $server) + public function show(Server $server): View { - return view('servers.logs', compact('server')); + return view('servers.show', [ + 'server' => $server, + 'logs' => $server->logs()->latest()->limit(10)->get(), + ]); + } + + public function delete(Server $server): RedirectResponse + { + $server->delete(); + + Toast::success('Server deleted successfully.'); + + return redirect()->route('servers'); } } diff --git a/app/Http/Controllers/ServerLogController.php b/app/Http/Controllers/ServerLogController.php new file mode 100644 index 0000000..f571c00 --- /dev/null +++ b/app/Http/Controllers/ServerLogController.php @@ -0,0 +1,29 @@ + $server, + ]); + } + + public function show(Server $server, ServerLog $serverLog): RedirectResponse + { + if ($server->id != $serverLog->server_id) { + abort(404); + } + + return back()->with([ + 'content' => $serverLog->content, + ]); + } +} diff --git a/app/Http/Controllers/ServerSettingController.php b/app/Http/Controllers/ServerSettingController.php index 9c1da24..9063427 100644 --- a/app/Http/Controllers/ServerSettingController.php +++ b/app/Http/Controllers/ServerSettingController.php @@ -2,12 +2,55 @@ namespace App\Http\Controllers; +use App\Actions\Server\EditServer; +use App\Facades\Toast; +use App\Helpers\HtmxResponse; use App\Models\Server; +use Illuminate\Contracts\View\View; +use Illuminate\Http\RedirectResponse; +use Illuminate\Http\Request; class ServerSettingController extends Controller { - public function index(Server $server) + public function index(Server $server): View { return view('server-settings.index', compact('server')); } + + public function checkConnection(Server $server): RedirectResponse|HtmxResponse + { + $oldStatus = $server->status; + + $server = $server->checkConnection(); + + if ($server->status == 'disconnected') { + Toast::error('Server is disconnected.'); + } + + if ($server->status == 'ready') { + Toast::success('Server is ready.'); + } + + if ($oldStatus != $server->status) { + return htmx()->redirect(back()->getTargetUrl()); + } + + return back(); + } + + public function reboot(Server $server): HtmxResponse + { + $server->reboot(); + + return htmx()->redirect(back()->getTargetUrl()); + } + + public function edit(Request $request, Server $server): RedirectResponse + { + app(EditServer::class)->edit($server, $request->input()); + + Toast::success('Server updated.'); + + return back(); + } } diff --git a/app/Http/Controllers/ServiceController.php b/app/Http/Controllers/ServiceController.php index 5f00f49..8e6b4ff 100644 --- a/app/Http/Controllers/ServiceController.php +++ b/app/Http/Controllers/ServiceController.php @@ -2,14 +2,46 @@ namespace App\Http\Controllers; +use App\Facades\Toast; use App\Models\Server; +use App\Models\Service; +use Illuminate\Contracts\View\View; +use Illuminate\Http\RedirectResponse; class ServiceController extends Controller { - public function index(Server $server) + public function index(Server $server): View { return view('services.index', [ 'server' => $server, + 'services' => $server->services, ]); } + + public function start(Server $server, Service $service): RedirectResponse + { + $service->start(); + + Toast::success('Service is being started!'); + + return back(); + } + + public function stop(Server $server, Service $service): RedirectResponse + { + $service->stop(); + + Toast::success('Service is being stopped!'); + + return back(); + } + + public function restart(Server $server, Service $service): RedirectResponse + { + $service->restart(); + + Toast::success('Service is being restarted!'); + + return back(); + } } diff --git a/app/Http/Controllers/Settings/NotificationChannelController.php b/app/Http/Controllers/Settings/NotificationChannelController.php new file mode 100644 index 0000000..88e1223 --- /dev/null +++ b/app/Http/Controllers/Settings/NotificationChannelController.php @@ -0,0 +1,45 @@ + NotificationChannel::query()->latest()->get(), + ]); + } + + public function add(Request $request): HtmxResponse + { + app(AddChannel::class)->add( + $request->user(), + $request->input() + ); + + Toast::success('Channel added successfully'); + + return htmx()->redirect(route('notification-channels')); + } + + public function delete(int $id): RedirectResponse + { + $channel = NotificationChannel::query()->findOrFail($id); + + $channel->delete(); + + Toast::success('Channel deleted successfully'); + + return redirect()->route('notification-channels'); + } +} diff --git a/app/Http/Controllers/Settings/ProfileController.php b/app/Http/Controllers/Settings/ProfileController.php new file mode 100644 index 0000000..4476c6a --- /dev/null +++ b/app/Http/Controllers/Settings/ProfileController.php @@ -0,0 +1,43 @@ +update( + $request->user(), + $request->input() + ); + + Toast::success('Profile information updated.'); + + return back(); + } + + public function password(Request $request): RedirectResponse + { + app(UpdateUserPassword::class)->update( + $request->user(), + $request->input() + ); + + Toast::success('Password updated.'); + + return back(); + } +} diff --git a/app/Http/Controllers/Settings/ProjectController.php b/app/Http/Controllers/Settings/ProjectController.php new file mode 100644 index 0000000..085e1f1 --- /dev/null +++ b/app/Http/Controllers/Settings/ProjectController.php @@ -0,0 +1,79 @@ + auth()->user()->projects, + ]); + } + + public function create(Request $request): HtmxResponse + { + app(CreateProject::class)->create($request->user(), $request->input()); + + Toast::success('Project created.'); + + return htmx()->redirect(route('projects')); + } + + public function update(Request $request, Project $project): HtmxResponse + { + /** @var Project $project */ + $project = $request->user()->projects()->findOrFail($project->id); + + app(UpdateProject::class)->update($project, $request->input()); + + Toast::success('Project updated.'); + + return htmx()->redirect(route('projects')); + } + + public function delete(Project $project): RedirectResponse + { + /** @var Project $project */ + $project = auth()->user()->projects()->findOrFail($project->id); + + try { + app(DeleteProject::class)->delete(auth()->user(), $project); + } catch (ValidationException $e) { + Toast::error($e->getMessage()); + + return back(); + } + + Toast::success('Project deleted.'); + + return back(); + } + + public function switch($projectId): RedirectResponse + { + /** @var User $user */ + $user = auth()->user(); + + /** @var Project $project */ + $project = $user->projects()->findOrFail($projectId); + + $user->current_project_id = $project->id; + $user->save(); + + return redirect()->route('servers'); + } +} diff --git a/app/Http/Controllers/Settings/SSHKeyController.php b/app/Http/Controllers/Settings/SSHKeyController.php new file mode 100644 index 0000000..5fc1495 --- /dev/null +++ b/app/Http/Controllers/Settings/SSHKeyController.php @@ -0,0 +1,45 @@ + SshKey::query()->latest()->get(), + ]); + } + + public function add(Request $request): HtmxResponse + { + app(CreateSshKey::class)->create( + $request->user(), + $request->input() + ); + + Toast::success('SSH Key added'); + + return htmx()->redirect(route('ssh-keys')); + } + + public function delete(int $id): RedirectResponse + { + $key = SshKey::query()->findOrFail($id); + + $key->delete(); + + Toast::success('SSH Key deleted'); + + return redirect()->route('ssh-keys'); + } +} diff --git a/app/Http/Controllers/Settings/ServerProviderController.php b/app/Http/Controllers/Settings/ServerProviderController.php new file mode 100644 index 0000000..9d7b76e --- /dev/null +++ b/app/Http/Controllers/Settings/ServerProviderController.php @@ -0,0 +1,48 @@ + auth()->user()->serverProviders, + ]); + } + + public function connect(Request $request): HtmxResponse + { + app(CreateServerProvider::class)->create( + $request->user(), + $request->input() + ); + + Toast::success('Server provider connected.'); + + return htmx()->redirect(route('server-providers')); + } + + /** + * @TODO Update servers using this provider + */ + public function delete(int $id): RedirectResponse + { + $serverProvider = ServerProvider::query()->findOrFail($id); + + $serverProvider->delete(); + + Toast::success('Server provider deleted.'); + + return back(); + } +} diff --git a/app/Http/Controllers/Settings/SourceControlController.php b/app/Http/Controllers/Settings/SourceControlController.php new file mode 100644 index 0000000..c17ae98 --- /dev/null +++ b/app/Http/Controllers/Settings/SourceControlController.php @@ -0,0 +1,47 @@ + SourceControl::query()->orderByDesc('id')->get(), + ]); + } + + public function connect(Request $request): HtmxResponse + { + app(ConnectSourceControl::class)->connect( + $request->input(), + ); + + Toast::success('Source control connected.'); + + return htmx()->redirect(route('source-controls')); + } + + public function delete(int $id): RedirectResponse + { + $sourceControl = SourceControl::query()->findOrFail($id); + + $sourceControl->delete(); + + Toast::success('Source control deleted.'); + + return redirect()->route('source-controls'); + } +} diff --git a/app/Http/Controllers/Settings/StorageProviderController.php b/app/Http/Controllers/Settings/StorageProviderController.php new file mode 100644 index 0000000..bc57676 --- /dev/null +++ b/app/Http/Controllers/Settings/StorageProviderController.php @@ -0,0 +1,48 @@ + auth()->user()->storageProviders, + ]); + } + + public function connect(Request $request): HtmxResponse + { + app(CreateStorageProvider::class)->create( + $request->user(), + $request->input() + ); + + Toast::success('Storage provider connected.'); + + return htmx()->redirect(route('storage-providers')); + } + + /** + * @TODO Update servers using this provider + */ + public function delete(int $id): RedirectResponse + { + $storageProvider = StorageProvider::query()->findOrFail($id); + + $storageProvider->delete(); + + Toast::success('Storage provider deleted.'); + + return back(); + } +} diff --git a/app/Http/Controllers/SiteController.php b/app/Http/Controllers/SiteController.php index d100795..8edf315 100644 --- a/app/Http/Controllers/SiteController.php +++ b/app/Http/Controllers/SiteController.php @@ -2,9 +2,16 @@ namespace App\Http\Controllers; +use App\Actions\Site\CreateSite; +use App\Enums\SiteType; +use App\Facades\Toast; +use App\Helpers\HtmxResponse; use App\Models\Server; use App\Models\Site; +use App\Models\SourceControl; use Illuminate\Contracts\View\View; +use Illuminate\Http\RedirectResponse; +use Illuminate\Http\Request; class SiteController extends Controller { @@ -12,13 +19,25 @@ public function index(Server $server): View { return view('sites.index', [ 'server' => $server, + 'sites' => $server->sites()->orderByDesc('id')->get(), ]); } + public function store(Server $server, Request $request): HtmxResponse + { + $site = app(CreateSite::class)->create($server, $request->input()); + + Toast::success('Site created'); + + return htmx()->redirect(route('servers.sites.show', [$server, $site])); + } + public function create(Server $server): View { return view('sites.create', [ 'server' => $server, + 'type' => old('type', request()->query('type', SiteType::LARAVEL)), + 'sourceControls' => SourceControl::all(), ]); } @@ -30,43 +49,12 @@ public function show(Server $server, Site $site): View ]); } - public function application(Server $server, Site $site): View + public function destroy(Server $server, Site $site): RedirectResponse { - return view('sites.application', [ - 'server' => $server, - 'site' => $site, - ]); - } + $site->remove(); - public function ssl(Server $server, Site $site): View - { - return view('sites.ssl', [ - 'server' => $server, - 'site' => $site, - ]); - } + Toast::success('Site is being deleted'); - public function queues(Server $server, Site $site): View - { - return view('sites.queues', [ - 'server' => $server, - 'site' => $site, - ]); - } - - public function settings(Server $server, Site $site): View - { - return view('sites.settings', [ - 'server' => $server, - 'site' => $site, - ]); - } - - public function logs(Server $server, Site $site): View - { - return view('sites.logs', [ - 'server' => $server, - 'site' => $site, - ]); + return redirect()->route('servers.sites', $server); } } diff --git a/app/Http/Controllers/SiteLogController.php b/app/Http/Controllers/SiteLogController.php new file mode 100644 index 0000000..349f894 --- /dev/null +++ b/app/Http/Controllers/SiteLogController.php @@ -0,0 +1,18 @@ + $server, + 'site' => $site, + ]); + } +} diff --git a/app/Http/Controllers/SiteSettingController.php b/app/Http/Controllers/SiteSettingController.php new file mode 100644 index 0000000..6382af3 --- /dev/null +++ b/app/Http/Controllers/SiteSettingController.php @@ -0,0 +1,65 @@ + $server, + 'site' => $site, + ]); + } + + public function getVhost(Server $server, Site $site): RedirectResponse + { + return back()->with('vhost', $server->webserver()->handler()->getVHost($site)); + } + + public function updateVhost(Server $server, Site $site, Request $request): RedirectResponse + { + $this->validate($request, [ + 'vhost' => 'required|string', + ]); + + try { + $server->webserver()->handler()->updateVHost($site, false, $request->input('vhost')); + + Toast::success('VHost updated successfully!'); + } catch (Throwable $e) { + Toast::error($e->getMessage()); + } + + return back(); + } + + public function updatePHPVersion(Server $server, Site $site, Request $request): RedirectResponse + { + $this->validate($request, [ + 'version' => [ + 'required', + Rule::exists('services', 'version')->where('type', 'php'), + ], + ]); + + try { + $site->changePHPVersion($request->input('version')); + + Toast::success('PHP version updated successfully!'); + } catch (Throwable $e) { + Toast::error($e->getMessage()); + } + + return back(); + } +} diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index ec01584..cc22227 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -2,6 +2,7 @@ namespace App\Http; +use App\Http\Middleware\HandleSSHErrors; use App\Http\Middleware\ServerIsReadyMiddleware; use Illuminate\Foundation\Http\Kernel as HttpKernel; @@ -65,5 +66,6 @@ class Kernel extends HttpKernel 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, 'server-is-ready' => ServerIsReadyMiddleware::class, + 'handle-ssh-errors' => HandleSSHErrors::class, ]; } diff --git a/app/Http/Livewire/Application/AutoDeployment.php b/app/Http/Livewire/Application/AutoDeployment.php deleted file mode 100644 index fe17dae..0000000 --- a/app/Http/Livewire/Application/AutoDeployment.php +++ /dev/null @@ -1,60 +0,0 @@ -site->auto_deployment) { - try { - $this->site->enableAutoDeployment(); - - $this->site->refresh(); - - $this->toast()->success(__('Auto deployment has been enabled.')); - } catch (SourceControlIsNotConnected) { - $this->toast()->error(__('Source control is not connected. Check site\'s settings.')); - } - } - } - - /** - * @throws Throwable - */ - public function disable(): void - { - if ($this->site->auto_deployment) { - try { - $this->site->disableAutoDeployment(); - - $this->site->refresh(); - - $this->toast()->success(__('Auto deployment has been disabled.')); - } catch (SourceControlIsNotConnected) { - $this->toast()->error(__('Source control is not connected. Check site\'s settings.')); - } - } - } - - public function render(): View - { - return view('livewire.application.auto-deployment'); - } -} diff --git a/app/Http/Livewire/Application/ChangeBranch.php b/app/Http/Livewire/Application/ChangeBranch.php deleted file mode 100644 index f528e0a..0000000 --- a/app/Http/Livewire/Application/ChangeBranch.php +++ /dev/null @@ -1,35 +0,0 @@ -branch = $this->site->branch; - } - - public function change(): void - { - app(UpdateBranch::class)->update($this->site, $this->all()); - - session()->flash('status', 'updating-branch'); - } - - public function render(): View - { - return view('livewire.application.change-branch'); - } -} diff --git a/app/Http/Livewire/Application/Deploy.php b/app/Http/Livewire/Application/Deploy.php deleted file mode 100644 index a84b377..0000000 --- a/app/Http/Livewire/Application/Deploy.php +++ /dev/null @@ -1,40 +0,0 @@ -site->deploy(); - - $this->toast()->success(__('Deployment started!')); - - $this->dispatch('$refresh')->to(DeploymentsList::class); - - $this->dispatch('$refresh')->to(DeploymentScript::class); - } catch (SourceControlIsNotConnected $e) { - session()->flash('toast.type', 'error'); - session()->flash('toast.message', $e->getMessage()); - $this->redirect(route('source-controls')); - } - } - - public function render(): View - { - return view('livewire.application.deploy'); - } -} diff --git a/app/Http/Livewire/Application/DeploymentScript.php b/app/Http/Livewire/Application/DeploymentScript.php deleted file mode 100644 index 0299909..0000000 --- a/app/Http/Livewire/Application/DeploymentScript.php +++ /dev/null @@ -1,38 +0,0 @@ -script = $this->site->deploymentScript->content; - } - - public function save(): void - { - app(UpdateDeploymentScript::class)->update($this->site, $this->all()); - - session()->flash('status', 'script-updated'); - - $this->dispatch('$refresh')->to(Deploy::class); - $this->dispatch('$refresh')->to(AutoDeployment::class); - } - - public function render(): View - { - return view('livewire.application.deployment-script'); - } -} diff --git a/app/Http/Livewire/Application/DeploymentsList.php b/app/Http/Livewire/Application/DeploymentsList.php deleted file mode 100644 index 7f030c2..0000000 --- a/app/Http/Livewire/Application/DeploymentsList.php +++ /dev/null @@ -1,34 +0,0 @@ -site->deployments()->findOrFail($id); - $this->logContent = $deployment->log->content; - - $this->dispatch('open-modal', 'show-log'); - } - - public function render(): View - { - return view('livewire.application.deployments-list', [ - 'deployments' => $this->site->deployments()->latest()->simplePaginate(10), - ]); - } -} diff --git a/app/Http/Livewire/Application/Env.php b/app/Http/Livewire/Application/Env.php deleted file mode 100644 index abfcfc0..0000000 --- a/app/Http/Livewire/Application/Env.php +++ /dev/null @@ -1,37 +0,0 @@ -env = $this->site->getEnv(); - } - - public function save(): void - { - app(UpdateEnv::class)->update($this->site, $this->all()); - - session()->flash('status', 'updating-env'); - - $this->dispatch('$refresh')->to(Deploy::class); - } - - public function render(): View - { - return view('livewire.application.env'); - } -} diff --git a/app/Http/Livewire/Application/LaravelApp.php b/app/Http/Livewire/Application/LaravelApp.php deleted file mode 100644 index 11d44b1..0000000 --- a/app/Http/Livewire/Application/LaravelApp.php +++ /dev/null @@ -1,20 +0,0 @@ -dispatch('broadcast', $event); - } - - return view('livewire.broadcast'); - } -} diff --git a/app/Http/Livewire/Cronjobs/CreateCronjob.php b/app/Http/Livewire/Cronjobs/CreateCronjob.php deleted file mode 100644 index 90147f8..0000000 --- a/app/Http/Livewire/Cronjobs/CreateCronjob.php +++ /dev/null @@ -1,34 +0,0 @@ -create($this->server, $this->all()); - - $this->dispatch('$refresh')->to(CronjobsList::class); - - $this->dispatch('created'); - } - - public function render(): View - { - return view('livewire.cronjobs.create-cronjob'); - } -} diff --git a/app/Http/Livewire/Cronjobs/CronjobsList.php b/app/Http/Livewire/Cronjobs/CronjobsList.php deleted file mode 100644 index 184c7f5..0000000 --- a/app/Http/Livewire/Cronjobs/CronjobsList.php +++ /dev/null @@ -1,35 +0,0 @@ -server->cronJobs()->where('id', $this->deleteId)->firstOrFail(); - - $cronjob->removeFromServer(); - - $this->refreshComponent([]); - - $this->dispatch('confirmed'); - } - - public function render(): View - { - return view('livewire.cronjobs.cronjobs-list', [ - 'cronjobs' => $this->server->cronJobs, - ]); - } -} diff --git a/app/Http/Livewire/Databases/DatabaseBackupFiles.php b/app/Http/Livewire/Databases/DatabaseBackupFiles.php deleted file mode 100644 index 1d2d2a9..0000000 --- a/app/Http/Livewire/Databases/DatabaseBackupFiles.php +++ /dev/null @@ -1,67 +0,0 @@ -backup->run(); - - $this->refreshComponent([]); - } - - public function restore(): void - { - /** @var BackupFile $file */ - $file = BackupFile::query()->findOrFail($this->restoreId); - - /** @var Database $database */ - $database = Database::query()->findOrFail($this->restoreDatabaseId); - - $file->restore($database); - - $this->refreshComponent([]); - - $this->dispatch('restored'); - } - - public function delete(): void - { - /** @var BackupFile $file */ - $file = BackupFile::query()->findOrFail($this->deleteId); - - $file->delete(); - - $this->dispatch('confirmed'); - } - - public function render(): View - { - return view('livewire.databases.database-backup-files', [ - 'files' => $this->backup->files()->orderByDesc('id')->simplePaginate(10), - ]); - } -} diff --git a/app/Http/Livewire/Databases/DatabaseBackups.php b/app/Http/Livewire/Databases/DatabaseBackups.php deleted file mode 100644 index 4c0d62a..0000000 --- a/app/Http/Livewire/Databases/DatabaseBackups.php +++ /dev/null @@ -1,81 +0,0 @@ -create('database', $this->server, $this->all()); - - $this->refreshComponent([]); - - $this->dispatch('backup-created'); - } - - public function files(int $id): void - { - $backup = Backup::query()->findOrFail($id); - $this->backup = $backup; - $this->files = $backup->files()->orderByDesc('id')->simplePaginate(1); - $this->dispatch('show-files'); - } - - public function backup(): void - { - $this->backup?->run(); - - $this->files = $this->backup?->files()->orderByDesc('id')->simplePaginate(); - - $this->dispatch('backup-running'); - } - - public function delete(): void - { - /** @var Backup $backup */ - $backup = Backup::query()->findOrFail($this->deleteId); - - $backup->delete(); - - $this->dispatch('confirmed'); - } - - public function render(): View - { - return view('livewire.databases.database-backups', [ - 'backups' => $this->server->backups, - 'databases' => $this->server->databases, - 'files' => $this->files, - ]); - } -} diff --git a/app/Http/Livewire/Databases/DatabaseList.php b/app/Http/Livewire/Databases/DatabaseList.php deleted file mode 100644 index 881aa37..0000000 --- a/app/Http/Livewire/Databases/DatabaseList.php +++ /dev/null @@ -1,66 +0,0 @@ -create($this->server, $this->all()); - - if ($this->all()['user']) { - app(CreateDatabaseUser::class)->create($this->server, $this->all(), [$database->name]); - } - - $this->refreshComponent([]); - - $this->dispatch('database-created'); - } - - public function delete(): void - { - /** @var Database $database */ - $database = Database::query()->findOrFail($this->deleteId); - - $database->deleteFromServer(); - - $this->refreshComponent([]); - - $this->dispatch('$refresh')->to(DatabaseUserList::class); - - $this->dispatch('confirmed'); - } - - public function render(): View - { - return view('livewire.databases.database-list', [ - 'databases' => $this->server->databases, - ]); - } -} diff --git a/app/Http/Livewire/Databases/DatabaseUserList.php b/app/Http/Livewire/Databases/DatabaseUserList.php deleted file mode 100644 index cf64ac9..0000000 --- a/app/Http/Livewire/Databases/DatabaseUserList.php +++ /dev/null @@ -1,98 +0,0 @@ -create($this->server, $this->all()); - - $this->refreshComponent([]); - - $this->dispatch('database-user-created'); - } - - public function delete(): void - { - /** @var DatabaseUser $databaseUser */ - $databaseUser = DatabaseUser::query()->findOrFail($this->deleteId); - - $databaseUser->deleteFromServer(); - - $this->refreshComponent([]); - - $this->dispatch('$refresh')->to(DatabaseList::class); - - $this->dispatch('confirmed'); - } - - public function viewPassword(int $id): void - { - /** @var DatabaseUser $databaseUser */ - $databaseUser = DatabaseUser::query()->findOrFail($id); - - $this->viewPassword = $databaseUser->password; - - $this->dispatch('open-modal', 'database-user-password'); - } - - public function showLink(int $id): void - { - /** @var DatabaseUser $databaseUser */ - $databaseUser = DatabaseUser::query()->findOrFail($id); - - $this->linkId = $id; - $this->link = $databaseUser->databases ?? []; - - $this->dispatch('open-modal', 'link-database-user'); - } - - public function link(): void - { - /** @var DatabaseUser $databaseUser */ - $databaseUser = DatabaseUser::query()->findOrFail($this->linkId); - - app(LinkUser::class)->link($databaseUser, $this->link); - - $this->refreshComponent([]); - - $this->dispatch('linked'); - } - - public function render(): View - { - return view('livewire.databases.database-user-list', [ - 'databases' => $this->server->databases, - 'databaseUsers' => $this->server->databaseUsers, - ]); - } -} diff --git a/app/Http/Livewire/Firewall/CreateFirewallRule.php b/app/Http/Livewire/Firewall/CreateFirewallRule.php deleted file mode 100644 index 2777076..0000000 --- a/app/Http/Livewire/Firewall/CreateFirewallRule.php +++ /dev/null @@ -1,40 +0,0 @@ -create($this->server, $this->all()); - - $this->dispatch('$refresh')->to(FirewallRulesList::class); - - $this->dispatch('created'); - } - - public function render(): View - { - return view('livewire.firewall.create-firewall-rule'); - } -} diff --git a/app/Http/Livewire/Firewall/FirewallRulesList.php b/app/Http/Livewire/Firewall/FirewallRulesList.php deleted file mode 100644 index 92925aa..0000000 --- a/app/Http/Livewire/Firewall/FirewallRulesList.php +++ /dev/null @@ -1,37 +0,0 @@ -findOrFail($this->deleteId); - - $rule->removeFromServer(); - - $this->refreshComponent([]); - - $this->dispatch('confirmed'); - } - - public function render(): View - { - return view('livewire.firewall.firewall-rules-list', [ - 'rules' => $this->server->firewallRules, - ]); - } -} diff --git a/app/Http/Livewire/NotificationChannels/AddChannel.php b/app/Http/Livewire/NotificationChannels/AddChannel.php deleted file mode 100644 index f68b68a..0000000 --- a/app/Http/Livewire/NotificationChannels/AddChannel.php +++ /dev/null @@ -1,38 +0,0 @@ -add( - auth()->user(), - $this->all() - ); - - $this->dispatch('$refresh')->to(ChannelsList::class); - - $this->dispatch('added'); - } - - public function render(): View - { - return view('livewire.notification-channels.add-channel'); - } -} diff --git a/app/Http/Livewire/NotificationChannels/ChannelsList.php b/app/Http/Livewire/NotificationChannels/ChannelsList.php deleted file mode 100644 index b8aab5f..0000000 --- a/app/Http/Livewire/NotificationChannels/ChannelsList.php +++ /dev/null @@ -1,37 +0,0 @@ -findOrFail($this->deleteId); - - $channel->delete(); - - $this->refreshComponent([]); - - $this->dispatch('confirmed'); - } - - public function render(): View - { - return view('livewire.notification-channels.channels-list', [ - 'channels' => NotificationChannel::query()->latest()->get(), - ]); - } -} diff --git a/app/Http/Livewire/Php/DefaultCli.php b/app/Http/Livewire/Php/DefaultCli.php deleted file mode 100644 index 1a998d5..0000000 --- a/app/Http/Livewire/Php/DefaultCli.php +++ /dev/null @@ -1,30 +0,0 @@ -server->php($version)->handler()->setDefaultCli(); - - $this->refreshComponent([]); - } - - public function render(): View - { - return view('livewire.php.default-cli', [ - 'defaultPHP' => $this->server->defaultService('php'), - 'phps' => $this->server->services()->where('type', 'php')->get(), // - ]); - } -} diff --git a/app/Http/Livewire/Php/InstalledVersions.php b/app/Http/Livewire/Php/InstalledVersions.php deleted file mode 100644 index 3b7f71c..0000000 --- a/app/Http/Livewire/Php/InstalledVersions.php +++ /dev/null @@ -1,117 +0,0 @@ -install($this->server, [ - 'version' => $version, - ]); - - $this->refreshComponent([]); - } - - public function restart(int $id): void - { - /** @var Service $service */ - $service = Service::query()->findOrFail($id); - $service->restart(); - - $this->refreshComponent([]); - } - - public function uninstall(): void - { - /** @var Service $service */ - $service = Service::query()->findOrFail($this->uninstallId); - $service->uninstall(); - - $this->refreshComponent([]); - - $this->dispatch('confirmed'); - } - - public function loadIni(int $id): void - { - $this->iniId = $id; - $this->ini = 'Loading php.ini'; - - /** @var Service $service */ - $service = Service::query()->findOrFail($this->iniId); - - try { - $this->ini = $service->server->ssh()->exec(new GetPHPIniCommand($service->version)); - } catch (Throwable) { - // - } - } - - public function saveIni(): void - { - /** @var Service $service */ - $service = Service::query()->findOrFail($this->iniId); - - app(UpdatePHPIni::class)->update($service, $this->all()['ini']); - - $this->refreshComponent([]); - - session()->flash('status', 'ini-updated'); - } - - /** - * @throws Exception - */ - public function installExtension(): void - { - /** @var Service $service */ - $service = Service::query()->findOrFail($this->extensionId); - - app(InstallPHPExtension::class)->handle($service, [ - 'name' => $this->extension, - ]); - - session()->flash('status', 'started-installation'); - } - - public function render(): View - { - if ($this->extensionId) { - /** @var Service $php */ - $php = Service::query()->findOrFail($this->extensionId); - $installedExtensions = $php->type_data['extensions'] ?? []; - } - - return view('livewire.php.installed-versions', [ - 'phps' => $this->server->services()->where('type', 'php')->get(), - 'installedExtensions' => $installedExtensions ?? [], - ]); - } -} diff --git a/app/Http/Livewire/Profile/TwoFactorAuthentication.php b/app/Http/Livewire/Profile/TwoFactorAuthentication.php deleted file mode 100644 index 02c5d84..0000000 --- a/app/Http/Livewire/Profile/TwoFactorAuthentication.php +++ /dev/null @@ -1,14 +0,0 @@ -update(auth()->user(), $this->all()); - - $this->current_password = ''; - $this->password = ''; - $this->password_confirmation = ''; - - session()->flash('status', 'password-updated'); - } - - public function render(): View - { - return view('livewire.profile.update-password'); - } -} diff --git a/app/Http/Livewire/Profile/UpdateProfileInformation.php b/app/Http/Livewire/Profile/UpdateProfileInformation.php deleted file mode 100644 index 58dc440..0000000 --- a/app/Http/Livewire/Profile/UpdateProfileInformation.php +++ /dev/null @@ -1,54 +0,0 @@ -name = auth()->user()->name; - $this->email = auth()->user()->email; - $this->timezone = auth()->user()->timezone; - } - - /** - * @throws Exception - */ - public function submit(): void - { - app(UpdateUserProfileInformation::class)->update(auth()->user(), $this->all()); - - session()->flash('status', 'profile-updated'); - - $this->dispatch('$refresh')->to(UserDropdown::class); - } - - public function sendVerificationEmail(): void - { - /** @var User $user */ - $user = auth()->user(); - if (! $user->hasVerifiedEmail()) { - $user->sendEmailVerificationNotification(); - - session()->flash('status', 'verification-link-sent'); - } - } - - public function render(): View - { - return view('livewire.profile.update-profile-information'); - } -} diff --git a/app/Http/Livewire/Projects/CreateProject.php b/app/Http/Livewire/Projects/CreateProject.php deleted file mode 100644 index 8966fc5..0000000 --- a/app/Http/Livewire/Projects/CreateProject.php +++ /dev/null @@ -1,37 +0,0 @@ -create(auth()->user(), $this->inputs); - - $this->dispatch('$refresh')->to(ProjectsList::class); - - $this->dispatch('created'); - } - - public function render(): View - { - if (request()->query('create')) { - $this->open = true; - } - - return view('livewire.projects.create-project'); - } -} diff --git a/app/Http/Livewire/Projects/EditProject.php b/app/Http/Livewire/Projects/EditProject.php deleted file mode 100644 index 5532462..0000000 --- a/app/Http/Livewire/Projects/EditProject.php +++ /dev/null @@ -1,37 +0,0 @@ -update($this->project, $this->inputs); - - $this->redirect(route('projects')); - } - - public function mount(): void - { - $this->inputs = [ - 'name' => $this->project->name, - ]; - } - - public function render(): View - { - return view('livewire.projects.edit-project'); - } -} diff --git a/app/Http/Livewire/Projects/ProjectsList.php b/app/Http/Livewire/Projects/ProjectsList.php deleted file mode 100644 index 68e233e..0000000 --- a/app/Http/Livewire/Projects/ProjectsList.php +++ /dev/null @@ -1,42 +0,0 @@ -delete(auth()->user(), $this->deleteId); - - $this->redirect(route('projects')); - - return; - } catch (ValidationException $e) { - $this->toast()->error($e->getMessage()); - } - } - - public function render(): View - { - return view('livewire.projects.projects-list', [ - 'projects' => auth()->user()->projects()->orderByDesc('id')->get(), - ]); - } -} diff --git a/app/Http/Livewire/Queues/CreateQueue.php b/app/Http/Livewire/Queues/CreateQueue.php deleted file mode 100644 index e6eeb30..0000000 --- a/app/Http/Livewire/Queues/CreateQueue.php +++ /dev/null @@ -1,36 +0,0 @@ -create($this->site, $this->all()); - - $this->dispatch('$refresh')->to(QueuesList::class); - - $this->dispatch('created'); - } - - public function render(): View - { - return view('livewire.queues.create-queue'); - } -} diff --git a/app/Http/Livewire/Queues/QueuesList.php b/app/Http/Livewire/Queues/QueuesList.php deleted file mode 100644 index 983961e..0000000 --- a/app/Http/Livewire/Queues/QueuesList.php +++ /dev/null @@ -1,57 +0,0 @@ -site->queues()->findOrFail($this->deleteId); - - $queue->remove(); - - $this->refreshComponent([]); - - $this->dispatch('confirmed'); - } - - public function start(Queue $queue): void - { - $queue->start(); - - $this->refreshComponent([]); - } - - public function restart(Queue $queue): void - { - $queue->restart(); - - $this->refreshComponent([]); - } - - public function stop(Queue $queue): void - { - $queue->stop(); - - $this->refreshComponent([]); - } - - public function render(): View - { - return view('livewire.queues.queues-list', [ - 'queues' => $this->site->queues, - ]); - } -} diff --git a/app/Http/Livewire/ServerLogs/LogsList.php b/app/Http/Livewire/ServerLogs/LogsList.php deleted file mode 100644 index 8e21e31..0000000 --- a/app/Http/Livewire/ServerLogs/LogsList.php +++ /dev/null @@ -1,58 +0,0 @@ -server->logs()->findOrFail($id); - $this->logContent = $log->content; - - $this->dispatch('open-modal', 'show-log'); - } - - public function render(): View - { - if ($this->site) { - return $this->renderSite(); - } - - if ($this->count) { - $logs = $this->server->logs()->latest()->take(10)->get(); - } else { - $logs = $this->server->logs()->latest()->simplePaginate(10); - } - - return view('livewire.server-logs.logs-list', compact('logs')); - } - - private function renderSite(): View - { - if ($this->count) { - $logs = $this->site->logs()->latest()->take(10)->get(); - } else { - $logs = $this->site->logs()->latest()->simplePaginate(10); - } - - return view('livewire.server-logs.logs-list', compact('logs')); - } -} diff --git a/app/Http/Livewire/ServerProviders/ConnectProvider.php b/app/Http/Livewire/ServerProviders/ConnectProvider.php deleted file mode 100644 index 7f315f4..0000000 --- a/app/Http/Livewire/ServerProviders/ConnectProvider.php +++ /dev/null @@ -1,40 +0,0 @@ -create(auth()->user(), $this->all()); - - $this->dispatch('$refresh')->to(ProvidersList::class); - - $this->dispatch('connected'); - } - - public function render(): View - { - if (request()->query('provider')) { - $this->provider = request()->query('provider'); - } - - return view('livewire.server-providers.connect-provider', [ - 'open' => ! is_null(request()->query('provider')), - ]); - } -} diff --git a/app/Http/Livewire/ServerProviders/ProvidersList.php b/app/Http/Livewire/ServerProviders/ProvidersList.php deleted file mode 100644 index 5fb7ded..0000000 --- a/app/Http/Livewire/ServerProviders/ProvidersList.php +++ /dev/null @@ -1,37 +0,0 @@ -findOrFail($this->deleteId); - - $provider->delete(); - - $this->refreshComponent([]); - - $this->dispatch('confirmed'); - } - - public function render(): View - { - return view('livewire.server-providers.providers-list', [ - 'providers' => ServerProvider::query()->latest()->get(), - ]); - } -} diff --git a/app/Http/Livewire/ServerSettings/CheckConnection.php b/app/Http/Livewire/ServerSettings/CheckConnection.php deleted file mode 100644 index d95c66f..0000000 --- a/app/Http/Livewire/ServerSettings/CheckConnection.php +++ /dev/null @@ -1,24 +0,0 @@ -server->checkConnection(); - - session()->flash('status', 'checking-connection'); - } - - public function render(): View - { - return view('livewire.server-settings.check-connection'); - } -} diff --git a/app/Http/Livewire/ServerSettings/EditServer.php b/app/Http/Livewire/ServerSettings/EditServer.php deleted file mode 100644 index 75721be..0000000 --- a/app/Http/Livewire/ServerSettings/EditServer.php +++ /dev/null @@ -1,40 +0,0 @@ -name = $this->server->name; - $this->ip = $this->server->ip; - $this->port = $this->server->port; - } - - public function update(): void - { - app(\App\Actions\Server\EditServer::class)->edit($this->server, $this->all()); - - session()->flash('status', 'server-updated'); - } - - public function render(): View - { - return view('livewire.server-settings.edit-server'); - } -} diff --git a/app/Http/Livewire/ServerSettings/RebootServer.php b/app/Http/Livewire/ServerSettings/RebootServer.php deleted file mode 100644 index cd908fd..0000000 --- a/app/Http/Livewire/ServerSettings/RebootServer.php +++ /dev/null @@ -1,24 +0,0 @@ -server->reboot(); - - session()->flash('status', 'rebooting-server'); - } - - public function render(): View - { - return view('livewire.server-settings.reboot-server'); - } -} diff --git a/app/Http/Livewire/ServerSettings/ServerDetails.php b/app/Http/Livewire/ServerSettings/ServerDetails.php deleted file mode 100644 index 5991e09..0000000 --- a/app/Http/Livewire/ServerSettings/ServerDetails.php +++ /dev/null @@ -1,20 +0,0 @@ -findOrFail($this->all()['key_id']); - - $key->deployTo($this->server); - - $this->dispatch('$refresh')->to(ServerKeysList::class); - - $this->dispatch('added'); - } - - public function render(): View - { - return view('livewire.server-ssh-keys.add-existing-key', [ - 'keys' => SshKey::all(), - ]); - } -} diff --git a/app/Http/Livewire/ServerSshKeys/AddNewKey.php b/app/Http/Livewire/ServerSshKeys/AddNewKey.php deleted file mode 100644 index 3817249..0000000 --- a/app/Http/Livewire/ServerSshKeys/AddNewKey.php +++ /dev/null @@ -1,36 +0,0 @@ -create( - auth()->user(), - $this->all() - ); - - $key->deployTo($this->server); - - $this->dispatch('$refresh')->to(ServerKeysList::class); - - $this->dispatch('added'); - } - - public function render(): View - { - return view('livewire.server-ssh-keys.add-new-key'); - } -} diff --git a/app/Http/Livewire/ServerSshKeys/ServerKeysList.php b/app/Http/Livewire/ServerSshKeys/ServerKeysList.php deleted file mode 100644 index 09f1b18..0000000 --- a/app/Http/Livewire/ServerSshKeys/ServerKeysList.php +++ /dev/null @@ -1,40 +0,0 @@ -findOrFail($this->deleteId); - - $key->deleteFrom($this->server); - - $this->refreshComponent([]); - - $this->dispatch('confirmed'); - } - - public function render(): View - { - return view('livewire.server-ssh-keys.server-keys-list', [ - 'keys' => $this->server->sshKeys, - ]); - } -} diff --git a/app/Http/Livewire/Servers/CreateServer.php b/app/Http/Livewire/Servers/CreateServer.php deleted file mode 100644 index 326d191..0000000 --- a/app/Http/Livewire/Servers/CreateServer.php +++ /dev/null @@ -1,64 +0,0 @@ -create( - auth()->user(), - $this->all() - ); - - $this->redirect(route('servers.show', ['server' => $server])); - } - - public function render(): View - { - $serverProviders = ServerProvider::query()->where('provider', $this->provider)->get(); - - return view( - 'livewire.servers.create-server', - compact([ - 'serverProviders', - ]) - ); - } -} diff --git a/app/Http/Livewire/Servers/DeleteServer.php b/app/Http/Livewire/Servers/DeleteServer.php deleted file mode 100644 index 7c63d23..0000000 --- a/app/Http/Livewire/Servers/DeleteServer.php +++ /dev/null @@ -1,29 +0,0 @@ -server = $server; - } - - public function delete(): void - { - $this->server->delete(); - - $this->redirect(route('servers')); - } - - public function render(): View - { - return view('livewire.servers.delete-server'); - } -} diff --git a/app/Http/Livewire/Servers/ServerStatus.php b/app/Http/Livewire/Servers/ServerStatus.php deleted file mode 100644 index 695cb21..0000000 --- a/app/Http/Livewire/Servers/ServerStatus.php +++ /dev/null @@ -1,20 +0,0 @@ -user(); - $servers = $user->currentProject->servers()->orderByDesc('created_at')->get(); - - return view('livewire.servers.servers-list', [ - 'servers' => $servers, - ]); - } -} diff --git a/app/Http/Livewire/Servers/ShowServer.php b/app/Http/Livewire/Servers/ShowServer.php deleted file mode 100644 index 0948cf7..0000000 --- a/app/Http/Livewire/Servers/ShowServer.php +++ /dev/null @@ -1,31 +0,0 @@ -redirect(route('servers.show', ['server' => $this->server])); - - return; - } - - $this->dispatch('refreshComponent'); - } - - public function render(): View - { - return view('livewire.servers.show-server'); - } -} diff --git a/app/Http/Livewire/Services/InstallPHPMyAdmin.php b/app/Http/Livewire/Services/InstallPHPMyAdmin.php deleted file mode 100644 index 92eabfe..0000000 --- a/app/Http/Livewire/Services/InstallPHPMyAdmin.php +++ /dev/null @@ -1,31 +0,0 @@ -install($this->server, $this->all()); - - $this->dispatch('started'); - - $this->dispatch('$refresh')->to(ServicesList::class); - } - - public function render(): View - { - return view('livewire.services.install-phpmyadmin'); - } -} diff --git a/app/Http/Livewire/Services/ServicesList.php b/app/Http/Livewire/Services/ServicesList.php deleted file mode 100644 index 4cc98f3..0000000 --- a/app/Http/Livewire/Services/ServicesList.php +++ /dev/null @@ -1,63 +0,0 @@ -server->services()->where('id', $id)->firstOrFail(); - - $service->stop(); - - $this->refreshComponent([]); - } - - public function start(int $id): void - { - /** @var Service $service */ - $service = $this->server->services()->where('id', $id)->firstOrFail(); - - $service->start(); - - $this->refreshComponent([]); - } - - public function restart(int $id): void - { - /** @var Service $service */ - $service = $this->server->services()->where('id', $id)->firstOrFail(); - - $service->restart(); - - $this->refreshComponent([]); - } - - public function uninstall(int $id): void - { - /** @var Service $service */ - $service = $this->server->services()->where('id', $id)->firstOrFail(); - - $service->uninstall(); - - $this->refreshComponent([]); - } - - public function render(): View - { - return view('livewire.services.services-list', [ - 'services' => $this->server->services, - ]); - } -} diff --git a/app/Http/Livewire/Sites/ChangePhpVersion.php b/app/Http/Livewire/Sites/ChangePhpVersion.php deleted file mode 100644 index 07ef34e..0000000 --- a/app/Http/Livewire/Sites/ChangePhpVersion.php +++ /dev/null @@ -1,41 +0,0 @@ -version = $site->php_version; - } - - public function change(): void - { - $this->site->changePHPVersion($this->version); - - session()->flash('status', 'changing-php-version'); - } - - public function refreshComponent(array $data): void - { - if (isset($data['type'])) { - session()->flash('status', $data['type']); - } - } - - public function render(): View - { - return view('livewire.sites.change-php-version'); - } -} diff --git a/app/Http/Livewire/Sites/CreateSite.php b/app/Http/Livewire/Sites/CreateSite.php deleted file mode 100644 index b93c5ed..0000000 --- a/app/Http/Livewire/Sites/CreateSite.php +++ /dev/null @@ -1,47 +0,0 @@ - '', - 'web_directory' => 'public', - 'source_control' => '', - 'php_version' => '', - ]; - - /** - * @throws SourceControlIsNotConnected - */ - public function create(): void - { - $site = app(\App\Actions\Site\CreateSite::class)->create( - $this->server, - $this->inputs - ); - - $this->redirect(route('servers.sites.show', [ - 'server' => $site->server, - 'site' => $site, - ])); - } - - public function render(): View - { - return view('livewire.sites.create-site', [ - 'sourceControls' => SourceControl::all(), - ]); - } -} diff --git a/app/Http/Livewire/Sites/DeleteSite.php b/app/Http/Livewire/Sites/DeleteSite.php deleted file mode 100644 index 4ad1056..0000000 --- a/app/Http/Livewire/Sites/DeleteSite.php +++ /dev/null @@ -1,27 +0,0 @@ -site->remove(); - - $this->redirect(route('servers.sites', ['server' => $this->site->server])); - } - - public function render(): View - { - return view('livewire.sites.delete-site'); - } -} diff --git a/app/Http/Livewire/Sites/ShowSite.php b/app/Http/Livewire/Sites/ShowSite.php deleted file mode 100644 index f3a2724..0000000 --- a/app/Http/Livewire/Sites/ShowSite.php +++ /dev/null @@ -1,20 +0,0 @@ -redirect( - route('servers.sites.show', [ - 'server' => $this->server, - 'site' => $data['data']['site']['id'], - ]) - ); - - return; - } - - $this->dispatch('refreshComponent'); - } - - public function render(): View - { - return view('livewire.sites.sites-list', [ - 'sites' => $this->server->sites()->latest()->get(), - ]); - } -} diff --git a/app/Http/Livewire/Sites/UpdateSourceControlProvider.php b/app/Http/Livewire/Sites/UpdateSourceControlProvider.php deleted file mode 100644 index ed72e47..0000000 --- a/app/Http/Livewire/Sites/UpdateSourceControlProvider.php +++ /dev/null @@ -1,33 +0,0 @@ -update($this->site, $this->all()); - - $this->resetErrorBag(); - - session()->flash('status', 'source-control-updated'); - } - - public function render(): View - { - if (! $this->source_control) { - $this->source_control = $this->site->source_control_id; - } - - return view('livewire.sites.update-source-control-provider'); - } -} diff --git a/app/Http/Livewire/Sites/UpdateVHost.php b/app/Http/Livewire/Sites/UpdateVHost.php deleted file mode 100644 index 26ec759..0000000 --- a/app/Http/Livewire/Sites/UpdateVHost.php +++ /dev/null @@ -1,41 +0,0 @@ -vHost = $this->site->server->webserver()->handler()->getVHost($this->site); - } - - public function update(): void - { - try { - $this->site->server->webserver()->handler()->updateVHost($this->site, false, $this->vHost); - - $this->toast()->success('VHost updated successfully!'); - } catch (Throwable $e) { - $this->toast()->error($e->getMessage()); - } - } - - public function render(): View - { - return view('livewire.sites.update-v-host'); - } -} diff --git a/app/Http/Livewire/SourceControls/Connect.php b/app/Http/Livewire/SourceControls/Connect.php deleted file mode 100644 index 9cadffa..0000000 --- a/app/Http/Livewire/SourceControls/Connect.php +++ /dev/null @@ -1,38 +0,0 @@ -connect($this->all()); - - $this->dispatch('$refresh')->to(SourceControlsList::class); - - $this->dispatch('connected'); - } - - public function render(): View - { - if (request()->query('provider')) { - $this->provider = request()->query('provider'); - } - - return view('livewire.source-controls.connect', [ - 'open' => ! is_null(request()->query('provider')), - ]); - } -} diff --git a/app/Http/Livewire/SourceControls/SourceControlsList.php b/app/Http/Livewire/SourceControls/SourceControlsList.php deleted file mode 100644 index 64ce831..0000000 --- a/app/Http/Livewire/SourceControls/SourceControlsList.php +++ /dev/null @@ -1,37 +0,0 @@ -findOrFail($this->deleteId); - - $provider->delete(); - - $this->refreshComponent([]); - - $this->dispatch('confirmed'); - } - - public function render(): View - { - return view('livewire.source-controls.source-controls-list', [ - 'sourceControls' => SourceControl::query()->latest()->get(), - ]); - } -} diff --git a/app/Http/Livewire/SshKeys/AddKey.php b/app/Http/Livewire/SshKeys/AddKey.php deleted file mode 100644 index d6099c6..0000000 --- a/app/Http/Livewire/SshKeys/AddKey.php +++ /dev/null @@ -1,31 +0,0 @@ -create( - auth()->user(), - $this->all() - ); - - $this->dispatch('$refresh')->to(KeysList::class); - - $this->dispatch('added'); - } - - public function render(): View - { - return view('livewire.ssh-keys.add-key'); - } -} diff --git a/app/Http/Livewire/SshKeys/KeysList.php b/app/Http/Livewire/SshKeys/KeysList.php deleted file mode 100644 index 9b5a401..0000000 --- a/app/Http/Livewire/SshKeys/KeysList.php +++ /dev/null @@ -1,37 +0,0 @@ -findOrFail($this->deleteId); - - $key->delete(); - - $this->refreshComponent([]); - - $this->dispatch('confirmed'); - } - - public function render(): View - { - return view('livewire.ssh-keys.keys-list', [ - 'keys' => SshKey::query()->latest()->get(), - ]); - } -} diff --git a/app/Http/Livewire/Ssl/CreateSsl.php b/app/Http/Livewire/Ssl/CreateSsl.php deleted file mode 100644 index 2277e4b..0000000 --- a/app/Http/Livewire/Ssl/CreateSsl.php +++ /dev/null @@ -1,35 +0,0 @@ -create($this->site, $this->all()); - - $this->dispatch('$refresh')->to(SslsList::class); - - $this->dispatch('created'); - } - - public function render(): View - { - return view('livewire.ssl.create-ssl'); - } -} diff --git a/app/Http/Livewire/Ssl/SslsList.php b/app/Http/Livewire/Ssl/SslsList.php deleted file mode 100644 index f7fb5a5..0000000 --- a/app/Http/Livewire/Ssl/SslsList.php +++ /dev/null @@ -1,46 +0,0 @@ -site->ssls()->where('id', $this->deleteId)->firstOrFail(); - - $ssl->remove(); - - $this->refreshComponent([]); - - $this->dispatch('confirmed'); - } - - public function refreshComponent(array $data): void - { - if (isset($data['type']) && $data['type'] == 'deploy-ssl-failed') { - $this->toast()->error(__('SSL creation failed!')); - } - - $this->dispatch('refreshComponent'); - } - - public function render(): View - { - return view('livewire.ssl.ssls-list', [ - 'ssls' => $this->site->ssls, - ]); - } -} diff --git a/app/Http/Livewire/StorageProviders/ConnectProvider.php b/app/Http/Livewire/StorageProviders/ConnectProvider.php deleted file mode 100644 index 4499896..0000000 --- a/app/Http/Livewire/StorageProviders/ConnectProvider.php +++ /dev/null @@ -1,44 +0,0 @@ -create(auth()->user(), $this->all()); - - $this->dispatch('$refresh')->to(ProvidersList::class); - - $this->dispatch('connected'); - } - - public function render(): View - { - return view('livewire.storage-providers.connect-provider'); - } -} diff --git a/app/Http/Livewire/StorageProviders/ProvidersList.php b/app/Http/Livewire/StorageProviders/ProvidersList.php deleted file mode 100644 index 865a05f..0000000 --- a/app/Http/Livewire/StorageProviders/ProvidersList.php +++ /dev/null @@ -1,37 +0,0 @@ -findOrFail($this->deleteId); - - $provider->delete(); - - $this->refreshComponent([]); - - $this->dispatch('confirmed'); - } - - public function render(): View - { - return view('livewire.storage-providers.providers-list', [ - 'providers' => StorageProvider::query()->latest()->get(), - ]); - } -} diff --git a/app/Http/Livewire/UserDropdown.php b/app/Http/Livewire/UserDropdown.php deleted file mode 100644 index 69db7c3..0000000 --- a/app/Http/Livewire/UserDropdown.php +++ /dev/null @@ -1,18 +0,0 @@ -exception) { + if ($res->exception instanceof SSHConnectionError || $res->exception instanceof SSHCommandError) { + Toast::error($res->exception->getMessage()); + + if ($request->hasHeader('HX-Request')) { + return htmx()->back(); + } + + return back(); + } + } + + return $res; + } +} diff --git a/app/Http/Requests/Auth/LoginRequest.php b/app/Http/Requests/Auth/LoginRequest.php deleted file mode 100644 index 7a19bc0..0000000 --- a/app/Http/Requests/Auth/LoginRequest.php +++ /dev/null @@ -1,85 +0,0 @@ - - */ - public function rules(): array - { - return [ - 'email' => ['required', 'string', 'email'], - 'password' => ['required', 'string'], - ]; - } - - /** - * Attempt to authenticate the request's credentials. - * - * @throws \Illuminate\Validation\ValidationException - */ - public function authenticate(): void - { - $this->ensureIsNotRateLimited(); - - if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) { - RateLimiter::hit($this->throttleKey()); - - throw ValidationException::withMessages([ - 'email' => trans('auth.failed'), - ]); - } - - RateLimiter::clear($this->throttleKey()); - } - - /** - * Ensure the login request is not rate limited. - * - * @throws \Illuminate\Validation\ValidationException - */ - public function ensureIsNotRateLimited(): void - { - if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) { - return; - } - - event(new Lockout($this)); - - $seconds = RateLimiter::availableIn($this->throttleKey()); - - throw ValidationException::withMessages([ - 'email' => trans('auth.throttle', [ - 'seconds' => $seconds, - 'minutes' => ceil($seconds / 60), - ]), - ]); - } - - /** - * Get the rate limiting throttle key for the request. - */ - public function throttleKey(): string - { - return Str::transliterate(Str::lower($this->input('email')).'|'.$this->ip()); - } -} diff --git a/app/Http/Requests/ProfileUpdateRequest.php b/app/Http/Requests/ProfileUpdateRequest.php deleted file mode 100644 index 327ce6f..0000000 --- a/app/Http/Requests/ProfileUpdateRequest.php +++ /dev/null @@ -1,23 +0,0 @@ - - */ - public function rules(): array - { - return [ - 'name' => ['string', 'max:255'], - 'email' => ['email', 'max:255', Rule::unique(User::class)->ignore($this->user()->id)], - ]; - } -} diff --git a/app/Jobs/Backup/RestoreDatabase.php b/app/Jobs/Backup/RestoreDatabase.php index 081e16a..aaa5773 100644 --- a/app/Jobs/Backup/RestoreDatabase.php +++ b/app/Jobs/Backup/RestoreDatabase.php @@ -3,7 +3,6 @@ namespace App\Jobs\Backup; use App\Enums\BackupFileStatus; -use App\Events\Broadcast; use App\Jobs\Job; use App\Models\BackupFile; use App\Models\Database; @@ -27,22 +26,11 @@ public function handle(): void $this->backupFile->status = BackupFileStatus::RESTORED; $this->backupFile->restored_at = now(); $this->backupFile->save(); - - event( - new Broadcast('backup-restore-finished', [ - 'file' => $this->backupFile, - ]) - ); } public function failed(): void { $this->backupFile->status = BackupFileStatus::RESTORE_FAILED; $this->backupFile->save(); - event( - new Broadcast('backup-restore-failed', [ - 'file' => $this->backupFile, - ]) - ); } } diff --git a/app/Jobs/Backup/RunBackup.php b/app/Jobs/Backup/RunBackup.php index d08b755..d4d9445 100644 --- a/app/Jobs/Backup/RunBackup.php +++ b/app/Jobs/Backup/RunBackup.php @@ -3,7 +3,6 @@ namespace App\Jobs\Backup; use App\Enums\BackupFileStatus; -use App\Events\Broadcast; use App\Jobs\Job; use App\Models\BackupFile; @@ -24,22 +23,11 @@ public function handle(): void $this->backupFile->status = BackupFileStatus::CREATED; $this->backupFile->save(); - - event( - new Broadcast('run-backup-finished', [ - 'file' => $this->backupFile, - ]) - ); } public function failed(): void { $this->backupFile->status = BackupFileStatus::FAILED; $this->backupFile->save(); - event( - new Broadcast('run-backup-failed', [ - 'file' => $this->backupFile, - ]) - ); } } diff --git a/app/Jobs/CronJob/AddToServer.php b/app/Jobs/CronJob/AddToServer.php deleted file mode 100644 index d1e28c4..0000000 --- a/app/Jobs/CronJob/AddToServer.php +++ /dev/null @@ -1,48 +0,0 @@ -cronJob = $cronJob; - } - - /** - * @throws Throwable - */ - public function handle(): void - { - $this->cronJob->server->ssh()->exec( - new UpdateCronJobsCommand($this->cronJob->user, $this->cronJob->crontab), - 'update-crontab' - ); - $this->cronJob->status = CronjobStatus::READY; - $this->cronJob->save(); - event( - new Broadcast('add-cronjob-finished', [ - 'cronJob' => $this->cronJob, - ]) - ); - } - - public function failed(): void - { - $this->cronJob->delete(); - event( - new Broadcast('add-cronjob-failed', [ - 'cronJob' => $this->cronJob, - ]) - ); - } -} diff --git a/app/Jobs/CronJob/RemoveFromServer.php b/app/Jobs/CronJob/RemoveFromServer.php deleted file mode 100644 index dce0992..0000000 --- a/app/Jobs/CronJob/RemoveFromServer.php +++ /dev/null @@ -1,46 +0,0 @@ -cronJob = $cronJob; - } - - /** - * @throws Throwable - */ - public function handle(): void - { - $this->cronJob->server->ssh()->exec( - new UpdateCronJobsCommand($this->cronJob->user, $this->cronJob->crontab), - 'update-crontab' - ); - $this->cronJob->delete(); - event( - new Broadcast('remove-cronjob-finished', [ - 'id' => $this->cronJob->id, - ]) - ); - } - - public function failed(): void - { - $this->cronJob->save(); - event( - new Broadcast('remove-cronjob-failed', [ - 'cronJob' => $this->cronJob, - ]) - ); - } -} diff --git a/app/Jobs/Database/CreateOnServer.php b/app/Jobs/Database/CreateOnServer.php deleted file mode 100644 index eee40a2..0000000 --- a/app/Jobs/Database/CreateOnServer.php +++ /dev/null @@ -1,35 +0,0 @@ -database = $database; - } - - public function handle(): void - { - $this->database->server->database()->handler()->create($this->database->name); - $this->database->status = DatabaseStatus::READY; - $this->database->save(); - event(new Broadcast('create-database-finished', [ - 'id' => $this->database->id, - ])); - } - - public function failed(): void - { - event(new Broadcast('create-database-failed', [ - 'id' => $this->database->id, - ])); - } -} diff --git a/app/Jobs/Database/DeleteFromServer.php b/app/Jobs/Database/DeleteFromServer.php deleted file mode 100644 index 57e432e..0000000 --- a/app/Jobs/Database/DeleteFromServer.php +++ /dev/null @@ -1,37 +0,0 @@ -database = $database; - } - - public function handle(): void - { - $this->database->server->database()->handler()->delete($this->database->name); - event( - new Broadcast('delete-database-finished', [ - 'id' => $this->database->id, - ]) - ); - $this->database->delete(); - } - - public function failed(): void - { - event( - new Broadcast('delete-database-failed', [ - 'id' => $this->database->id, - ]) - ); - } -} diff --git a/app/Jobs/DatabaseUser/CreateOnServer.php b/app/Jobs/DatabaseUser/CreateOnServer.php deleted file mode 100644 index a76839f..0000000 --- a/app/Jobs/DatabaseUser/CreateOnServer.php +++ /dev/null @@ -1,48 +0,0 @@ -databaseUser = $databaseUser; - } - - public function handle(): void - { - $this->databaseUser->server->database()->handler()->createUser( - $this->databaseUser->username, - $this->databaseUser->password, - $this->databaseUser->host - ); - $this->databaseUser->status = DatabaseUserStatus::READY; - $this->databaseUser->save(); - - if (count($this->databaseUser->databases) > 0) { - (new LinkUser($this->databaseUser))->handle(); - } - - event( - new Broadcast('create-database-user-finished', [ - 'id' => $this->databaseUser->id, - ]) - ); - } - - public function failed(): void - { - event( - new Broadcast('create-database-user-failed', [ - 'id' => $this->databaseUser->id, - ]) - ); - } -} diff --git a/app/Jobs/DatabaseUser/DeleteFromServer.php b/app/Jobs/DatabaseUser/DeleteFromServer.php deleted file mode 100644 index 65e58cb..0000000 --- a/app/Jobs/DatabaseUser/DeleteFromServer.php +++ /dev/null @@ -1,40 +0,0 @@ -databaseUser = $databaseUser; - } - - public function handle(): void - { - $this->databaseUser->server->database()->handler()->deleteUser( - $this->databaseUser->username, - $this->databaseUser->host - ); - event( - new Broadcast('delete-database-user-finished', [ - 'id' => $this->databaseUser->id, - ]) - ); - $this->databaseUser->delete(); - } - - public function failed(): void - { - event( - new Broadcast('delete-database-user-failed', [ - 'id' => $this->databaseUser->id, - ]) - ); - } -} diff --git a/app/Jobs/DatabaseUser/LinkUser.php b/app/Jobs/DatabaseUser/LinkUser.php deleted file mode 100644 index dfd6add..0000000 --- a/app/Jobs/DatabaseUser/LinkUser.php +++ /dev/null @@ -1,40 +0,0 @@ -databaseUser = $databaseUser; - } - - public function handle(): void - { - $this->databaseUser->server->database()->handler()->link( - $this->databaseUser->username, - $this->databaseUser->host, - $this->databaseUser->databases - ); - event( - new Broadcast('link-database-user-finished', [ - 'id' => $this->databaseUser->id, - ]) - ); - } - - public function failed(): void - { - event( - new Broadcast('link-database-user-failed', [ - 'id' => $this->databaseUser->id, - ]) - ); - } -} diff --git a/app/Jobs/DatabaseUser/UnlinkUser.php b/app/Jobs/DatabaseUser/UnlinkUser.php deleted file mode 100644 index 8ca8ffb..0000000 --- a/app/Jobs/DatabaseUser/UnlinkUser.php +++ /dev/null @@ -1,39 +0,0 @@ -databaseUser = $databaseUser; - } - - public function handle(): void - { - $this->databaseUser->server->database()->handler()->unlink( - $this->databaseUser->username, - $this->databaseUser->host, - ); - event( - new Broadcast('unlink-database-user-finished', [ - 'id' => $this->databaseUser->id, - ]) - ); - } - - public function failed(): void - { - event( - new Broadcast('unlink-database-user-failed', [ - 'id' => $this->databaseUser->id, - ]) - ); - } -} diff --git a/app/Jobs/Firewall/AddToServer.php b/app/Jobs/Firewall/AddToServer.php deleted file mode 100644 index 912a93b..0000000 --- a/app/Jobs/Firewall/AddToServer.php +++ /dev/null @@ -1,48 +0,0 @@ -firewallRule = $firewallRule; - } - - public function handle(): void - { - $this->firewallRule->server->firewall() - ->handler() - ->addRule( - $this->firewallRule->type, - $this->firewallRule->real_protocol, - $this->firewallRule->port, - $this->firewallRule->source, - $this->firewallRule->mask - ); - $this->firewallRule->status = FirewallRuleStatus::READY; - $this->firewallRule->save(); - event( - new Broadcast('create-firewall-rule-finished', [ - 'firewallRule' => $this->firewallRule, - ]) - ); - } - - public function failed(): void - { - $this->firewallRule->delete(); - event( - new Broadcast('create-firewall-rule-failed', [ - 'firewallRule' => $this->firewallRule, - ]) - ); - } -} diff --git a/app/Jobs/Firewall/RemoveFromServer.php b/app/Jobs/Firewall/RemoveFromServer.php deleted file mode 100644 index fc3644f..0000000 --- a/app/Jobs/Firewall/RemoveFromServer.php +++ /dev/null @@ -1,48 +0,0 @@ -firewallRule = $firewallRule; - } - - public function handle(): void - { - $this->firewallRule->server->firewall() - ->handler() - ->removeRule( - $this->firewallRule->type, - $this->firewallRule->real_protocol, - $this->firewallRule->port, - $this->firewallRule->source, - $this->firewallRule->mask - ); - $this->firewallRule->delete(); - event( - new Broadcast('delete-firewall-rule-finished', [ - 'id' => $this->firewallRule->id, - ]) - ); - } - - public function failed(): void - { - $this->firewallRule->status = FirewallRuleStatus::READY; - $this->firewallRule->save(); - event( - new Broadcast('delete-firewall-rule-failed', [ - 'firewallRule' => $this->firewallRule, - ]) - ); - } -} diff --git a/app/Jobs/Installation/InstallPHPMyAdmin.php b/app/Jobs/Installation/InstallPHPMyAdmin.php index b9fb301..cd83cbb 100644 --- a/app/Jobs/Installation/InstallPHPMyAdmin.php +++ b/app/Jobs/Installation/InstallPHPMyAdmin.php @@ -13,6 +13,9 @@ use Illuminate\Support\Str; use Throwable; +/** + * @deprecated + */ class InstallPHPMyAdmin extends Job { protected Service $service; diff --git a/app/Jobs/Installation/UninstallPHPMyAdmin.php b/app/Jobs/Installation/UninstallPHPMyAdmin.php index 5f11aae..9a80ec6 100644 --- a/app/Jobs/Installation/UninstallPHPMyAdmin.php +++ b/app/Jobs/Installation/UninstallPHPMyAdmin.php @@ -9,6 +9,9 @@ use Exception; use Throwable; +/** + * @deprecated + */ class UninstallPHPMyAdmin extends Job { protected Service $service; diff --git a/app/Jobs/PHP/SetDefaultCli.php b/app/Jobs/PHP/SetDefaultCli.php deleted file mode 100644 index 7cc9eba..0000000 --- a/app/Jobs/PHP/SetDefaultCli.php +++ /dev/null @@ -1,46 +0,0 @@ -service = $service; - } - - /** - * @throws Throwable - */ - public function handle(): void - { - $this->service->server->ssh()->exec( - new ChangeDefaultPHPCommand($this->service->version), - 'change-default-php' - ); - $this->service->server->defaultService('php')->update(['is_default' => 0]); - $this->service->update(['is_default' => 1]); - $this->service->update(['status' => ServiceStatus::READY]); - event( - new Broadcast('set-default-cli-finished', [ - 'defaultPHP' => $this->service->server->defaultService('php'), - ]) - ); - } - - public function failed(): void - { - event(new Broadcast('set-default-cli-failed', [ - 'defaultPHP' => $this->service->server->defaultService('php'), - ])); - } -} diff --git a/app/Jobs/Server/CheckConnection.php b/app/Jobs/Server/CheckConnection.php deleted file mode 100644 index 4165a2d..0000000 --- a/app/Jobs/Server/CheckConnection.php +++ /dev/null @@ -1,51 +0,0 @@ -server = $server; - } - - /** - * @throws Throwable - */ - public function handle(): void - { - $status = $this->server->status; - $this->server->ssh()->connect(); - $this->server->refresh(); - if ($status == 'disconnected') { - $this->server->status = 'ready'; - $this->server->save(); - } - event( - new Broadcast('server-status-finished', [ - 'server' => $this->server, - ]) - ); - } - - public function failed(): void - { - $this->server->status = 'disconnected'; - $this->server->save(); - Notifier::send($this->server, new ServerDisconnected($this->server)); - event( - new Broadcast('server-status-failed', [ - 'server' => $this->server, - ]) - ); - } -} diff --git a/app/Jobs/Server/RebootServer.php b/app/Jobs/Server/RebootServer.php deleted file mode 100644 index 5e55cf9..0000000 --- a/app/Jobs/Server/RebootServer.php +++ /dev/null @@ -1,45 +0,0 @@ -server = $server; - } - - /** - * @throws Throwable - */ - public function handle(): void - { - $this->server->ssh()->exec(new RebootCommand(), 'reboot'); - event( - new Broadcast('reboot-server-finished', [ - 'message' => __('The server is being rebooted. It can take several minutes to boot up'), - 'id' => $this->server->id, - ]) - ); - } - - public function failed(): void - { - $this->server->status = 'ready'; - $this->server->save(); - event( - new Broadcast('reboot-server-failed', [ - 'message' => __('Failed to reboot the server'), - 'server' => $this->server, - ]) - ); - } -} diff --git a/app/Jobs/Site/ChangePHPVersion.php b/app/Jobs/Site/ChangePHPVersion.php deleted file mode 100644 index e2b9c87..0000000 --- a/app/Jobs/Site/ChangePHPVersion.php +++ /dev/null @@ -1,43 +0,0 @@ -site = $site; - $this->version = $version; - } - - public function handle(): void - { - $this->site->php_version = $this->version; - $this->site->server->webserver()->handler()->changePHPVersion($this->site, $this->version); - $this->site->save(); - event( - new Broadcast('change-site-php-finished', [ - 'id' => $this->site->id, - 'php_version' => $this->site->php_version, - ]) - ); - } - - public function failed(): void - { - event( - new Broadcast('change-site-php-failed', [ - 'message' => __('Failed to change PHP!'), - 'id' => $this->site->id, - ]) - ); - } -} diff --git a/app/Models/CronJob.php b/app/Models/CronJob.php index cba2ee4..4a2cbed 100755 --- a/app/Models/CronJob.php +++ b/app/Models/CronJob.php @@ -2,9 +2,6 @@ namespace App\Models; -use App\Enums\CronjobStatus; -use App\Jobs\CronJob\AddToServer; -use App\Jobs\CronJob\RemoveFromServer; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -46,10 +43,10 @@ public function server(): BelongsTo return $this->belongsTo(Server::class); } - public function getCrontabAttribute(): string + public static function crontab(Server $server, string $user): string { $data = ''; - $cronJobs = $this->server->cronJobs()->where('user', $this->user)->get(); + $cronJobs = $server->cronJobs()->where('user', $user)->get(); foreach ($cronJobs as $key => $cronJob) { $data .= $cronJob->frequency.' '.$cronJob->command; if ($key != count($cronJobs) - 1) { @@ -60,18 +57,6 @@ public function getCrontabAttribute(): string return $data; } - public function addToServer(): void - { - dispatch(new AddToServer($this))->onConnection('ssh'); - } - - public function removeFromServer(): void - { - $this->status = CronjobStatus::DELETING; - $this->save(); - dispatch(new RemoveFromServer($this))->onConnection('ssh'); - } - public function getFrequencyLabelAttribute(): string { $labels = [ diff --git a/app/Models/Database.php b/app/Models/Database.php index 304f7d9..5a109c6 100755 --- a/app/Models/Database.php +++ b/app/Models/Database.php @@ -2,9 +2,6 @@ namespace App\Models; -use App\Enums\DatabaseStatus; -use App\Jobs\Database\CreateOnServer; -use App\Jobs\Database\DeleteFromServer; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; @@ -54,24 +51,6 @@ public function server(): BelongsTo return $this->belongsTo(Server::class); } - /** - * create database on server - */ - public function createOnServer(string $queue = 'ssh'): void - { - dispatch(new CreateOnServer($this))->onConnection($queue); - } - - /** - * delete database from server - */ - public function deleteFromServer(string $queue = 'ssh'): void - { - $this->status = DatabaseStatus::DELETING; - $this->save(); - dispatch(new DeleteFromServer($this))->onConnection($queue); - } - public function backups(): HasMany { return $this->hasMany(Backup::class)->where('type', 'database'); diff --git a/app/Models/DatabaseUser.php b/app/Models/DatabaseUser.php index 492a512..072eb21 100755 --- a/app/Models/DatabaseUser.php +++ b/app/Models/DatabaseUser.php @@ -2,11 +2,6 @@ namespace App\Models; -use App\Enums\DatabaseStatus; -use App\Jobs\DatabaseUser\CreateOnServer; -use App\Jobs\DatabaseUser\DeleteFromServer; -use App\Jobs\DatabaseUser\LinkUser; -use App\Jobs\DatabaseUser\UnlinkUser; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -54,41 +49,6 @@ public function scopeHasDatabase(Builder $query, string $databaseName): Builder return $query->where('databases', 'like', "%\"$databaseName\"%"); } - public function createOnServer(string $queue = 'ssh'): void - { - dispatch(new CreateOnServer($this))->onConnection($queue); - } - - public function deleteFromServer(string $queue = 'ssh'): void - { - $this->status = DatabaseStatus::DELETING; - $this->save(); - - dispatch(new DeleteFromServer($this))->onConnection($queue); - } - - public function linkNewDatabase(string $name): void - { - $linkedDatabases = $this->databases ?? []; - if (! in_array($name, $linkedDatabases)) { - $linkedDatabases[] = $name; - $this->databases = $linkedDatabases; - $this->unlinkUser(); - $this->linkUser(); - $this->save(); - } - } - - public function linkUser(string $queue = 'ssh'): void - { - dispatch(new LinkUser($this))->onConnection($queue); - } - - public function unlinkUser(string $queue = 'ssh'): void - { - dispatch(new UnlinkUser($this))->onConnection($queue); - } - public function getFullUserAttribute(): string { return $this->username.'@'.$this->host; diff --git a/app/Models/FirewallRule.php b/app/Models/FirewallRule.php index ef5a60e..e78d355 100755 --- a/app/Models/FirewallRule.php +++ b/app/Models/FirewallRule.php @@ -2,9 +2,6 @@ namespace App\Models; -use App\Enums\FirewallRuleStatus; -use App\Jobs\Firewall\AddToServer; -use App\Jobs\Firewall\RemoveFromServer; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -49,18 +46,6 @@ public function server(): BelongsTo return $this->belongsTo(Server::class); } - public function addToServer(): void - { - dispatch(new AddToServer($this))->onConnection('ssh'); - } - - public function removeFromServer(): void - { - $this->status = FirewallRuleStatus::DELETING; - $this->save(); - dispatch(new RemoveFromServer($this))->onConnection('ssh'); - } - public function getRealProtocolAttribute(): string { return $this->protocol === 'udp' ? 'udp' : 'tcp'; diff --git a/app/Models/Server.php b/app/Models/Server.php index 8092c49..a91cd7f 100755 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -2,14 +2,14 @@ namespace App\Models; +use App\Actions\Server\CheckConnection; +use App\Actions\Server\RebootServer; use App\Contracts\ServerType; use App\Enums\ServerStatus; use App\Enums\ServiceStatus; 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; @@ -384,9 +384,9 @@ public function getServiceUnits(): array return $units; } - public function checkConnection(): void + public function checkConnection(): self { - dispatch(new CheckConnection($this))->onConnection('ssh'); + return app(CheckConnection::class)->check($this); } public function installUpdates(): void @@ -397,11 +397,9 @@ public function installUpdates(): void dispatch(new Upgrade($this))->onConnection('ssh'); } - public function reboot(): void + public function reboot(): self { - $this->status = 'disconnected'; - $this->save(); - dispatch(new RebootServer($this))->onConnection('ssh'); + return app(RebootServer::class)->reboot($this); } public function getHostnameAttribute(): string diff --git a/app/Models/Site.php b/app/Models/Site.php index f2c59fb..02897ed 100755 --- a/app/Models/Site.php +++ b/app/Models/Site.php @@ -9,9 +9,7 @@ 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; @@ -234,7 +232,8 @@ public function php(): ?Service public function changePHPVersion($version): void { - dispatch(new ChangePHPVersion($this, $version))->onConnection('ssh'); + $this->php_version = $version; + $this->server->webserver()->handler()->changePHPVersion($this, $version); } public function getDeploymentScriptTextAttribute(): string @@ -288,11 +287,6 @@ public function getEnvAttribute(): string return $typeData['env']; } - public function deployEnv(): void - { - dispatch(new DeployEnv($this))->onConnection('ssh'); - } - public function activeSsl(): HasOne { return $this->hasOne(Ssl::class) diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 75855f8..3155398 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -4,6 +4,7 @@ use App\Helpers\Notifier; use App\Helpers\SSH; +use App\Helpers\Toast; use App\Support\SocialiteProviders\DropboxProvider; use Illuminate\Contracts\Container\BindingResolutionException; use Illuminate\Http\Resources\Json\ResourceCollection; @@ -34,12 +35,19 @@ public function boot(): void $this->app->bind('notifier', function () { return new Notifier; }); + $this->app->bind('toast', function () { + return new Toast; + }); $this->extendSocialite(); if (str(config('app.url'))->startsWith('https://')) { URL::forceScheme('https'); } + + if ($this->app->environment('local')) { + \App\Facades\SSH::fake(); + } } /** diff --git a/app/ServiceHandlers/PHP.php b/app/ServiceHandlers/PHP.php index fb337b6..1e64c9c 100644 --- a/app/ServiceHandlers/PHP.php +++ b/app/ServiceHandlers/PHP.php @@ -2,10 +2,9 @@ namespace App\ServiceHandlers; -use App\Enums\ServiceStatus; use App\Jobs\PHP\InstallPHPExtension; -use App\Jobs\PHP\SetDefaultCli; use App\Models\Service; +use App\SSHCommands\PHP\ChangeDefaultPHPCommand; class PHP { @@ -18,13 +17,14 @@ public function __construct(Service $service) public function setDefaultCli(): void { - $this->service->update(['status' => ServiceStatus::RESTARTING]); - - dispatch(new SetDefaultCli($this->service))->onConnection('ssh'); + $this->service->server->ssh()->exec( + new ChangeDefaultPHPCommand($this->service->version), + 'change-default-php' + ); } public function installExtension($name): void { - dispatch(new InstallPHPExtension($this->service, $name))->onConnection('ssh-long'); + dispatch(new InstallPHPExtension($this->service, $name))->onConnection('ssh'); } } diff --git a/app/SiteTypes/Wordpress.php b/app/SiteTypes/Wordpress.php index f6218a0..27c430e 100755 --- a/app/SiteTypes/Wordpress.php +++ b/app/SiteTypes/Wordpress.php @@ -2,6 +2,9 @@ namespace App\SiteTypes; +use App\Actions\Database\CreateDatabase; +use App\Actions\Database\CreateDatabaseUser; +use App\Actions\Database\LinkUser; use App\Enums\SiteFeature; use App\Jobs\Site\CreateVHost; use App\Jobs\Site\InstallWordpress; @@ -88,19 +91,19 @@ public function install(): void $this->progress(15), function () { /** @var Database $database */ - $database = $this->site->server->databases()->create([ + $database = app(CreateDatabase::class)->create($this->site->server, [ 'name' => $this->site->type_data['database'], ]); - $database->createOnServer('sync'); /** @var DatabaseUser $databaseUser */ - $databaseUser = $this->site->server->databaseUsers()->create([ + $databaseUser = app(CreateDatabaseUser::class)->create($this->site->server, [ 'username' => $this->site->type_data['database_user'], 'password' => $this->site->type_data['database_password'], - 'databases' => [$this->site->type_data['database']], + 'remote' => false, + 'host' => 'localhost', + ], [$database->name]); + app(LinkUser::class)->link($databaseUser, [ + 'databases' => [$database->name], ]); - $databaseUser->createOnServer('sync'); - $databaseUser->unlinkUser('sync'); - $databaseUser->linkUser('sync'); }, $this->progress(50), new InstallWordpress($this->site), diff --git a/app/Support/helpers.php b/app/Support/helpers.php index dc45fba..d22c9a2 100755 --- a/app/Support/helpers.php +++ b/app/Support/helpers.php @@ -1,36 +1,6 @@ format('Y-m-d H:i:s'); } -function cast_to_json(array $json): Illuminate\Database\Query\Expression|Expression +function htmx(): HtmxResponse { - $json = addslashes(json_encode($json)); - - return DB::raw("CAST('{$json}' AS JSON)"); + return new HtmxResponse(); } diff --git a/app/Traits/HasCustomPaginationView.php b/app/Traits/HasCustomPaginationView.php deleted file mode 100644 index 0bf06e9..0000000 --- a/app/Traits/HasCustomPaginationView.php +++ /dev/null @@ -1,15 +0,0 @@ - 'refreshComponent', - 'refreshComponent' => '$refresh', - '$refresh', - ]; - } - - public function refreshComponent(array $data): void - { - $this->dispatch('refreshComponent'); - } -} diff --git a/app/ValidationRules/CronRule.php b/app/ValidationRules/CronRule.php index 3b79b22..8f03a4d 100755 --- a/app/ValidationRules/CronRule.php +++ b/app/ValidationRules/CronRule.php @@ -7,9 +7,16 @@ class CronRule implements Rule { + private bool $acceptCustom; + + public function __construct(bool $acceptCustom = false) + { + $this->acceptCustom = $acceptCustom; + } + public function passes($attribute, $value): bool { - return CronExpression::isValidExpression($value); + return CronExpression::isValidExpression($value) || ($this->acceptCustom && $value === 'custom'); } public function message(): string diff --git a/composer.json b/composer.json index b71d6ef..57b3d3f 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,6 @@ "laravel/sanctum": "^3.2", "laravel/socialite": "^5.2", "laravel/tinker": "^2.8", - "livewire/livewire": "^3.0", "opcodesio/log-viewer": "^3.0", "owenvoke/blade-fontawesome": "^2.5", "phpseclib/phpseclib": "~3.0" diff --git a/composer.lock b/composer.lock index 57a8f6b..8690b7b 100644 --- a/composer.lock +++ b/composer.lock @@ -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": "f2e6a21fc0abada9bc40b4e80df42b26", + "content-hash": "9d9b253b12f7a1ca79c0e3c783f31f29", "packages": [ { "name": "aws/aws-crt-php", @@ -2929,81 +2929,6 @@ }, "time": "2022-04-15T14:02:14+00:00" }, - { - "name": "livewire/livewire", - "version": "v3.4.4", - "source": { - "type": "git", - "url": "https://github.com/livewire/livewire.git", - "reference": "c0489d4a76382f6dcf6e2702112f86aa089d0c8d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/livewire/livewire/zipball/c0489d4a76382f6dcf6e2702112f86aa089d0c8d", - "reference": "c0489d4a76382f6dcf6e2702112f86aa089d0c8d", - "shasum": "" - }, - "require": { - "illuminate/database": "^10.0|^11.0", - "illuminate/routing": "^10.0|^11.0", - "illuminate/support": "^10.0|^11.0", - "illuminate/validation": "^10.0|^11.0", - "league/mime-type-detection": "^1.9", - "php": "^8.1", - "symfony/http-kernel": "^6.2|^7.0" - }, - "require-dev": { - "calebporzio/sushi": "^2.1", - "laravel/framework": "^10.0|^11.0", - "laravel/prompts": "^0.1.6", - "mockery/mockery": "^1.3.1", - "orchestra/testbench": "8.20.0|^9.0", - "orchestra/testbench-dusk": "8.20.0|^9.0", - "phpunit/phpunit": "^10.4", - "psy/psysh": "^0.11.22|^0.12" - }, - "type": "library", - "extra": { - "laravel": { - "providers": [ - "Livewire\\LivewireServiceProvider" - ], - "aliases": { - "Livewire": "Livewire\\Livewire" - } - } - }, - "autoload": { - "files": [ - "src/helpers.php" - ], - "psr-4": { - "Livewire\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Caleb Porzio", - "email": "calebporzio@gmail.com" - } - ], - "description": "A front-end framework for Laravel.", - "support": { - "issues": "https://github.com/livewire/livewire/issues", - "source": "https://github.com/livewire/livewire/tree/v3.4.4" - }, - "funding": [ - { - "url": "https://github.com/livewire", - "type": "github" - } - ], - "time": "2024-01-28T19:07:11+00:00" - }, { "name": "monolog/monolog", "version": "3.5.0", diff --git a/config/livewire.php b/config/livewire.php deleted file mode 100644 index b660277..0000000 --- a/config/livewire.php +++ /dev/null @@ -1,159 +0,0 @@ - 'App\\Http\\Livewire', - - /* - |--------------------------------------------------------------------------- - | View Path - |--------------------------------------------------------------------------- - | - | This value is used to specify where Livewire component Blade templates are - | stored when running file creation commands like `artisan make:livewire`. - | It is also used if you choose to omit a component's render() method. - | - */ - - 'view_path' => resource_path('views/livewire'), - - /* - |--------------------------------------------------------------------------- - | Layout - |--------------------------------------------------------------------------- - | The view that will be used as the layout when rendering a single component - | as an entire page via `Route::get('/post/create', CreatePost::class);`. - | In this case, the view returned by CreatePost will render into $slot. - | - */ - - 'layout' => 'layouts.app', - - /* - |--------------------------------------------------------------------------- - | Lazy Loading Placeholder - |--------------------------------------------------------------------------- - | Livewire allows you to lazy load components that would otherwise slow down - | the initial page load. Every component can have a custom placeholder or - | you can define the default placeholder view for all components below. - | - */ - - 'lazy_placeholder' => null, - - /* - |--------------------------------------------------------------------------- - | Temporary File Uploads - |--------------------------------------------------------------------------- - | - | Livewire handles file uploads by storing uploads in a temporary directory - | before the file is stored permanently. All file uploads are directed to - | a global endpoint for temporary storage. You may configure this below: - | - */ - - 'temporary_file_upload' => [ - 'disk' => null, // Example: 'local', 's3' | Default: 'default' - 'rules' => null, // Example: ['file', 'mimes:png,jpg'] | Default: ['required', 'file', 'max:12288'] (12MB) - 'directory' => null, // Example: 'tmp' | Default: 'livewire-tmp' - 'middleware' => null, // Example: 'throttle:5,1' | Default: 'throttle:60,1' - 'preview_mimes' => [ // Supported file types for temporary pre-signed file URLs... - 'png', 'gif', 'bmp', 'svg', 'wav', 'mp4', - 'mov', 'avi', 'wmv', 'mp3', 'm4a', - 'jpg', 'jpeg', 'mpga', 'webp', 'wma', - ], - 'max_upload_time' => 5, // Max duration (in minutes) before an upload is invalidated... - ], - - /* - |--------------------------------------------------------------------------- - | Render On Redirect - |--------------------------------------------------------------------------- - | - | This value determines if Livewire will run a component's `render()` method - | after a redirect has been triggered using something like `redirect(...)` - | Setting this to true will render the view once more before redirecting - | - */ - - 'render_on_redirect' => false, - - /* - |--------------------------------------------------------------------------- - | Eloquent Model Binding - |--------------------------------------------------------------------------- - | - | Previous versions of Livewire supported binding directly to eloquent model - | properties using wire:model by default. However, this behavior has been - | deemed too "magical" and has therefore been put under a feature flag. - | - */ - - 'legacy_model_binding' => false, - - /* - |--------------------------------------------------------------------------- - | Auto-inject Frontend Assets - |--------------------------------------------------------------------------- - | - | By default, Livewire automatically injects its JavaScript and CSS into the - | and of pages containing Livewire components. By disabling - | this behavior, you need to use @livewireStyles and @livewireScripts. - | - */ - - 'inject_assets' => true, - - /* - |--------------------------------------------------------------------------- - | Navigate (SPA mode) - |--------------------------------------------------------------------------- - | - | By adding `wire:navigate` to links in your Livewire application, Livewire - | will prevent the default link handling and instead request those pages - | via AJAX, creating an SPA-like effect. Configure this behavior here. - | - */ - - 'navigate' => [ - 'show_progress_bar' => true, - 'progress_bar_color' => '#2299dd', - ], - - /* - |--------------------------------------------------------------------------- - | HTML Morph Markers - |--------------------------------------------------------------------------- - | - | Livewire intelligently "morphs" existing HTML into the newly rendered HTML - | after each update. To make this process more reliable, Livewire injects - | "markers" into the rendered Blade surrounding @if, @class & @foreach. - | - */ - - 'inject_morph_markers' => true, - - /* - |--------------------------------------------------------------------------- - | Pagination Theme - |--------------------------------------------------------------------------- - | - | When enabling Livewire's pagination feature by using the `WithPagination` - | trait, Livewire will use Tailwind templates to render pagination views - | on the page. If you want Bootstrap CSS, you can specify: "bootstrap" - | - */ - - 'pagination_theme' => 'tailwind', -]; diff --git a/database/migrations/2023_08_13_095440_update_storage_providers_table.php b/database/migrations/2023_08_13_095440_update_storage_providers_table.php index 93deebb..200140e 100644 --- a/database/migrations/2023_08_13_095440_update_storage_providers_table.php +++ b/database/migrations/2023_08_13_095440_update_storage_providers_table.php @@ -23,10 +23,10 @@ public function up(): void public function down(): void { Schema::table('storage_providers', function (Blueprint $table) { - $table->string('token'); - $table->string('refresh_token'); - $table->string('token_expires_at'); - $table->string('label'); + $table->string('token')->nullable(); + $table->string('refresh_token')->nullable(); + $table->string('token_expires_at')->nullable(); + $table->string('label')->nullable(); $table->dropColumn('user_id'); $table->dropColumn('profile'); $table->dropColumn('credentials'); diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 134354b..a8ffd52 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -45,6 +45,12 @@ public function run(): void 'version' => 'latest', 'status' => ServiceStatus::READY, ]); + $server->services()->create([ + 'type' => 'firewall', + 'name' => 'ufw', + 'version' => 'latest', + 'status' => ServiceStatus::READY, + ]); Site::factory()->create([ 'server_id' => $server->id, 'type' => SiteType::LARAVEL, diff --git a/install/install.sh b/install/install.sh index c86e060..0985e2e 100644 --- a/install/install.sh +++ b/install/install.sh @@ -60,6 +60,7 @@ apt autoremove -y # requirements apt install -y software-properties-common curl zip unzip git gcc + # nodejs curl -fsSL https://deb.nodesource.com/setup_lts.x | -E bash - apt update diff --git a/package-lock.json b/package-lock.json index 50f137b..384dd67 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,9 +11,13 @@ "alpinejs": "^3.4.2", "autoprefixer": "^10.4.2", "axios": "^1.6.0", + "htmx.org": "^1.9.10", "laravel-echo": "^1.15.0", "laravel-vite-plugin": "^0.7.2", "postcss": "^8.4.31", + "prettier": "^3.2.5", + "prettier-plugin-blade": "^2.1.6", + "prettier-plugin-tailwindcss": "^0.5.11", "pusher-js": "^4.3.1", "tailwindcss": "^3.1.0", "toastr": "^2.1.4", @@ -1035,6 +1039,12 @@ "node": ">= 0.4.0" } }, + "node_modules/htmx.org": { + "version": "1.9.10", + "resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-1.9.10.tgz", + "integrity": "sha512-UgchasltTCrTuU2DQLom3ohHrBvwr7OqpwyAVJ9VxtNBng4XKkVsqrv0Qr3srqvM9ZNI3f1MmvVQQqK7KW/bTA==", + "dev": true + }, "node_modules/http-parser-js": { "version": "0.5.8", "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", @@ -1510,6 +1520,102 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "dev": true }, + "node_modules/prettier": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", + "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-plugin-blade": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/prettier-plugin-blade/-/prettier-plugin-blade-2.1.6.tgz", + "integrity": "sha512-zuLan1zhaDcX1NVBo71KKW5QY59+3dLWRGapVOyVq7WrRUyaUR5jkCTRlcAky9GlMw+f+ryQMYy4jIo3HOzGxQ==", + "dev": true, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "prettier": ">=3" + } + }, + "node_modules/prettier-plugin-tailwindcss": { + "version": "0.5.11", + "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.5.11.tgz", + "integrity": "sha512-AvI/DNyMctyyxGOjyePgi/gqj5hJYClZ1avtQvLlqMT3uDZkRbi4HhGUpok3DRzv9z7Lti85Kdj3s3/1CeNI0w==", + "dev": true, + "engines": { + "node": ">=14.21.3" + }, + "peerDependencies": { + "@ianvs/prettier-plugin-sort-imports": "*", + "@prettier/plugin-pug": "*", + "@shopify/prettier-plugin-liquid": "*", + "@trivago/prettier-plugin-sort-imports": "*", + "prettier": "^3.0", + "prettier-plugin-astro": "*", + "prettier-plugin-css-order": "*", + "prettier-plugin-import-sort": "*", + "prettier-plugin-jsdoc": "*", + "prettier-plugin-marko": "*", + "prettier-plugin-organize-attributes": "*", + "prettier-plugin-organize-imports": "*", + "prettier-plugin-style-order": "*", + "prettier-plugin-svelte": "*" + }, + "peerDependenciesMeta": { + "@ianvs/prettier-plugin-sort-imports": { + "optional": true + }, + "@prettier/plugin-pug": { + "optional": true + }, + "@shopify/prettier-plugin-liquid": { + "optional": true + }, + "@trivago/prettier-plugin-sort-imports": { + "optional": true + }, + "prettier-plugin-astro": { + "optional": true + }, + "prettier-plugin-css-order": { + "optional": true + }, + "prettier-plugin-import-sort": { + "optional": true + }, + "prettier-plugin-jsdoc": { + "optional": true + }, + "prettier-plugin-marko": { + "optional": true + }, + "prettier-plugin-organize-attributes": { + "optional": true + }, + "prettier-plugin-organize-imports": { + "optional": true + }, + "prettier-plugin-style-order": { + "optional": true + }, + "prettier-plugin-svelte": { + "optional": true + }, + "prettier-plugin-twig-melody": { + "optional": true + } + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -2604,6 +2710,12 @@ "function-bind": "^1.1.1" } }, + "htmx.org": { + "version": "1.9.10", + "resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-1.9.10.tgz", + "integrity": "sha512-UgchasltTCrTuU2DQLom3ohHrBvwr7OqpwyAVJ9VxtNBng4XKkVsqrv0Qr3srqvM9ZNI3f1MmvVQQqK7KW/bTA==", + "dev": true + }, "http-parser-js": { "version": "0.5.8", "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", @@ -2927,6 +3039,26 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "dev": true }, + "prettier": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", + "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "dev": true + }, + "prettier-plugin-blade": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/prettier-plugin-blade/-/prettier-plugin-blade-2.1.6.tgz", + "integrity": "sha512-zuLan1zhaDcX1NVBo71KKW5QY59+3dLWRGapVOyVq7WrRUyaUR5jkCTRlcAky9GlMw+f+ryQMYy4jIo3HOzGxQ==", + "dev": true, + "requires": {} + }, + "prettier-plugin-tailwindcss": { + "version": "0.5.11", + "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.5.11.tgz", + "integrity": "sha512-AvI/DNyMctyyxGOjyePgi/gqj5hJYClZ1avtQvLlqMT3uDZkRbi4HhGUpok3DRzv9z7Lti85Kdj3s3/1CeNI0w==", + "dev": true, + "requires": {} + }, "proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", diff --git a/package.json b/package.json index 3a31d36..cf6f0eb 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,9 @@ "private": true, "scripts": { "dev": "vite", - "build": "vite build" + "build": "vite build", + "lint": "prettier --check ./resources/views", + "lint:fix": "prettier --write ./resources/views" }, "devDependencies": { "@ryangjchandler/alpine-clipboard": "^2.2.0", @@ -11,9 +13,13 @@ "alpinejs": "^3.4.2", "autoprefixer": "^10.4.2", "axios": "^1.6.0", + "htmx.org": "^1.9.10", "laravel-echo": "^1.15.0", "laravel-vite-plugin": "^0.7.2", "postcss": "^8.4.31", + "prettier": "^3.2.5", + "prettier-plugin-blade": "^2.1.6", + "prettier-plugin-tailwindcss": "^0.5.11", "pusher-js": "^4.3.1", "tailwindcss": "^3.1.0", "toastr": "^2.1.4", diff --git a/public/build/assets/app-5db42bc3.css b/public/build/assets/app-5db42bc3.css new file mode 100644 index 0000000..c4d4248 --- /dev/null +++ b/public/build/assets/app-5db42bc3.css @@ -0,0 +1 @@ +.toast-title{font-weight:700}.toast-message{-ms-word-wrap:break-word;word-wrap:break-word}.toast-message a,.toast-message label{color:#fff}.toast-message a:hover{color:#ccc;text-decoration:none}.toast-close-button{position:relative;right:-.3em;top:-.3em;float:right;font-size:20px;font-weight:700;color:#fff;-webkit-text-shadow:0 1px 0 #ffffff;text-shadow:0 1px 0 #ffffff;opacity:.8;-ms-filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=80);filter:alpha(opacity=80);line-height:1}.toast-close-button:hover,.toast-close-button:focus{color:#000;text-decoration:none;cursor:pointer;opacity:.4;-ms-filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=40);filter:alpha(opacity=40)}.rtl .toast-close-button{left:-.3em;float:left;right:.3em}button.toast-close-button{padding:0;cursor:pointer;background:transparent;border:0;-webkit-appearance:none}.toast-top-center{top:0;right:0;width:100%}.toast-bottom-center{bottom:0;right:0;width:100%}.toast-top-full-width{top:0;right:0;width:100%}.toast-bottom-full-width{bottom:0;right:0;width:100%}.toast-top-left{top:12px;left:12px}.toast-top-right{top:12px;right:12px}.toast-bottom-right{right:12px;bottom:12px}.toast-bottom-left{bottom:12px;left:12px}#toast-container{position:fixed;z-index:999999;pointer-events:none}#toast-container *{box-sizing:border-box}#toast-container>div{border-radius:.5rem;position:relative;pointer-events:auto;overflow:hidden;margin:0 0 6px;padding:15px 15px 15px 50px;width:300px;background-position:15px center;background-repeat:no-repeat;color:#fff}#toast-container>div.rtl{direction:rtl;padding:15px 50px 15px 15px;background-position:right 15px center}#toast-container>div:hover{cursor:pointer;--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / .1), 0 2px 4px -2px rgb(0 0 0 / .1);--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}#toast-container>.toast-info{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGwSURBVEhLtZa9SgNBEMc9sUxxRcoUKSzSWIhXpFMhhYWFhaBg4yPYiWCXZxBLERsLRS3EQkEfwCKdjWJAwSKCgoKCcudv4O5YLrt7EzgXhiU3/4+b2ckmwVjJSpKkQ6wAi4gwhT+z3wRBcEz0yjSseUTrcRyfsHsXmD0AmbHOC9Ii8VImnuXBPglHpQ5wwSVM7sNnTG7Za4JwDdCjxyAiH3nyA2mtaTJufiDZ5dCaqlItILh1NHatfN5skvjx9Z38m69CgzuXmZgVrPIGE763Jx9qKsRozWYw6xOHdER+nn2KkO+Bb+UV5CBN6WC6QtBgbRVozrahAbmm6HtUsgtPC19tFdxXZYBOfkbmFJ1VaHA1VAHjd0pp70oTZzvR+EVrx2Ygfdsq6eu55BHYR8hlcki+n+kERUFG8BrA0BwjeAv2M8WLQBtcy+SD6fNsmnB3AlBLrgTtVW1c2QN4bVWLATaIS60J2Du5y1TiJgjSBvFVZgTmwCU+dAZFoPxGEEs8nyHC9Bwe2GvEJv2WXZb0vjdyFT4Cxk3e/kIqlOGoVLwwPevpYHT+00T+hWwXDf4AJAOUqWcDhbwAAAAASUVORK5CYII=)!important}#toast-container>.toast-error{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAHOSURBVEhLrZa/SgNBEMZzh0WKCClSCKaIYOED+AAKeQQLG8HWztLCImBrYadgIdY+gIKNYkBFSwu7CAoqCgkkoGBI/E28PdbLZmeDLgzZzcx83/zZ2SSXC1j9fr+I1Hq93g2yxH4iwM1vkoBWAdxCmpzTxfkN2RcyZNaHFIkSo10+8kgxkXIURV5HGxTmFuc75B2RfQkpxHG8aAgaAFa0tAHqYFfQ7Iwe2yhODk8+J4C7yAoRTWI3w/4klGRgR4lO7Rpn9+gvMyWp+uxFh8+H+ARlgN1nJuJuQAYvNkEnwGFck18Er4q3egEc/oO+mhLdKgRyhdNFiacC0rlOCbhNVz4H9FnAYgDBvU3QIioZlJFLJtsoHYRDfiZoUyIxqCtRpVlANq0EU4dApjrtgezPFad5S19Wgjkc0hNVnuF4HjVA6C7QrSIbylB+oZe3aHgBsqlNqKYH48jXyJKMuAbiyVJ8KzaB3eRc0pg9VwQ4niFryI68qiOi3AbjwdsfnAtk0bCjTLJKr6mrD9g8iq/S/B81hguOMlQTnVyG40wAcjnmgsCNESDrjme7wfftP4P7SP4N3CJZdvzoNyGq2c/HWOXJGsvVg+RA/k2MC/wN6I2YA2Pt8GkAAAAASUVORK5CYII=)!important}#toast-container>.toast-success{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADsSURBVEhLY2AYBfQMgf///3P8+/evAIgvA/FsIF+BavYDDWMBGroaSMMBiE8VC7AZDrIFaMFnii3AZTjUgsUUWUDA8OdAH6iQbQEhw4HyGsPEcKBXBIC4ARhex4G4BsjmweU1soIFaGg/WtoFZRIZdEvIMhxkCCjXIVsATV6gFGACs4Rsw0EGgIIH3QJYJgHSARQZDrWAB+jawzgs+Q2UO49D7jnRSRGoEFRILcdmEMWGI0cm0JJ2QpYA1RDvcmzJEWhABhD/pqrL0S0CWuABKgnRki9lLseS7g2AlqwHWQSKH4oKLrILpRGhEQCw2LiRUIa4lwAAAABJRU5ErkJggg==)!important}#toast-container>.toast-warning{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGYSURBVEhL5ZSvTsNQFMbXZGICMYGYmJhAQIJAICYQPAACiSDB8AiICQQJT4CqQEwgJvYASAQCiZiYmJhAIBATCARJy+9rTsldd8sKu1M0+dLb057v6/lbq/2rK0mS/TRNj9cWNAKPYIJII7gIxCcQ51cvqID+GIEX8ASG4B1bK5gIZFeQfoJdEXOfgX4QAQg7kH2A65yQ87lyxb27sggkAzAuFhbbg1K2kgCkB1bVwyIR9m2L7PRPIhDUIXgGtyKw575yz3lTNs6X4JXnjV+LKM/m3MydnTbtOKIjtz6VhCBq4vSm3ncdrD2lk0VgUXSVKjVDJXJzijW1RQdsU7F77He8u68koNZTz8Oz5yGa6J3H3lZ0xYgXBK2QymlWWA+RWnYhskLBv2vmE+hBMCtbA7KX5drWyRT/2JsqZ2IvfB9Y4bWDNMFbJRFmC9E74SoS0CqulwjkC0+5bpcV1CZ8NMej4pjy0U+doDQsGyo1hzVJttIjhQ7GnBtRFN1UarUlH8F3xict+HY07rEzoUGPlWcjRFRr4/gChZgc3ZL2d8oAAAAASUVORK5CYII=)!important}#toast-container.toast-top-center>div,#toast-container.toast-bottom-center>div{width:300px;margin-left:auto;margin-right:auto}#toast-container.toast-top-full-width>div,#toast-container.toast-bottom-full-width>div{width:96%;margin-left:auto;margin-right:auto}.toast{background-color:#030303}.toast-success{--tw-bg-opacity: 1;background-color:rgb(34 197 94 / var(--tw-bg-opacity))}.toast-error{--tw-bg-opacity: 1;background-color:rgb(239 68 68 / var(--tw-bg-opacity))}.toast-info{--tw-bg-opacity: 1;background-color:rgb(99 102 241 / var(--tw-bg-opacity))}.toast-warning{--tw-bg-opacity: 1;background-color:rgb(234 179 8 / var(--tw-bg-opacity))}.toast-progress{position:absolute;left:0;bottom:0;height:4px;background-color:#000;opacity:.4;-ms-filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=40);filter:alpha(opacity=40)}@media all and (max-width: 240px){#toast-container>div{padding:8px 8px 8px 50px;width:11em}#toast-container>div.rtl{padding:8px 50px 8px 8px}#toast-container .toast-close-button{right:-.2em;top:-.2em}#toast-container .rtl .toast-close-button{left:-.2em;right:.2em}}@media all and (min-width: 241px) and (max-width: 480px){#toast-container>div{padding:8px 8px 8px 50px;width:18em}#toast-container>div.rtl{padding:8px 50px 8px 8px}#toast-container .toast-close-button{right:-.2em;top:-.2em}#toast-container .rtl .toast-close-button{left:-.2em;right:.2em}}@media all and (min-width: 481px) and (max-width: 768px){#toast-container>div{padding:15px 15px 15px 50px;width:25em}#toast-container>div.rtl{padding:15px 50px 15px 15px}}/*! tailwindcss v3.3.1 | MIT License | https://tailwindcss.com*/*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e2e8f0}:before,:after{--tw-content: ""}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:Figtree,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#94a3b8}input::placeholder,textarea::placeholder{opacity:1;color:#94a3b8}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}[type=text],[type=email],[type=url],[type=password],[type=number],[type=date],[type=datetime-local],[type=month],[type=search],[type=tel],[type=time],[type=week],[multiple],textarea,select{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;border-color:#64748b;border-width:1px;border-radius:0;padding:.5rem .75rem;font-size:1rem;line-height:1.5rem;--tw-shadow: 0 0 #0000}[type=text]:focus,[type=email]:focus,[type=url]:focus,[type=password]:focus,[type=number]:focus,[type=date]:focus,[type=datetime-local]:focus,[type=month]:focus,[type=search]:focus,[type=tel]:focus,[type=time]:focus,[type=week]:focus,[multiple]:focus,textarea:focus,select:focus{outline:2px solid transparent;outline-offset:2px;--tw-ring-inset: var(--tw-empty, );--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: #2563eb;--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);border-color:#2563eb}input::-moz-placeholder,textarea::-moz-placeholder{color:#64748b;opacity:1}input::placeholder,textarea::placeholder{color:#64748b;opacity:1}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-date-and-time-value{min-height:1.5em}::-webkit-datetime-edit,::-webkit-datetime-edit-year-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-meridiem-field{padding-top:0;padding-bottom:0}select{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%2364748b' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");background-position:right .5rem center;background-repeat:no-repeat;background-size:1.5em 1.5em;padding-right:2.5rem;-webkit-print-color-adjust:exact;print-color-adjust:exact}[multiple]{background-image:initial;background-position:initial;background-repeat:unset;background-size:initial;padding-right:.75rem;-webkit-print-color-adjust:unset;print-color-adjust:unset}[type=checkbox],[type=radio]{-webkit-appearance:none;-moz-appearance:none;appearance:none;padding:0;-webkit-print-color-adjust:exact;print-color-adjust:exact;display:inline-block;vertical-align:middle;background-origin:border-box;-webkit-user-select:none;-moz-user-select:none;user-select:none;flex-shrink:0;height:1rem;width:1rem;color:#2563eb;background-color:#fff;border-color:#64748b;border-width:1px;--tw-shadow: 0 0 #0000}[type=checkbox]{border-radius:0}[type=radio]{border-radius:100%}[type=checkbox]:focus,[type=radio]:focus{outline:2px solid transparent;outline-offset:2px;--tw-ring-inset: var(--tw-empty, );--tw-ring-offset-width: 2px;--tw-ring-offset-color: #fff;--tw-ring-color: #2563eb;--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}[type=checkbox]:checked,[type=radio]:checked{border-color:transparent;background-color:currentColor;background-size:100% 100%;background-position:center;background-repeat:no-repeat}[type=checkbox]:checked{background-image:url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e")}[type=radio]:checked{background-image:url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='8' cy='8' r='3'/%3e%3c/svg%3e")}[type=checkbox]:checked:hover,[type=checkbox]:checked:focus,[type=radio]:checked:hover,[type=radio]:checked:focus{border-color:transparent;background-color:currentColor}[type=checkbox]:indeterminate{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3e%3cpath stroke='white' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3e%3c/svg%3e");border-color:transparent;background-color:currentColor;background-size:100% 100%;background-position:center;background-repeat:no-repeat}[type=checkbox]:indeterminate:hover,[type=checkbox]:indeterminate:focus{border-color:transparent;background-color:currentColor}[type=file]{background:unset;border-color:inherit;border-width:0;border-radius:0;padding:0;font-size:unset;line-height:inherit}[type=file]:focus{outline:1px solid ButtonText;outline:1px auto -webkit-focus-ring-color}*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.container{width:100%}@media (min-width: 640px){.container{max-width:640px}}@media (min-width: 768px){.container{max-width:768px}}@media (min-width: 1024px){.container{max-width:1024px}}@media (min-width: 1280px){.container{max-width:1280px}}@media (min-width: 1536px){.container{max-width:1536px}}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{top:0;right:0;bottom:0;left:0}.inset-y-0{top:0;bottom:0}.left-0{left:0}.right-0{right:0}.top-0{top:0}.top-1{top:.25rem}.z-0{z-index:0}.z-10{z-index:10}.z-50{z-index:50}.float-right{float:right}.mx-5{margin-left:1.25rem;margin-right:1.25rem}.mx-auto{margin-left:auto;margin-right:auto}.-ml-px{margin-left:-1px}.-mt-px{margin-top:-1px}.mb-1{margin-bottom:.25rem}.mb-10{margin-bottom:2.5rem}.mb-2{margin-bottom:.5rem}.mb-4{margin-bottom:1rem}.mb-5{margin-bottom:1.25rem}.mb-6{margin-bottom:1.5rem}.ml-1{margin-left:.25rem}.ml-12{margin-left:3rem}.ml-2{margin-left:.5rem}.ml-3{margin-left:.75rem}.ml-4{margin-left:1rem}.mr-1{margin-right:.25rem}.mr-2{margin-right:.5rem}.mr-3{margin-right:.75rem}.mt-1{margin-top:.25rem}.mt-10{margin-top:2.5rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-5{margin-top:1.25rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-10{height:2.5rem}.h-16{height:4rem}.h-20{height:5rem}.h-4{height:1rem}.h-48{height:12rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-7{height:1.75rem}.h-8{height:2rem}.h-9{height:2.25rem}.h-96{height:24rem}.max-h-\[350px\]{max-height:350px}.min-h-screen{min-height:100vh}.w-1{width:.25rem}.w-10{width:2.5rem}.w-4{width:1rem}.w-48{width:12rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-64{width:16rem}.w-7{width:1.75rem}.w-8{width:2rem}.w-9{width:2.25rem}.w-auto{width:auto}.w-full{width:100%}.w-max{width:-moz-max-content;width:max-content}.min-w-full{min-width:100%}.min-w-max{min-width:-moz-max-content;min-width:max-content}.max-w-6xl{max-width:72rem}.max-w-7xl{max-width:80rem}.max-w-full{max-width:100%}.max-w-xl{max-width:36rem}.flex-1{flex:1 1 0%}.flex-none{flex:none}.flex-grow{flex-grow:1}.origin-top{transform-origin:top}.origin-top-left{transform-origin:top left}.origin-top-right{transform-origin:top right}.translate-y-0{--tw-translate-y: 0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-y-4{--tw-translate-y: 1rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.scale-100{--tw-scale-x: 1;--tw-scale-y: 1;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.scale-95{--tw-scale-x: .95;--tw-scale-y: .95;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.cursor-default{cursor:default}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.flex-col{flex-direction:column}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.justify-start{justify-content:flex-start}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.justify-items-center{justify-items:center}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-5{gap:1.25rem}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.space-y-10>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(2.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(2.5rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.overflow-y-hidden{overflow-y:hidden}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-line{white-space:pre-line}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-sm{border-radius:.125rem}.rounded-b-md{border-bottom-right-radius:.375rem;border-bottom-left-radius:.375rem}.rounded-l-md{border-top-left-radius:.375rem;border-bottom-left-radius:.375rem}.rounded-r-md{border-top-right-radius:.375rem;border-bottom-right-radius:.375rem}.rounded-t-md{border-top-left-radius:.375rem;border-top-right-radius:.375rem}.border{border-width:1px}.border-2{border-width:2px}.border-b{border-bottom-width:1px}.border-b-2{border-bottom-width:2px}.border-l{border-left-width:1px}.border-l-4{border-left-width:4px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-gray-100{--tw-border-opacity: 1;border-color:rgb(241 245 249 / var(--tw-border-opacity))}.border-gray-200{--tw-border-opacity: 1;border-color:rgb(226 232 240 / var(--tw-border-opacity))}.border-gray-300{--tw-border-opacity: 1;border-color:rgb(203 213 225 / var(--tw-border-opacity))}.border-gray-400{--tw-border-opacity: 1;border-color:rgb(148 163 184 / var(--tw-border-opacity))}.border-primary-200{--tw-border-opacity: 1;border-color:rgb(199 210 254 / var(--tw-border-opacity))}.border-primary-400{--tw-border-opacity: 1;border-color:rgb(129 140 248 / var(--tw-border-opacity))}.border-primary-600{--tw-border-opacity: 1;border-color:rgb(79 70 229 / var(--tw-border-opacity))}.border-transparent{border-color:transparent}.border-yellow-500{--tw-border-opacity: 1;border-color:rgb(234 179 8 / var(--tw-border-opacity))}.border-t-transparent{border-top-color:transparent}.bg-gray-100{--tw-bg-opacity: 1;background-color:rgb(241 245 249 / var(--tw-bg-opacity))}.bg-gray-200{--tw-bg-opacity: 1;background-color:rgb(226 232 240 / var(--tw-bg-opacity))}.bg-gray-50{--tw-bg-opacity: 1;background-color:rgb(248 250 252 / var(--tw-bg-opacity))}.bg-gray-500{--tw-bg-opacity: 1;background-color:rgb(100 116 139 / var(--tw-bg-opacity))}.bg-gray-800{--tw-bg-opacity: 1;background-color:rgb(30 41 59 / var(--tw-bg-opacity))}.bg-gray-900{--tw-bg-opacity: 1;background-color:rgb(15 23 42 / var(--tw-bg-opacity))}.bg-green-50{--tw-bg-opacity: 1;background-color:rgb(240 253 244 / var(--tw-bg-opacity))}.bg-primary-200{--tw-bg-opacity: 1;background-color:rgb(199 210 254 / var(--tw-bg-opacity))}.bg-primary-50{--tw-bg-opacity: 1;background-color:rgb(238 242 255 / var(--tw-bg-opacity))}.bg-primary-500{--tw-bg-opacity: 1;background-color:rgb(99 102 241 / var(--tw-bg-opacity))}.bg-primary-600{--tw-bg-opacity: 1;background-color:rgb(79 70 229 / var(--tw-bg-opacity))}.bg-red-50{--tw-bg-opacity: 1;background-color:rgb(254 242 242 / var(--tw-bg-opacity))}.bg-red-600{--tw-bg-opacity: 1;background-color:rgb(220 38 38 / var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity))}.bg-yellow-100{--tw-bg-opacity: 1;background-color:rgb(254 249 195 / var(--tw-bg-opacity))}.bg-yellow-50{--tw-bg-opacity: 1;background-color:rgb(254 252 232 / var(--tw-bg-opacity))}.p-1{padding:.25rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.p-7{padding:1.75rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.px-8{padding-left:2rem;padding-right:2rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-12{padding-top:3rem;padding-bottom:3rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-5{padding-top:1.25rem;padding-bottom:1.25rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.pb-1{padding-bottom:.25rem}.pb-2{padding-bottom:.5rem}.pl-3{padding-left:.75rem}.pr-10{padding-right:2.5rem}.pr-2{padding-right:.5rem}.pr-3{padding-right:.75rem}.pr-4{padding-right:1rem}.pt-1{padding-top:.25rem}.pt-3{padding-top:.75rem}.pt-6{padding-top:1.5rem}.pt-8{padding-top:2rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.align-middle{vertical-align:middle}.font-sans{font-family:Figtree,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji"}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-extrabold{font-weight:800}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.lowercase{text-transform:lowercase}.capitalize{text-transform:capitalize}.leading-4{line-height:1rem}.leading-5{line-height:1.25rem}.leading-7{line-height:1.75rem}.tracking-wider{letter-spacing:.05em}.text-black{--tw-text-opacity: 1;color:rgb(0 0 0 / var(--tw-text-opacity))}.text-gray-100{--tw-text-opacity: 1;color:rgb(241 245 249 / var(--tw-text-opacity))}.text-gray-200{--tw-text-opacity: 1;color:rgb(226 232 240 / var(--tw-text-opacity))}.text-gray-300{--tw-text-opacity: 1;color:rgb(203 213 225 / var(--tw-text-opacity))}.text-gray-400{--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity))}.text-gray-50{--tw-text-opacity: 1;color:rgb(248 250 252 / var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity: 1;color:rgb(100 116 139 / var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity: 1;color:rgb(71 85 105 / var(--tw-text-opacity))}.text-gray-700{--tw-text-opacity: 1;color:rgb(51 65 85 / var(--tw-text-opacity))}.text-gray-800{--tw-text-opacity: 1;color:rgb(30 41 59 / var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity: 1;color:rgb(15 23 42 / var(--tw-text-opacity))}.text-green-500{--tw-text-opacity: 1;color:rgb(34 197 94 / var(--tw-text-opacity))}.text-green-600{--tw-text-opacity: 1;color:rgb(22 163 74 / var(--tw-text-opacity))}.text-indigo-600{--tw-text-opacity: 1;color:rgb(79 70 229 / var(--tw-text-opacity))}.text-primary-500{--tw-text-opacity: 1;color:rgb(99 102 241 / var(--tw-text-opacity))}.text-primary-600{--tw-text-opacity: 1;color:rgb(79 70 229 / var(--tw-text-opacity))}.text-primary-700{--tw-text-opacity: 1;color:rgb(67 56 202 / var(--tw-text-opacity))}.text-red-500{--tw-text-opacity: 1;color:rgb(239 68 68 / var(--tw-text-opacity))}.text-red-600{--tw-text-opacity: 1;color:rgb(220 38 38 / var(--tw-text-opacity))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.text-yellow-500{--tw-text-opacity: 1;color:rgb(234 179 8 / var(--tw-text-opacity))}.text-yellow-700{--tw-text-opacity: 1;color:rgb(161 98 7 / var(--tw-text-opacity))}.underline{text-decoration-line:underline}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.opacity-0{opacity:0}.opacity-100{opacity:1}.opacity-25{opacity:.25}.opacity-75{opacity:.75}.shadow{--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1);--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-md{--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / .1), 0 2px 4px -2px rgb(0 0 0 / .1);--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-none{--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-xl{--tw-shadow: 0 20px 25px -5px rgb(0 0 0 / .1), 0 8px 10px -6px rgb(0 0 0 / .1);--tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.outline-0{outline-width:0px}.ring-1{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-black{--tw-ring-opacity: 1;--tw-ring-color: rgb(0 0 0 / var(--tw-ring-opacity))}.ring-gray-300{--tw-ring-opacity: 1;--tw-ring-color: rgb(203 213 225 / var(--tw-ring-opacity))}.ring-opacity-5{--tw-ring-opacity: .05}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-100{transition-duration:.1s}.duration-150{transition-duration:.15s}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.duration-75{transition-duration:75ms}.ease-in{transition-timing-function:cubic-bezier(.4,0,1,1)}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}[x-cloak]{display:none!important}.hover\:border-gray-300:hover{--tw-border-opacity: 1;border-color:rgb(203 213 225 / var(--tw-border-opacity))}.hover\:bg-gray-100:hover{--tw-bg-opacity: 1;background-color:rgb(241 245 249 / var(--tw-bg-opacity))}.hover\:bg-gray-50:hover{--tw-bg-opacity: 1;background-color:rgb(248 250 252 / var(--tw-bg-opacity))}.hover\:bg-gray-900:hover{--tw-bg-opacity: 1;background-color:rgb(15 23 42 / var(--tw-bg-opacity))}.hover\:bg-primary-600:hover{--tw-bg-opacity: 1;background-color:rgb(79 70 229 / var(--tw-bg-opacity))}.hover\:bg-primary-700:hover{--tw-bg-opacity: 1;background-color:rgb(67 56 202 / var(--tw-bg-opacity))}.hover\:bg-red-500:hover{--tw-bg-opacity: 1;background-color:rgb(239 68 68 / var(--tw-bg-opacity))}.hover\:text-gray-400:hover{--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity))}.hover\:text-gray-500:hover{--tw-text-opacity: 1;color:rgb(100 116 139 / var(--tw-text-opacity))}.hover\:text-gray-700:hover{--tw-text-opacity: 1;color:rgb(51 65 85 / var(--tw-text-opacity))}.hover\:text-gray-800:hover{--tw-text-opacity: 1;color:rgb(30 41 59 / var(--tw-text-opacity))}.hover\:text-gray-900:hover{--tw-text-opacity: 1;color:rgb(15 23 42 / var(--tw-text-opacity))}.hover\:text-white:hover{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.hover\:opacity-50:hover{opacity:.5}.focus\:z-10:focus{z-index:10}.focus\:border-blue-300:focus{--tw-border-opacity: 1;border-color:rgb(147 197 253 / var(--tw-border-opacity))}.focus\:border-gray-300:focus{--tw-border-opacity: 1;border-color:rgb(203 213 225 / var(--tw-border-opacity))}.focus\:border-primary-300:focus{--tw-border-opacity: 1;border-color:rgb(165 180 252 / var(--tw-border-opacity))}.focus\:border-primary-500:focus{--tw-border-opacity: 1;border-color:rgb(99 102 241 / var(--tw-border-opacity))}.focus\:border-primary-700:focus{--tw-border-opacity: 1;border-color:rgb(67 56 202 / var(--tw-border-opacity))}.focus\:border-red-700:focus{--tw-border-opacity: 1;border-color:rgb(185 28 28 / var(--tw-border-opacity))}.focus\:bg-gray-100:focus{--tw-bg-opacity: 1;background-color:rgb(241 245 249 / var(--tw-bg-opacity))}.focus\:bg-gray-50:focus{--tw-bg-opacity: 1;background-color:rgb(248 250 252 / var(--tw-bg-opacity))}.focus\:bg-primary-100:focus{--tw-bg-opacity: 1;background-color:rgb(224 231 255 / var(--tw-bg-opacity))}.focus\:text-gray-700:focus{--tw-text-opacity: 1;color:rgb(51 65 85 / var(--tw-text-opacity))}.focus\:text-gray-800:focus{--tw-text-opacity: 1;color:rgb(30 41 59 / var(--tw-text-opacity))}.focus\:text-primary-800:focus{--tw-text-opacity: 1;color:rgb(55 48 163 / var(--tw-text-opacity))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-1:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-2:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-gray-400:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(148 163 184 / var(--tw-ring-opacity))}.focus\:ring-gray-500:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(100 116 139 / var(--tw-ring-opacity))}.focus\:ring-gray-700:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(51 65 85 / var(--tw-ring-opacity))}.focus\:ring-indigo-500:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(99 102 241 / var(--tw-ring-opacity))}.focus\:ring-primary-200:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(199 210 254 / var(--tw-ring-opacity))}.focus\:ring-primary-500:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(99 102 241 / var(--tw-ring-opacity))}.focus\:ring-red-200:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(254 202 202 / var(--tw-ring-opacity))}.focus\:ring-opacity-50:focus{--tw-ring-opacity: .5}.focus\:ring-offset-2:focus{--tw-ring-offset-width: 2px}.active\:bg-gray-100:active{--tw-bg-opacity: 1;background-color:rgb(241 245 249 / var(--tw-bg-opacity))}.active\:bg-primary-700:active{--tw-bg-opacity: 1;background-color:rgb(67 56 202 / var(--tw-bg-opacity))}.active\:bg-red-600:active{--tw-bg-opacity: 1;background-color:rgb(220 38 38 / var(--tw-bg-opacity))}.active\:text-gray-500:active{--tw-text-opacity: 1;color:rgb(100 116 139 / var(--tw-text-opacity))}.active\:text-gray-700:active{--tw-text-opacity: 1;color:rgb(51 65 85 / var(--tw-text-opacity))}.disabled\:opacity-25:disabled{opacity:.25}:is(.dark .dark\:border-r-2){border-right-width:2px}:is(.dark .dark\:border-gray-500){--tw-border-opacity: 1;border-color:rgb(100 116 139 / var(--tw-border-opacity))}:is(.dark .dark\:border-gray-600){--tw-border-opacity: 1;border-color:rgb(71 85 105 / var(--tw-border-opacity))}:is(.dark .dark\:border-gray-700){--tw-border-opacity: 1;border-color:rgb(51 65 85 / var(--tw-border-opacity))}:is(.dark .dark\:border-gray-800){--tw-border-opacity: 1;border-color:rgb(30 41 59 / var(--tw-border-opacity))}:is(.dark .dark\:border-gray-900){--tw-border-opacity: 1;border-color:rgb(15 23 42 / var(--tw-border-opacity))}:is(.dark .dark\:border-primary-600){--tw-border-opacity: 1;border-color:rgb(79 70 229 / var(--tw-border-opacity))}:is(.dark .dark\:border-t-transparent){border-top-color:transparent}:is(.dark .dark\:border-opacity-20){--tw-border-opacity: .2}:is(.dark .dark\:bg-gray-500){--tw-bg-opacity: 1;background-color:rgb(100 116 139 / var(--tw-bg-opacity))}:is(.dark .dark\:bg-gray-700){--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity))}:is(.dark .dark\:bg-gray-800){--tw-bg-opacity: 1;background-color:rgb(30 41 59 / var(--tw-bg-opacity))}:is(.dark .dark\:bg-gray-800\/50){background-color:#1e293b80}:is(.dark .dark\:bg-gray-900){--tw-bg-opacity: 1;background-color:rgb(15 23 42 / var(--tw-bg-opacity))}:is(.dark .dark\:bg-green-500){--tw-bg-opacity: 1;background-color:rgb(34 197 94 / var(--tw-bg-opacity))}:is(.dark .dark\:bg-primary-500){--tw-bg-opacity: 1;background-color:rgb(99 102 241 / var(--tw-bg-opacity))}:is(.dark .dark\:bg-primary-900\/50){background-color:#312e8180}:is(.dark .dark\:bg-red-500){--tw-bg-opacity: 1;background-color:rgb(239 68 68 / var(--tw-bg-opacity))}:is(.dark .dark\:bg-yellow-500){--tw-bg-opacity: 1;background-color:rgb(234 179 8 / var(--tw-bg-opacity))}:is(.dark .dark\:bg-opacity-10){--tw-bg-opacity: .1}:is(.dark .dark\:bg-opacity-20){--tw-bg-opacity: .2}:is(.dark .dark\:bg-opacity-30){--tw-bg-opacity: .3}:is(.dark .dark\:bg-opacity-70){--tw-bg-opacity: .7}:is(.dark .dark\:text-gray-100){--tw-text-opacity: 1;color:rgb(241 245 249 / var(--tw-text-opacity))}:is(.dark .dark\:text-gray-200){--tw-text-opacity: 1;color:rgb(226 232 240 / var(--tw-text-opacity))}:is(.dark .dark\:text-gray-300){--tw-text-opacity: 1;color:rgb(203 213 225 / var(--tw-text-opacity))}:is(.dark .dark\:text-gray-400){--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity))}:is(.dark .dark\:text-green-400){--tw-text-opacity: 1;color:rgb(74 222 128 / var(--tw-text-opacity))}:is(.dark .dark\:text-primary-300){--tw-text-opacity: 1;color:rgb(165 180 252 / var(--tw-text-opacity))}:is(.dark .dark\:text-red-400){--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity))}:is(.dark .dark\:text-white){--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}:is(.dark .dark\:text-yellow-500){--tw-text-opacity: 1;color:rgb(234 179 8 / var(--tw-text-opacity))}:is(.dark .dark\:hover\:border-gray-600:hover){--tw-border-opacity: 1;border-color:rgb(71 85 105 / var(--tw-border-opacity))}:is(.dark .dark\:hover\:border-gray-700:hover){--tw-border-opacity: 1;border-color:rgb(51 65 85 / var(--tw-border-opacity))}:is(.dark .dark\:hover\:bg-gray-700:hover){--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity))}:is(.dark .dark\:hover\:bg-gray-800:hover){--tw-bg-opacity: 1;background-color:rgb(30 41 59 / var(--tw-bg-opacity))}:is(.dark .dark\:hover\:text-gray-100:hover){--tw-text-opacity: 1;color:rgb(241 245 249 / var(--tw-text-opacity))}:is(.dark .dark\:hover\:text-gray-200:hover){--tw-text-opacity: 1;color:rgb(226 232 240 / var(--tw-text-opacity))}:is(.dark .dark\:hover\:text-gray-300:hover){--tw-text-opacity: 1;color:rgb(203 213 225 / var(--tw-text-opacity))}:is(.dark .dark\:focus\:border-gray-600:focus){--tw-border-opacity: 1;border-color:rgb(71 85 105 / var(--tw-border-opacity))}:is(.dark .dark\:focus\:border-gray-700:focus){--tw-border-opacity: 1;border-color:rgb(51 65 85 / var(--tw-border-opacity))}:is(.dark .dark\:focus\:border-primary-300:focus){--tw-border-opacity: 1;border-color:rgb(165 180 252 / var(--tw-border-opacity))}:is(.dark .dark\:focus\:border-primary-600:focus){--tw-border-opacity: 1;border-color:rgb(79 70 229 / var(--tw-border-opacity))}:is(.dark .dark\:focus\:border-primary-700:focus){--tw-border-opacity: 1;border-color:rgb(67 56 202 / var(--tw-border-opacity))}:is(.dark .dark\:focus\:bg-gray-700:focus){--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity))}:is(.dark .dark\:focus\:bg-primary-900:focus){--tw-bg-opacity: 1;background-color:rgb(49 46 129 / var(--tw-bg-opacity))}:is(.dark .dark\:focus\:text-gray-200:focus){--tw-text-opacity: 1;color:rgb(226 232 240 / var(--tw-text-opacity))}:is(.dark .dark\:focus\:text-gray-300:focus){--tw-text-opacity: 1;color:rgb(203 213 225 / var(--tw-text-opacity))}:is(.dark .dark\:focus\:text-primary-200:focus){--tw-text-opacity: 1;color:rgb(199 210 254 / var(--tw-text-opacity))}:is(.dark .dark\:focus\:ring-indigo-600:focus){--tw-ring-opacity: 1;--tw-ring-color: rgb(79 70 229 / var(--tw-ring-opacity))}:is(.dark .dark\:focus\:ring-primary-600:focus){--tw-ring-opacity: 1;--tw-ring-color: rgb(79 70 229 / var(--tw-ring-opacity))}:is(.dark .dark\:focus\:ring-primary-700:focus){--tw-ring-opacity: 1;--tw-ring-color: rgb(67 56 202 / var(--tw-ring-opacity))}:is(.dark .dark\:focus\:ring-opacity-40:focus){--tw-ring-opacity: .4}:is(.dark .dark\:focus\:ring-offset-gray-800:focus){--tw-ring-offset-color: #1e293b}@media (min-width: 640px){.sm\:mx-auto{margin-left:auto;margin-right:auto}.sm\:flex{display:flex}.sm\:hidden{display:none}.sm\:w-full{width:100%}.sm\:max-w-2xl{max-width:42rem}.sm\:max-w-3xl{max-width:48rem}.sm\:max-w-4xl{max-width:56rem}.sm\:max-w-lg{max-width:32rem}.sm\:max-w-md{max-width:28rem}.sm\:max-w-sm{max-width:24rem}.sm\:max-w-xl{max-width:36rem}.sm\:flex-1{flex:1 1 0%}.sm\:translate-y-0{--tw-translate-y: 0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.sm\:scale-100{--tw-scale-x: 1;--tw-scale-y: 1;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.sm\:scale-95{--tw-scale-x: .95;--tw-scale-y: .95;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.sm\:items-center{align-items:center}.sm\:justify-start{justify-content:flex-start}.sm\:justify-center{justify-content:center}.sm\:justify-between{justify-content:space-between}.sm\:rounded-md{border-radius:.375rem}.sm\:rounded-bl-md{border-bottom-left-radius:.375rem}.sm\:rounded-br-md{border-bottom-right-radius:.375rem}.sm\:rounded-tl-md{border-top-left-radius:.375rem}.sm\:rounded-tr-md{border-top-right-radius:.375rem}.sm\:p-6{padding:1.5rem}.sm\:px-0{padding-left:0;padding-right:0}.sm\:px-6{padding-left:1.5rem;padding-right:1.5rem}.sm\:pt-0{padding-top:0}.sm\:text-sm{font-size:.875rem;line-height:1.25rem}}@media (min-width: 768px){.md\:col-span-1{grid-column:span 1 / span 1}.md\:block{display:block}.md\:justify-start{justify-content:flex-start}.md\:text-left{text-align:left}}@media (min-width: 1024px){.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:px-8{padding-left:2rem;padding-right:2rem}} diff --git a/public/build/assets/app-887de6f7.css b/public/build/assets/app-887de6f7.css deleted file mode 100644 index b878ca9..0000000 --- a/public/build/assets/app-887de6f7.css +++ /dev/null @@ -1 +0,0 @@ -.toast-title{font-weight:700}.toast-message{-ms-word-wrap:break-word;word-wrap:break-word}.toast-message a,.toast-message label{color:#fff}.toast-message a:hover{color:#ccc;text-decoration:none}.toast-close-button{position:relative;right:-.3em;top:-.3em;float:right;font-size:20px;font-weight:700;color:#fff;-webkit-text-shadow:0 1px 0 #ffffff;text-shadow:0 1px 0 #ffffff;opacity:.8;-ms-filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=80);filter:alpha(opacity=80);line-height:1}.toast-close-button:hover,.toast-close-button:focus{color:#000;text-decoration:none;cursor:pointer;opacity:.4;-ms-filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=40);filter:alpha(opacity=40)}.rtl .toast-close-button{left:-.3em;float:left;right:.3em}button.toast-close-button{padding:0;cursor:pointer;background:transparent;border:0;-webkit-appearance:none}.toast-top-center{top:0;right:0;width:100%}.toast-bottom-center{bottom:0;right:0;width:100%}.toast-top-full-width{top:0;right:0;width:100%}.toast-bottom-full-width{bottom:0;right:0;width:100%}.toast-top-left{top:12px;left:12px}.toast-top-right{top:12px;right:12px}.toast-bottom-right{right:12px;bottom:12px}.toast-bottom-left{bottom:12px;left:12px}#toast-container{position:fixed;z-index:999999;pointer-events:none}#toast-container *{box-sizing:border-box}#toast-container>div{border-radius:.5rem;position:relative;pointer-events:auto;overflow:hidden;margin:0 0 6px;padding:15px 15px 15px 50px;width:300px;background-position:15px center;background-repeat:no-repeat;color:#fff}#toast-container>div.rtl{direction:rtl;padding:15px 50px 15px 15px;background-position:right 15px center}#toast-container>div:hover{cursor:pointer;--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / .1), 0 2px 4px -2px rgb(0 0 0 / .1);--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}#toast-container>.toast-info{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGwSURBVEhLtZa9SgNBEMc9sUxxRcoUKSzSWIhXpFMhhYWFhaBg4yPYiWCXZxBLERsLRS3EQkEfwCKdjWJAwSKCgoKCcudv4O5YLrt7EzgXhiU3/4+b2ckmwVjJSpKkQ6wAi4gwhT+z3wRBcEz0yjSseUTrcRyfsHsXmD0AmbHOC9Ii8VImnuXBPglHpQ5wwSVM7sNnTG7Za4JwDdCjxyAiH3nyA2mtaTJufiDZ5dCaqlItILh1NHatfN5skvjx9Z38m69CgzuXmZgVrPIGE763Jx9qKsRozWYw6xOHdER+nn2KkO+Bb+UV5CBN6WC6QtBgbRVozrahAbmm6HtUsgtPC19tFdxXZYBOfkbmFJ1VaHA1VAHjd0pp70oTZzvR+EVrx2Ygfdsq6eu55BHYR8hlcki+n+kERUFG8BrA0BwjeAv2M8WLQBtcy+SD6fNsmnB3AlBLrgTtVW1c2QN4bVWLATaIS60J2Du5y1TiJgjSBvFVZgTmwCU+dAZFoPxGEEs8nyHC9Bwe2GvEJv2WXZb0vjdyFT4Cxk3e/kIqlOGoVLwwPevpYHT+00T+hWwXDf4AJAOUqWcDhbwAAAAASUVORK5CYII=)!important}#toast-container>.toast-error{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAHOSURBVEhLrZa/SgNBEMZzh0WKCClSCKaIYOED+AAKeQQLG8HWztLCImBrYadgIdY+gIKNYkBFSwu7CAoqCgkkoGBI/E28PdbLZmeDLgzZzcx83/zZ2SSXC1j9fr+I1Hq93g2yxH4iwM1vkoBWAdxCmpzTxfkN2RcyZNaHFIkSo10+8kgxkXIURV5HGxTmFuc75B2RfQkpxHG8aAgaAFa0tAHqYFfQ7Iwe2yhODk8+J4C7yAoRTWI3w/4klGRgR4lO7Rpn9+gvMyWp+uxFh8+H+ARlgN1nJuJuQAYvNkEnwGFck18Er4q3egEc/oO+mhLdKgRyhdNFiacC0rlOCbhNVz4H9FnAYgDBvU3QIioZlJFLJtsoHYRDfiZoUyIxqCtRpVlANq0EU4dApjrtgezPFad5S19Wgjkc0hNVnuF4HjVA6C7QrSIbylB+oZe3aHgBsqlNqKYH48jXyJKMuAbiyVJ8KzaB3eRc0pg9VwQ4niFryI68qiOi3AbjwdsfnAtk0bCjTLJKr6mrD9g8iq/S/B81hguOMlQTnVyG40wAcjnmgsCNESDrjme7wfftP4P7SP4N3CJZdvzoNyGq2c/HWOXJGsvVg+RA/k2MC/wN6I2YA2Pt8GkAAAAASUVORK5CYII=)!important}#toast-container>.toast-success{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADsSURBVEhLY2AYBfQMgf///3P8+/evAIgvA/FsIF+BavYDDWMBGroaSMMBiE8VC7AZDrIFaMFnii3AZTjUgsUUWUDA8OdAH6iQbQEhw4HyGsPEcKBXBIC4ARhex4G4BsjmweU1soIFaGg/WtoFZRIZdEvIMhxkCCjXIVsATV6gFGACs4Rsw0EGgIIH3QJYJgHSARQZDrWAB+jawzgs+Q2UO49D7jnRSRGoEFRILcdmEMWGI0cm0JJ2QpYA1RDvcmzJEWhABhD/pqrL0S0CWuABKgnRki9lLseS7g2AlqwHWQSKH4oKLrILpRGhEQCw2LiRUIa4lwAAAABJRU5ErkJggg==)!important}#toast-container>.toast-warning{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGYSURBVEhL5ZSvTsNQFMbXZGICMYGYmJhAQIJAICYQPAACiSDB8AiICQQJT4CqQEwgJvYASAQCiZiYmJhAIBATCARJy+9rTsldd8sKu1M0+dLb057v6/lbq/2rK0mS/TRNj9cWNAKPYIJII7gIxCcQ51cvqID+GIEX8ASG4B1bK5gIZFeQfoJdEXOfgX4QAQg7kH2A65yQ87lyxb27sggkAzAuFhbbg1K2kgCkB1bVwyIR9m2L7PRPIhDUIXgGtyKw575yz3lTNs6X4JXnjV+LKM/m3MydnTbtOKIjtz6VhCBq4vSm3ncdrD2lk0VgUXSVKjVDJXJzijW1RQdsU7F77He8u68koNZTz8Oz5yGa6J3H3lZ0xYgXBK2QymlWWA+RWnYhskLBv2vmE+hBMCtbA7KX5drWyRT/2JsqZ2IvfB9Y4bWDNMFbJRFmC9E74SoS0CqulwjkC0+5bpcV1CZ8NMej4pjy0U+doDQsGyo1hzVJttIjhQ7GnBtRFN1UarUlH8F3xict+HY07rEzoUGPlWcjRFRr4/gChZgc3ZL2d8oAAAAASUVORK5CYII=)!important}#toast-container.toast-top-center>div,#toast-container.toast-bottom-center>div{width:300px;margin-left:auto;margin-right:auto}#toast-container.toast-top-full-width>div,#toast-container.toast-bottom-full-width>div{width:96%;margin-left:auto;margin-right:auto}.toast{background-color:#030303}.toast-success{--tw-bg-opacity: 1;background-color:rgb(34 197 94 / var(--tw-bg-opacity))}.toast-error{--tw-bg-opacity: 1;background-color:rgb(239 68 68 / var(--tw-bg-opacity))}.toast-info{--tw-bg-opacity: 1;background-color:rgb(99 102 241 / var(--tw-bg-opacity))}.toast-warning{--tw-bg-opacity: 1;background-color:rgb(234 179 8 / var(--tw-bg-opacity))}.toast-progress{position:absolute;left:0;bottom:0;height:4px;background-color:#000;opacity:.4;-ms-filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=40);filter:alpha(opacity=40)}@media all and (max-width: 240px){#toast-container>div{padding:8px 8px 8px 50px;width:11em}#toast-container>div.rtl{padding:8px 50px 8px 8px}#toast-container .toast-close-button{right:-.2em;top:-.2em}#toast-container .rtl .toast-close-button{left:-.2em;right:.2em}}@media all and (min-width: 241px) and (max-width: 480px){#toast-container>div{padding:8px 8px 8px 50px;width:18em}#toast-container>div.rtl{padding:8px 50px 8px 8px}#toast-container .toast-close-button{right:-.2em;top:-.2em}#toast-container .rtl .toast-close-button{left:-.2em;right:.2em}}@media all and (min-width: 481px) and (max-width: 768px){#toast-container>div{padding:15px 15px 15px 50px;width:25em}#toast-container>div.rtl{padding:15px 50px 15px 15px}}/*! tailwindcss v3.3.1 | MIT License | https://tailwindcss.com*/*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e2e8f0}:before,:after{--tw-content: ""}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:Figtree,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#94a3b8}input::placeholder,textarea::placeholder{opacity:1;color:#94a3b8}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}[type=text],[type=email],[type=url],[type=password],[type=number],[type=date],[type=datetime-local],[type=month],[type=search],[type=tel],[type=time],[type=week],[multiple],textarea,select{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;border-color:#64748b;border-width:1px;border-radius:0;padding:.5rem .75rem;font-size:1rem;line-height:1.5rem;--tw-shadow: 0 0 #0000}[type=text]:focus,[type=email]:focus,[type=url]:focus,[type=password]:focus,[type=number]:focus,[type=date]:focus,[type=datetime-local]:focus,[type=month]:focus,[type=search]:focus,[type=tel]:focus,[type=time]:focus,[type=week]:focus,[multiple]:focus,textarea:focus,select:focus{outline:2px solid transparent;outline-offset:2px;--tw-ring-inset: var(--tw-empty, );--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: #2563eb;--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);border-color:#2563eb}input::-moz-placeholder,textarea::-moz-placeholder{color:#64748b;opacity:1}input::placeholder,textarea::placeholder{color:#64748b;opacity:1}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-date-and-time-value{min-height:1.5em}::-webkit-datetime-edit,::-webkit-datetime-edit-year-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-meridiem-field{padding-top:0;padding-bottom:0}select{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%2364748b' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");background-position:right .5rem center;background-repeat:no-repeat;background-size:1.5em 1.5em;padding-right:2.5rem;-webkit-print-color-adjust:exact;print-color-adjust:exact}[multiple]{background-image:initial;background-position:initial;background-repeat:unset;background-size:initial;padding-right:.75rem;-webkit-print-color-adjust:unset;print-color-adjust:unset}[type=checkbox],[type=radio]{-webkit-appearance:none;-moz-appearance:none;appearance:none;padding:0;-webkit-print-color-adjust:exact;print-color-adjust:exact;display:inline-block;vertical-align:middle;background-origin:border-box;-webkit-user-select:none;-moz-user-select:none;user-select:none;flex-shrink:0;height:1rem;width:1rem;color:#2563eb;background-color:#fff;border-color:#64748b;border-width:1px;--tw-shadow: 0 0 #0000}[type=checkbox]{border-radius:0}[type=radio]{border-radius:100%}[type=checkbox]:focus,[type=radio]:focus{outline:2px solid transparent;outline-offset:2px;--tw-ring-inset: var(--tw-empty, );--tw-ring-offset-width: 2px;--tw-ring-offset-color: #fff;--tw-ring-color: #2563eb;--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}[type=checkbox]:checked,[type=radio]:checked{border-color:transparent;background-color:currentColor;background-size:100% 100%;background-position:center;background-repeat:no-repeat}[type=checkbox]:checked{background-image:url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e")}[type=radio]:checked{background-image:url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='8' cy='8' r='3'/%3e%3c/svg%3e")}[type=checkbox]:checked:hover,[type=checkbox]:checked:focus,[type=radio]:checked:hover,[type=radio]:checked:focus{border-color:transparent;background-color:currentColor}[type=checkbox]:indeterminate{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3e%3cpath stroke='white' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3e%3c/svg%3e");border-color:transparent;background-color:currentColor;background-size:100% 100%;background-position:center;background-repeat:no-repeat}[type=checkbox]:indeterminate:hover,[type=checkbox]:indeterminate:focus{border-color:transparent;background-color:currentColor}[type=file]{background:unset;border-color:inherit;border-width:0;border-radius:0;padding:0;font-size:unset;line-height:inherit}[type=file]:focus{outline:1px solid ButtonText;outline:1px auto -webkit-focus-ring-color}*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.container{width:100%}@media (min-width: 640px){.container{max-width:640px}}@media (min-width: 768px){.container{max-width:768px}}@media (min-width: 1024px){.container{max-width:1024px}}@media (min-width: 1280px){.container{max-width:1280px}}@media (min-width: 1536px){.container{max-width:1536px}}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{top:0;right:0;bottom:0;left:0}.inset-y-0{top:0;bottom:0}.left-0{left:0}.right-0{right:0}.top-0{top:0}.top-1{top:.25rem}.z-0{z-index:0}.z-10{z-index:10}.z-50{z-index:50}.float-right{float:right}.mx-5{margin-left:1.25rem;margin-right:1.25rem}.mx-auto{margin-left:auto;margin-right:auto}.-ml-px{margin-left:-1px}.mb-1{margin-bottom:.25rem}.mb-10{margin-bottom:2.5rem}.mb-2{margin-bottom:.5rem}.mb-4{margin-bottom:1rem}.mb-5{margin-bottom:1.25rem}.mb-6{margin-bottom:1.5rem}.ml-1{margin-left:.25rem}.ml-2{margin-left:.5rem}.ml-3{margin-left:.75rem}.mr-1{margin-right:.25rem}.mr-2{margin-right:.5rem}.mr-3{margin-right:.75rem}.mt-1{margin-top:.25rem}.mt-10{margin-top:2.5rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-5{margin-top:1.25rem}.mt-6{margin-top:1.5rem}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-10{height:2.5rem}.h-16{height:4rem}.h-20{height:5rem}.h-4{height:1rem}.h-48{height:12rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-7{height:1.75rem}.h-8{height:2rem}.h-9{height:2.25rem}.h-96{height:24rem}.max-h-\[350px\]{max-height:350px}.min-h-screen{min-height:100vh}.w-1{width:.25rem}.w-10{width:2.5rem}.w-4{width:1rem}.w-48{width:12rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-64{width:16rem}.w-7{width:1.75rem}.w-8{width:2rem}.w-9{width:2.25rem}.w-full{width:100%}.w-max{width:-moz-max-content;width:max-content}.min-w-full{min-width:100%}.min-w-max{min-width:-moz-max-content;min-width:max-content}.max-w-7xl{max-width:80rem}.max-w-full{max-width:100%}.flex-1{flex:1 1 0%}.flex-none{flex:none}.flex-grow{flex-grow:1}.origin-top{transform-origin:top}.origin-top-left{transform-origin:top left}.origin-top-right{transform-origin:top right}.translate-y-0{--tw-translate-y: 0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-y-4{--tw-translate-y: 1rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.scale-100{--tw-scale-x: 1;--tw-scale-y: 1;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.scale-95{--tw-scale-x: .95;--tw-scale-y: .95;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.cursor-default{cursor:default}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.flex-col{flex-direction:column}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.justify-start{justify-content:flex-start}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.justify-items-center{justify-items:center}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-5{gap:1.25rem}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.space-y-10>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(2.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(2.5rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.overflow-y-hidden{overflow-y:hidden}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-line{white-space:pre-line}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-sm{border-radius:.125rem}.rounded-b-md{border-bottom-right-radius:.375rem;border-bottom-left-radius:.375rem}.rounded-l-md{border-top-left-radius:.375rem;border-bottom-left-radius:.375rem}.rounded-r-md{border-top-right-radius:.375rem;border-bottom-right-radius:.375rem}.rounded-t-md{border-top-left-radius:.375rem;border-top-right-radius:.375rem}.rounded-tr-md{border-top-right-radius:.375rem}.border{border-width:1px}.border-2{border-width:2px}.border-b{border-bottom-width:1px}.border-b-2{border-bottom-width:2px}.border-l{border-left-width:1px}.border-l-4{border-left-width:4px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-gray-100{--tw-border-opacity: 1;border-color:rgb(241 245 249 / var(--tw-border-opacity))}.border-gray-200{--tw-border-opacity: 1;border-color:rgb(226 232 240 / var(--tw-border-opacity))}.border-gray-300{--tw-border-opacity: 1;border-color:rgb(203 213 225 / var(--tw-border-opacity))}.border-primary-200{--tw-border-opacity: 1;border-color:rgb(199 210 254 / var(--tw-border-opacity))}.border-primary-400{--tw-border-opacity: 1;border-color:rgb(129 140 248 / var(--tw-border-opacity))}.border-primary-600{--tw-border-opacity: 1;border-color:rgb(79 70 229 / var(--tw-border-opacity))}.border-transparent{border-color:transparent}.border-yellow-500{--tw-border-opacity: 1;border-color:rgb(234 179 8 / var(--tw-border-opacity))}.border-t-transparent{border-top-color:transparent}.bg-gray-100{--tw-bg-opacity: 1;background-color:rgb(241 245 249 / var(--tw-bg-opacity))}.bg-gray-200{--tw-bg-opacity: 1;background-color:rgb(226 232 240 / var(--tw-bg-opacity))}.bg-gray-50{--tw-bg-opacity: 1;background-color:rgb(248 250 252 / var(--tw-bg-opacity))}.bg-gray-500{--tw-bg-opacity: 1;background-color:rgb(100 116 139 / var(--tw-bg-opacity))}.bg-gray-800{--tw-bg-opacity: 1;background-color:rgb(30 41 59 / var(--tw-bg-opacity))}.bg-gray-900{--tw-bg-opacity: 1;background-color:rgb(15 23 42 / var(--tw-bg-opacity))}.bg-green-50{--tw-bg-opacity: 1;background-color:rgb(240 253 244 / var(--tw-bg-opacity))}.bg-primary-200{--tw-bg-opacity: 1;background-color:rgb(199 210 254 / var(--tw-bg-opacity))}.bg-primary-50{--tw-bg-opacity: 1;background-color:rgb(238 242 255 / var(--tw-bg-opacity))}.bg-primary-500{--tw-bg-opacity: 1;background-color:rgb(99 102 241 / var(--tw-bg-opacity))}.bg-primary-600{--tw-bg-opacity: 1;background-color:rgb(79 70 229 / var(--tw-bg-opacity))}.bg-red-50{--tw-bg-opacity: 1;background-color:rgb(254 242 242 / var(--tw-bg-opacity))}.bg-red-600{--tw-bg-opacity: 1;background-color:rgb(220 38 38 / var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity))}.bg-yellow-100{--tw-bg-opacity: 1;background-color:rgb(254 249 195 / var(--tw-bg-opacity))}.bg-yellow-50{--tw-bg-opacity: 1;background-color:rgb(254 252 232 / var(--tw-bg-opacity))}.p-1{padding:.25rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.p-7{padding:1.75rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.px-8{padding-left:2rem;padding-right:2rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-12{padding-top:3rem;padding-bottom:3rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-5{padding-top:1.25rem;padding-bottom:1.25rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.pb-1{padding-bottom:.25rem}.pb-2{padding-bottom:.5rem}.pl-3{padding-left:.75rem}.pr-10{padding-right:2.5rem}.pr-2{padding-right:.5rem}.pr-3{padding-right:.75rem}.pr-4{padding-right:1rem}.pt-1{padding-top:.25rem}.pt-3{padding-top:.75rem}.pt-6{padding-top:1.5rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.align-middle{vertical-align:middle}.font-sans{font-family:Figtree,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji"}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-extrabold{font-weight:800}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.lowercase{text-transform:lowercase}.capitalize{text-transform:capitalize}.leading-4{line-height:1rem}.leading-5{line-height:1.25rem}.tracking-wider{letter-spacing:.05em}.text-black{--tw-text-opacity: 1;color:rgb(0 0 0 / var(--tw-text-opacity))}.text-gray-100{--tw-text-opacity: 1;color:rgb(241 245 249 / var(--tw-text-opacity))}.text-gray-300{--tw-text-opacity: 1;color:rgb(203 213 225 / var(--tw-text-opacity))}.text-gray-400{--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity))}.text-gray-50{--tw-text-opacity: 1;color:rgb(248 250 252 / var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity: 1;color:rgb(100 116 139 / var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity: 1;color:rgb(71 85 105 / var(--tw-text-opacity))}.text-gray-700{--tw-text-opacity: 1;color:rgb(51 65 85 / var(--tw-text-opacity))}.text-gray-800{--tw-text-opacity: 1;color:rgb(30 41 59 / var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity: 1;color:rgb(15 23 42 / var(--tw-text-opacity))}.text-green-500{--tw-text-opacity: 1;color:rgb(34 197 94 / var(--tw-text-opacity))}.text-green-600{--tw-text-opacity: 1;color:rgb(22 163 74 / var(--tw-text-opacity))}.text-indigo-600{--tw-text-opacity: 1;color:rgb(79 70 229 / var(--tw-text-opacity))}.text-primary-500{--tw-text-opacity: 1;color:rgb(99 102 241 / var(--tw-text-opacity))}.text-primary-600{--tw-text-opacity: 1;color:rgb(79 70 229 / var(--tw-text-opacity))}.text-primary-700{--tw-text-opacity: 1;color:rgb(67 56 202 / var(--tw-text-opacity))}.text-red-500{--tw-text-opacity: 1;color:rgb(239 68 68 / var(--tw-text-opacity))}.text-red-600{--tw-text-opacity: 1;color:rgb(220 38 38 / var(--tw-text-opacity))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.text-yellow-500{--tw-text-opacity: 1;color:rgb(234 179 8 / var(--tw-text-opacity))}.text-yellow-700{--tw-text-opacity: 1;color:rgb(161 98 7 / var(--tw-text-opacity))}.underline{text-decoration-line:underline}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.opacity-0{opacity:0}.opacity-100{opacity:1}.opacity-25{opacity:.25}.opacity-75{opacity:.75}.shadow-lg{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-md{--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / .1), 0 2px 4px -2px rgb(0 0 0 / .1);--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-none{--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-xl{--tw-shadow: 0 20px 25px -5px rgb(0 0 0 / .1), 0 8px 10px -6px rgb(0 0 0 / .1);--tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.outline-0{outline-width:0px}.ring-1{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-black{--tw-ring-opacity: 1;--tw-ring-color: rgb(0 0 0 / var(--tw-ring-opacity))}.ring-gray-300{--tw-ring-opacity: 1;--tw-ring-color: rgb(203 213 225 / var(--tw-ring-opacity))}.ring-opacity-5{--tw-ring-opacity: .05}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-100{transition-duration:.1s}.duration-150{transition-duration:.15s}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.duration-75{transition-duration:75ms}.ease-in{transition-timing-function:cubic-bezier(.4,0,1,1)}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}[x-cloak]{display:none!important}.hover\:border-gray-300:hover{--tw-border-opacity: 1;border-color:rgb(203 213 225 / var(--tw-border-opacity))}.hover\:bg-gray-100:hover{--tw-bg-opacity: 1;background-color:rgb(241 245 249 / var(--tw-bg-opacity))}.hover\:bg-gray-50:hover{--tw-bg-opacity: 1;background-color:rgb(248 250 252 / var(--tw-bg-opacity))}.hover\:bg-gray-900:hover{--tw-bg-opacity: 1;background-color:rgb(15 23 42 / var(--tw-bg-opacity))}.hover\:bg-primary-600:hover{--tw-bg-opacity: 1;background-color:rgb(79 70 229 / var(--tw-bg-opacity))}.hover\:bg-primary-700:hover{--tw-bg-opacity: 1;background-color:rgb(67 56 202 / var(--tw-bg-opacity))}.hover\:bg-red-500:hover{--tw-bg-opacity: 1;background-color:rgb(239 68 68 / var(--tw-bg-opacity))}.hover\:text-gray-400:hover{--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity))}.hover\:text-gray-500:hover{--tw-text-opacity: 1;color:rgb(100 116 139 / var(--tw-text-opacity))}.hover\:text-gray-700:hover{--tw-text-opacity: 1;color:rgb(51 65 85 / var(--tw-text-opacity))}.hover\:text-gray-800:hover{--tw-text-opacity: 1;color:rgb(30 41 59 / var(--tw-text-opacity))}.hover\:text-gray-900:hover{--tw-text-opacity: 1;color:rgb(15 23 42 / var(--tw-text-opacity))}.hover\:text-white:hover{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.hover\:opacity-50:hover{opacity:.5}.focus\:z-10:focus{z-index:10}.focus\:border-blue-300:focus{--tw-border-opacity: 1;border-color:rgb(147 197 253 / var(--tw-border-opacity))}.focus\:border-gray-300:focus{--tw-border-opacity: 1;border-color:rgb(203 213 225 / var(--tw-border-opacity))}.focus\:border-primary-300:focus{--tw-border-opacity: 1;border-color:rgb(165 180 252 / var(--tw-border-opacity))}.focus\:border-primary-500:focus{--tw-border-opacity: 1;border-color:rgb(99 102 241 / var(--tw-border-opacity))}.focus\:border-primary-700:focus{--tw-border-opacity: 1;border-color:rgb(67 56 202 / var(--tw-border-opacity))}.focus\:border-red-700:focus{--tw-border-opacity: 1;border-color:rgb(185 28 28 / var(--tw-border-opacity))}.focus\:bg-gray-100:focus{--tw-bg-opacity: 1;background-color:rgb(241 245 249 / var(--tw-bg-opacity))}.focus\:bg-gray-50:focus{--tw-bg-opacity: 1;background-color:rgb(248 250 252 / var(--tw-bg-opacity))}.focus\:bg-primary-100:focus{--tw-bg-opacity: 1;background-color:rgb(224 231 255 / var(--tw-bg-opacity))}.focus\:text-gray-700:focus{--tw-text-opacity: 1;color:rgb(51 65 85 / var(--tw-text-opacity))}.focus\:text-gray-800:focus{--tw-text-opacity: 1;color:rgb(30 41 59 / var(--tw-text-opacity))}.focus\:text-primary-800:focus{--tw-text-opacity: 1;color:rgb(55 48 163 / var(--tw-text-opacity))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-1:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-2:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-gray-400:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(148 163 184 / var(--tw-ring-opacity))}.focus\:ring-gray-500:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(100 116 139 / var(--tw-ring-opacity))}.focus\:ring-gray-700:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(51 65 85 / var(--tw-ring-opacity))}.focus\:ring-indigo-500:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(99 102 241 / var(--tw-ring-opacity))}.focus\:ring-primary-200:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(199 210 254 / var(--tw-ring-opacity))}.focus\:ring-primary-500:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(99 102 241 / var(--tw-ring-opacity))}.focus\:ring-red-200:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(254 202 202 / var(--tw-ring-opacity))}.focus\:ring-opacity-50:focus{--tw-ring-opacity: .5}.focus\:ring-offset-2:focus{--tw-ring-offset-width: 2px}.active\:bg-gray-100:active{--tw-bg-opacity: 1;background-color:rgb(241 245 249 / var(--tw-bg-opacity))}.active\:bg-primary-700:active{--tw-bg-opacity: 1;background-color:rgb(67 56 202 / var(--tw-bg-opacity))}.active\:bg-red-600:active{--tw-bg-opacity: 1;background-color:rgb(220 38 38 / var(--tw-bg-opacity))}.active\:text-gray-500:active{--tw-text-opacity: 1;color:rgb(100 116 139 / var(--tw-text-opacity))}.active\:text-gray-700:active{--tw-text-opacity: 1;color:rgb(51 65 85 / var(--tw-text-opacity))}.disabled\:opacity-25:disabled{opacity:.25}:is(.dark .dark\:border-r-2){border-right-width:2px}:is(.dark .dark\:border-gray-500){--tw-border-opacity: 1;border-color:rgb(100 116 139 / var(--tw-border-opacity))}:is(.dark .dark\:border-gray-600){--tw-border-opacity: 1;border-color:rgb(71 85 105 / var(--tw-border-opacity))}:is(.dark .dark\:border-gray-700){--tw-border-opacity: 1;border-color:rgb(51 65 85 / var(--tw-border-opacity))}:is(.dark .dark\:border-gray-800){--tw-border-opacity: 1;border-color:rgb(30 41 59 / var(--tw-border-opacity))}:is(.dark .dark\:border-gray-900){--tw-border-opacity: 1;border-color:rgb(15 23 42 / var(--tw-border-opacity))}:is(.dark .dark\:border-primary-600){--tw-border-opacity: 1;border-color:rgb(79 70 229 / var(--tw-border-opacity))}:is(.dark .dark\:border-t-transparent){border-top-color:transparent}:is(.dark .dark\:border-opacity-20){--tw-border-opacity: .2}:is(.dark .dark\:bg-gray-500){--tw-bg-opacity: 1;background-color:rgb(100 116 139 / var(--tw-bg-opacity))}:is(.dark .dark\:bg-gray-700){--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity))}:is(.dark .dark\:bg-gray-800){--tw-bg-opacity: 1;background-color:rgb(30 41 59 / var(--tw-bg-opacity))}:is(.dark .dark\:bg-gray-800\/50){background-color:#1e293b80}:is(.dark .dark\:bg-gray-900){--tw-bg-opacity: 1;background-color:rgb(15 23 42 / var(--tw-bg-opacity))}:is(.dark .dark\:bg-green-500){--tw-bg-opacity: 1;background-color:rgb(34 197 94 / var(--tw-bg-opacity))}:is(.dark .dark\:bg-primary-500){--tw-bg-opacity: 1;background-color:rgb(99 102 241 / var(--tw-bg-opacity))}:is(.dark .dark\:bg-primary-900\/50){background-color:#312e8180}:is(.dark .dark\:bg-red-500){--tw-bg-opacity: 1;background-color:rgb(239 68 68 / var(--tw-bg-opacity))}:is(.dark .dark\:bg-yellow-500){--tw-bg-opacity: 1;background-color:rgb(234 179 8 / var(--tw-bg-opacity))}:is(.dark .dark\:bg-opacity-10){--tw-bg-opacity: .1}:is(.dark .dark\:bg-opacity-20){--tw-bg-opacity: .2}:is(.dark .dark\:bg-opacity-30){--tw-bg-opacity: .3}:is(.dark .dark\:bg-opacity-70){--tw-bg-opacity: .7}:is(.dark .dark\:text-gray-100){--tw-text-opacity: 1;color:rgb(241 245 249 / var(--tw-text-opacity))}:is(.dark .dark\:text-gray-200){--tw-text-opacity: 1;color:rgb(226 232 240 / var(--tw-text-opacity))}:is(.dark .dark\:text-gray-300){--tw-text-opacity: 1;color:rgb(203 213 225 / var(--tw-text-opacity))}:is(.dark .dark\:text-gray-400){--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity))}:is(.dark .dark\:text-green-400){--tw-text-opacity: 1;color:rgb(74 222 128 / var(--tw-text-opacity))}:is(.dark .dark\:text-primary-300){--tw-text-opacity: 1;color:rgb(165 180 252 / var(--tw-text-opacity))}:is(.dark .dark\:text-red-400){--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity))}:is(.dark .dark\:text-white){--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}:is(.dark .dark\:text-yellow-500){--tw-text-opacity: 1;color:rgb(234 179 8 / var(--tw-text-opacity))}:is(.dark .dark\:hover\:border-gray-600:hover){--tw-border-opacity: 1;border-color:rgb(71 85 105 / var(--tw-border-opacity))}:is(.dark .dark\:hover\:border-gray-700:hover){--tw-border-opacity: 1;border-color:rgb(51 65 85 / var(--tw-border-opacity))}:is(.dark .dark\:hover\:bg-gray-700:hover){--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity))}:is(.dark .dark\:hover\:bg-gray-800:hover){--tw-bg-opacity: 1;background-color:rgb(30 41 59 / var(--tw-bg-opacity))}:is(.dark .dark\:hover\:text-gray-100:hover){--tw-text-opacity: 1;color:rgb(241 245 249 / var(--tw-text-opacity))}:is(.dark .dark\:hover\:text-gray-200:hover){--tw-text-opacity: 1;color:rgb(226 232 240 / var(--tw-text-opacity))}:is(.dark .dark\:hover\:text-gray-300:hover){--tw-text-opacity: 1;color:rgb(203 213 225 / var(--tw-text-opacity))}:is(.dark .dark\:focus\:border-gray-600:focus){--tw-border-opacity: 1;border-color:rgb(71 85 105 / var(--tw-border-opacity))}:is(.dark .dark\:focus\:border-gray-700:focus){--tw-border-opacity: 1;border-color:rgb(51 65 85 / var(--tw-border-opacity))}:is(.dark .dark\:focus\:border-primary-300:focus){--tw-border-opacity: 1;border-color:rgb(165 180 252 / var(--tw-border-opacity))}:is(.dark .dark\:focus\:border-primary-600:focus){--tw-border-opacity: 1;border-color:rgb(79 70 229 / var(--tw-border-opacity))}:is(.dark .dark\:focus\:border-primary-700:focus){--tw-border-opacity: 1;border-color:rgb(67 56 202 / var(--tw-border-opacity))}:is(.dark .dark\:focus\:bg-gray-700:focus){--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity))}:is(.dark .dark\:focus\:bg-primary-900:focus){--tw-bg-opacity: 1;background-color:rgb(49 46 129 / var(--tw-bg-opacity))}:is(.dark .dark\:focus\:text-gray-200:focus){--tw-text-opacity: 1;color:rgb(226 232 240 / var(--tw-text-opacity))}:is(.dark .dark\:focus\:text-gray-300:focus){--tw-text-opacity: 1;color:rgb(203 213 225 / var(--tw-text-opacity))}:is(.dark .dark\:focus\:text-primary-200:focus){--tw-text-opacity: 1;color:rgb(199 210 254 / var(--tw-text-opacity))}:is(.dark .dark\:focus\:ring-indigo-600:focus){--tw-ring-opacity: 1;--tw-ring-color: rgb(79 70 229 / var(--tw-ring-opacity))}:is(.dark .dark\:focus\:ring-primary-600:focus){--tw-ring-opacity: 1;--tw-ring-color: rgb(79 70 229 / var(--tw-ring-opacity))}:is(.dark .dark\:focus\:ring-primary-700:focus){--tw-ring-opacity: 1;--tw-ring-color: rgb(67 56 202 / var(--tw-ring-opacity))}:is(.dark .dark\:focus\:ring-opacity-40:focus){--tw-ring-opacity: .4}:is(.dark .dark\:focus\:ring-offset-gray-800:focus){--tw-ring-offset-color: #1e293b}@media (min-width: 640px){.sm\:mx-auto{margin-left:auto;margin-right:auto}.sm\:flex{display:flex}.sm\:hidden{display:none}.sm\:w-full{width:100%}.sm\:max-w-2xl{max-width:42rem}.sm\:max-w-3xl{max-width:48rem}.sm\:max-w-4xl{max-width:56rem}.sm\:max-w-lg{max-width:32rem}.sm\:max-w-md{max-width:28rem}.sm\:max-w-sm{max-width:24rem}.sm\:max-w-xl{max-width:36rem}.sm\:flex-1{flex:1 1 0%}.sm\:translate-y-0{--tw-translate-y: 0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.sm\:scale-100{--tw-scale-x: 1;--tw-scale-y: 1;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.sm\:scale-95{--tw-scale-x: .95;--tw-scale-y: .95;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.sm\:items-center{align-items:center}.sm\:justify-center{justify-content:center}.sm\:justify-between{justify-content:space-between}.sm\:rounded-md{border-radius:.375rem}.sm\:rounded-bl-md{border-bottom-left-radius:.375rem}.sm\:rounded-br-md{border-bottom-right-radius:.375rem}.sm\:rounded-tl-md{border-top-left-radius:.375rem}.sm\:rounded-tr-md{border-top-right-radius:.375rem}.sm\:p-6{padding:1.5rem}.sm\:px-0{padding-left:0;padding-right:0}.sm\:px-6{padding-left:1.5rem;padding-right:1.5rem}.sm\:pt-0{padding-top:0}.sm\:text-sm{font-size:.875rem;line-height:1.25rem}}@media (min-width: 768px){.md\:col-span-1{grid-column:span 1 / span 1}.md\:block{display:block}.md\:justify-start{justify-content:flex-start}.md\:text-left{text-align:left}}@media (min-width: 1024px){.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:px-8{padding-left:2rem;padding-right:2rem}} diff --git a/public/build/assets/app-c41c626e.js b/public/build/assets/app-c41c626e.js new file mode 100644 index 0000000..00390c2 --- /dev/null +++ b/public/build/assets/app-c41c626e.js @@ -0,0 +1,21 @@ +var flushPending=!1,flushing=!1,queue=[],lastFlushedIndex=-1;function scheduler(ze){queueJob(ze)}function queueJob(ze){queue.includes(ze)||queue.push(ze),queueFlush()}function dequeueJob(ze){let Qr=queue.indexOf(ze);Qr!==-1&&Qr>lastFlushedIndex&&queue.splice(Qr,1)}function queueFlush(){!flushing&&!flushPending&&(flushPending=!0,queueMicrotask(flushJobs))}function flushJobs(){flushPending=!1,flushing=!0;for(let ze=0;zeze.effect(Qr,{scheduler:Kr=>{shouldSchedule?scheduler(Kr):Kr()}}),raw=ze.raw}function overrideEffect(ze){effect=ze}function elementBoundEffect(ze){let Qr=()=>{};return[Gr=>{let en=effect(Gr);return ze._x_effects||(ze._x_effects=new Set,ze._x_runEffects=()=>{ze._x_effects.forEach(nn=>nn())}),ze._x_effects.add(en),Qr=()=>{en!==void 0&&(ze._x_effects.delete(en),release(en))},en},()=>{Qr()}]}var onAttributeAddeds=[],onElRemoveds=[],onElAddeds=[];function onElAdded(ze){onElAddeds.push(ze)}function onElRemoved(ze,Qr){typeof Qr=="function"?(ze._x_cleanups||(ze._x_cleanups=[]),ze._x_cleanups.push(Qr)):(Qr=ze,onElRemoveds.push(Qr))}function onAttributesAdded(ze){onAttributeAddeds.push(ze)}function onAttributeRemoved(ze,Qr,Kr){ze._x_attributeCleanups||(ze._x_attributeCleanups={}),ze._x_attributeCleanups[Qr]||(ze._x_attributeCleanups[Qr]=[]),ze._x_attributeCleanups[Qr].push(Kr)}function cleanupAttributes(ze,Qr){ze._x_attributeCleanups&&Object.entries(ze._x_attributeCleanups).forEach(([Kr,Gr])=>{(Qr===void 0||Qr.includes(Kr))&&(Gr.forEach(en=>en()),delete ze._x_attributeCleanups[Kr])})}var observer=new MutationObserver(onMutate),currentlyObserving=!1;function startObservingMutations(){observer.observe(document,{subtree:!0,childList:!0,attributes:!0,attributeOldValue:!0}),currentlyObserving=!0}function stopObservingMutations(){flushObserver(),observer.disconnect(),currentlyObserving=!1}var recordQueue=[],willProcessRecordQueue=!1;function flushObserver(){recordQueue=recordQueue.concat(observer.takeRecords()),recordQueue.length&&!willProcessRecordQueue&&(willProcessRecordQueue=!0,queueMicrotask(()=>{processRecordQueue(),willProcessRecordQueue=!1}))}function processRecordQueue(){onMutate(recordQueue),recordQueue.length=0}function mutateDom(ze){if(!currentlyObserving)return ze();stopObservingMutations();let Qr=ze();return startObservingMutations(),Qr}var isCollecting=!1,deferredMutations=[];function deferMutations(){isCollecting=!0}function flushAndStopDeferringMutations(){isCollecting=!1,onMutate(deferredMutations),deferredMutations=[]}function onMutate(ze){if(isCollecting){deferredMutations=deferredMutations.concat(ze);return}let Qr=[],Kr=[],Gr=new Map,en=new Map;for(let nn=0;nnsn.nodeType===1&&Qr.push(sn)),ze[nn].removedNodes.forEach(sn=>sn.nodeType===1&&Kr.push(sn))),ze[nn].type==="attributes")){let sn=ze[nn].target,ln=ze[nn].attributeName,pn=ze[nn].oldValue,bn=()=>{Gr.has(sn)||Gr.set(sn,[]),Gr.get(sn).push({name:ln,value:sn.getAttribute(ln)})},Tn=()=>{en.has(sn)||en.set(sn,[]),en.get(sn).push(ln)};sn.hasAttribute(ln)&&pn===null?bn():sn.hasAttribute(ln)?(Tn(),bn()):Tn()}en.forEach((nn,sn)=>{cleanupAttributes(sn,nn)}),Gr.forEach((nn,sn)=>{onAttributeAddeds.forEach(ln=>ln(sn,nn))});for(let nn of Kr)if(!Qr.includes(nn)&&(onElRemoveds.forEach(sn=>sn(nn)),nn._x_cleanups))for(;nn._x_cleanups.length;)nn._x_cleanups.pop()();Qr.forEach(nn=>{nn._x_ignoreSelf=!0,nn._x_ignore=!0});for(let nn of Qr)Kr.includes(nn)||nn.isConnected&&(delete nn._x_ignoreSelf,delete nn._x_ignore,onElAddeds.forEach(sn=>sn(nn)),nn._x_ignore=!0,nn._x_ignoreSelf=!0);Qr.forEach(nn=>{delete nn._x_ignoreSelf,delete nn._x_ignore}),Qr=null,Kr=null,Gr=null,en=null}function scope(ze){return mergeProxies(closestDataStack(ze))}function addScopeToNode(ze,Qr,Kr){return ze._x_dataStack=[Qr,...closestDataStack(Kr||ze)],()=>{ze._x_dataStack=ze._x_dataStack.filter(Gr=>Gr!==Qr)}}function refreshScope(ze,Qr){let Kr=ze._x_dataStack[0];Object.entries(Qr).forEach(([Gr,en])=>{Kr[Gr]=en})}function closestDataStack(ze){return ze._x_dataStack?ze._x_dataStack:typeof ShadowRoot=="function"&&ze instanceof ShadowRoot?closestDataStack(ze.host):ze.parentNode?closestDataStack(ze.parentNode):[]}function mergeProxies(ze){let Qr=new Proxy({},{ownKeys:()=>Array.from(new Set(ze.flatMap(Kr=>Object.keys(Kr)))),has:(Kr,Gr)=>ze.some(en=>en.hasOwnProperty(Gr)),get:(Kr,Gr)=>(ze.find(en=>{if(en.hasOwnProperty(Gr)){let nn=Object.getOwnPropertyDescriptor(en,Gr);if(nn.get&&nn.get._x_alreadyBound||nn.set&&nn.set._x_alreadyBound)return!0;if((nn.get||nn.set)&&nn.enumerable){let sn=nn.get,ln=nn.set,pn=nn;sn=sn&&sn.bind(Qr),ln=ln&&ln.bind(Qr),sn&&(sn._x_alreadyBound=!0),ln&&(ln._x_alreadyBound=!0),Object.defineProperty(en,Gr,{...pn,get:sn,set:ln})}return!0}return!1})||{})[Gr],set:(Kr,Gr,en)=>{let nn=ze.find(sn=>sn.hasOwnProperty(Gr));return nn?nn[Gr]=en:ze[ze.length-1][Gr]=en,!0}});return Qr}function initInterceptors(ze){let Qr=Gr=>typeof Gr=="object"&&!Array.isArray(Gr)&&Gr!==null,Kr=(Gr,en="")=>{Object.entries(Object.getOwnPropertyDescriptors(Gr)).forEach(([nn,{value:sn,enumerable:ln}])=>{if(ln===!1||sn===void 0)return;let pn=en===""?nn:`${en}.${nn}`;typeof sn=="object"&&sn!==null&&sn._x_interceptor?Gr[nn]=sn.initialize(ze,pn,nn):Qr(sn)&&sn!==Gr&&!(sn instanceof Element)&&Kr(sn,pn)})};return Kr(ze)}function interceptor(ze,Qr=()=>{}){let Kr={initialValue:void 0,_x_interceptor:!0,initialize(Gr,en,nn){return ze(this.initialValue,()=>get(Gr,en),sn=>set(Gr,en,sn),en,nn)}};return Qr(Kr),Gr=>{if(typeof Gr=="object"&&Gr!==null&&Gr._x_interceptor){let en=Kr.initialize.bind(Kr);Kr.initialize=(nn,sn,ln)=>{let pn=Gr.initialize(nn,sn,ln);return Kr.initialValue=pn,en(nn,sn,ln)}}else Kr.initialValue=Gr;return Kr}}function get(ze,Qr){return Qr.split(".").reduce((Kr,Gr)=>Kr[Gr],ze)}function set(ze,Qr,Kr){if(typeof Qr=="string"&&(Qr=Qr.split(".")),Qr.length===1)ze[Qr[0]]=Kr;else{if(Qr.length===0)throw error;return ze[Qr[0]]||(ze[Qr[0]]={}),set(ze[Qr[0]],Qr.slice(1),Kr)}}var magics={};function magic(ze,Qr){magics[ze]=Qr}function injectMagics(ze,Qr){return Object.entries(magics).forEach(([Kr,Gr])=>{Object.defineProperty(ze,`$${Kr}`,{get(){let[en,nn]=getElementBoundUtilities(Qr);return en={interceptor,...en},onElRemoved(Qr,nn),Gr(Qr,en)},enumerable:!1})}),ze}function tryCatch(ze,Qr,Kr,...Gr){try{return Kr(...Gr)}catch(en){handleError(en,ze,Qr)}}function handleError(ze,Qr,Kr=void 0){Object.assign(ze,{el:Qr,expression:Kr}),console.warn(`Alpine Expression Error: ${ze.message} + +${Kr?'Expression: "'+Kr+`" + +`:""}`,Qr),setTimeout(()=>{throw ze},0)}var shouldAutoEvaluateFunctions=!0;function dontAutoEvaluateFunctions(ze){let Qr=shouldAutoEvaluateFunctions;shouldAutoEvaluateFunctions=!1,ze(),shouldAutoEvaluateFunctions=Qr}function evaluate(ze,Qr,Kr={}){let Gr;return evaluateLater(ze,Qr)(en=>Gr=en,Kr),Gr}function evaluateLater(...ze){return theEvaluatorFunction(...ze)}var theEvaluatorFunction=normalEvaluator;function setEvaluator(ze){theEvaluatorFunction=ze}function normalEvaluator(ze,Qr){let Kr={};injectMagics(Kr,ze);let Gr=[Kr,...closestDataStack(ze)],en=typeof Qr=="function"?generateEvaluatorFromFunction(Gr,Qr):generateEvaluatorFromString(Gr,Qr,ze);return tryCatch.bind(null,ze,Qr,en)}function generateEvaluatorFromFunction(ze,Qr){return(Kr=()=>{},{scope:Gr={},params:en=[]}={})=>{let nn=Qr.apply(mergeProxies([Gr,...ze]),en);runIfTypeOfFunction(Kr,nn)}}var evaluatorMemo={};function generateFunctionFromString(ze,Qr){if(evaluatorMemo[ze])return evaluatorMemo[ze];let Kr=Object.getPrototypeOf(async function(){}).constructor,Gr=/^[\n\s]*if.*\(.*\)/.test(ze)||/^(let|const)\s/.test(ze)?`(async()=>{ ${ze} })()`:ze,nn=(()=>{try{return new Kr(["__self","scope"],`with (scope) { __self.result = ${Gr} }; __self.finished = true; return __self.result;`)}catch(sn){return handleError(sn,Qr,ze),Promise.resolve()}})();return evaluatorMemo[ze]=nn,nn}function generateEvaluatorFromString(ze,Qr,Kr){let Gr=generateFunctionFromString(Qr,Kr);return(en=()=>{},{scope:nn={},params:sn=[]}={})=>{Gr.result=void 0,Gr.finished=!1;let ln=mergeProxies([nn,...ze]);if(typeof Gr=="function"){let pn=Gr(Gr,ln).catch(bn=>handleError(bn,Kr,Qr));Gr.finished?(runIfTypeOfFunction(en,Gr.result,ln,sn,Kr),Gr.result=void 0):pn.then(bn=>{runIfTypeOfFunction(en,bn,ln,sn,Kr)}).catch(bn=>handleError(bn,Kr,Qr)).finally(()=>Gr.result=void 0)}}}function runIfTypeOfFunction(ze,Qr,Kr,Gr,en){if(shouldAutoEvaluateFunctions&&typeof Qr=="function"){let nn=Qr.apply(Kr,Gr);nn instanceof Promise?nn.then(sn=>runIfTypeOfFunction(ze,sn,Kr,Gr)).catch(sn=>handleError(sn,en,Qr)):ze(nn)}else typeof Qr=="object"&&Qr instanceof Promise?Qr.then(nn=>ze(nn)):ze(Qr)}var prefixAsString="x-";function prefix(ze=""){return prefixAsString+ze}function setPrefix(ze){prefixAsString=ze}var directiveHandlers={};function directive(ze,Qr){return directiveHandlers[ze]=Qr,{before(Kr){if(!directiveHandlers[Kr]){console.warn("Cannot find directive `${directive}`. `${name}` will use the default order of execution");return}const Gr=directiveOrder.indexOf(Kr);directiveOrder.splice(Gr>=0?Gr:directiveOrder.indexOf("DEFAULT"),0,ze)}}}function directives(ze,Qr,Kr){if(Qr=Array.from(Qr),ze._x_virtualDirectives){let nn=Object.entries(ze._x_virtualDirectives).map(([ln,pn])=>({name:ln,value:pn})),sn=attributesOnly(nn);nn=nn.map(ln=>sn.find(pn=>pn.name===ln.name)?{name:`x-bind:${ln.name}`,value:`"${ln.value}"`}:ln),Qr=Qr.concat(nn)}let Gr={};return Qr.map(toTransformedAttributes((nn,sn)=>Gr[nn]=sn)).filter(outNonAlpineAttributes).map(toParsedDirectives(Gr,Kr)).sort(byPriority).map(nn=>getDirectiveHandler(ze,nn))}function attributesOnly(ze){return Array.from(ze).map(toTransformedAttributes()).filter(Qr=>!outNonAlpineAttributes(Qr))}var isDeferringHandlers=!1,directiveHandlerStacks=new Map,currentHandlerStackKey=Symbol();function deferHandlingDirectives(ze){isDeferringHandlers=!0;let Qr=Symbol();currentHandlerStackKey=Qr,directiveHandlerStacks.set(Qr,[]);let Kr=()=>{for(;directiveHandlerStacks.get(Qr).length;)directiveHandlerStacks.get(Qr).shift()();directiveHandlerStacks.delete(Qr)},Gr=()=>{isDeferringHandlers=!1,Kr()};ze(Kr),Gr()}function getElementBoundUtilities(ze){let Qr=[],Kr=ln=>Qr.push(ln),[Gr,en]=elementBoundEffect(ze);return Qr.push(en),[{Alpine:alpine_default,effect:Gr,cleanup:Kr,evaluateLater:evaluateLater.bind(evaluateLater,ze),evaluate:evaluate.bind(evaluate,ze)},()=>Qr.forEach(ln=>ln())]}function getDirectiveHandler(ze,Qr){let Kr=()=>{},Gr=directiveHandlers[Qr.type]||Kr,[en,nn]=getElementBoundUtilities(ze);onAttributeRemoved(ze,Qr.original,nn);let sn=()=>{ze._x_ignore||ze._x_ignoreSelf||(Gr.inline&&Gr.inline(ze,Qr,en),Gr=Gr.bind(Gr,ze,Qr,en),isDeferringHandlers?directiveHandlerStacks.get(currentHandlerStackKey).push(Gr):Gr())};return sn.runCleanups=nn,sn}var startingWith=(ze,Qr)=>({name:Kr,value:Gr})=>(Kr.startsWith(ze)&&(Kr=Kr.replace(ze,Qr)),{name:Kr,value:Gr}),into=ze=>ze;function toTransformedAttributes(ze=()=>{}){return({name:Qr,value:Kr})=>{let{name:Gr,value:en}=attributeTransformers.reduce((nn,sn)=>sn(nn),{name:Qr,value:Kr});return Gr!==Qr&&ze(Gr,Qr),{name:Gr,value:en}}}var attributeTransformers=[];function mapAttributes(ze){attributeTransformers.push(ze)}function outNonAlpineAttributes({name:ze}){return alpineAttributeRegex().test(ze)}var alpineAttributeRegex=()=>new RegExp(`^${prefixAsString}([^:^.]+)\\b`);function toParsedDirectives(ze,Qr){return({name:Kr,value:Gr})=>{let en=Kr.match(alpineAttributeRegex()),nn=Kr.match(/:([a-zA-Z0-9\-:]+)/),sn=Kr.match(/\.[^.\]]+(?=[^\]]*$)/g)||[],ln=Qr||ze[Kr]||Kr;return{type:en?en[1]:null,value:nn?nn[1]:null,modifiers:sn.map(pn=>pn.replace(".","")),expression:Gr,original:ln}}}var DEFAULT="DEFAULT",directiveOrder=["ignore","ref","data","id","bind","init","for","model","modelable","transition","show","if",DEFAULT,"teleport"];function byPriority(ze,Qr){let Kr=directiveOrder.indexOf(ze.type)===-1?DEFAULT:ze.type,Gr=directiveOrder.indexOf(Qr.type)===-1?DEFAULT:Qr.type;return directiveOrder.indexOf(Kr)-directiveOrder.indexOf(Gr)}function dispatch(ze,Qr,Kr={}){ze.dispatchEvent(new CustomEvent(Qr,{detail:Kr,bubbles:!0,composed:!0,cancelable:!0}))}function walk(ze,Qr){if(typeof ShadowRoot=="function"&&ze instanceof ShadowRoot){Array.from(ze.children).forEach(en=>walk(en,Qr));return}let Kr=!1;if(Qr(ze,()=>Kr=!0),Kr)return;let Gr=ze.firstElementChild;for(;Gr;)walk(Gr,Qr),Gr=Gr.nextElementSibling}function warn(ze,...Qr){console.warn(`Alpine Warning: ${ze}`,...Qr)}function start(){document.body||warn("Unable to initialize. Trying to load Alpine before `` is available. Did you forget to add `defer` in Alpine's ` + diff --git a/resources/views/components/icon-button.blade.php b/resources/views/components/icon-button.blade.php index 01a6fe7..433ba86 100644 --- a/resources/views/components/icon-button.blade.php +++ b/resources/views/components/icon-button.blade.php @@ -1,15 +1,18 @@ -@props(['href']) +@props([ + "href", +]) @php - $class = 'w-max inline-flex items-center justify-center px-2 py-1 font-semibold capitalize transition hover:opacity-50 outline-0 focus:ring focus:ring-primary-200 disabled:opacity-25 dark:focus:ring-primary-700 dark:focus:ring-opacity-40'; + $class = + "inline-flex w-max items-center justify-center px-2 py-1 font-semibold capitalize outline-0 transition hover:opacity-50 focus:ring focus:ring-primary-200 disabled:opacity-25 dark:focus:ring-primary-700 dark:focus:ring-opacity-40"; @endphp -@if(isset($href)) - merge(['class' => $class]) }}> +@if (isset($href)) + merge(["class" => $class]) }}> {{ $slot }} @else - @endif diff --git a/resources/views/components/input-error.blade.php b/resources/views/components/input-error.blade.php index ad95f6b..4351d5a 100644 --- a/resources/views/components/input-error.blade.php +++ b/resources/views/components/input-error.blade.php @@ -1,7 +1,9 @@ -@props(['messages']) +@props([ + "messages", +]) @if ($messages) -
    merge(['class' => 'text-sm text-red-600 dark:text-red-400 space-y-1']) }}> +
      merge(["class" => "text-sm text-red-600 dark:text-red-400 space-y-1"]) }}> @foreach ((array) $messages as $message)
    • {{ $message }}
    • @endforeach diff --git a/resources/views/components/input-help.blade.php b/resources/views/components/input-help.blade.php index 808655e..62faf84 100644 --- a/resources/views/components/input-help.blade.php +++ b/resources/views/components/input-help.blade.php @@ -1 +1,5 @@ -

      merge(['class' => 'mt-2 text-sm text-gray-500 dark:text-gray-300']) }}>{{ $slot }}

      +

      merge(["class" => "mt-2 text-sm text-gray-500 dark:text-gray-300"]) }} +> + {{ $slot }} +

      diff --git a/resources/views/components/input-label.blade.php b/resources/views/components/input-label.blade.php index e93b059..8afad86 100644 --- a/resources/views/components/input-label.blade.php +++ b/resources/views/components/input-label.blade.php @@ -1,5 +1,9 @@ -@props(['value']) +@props([ + "value", +]) -