diff --git a/app/Actions/CronJob/CreateCronJob.php b/app/Actions/CronJob/CreateCronJob.php index 905221b..6307d09 100755 --- a/app/Actions/CronJob/CreateCronJob.php +++ b/app/Actions/CronJob/CreateCronJob.php @@ -6,6 +6,7 @@ use App\Models\CronJob; use App\Models\Server; use App\ValidationRules\CronRule; +use Illuminate\Validation\Rule; class CreateCronJob { @@ -27,7 +28,7 @@ public function create(Server $server, array $input): CronJob return $cronJob; } - public static function rules(array $input): array + public static function rules(array $input, Server $server): array { $rules = [ 'command' => [ @@ -35,7 +36,7 @@ public static function rules(array $input): array ], 'user' => [ 'required', - 'in:root,'.config('core.ssh_user'), + Rule::in($server->getSshUsers()), ], 'frequency' => [ 'required', diff --git a/app/Actions/Queue/CreateQueue.php b/app/Actions/Queue/CreateQueue.php index 68efde0..eacc84c 100644 --- a/app/Actions/Queue/CreateQueue.php +++ b/app/Actions/Queue/CreateQueue.php @@ -46,7 +46,7 @@ public function create(mixed $queueable, array $input): void })->onConnection('ssh'); } - public static function rules(Server $server): array + public static function rules(Site $site): array { return [ 'command' => [ @@ -56,7 +56,7 @@ public static function rules(Server $server): array 'required', Rule::in([ 'root', - $server->ssh_user, + $site->user, ]), ], 'numprocs' => [ diff --git a/app/Actions/Script/ExecuteScript.php b/app/Actions/Script/ExecuteScript.php index ca2dd0a..0740ba3 100644 --- a/app/Actions/Script/ExecuteScript.php +++ b/app/Actions/Script/ExecuteScript.php @@ -41,9 +41,11 @@ public function execute(Script $script, array $input): ScriptExecution public static function rules(array $input): array { + $users = ['root']; if (isset($input['server'])) { /** @var ?Server $server */ $server = Server::query()->find($input['server']); + $users = $server->getSshUsers(); } return [ @@ -53,10 +55,7 @@ public static function rules(array $input): array ], 'user' => [ 'required', - Rule::in([ - 'root', - isset($server) ? $server?->ssh_user : null, - ]), + Rule::in($users), ], 'variables' => 'array', 'variables.*' => [ diff --git a/app/Actions/Site/CreateSite.php b/app/Actions/Site/CreateSite.php index f5ec875..195dc26 100755 --- a/app/Actions/Site/CreateSite.php +++ b/app/Actions/Site/CreateSite.php @@ -23,12 +23,14 @@ public function create(Server $server, array $input): Site { DB::beginTransaction(); try { + $user = $input['user'] ?? $server->getSshUser(); $site = new Site([ 'server_id' => $server->id, 'type' => $input['type'], 'domain' => $input['domain'], 'aliases' => $input['aliases'] ?? [], - 'path' => '/home/'.$server->getSshUser().'/'.$input['domain'], + 'user' => $user, + 'path' => '/home/'.$user.'/'.$input['domain'], 'status' => SiteStatus::INSTALLING, ]); @@ -108,6 +110,13 @@ public static function rules(Server $server, array $input): array 'aliases.*' => [ new DomainRule, ], + 'user' => [ + 'regex:/^[a-z_][a-z0-9_-]*[a-z0-9]$/', + 'min:3', + 'max:32', + 'unique:sites,user', + Rule::notIn($server->getSshUsers()), + ], ]; return array_merge($rules, self::typeRules($server, $input)); diff --git a/app/Actions/Site/DeleteSite.php b/app/Actions/Site/DeleteSite.php index 2258e40..e8de99b 100644 --- a/app/Actions/Site/DeleteSite.php +++ b/app/Actions/Site/DeleteSite.php @@ -3,6 +3,7 @@ namespace App\Actions\Site; use App\Models\Site; +use App\SSH\Services\PHP\PHP; use App\SSH\Services\Webserver\Webserver; class DeleteSite @@ -12,6 +13,16 @@ public function delete(Site $site): void /** @var Webserver $webserverHandler */ $webserverHandler = $site->server->webserver()->handler(); $webserverHandler->deleteSite($site); + + if ($site->isIsolated()) { + /** @var PHP $php */ + $php = $site->server->php()->handler(); + $php->removeFpmPool($site->user, $site->php_version, $site->id); + + $os = $site->server->os(); + $os->deleteIsolatedUser($site->user); + } + $site->delete(); } } diff --git a/app/Actions/Site/Deploy.php b/app/Actions/Site/Deploy.php index 7a86686..8b8781d 100644 --- a/app/Actions/Site/Deploy.php +++ b/app/Actions/Site/Deploy.php @@ -48,7 +48,8 @@ public function run(Site $site): Deployment path: $site->path, script: $site->deploymentScript->content, serverLog: $log, - variables: $site->environmentVariables($deployment) + user: $site->user, + variables: $site->environmentVariables($deployment), ); $deployment->status = DeploymentStatus::FINISHED; $deployment->save(); diff --git a/app/Helpers/SSH.php b/app/Helpers/SSH.php index 3ef814e..4a1900c 100755 --- a/app/Helpers/SSH.php +++ b/app/Helpers/SSH.php @@ -43,7 +43,6 @@ public function init(Server $server, ?string $asUser = null): self $this->server = $server->refresh(); $this->user = $server->getSshUser(); if ($asUser && $asUser != $server->getSshUser()) { - $this->user = $asUser; $this->asUser = $asUser; } $this->privateKey = PublicKeyLoader::loadPrivateKey( @@ -146,6 +145,10 @@ public function exec(string $command, string $log = '', ?int $siteId = null, ?bo return $output; } } catch (Throwable $e) { + Log::error('Error executing command', [ + 'msg' => $e->getMessage(), + 'log' => $this->log, + ]); throw new SSHCommandError( message: $e->getMessage(), log: $this->log diff --git a/app/Http/Controllers/API/CronJobController.php b/app/Http/Controllers/API/CronJobController.php index cdb3fbc..d4a6b7c 100644 --- a/app/Http/Controllers/API/CronJobController.php +++ b/app/Http/Controllers/API/CronJobController.php @@ -51,7 +51,7 @@ public function create(Request $request, Project $project, Server $server): Cron $this->validateRoute($project, $server); - $this->validate($request, CreateCronJob::rules($request->all())); + $this->validate($request, CreateCronJob::rules($request->all(), $server)); $cronJob = app(CreateCronJob::class)->create($server, $request->all()); diff --git a/app/Http/Resources/SiteResource.php b/app/Http/Resources/SiteResource.php index 4d97591..22a48bb 100644 --- a/app/Http/Resources/SiteResource.php +++ b/app/Http/Resources/SiteResource.php @@ -26,6 +26,7 @@ public function toArray(Request $request): array 'branch' => $this->branch, 'status' => $this->status, 'port' => $this->port, + 'user' => $this->user, 'progress' => $this->progress, 'created_at' => $this->created_at, 'updated_at' => $this->updated_at, diff --git a/app/Models/Server.php b/app/Models/Server.php index a800e45..2248760 100755 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -267,6 +267,15 @@ public function getSshUser(): string return config('core.ssh_user'); } + public function getSshUsers(): array + { + $users = ['root', $this->getSshUser()]; + $isolatedSites = $this->sites()->pluck('user')->toArray(); + $users = array_merge($users, $isolatedSites); + + return array_unique($users); + } + public function service($type, $version = null): ?Service { /* @var Service $service */ diff --git a/app/Models/Site.php b/app/Models/Site.php index 0852df8..c4f3c4d 100755 --- a/app/Models/Site.php +++ b/app/Models/Site.php @@ -7,6 +7,7 @@ use App\Exceptions\SourceControlIsNotConnected; use App\Exceptions\SSHError; use App\SiteTypes\SiteType; +use App\SSH\Services\PHP\PHP; use App\SSH\Services\Webserver\Webserver; use App\Traits\HasProjectThroughServer; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -33,6 +34,7 @@ * @property string $status * @property int $port * @property int $progress + * @property string $user * @property Server $server * @property ServerLog[] $logs * @property Deployment[] $deployments @@ -68,6 +70,7 @@ class Site extends AbstractModel 'status', 'port', 'progress', + 'user', ]; protected $casts = [ @@ -200,6 +203,14 @@ public function changePHPVersion($version): void /** @var Webserver $handler */ $handler = $this->server->webserver()->handler(); $handler->changePHPVersion($this, $version); + + if ($this->isIsolated()) { + /** @var PHP $php */ + $php = $this->server->php()->handler(); + $php->removeFpmPool($this->user, $this->php_version, $this->id); + $php->createFpmPool($this->user, $version, $this->id); + } + $this->php_version = $version; $this->save(); } @@ -307,4 +318,31 @@ public function environmentVariables(?Deployment $deployment = null): array 'PHP_PATH' => '/usr/bin/php'.$this->php_version, ]; } + + public function isolate(): void + { + if (! $this->isIsolated()) { + return; + } + + $this->server->os()->createIsolatedUser( + $this->user, + Str::random(15), + $this->id + ); + + // Generate the FPM pool + /** @var PHP $php */ + $php = $this->php()->handler(); + $php->createFpmPool( + $this->user, + $this->php_version, + $this->id + ); + } + + public function isIsolated(): bool + { + return $this->user != $this->server->getSshUser(); + } } diff --git a/app/SSH/Composer/Composer.php b/app/SSH/Composer/Composer.php index e7a6f61..cea0acb 100644 --- a/app/SSH/Composer/Composer.php +++ b/app/SSH/Composer/Composer.php @@ -11,7 +11,7 @@ class Composer public function installDependencies(Site $site): void { - $site->server->ssh()->exec( + $site->server->ssh($site->user)->exec( $this->getScript('composer-install.sh', [ 'path' => $site->path, 'php_version' => $site->php_version, diff --git a/app/SSH/Git/Git.php b/app/SSH/Git/Git.php index 22c1f0d..a02b58e 100644 --- a/app/SSH/Git/Git.php +++ b/app/SSH/Git/Git.php @@ -11,7 +11,7 @@ class Git public function clone(Site $site): void { - $site->server->ssh()->exec( + $site->server->ssh($site->user)->exec( $this->getScript('clone.sh', [ 'host' => str($site->getFullRepositoryUrl())->after('@')->before('-'), 'repo' => $site->getFullRepositoryUrl(), @@ -26,7 +26,7 @@ public function clone(Site $site): void public function checkout(Site $site): void { - $site->server->ssh()->exec( + $site->server->ssh($site->user)->exec( $this->getScript('checkout.sh', [ 'path' => $site->path, 'branch' => $site->branch, diff --git a/app/SSH/Git/scripts/clone.sh b/app/SSH/Git/scripts/clone.sh index e11a140..a7fa140 100755 --- a/app/SSH/Git/scripts/clone.sh +++ b/app/SSH/Git/scripts/clone.sh @@ -2,6 +2,8 @@ echo "Host __host__-__key__ Hostname __host__ IdentityFile=~/.ssh/__key__" >> ~/.ssh/config +chmod 600 ~/.ssh/config + ssh-keyscan -H __host__ >> ~/.ssh/known_hosts rm -rf __path__ diff --git a/app/SSH/OS/OS.php b/app/SSH/OS/OS.php index 3ce5fa0..3df64b8 100644 --- a/app/SSH/OS/OS.php +++ b/app/SSH/OS/OS.php @@ -5,6 +5,7 @@ use App\Exceptions\SSHUploadFailed; use App\Models\Server; use App\Models\ServerLog; +use App\Models\Site; use App\SSH\HasScripts; use Illuminate\Filesystem\FilesystemAdapter; use Illuminate\Support\Facades\Storage; @@ -58,6 +59,30 @@ public function createUser(string $user, string $password, string $key): void ); } + public function createIsolatedUser(string $user, string $password, int $site_id): void + { + $this->server->ssh()->exec( + $this->getScript('create-isolated-user.sh', [ + 'user' => $user, + 'server_user' => $this->server->getSshUser(), + 'password' => $password, + ]), + 'create-isolated-user', + $site_id + ); + } + + public function deleteIsolatedUser(string $user): void + { + $this->server->ssh()->exec( + $this->getScript('delete-isolated-user.sh', [ + 'user' => $user, + 'server_user' => $this->server->getSshUser(), + ]), + 'delete-isolated-user' + ); + } + public function getPublicKey(string $user): string { return $this->server->ssh()->exec( @@ -88,19 +113,20 @@ public function deleteSSHKey(string $key): void ); } - public function generateSSHKey(string $name): void + public function generateSSHKey(string $name, ?Site $site = null): void { - $this->server->ssh()->exec( + $site->server->ssh($site->user)->exec( $this->getScript('generate-ssh-key.sh', [ 'name' => $name, ]), - 'generate-ssh-key' + 'generate-ssh-key', + $site?->id ); } - public function readSSHKey(string $name): string + public function readSSHKey(string $name, ?Site $site = null): string { - return $this->server->ssh()->exec( + return $site->server->ssh($site->user)->exec( $this->getScript('read-ssh-key.sh', [ 'name' => $name, ]), diff --git a/app/SSH/OS/scripts/create-isolated-user.sh b/app/SSH/OS/scripts/create-isolated-user.sh new file mode 100644 index 0000000..8c3280d --- /dev/null +++ b/app/SSH/OS/scripts/create-isolated-user.sh @@ -0,0 +1,17 @@ +export DEBIAN_FRONTEND=noninteractive +if ! sudo useradd -p $(openssl passwd -1 __password__) __user__; then + echo 'VITO_SSH_ERROR' && exit 1 +fi + +sudo mkdir /home/__user__ +sudo mkdir /home/__user__/.logs +sudo mkdir /home/__user__/tmp +sudo mkdir /home/__user__/bin +sudo mkdir /home/__user__/.ssh +echo 'export PATH="/home/__user__/bin:$PATH"' | sudo tee -a /home/__user__/.bashrc +sudo usermod -a -G __user__ __server_user__ +sudo chown -R __user__:__user__ /home/__user__ +sudo chmod -R 755 /home/__user__ +sudo chmod -R 700 /home/__user__/.ssh +sudo chsh -s /bin/bash __user__ +echo "Created user __user__." diff --git a/app/SSH/OS/scripts/delete-isolated-user.sh b/app/SSH/OS/scripts/delete-isolated-user.sh new file mode 100644 index 0000000..d73f38c --- /dev/null +++ b/app/SSH/OS/scripts/delete-isolated-user.sh @@ -0,0 +1,3 @@ +sudo gpasswd -d __server_user__ __user__ +sudo userdel -r "__user__" +echo "User __user__ has been deleted." diff --git a/app/SSH/PHPMyAdmin/PHPMyAdmin.php b/app/SSH/PHPMyAdmin/PHPMyAdmin.php index a8160fd..1ffbff8 100644 --- a/app/SSH/PHPMyAdmin/PHPMyAdmin.php +++ b/app/SSH/PHPMyAdmin/PHPMyAdmin.php @@ -11,7 +11,7 @@ class PHPMyAdmin public function install(Site $site): void { - $site->server->ssh()->exec( + $site->server->ssh($site->user)->exec( $this->getScript('install.sh', [ 'version' => $site->type_data['version'], 'path' => $site->path, diff --git a/app/SSH/PHPMyAdmin/scripts/install.sh b/app/SSH/PHPMyAdmin/scripts/install.sh index 34b7cf0..4e5d67c 100755 --- a/app/SSH/PHPMyAdmin/scripts/install.sh +++ b/app/SSH/PHPMyAdmin/scripts/install.sh @@ -1,6 +1,10 @@ -sudo rm -rf phpmyadmin +if ! rm -rf phpmyadmin; then + echo 'VITO_SSH_ERROR' && exit 1 +fi -sudo rm -rf __path__ +if ! rm -rf __path__; then + echo 'VITO_SSH_ERROR' && exit 1 +fi if ! wget https://files.phpmyadmin.net/phpMyAdmin/__version__/phpMyAdmin-__version__-all-languages.zip; then echo 'VITO_SSH_ERROR' && exit 1 diff --git a/app/SSH/Services/PHP/PHP.php b/app/SSH/Services/PHP/PHP.php index 3e6c322..b8baa81 100644 --- a/app/SSH/Services/PHP/PHP.php +++ b/app/SSH/Services/PHP/PHP.php @@ -110,4 +110,32 @@ public function getPHPIni(string $type): string sprintf('/etc/php/%s/%s/php.ini', $this->service->version, $type) ); } + + public function createFpmPool(string $user, string $version, $site_id): void + { + $this->service->server->ssh()->exec( + $this->getScript('create-fpm-pool.sh', [ + 'user' => $user, + 'version' => $version, + 'config' => $this->getScript('fpm-pool.conf', [ + 'user' => $user, + 'version' => $version, + ]), + ]), + "create-{$version}fpm-pool-{$user}", + $site_id + ); + } + + public function removeFpmPool(string $user, string $version, $site_id): void + { + $this->service->server->ssh()->exec( + $this->getScript('remove-fpm-pool.sh', [ + 'user' => $user, + 'version' => $version, + ]), + "remove-{$version}fpm-pool-{$user}", + $site_id + ); + } } diff --git a/app/SSH/Services/PHP/scripts/create-fpm-pool.sh b/app/SSH/Services/PHP/scripts/create-fpm-pool.sh new file mode 100644 index 0000000..4eaee89 --- /dev/null +++ b/app/SSH/Services/PHP/scripts/create-fpm-pool.sh @@ -0,0 +1,2 @@ +echo '__config__' | sudo tee /etc/php/__version__/fpm/pool.d/__user__.conf +sudo service php__version__-fpm restart diff --git a/app/SSH/Services/PHP/scripts/fpm-pool.conf b/app/SSH/Services/PHP/scripts/fpm-pool.conf new file mode 100644 index 0000000..da34b87 --- /dev/null +++ b/app/SSH/Services/PHP/scripts/fpm-pool.conf @@ -0,0 +1,22 @@ +[__user__] +user = __user__ +group = __user__ + +listen = /run/php/php__version__-fpm-__user__.sock +listen.owner = vito +listen.group = vito +listen.mode = 0660 + +pm = dynamic +pm.max_children = 5 +pm.start_servers = 2 +pm.min_spare_servers = 1 +pm.max_spare_servers = 3 +pm.max_requests = 500 + +php_admin_value[open_basedir] = /home/__user__/:/tmp/ +php_admin_value[upload_tmp_dir] = /home/__user__/tmp +php_admin_value[session.save_path] = /home/__user__/tmp +php_admin_value[display_errors] = off +php_admin_value[log_errors] = on +php_admin_value[error_log] = /home/__user__/.logs/php_errors.log diff --git a/app/SSH/Services/PHP/scripts/remove-fpm-pool.sh b/app/SSH/Services/PHP/scripts/remove-fpm-pool.sh new file mode 100644 index 0000000..924f3b6 --- /dev/null +++ b/app/SSH/Services/PHP/scripts/remove-fpm-pool.sh @@ -0,0 +1,2 @@ +sudo rm -f /etc/php/__version__/fpm/pool.d/__user__.conf +sudo service php__version__-fpm restart diff --git a/app/SSH/Services/ProcessManager/Supervisor.php b/app/SSH/Services/ProcessManager/Supervisor.php index 5f84075..04dcf6e 100644 --- a/app/SSH/Services/ProcessManager/Supervisor.php +++ b/app/SSH/Services/ProcessManager/Supervisor.php @@ -53,9 +53,11 @@ public function create( ), true ); - $this->service->server->ssh($user)->exec( + $this->service->server->ssh()->exec( $this->getScript('supervisor/create-worker.sh', [ 'id' => $id, + 'log_file' => $logFile, + 'user' => $user, ]), 'create-worker', $siteId diff --git a/app/SSH/Services/ProcessManager/scripts/supervisor/create-worker.sh b/app/SSH/Services/ProcessManager/scripts/supervisor/create-worker.sh index 42801eb..846a20d 100644 --- a/app/SSH/Services/ProcessManager/scripts/supervisor/create-worker.sh +++ b/app/SSH/Services/ProcessManager/scripts/supervisor/create-worker.sh @@ -1,8 +1,14 @@ -mkdir -p ~/.logs +if ! sudo mkdir -p "$(dirname __log_file__)"; then + echo 'VITO_SSH_ERROR' && exit 1 +fi -mkdir -p ~/.logs/workers +if ! sudo touch __log_file__; then + echo 'VITO_SSH_ERROR' && exit 1 +fi -touch ~/.logs/workers/__id__.log +if ! sudo chown __user__:__user__ __log_file__; then + echo 'VITO_SSH_ERROR' && exit 1 +fi if ! sudo supervisorctl reread; then echo 'VITO_SSH_ERROR' && exit 1 diff --git a/app/SSH/Services/Webserver/Nginx.php b/app/SSH/Services/Webserver/Nginx.php index f1e749b..63463c6 100755 --- a/app/SSH/Services/Webserver/Nginx.php +++ b/app/SSH/Services/Webserver/Nginx.php @@ -52,8 +52,23 @@ public function uninstall(): void $this->service->server->os()->cleanup(); } + /** + * @throws SSHError + */ public function createVHost(Site $site): void { + // We need to get the isolated user first, if the site is isolated + // otherwise, use the default ssh user + $ssh = $this->service->server->ssh($site->user); + + $ssh->exec( + $this->getScript('nginx/create-path.sh', [ + 'path' => $site->path, + ]), + 'create-path', + $site->id + ); + $this->service->server->ssh()->exec( $this->getScript('nginx/create-vhost.sh', [ 'domain' => $site->domain, @@ -189,10 +204,16 @@ protected function generateVhost(Site $site, bool $noSSL = false): string $vhost = Str::replace('__port__', (string) $site->port, $vhost); } + $php_socket = 'unix:/var/run/php/php-fpm.sock'; + if ($site->isIsolated()) { + $php_socket = "unix:/run/php/php{$site->php_version}-fpm-{$site->user}.sock"; + } + $vhost = Str::replace('__domain__', $site->domain, $vhost); $vhost = Str::replace('__aliases__', $site->getAliasesString(), $vhost); $vhost = Str::replace('__path__', $site->path, $vhost); $vhost = Str::replace('__web_directory__', $site->web_directory, $vhost); + $vhost = Str::replace('__php_socket__', $php_socket, $vhost); if ($ssl) { $vhost = Str::replace('__certificate__', $ssl->getCertificatePath(), $vhost); diff --git a/app/SSH/Services/Webserver/scripts/nginx/create-path.sh b/app/SSH/Services/Webserver/scripts/nginx/create-path.sh new file mode 100644 index 0000000..aa9dd18 --- /dev/null +++ b/app/SSH/Services/Webserver/scripts/nginx/create-path.sh @@ -0,0 +1,16 @@ +export DEBIAN_FRONTEND=noninteractive + +if ! rm -rf __path__; then + echo 'VITO_SSH_ERROR' + exit 1 +fi + +if ! mkdir __path__; then + echo 'VITO_SSH_ERROR' + exit 1 +fi + +if ! chmod -R 755 __path__; then + echo 'VITO_SSH_ERROR' + exit 1 +fi diff --git a/app/SSH/Services/Webserver/scripts/nginx/create-vhost.sh b/app/SSH/Services/Webserver/scripts/nginx/create-vhost.sh index d12630d..b7f44df 100755 --- a/app/SSH/Services/Webserver/scripts/nginx/create-vhost.sh +++ b/app/SSH/Services/Webserver/scripts/nginx/create-vhost.sh @@ -1,15 +1,3 @@ -if ! rm -rf __path__; then - echo 'VITO_SSH_ERROR' && exit 1 -fi - -if ! mkdir __path__; then - echo 'VITO_SSH_ERROR' && exit 1 -fi - -if ! sudo chown -R 755 __path__; then - echo 'VITO_SSH_ERROR' && exit 1 -fi - if ! echo '' | sudo tee /etc/nginx/conf.d/__domain___redirects; then echo 'VITO_SSH_ERROR' && exit 1 fi diff --git a/app/SSH/Services/Webserver/scripts/nginx/php-vhost-ssl.conf b/app/SSH/Services/Webserver/scripts/nginx/php-vhost-ssl.conf index 480f381..596bd81 100755 --- a/app/SSH/Services/Webserver/scripts/nginx/php-vhost-ssl.conf +++ b/app/SSH/Services/Webserver/scripts/nginx/php-vhost-ssl.conf @@ -24,7 +24,7 @@ server { error_page 404 /index.php; location ~ \.php$ { - fastcgi_pass unix:/var/run/php/php__php_version__-fpm.sock; + fastcgi_pass __php_socket__; fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; include fastcgi_params; fastcgi_hide_header X-Powered-By; diff --git a/app/SSH/Services/Webserver/scripts/nginx/php-vhost.conf b/app/SSH/Services/Webserver/scripts/nginx/php-vhost.conf index 5734f58..80f33e5 100755 --- a/app/SSH/Services/Webserver/scripts/nginx/php-vhost.conf +++ b/app/SSH/Services/Webserver/scripts/nginx/php-vhost.conf @@ -20,7 +20,7 @@ server { error_page 404 /index.php; location ~ \.php$ { - fastcgi_pass unix:/var/run/php/php__php_version__-fpm.sock; + fastcgi_pass __php_socket__; fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; include fastcgi_params; fastcgi_hide_header X-Powered-By; diff --git a/app/SSH/Wordpress/Wordpress.php b/app/SSH/Wordpress/Wordpress.php index ca48767..12835ca 100644 --- a/app/SSH/Wordpress/Wordpress.php +++ b/app/SSH/Wordpress/Wordpress.php @@ -11,10 +11,12 @@ class Wordpress public function install(Site $site): void { - $site->server->ssh()->exec( + $site->server->ssh($site->user)->exec( $this->getScript('install.sh', [ 'path' => $site->path, 'domain' => $site->domain, + 'is_isolated' => $site->isIsolated(), + 'isolated_username' => $site->user, 'db_name' => $site->type_data['database'], 'db_user' => $site->type_data['database_user'], 'db_pass' => $site->type_data['database_password'], @@ -25,7 +27,8 @@ public function install(Site $site): void 'email' => $site->type_data['email'], 'title' => $site->type_data['title'], ]), - 'install-wordpress' + 'install-wordpress', + $site->id ); } } diff --git a/app/SSH/Wordpress/scripts/install.sh b/app/SSH/Wordpress/scripts/install.sh index 6a75987..3d8ca5c 100644 --- a/app/SSH/Wordpress/scripts/install.sh +++ b/app/SSH/Wordpress/scripts/install.sh @@ -6,8 +6,14 @@ if ! chmod +x wp-cli.phar; then echo 'VITO_SSH_ERROR' && exit 1 fi -if ! sudo mv wp-cli.phar /usr/local/bin/wp; then - echo 'VITO_SSH_ERROR' && exit 1 +if [ "__is_isolated__" == "true" ]; then + if ! mv wp-cli.phar /home/__isolated_username__/bin/wp; then + echo 'VITO_SSH_ERROR' && exit 1 + fi +else + if ! mv wp-cli.phar /usr/local/bin/wp; then + echo 'VITO_SSH_ERROR' && exit 1 + fi fi rm -rf __path__ @@ -16,12 +22,20 @@ if ! wp --path=__path__ core download; then echo 'VITO_SSH_ERROR' && exit 1 fi -if ! wp --path=__path__ core config --dbname='__db_name__' --dbuser='__db_user__' --dbpass='__db_pass__' --dbhost='__db_host__' --dbprefix='__db_prefix__'; then +if ! wp --path=__path__ core config \ + --dbname="__db_name__" \ + --dbuser="__db_user__" \ + --dbpass="__db_pass__" \ + --dbhost="__db_host__" \ + --dbprefix="__db_prefix__"; then echo 'VITO_SSH_ERROR' && exit 1 fi -if ! wp --path=__path__ core install --url='http://__domain__' --title="__title__" --admin_user='__username__' --admin_password="__password__" --admin_email='__email__'; then +if ! wp --path=__path__ core install \ + --url="http://__domain__" \ + --title="__title__" \ + --admin_user="__username__" \ + --admin_password="__password__" \ + --admin_email="__email__"; then echo 'VITO_SSH_ERROR' && exit 1 fi - -echo "Wordpress installed!" diff --git a/app/SiteTypes/AbstractSiteType.php b/app/SiteTypes/AbstractSiteType.php index 9e85e74..7450200 100755 --- a/app/SiteTypes/AbstractSiteType.php +++ b/app/SiteTypes/AbstractSiteType.php @@ -26,8 +26,8 @@ protected function progress(int $percentage): void protected function deployKey(): void { $os = $this->site->server->os(); - $os->generateSSHKey($this->site->getSshKeyName()); - $this->site->ssh_key = $os->readSSHKey($this->site->getSshKeyName()); + $os->generateSSHKey($this->site->getSshKeyName(), $this->site); + $this->site->ssh_key = $os->readSSHKey($this->site->getSshKeyName(), $this->site); $this->site->save(); $this->site->sourceControl?->provider()?->deployKey( $this->site->domain.'-key-'.$this->site->id, diff --git a/app/SiteTypes/PHPBlank.php b/app/SiteTypes/PHPBlank.php index fd65b04..ad9c01d 100755 --- a/app/SiteTypes/PHPBlank.php +++ b/app/SiteTypes/PHPBlank.php @@ -43,6 +43,8 @@ public function data(array $input): array public function install(): void { + $this->site->isolate(); + /** @var Webserver $webserver */ $webserver = $this->site->server->webserver()->handler(); $webserver->createVHost($this->site); diff --git a/app/SiteTypes/PHPMyAdmin.php b/app/SiteTypes/PHPMyAdmin.php index 6408db3..6520bed 100755 --- a/app/SiteTypes/PHPMyAdmin.php +++ b/app/SiteTypes/PHPMyAdmin.php @@ -43,6 +43,8 @@ public function data(array $input): array public function install(): void { + $this->site->isolate(); + /** @var Webserver $webserver */ $webserver = $this->site->server->webserver()->handler(); $webserver->createVHost($this->site); diff --git a/app/SiteTypes/PHPSite.php b/app/SiteTypes/PHPSite.php index 1934aa3..04b8339 100755 --- a/app/SiteTypes/PHPSite.php +++ b/app/SiteTypes/PHPSite.php @@ -76,6 +76,8 @@ public function data(array $input): array */ public function install(): void { + $this->site->isolate(); + /** @var Webserver $webserver */ $webserver = $this->site->server->webserver()->handler(); $webserver->createVHost($this->site); diff --git a/app/SiteTypes/Wordpress.php b/app/SiteTypes/Wordpress.php index 83365ee..9884aa0 100755 --- a/app/SiteTypes/Wordpress.php +++ b/app/SiteTypes/Wordpress.php @@ -84,6 +84,8 @@ public function data(array $input): array public function install(): void { + $this->site->isolate(); + /** @var Webserver $webserver */ $webserver = $this->site->server->webserver()->handler(); $webserver->createVHost($this->site); diff --git a/app/Support/Testing/SSHFake.php b/app/Support/Testing/SSHFake.php index e4cbcb9..5973d8f 100644 --- a/app/Support/Testing/SSHFake.php +++ b/app/Support/Testing/SSHFake.php @@ -37,7 +37,6 @@ public function init(Server $server, ?string $asUser = null): self $this->server = $server->refresh(); $this->user = $server->getSshUser(); if ($asUser && $asUser != $server->getSshUser()) { - $this->user = $asUser; $this->asUser = $asUser; } diff --git a/app/Web/Pages/Scripts/Executions.php b/app/Web/Pages/Scripts/Executions.php index 61d229d..8705c45 100644 --- a/app/Web/Pages/Scripts/Executions.php +++ b/app/Web/Pages/Scripts/Executions.php @@ -52,16 +52,14 @@ protected function getHeaderActions(): array ->rules(fn (Get $get) => ExecuteScript::rules($get())['user']) ->native(false) ->options(function (Get $get) { - $options = [ - 'root' => 'root', - ]; + $users = ['root']; $server = Server::query()->find($get('server')); if ($server) { - $options[$server->ssh_user] = $server->ssh_user; + $users = $server->getSshUsers(); } - return $options; + return array_combine($users, $users); }), ]; diff --git a/app/Web/Pages/Servers/CronJobs/Index.php b/app/Web/Pages/Servers/CronJobs/Index.php index bf0385d..e11f3e4 100644 --- a/app/Web/Pages/Servers/CronJobs/Index.php +++ b/app/Web/Pages/Servers/CronJobs/Index.php @@ -33,6 +33,8 @@ public function getWidgets(): array protected function getHeaderActions(): array { + $users = $this->server->getSshUsers(); + return [ Action::make('read-the-docs') ->label('Read the Docs') @@ -46,25 +48,22 @@ protected function getHeaderActions(): array ->modalWidth(MaxWidth::ExtraLarge) ->form([ TextInput::make('command') - ->rules(fn (callable $get) => CreateCronJob::rules($get())['command']) + ->rules(fn (callable $get) => CreateCronJob::rules($get(), $this->server)['command']) ->helperText(fn () => view('components.link', [ 'href' => 'https://vitodeploy.com/servers/cronjobs', 'external' => true, 'text' => 'How the command should look like?', ])), Select::make('user') - ->rules(fn (callable $get) => CreateCronJob::rules($get())['user']) - ->options([ - 'vito' => $this->server->ssh_user, - 'root' => 'root', - ]), + ->rules(fn (callable $get) => CreateCronJob::rules($get(), $this->server)['user']) + ->options(array_combine($users, $users)), Select::make('frequency') ->options(config('core.cronjob_intervals')) ->reactive() - ->rules(fn (callable $get) => CreateCronJob::rules($get())['frequency']), + ->rules(fn (callable $get) => CreateCronJob::rules($get(), $this->server)['frequency']), TextInput::make('custom') ->label('Custom Frequency (Cron)') - ->rules(fn (callable $get) => CreateCronJob::rules($get())['custom']) + ->rules(fn (callable $get) => CreateCronJob::rules($get(), $this->server)['custom']) ->visible(fn (callable $get) => $get('frequency') === 'custom') ->placeholder('0 * * * *'), ]) diff --git a/app/Web/Pages/Servers/Sites/Index.php b/app/Web/Pages/Servers/Sites/Index.php index 7c0ec60..528ff25 100644 --- a/app/Web/Pages/Servers/Sites/Index.php +++ b/app/Web/Pages/Servers/Sites/Index.php @@ -133,6 +133,13 @@ protected function getHeaderActions(): array ->rules(fn (Get $get) => CreateSite::rules($this->server, $get())['version']), // WordPress $this->wordpressFields(), + TextInput::make('user') + ->label('Username') + ->hintIcon('heroicon-o-information-circle') + ->hintIconTooltip( + 'Optional. If provided, a new user will be created and the site will be owned by this user.' + ) + ->rules(fn (Get $get) => CreateSite::rules($this->server, $get())['user']), ]) ->action(function (array $data) { $this->authorize('create', [Site::class, $this->server]); diff --git a/app/Web/Pages/Servers/Sites/Pages/Queues/Index.php b/app/Web/Pages/Servers/Sites/Pages/Queues/Index.php index aa3496e..d9cce81 100644 --- a/app/Web/Pages/Servers/Sites/Pages/Queues/Index.php +++ b/app/Web/Pages/Servers/Sites/Pages/Queues/Index.php @@ -47,17 +47,17 @@ protected function getHeaderActions(): array ->label('New Queue') ->form([ TextInput::make('command') - ->rules(CreateQueue::rules($this->server)['command']) + ->rules(CreateQueue::rules($this->site)['command']) ->helperText('Example: php /home/vito/your-site/artisan queue:work'), Select::make('user') - ->rules(fn (callable $get) => CreateQueue::rules($this->server)['user']) + ->rules(fn (callable $get) => CreateQueue::rules($this->site)['user']) ->options([ - 'vito' => $this->server->ssh_user, 'root' => 'root', + $this->site->user => $this->site->user, ]), TextInput::make('numprocs') ->default(1) - ->rules(CreateQueue::rules($this->server)['numprocs']) + ->rules(CreateQueue::rules($this->site)['numprocs']) ->helperText('Number of processes'), Grid::make() ->schema([ diff --git a/app/Web/Pages/Servers/Sites/Widgets/SiteDetails.php b/app/Web/Pages/Servers/Sites/Widgets/SiteDetails.php index 41ae30d..d9adc8b 100644 --- a/app/Web/Pages/Servers/Sites/Widgets/SiteDetails.php +++ b/app/Web/Pages/Servers/Sites/Widgets/SiteDetails.php @@ -50,6 +50,9 @@ public function infolist(Infolist $infolist): Infolist ->inlineLabel() ->hintIcon('heroicon-o-information-circle') ->hintIconTooltip('Site unique identifier to use in the API'), + TextEntry::make('user') + ->label('Site User') + ->inlineLabel(), TextEntry::make('created_at') ->label('Created At') ->formatStateUsing(fn ($record) => $record->created_at_by_timezone) diff --git a/database/factories/SiteFactory.php b/database/factories/SiteFactory.php index d3a79ff..59df0b8 100644 --- a/database/factories/SiteFactory.php +++ b/database/factories/SiteFactory.php @@ -21,6 +21,7 @@ public function definition(): array 'progress' => '100', 'php_version' => '8.2', 'branch' => 'main', + 'user' => 'vito', ]; } } diff --git a/database/migrations/2025_01_12_173141_add_user_to_sites.php b/database/migrations/2025_01_12_173141_add_user_to_sites.php new file mode 100644 index 0000000..4903407 --- /dev/null +++ b/database/migrations/2025_01_12_173141_add_user_to_sites.php @@ -0,0 +1,28 @@ +string('user')->default(config('core.ssh_user')); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('sites', function (Blueprint $table) { + $table->dropColumn('user'); + }); + } +}; diff --git a/tests/Feature/API/SitesTest.php b/tests/Feature/API/SitesTest.php index 2a38f26..2439aa9 100644 --- a/tests/Feature/API/SitesTest.php +++ b/tests/Feature/API/SitesTest.php @@ -43,6 +43,8 @@ public function test_create_site(array $inputs): void ->assertJsonFragment([ 'domain' => $inputs['domain'], 'aliases' => $inputs['aliases'] ?? [], + 'user' => $inputs['user'] ?? $this->server->getSshUser(), + 'path' => '/home/'.($inputs['user'] ?? $this->server->getSshUser()).'/'.$inputs['domain'], ]); } diff --git a/tests/Feature/CronjobTest.php b/tests/Feature/CronjobTest.php index f49a257..be9925f 100644 --- a/tests/Feature/CronjobTest.php +++ b/tests/Feature/CronjobTest.php @@ -5,6 +5,8 @@ use App\Enums\CronjobStatus; use App\Facades\SSH; use App\Models\CronJob; +use App\Models\Server; +use App\Models\Site; use App\Web\Pages\Servers\CronJobs\Index; use App\Web\Pages\Servers\CronJobs\Widgets\CronJobsList; use Illuminate\Foundation\Testing\RefreshDatabase; @@ -83,6 +85,79 @@ public function test_create_cronjob() SSH::assertExecutedContains('sudo -u vito crontab -l'); } + public function test_create_cronjob_for_isolated_user(): void + { + SSH::fake(); + $this->actingAs($this->user); + + $this->site->user = 'example'; + $this->site->save(); + + Livewire::test(Index::class, [ + 'server' => $this->server, + ]) + ->callAction('create', [ + 'command' => 'ls -la', + 'user' => 'example', + 'frequency' => '* * * * *', + ]) + ->assertSuccessful(); + + $this->assertDatabaseHas('cron_jobs', [ + 'server_id' => $this->server->id, + 'user' => 'example', + ]); + + SSH::assertExecutedContains("echo '* * * * * ls -la' | sudo -u example crontab -"); + SSH::assertExecutedContains('sudo -u example crontab -l'); + } + + public function test_cannot_create_cronjob_for_non_existing_user(): void + { + SSH::fake(); + $this->actingAs($this->user); + + Livewire::test(Index::class, [ + 'server' => $this->server, + ]) + ->callAction('create', [ + 'command' => 'ls -la', + 'user' => 'example', + 'frequency' => '* * * * *', + ]) + ->assertHasActionErrors(); + + $this->assertDatabaseMissing('cron_jobs', [ + 'server_id' => $this->server->id, + 'user' => 'example', + ]); + } + + public function test_cannot_create_cronjob_for_user_on_another_server(): void + { + SSH::fake(); + $this->actingAs($this->user); + + Site::factory()->create([ + 'server_id' => Server::factory()->create(['user_id' => 1])->id, + 'user' => 'example', + ]); + + Livewire::test(Index::class, [ + 'server' => $this->server, + ]) + ->callAction('create', [ + 'command' => 'ls -la', + 'user' => 'example', + 'frequency' => '* * * * *', + ]) + ->assertHasActionErrors(); + + $this->assertDatabaseMissing('cron_jobs', [ + 'user' => 'example', + ]); + } + public function test_create_custom_cronjob() { SSH::fake(); diff --git a/tests/Feature/QueuesTest.php b/tests/Feature/QueuesTest.php index 78c61e7..e42bb37 100644 --- a/tests/Feature/QueuesTest.php +++ b/tests/Feature/QueuesTest.php @@ -5,6 +5,7 @@ use App\Enums\QueueStatus; use App\Facades\SSH; use App\Models\Queue; +use App\Models\Site; use App\Web\Pages\Servers\Sites\Pages\Queues\Index; use App\Web\Pages\Servers\Sites\Pages\Queues\Widgets\QueuesList; use Illuminate\Foundation\Testing\RefreshDatabase; @@ -88,6 +89,97 @@ public function test_create_queue() ]); } + public function test_create_queue_as_isolated_user(): void + { + SSH::fake(); + + $this->actingAs($this->user); + + $this->site->user = 'example'; + $this->site->save(); + + Livewire::test(Index::class, [ + 'server' => $this->server, + 'site' => $this->site, + ]) + ->callAction('create', [ + 'command' => 'php artisan queue:work', + 'user' => 'example', + 'auto_start' => 1, + 'auto_restart' => 1, + 'numprocs' => 1, + ]) + ->assertSuccessful(); + + $this->assertDatabaseHas('queues', [ + 'server_id' => $this->server->id, + 'site_id' => $this->site->id, + 'command' => 'php artisan queue:work', + 'user' => 'example', + 'auto_start' => 1, + 'auto_restart' => 1, + 'numprocs' => 1, + 'status' => QueueStatus::RUNNING, + ]); + } + + public function test_cannot_create_queue_as_invalid_user(): void + { + SSH::fake(); + + $this->actingAs($this->user); + + Livewire::test(Index::class, [ + 'server' => $this->server, + 'site' => $this->site, + ]) + ->callAction('create', [ + 'command' => 'php artisan queue:work', + 'user' => 'example', + 'auto_start' => 1, + 'auto_restart' => 1, + 'numprocs' => 1, + ]) + ->assertHasActionErrors(); + + $this->assertDatabaseMissing('queues', [ + 'server_id' => $this->server->id, + 'site_id' => $this->site->id, + 'user' => 'example', + ]); + } + + public function test_cannot_create_queue_on_another_sites_user(): void + { + SSH::fake(); + + $this->actingAs($this->user); + + Site::factory()->create([ + 'server_id' => $this->server->id, + 'user' => 'example', + ]); + + Livewire::test(Index::class, [ + 'server' => $this->server, + 'site' => $this->site, + ]) + ->callAction('create', [ + 'command' => 'php artisan queue:work', + 'user' => 'example', + 'auto_start' => 1, + 'auto_restart' => 1, + 'numprocs' => 1, + ]) + ->assertHasActionErrors(); + + $this->assertDatabaseMissing('queues', [ + 'server_id' => $this->server->id, + 'site_id' => $this->site->id, + 'user' => 'example', + ]); + } + public function test_start_queue(): void { SSH::fake(); diff --git a/tests/Feature/ScriptTest.php b/tests/Feature/ScriptTest.php index 3db50b6..c336ad4 100644 --- a/tests/Feature/ScriptTest.php +++ b/tests/Feature/ScriptTest.php @@ -6,6 +6,8 @@ use App\Facades\SSH; use App\Models\Script; use App\Models\ScriptExecution; +use App\Models\Server; +use App\Models\Site; use App\Web\Pages\Scripts\Executions; use App\Web\Pages\Scripts\Index; use App\Web\Pages\Scripts\Widgets\ScriptExecutionsList; @@ -118,6 +120,7 @@ public function test_execute_script_and_view_log(): void $this->assertDatabaseHas('script_executions', [ 'script_id' => $script->id, 'status' => ScriptExecutionStatus::COMPLETED, + 'user' => 'root', ]); $this->assertDatabaseHas('server_logs', [ @@ -133,6 +136,88 @@ public function test_execute_script_and_view_log(): void ->assertSuccessful(); } + public function test_execute_script_as_isolated_user(): void + { + SSH::fake('script output'); + + $this->actingAs($this->user); + + $script = Script::factory()->create([ + 'user_id' => $this->user->id, + ]); + + Site::factory()->create([ + 'server_id' => $this->server->id, + 'user' => 'example', + ]); + + Livewire::test(Executions::class, [ + 'script' => $script, + ]) + ->callAction('execute', [ + 'server' => $this->server->id, + 'user' => 'example', + ]) + ->assertSuccessful(); + + $this->assertDatabaseHas('script_executions', [ + 'script_id' => $script->id, + 'status' => ScriptExecutionStatus::COMPLETED, + 'user' => 'example', + ]); + } + + public function test_cannot_execute_script_as_non_existing_user(): void + { + $this->actingAs($this->user); + + $script = Script::factory()->create([ + 'user_id' => $this->user->id, + ]); + + Livewire::test(Executions::class, [ + 'script' => $script, + ]) + ->callAction('execute', [ + 'server' => $this->server->id, + 'user' => 'example', + ]) + ->assertHasActionErrors(); + + $this->assertDatabaseMissing('script_executions', [ + 'script_id' => $script->id, + 'user' => 'example', + ]); + } + + public function test_cannot_execute_script_as_user_not_on_server(): void + { + $this->actingAs($this->user); + + $script = Script::factory()->create([ + 'user_id' => $this->user->id, + ]); + + Site::factory()->create([ + 'server_id' => Server::factory()->create(['user_id' => 1])->id, + 'user' => 'example', + ]); + + Livewire::test(Executions::class, [ + 'script' => $script, + ]) + ->callAction('execute', [ + 'server' => $this->server->id, + 'user' => 'example', + ]) + ->assertHasActionErrors(); + + $this->assertDatabaseMissing('script_executions', [ + 'script_id' => $script->id, + 'user' => 'example', + ]); + } + public function test_see_executions(): void { $this->actingAs($this->user); diff --git a/tests/Feature/SitesTest.php b/tests/Feature/SitesTest.php index a6cec3e..43a15fe 100644 --- a/tests/Feature/SitesTest.php +++ b/tests/Feature/SitesTest.php @@ -48,13 +48,31 @@ public function test_create_site(array $inputs): void ->assertHasNoActionErrors() ->assertSuccessful(); + $expectedUser = empty($inputs['user']) ? $this->server->getSshUser() : $inputs['user']; $this->assertDatabaseHas('sites', [ 'domain' => $inputs['domain'], 'aliases' => json_encode($inputs['aliases'] ?? []), 'status' => SiteStatus::READY, + 'user' => $expectedUser, + 'path' => '/home/'.$expectedUser.'/'.$inputs['domain'], ]); } + /** + * @dataProvider failure_create_data + */ + public function test_isolated_user_failure(array $inputs): void + { + SSH::fake(); + $this->actingAs($this->user); + + Livewire::test(Index::class, [ + 'server' => $this->server, + ]) + ->callAction('create', $inputs) + ->assertHasActionErrors(); + } + /** * @dataProvider create_failure_data */ @@ -247,6 +265,62 @@ public function test_see_logs(): void ->assertSee('Logs'); } + public static function failure_create_data(): array + { + return [ + [ + [ + 'type' => SiteType::PHP_BLANK, + 'domain' => 'example.com', + 'aliases' => ['www.example.com'], + 'php_version' => '8.2', + 'web_directory' => 'public', + 'user' => 'a', + ], + ], + [ + [ + 'type' => SiteType::PHP_BLANK, + 'domain' => 'example.com', + 'aliases' => ['www.example.com'], + 'php_version' => '8.2', + 'web_directory' => 'public', + 'user' => 'root', + ], + ], + [ + [ + 'type' => SiteType::PHP_BLANK, + 'domain' => 'example.com', + 'aliases' => ['www.example.com'], + 'php_version' => '8.2', + 'web_directory' => 'public', + 'user' => 'vito', + ], + ], + [ + [ + 'type' => SiteType::PHP_BLANK, + 'domain' => 'example.com', + 'aliases' => ['www.example.com'], + 'php_version' => '8.2', + 'web_directory' => 'public', + 'user' => '123', + ], + ], + [ + [ + 'type' => SiteType::PHP_BLANK, + 'domain' => 'example.com', + 'aliases' => ['www.example.com'], + 'php_version' => '8.2', + 'web_directory' => 'public', + 'user' => 'qwertyuiopasdfghjklzxcvbnmqwertyu', + ], + ], + ]; + } + public static function create_data(): array { return [ @@ -262,6 +336,19 @@ public static function create_data(): array 'composer' => true, ], ], + [ + [ + 'type' => SiteType::LARAVEL, + 'domain' => 'example.com', + 'aliases' => ['www.example.com', 'www2.example.com'], + 'php_version' => '8.2', + 'web_directory' => 'public', + 'repository' => 'test/test', + 'branch' => 'main', + 'composer' => true, + 'user' => 'example', + ], + ], [ [ 'type' => SiteType::WORDPRESS, @@ -277,6 +364,22 @@ public static function create_data(): array 'database_password' => 'password', ], ], + [ + [ + 'type' => SiteType::WORDPRESS, + 'domain' => 'example.com', + 'aliases' => ['www.example.com'], + 'php_version' => '8.2', + 'title' => 'Example', + 'username' => 'example', + 'email' => 'email@example.com', + 'password' => 'password', + 'database' => 'example', + 'database_user' => 'example', + 'database_password' => 'password', + 'user' => 'example', + ], + ], [ [ 'type' => SiteType::PHP_BLANK, @@ -286,6 +389,16 @@ public static function create_data(): array 'web_directory' => 'public', ], ], + [ + [ + 'type' => SiteType::PHP_BLANK, + 'domain' => 'example.com', + 'aliases' => ['www.example.com'], + 'php_version' => '8.2', + 'web_directory' => 'public', + 'user' => 'example', + ], + ], [ [ 'type' => SiteType::PHPMYADMIN, @@ -295,6 +408,16 @@ public static function create_data(): array 'version' => '5.1.2', ], ], + [ + [ + 'type' => SiteType::PHPMYADMIN, + 'domain' => 'example.com', + 'aliases' => ['www.example.com'], + 'php_version' => '8.2', + 'version' => '5.1.2', + 'user' => 'example', + ], + ], ]; }