Compare commits

...

19 Commits
0.1.1 ... 0.3.0

Author SHA1 Message Date
f51d7900f0 build assets 2023-10-10 17:18:54 +02:00
4bd4b34d24 phpmyadmin (#66)
* add phpmyadmin

* add tests
2023-10-10 17:15:58 +02:00
7c5505be16 update laravel (#65)
* update laravel

* fix tests
2023-10-03 13:23:30 +02:00
7249cf9ed6 include unit tests in the workflows 2023-09-30 16:23:16 +02:00
8282d39722 add timezone edit to profile (#63) 2023-09-30 16:15:09 +02:00
38e23a1ceb fix consecutive deployment issue (#62)
* fix consecutive deployment issue

* fix styles
2023-09-29 23:08:56 +02:00
1e1204fe40 Gitlab Self-managed Support (#56)
* Update UI to ask for URL for Gitlab as provider.

* Create state for each source control in factory.

* Save url for SourceControl when given.

* Make Gitlab dynamically use correct URL (custom or default)

* Update SourceControlsTest to check for custom url when given.

* Style fixes.

---------

Co-authored-by: Saeed Vaziry <61919774+saeedvaziry@users.noreply.github.com>
2023-09-25 10:11:01 +02:00
7d98986f52 added FTP support to storage providers (#58)
* added FTP support to storage providers

* build and code style fix
2023-09-24 12:50:01 +02:00
2c81e324f6 Update .env.example 2023-09-23 23:18:17 +02:00
422da431ec Update .env.example 2023-09-23 23:17:37 +02:00
6c27215ea1 fix AWS image ids (#57)
* fix AWS image ids

* fix code style
2023-09-23 12:44:40 +02:00
fb840204ad login page ui alignment 2023-09-18 19:41:25 +02:00
e07e197dd9 Added Dark mode & Light mode toggle button (#50)
* wip

* Added Show Password checkbox & Added Toggle Dark Mode & Light Mode button on Dropdown menu

* resloved the PR comments

* Fixed the reload issues on theme change & rewammped  the UI of login page as per PR comments

---------

Co-authored-by: saravana sai <saravanasao002@gmail.com>
Co-authored-by: Saeed Vaziry <61919774+saeedvaziry@users.noreply.github.com>
2023-09-18 19:39:26 +02:00
3a2dba4ad3 upgrade workflows os to ubuntu 22.04 (#52) 2023-09-17 18:46:34 +02:00
b990f9ce32 remove pusher 2023-09-17 18:37:59 +02:00
a6727ff459 cleanup .env files 2023-09-17 18:22:17 +02:00
170535760f change app to vito on docker-compose to make sail works 2023-09-17 18:17:27 +02:00
648529c3bd add sails env 2023-09-17 18:10:57 +02:00
84192b7cb7 fix default key names 2023-09-17 11:08:47 +02:00
67 changed files with 1225 additions and 498 deletions

View File

@ -2,7 +2,7 @@ APP_NAME=Vito
APP_ENV=local APP_ENV=local
APP_KEY= APP_KEY=
APP_DEBUG=true APP_DEBUG=true
APP_URL=http://localhost:2080 APP_URL=http://vito.test
LOG_CHANNEL=stack LOG_CHANNEL=stack
LOG_LEVEL=debug LOG_LEVEL=debug
@ -15,14 +15,12 @@ DB_USERNAME=root
DB_PASSWORD= DB_PASSWORD=
BROADCAST_DRIVER=null BROADCAST_DRIVER=null
CACHE_DRIVER=redis CACHE_DRIVER=file
FILESYSTEM_DRIVER=local FILESYSTEM_DRIVER=local
QUEUE_CONNECTION=sync QUEUE_CONNECTION=default
SESSION_DRIVER=database SESSION_DRIVER=database
SESSION_LIFETIME=120 SESSION_LIFETIME=120
MEMCACHED_HOST=127.0.0.1
REDIS_HOST=127.0.0.1 REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null REDIS_PASSWORD=null
REDIS_PORT=6379 REDIS_PORT=6379
@ -36,31 +34,5 @@ MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS=null MAIL_FROM_ADDRESS=null
MAIL_FROM_NAME="${APP_NAME}" MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID= SSH_PUBLIC_KEY_NAME=ssh-public.key
AWS_SECRET_ACCESS_KEY= SSH_PRIVATE_KEY_NAME=ssh-private.pem
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
PUSHER_APP_CLUSTER=mt1
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
FORWARD_REDIS_PORT=2060
FORWARD_DB_PORT=2070
APP_PORT=2080
HMR_PORT=2090
SENTRY_LARAVEL_DSN=
APP_SERVICE=vito
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
ABLY_KEY=
ABLY_PUBLIC_KEY=

View File

@ -14,7 +14,7 @@ DB_DATABASE=
DB_USERNAME= DB_USERNAME=
DB_PASSWORD= DB_PASSWORD=
BROADCAST_DRIVER=pusher BROADCAST_DRIVER=null
CACHE_DRIVER=file CACHE_DRIVER=file
FILESYSTEM_DRIVER=local FILESYSTEM_DRIVER=local
QUEUE_CONNECTION=default QUEUE_CONNECTION=default
@ -34,31 +34,5 @@ MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS=null MAIL_FROM_ADDRESS=null
MAIL_FROM_NAME="${APP_NAME}" MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
PUSHER_APP_ID=app-id
PUSHER_APP_KEY=app-key
PUSHER_APP_SECRET=app-secret
PUSHER_HOST=soketi
PUSHER_PORT=6001
PUSHER_SCHEME=http
PUSHER_APP_CLUSTER=mt1
VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
VITE_PUSHER_HOST="${PUSHER_HOST}"
VITE_PUSHER_PORT="${PUSHER_PORT}"
VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
SSH_PUBLIC_KEY_NAME=ssh-public.key SSH_PUBLIC_KEY_NAME=ssh-public.key
SSH_PRIVATE_KEY_NAME=ssh-private.pem SSH_PRIVATE_KEY_NAME=ssh-private.pem
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
ABLY_KEY=
ABLY_PUBLIC_KEY=

45
.env.sail Normal file
View File

@ -0,0 +1,45 @@
APP_NAME=Vito
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://vito.test
LOG_CHANNEL=stack
LOG_LEVEL=debug
DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=vito
DB_USERNAME=sail
DB_PASSWORD=password
BROADCAST_DRIVER=null
CACHE_DRIVER=redis
FILESYSTEM_DRIVER=local
QUEUE_CONNECTION=default
SESSION_DRIVER=database
SESSION_LIFETIME=120
REDIS_HOST=redis
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=smtp
MAIL_HOST=mailhog
MAIL_PORT=1025
MAIL_USERNAME=null
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
APP_SERVICE=vito
FORWARD_REDIS_PORT=2060
FORWARD_DB_PORT=2070
APP_PORT=2080
HMR_PORT=2090

View File

@ -14,7 +14,7 @@ DB_DATABASE=vito_test
DB_USERNAME=root DB_USERNAME=root
DB_PASSWORD= DB_PASSWORD=
BROADCAST_DRIVER=pusher BROADCAST_DRIVER=null
CACHE_DRIVER=array CACHE_DRIVER=array
FILESYSTEM_DRIVER=local FILESYSTEM_DRIVER=local
QUEUE_CONNECTION=database QUEUE_CONNECTION=database
@ -23,19 +23,5 @@ SESSION_LIFETIME=120
MAIL_MAILER=array MAIL_MAILER=array
PUSHER_APP_ID=app-id
PUSHER_APP_KEY=app-key
PUSHER_APP_SECRET=app-secret
PUSHER_HOST=soketi
PUSHER_PORT=6001
PUSHER_SCHEME=http
PUSHER_APP_CLUSTER=mt1
VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
VITE_PUSHER_HOST="${PUSHER_HOST}"
VITE_PUSHER_PORT="${PUSHER_PORT}"
VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
SSH_PUBLIC_KEY_NAME=ssh-public.key SSH_PUBLIC_KEY_NAME=ssh-public.key
SSH_PRIVATE_KEY_NAME=ssh-private.pem SSH_PRIVATE_KEY_NAME=ssh-private.pem

View File

@ -8,7 +8,7 @@ on:
jobs: jobs:
code-style: code-style:
runs-on: ubuntu-20.04 runs-on: ubuntu-22.04
strategy: strategy:
fail-fast: true fail-fast: true

View File

@ -8,7 +8,7 @@ on:
jobs: jobs:
tests: tests:
runs-on: ubuntu-20.04 runs-on: ubuntu-22.04
services: services:
mysql: mysql:

View File

@ -1,7 +1,8 @@
<?php <?php
namespace App\Actions\Database; namespace App\Actions\Service;
use App\Enums\ServiceStatus;
use App\Models\Server; use App\Models\Server;
use App\Models\Service; use App\Models\Service;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;
@ -18,23 +19,21 @@ public function install(Server $server, array $input): Service
$phpMyAdmin = $server->defaultService('phpmyadmin'); $phpMyAdmin = $server->defaultService('phpmyadmin');
if ($phpMyAdmin) { if ($phpMyAdmin) {
if ($phpMyAdmin->status === 'ready') {
throw ValidationException::withMessages([ throw ValidationException::withMessages([
'install' => __('Already installed'), 'allowed_ip' => __('Already installed'),
])->errorBag('installPHPMyAdmin'); ]);
}
$phpMyAdmin->delete();
} }
$phpMyAdmin = new Service([ $phpMyAdmin = new Service([
'server_id' => $server->id, 'server_id' => $server->id,
'type' => 'phpmyadmin', 'type' => 'phpmyadmin',
'type_data' => [ 'type_data' => [
'allowed_ip' => $input['allowed_ip'], 'allowed_ip' => $input['allowed_ip'],
'port' => $input['port'],
'php' => $server->defaultService('php')->version, 'php' => $server->defaultService('php')->version,
], ],
'name' => 'phpmyadmin', 'name' => 'phpmyadmin',
'version' => '5.1.2', 'version' => '5.1.2',
'status' => 'installing', 'status' => ServiceStatus::INSTALLING,
'is_default' => 1, 'is_default' => 1,
]); ]);
$phpMyAdmin->save(); $phpMyAdmin->save();
@ -50,6 +49,12 @@ private function validate(array $input): void
{ {
Validator::make($input, [ Validator::make($input, [
'allowed_ip' => 'required', 'allowed_ip' => 'required',
])->validateWithBag('installPHPMyAdmin'); 'port' => [
'required',
'numeric',
'min:1',
'max:65535',
],
])->validate();
} }
} }

View File

@ -3,6 +3,7 @@
namespace App\Actions\SourceControl; namespace App\Actions\SourceControl;
use App\Models\SourceControl; use App\Models\SourceControl;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule; use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException; use Illuminate\Validation\ValidationException;
@ -16,6 +17,7 @@ public function connect(array $input): void
'provider' => $input['provider'], 'provider' => $input['provider'],
'profile' => $input['name'], 'profile' => $input['name'],
'access_token' => $input['token'], 'access_token' => $input['token'],
'url' => Arr::has($input, 'url') ? $input['url'] : null,
]); ]);
if (! $sourceControl->provider()->connect()) { if (! $sourceControl->provider()->connect()) {
@ -44,6 +46,11 @@ private function validate(array $input): void
'token' => [ 'token' => [
'required', 'required',
], ],
'url' => [
'nullable',
'url:http,https',
'ends_with:/',
],
]; ];
Validator::make($input, $rules)->validate(); Validator::make($input, $rules)->validate();
} }

View File

@ -21,13 +21,15 @@ public function create(User $user, array $input): void
'user_id' => $user->id, 'user_id' => $user->id,
'provider' => $input['provider'], 'provider' => $input['provider'],
'profile' => $input['name'], 'profile' => $input['name'],
'credentials' => [
'token' => $input['token'],
],
]); ]);
$this->validateProvider($input, $storageProvider->provider()->validationRules());
$storageProvider->credentials = $storageProvider->provider()->credentialData($input);
if (! $storageProvider->provider()->connect()) { if (! $storageProvider->provider()->connect()) {
throw ValidationException::withMessages([ throw ValidationException::withMessages([
'token' => __("Couldn't connect to the provider"), 'provider' => __("Couldn't connect to the provider"),
]); ]);
} }
$storageProvider->save(); $storageProvider->save();
@ -44,9 +46,11 @@ private function validate(User $user, array $input): void
'required', 'required',
Rule::unique('storage_providers', 'profile')->where('user_id', $user->id), Rule::unique('storage_providers', 'profile')->where('user_id', $user->id),
], ],
'token' => [
'required',
],
])->validate(); ])->validate();
} }
private function validateProvider(array $input, array $rules): void
{
Validator::make($input, $rules)->validate();
}
} }

View File

@ -19,6 +19,10 @@ public function update(User $user, array $input): void
Validator::make($input, [ Validator::make($input, [
'name' => ['required', 'string', 'max:255'], 'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'email', 'max:255', Rule::unique('users')->ignore($user->id)], 'email' => ['required', 'email', 'max:255', Rule::unique('users')->ignore($user->id)],
'timezone' => [
'required',
Rule::in(timezone_identifiers_list()),
],
])->validateWithBag('updateProfileInformation'); ])->validateWithBag('updateProfileInformation');
if ($input['email'] !== $user->email) { if ($input['email'] !== $user->email) {
@ -27,6 +31,7 @@ public function update(User $user, array $input): void
$user->forceFill([ $user->forceFill([
'name' => $input['name'], 'name' => $input['name'],
'email' => $input['email'], 'email' => $input['email'],
'timezone' => $input['timezone'],
])->save(); ])->save();
} }
} }
@ -39,6 +44,7 @@ protected function updateVerifiedUser(User $user, array $input): void
$user->forceFill([ $user->forceFill([
'name' => $input['name'], 'name' => $input['name'],
'email' => $input['email'], 'email' => $input['email'],
'timezone' => $input['timezone'],
])->save(); ])->save();
} }
} }

View File

@ -6,6 +6,10 @@
interface StorageProvider interface StorageProvider
{ {
public function validationRules(): array;
public function credentialData(array $input): array;
public function connect(): bool; public function connect(): bool;
public function upload(Server $server, string $src, string $dest): array; public function upload(Server $server, string $src, string $dest): array;

View File

@ -6,7 +6,7 @@
final class StorageProvider extends Enum final class StorageProvider extends Enum
{ {
const GOOGLE = 'google';
const DROPBOX = 'dropbox'; const DROPBOX = 'dropbox';
const FTP = 'ftp';
} }

View File

@ -15,10 +15,13 @@ class UpdateProfileInformation extends Component
public string $email; public string $email;
public string $timezone;
public function mount(): void public function mount(): void
{ {
$this->name = auth()->user()->name; $this->name = auth()->user()->name;
$this->email = auth()->user()->email; $this->email = auth()->user()->email;
$this->timezone = auth()->user()->timezone;
} }
/** /**

View File

@ -0,0 +1,31 @@
<?php
namespace App\Http\Livewire\Services;
use App\Actions\Service\InstallPHPMyAdmin as InstallPHPMyAdminAction;
use App\Models\Server;
use Illuminate\Contracts\View\View;
use Livewire\Component;
class InstallPHPMyAdmin extends Component
{
public Server $server;
public string $allowed_ip;
public string $port = '5433';
public function install(): void
{
app(InstallPHPMyAdminAction::class)->install($this->server, $this->all());
$this->dispatchBrowserEvent('started', true);
$this->emitTo(ServicesList::class, '$refresh');
}
public function render(): View
{
return view('livewire.services.install-phpmyadmin');
}
}

View File

@ -3,6 +3,7 @@
namespace App\Http\Livewire\Services; namespace App\Http\Livewire\Services;
use App\Models\Server; use App\Models\Server;
use App\Models\Service;
use App\Traits\RefreshComponentOnBroadcast; use App\Traits\RefreshComponentOnBroadcast;
use Illuminate\Contracts\View\View; use Illuminate\Contracts\View\View;
use Livewire\Component; use Livewire\Component;
@ -15,6 +16,7 @@ class ServicesList extends Component
public function stop(int $id): void public function stop(int $id): void
{ {
/** @var Service $service */
$service = $this->server->services()->where('id', $id)->firstOrFail(); $service = $this->server->services()->where('id', $id)->firstOrFail();
$service->stop(); $service->stop();
@ -24,6 +26,7 @@ public function stop(int $id): void
public function start(int $id): void public function start(int $id): void
{ {
/** @var Service $service */
$service = $this->server->services()->where('id', $id)->firstOrFail(); $service = $this->server->services()->where('id', $id)->firstOrFail();
$service->start(); $service->start();
@ -33,6 +36,7 @@ public function start(int $id): void
public function restart(int $id): void public function restart(int $id): void
{ {
/** @var Service $service */
$service = $this->server->services()->where('id', $id)->firstOrFail(); $service = $this->server->services()->where('id', $id)->firstOrFail();
$service->restart(); $service->restart();
@ -40,6 +44,16 @@ public function restart(int $id): void
$this->refreshComponent([]); $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 public function render(): View
{ {
return view('livewire.services.services-list', [ return view('livewire.services.services-list', [

View File

@ -14,6 +14,20 @@ class ConnectProvider extends Component
public string $token; public string $token;
public string $host;
public string $port;
public string $path = '/';
public string $username;
public string $password;
public int $ssl = 1;
public int $passive = 0;
public function connect(): void public function connect(): void
{ {
app(CreateStorageProvider::class)->create(auth()->user(), $this->all()); app(CreateStorageProvider::class)->create(auth()->user(), $this->all());

View File

@ -3,6 +3,7 @@
namespace App\Jobs\Installation; namespace App\Jobs\Installation;
use App\Actions\FirewallRule\CreateRule; use App\Actions\FirewallRule\CreateRule;
use App\Enums\ServiceStatus;
use App\Jobs\Job; use App\Jobs\Job;
use App\Models\FirewallRule; use App\Models\FirewallRule;
use App\Models\Service; use App\Models\Service;
@ -32,6 +33,9 @@ public function handle(): void
$this->downloadSource(); $this->downloadSource();
$this->setUpVHost(); $this->setUpVHost();
$this->restartPHP(); $this->restartPHP();
$this->service->update([
'status' => ServiceStatus::READY,
]);
} }
/** /**
@ -41,7 +45,7 @@ private function setUpFirewall(): void
{ {
$this->firewallRule = FirewallRule::query() $this->firewallRule = FirewallRule::query()
->where('server_id', $this->service->server_id) ->where('server_id', $this->service->server_id)
->where('port', '54331') ->where('port', $this->service->type_data['port'])
->first(); ->first();
if ($this->firewallRule) { if ($this->firewallRule) {
$this->firewallRule->source = $this->service->type_data['allowed_ip']; $this->firewallRule->source = $this->service->type_data['allowed_ip'];
@ -52,7 +56,7 @@ private function setUpFirewall(): void
[ [
'type' => 'allow', 'type' => 'allow',
'protocol' => 'tcp', 'protocol' => 'tcp',
'port' => '54331', 'port' => $this->service->type_data['port'],
'source' => $this->service->type_data['allowed_ip'], 'source' => $this->service->type_data['allowed_ip'],
'mask' => '0', 'mask' => '0',
] ]
@ -78,6 +82,7 @@ private function setUpVHost(): void
{ {
$vhost = File::get(resource_path('commands/webserver/nginx/phpmyadmin-vhost.conf')); $vhost = File::get(resource_path('commands/webserver/nginx/phpmyadmin-vhost.conf'));
$vhost = Str::replace('__php_version__', $this->service->server->defaultService('php')->version, $vhost); $vhost = Str::replace('__php_version__', $this->service->server->defaultService('php')->version, $vhost);
$vhost = Str::replace('__port__', $this->service->type_data['port'], $vhost);
$this->service->server->ssh()->exec( $this->service->server->ssh()->exec(
new CreateNginxPHPMyAdminVHostCommand($vhost), new CreateNginxPHPMyAdminVHostCommand($vhost),
'create-phpmyadmin-vhost' 'create-phpmyadmin-vhost'
@ -98,6 +103,9 @@ private function restartPHP(): void
public function failed(Throwable $throwable): Throwable public function failed(Throwable $throwable): Throwable
{ {
$this->firewallRule?->removeFromServer(); $this->firewallRule?->removeFromServer();
$this->service->update([
'status' => ServiceStatus::INSTALLATION_FAILED,
]);
throw $throwable; throw $throwable;
} }
} }

View File

@ -37,7 +37,7 @@ private function removeFirewallRule(): void
/** @var ?FirewallRule $rule */ /** @var ?FirewallRule $rule */
$rule = FirewallRule::query() $rule = FirewallRule::query()
->where('server_id', $this->service->server_id) ->where('server_id', $this->service->server_id)
->where('port', '54331') ->where('port', $this->service->type_data['port'])
->first(); ->first();
$rule?->removeFromServer(); $rule?->removeFromServer();
} }

View File

@ -3,13 +3,12 @@
namespace App\Jobs; namespace App\Jobs;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
abstract class Job implements ShouldQueue, ShouldBeUnique abstract class Job implements ShouldQueue
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
} }

View File

@ -4,7 +4,6 @@
use App\Enums\DeploymentStatus; use App\Enums\DeploymentStatus;
use App\Events\Broadcast; use App\Events\Broadcast;
use App\Helpers\SSH;
use App\Jobs\Job; use App\Jobs\Job;
use App\Models\Deployment; use App\Models\Deployment;
use App\SSHCommands\System\RunScriptCommand; use App\SSHCommands\System\RunScriptCommand;
@ -18,13 +17,11 @@ class Deploy extends Job
protected string $script; protected string $script;
protected SSH $ssh; public function __construct(Deployment $deployment, string $path)
public function __construct(Deployment $deployment, string $path, string $script)
{ {
$this->script = $deployment->deploymentScript->content;
$this->deployment = $deployment; $this->deployment = $deployment;
$this->path = $path; $this->path = $path;
$this->script = $script;
} }
/** /**
@ -32,28 +29,31 @@ public function __construct(Deployment $deployment, string $path, string $script
*/ */
public function handle(): void public function handle(): void
{ {
$this->ssh = $this->deployment->site->server->ssh(); $ssh = $this->deployment->site->server->ssh();
$this->ssh->exec( try {
$ssh->exec(
new RunScriptCommand($this->path, $this->script), new RunScriptCommand($this->path, $this->script),
'deploy', 'deploy',
$this->deployment->site_id $this->deployment->site_id
); );
$this->deployment->status = DeploymentStatus::FINISHED; $this->deployment->status = DeploymentStatus::FINISHED;
$this->deployment->log_id = $this->ssh->log->id; $this->deployment->log_id = $ssh->log->id;
$this->deployment->save(); $this->deployment->save();
event( event(
new Broadcast('deploy-site-finished', [ new Broadcast('deploy-site-finished', [
'deployment' => $this->deployment, 'deployment' => $this->deployment,
]) ])
); );
} catch (Throwable) {
$this->deployment->log_id = $ssh->log->id;
$this->deployment->save();
$this->failed();
}
} }
public function failed(): void public function failed(): void
{ {
$this->deployment->status = DeploymentStatus::FAILED; $this->deployment->status = DeploymentStatus::FAILED;
if ($this->ssh->log) {
$this->deployment->log_id = $this->ssh->log->id;
}
$this->deployment->save(); $this->deployment->save();
event( event(
new Broadcast('deploy-site-failed', [ new Broadcast('deploy-site-failed', [

View File

@ -13,7 +13,7 @@ public function __construct()
public function handle(Broadcast $event): void public function handle(Broadcast $event): void
{ {
Cache::set('broadcast', [ Cache::put('broadcast', [
'type' => $event->type, 'type' => $event->type,
'data' => $event->data, 'data' => $event->data,
], now()->addMinutes(5)); ], now()->addMinutes(5));

View File

@ -76,7 +76,7 @@ public function getPathAttribute(): string
public function getStoragePathAttribute(): string public function getStoragePathAttribute(): string
{ {
return '/'.$this->backup->database->name.'/'.$this->name.'.zip'; return '/'.$this->name.'.zip';
} }
public function restore(Database $database): void public function restore(Database $database): void

View File

@ -76,7 +76,7 @@ public function uninstaller(): mixed
return new $uninstaller($this); return new $uninstaller($this);
} }
public function getUnitAttribute($value): string public function getUnitAttribute($value): ?string
{ {
if ($value) { if ($value) {
return $value; return $value;

View File

@ -266,13 +266,7 @@ public function deploy(): Deployment
} }
$deployment->save(); $deployment->save();
dispatch( dispatch(new Deploy($deployment, $this->path))->onConnection('ssh');
new Deploy(
$deployment,
$this->path,
$this->deployment_script_text
)
)->onConnection('ssh');
return $deployment; return $deployment;
} }

View File

@ -0,0 +1,40 @@
<?php
namespace App\SSHCommands\Storage;
use App\SSHCommands\Command;
use Illuminate\Support\Facades\File;
class DownloadFromFTPCommand extends Command
{
public function __construct(
protected string $src,
protected string $dest,
protected string $host,
protected string $port,
protected string $username,
protected string $password,
protected bool $ssl,
protected bool $passive,
) {
}
public function file(): string
{
return File::get(resource_path('commands/storage/download-from-ftp.sh'));
}
public function content(): string
{
return str($this->file())
->replace('__src__', $this->src)
->replace('__dest__', $this->dest)
->replace('__host__', $this->host)
->replace('__port__', $this->port)
->replace('__username__', $this->username)
->replace('__password__', $this->password)
->replace('__ssl__', $this->ssl ? 's' : '')
->replace('__passive__', $this->passive ? '--ftp-pasv' : '')
->toString();
}
}

View File

@ -0,0 +1,40 @@
<?php
namespace App\SSHCommands\Storage;
use App\SSHCommands\Command;
use Illuminate\Support\Facades\File;
class UploadToFTPCommand extends Command
{
public function __construct(
protected string $src,
protected string $dest,
protected string $host,
protected string $port,
protected string $username,
protected string $password,
protected bool $ssl,
protected bool $passive,
) {
}
public function file(): string
{
return File::get(resource_path('commands/storage/upload-to-ftp.sh'));
}
public function content(): string
{
return str($this->file())
->replace('__src__', $this->src)
->replace('__dest__', $this->dest)
->replace('__host__', $this->host)
->replace('__port__', $this->port)
->replace('__username__', $this->username)
->replace('__password__', $this->password)
->replace('__ssl__', $this->ssl ? 's' : '')
->replace('__passive__', $this->passive ? '--ftp-pasv' : '')
->toString();
}
}

View File

@ -2,6 +2,7 @@
namespace App\ServerTypes; namespace App\ServerTypes;
use App\Enums\ServerStatus;
use App\Events\Broadcast; use App\Events\Broadcast;
use App\Jobs\Installation\Initialize; use App\Jobs\Installation\Initialize;
use App\Jobs\Installation\InstallCertbot; use App\Jobs\Installation\InstallCertbot;
@ -79,7 +80,7 @@ public function install(): void
$jobs[] = function () { $jobs[] = function () {
$this->server->update([ $this->server->update([
'status' => 'ready', 'status' => ServerStatus::READY,
'progress' => 100, 'progress' => 100,
]); ]);
event( event(

View File

@ -10,12 +10,14 @@
class Gitlab extends AbstractSourceControlProvider class Gitlab extends AbstractSourceControlProvider
{ {
protected string $apiUrl = 'https://gitlab.com/api/v4'; protected string $defaultApiHost = 'https://gitlab.com/';
protected string $apiVersion = 'api/v4';
public function connect(): bool public function connect(): bool
{ {
$res = Http::withToken($this->sourceControl->access_token) $res = Http::withToken($this->sourceControl->access_token)
->get($this->apiUrl.'/projects'); ->get($this->getApiUrl().'/projects');
return $res->successful(); return $res->successful();
} }
@ -27,7 +29,7 @@ public function getRepo(string $repo = null): mixed
{ {
$repository = $repo ? urlencode($repo) : null; $repository = $repo ? urlencode($repo) : null;
$res = Http::withToken($this->sourceControl->access_token) $res = Http::withToken($this->sourceControl->access_token)
->get($this->apiUrl.'/projects/'.$repository.'/repository/commits'); ->get($this->getApiUrl().'/projects/'.$repository.'/repository/commits');
$this->handleResponseErrors($res, $repo); $this->handleResponseErrors($res, $repo);
@ -36,7 +38,9 @@ public function getRepo(string $repo = null): mixed
public function fullRepoUrl(string $repo, string $key): string public function fullRepoUrl(string $repo, string $key): string
{ {
return sprintf('git@gitlab.com-%s:%s.git', $key, $repo); $host = parse_url($this->getApiUrl())['host'];
return sprintf('git@%s-%s:%s.git', $host, $key, $repo);
} }
/** /**
@ -46,7 +50,7 @@ public function deployHook(string $repo, array $events, string $secret): array
{ {
$repository = urlencode($repo); $repository = urlencode($repo);
$response = Http::withToken($this->sourceControl->access_token)->post( $response = Http::withToken($this->sourceControl->access_token)->post(
$this->apiUrl.'/projects/'.$repository.'/hooks', $this->getApiUrl().'/projects/'.$repository.'/hooks',
[ [
'description' => 'deploy', 'description' => 'deploy',
'url' => url('/git-hooks?secret='.$secret), 'url' => url('/git-hooks?secret='.$secret),
@ -81,7 +85,7 @@ public function destroyHook(string $repo, string $hookId): void
{ {
$repository = urlencode($repo); $repository = urlencode($repo);
$response = Http::withToken($this->sourceControl->access_token)->delete( $response = Http::withToken($this->sourceControl->access_token)->delete(
$this->apiUrl.'/projects/'.$repository.'/hooks/'.$hookId $this->getApiUrl().'/projects/'.$repository.'/hooks/'.$hookId
); );
if ($response->status() != 204) { if ($response->status() != 204) {
@ -96,7 +100,7 @@ public function getLastCommit(string $repo, string $branch): ?array
{ {
$repository = urlencode($repo); $repository = urlencode($repo);
$res = Http::withToken($this->sourceControl->access_token) $res = Http::withToken($this->sourceControl->access_token)
->get($this->apiUrl.'/projects/'.$repository.'/repository/commits?ref_name='.$branch); ->get($this->getApiUrl().'/projects/'.$repository.'/repository/commits?ref_name='.$branch);
$this->handleResponseErrors($res, $repo); $this->handleResponseErrors($res, $repo);
@ -123,7 +127,7 @@ public function deployKey(string $title, string $repo, string $key): void
{ {
$repository = urlencode($repo); $repository = urlencode($repo);
$response = Http::withToken($this->sourceControl->access_token)->post( $response = Http::withToken($this->sourceControl->access_token)->post(
$this->apiUrl.'/projects/'.$repository.'/deploy_keys', $this->getApiUrl().'/projects/'.$repository.'/deploy_keys',
[ [
'title' => $title, 'title' => $title,
'key' => $key, 'key' => $key,
@ -135,4 +139,13 @@ public function deployKey(string $title, string $repo, string $key): void
throw new FailedToDeployGitKey(json_decode($response->body())->message); throw new FailedToDeployGitKey(json_decode($response->body())->message);
} }
} }
public function getApiUrl(): string
{
$host = $this->sourceControl->url === null
? $this->defaultApiHost
: $this->sourceControl->url;
return $host.$this->apiVersion;
}
} }

View File

@ -13,6 +13,20 @@ class Dropbox extends AbstractStorageProvider
{ {
protected string $apiUrl = 'https://api.dropboxapi.com/2'; protected string $apiUrl = 'https://api.dropboxapi.com/2';
public function validationRules(): array
{
return [
'token' => 'required',
];
}
public function credentialData(array $input): array
{
return [
'token' => $input['token'],
];
}
public function connect(): bool public function connect(): bool
{ {
$res = Http::withToken($this->storageProvider->credentials['token']) $res = Http::withToken($this->storageProvider->credentials['token'])

View File

@ -0,0 +1,129 @@
<?php
namespace App\StorageProviders;
use App\Models\Server;
use App\SSHCommands\Storage\DownloadFromFTPCommand;
use App\SSHCommands\Storage\UploadToFTPCommand;
use FTP\Connection;
use Throwable;
class FTP extends AbstractStorageProvider
{
public function validationRules(): array
{
return [
'host' => 'required',
'port' => 'required|numeric',
'path' => 'required',
'username' => 'required',
'password' => 'required',
'ssl' => 'required',
'passive' => 'required',
];
}
public function credentialData(array $input): array
{
return [
'host' => $input['host'],
'port' => $input['port'],
'path' => $input['path'],
'username' => $input['username'],
'password' => $input['password'],
'ssl' => (bool) $input['ssl'],
'passive' => (bool) $input['passive'],
];
}
public function connect(): bool
{
$connection = $this->connection();
$isConnected = $connection && $this->login($connection);
if ($isConnected) {
ftp_close($connection);
}
return $isConnected;
}
/**
* @throws Throwable
*/
public function upload(Server $server, string $src, string $dest): array
{
$server->ssh()->exec(
new UploadToFTPCommand(
$src,
$this->storageProvider->credentials['path'].'/'.$dest,
$this->storageProvider->credentials['host'],
$this->storageProvider->credentials['port'],
$this->storageProvider->credentials['username'],
$this->storageProvider->credentials['password'],
$this->storageProvider->credentials['ssl'],
$this->storageProvider->credentials['passive'],
),
'upload-to-ftp'
);
return [
'size' => null,
];
}
/**
* @throws Throwable
*/
public function download(Server $server, string $src, string $dest): void
{
$server->ssh()->exec(
new DownloadFromFTPCommand(
$this->storageProvider->credentials['path'].'/'.$src,
$dest,
$this->storageProvider->credentials['host'],
$this->storageProvider->credentials['port'],
$this->storageProvider->credentials['username'],
$this->storageProvider->credentials['password'],
$this->storageProvider->credentials['ssl'],
$this->storageProvider->credentials['passive'],
),
'download-from-ftp'
);
}
public function delete(array $paths): void
{
$connection = $this->connection();
if ($connection && $this->login($connection)) {
if ($this->storageProvider->credentials['passive']) {
ftp_pasv($connection, true);
}
foreach ($paths as $path) {
ftp_delete($connection, $this->storageProvider->credentials['path'].'/'.$path);
}
}
ftp_close($connection);
}
private function connection(): bool|Connection
{
$credentials = $this->storageProvider->credentials;
if ($credentials['ssl']) {
return ftp_ssl_connect($credentials['host'], $credentials['port'], 5);
}
return ftp_connect($credentials['host'], $credentials['port'], 5);
}
private function login(Connection $connection): bool
{
$credentials = $this->storageProvider->credentials;
return ftp_login($connection, $credentials['username'], $credentials['password']);
}
}

View File

@ -16,7 +16,7 @@
"laravel/tinker": "^2.8", "laravel/tinker": "^2.8",
"livewire/livewire": "^2.12", "livewire/livewire": "^2.12",
"phpseclib/phpseclib": "~3.0", "phpseclib/phpseclib": "~3.0",
"pusher/pusher-php-server": "^7.2" "ext-ftp": "*"
}, },
"require-dev": { "require-dev": {
"fakerphp/faker": "^1.9.1", "fakerphp/faker": "^1.9.1",

164
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "0f725da8271d545b318e319f7b616053", "content-hash": "53be6925a69aeafb21d079b82e51c1c4",
"packages": [ "packages": [
{ {
"name": "aws/aws-crt-php", "name": "aws/aws-crt-php",
@ -1586,16 +1586,16 @@
}, },
{ {
"name": "laravel/framework", "name": "laravel/framework",
"version": "v10.7.1", "version": "v10.16.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/laravel/framework.git", "url": "https://github.com/laravel/framework.git",
"reference": "ddbbb2b50388721fe63312bb4469cae13163fd36" "reference": "5c93d2795c393b462481179ce42dedfb30cc19b5"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/laravel/framework/zipball/ddbbb2b50388721fe63312bb4469cae13163fd36", "url": "https://api.github.com/repos/laravel/framework/zipball/5c93d2795c393b462481179ce42dedfb30cc19b5",
"reference": "ddbbb2b50388721fe63312bb4469cae13163fd36", "reference": "5c93d2795c393b462481179ce42dedfb30cc19b5",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -1782,7 +1782,7 @@
"issues": "https://github.com/laravel/framework/issues", "issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework" "source": "https://github.com/laravel/framework"
}, },
"time": "2023-04-11T14:11:49+00:00" "time": "2023-07-26T03:30:46+00:00"
}, },
{ {
"name": "laravel/sanctum", "name": "laravel/sanctum",
@ -3198,92 +3198,6 @@
}, },
"time": "2020-10-15T08:29:30+00:00" "time": "2020-10-15T08:29:30+00:00"
}, },
{
"name": "paragonie/sodium_compat",
"version": "v1.19.0",
"source": {
"type": "git",
"url": "https://github.com/paragonie/sodium_compat.git",
"reference": "cb15e403ecbe6a6cc515f855c310eb6b1872a933"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/paragonie/sodium_compat/zipball/cb15e403ecbe6a6cc515f855c310eb6b1872a933",
"reference": "cb15e403ecbe6a6cc515f855c310eb6b1872a933",
"shasum": ""
},
"require": {
"paragonie/random_compat": ">=1",
"php": "^5.2.4|^5.3|^5.4|^5.5|^5.6|^7|^8"
},
"require-dev": {
"phpunit/phpunit": "^3|^4|^5|^6|^7|^8|^9"
},
"suggest": {
"ext-libsodium": "PHP < 7.0: Better performance, password hashing (Argon2i), secure memory management (memzero), and better security.",
"ext-sodium": "PHP >= 7.0: Better performance, password hashing (Argon2i), secure memory management (memzero), and better security."
},
"type": "library",
"autoload": {
"files": [
"autoload.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"ISC"
],
"authors": [
{
"name": "Paragon Initiative Enterprises",
"email": "security@paragonie.com"
},
{
"name": "Frank Denis",
"email": "jedisct1@pureftpd.org"
}
],
"description": "Pure PHP implementation of libsodium; uses the PHP extension if it exists",
"keywords": [
"Authentication",
"BLAKE2b",
"ChaCha20",
"ChaCha20-Poly1305",
"Chapoly",
"Curve25519",
"Ed25519",
"EdDSA",
"Edwards-curve Digital Signature Algorithm",
"Elliptic Curve Diffie-Hellman",
"Poly1305",
"Pure-PHP cryptography",
"RFC 7748",
"RFC 8032",
"Salpoly",
"Salsa20",
"X25519",
"XChaCha20-Poly1305",
"XSalsa20-Poly1305",
"Xchacha20",
"Xsalsa20",
"aead",
"cryptography",
"ecdh",
"elliptic curve",
"elliptic curve cryptography",
"encryption",
"libsodium",
"php",
"public-key cryptography",
"secret-key cryptography",
"side-channel resistant"
],
"support": {
"issues": "https://github.com/paragonie/sodium_compat/issues",
"source": "https://github.com/paragonie/sodium_compat/tree/v1.19.0"
},
"time": "2022-09-26T03:40:35+00:00"
},
{ {
"name": "phpoption/phpoption", "name": "phpoption/phpoption",
"version": "1.9.1", "version": "1.9.1",
@ -3961,67 +3875,6 @@
}, },
"time": "2023-04-07T21:57:09+00:00" "time": "2023-04-07T21:57:09+00:00"
}, },
{
"name": "pusher/pusher-php-server",
"version": "7.2.2",
"source": {
"type": "git",
"url": "https://github.com/pusher/pusher-http-php.git",
"reference": "4ace4873873b06c25cecb2dd6d9fdcbf2f20b640"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/pusher/pusher-http-php/zipball/4ace4873873b06c25cecb2dd6d9fdcbf2f20b640",
"reference": "4ace4873873b06c25cecb2dd6d9fdcbf2f20b640",
"shasum": ""
},
"require": {
"ext-curl": "*",
"ext-json": "*",
"guzzlehttp/guzzle": "^7.2",
"paragonie/sodium_compat": "^1.6",
"php": "^7.3|^8.0",
"psr/log": "^1.0|^2.0|^3.0"
},
"require-dev": {
"overtrue/phplint": "^2.3",
"phpunit/phpunit": "^9.3"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "5.0-dev"
}
},
"autoload": {
"psr-4": {
"Pusher\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Library for interacting with the Pusher REST API",
"keywords": [
"events",
"messaging",
"php-pusher-server",
"publish",
"push",
"pusher",
"real time",
"real-time",
"realtime",
"rest",
"trigger"
],
"support": {
"issues": "https://github.com/pusher/pusher-http-php/issues",
"source": "https://github.com/pusher/pusher-http-php/tree/7.2.2"
},
"time": "2022-12-20T19:52:36+00:00"
},
{ {
"name": "ralouphie/getallheaders", "name": "ralouphie/getallheaders",
"version": "3.0.3", "version": "3.0.3",
@ -9227,8 +9080,9 @@
"prefer-stable": true, "prefer-stable": true,
"prefer-lowest": false, "prefer-lowest": false,
"platform": { "platform": {
"php": "^8.1" "php": "^8.1",
"ext-ftp": "*"
}, },
"platform-dev": [], "platform-dev": [],
"plugin-api-version": "2.3.0" "plugin-api-version": "2.2.0"
} }

View File

@ -30,14 +30,15 @@
use App\SourceControlProviders\Github; use App\SourceControlProviders\Github;
use App\SourceControlProviders\Gitlab; use App\SourceControlProviders\Gitlab;
use App\StorageProviders\Dropbox; use App\StorageProviders\Dropbox;
use App\StorageProviders\FTP;
return [ return [
/* /*
* SSH * SSH
*/ */
'ssh_user' => env('SSH_USER', 'vito'), 'ssh_user' => env('SSH_USER', 'vito'),
'ssh_public_key_name' => env('SSH_PUBLIC_KEY_NAME'), 'ssh_public_key_name' => env('SSH_PUBLIC_KEY_NAME', 'ssh-public.key'),
'ssh_private_key_name' => env('SSH_PRIVATE_KEY_NAME'), 'ssh_private_key_name' => env('SSH_PRIVATE_KEY_NAME', 'ssh-private.pem'),
'logs_disk' => env('SERVER_LOGS_DISK', 'server-logs-local'), 'logs_disk' => env('SERVER_LOGS_DISK', 'server-logs-local'),
'key_pairs_disk' => env('KEY_PAIRS_DISK', 'key-pairs-local'), 'key_pairs_disk' => env('KEY_PAIRS_DISK', 'key-pairs-local'),
@ -323,7 +324,6 @@
'https' => 443, 'https' => 443,
'mysql' => 3306, 'mysql' => 3306,
'ftp' => 21, 'ftp' => 21,
'phpmyadmin' => 54331,
'tcp' => '', 'tcp' => '',
'udp' => '', 'udp' => '',
], ],
@ -355,8 +355,10 @@
*/ */
'storage_providers' => [ 'storage_providers' => [
'dropbox', 'dropbox',
'ftp',
], ],
'storage_providers_class' => [ 'storage_providers_class' => [
'dropbox' => Dropbox::class, 'dropbox' => Dropbox::class,
'ftp' => FTP::class,
], ],
]; ];

View File

@ -13,7 +13,7 @@
| |
*/ */
'default' => env('QUEUE_CONNECTION', 'sync'), 'default' => env('QUEUE_CONNECTION', 'default'),
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------

View File

@ -139,85 +139,125 @@
], ],
], ],
'images' => [ 'images' => [
'us-east-1' => [ 'af-south-1' => [
'ubuntu_18' => 'ami-0279c3b3186e54acd', 'ubuntu_20' => 'ami-03684d4c2541e5333',
'ubuntu_20' => 'ami-083654bd07b5da81d', 'ubuntu_22' => 'ami-05759acc7d8973892',
],
'us-east-2' => [
'ubuntu_18' => 'ami-020db2c14939a8efb',
'ubuntu_20' => 'ami-0629230e074c580f2',
],
'us-west-1' => [
'ubuntu_18' => 'ami-083f68207d3376798',
'ubuntu_20' => 'ami-053ac55bdcfe96e85',
],
'us-west-2' => [
'ubuntu_18' => 'ami-09889d8d54f9e0a0e',
'ubuntu_20' => 'ami-036d46416a34a611c',
], ],
'ap-east-1' => [ 'ap-east-1' => [
'ubuntu_18' => 'ami-032c0a4bd39a5772c', 'ubuntu_20' => 'ami-0b19a97bf326b4931',
'ubuntu_20' => 'ami-0a9c1cc3697104990', 'ubuntu_22' => 'ami-03490b1b7425e5fe3',
],
'ap-south-1' => [
'ubuntu_18' => 'ami-00782a7608c7fc226',
'ubuntu_20' => 'ami-0567e0d2b4b2169ae',
], ],
'ap-northeast-1' => [ 'ap-northeast-1' => [
'ubuntu_18' => 'ami-085e9421f80dbe728', 'ubuntu_20' => 'ami-0e25df74d27e028e6',
'ubuntu_20' => 'ami-036d0684fc96830ca', 'ubuntu_22' => 'ami-09a81b370b76de6a2',
], ],
'ap-northeast-2' => [ 'ap-northeast-2' => [
'ubuntu_18' => 'ami-0252a84eb1d66c2a0', 'ubuntu_20' => 'ami-003a709e1e4ce3729',
'ubuntu_20' => 'ami-0f8b8babb98cc66d0', 'ubuntu_22' => 'ami-086cae3329a3f7d75',
],
'ap-northeast-3' => [
'ubuntu_20' => 'ami-06c1367bd83de7d47',
'ubuntu_22' => 'ami-0690c54203f5f67da',
],
'ap-south-1' => [
'ubuntu_20' => 'ami-0b88997c830e88c76',
'ubuntu_22' => 'ami-0287a05f0ef0e9d9a',
],
'ap-south-2' => [
'ubuntu_20' => 'ami-049e2ae605332dba6',
'ubuntu_22' => 'ami-06fe902e167e67d33',
], ],
'ap-southeast-1' => [ 'ap-southeast-1' => [
'ubuntu_18' => 'ami-0907c2c44ea451f84', 'ubuntu_20' => 'ami-0a6461ddb52e9db63',
'ubuntu_20' => 'ami-0fed77069cd5a6d6c', 'ubuntu_22' => 'ami-078c1149d8ad719a7',
], ],
'ap-southeast-2' => [ 'ap-southeast-2' => [
'ubuntu_18' => 'ami-00abf0511a7f4cee5', 'ubuntu_20' => 'ami-0a9fb81cc3289919c',
'ubuntu_20' => 'ami-0bf8b986de7e3c7ce', 'ubuntu_22' => 'ami-0df4b2961410d4cff',
],
'ap-southeast-3' => [
'ubuntu_20' => 'ami-05ee5bed682a3fff0',
'ubuntu_22' => 'ami-0fb6d1fdeeea10488',
],
'ap-southeast-4' => [
'ubuntu_20' => 'ami-02f9759882b112414',
'ubuntu_22' => 'ami-043a030d3eeabec75',
], ],
'ca-central-1' => [ 'ca-central-1' => [
'ubuntu_18' => 'ami-0e471deaa43652c4a', 'ubuntu_20' => 'ami-0daaea212e620de87',
'ubuntu_20' => 'ami-0bb84e7329f4fa1f7', 'ubuntu_22' => 'ami-06873c81b882339ac',
],
'cn-north-1' => [
'ubuntu_20' => 'ami-0c8bcac1fe3389a72',
'ubuntu_22' => 'ami-0728a1a4cc9e07753',
],
'cn-northwest-1' => [
'ubuntu_20' => 'ami-0415bfb3ea62e17c0',
'ubuntu_22' => 'ami-05529cf859783e600',
], ],
'eu-central-1' => [ 'eu-central-1' => [
'ubuntu_18' => 'ami-00d5e377dd7fad751', 'ubuntu_20' => 'ami-0b369586722023326',
'ubuntu_20' => 'ami-0a49b025fffbbdac6', 'ubuntu_22' => 'ami-06dd92ecc74fdfb36',
], ],
'eu-west-1' => [ 'eu-central-2' => [
'ubuntu_18' => 'ami-095b735dce49535b5', 'ubuntu_20' => 'ami-070c78d5ed65f11c8',
'ubuntu_20' => 'ami-08edbb0e85d6a0a07', 'ubuntu_22' => 'ami-07cf963e6321c9e6a',
],
'eu-west-2' => [
'ubuntu_18' => 'ami-008485ca60c91a0f3',
'ubuntu_20' => 'ami-0fdf70ed5c34c5f52',
],
'eu-west-3' => [
'ubuntu_18' => 'ami-0df7d9cc2767d16cd',
'ubuntu_20' => 'ami-06d79c60d7454e2af',
],
'eu-south-1' => [
'ubuntu_18' => 'ami-09f165dd6bd167be5',
'ubuntu_20' => 'ami-0f8ce9c417115413d',
], ],
'eu-north-1' => [ 'eu-north-1' => [
'ubuntu_18' => 'ami-038904f9024f34a0c', 'ubuntu_20' => 'ami-0c5863072fc83557e',
'ubuntu_20' => 'ami-0bd9c26722573e69b', 'ubuntu_22' => 'ami-0fe8bec493a81c7da',
],
'eu-south-1' => [
'ubuntu_20' => 'ami-0966ff128f1497260',
'ubuntu_22' => 'ami-0b03947fd0ce0eed2',
],
'eu-south-2' => [
'ubuntu_20' => 'ami-087296a5b46cb95ce',
'ubuntu_22' => 'ami-03486abd2962c176f',
],
'eu-west-1' => [
'ubuntu_20' => 'ami-0e3e7f215a53e2a86',
'ubuntu_22' => 'ami-0694d931cee176e7d',
],
'eu-west-2' => [
'ubuntu_20' => 'ami-0b22eee5ba6bb6772',
'ubuntu_22' => 'ami-0505148b3591e4c07',
],
'eu-west-3' => [
'ubuntu_20' => 'ami-0f14fa1f9c69f4111',
'ubuntu_22' => 'ami-00983e8a26e4c9bd9',
],
'il-central-1' => [
'ubuntu_20' => 'ami-0703881563bf5fab7',
'ubuntu_22' => 'ami-03869c813f5a2e20c',
],
'me-central-1' => [
'ubuntu_20' => 'ami-04a5bde3b044c7c21',
'ubuntu_22' => 'ami-02168d82d5c12118f',
], ],
'me-south-1' => [ 'me-south-1' => [
'ubuntu_18' => 'ami-0ef669c57b73af73b', 'ubuntu_20' => 'ami-0165b692f5714e330',
'ubuntu_20' => 'ami-0b4946d7420c44be4', 'ubuntu_22' => 'ami-0f8d2a6080634ee69',
], ],
'sa-east-1' => [ 'sa-east-1' => [
'ubuntu_18' => 'ami-0ed2b3edeb28afa59', 'ubuntu_20' => 'ami-095ca107fb46b81e6',
'ubuntu_20' => 'ami-0e66f5495b4efdd0f', 'ubuntu_22' => 'ami-0b6c2d49148000cd5',
], ],
'af-south-1' => [ 'us-east-1' => [
'ubuntu_18' => 'ami-0191bb2cf509687ee', 'ubuntu_20' => 'ami-0fe0238291c8e3f07',
'ubuntu_20' => 'ami-0ff86122fd4ad7208', 'ubuntu_22' => 'ami-0fc5d935ebf8bc3bc',
],
'us-east-2' => [
'ubuntu_20' => 'ami-0b6968e5c7117349a',
'ubuntu_22' => 'ami-0e83be366243f524a',
],
'us-west-1' => [
'ubuntu_20' => 'ami-092efbcc9a2d2be8a',
'ubuntu_22' => 'ami-0cbd40f694b804622',
],
'us-west-2' => [
'ubuntu_20' => 'ami-0a55cdf919d10eac9',
'ubuntu_22' => 'ami-0efcece6bed30fd98',
], ],
], ],
], ],

View File

@ -17,4 +17,40 @@ public function definition(): array
'access_token' => Str::random(10), 'access_token' => Str::random(10),
]; ];
} }
public function gitlab(): Factory
{
return $this->state(function (array $attributes) {
return [
'provider' => \App\Enums\SourceControl::GITLAB,
];
});
}
public function github(): Factory
{
return $this->state(function (array $attributes) {
return [
'provider' => \App\Enums\SourceControl::GITHUB,
];
});
}
public function bitbucket(): Factory
{
return $this->state(function (array $attributes) {
return [
'provider' => \App\Enums\SourceControl::BITBUCKET,
];
});
}
public function custom(): Factory
{
return $this->state(function (array $attributes) {
return [
'provider' => \App\Enums\SourceControl::CUSTOM,
];
});
}
} }

View File

@ -1,6 +1,6 @@
version: '3' version: '3'
services: services:
app: vito:
build: build:
context: ./docker/8.1 context: ./docker/8.1
dockerfile: Dockerfile dockerfile: Dockerfile

View File

@ -4,6 +4,9 @@
<testsuite name="Feature"> <testsuite name="Feature">
<directory suffix="Test.php">./tests/Feature</directory> <directory suffix="Test.php">./tests/Feature</directory>
</testsuite> </testsuite>
<testsuite name="Unit">
<directory suffix="Test.php">./tests/Unit</directory>
</testsuite>
</testsuites> </testsuites>
<coverage/> <coverage/>
<php> <php>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,11 +1,11 @@
{ {
"resources/css/app.css": { "resources/css/app.css": {
"file": "assets/app-bdf134de.css", "file": "assets/app-46ad72d7.css",
"isEntry": true, "isEntry": true,
"src": "resources/css/app.css" "src": "resources/css/app.css"
}, },
"resources/js/app.js": { "resources/js/app.js": {
"file": "assets/app-fa1f93fa.js", "file": "assets/app-dfd48f80.js",
"isEntry": true, "isEntry": true,
"src": "resources/js/app.js" "src": "resources/js/app.js"
} }

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" version="1.1" width="3890" height="2168" id="svg2">
<metadata id="metadata8">
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
<dc:title/>
</cc:Work>
</rdf:RDF>
</metadata>
<defs id="defs6"/>
<g id="g5" style="fill:#cccccc">
<path d="m 2889.39,6.3480122 -2.04,-4.07 c -1.01,-1.01 -2.03,-2.04 -4.06,-2.04 l -4.08,1.03 c -2.03,2.03 -3.05,3.05 -3.05,5.08 L 2789.64,1572.695 l 13.24,-2.035 83.46,-1523.70199 99.75,163.88 -1.02,0 c 75.32,221.88 106.87,458.02 94.66,708.41003 l 6.11,9.16 98.73,175.05996 6.1,6.11 c 151.66,133.336 321.63,222.907 509.94,268.707 l 45.8,74.309 6.11,2.031 1.02,-2.031 -866.19,-1416.82599 2.04,-29.52" id="path14"/>
<path d="m 2858.86,559.02801 c 106.87,218.84 145.54,549.62999 117.05,992.39199 l 416.29,-50.894 z" id="path18"/>
<path d="m 3807.48,1520.881 c 42.74,-5.086 70.22,-14.246 82.44,-27.476 l -2178.16,265.656 -1.02,0 c 1.02,90.586 41.73,161.836 121.12,212.723 21.37,15.269 43.77,26.465 64.12,33.59 19.34,-22.387 40.72,-38.676 66.17,-53.946 l 1.01,0 c 228,-138.422 566.94,-159.8 1014.78,-65.136 l 5.09,1.011 c 48.86,10.18 97.71,22.399 143.52,36.645 13.22,2.035 24.42,-2.035 33.58,-10.18 16.29,-12.215 36.65,-21.375 64.13,-27.476 l 0,-1.02 c 72.27,-128.25 169.97,-222.906 292.12,-284.996 84.48,-41.727 182.19,-69.215 291.1,-79.395" id="path20"/>
<path d="M 2761.15,1560.585 2860.89,31.798012 C 2716.36,589.56801 2484.29,1110.698 2165.71,1594.167 l 595.44,-33.582" id="path22"/>
<path d="m 2200.32,329.008 -4.07,-2.04 -4.07,1.02 -2.04,5.08 -82.44,1323.18799 12.21,-1.019 82.45,-1322.169 -2.04,-4.06" id="path24"/>
<path d="m 2084.29,1627.756 0,0 87.53,-1290.608 -461.08,1330.308 373.55,-39.7" id="path26"/>
</g>
<g id="g13" style="fill:none;stroke:#cccccc;stroke-width:12;stroke-linecap:round;stroke-linejoin:round">
<path d="m 2102.61,2057.9534 c -115.02,54.9606 -198.48,64.1207 -251.41,30.5387 -52.93,-35.6247 -134.36,-27.4847 -244.28,24.4219" id="path34"/>
<path d="m 2372.33,2066.093 c -21.37,-7.125 -44.78,-12.211 -69.21,-14.25 -49.87,-4.066 -109.93,15.27 -180.16,57.0042 -70.23,42.746 -154.71,59.0351 -255.48,49.8711" id="path36"/>
<path d="m 3000.34,1962.273 c -484.5,-99.742 -833.61,-76.336 -1047.35,71.25" id="path38"/>
<path d="m 3305.69,2010.117 c -21.38,-8.145 -44.79,-13.235 -69.22,-14.254 -49.87,-4.067 -109.92,15.269 -180.15,57.004 -70.23,41.7262 -154.71,58.0114 -255.48,49.8708" id="path40"/>
<path d="m 2655.29,341.21801 c -49.88,-30.54 -101.78,-22.39 -155.73,25.44 -46.82,-47.83 -95.67,-55.98 -146.57,-25.44" id="path42" style="stroke-width:29"/>
<path d="m 2542.31,170.21801 c -50.89,-30.53 -100.76,-18.32 -151.66,36.64 -49.88,-54.96 -100.76,-67.17 -151.66,-36.64" id="path44" style="stroke-width:29"/>
</g>
<g style="fill:#6c78af" id="g33">
<path d="m 56.770404,1763.8603 134.914516,0 c 40.53785,0 70.3051,11.5406 89.30315,34.6149 18.99058,23.0812 25.15303,55.2422 18.48477,96.4878 -2.73388,16.9207 -7.61168,32.4501 -14.63698,46.5933 -7.02825,14.1442 -16.30891,27.067 -27.84598,38.7573 -13.71822,14.0684 -29.07355,24.148 -46.07783,30.2307 -17.00335,6.0818 -38.74881,9.1232 -65.23042,9.1232 l -60.093245,0 -14.951878,92.5154 -70.16450702,0 56.29840402,-348.3226 z m 61.252276,55.1047 -23.613063,146.1033 42.675303,0 c 28.28742,0 49.25184,-5.7744 62.90101,-17.3383 13.63797,-11.5649 22.6337,-30.807 26.98842,-57.7355 4.19709,-25.968 1.75658,-44.2941 -7.2996,-54.9935 -9.06329,-10.6883 -26.92099,-16.036 -53.57751,-16.036 l -48.07456,0" id="path66"/>
<path d="m 378.24196,1671.3451 69.63516,0 -14.95189,92.5152 61.94982,0 c 38.98987,0 66.01185,7.3901 81.07593,22.1571 15.06891,14.7762 19.95224,38.5804 14.64264,71.4329 l -26.22037,162.2172 -70.68973,0 24.9538,-154.3893 c 2.84147,-17.5516 1.46456,-29.4876 -4.12427,-35.815 -5.58913,-6.3193 -17.28356,-9.4871 -35.06873,-9.4871 l -55.57891,0 -32.27423,199.6914 -69.64729,0 56.29807,-348.3224" id="path68"/>
<path d="m 666.43774,1763.8603 134.9135,0 c 40.54695,0 70.30511,11.5406 89.30319,34.6149 18.99454,23.0812 25.15404,55.2422 18.4898,96.4878 -2.7339,16.9207 -7.61577,32.4501 -14.63297,46.5933 -7.03329,14.1442 -16.32202,27.067 -27.85103,38.7573 -13.72229,14.0684 -29.07657,24.148 -46.08084,30.2307 -17.00032,6.0818 -38.74478,9.1232 -65.23548,9.1232 l -60.08435,0 -14.95188,92.5154 -70.16093,0 56.29099,-348.3226 z m 61.26036,55.1047 -23.61306,146.1033 42.6753,0 c 28.28641,0 49.2478,-5.7744 62.88888,-17.3383 13.641,-11.5649 22.64179,-30.807 26.99549,-57.7355 4.19406,-25.968 1.7576,-44.2941 -7.30364,-54.9935 -9.06329,-10.6883 -26.92099,-16.036 -53.56841,-16.036 l -48.07456,0" id="path70"/>
</g>
<g style="fill:#f89c0e" id="g65">
<path d="m 1027.7488,1597.3243 134.9473,0 59.2813,323.8687 163.9729,-323.8687 134.4328,0 -68.203,422.055 -90.0955,0 60.5624,-328.7447 -165.335,328.7447 -97.63,0 -62.0922,-332.1897 -44.4691,332.1897 -93.58345,0 68.21155,-422.055" id="path72"/>
<path d="m 1682.3349,1951.0787 68.129,0 39.0484,-241.6556 84.6928,0 -48.7474,301.634 c -6.6176,41.0085 -21.4426,71.3829 -44.4508,91.1237 -23.0089,19.7312 -54.9295,29.6038 -95.7507,29.6038 l -166.0776,0 10.1515,-62.7878 151.4988,0 c 16.2574,0 29.3646,-3.7991 39.3223,-11.39 9.9443,-7.6043 16.0994,-18.6108 18.427,-33.0342 l 0.8443,-5.1933 -74.9653,0 c -47.9513,0 -80.6531,-9.1406 -98.0934,-27.4219 -17.4525,-18.2801 -22.7167,-48.7649 -15.8297,-91.453 l 30.891,-191.0813 83.7372,0 -29.8633,184.7645 c -3.7745,23.3729 -2.642,38.6968 3.4343,45.9753 6.0641,7.2784 20.5937,10.9158 43.6016,10.9158" id="path74"/>
<path d="m 2102.3044,1597.3243 97.5197,0 118.1874,422.055 -102.2734,0 -23.907,-100.4601 -189.9554,0 -55.1868,100.4601 -97.5321,0 253.1476,-422.055 z m 31.6507,83.8058 -90.1808,162.444 130.2183,0 -40.0375,-162.444" id="path76"/>
<path d="m 2637.3031,2019.3793 -162.6964,0 c -49.1642,0 -85.2663,-13.9786 -108.3193,-41.9409 -23.0404,-27.9573 -30.5053,-66.9321 -22.4309,-116.9135 3.316,-20.4913 9.2287,-39.3092 17.7509,-56.4463 8.5217,-17.1419 19.771,-32.7965 33.7592,-46.9712 16.6286,-17.0415 35.3095,-29.2498 56.0305,-36.6239 20.7209,-7.3752 47.0449,-11.0604 78.9594,-11.0604 l 72.6621,0 18.1132,-112.0988 84.3865,0 -68.2152,422.055 z m -73.6376,-66.4629 28.5697,-176.7241 -51.2224,0 c -34.3524,0 -59.8397,6.9489 -76.4874,20.8321 -16.6223,13.8929 -27.5723,37.0269 -32.8017,69.397 -5.0795,31.4221 -2.0777,53.6898 9.0062,66.8122 11.0837,13.1222 32.7053,19.6828 64.8648,19.6828 l 58.0708,0" id="path78"/>
<path d="m 2761.0228,1709.4231 325.2334,0 c 47.155,0 79.7016,8.9067 97.6259,26.6992 17.9129,17.8022 23.6511,46.7028 17.1903,86.702 l -31.7718,196.555 -85.3299,0 30.0434,-185.9259 c 3.6903,-22.7947 2.5585,-37.8305 -3.3836,-45.1151 -5.966,-7.2785 -20.0182,-10.9208 -42.1807,-10.9208 l -47.5103,0 -39.1073,241.9618 -86.604,0 39.1073,-241.9618 -98.9777,0 -39.1073,241.9618 -85.3298,0 50.1021,-309.9562" id="path80"/>
<path d="m 3389.6515,1674.2008 -88.8214,0 12.4228,-76.8765 88.8214,0 -12.4228,76.8765 z m -55.7924,345.1785 -88.8215,0 50.0899,-309.9562 88.8214,0 -50.0898,309.9562" id="path82"/>
<path d="m 3457.2112,1709.4231 159.3518,0 c 48.1596,0 81.1348,8.7634 98.9115,26.2691 17.7775,17.5156 23.3935,46.5596 16.8359,87.1321 l -31.7597,196.555 -84.705,0 30.1524,-186.5005 c 3.7421,-23.1817 2.4511,-38.2189 -3.8732,-45.1102 -6.325,-6.9 -20.6913,-10.3511 -43.0866,-10.3511 l -68.1168,0 -39.1072,241.9618 -84.6928,0 50.0897,-309.9562" id="path84"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

@ -0,0 +1 @@
curl __passive__ -u "__username__:__password__" ftp__ssl__://__host__:__port__/__src__ -o "__dest__"

View File

@ -0,0 +1 @@
curl __passive__ -T "__src__" -u "__username__:__password__" ftp__ssl__://__host__:__port__/__dest__

View File

@ -2,6 +2,4 @@ if ! cd __path__; then
echo 'VITO_SSH_ERROR' && exit 1 echo 'VITO_SSH_ERROR' && exit 1
fi fi
if ! __script__; then __script__
echo 'VITO_SSH_ERROR' && exit 1
fi

View File

@ -1,5 +1,5 @@
server { server {
listen 54331; listen __port__;
server_name _; server_name _;
root /home/vito/phpmyadmin; root /home/vito/phpmyadmin;

View File

@ -4,37 +4,49 @@
<form method="POST" action="{{ route('login') }}"> <form method="POST" action="{{ route('login') }}">
@csrf @csrf
<div x-data="{ isPasswordVisible: false }">
<!-- Email Address --> <!-- Email Address -->
<div> <div>
<x-input-label for="email" :value="__('Email')" /> <x-input-label for="email" :value="__('Email')" />
<x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required autofocus autocomplete="username" /> <x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')"
required autofocus autocomplete="username" />
<x-input-error :messages="$errors->get('email')" class="mt-2" /> <x-input-error :messages="$errors->get('email')" class="mt-2" />
</div> </div>
<!-- Password --> <!-- Password -->
<div class="mt-4"> <div class="mt-4">
<x-input-label for="password" :value="__('Password')" /> <x-input-label for="password" :value="__('Password')" />
<x-text-input id="password" class="block mt-1 w-full" <x-text-input id="password" class="block mt-1 w-full"
type="password" x-bind:type="isPasswordVisible ? 'text' : 'password'" name="password" required
name="password" autocomplete="current-password" />
required autocomplete="current-password" />
<x-input-error :messages="$errors->get('password')" class="mt-2" /> <x-input-error :messages="$errors->get('password')" class="mt-2" />
</div> </div>
<div class="flex items-center justify-between">
<!-- Remember Me --> <!-- Remember Me -->
<div class="block mt-4 "> <div class="block mt-4 ">
<label for="remember_me" class="inline-flex items-center"> <label for="remember_me" class="inline-flex items-center">
<input id="remember_me" type="checkbox" class="rounded dark:bg-gray-900 border-gray-300 dark:border-gray-700 text-indigo-600 shadow-sm focus:ring-indigo-500 dark:focus:ring-indigo-600 dark:focus:ring-offset-gray-800" name="remember"> <input id="remember_me" type="checkbox"
class="rounded dark:bg-gray-900 border-gray-300 dark:border-gray-700 text-indigo-600 shadow-sm focus:ring-indigo-500 dark:focus:ring-indigo-600 dark:focus:ring-offset-gray-800"
name="remember">
<span class="ml-2 text-sm text-gray-600 dark:text-gray-400">{{ __('Remember me') }}</span> <span class="ml-2 text-sm text-gray-600 dark:text-gray-400">{{ __('Remember me') }}</span>
</label> </label>
</div> </div>
<!-- Show Password -->
<div class="block mt-4">
<label for="show_password" class="inline-flex items-center float-right">
<input id="show_password" type="checkbox" x-model="isPasswordVisible"
class="rounded dark:bg-gray-900 border-gray-300 dark:border-gray-700 text-indigo-600 shadow-sm focus:ring-indigo-500 dark:focus:ring-indigo-600 dark:focus:ring-offset-gray-800">
<span class="ml-2 text-sm text-gray-600 dark:text-gray-400">{{ __('Show password') }}</span>
</label>
</div>
</div>
<div class="flex items-center justify-end mt-4"> <div class="flex items-center justify-end mt-4">
@if (Route::has('password.request')) @if (Route::has('password.request'))
<a class="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800" href="{{ route('password.request') }}"> <a class="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800"
href="{{ route('password.request') }}">
{{ __('Forgot your password?') }} {{ __('Forgot your password?') }}
</a> </a>
@endif @endif
@ -43,5 +55,6 @@
{{ __('Log in') }} {{ __('Log in') }}
</x-primary-button> </x-primary-button>
</div> </div>
</div>
</form> </form>
</x-guest-layout> </x-guest-layout>

View File

@ -0,0 +1 @@
<p {{ $attributes->merge(['class' => 'mt-2 text-sm text-gray-500 dark:text-gray-300']) }}>{{ $slot }}</p>

View File

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

View File

@ -29,4 +29,5 @@
</div> </div>
</div> </div>
</body> </body>
</html> </html>

View File

@ -18,7 +18,7 @@
<tr> <tr>
<x-th>{{ __("Name") }}</x-th> <x-th>{{ __("Name") }}</x-th>
<x-th>{{ __("Created") }}</x-th> <x-th>{{ __("Created") }}</x-th>
<x-th>{{ __("Size") }}</x-th> {{--<x-th>{{ __("Size") }}</x-th>--}}
<x-th>{{ __("Status") }}</x-th> <x-th>{{ __("Status") }}</x-th>
<x-th>{{ __("Restored") }}</x-th> <x-th>{{ __("Restored") }}</x-th>
<x-th>{{ __("Restored To") }}</x-th> <x-th>{{ __("Restored To") }}</x-th>
@ -30,7 +30,7 @@
<x-td> <x-td>
<x-datetime :value="$file->created_at" /> <x-datetime :value="$file->created_at" />
</x-td> </x-td>
<x-td>{{ $file->size }}</x-td> {{--<x-td>{{ $file->size }}</x-td>--}}
<x-td> <x-td>
<div class="inline-flex"> <div class="inline-flex">
@include('livewire.databases.partials.backup-file-status', ['status' => $file->status]) @include('livewire.databases.partials.backup-file-status', ['status' => $file->status])

View File

@ -45,6 +45,18 @@
</div> </div>
@endif @endif
</div> </div>
<div>
<x-input-label for="timezone" :value="__('Timezone')" />
<x-select-input wire:model.defer="timezone" id="timezone" name="timezone" class="mt-1 block w-full" required>
@foreach(timezone_identifiers_list() as $timezone)
<option value="{{ $timezone }}">{{ $timezone }}</option>
@endforeach
</x-select-input>
@error('timezone')
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
</form> </form>
<x-slot name="actions"> <x-slot name="actions">

View File

@ -0,0 +1,33 @@
<x-modal name="install-phpmyadmin">
<form wire:submit.prevent="install" class="p-6">
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
{{ __('Install PHPMyAdmin') }}
</h2>
<div class="mt-6">
<x-input-label for="allowed_ip" :value="__('Allowed IP')" />
<x-text-input wire:model.defer="allowed_ip" id="allowed_ip" name="allowed_ip" class="mt-1 w-full" />
@error('allowed_ip')
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
<div class="mt-6">
<x-input-label for="port" :value="__('Port')" />
<x-text-input wire:model.defer="port" id="port" name="port" class="mt-1 w-full" />
@error('port')
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
<div class="mt-6 flex justify-end">
<x-secondary-button type="button" x-on:click="$dispatch('close')">
{{ __('Cancel') }}
</x-secondary-button>
<x-primary-button class="ml-3" @started.window="$dispatch('close')">
{{ __('Install') }}
</x-primary-button>
</div>
</form>
</x-modal>

View File

@ -1,7 +1,25 @@
<div> <div>
<x-card-header> <x-card-header>
<x-slot name="title">{{ __("Services") }}</x-slot> <x-slot name="title">{{ __('Services') }}</x-slot>
<x-slot name="description">{{ __("All services that we installed on your server are here") }}</x-slot> <x-slot name="description">{{ __('All services that we installed on your server are here') }}</x-slot>
<x-slot name="aside">
<x-dropdown>
<x-slot name="trigger">
<x-primary-button>
{{ __('Install Service') }}
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 ml-1">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
</svg>
</x-primary-button>
</x-slot>
<x-slot name="content">
<x-dropdown-link class="cursor-pointer" x-on:click="$dispatch('open-modal', 'install-phpmyadmin')">
{{ __('PHPMyAdmin') }}
</x-dropdown-link>
</x-slot>
</x-dropdown>
<livewire:services.install-p-h-p-my-admin :server="$server" />
</x-slot>
</x-card-header> </x-card-header>
<div class="space-y-3"> <div class="space-y-3">
@ -20,24 +38,30 @@
<x-dropdown> <x-dropdown>
<x-slot name="trigger"> <x-slot name="trigger">
<x-secondary-button> <x-secondary-button>
{{ __("Actions") }} {{ __('Actions') }}
</x-secondary-button> </x-secondary-button>
</x-slot> </x-slot>
<x-slot name="content"> <x-slot name="content">
@if($service->unit)
@if ($service->status == \App\Enums\ServiceStatus::STOPPED) @if ($service->status == \App\Enums\ServiceStatus::STOPPED)
<x-dropdown-link class="cursor-pointer" wire:click="start({{ $service->id }})"> <x-dropdown-link class="cursor-pointer" wire:click="start({{ $service->id }})">
{{ __("Start") }} {{ __('Start') }}
</x-dropdown-link> </x-dropdown-link>
@endif @endif
@if ($service->status == \App\Enums\ServiceStatus::READY) @if ($service->status == \App\Enums\ServiceStatus::READY)
<x-dropdown-link class="cursor-pointer" wire:click="stop({{ $service->id }})"> <x-dropdown-link class="cursor-pointer" wire:click="stop({{ $service->id }})">
{{ __("Stop") }} {{ __('Stop') }}
</x-dropdown-link> </x-dropdown-link>
@endif @endif
<x-dropdown-link class="cursor-pointer" wire:click="restart({{ $service->id }})"> <x-dropdown-link class="cursor-pointer" wire:click="restart({{ $service->id }})">
{{ __("Restart") }} {{ __('Restart') }}
</x-dropdown-link> </x-dropdown-link>
@else
<x-dropdown-link class="cursor-pointer" wire:click="uninstall({{ $service->id }})">
{{ __('Uninstall') }}
</x-dropdown-link>
@endif
</x-slot> </x-slot>
</x-dropdown> </x-dropdown>
</div> </div>

View File

@ -32,6 +32,17 @@
@enderror @enderror
</div> </div>
@if($provider === App\Enums\SourceControl::GITLAB)
<div class="mt-6">
<x-input-label for="url" value="Url (optional)" />
<x-text-input wire:model.defer="url" id="url" name="url" type="text" class="mt-1 w-full" placeholder="e.g. https://gitlab.example.com/" />
<x-input-help>If you run a self-managed gitlab enter the url here, leave empty to use gitlab.com</x-input-help>
@error('url')
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
@endif
<div class="mt-6"> <div class="mt-6">
<x-input-label for="token" value="API Key" /> <x-input-label for="token" value="API Key" />
<x-text-input wire:model.defer="token" id="token" name="token" type="text" class="mt-1 w-full" /> <x-text-input wire:model.defer="token" id="token" name="token" type="text" class="mt-1 w-full" />

View File

@ -32,6 +32,7 @@
@enderror @enderror
</div> </div>
@if($provider == \App\Enums\StorageProvider::DROPBOX)
<div class="mt-6"> <div class="mt-6">
<x-input-label for="token" value="API Key" /> <x-input-label for="token" value="API Key" />
<x-text-input wire:model.defer="token" id="token" name="token" type="text" class="mt-1 w-full" /> <x-text-input wire:model.defer="token" id="token" name="token" type="text" class="mt-1 w-full" />
@ -39,6 +40,71 @@
<x-input-error class="mt-2" :messages="$message" /> <x-input-error class="mt-2" :messages="$message" />
@enderror @enderror
</div> </div>
@endif
@if($provider == \App\Enums\StorageProvider::FTP)
<div class="grid grid-cols-2 gap-2">
<div class="mt-6">
<x-input-label for="host" value="Host" />
<x-text-input wire:model.defer="host" id="host" name="host" type="text" class="mt-1 w-full" />
@error('host')
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
<div class="mt-6">
<x-input-label for="port" value="Port" />
<x-text-input wire:model.defer="port" id="port" name="port" type="text" class="mt-1 w-full" />
@error('port')
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
</div>
<div class="mt-6">
<x-input-label for="path" value="Path" />
<x-text-input wire:model.defer="path" id="path" name="path" type="text" class="mt-1 w-full" />
@error('path')
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
<div class="grid grid-cols-2 gap-2">
<div class="mt-6">
<x-input-label for="username" value="Username" />
<x-text-input wire:model.defer="username" id="username" name="username" type="text" class="mt-1 w-full" />
@error('username')
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
<div class="mt-6">
<x-input-label for="password" value="Password" />
<x-text-input wire:model.defer="password" id="password" name="password" type="text" class="mt-1 w-full" />
@error('password')
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
</div>
<div class="grid grid-cols-2 gap-2">
<div class="mt-6">
<x-input-label for="ssl" :value="__('SSL')" />
<x-select-input wire:model="ssl" id="ssl" name="ssl" class="mt-1 w-full">
<option value="1" @if($ssl) selected @endif>{{ __("Yes") }}</option>
<option value="0" @if(!$ssl) selected @endif>{{ __("No") }}</option>
</x-select-input>
@error('ssl')
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
<div class="mt-6">
<x-input-label for="passive" :value="__('Passive')" />
<x-select-input wire:model="passive" id="passive" name="passive" class="mt-1 w-full">
<option value="1" @if($passive) selected @endif>{{ __("Yes") }}</option>
<option value="0" @if(!$passive) selected @endif>{{ __("No") }}</option>
</x-select-input>
@error('passive')
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
</div>
@endif
<div class="mt-6 flex justify-end"> <div class="mt-6 flex justify-end">
<x-secondary-button type="button" x-on:click="$dispatch('close')"> <x-secondary-button type="button" x-on:click="$dispatch('close')">

View File

@ -11,7 +11,13 @@
@foreach($providers as $provider) @foreach($providers as $provider)
<x-item-card> <x-item-card>
<div class="flex-none"> <div class="flex-none">
@if($provider->provider == \App\Enums\StorageProvider::FTP)
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-10 h-10 text-gray-600 dark:text-gray-200">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
</svg>
@else
<img src="{{ asset('static/images/' . $provider->provider . '.svg') }}" class="h-10 w-10" alt=""> <img src="{{ asset('static/images/' . $provider->provider . '.svg') }}" class="h-10 w-10" alt="">
@endif
</div> </div>
<div class="ml-3 flex flex-grow flex-col items-start justify-center"> <div class="ml-3 flex flex-grow flex-col items-start justify-center">
<span class="mb-1">{{ $provider->profile }}</span> <span class="mb-1">{{ $provider->profile }}</span>

View File

@ -3,6 +3,7 @@ const colors = require("tailwindcss/colors");
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
module.exports = { module.exports = {
darkMode: 'class',
content: [ content: [
'./vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php', './vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php',
'./storage/framework/views/*.php', './storage/framework/views/*.php',

View File

@ -29,6 +29,7 @@ public function test_profile_information_can_be_updated(): void
Livewire::test(UpdateProfileInformation::class) Livewire::test(UpdateProfileInformation::class)
->set('name', 'Test') ->set('name', 'Test')
->set('email', 'test@example.com') ->set('email', 'test@example.com')
->set('timezone', 'Europe/Berlin')
->call('submit') ->call('submit')
->assertSuccessful(); ->assertSuccessful();
@ -36,5 +37,6 @@ public function test_profile_information_can_be_updated(): void
$this->assertSame('Test', $this->user->name); $this->assertSame('Test', $this->user->name);
$this->assertSame('test@example.com', $this->user->email); $this->assertSame('test@example.com', $this->user->email);
$this->assertSame('Europe/Berlin', $this->user->timezone);
} }
} }

View File

@ -3,8 +3,12 @@
namespace Tests\Feature\Http; namespace Tests\Feature\Http;
use App\Enums\ServiceStatus; use App\Enums\ServiceStatus;
use App\Http\Livewire\Services\InstallPHPMyAdmin;
use App\Http\Livewire\Services\ServicesList; use App\Http\Livewire\Services\ServicesList;
use App\Jobs\Installation\InstallPHPMyAdmin as InstallationInstallPHPMyAdmin;
use App\Jobs\Installation\UninstallPHPMyAdmin;
use App\Jobs\Service\Manage; use App\Jobs\Service\Manage;
use App\Models\Service;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Bus; use Illuminate\Support\Facades\Bus;
use Livewire\Livewire; use Livewire\Livewire;
@ -80,6 +84,45 @@ public function test_start_service(string $name): void
Bus::assertDispatched(Manage::class); Bus::assertDispatched(Manage::class);
} }
public function test_install_phpmyadmin(): void
{
Bus::fake();
Livewire::test(InstallPHPMyAdmin::class, ['server' => $this->server])
->set('allowed_ip', '0.0.0.0')
->set('port', 5433)
->call('install')
->assertSuccessful();
Bus::assertDispatched(InstallationInstallPHPMyAdmin::class);
}
public function test_uninstall_phpmyadmin(): void
{
$service = Service::factory()->create([
'server_id' => $this->server->id,
'type' => 'phpmyadmin',
'type_data' => [
'allowed_ip' => '0.0.0.0',
'port' => '5433',
'php' => '8.1',
],
'name' => 'phpmyadmin',
'version' => '5.1.2',
'status' => ServiceStatus::READY,
'is_default' => 1,
]);
Bus::fake();
Livewire::test(ServicesList::class, ['server' => $this->server])
->call('uninstall', $service->id)
->assertSuccessful();
Bus::assertDispatched(UninstallPHPMyAdmin::class);
}
public static function data(): array public static function data(): array
{ {
return [ return [

View File

@ -17,21 +17,28 @@ class SourceControlsTest extends TestCase
/** /**
* @dataProvider data * @dataProvider data
*/ */
public function test_connect_provider(string $provider): void public function test_connect_provider(string $provider, ?string $customUrl): void
{ {
$this->actingAs($this->user); $this->actingAs($this->user);
Http::fake(); Http::fake();
Livewire::test(Connect::class) $livewire = Livewire::test(Connect::class)
->set('token', 'token') ->set('token', 'token')
->set('name', 'profile') ->set('name', 'profile')
->set('provider', $provider) ->set('provider', $provider);
if ($customUrl !== null) {
$livewire->set('url', $customUrl);
}
$livewire
->call('connect') ->call('connect')
->assertSuccessful(); ->assertSuccessful();
$this->assertDatabaseHas('source_controls', [ $this->assertDatabaseHas('source_controls', [
'provider' => $provider, 'provider' => $provider,
'url' => $customUrl,
]); ]);
} }
@ -61,9 +68,10 @@ public function test_delete_provider(string $provider): void
public static function data(): array public static function data(): array
{ {
return [ return [
['github'], ['github', null],
['gitlab'], ['gitlab', null],
['bitbucket'], ['gitlab', 'https://git.example.com/'],
['bitbucket', null],
]; ];
} }
} }

View File

@ -0,0 +1,29 @@
<?php
namespace Tests\Feature\SSHCommands\Storage;
use App\SSHCommands\Storage\DownloadFromFTPCommand;
use Tests\TestCase;
class DownloadFromFTPCommandTest extends TestCase
{
public function test_generate_command()
{
$command = new DownloadFromFTPCommand(
'src',
'dest',
'1.1.1.1',
'21',
'username',
'password',
false,
true,
);
$expected = <<<'EOD'
curl --ftp-pasv -u "username:password" ftp://1.1.1.1:21/src -o "dest"
EOD;
$this->assertStringContainsString($expected, $command->content());
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace Tests\Feature\SSHCommands\Storage;
use App\SSHCommands\Storage\UploadToFTPCommand;
use Tests\TestCase;
class UploadToFTPCommandTest extends TestCase
{
public function test_generate_command()
{
$command = new UploadToFTPCommand(
'src',
'dest',
'1.1.1.1',
'21',
'username',
'password',
true,
true
);
$expected = <<<'EOD'
curl --ftp-pasv -T "src" -u "username:password" ftps://1.1.1.1:21/dest
EOD;
$this->assertStringContainsString($expected, $command->content());
}
}

View File

@ -16,9 +16,7 @@ public function test_generate_command()
echo 'VITO_SSH_ERROR' && exit 1 echo 'VITO_SSH_ERROR' && exit 1
fi fi
if ! script; then script
echo 'VITO_SSH_ERROR' && exit 1
fi
EOD; EOD;
$this->assertStringContainsString($expected, $command->content()); $this->assertStringContainsString($expected, $command->content());

View File

@ -0,0 +1,87 @@
<?php
namespace Tests\Unit\SourceControlProviders;
use App\Models\SourceControl;
use App\SourceControlProviders\Gitlab;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class GitlabTest extends TestCase
{
use RefreshDatabase;
public function test_default_gitlab_url(): void
{
$sourceControlModel = SourceControl::factory()
->gitlab()
->create();
$gitlab = new Gitlab($sourceControlModel);
$this->assertSame('https://gitlab.com/api/v4', $gitlab->getApiUrl());
}
public function test_default_gitlab_repo_url(): void
{
$repo = 'test/repo';
$key = 'TEST_KEY';
$sourceControlModel = SourceControl::factory()
->gitlab()
->create();
$gitlab = new Gitlab($sourceControlModel);
$this->assertSame('git@gitlab.com-TEST_KEY:test/repo.git', $gitlab->fullRepoUrl($repo, $key));
}
/**
* @dataProvider customUrlData
*/
public function test_custom_url(string $url, string $expected): void
{
$sourceControlModel = SourceControl::factory()
->gitlab()
->create(['url' => $url]);
$gitlab = new Gitlab($sourceControlModel);
$this->assertSame($expected, $gitlab->getApiUrl());
}
/**
* @dataProvider customRepoUrlData
*/
public function test_custom_full_repository_url(string $url, string $expected): void
{
$repo = 'test/repo';
$key = 'TEST_KEY';
$sourceControlModel = SourceControl::factory()
->gitlab()
->create(['url' => $url]);
$gitlab = new Gitlab($sourceControlModel);
$this->assertSame($expected, $gitlab->fullRepoUrl($repo, $key));
}
public static function customRepoUrlData(): array
{
return [
['https://git.example.com/', 'git@git.example.com-TEST_KEY:test/repo.git'],
['https://git.test.example.com/', 'git@git.test.example.com-TEST_KEY:test/repo.git'],
['https://git.example.co.uk/', 'git@git.example.co.uk-TEST_KEY:test/repo.git'],
];
}
public static function customUrlData(): array
{
return [
['https://git.example.com/', 'https://git.example.com/api/v4'],
['https://git.test.example.com/', 'https://git.test.example.com/api/v4'],
['https://git.example.co.uk/', 'https://git.example.co.uk/api/v4'],
];
}
}