Compare commits

...

11 Commits
0.3.0 ... 0.4.0

Author SHA1 Message Date
4cda14f4b8 deploy Wordpress sites via VitoDeploy (#83) 2024-01-01 16:20:57 +01:00
5e6d338bdc Bump phpseclib/phpseclib from 3.0.21 to 3.0.34 (#78)
Bumps [phpseclib/phpseclib](https://github.com/phpseclib/phpseclib) from 3.0.21 to 3.0.34.
- [Release notes](https://github.com/phpseclib/phpseclib/releases)
- [Changelog](https://github.com/phpseclib/phpseclib/blob/master/CHANGELOG.md)
- [Commits](https://github.com/phpseclib/phpseclib/compare/3.0.21...3.0.34)

---
updated-dependencies:
- dependency-name: phpseclib/phpseclib
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-07 22:40:24 +01:00
7312e3f515 Bump axios from 1.3.5 to 1.6.0 (#74)
Bumps [axios](https://github.com/axios/axios) from 1.3.5 to 1.6.0.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.3.5...v1.6.0)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-16 22:53:15 +01:00
b771db882b Update README.md 2023-11-05 11:00:31 +01:00
94977797cc Update README.md 2023-11-05 10:59:16 +01:00
c45872df55 Create CONTRIBUTING.md 2023-11-05 10:57:38 +01:00
16fae5334c Create SECURITY.md (#72) 2023-11-04 14:49:02 +01:00
7b8deddeca small bug fix to update source-control provider 2023-10-29 22:37:46 +01:00
1bf3c94358 add auto-deployment (#71)
add update source-control to site-settings
2023-10-29 22:20:15 +01:00
700cc5f44c Bump postcss from 8.4.21 to 8.4.31 (#67)
Bumps [postcss](https://github.com/postcss/postcss) from 8.4.21 to 8.4.31.
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.4.21...8.4.31)

---
updated-dependencies:
- dependency-name: postcss
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Saeed Vaziry <61919774+saeedvaziry@users.noreply.github.com>
2023-10-16 22:41:16 +02:00
9d13cc0756 enable php extension installation (#68) 2023-10-15 10:06:50 +02:00
66 changed files with 987 additions and 424 deletions

23
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,23 @@
# Contributing
Thank you for your interest in contributing! There are a couple of contribution guidelines that make it easier to apply the incoming suggestions.
If you want to contribute please start with the issues. Issues labeled with "Bug" are the higher priorities.
## Issues
1. Issues are the best place to propose a new feature.
2. If you are adding a feature that there is no issue for yet, please first open an issue and label it as "feature" and lets discuss it before you implement it.
3. Search the issues before proposing a feature to see if it is already under discussion. Referencing existing issues is a good way to increase the priority of your own.
4. We don't have an issue template yet, but the more detailed your explanation, the more quickly we'll be able to evaluate it.
5. Search for the issue that you also have. Give it a reaction (and comment, if you have something to add). We note that!
## Pull Requests
1. Open PRs represent issues that we're actively thinking about merging (at a pace we can manage). If we think a proposal needs more discussion, or that the existing code would require a lot of back-and-forth to merge, we might close it and suggest you make an issue.
2. All PRs should be made against the `main` branch. This can be changed in the future.
3. If you are making changes to the front-end layer, Please build the assets via `npm run build` and push it with the other changes.
4. Write tests for your code. Tests can be Unit or Feature.
5. Code refactors will be closed. For the architectural refactors open an issue first.
6. Use `./vendor/bin/pint` to style your code before opening a PR otherwise the actions will fail.
7. Typo fixes in documentation are welcome, but if it's at all debatable we might just close it.
## Misc
1. If you think we closed something incorrectly, feel free to (politely) tell us why! We're human and make mistakes.

View File

@ -12,7 +12,11 @@ ## Documentation
## Contribution ## Contribution
Feel free to open a PR Please read the contribution guide [Here](/CONTRIBUTING.md)
## Security
Please read the security policy [Here](/SECURITY.md)
## Credits ## Credits

11
SECURITY.md Normal file
View File

@ -0,0 +1,11 @@
# Security Policy
## Supported Versions
| Version | Supported |
| ------- | ------------------ |
| 0.x | :white_check_mark: |
## Reporting a Vulnerability
If you see a vulnerability, please open an issue or report it directly to me (sa.vaziry@gmail.com)

View File

@ -49,11 +49,6 @@ public function create(Server $server, array $input): Site
]); ]);
} }
// detect php version
if ($site->type()->language() === 'php') {
$site->php_version = $input['php_version'];
}
// validate type // validate type
$this->validateType($site, $input); $this->validateType($site, $input);

View File

@ -0,0 +1,35 @@
<?php
namespace App\Actions\Site;
use App\Models\Site;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
class UpdateSourceControl
{
/**
* @throws ValidationException
*/
public function update(Site $site, array $input): void
{
$this->validate($input);
$site->source_control_id = $input['source_control'];
$site->save();
}
/**
* @throws ValidationException
*/
protected function validate(array $input): void
{
Validator::make($input, [
'source_control' => [
'required',
Rule::exists('source_controls', 'id'),
],
])->validate();
}
}

View File

@ -6,6 +6,8 @@ interface SiteType
{ {
public function language(): string; public function language(): string;
public function supportedFeatures(): array;
public function createValidationRules(array $input): array; public function createValidationRules(array $input): array;
public function createFields(array $input): array; public function createFields(array $input): array;

16
app/Enums/SiteFeature.php Normal file
View File

@ -0,0 +1,16 @@
<?php
namespace App\Enums;
use BenSampo\Enum\Enum;
final class SiteFeature extends Enum
{
const DEPLOYMENT = 'deployment';
const ENV = 'env';
const SSL = 'ssl';
const QUEUES = 'queues';
}

View File

@ -11,6 +11,4 @@ final class SourceControl extends Enum
const GITLAB = 'gitlab'; const GITLAB = 'gitlab';
const BITBUCKET = 'bitbucket'; const BITBUCKET = 'bitbucket';
const CUSTOM = 'custom';
} }

View File

@ -7,7 +7,7 @@
class SourceControlIsNotConnected extends Exception class SourceControlIsNotConnected extends Exception
{ {
public function __construct(protected SourceControl|string $sourceControl, string $message = null) public function __construct(protected SourceControl|string|null $sourceControl, string $message = null)
{ {
parent::__construct($message ?? 'Source control is not connected'); parent::__construct($message ?? 'Source control is not connected');
} }

View File

@ -0,0 +1,39 @@
<?php
namespace App\Http\Controllers;
use App\Exceptions\SourceControlIsNotConnected;
use App\Models\GitHook;
use Illuminate\Http\Request;
use Throwable;
class GitHookController extends Controller
{
public function __invoke(Request $request)
{
if (! $request->input('secret')) {
abort(404);
}
/** @var GitHook $gitHook */
$gitHook = GitHook::query()
->where('secret', $request->input('secret'))
->firstOrFail();
foreach ($gitHook->actions as $action) {
if ($action == 'deploy') {
try {
$gitHook->site->deploy();
} catch (SourceControlIsNotConnected) {
// TODO: send notification
} catch (Throwable $e) {
Log::error('git-hook-exception', (array) $e);
}
}
}
return response()->json([
'success' => true,
]);
}
}

View File

@ -0,0 +1,60 @@
<?php
namespace App\Http\Livewire\Application;
use App\Exceptions\SourceControlIsNotConnected;
use App\Models\Site;
use App\Traits\HasToast;
use App\Traits\RefreshComponentOnBroadcast;
use Illuminate\Contracts\View\View;
use Livewire\Component;
use Throwable;
class AutoDeployment extends Component
{
use RefreshComponentOnBroadcast;
use HasToast;
public Site $site;
/**
* @throws Throwable
*/
public function enable(): void
{
if (! $this->site->auto_deployment) {
try {
$this->site->enableAutoDeployment();
$this->site->refresh();
$this->toast()->success(__('Auto deployment has been enabled.'));
} catch (SourceControlIsNotConnected) {
$this->toast()->error(__('Source control is not connected. Check site\'s settings.'));
}
}
}
/**
* @throws Throwable
*/
public function disable(): void
{
if ($this->site->auto_deployment) {
try {
$this->site->disableAutoDeployment();
$this->site->refresh();
$this->toast()->success(__('Auto deployment has been disabled.'));
} catch (SourceControlIsNotConnected) {
$this->toast()->error(__('Source control is not connected. Check site\'s settings.'));
}
}
}
public function render(): View
{
return view('livewire.application.auto-deployment');
}
}

View File

@ -28,6 +28,7 @@ public function save(): void
session()->flash('status', 'script-updated'); session()->flash('status', 'script-updated');
$this->emitTo(Deploy::class, '$refresh'); $this->emitTo(Deploy::class, '$refresh');
$this->emitTo(AutoDeployment::class, '$refresh');
} }
public function render(): View public function render(): View

View File

@ -3,11 +3,13 @@
namespace App\Http\Livewire\Php; namespace App\Http\Livewire\Php;
use App\Actions\PHP\InstallNewPHP; use App\Actions\PHP\InstallNewPHP;
use App\Actions\PHP\InstallPHPExtension;
use App\Actions\PHP\UpdatePHPIni; use App\Actions\PHP\UpdatePHPIni;
use App\Models\Server; use App\Models\Server;
use App\Models\Service; use App\Models\Service;
use App\SSHCommands\PHP\GetPHPIniCommand; use App\SSHCommands\PHP\GetPHPIniCommand;
use App\Traits\RefreshComponentOnBroadcast; use App\Traits\RefreshComponentOnBroadcast;
use Exception;
use Illuminate\Contracts\View\View; use Illuminate\Contracts\View\View;
use Livewire\Component; use Livewire\Component;
use Throwable; use Throwable;
@ -24,6 +26,10 @@ class InstalledVersions extends Component
public string $ini = 'Loading php.ini'; public string $ini = 'Loading php.ini';
public ?int $extensionId = null;
public string $extension = '';
public function install(string $version): void public function install(string $version): void
{ {
app(InstallNewPHP::class)->install($this->server, [ app(InstallNewPHP::class)->install($this->server, [
@ -35,6 +41,7 @@ public function install(string $version): void
public function restart(int $id): void public function restart(int $id): void
{ {
/** @var Service $service */
$service = Service::query()->findOrFail($id); $service = Service::query()->findOrFail($id);
$service->restart(); $service->restart();
@ -43,6 +50,7 @@ public function restart(int $id): void
public function uninstall(): void public function uninstall(): void
{ {
/** @var Service $service */
$service = Service::query()->findOrFail($this->uninstallId); $service = Service::query()->findOrFail($this->uninstallId);
$service->uninstall(); $service->uninstall();
@ -56,6 +64,7 @@ public function loadIni(int $id): void
$this->iniId = $id; $this->iniId = $id;
$this->ini = 'Loading php.ini'; $this->ini = 'Loading php.ini';
/** @var Service $service */
$service = Service::query()->findOrFail($this->iniId); $service = Service::query()->findOrFail($this->iniId);
try { try {
@ -67,6 +76,7 @@ public function loadIni(int $id): void
public function saveIni(): void public function saveIni(): void
{ {
/** @var Service $service */
$service = Service::query()->findOrFail($this->iniId); $service = Service::query()->findOrFail($this->iniId);
app(UpdatePHPIni::class)->update($service, $this->all()['ini']); app(UpdatePHPIni::class)->update($service, $this->all()['ini']);
@ -76,10 +86,32 @@ public function saveIni(): void
session()->flash('status', 'ini-updated'); session()->flash('status', 'ini-updated');
} }
/**
* @throws Exception
*/
public function installExtension(): void
{
/** @var Service $service */
$service = Service::query()->findOrFail($this->extensionId);
app(InstallPHPExtension::class)->handle($service, [
'name' => $this->extension,
]);
session()->flash('status', 'started-installation');
}
public function render(): View public function render(): View
{ {
if ($this->extensionId) {
/** @var Service $php */
$php = Service::query()->findOrFail($this->extensionId);
$installedExtensions = $php->type_data['extensions'] ?? [];
}
return view('livewire.php.installed-versions', [ return view('livewire.php.installed-versions', [
'phps' => $this->server->services()->where('type', 'php')->get(), 'phps' => $this->server->services()->where('type', 'php')->get(),
'installedExtensions' => $installedExtensions ?? [],
]); ]);
} }
} }

View File

@ -2,7 +2,6 @@
namespace App\Http\Livewire\Sites; namespace App\Http\Livewire\Sites;
use App\Enums\SiteType;
use App\Exceptions\SourceControlIsNotConnected; use App\Exceptions\SourceControlIsNotConnected;
use App\Models\Server; use App\Models\Server;
use App\Models\SourceControl; use App\Models\SourceControl;
@ -16,23 +15,12 @@ class CreateSite extends Component
public Server $server; public Server $server;
public string $type = SiteType::LARAVEL; public array $inputs = [
'type' => '',
public string $domain; 'web_directory' => 'public',
'source_control' => '',
public string $alias; 'php_version' => '',
];
public string $php_version = '';
public string $web_directory = 'public';
public string $source_control = '';
public string $repository;
public string $branch;
public bool $composer;
/** /**
* @throws SourceControlIsNotConnected * @throws SourceControlIsNotConnected
@ -41,7 +29,7 @@ public function create(): void
{ {
$site = app(\App\Actions\Site\CreateSite::class)->create( $site = app(\App\Actions\Site\CreateSite::class)->create(
$this->server, $this->server,
$this->all() $this->inputs
); );
$this->redirect(route('servers.sites.show', [ $this->redirect(route('servers.sites.show', [

View File

@ -0,0 +1,33 @@
<?php
namespace App\Http\Livewire\Sites;
use App\Actions\Site\UpdateSourceControl;
use App\Models\Site;
use Illuminate\Contracts\View\View;
use Livewire\Component;
class UpdateSourceControlProvider extends Component
{
public Site $site;
public $source_control = null;
public function update(): void
{
app(UpdateSourceControl::class)->update($this->site, $this->all());
$this->resetErrorBag();
session()->flash('status', 'source-control-updated');
}
public function render(): View
{
if (! $this->source_control) {
$this->source_control = $this->site->source_control_id;
}
return view('livewire.sites.update-source-control-provider');
}
}

View File

@ -52,7 +52,7 @@ public function handle(): void
$this->site->id $this->site->id
); );
if (! Str::contains($result, 'Wordpress installed!')) { if (! Str::contains($result, 'Success')) {
throw new FailedToInstallWordpress($result); throw new FailedToInstallWordpress($result);
} }
} }

View File

@ -57,19 +57,19 @@ public function server(): BelongsTo
/** /**
* create database on server * create database on server
*/ */
public function createOnServer(): void public function createOnServer(string $queue = 'ssh'): void
{ {
dispatch(new CreateOnServer($this))->onConnection('ssh'); dispatch(new CreateOnServer($this))->onConnection($queue);
} }
/** /**
* delete database from server * delete database from server
*/ */
public function deleteFromServer(): void public function deleteFromServer(string $queue = 'ssh'): void
{ {
$this->status = DatabaseStatus::DELETING; $this->status = DatabaseStatus::DELETING;
$this->save(); $this->save();
dispatch(new DeleteFromServer($this))->onConnection('ssh'); dispatch(new DeleteFromServer($this))->onConnection($queue);
} }
public function backups(): HasMany public function backups(): HasMany

View File

@ -54,17 +54,17 @@ public function scopeHasDatabase(Builder $query, string $databaseName): Builder
return $query->where('databases', 'like', "%\"$databaseName\"%"); return $query->where('databases', 'like', "%\"$databaseName\"%");
} }
public function createOnServer(): void public function createOnServer(string $queue = 'ssh'): void
{ {
dispatch(new CreateOnServer($this))->onConnection('ssh'); dispatch(new CreateOnServer($this))->onConnection($queue);
} }
public function deleteFromServer(): void public function deleteFromServer(string $queue = 'ssh'): void
{ {
$this->status = DatabaseStatus::DELETING; $this->status = DatabaseStatus::DELETING;
$this->save(); $this->save();
dispatch(new DeleteFromServer($this))->onConnection('ssh'); dispatch(new DeleteFromServer($this))->onConnection($queue);
} }
public function linkNewDatabase(string $name): void public function linkNewDatabase(string $name): void
@ -79,14 +79,14 @@ public function linkNewDatabase(string $name): void
} }
} }
public function linkUser(): void public function linkUser(string $queue = 'ssh'): void
{ {
dispatch(new LinkUser($this))->onConnection('ssh'); dispatch(new LinkUser($this))->onConnection($queue);
} }
public function unlinkUser(): void public function unlinkUser(string $queue = 'ssh'): void
{ {
dispatch(new UnlinkUser($this))->onConnection('ssh'); dispatch(new UnlinkUser($this))->onConnection($queue);
} }
public function getFullUserAttribute(): string public function getFullUserAttribute(): string

View File

@ -2,9 +2,9 @@
namespace App\Models; namespace App\Models;
use App\Exceptions\FailedToDeployGitHook;
use Exception; use Exception;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Throwable; use Throwable;
@ -22,6 +22,8 @@
*/ */
class GitHook extends AbstractModel class GitHook extends AbstractModel
{ {
use HasFactory;
protected $fillable = [ protected $fillable = [
'site_id', 'site_id',
'source_control_id', 'source_control_id',
@ -55,9 +57,6 @@ public function scopeHasEvent(Builder $query, string $event): Builder
return $query->where('events', 'like', "%\"{$event}\"%"); return $query->where('events', 'like', "%\"{$event}\"%");
} }
/**
* @throws FailedToDeployGitHook
*/
public function deployHook(): void public function deployHook(): void
{ {
$this->update( $this->update(

View File

@ -6,7 +6,7 @@
use App\Enums\DeploymentStatus; use App\Enums\DeploymentStatus;
use App\Enums\SiteStatus; use App\Enums\SiteStatus;
use App\Enums\SslStatus; use App\Enums\SslStatus;
use App\Exceptions\FailedToDeployGitHook; use App\Events\Broadcast;
use App\Exceptions\SourceControlIsNotConnected; use App\Exceptions\SourceControlIsNotConnected;
use App\Jobs\Site\ChangePHPVersion; use App\Jobs\Site\ChangePHPVersion;
use App\Jobs\Site\Deploy; use App\Jobs\Site\Deploy;
@ -19,7 +19,8 @@
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException; use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Throwable; use Throwable;
/** /**
@ -341,22 +342,16 @@ public function getWebDirectoryPathAttribute(): string
/** /**
* @throws SourceControlIsNotConnected * @throws SourceControlIsNotConnected
* @throws ValidationException
* @throws FailedToDeployGitHook
* @throws Throwable * @throws Throwable
*/ */
public function enableAutoDeployment(): void public function enableAutoDeployment(): void
{ {
if ($this->gitHook) { if ($this->gitHook) {
throw ValidationException::withMessages([ return;
'auto_deployment' => __('Auto deployment already enabled'),
])->errorBag('auto_deployment');
} }
if (! $this->sourceControl()) { if (! $this->sourceControl()) {
throw ValidationException::withMessages([ throw new SourceControlIsNotConnected($this->source_control);
'auto_deployment' => __('Your application does not use any source controls'),
])->errorBag('auto_deployment');
} }
try { try {
@ -364,7 +359,7 @@ public function enableAutoDeployment(): void
$gitHook = new GitHook([ $gitHook = new GitHook([
'site_id' => $this->id, 'site_id' => $this->id,
'source_control_id' => $this->sourceControl()->id, 'source_control_id' => $this->sourceControl()->id,
'secret' => generate_uid(), 'secret' => Str::uuid()->toString(),
'actions' => ['deploy'], 'actions' => ['deploy'],
'events' => ['push'], 'events' => ['push'],
]); ]);
@ -399,4 +394,49 @@ public function getSshKeyNameAttribute(): string
{ {
return str('site_'.$this->id)->toString(); return str('site_'.$this->id)->toString();
} }
public function installationFinished(): void
{
$this->update([
'status' => SiteStatus::READY,
'progress' => 100,
]);
event(
new Broadcast('install-site-finished', [
'site' => $this,
])
);
/** @todo notify */
}
/**
* @throws Throwable
*/
public function installationFailed(Throwable $e): void
{
$this->update([
'status' => SiteStatus::INSTALLATION_FAILED,
]);
event(
new Broadcast('install-site-failed', [
'site' => $this,
])
);
/** @todo notify */
Log::error('install-site-error', [
'error' => (string) $e,
]);
throw $e;
}
public function hasFeature(string $feature): bool
{
return in_array($feature, $this->type()->supportedFeatures());
}
public function isReady(): bool
{
return $this->status === SiteStatus::READY;
}
} }

View File

@ -2,14 +2,12 @@
namespace App\SiteTypes; namespace App\SiteTypes;
use App\Enums\SiteStatus; use App\Enums\SiteFeature;
use App\Events\Broadcast;
use App\Jobs\Site\CloneRepository; use App\Jobs\Site\CloneRepository;
use App\Jobs\Site\ComposerInstall; use App\Jobs\Site\ComposerInstall;
use App\Jobs\Site\CreateVHost; use App\Jobs\Site\CreateVHost;
use App\Jobs\Site\DeployKey; use App\Jobs\Site\DeployKey;
use Illuminate\Support\Facades\Bus; use Illuminate\Support\Facades\Bus;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\Rule; use Illuminate\Validation\Rule;
use Throwable; use Throwable;
@ -20,12 +18,22 @@ public function language(): string
return 'php'; return 'php';
} }
public function supportedFeatures(): array
{
return [
SiteFeature::DEPLOYMENT,
SiteFeature::ENV,
SiteFeature::SSL,
SiteFeature::QUEUES,
];
}
public function createValidationRules(array $input): array public function createValidationRules(array $input): array
{ {
return [ return [
'php_version' => [ 'php_version' => [
'required', 'required',
'in:'.implode(',', $this->site->server->installedPHPVersions()), Rule::in($this->site->server->installedPHPVersions()),
], ],
'source_control' => [ 'source_control' => [
'required', 'required',
@ -44,9 +52,9 @@ public function createFields(array $input): array
{ {
return [ return [
'web_directory' => $input['web_directory'] ?? '', 'web_directory' => $input['web_directory'] ?? '',
'source_control_id' => $input['source_control'] ?? '', 'source_control_id' => $input['source_control'],
'repository' => $input['repository'] ?? '', 'repository' => $input['repository'],
'branch' => $input['branch'] ?? '', 'branch' => $input['branch'],
]; ];
} }
@ -54,6 +62,7 @@ public function data(array $input): array
{ {
return [ return [
'composer' => (bool) $input['composer'], 'composer' => (bool) $input['composer'],
'php_version' => $input['php_version'],
]; ];
} }
@ -76,33 +85,12 @@ function () {
} }
$chain[] = function () { $chain[] = function () {
$this->site->update([ $this->site->installationFinished();
'status' => SiteStatus::READY,
'progress' => 100,
]);
event(
new Broadcast('install-site-finished', [
'site' => $this->site,
])
);
/** @todo notify */
}; };
Bus::chain($chain) Bus::chain($chain)
->catch(function (Throwable $e) { ->catch(function (Throwable $e) {
$this->site->update([ $this->site->installationFailed($e);
'status' => SiteStatus::INSTALLATION_FAILED,
]);
event(
new Broadcast('install-site-failed', [
'site' => $this->site,
])
);
/** @todo notify */
Log::error('install-site-error', [
'error' => (string) $e,
]);
throw $e;
}) })
->onConnection('ssh-long') ->onConnection('ssh-long')
->dispatch(); ->dispatch();

View File

@ -2,13 +2,15 @@
namespace App\SiteTypes; namespace App\SiteTypes;
use App\Enums\SiteStatus; use App\Enums\SiteFeature;
use App\Events\Broadcast;
use App\Jobs\Site\CreateVHost; use App\Jobs\Site\CreateVHost;
use App\Jobs\Site\InstallWordpress; use App\Jobs\Site\InstallWordpress;
use App\Models\Database;
use App\Models\DatabaseUser;
use App\SSHCommands\Wordpress\UpdateWordpressCommand; use App\SSHCommands\Wordpress\UpdateWordpressCommand;
use Closure;
use Illuminate\Support\Facades\Bus; use Illuminate\Support\Facades\Bus;
use Illuminate\Support\Facades\Log; use Illuminate\Validation\Rule;
use Throwable; use Throwable;
class Wordpress extends AbstractSiteType class Wordpress extends AbstractSiteType
@ -18,94 +20,100 @@ public function language(): string
return 'php'; return 'php';
} }
public function supportedFeatures(): array
{
return [
SiteFeature::SSL,
];
}
public function createValidationRules(array $input): array public function createValidationRules(array $input): array
{ {
return [ return [
'php_version' => [
'required',
Rule::in($this->site->server->installedPHPVersions()),
],
'title' => 'required', 'title' => 'required',
'username' => 'required', 'username' => 'required',
'password' => 'required', 'password' => 'required',
'email' => 'required|email', 'email' => 'required|email',
'database' => 'required', 'database' => [
'database_user' => 'required', 'required',
Rule::unique('databases', 'name')->where(function ($query) {
return $query->where('server_id', $this->site->server_id);
}),
function (string $attribute, mixed $value, Closure $fail) {
if (! $this->site->server->database()) {
$fail(__('Database is not installed'));
}
},
],
'database_user' => [
'required',
Rule::unique('database_users', 'username')->where(function ($query) {
return $query->where('server_id', $this->site->server_id);
}),
],
'database_password' => 'required',
]; ];
} }
public function createFields(array $input): array public function createFields(array $input): array
{ {
return [ return [
'web_directory' => $input['web_directory'] ?? '', 'web_directory' => '',
'php_version' => $input['php_version'],
]; ];
} }
public function data(array $input): array public function data(array $input): array
{ {
$data = $this->site->type_data; return [
$data['url'] = $this->site->url; 'url' => $this->site->url,
if (isset($input['title']) && $input['title']) { 'title' => $input['title'],
$data['title'] = $input['title']; 'username' => $input['username'],
} 'email' => $input['email'],
if (isset($input['username']) && $input['username']) { 'password' => $input['password'],
$data['username'] = $input['username']; 'database' => $input['database'],
} 'database_user' => $input['database_user'],
if (isset($input['email']) && $input['email']) { 'database_password' => $input['database_password'],
$data['email'] = $input['email']; ];
}
if (isset($input['password']) && $input['password']) {
$data['password'] = $input['password'];
}
if (isset($input['database']) && $input['database']) {
$data['database'] = $input['database'];
}
if (isset($input['database_user']) && $input['database_user']) {
$data['database_user'] = $input['database_user'];
}
if (isset($input['url']) && $input['url']) {
$data['url'] = $input['url'];
}
return $data;
} }
public function install(): void public function install(): void
{ {
$chain = [ $chain = [
new CreateVHost($this->site), new CreateVHost($this->site),
$this->progress(30), $this->progress(15),
function () {
/** @var Database $database */
$database = $this->site->server->databases()->create([
'name' => $this->site->type_data['database'],
]);
$database->createOnServer('sync');
/** @var DatabaseUser $databaseUser */
$databaseUser = $this->site->server->databaseUsers()->create([
'username' => $this->site->type_data['database_user'],
'password' => $this->site->type_data['database_password'],
'databases' => [$this->site->type_data['database']],
]);
$databaseUser->createOnServer('sync');
$databaseUser->unlinkUser('sync');
$databaseUser->linkUser('sync');
},
$this->progress(50),
new InstallWordpress($this->site), new InstallWordpress($this->site),
$this->progress(65), $this->progress(75),
function () { function () {
$this->site->php()?->restart(); $this->site->php()?->restart();
$this->site->installationFinished();
}, },
]; ];
$chain[] = function () {
$this->site->update([
'status' => SiteStatus::READY,
'progress' => 100,
]);
event(
new Broadcast('install-site-finished', [
'site' => $this->site,
])
);
/** @todo notify */
};
Bus::chain($chain) Bus::chain($chain)
->catch(function (Throwable $e) { ->catch(function (Throwable $e) {
$this->site->update([ $this->site->installationFailed($e);
'status' => SiteStatus::INSTALLATION_FAILED,
]);
event(
new Broadcast('install-site-failed', [
'site' => $this->site,
])
);
/** @todo notify */
Log::error('install-site-error', [
'error' => (string) $e,
]);
throw $e;
}) })
->onConnection('ssh-long') ->onConnection('ssh-long')
->dispatch(); ->dispatch();
@ -139,32 +147,13 @@ function () {
'update-wordpress', 'update-wordpress',
$this->site->id $this->site->id
); );
$this->site->update([ $this->site->installationFinished();
'status' => SiteStatus::READY,
]);
event(
new Broadcast('install-site-finished', [
'site' => $this->site,
])
);
}, },
]; ];
Bus::chain($chain) Bus::chain($chain)
->catch(function (Throwable $e) { ->catch(function (Throwable $e) {
$this->site->update([ $this->site->installationFailed($e);
'status' => SiteStatus::INSTALLATION_FAILED,
]);
event(
new Broadcast('install-site-failed', [
'site' => $this->site,
])
);
/** @todo notify */
Log::error('install-site-error', [
'error' => (string) $e,
]);
throw $e;
}) })
->onConnection('ssh') ->onConnection('ssh')
->dispatch(); ->dispatch();

View File

@ -46,7 +46,7 @@ public function deployHook(string $repo, array $events, string $secret): array
{ {
$response = Http::withToken($this->sourceControl->access_token)->post($this->apiUrl."/repositories/$repo/hooks", [ $response = Http::withToken($this->sourceControl->access_token)->post($this->apiUrl."/repositories/$repo/hooks", [
'description' => 'deploy', 'description' => 'deploy',
'url' => url('/git-hooks?secret='.$secret), 'url' => url('/api/git-hooks?secret='.$secret),
'events' => [ 'events' => [
'repo:'.implode(',', $events), 'repo:'.implode(',', $events),
], ],

View File

@ -1,41 +0,0 @@
<?php
namespace App\SourceControlProviders;
class Custom extends AbstractSourceControlProvider
{
public function connect(): bool
{
return true;
}
public function getRepo(string $repo = null): string
{
return '';
}
public function fullRepoUrl(string $repo, string $key): string
{
return $repo;
}
public function deployHook(string $repo, array $events, string $secret): array
{
return [];
}
public function destroyHook(string $repo, string $hookId): void
{
// TODO: Implement destroyHook() method.
}
public function getLastCommit(string $repo, string $branch): ?array
{
return null;
}
public function deployKey(string $title, string $repo, string $key): void
{
// TODO: Implement deployKey() method.
}
}

View File

@ -59,7 +59,7 @@ public function deployHook(string $repo, array $events, string $secret): array
'name' => 'web', 'name' => 'web',
'events' => $events, 'events' => $events,
'config' => [ 'config' => [
'url' => url('/git-hooks?secret='.$secret), 'url' => url('/api/git-hooks?secret='.$secret),
'content_type' => 'json', 'content_type' => 'json',
], ],
'active' => true, 'active' => true,

View File

@ -53,7 +53,7 @@ public function deployHook(string $repo, array $events, string $secret): array
$this->getApiUrl().'/projects/'.$repository.'/hooks', $this->getApiUrl().'/projects/'.$repository.'/hooks',
[ [
'description' => 'deploy', 'description' => 'deploy',
'url' => url('/git-hooks?secret='.$secret), 'url' => url('/api/git-hooks?secret='.$secret),
'push_events' => in_array('push', $events), 'push_events' => in_array('push', $events),
'issues_events' => false, 'issues_events' => false,
'job_events' => false, 'job_events' => false,

14
composer.lock generated
View File

@ -3275,16 +3275,16 @@
}, },
{ {
"name": "phpseclib/phpseclib", "name": "phpseclib/phpseclib",
"version": "3.0.21", "version": "3.0.34",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/phpseclib/phpseclib.git", "url": "https://github.com/phpseclib/phpseclib.git",
"reference": "4580645d3fc05c189024eb3b834c6c1e4f0f30a1" "reference": "56c79f16a6ae17e42089c06a2144467acc35348a"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/4580645d3fc05c189024eb3b834c6c1e4f0f30a1", "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/56c79f16a6ae17e42089c06a2144467acc35348a",
"reference": "4580645d3fc05c189024eb3b834c6c1e4f0f30a1", "reference": "56c79f16a6ae17e42089c06a2144467acc35348a",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -3365,7 +3365,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/phpseclib/phpseclib/issues", "issues": "https://github.com/phpseclib/phpseclib/issues",
"source": "https://github.com/phpseclib/phpseclib/tree/3.0.21" "source": "https://github.com/phpseclib/phpseclib/tree/3.0.34"
}, },
"funding": [ "funding": [
{ {
@ -3381,7 +3381,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2023-07-09T15:24:48+00:00" "time": "2023-11-27T11:13:31+00:00"
}, },
{ {
"name": "pragmarx/google2fa", "name": "pragmarx/google2fa",
@ -9084,5 +9084,5 @@
"ext-ftp": "*" "ext-ftp": "*"
}, },
"platform-dev": [], "platform-dev": [],
"plugin-api-version": "2.2.0" "plugin-api-version": "2.6.0"
} }

View File

@ -25,8 +25,8 @@
use App\ServiceHandlers\Webserver\Nginx; use App\ServiceHandlers\Webserver\Nginx;
use App\SiteTypes\Laravel; use App\SiteTypes\Laravel;
use App\SiteTypes\PHPSite; use App\SiteTypes\PHPSite;
use App\SiteTypes\Wordpress;
use App\SourceControlProviders\Bitbucket; use App\SourceControlProviders\Bitbucket;
use App\SourceControlProviders\Custom;
use App\SourceControlProviders\Github; use App\SourceControlProviders\Github;
use App\SourceControlProviders\Gitlab; use App\SourceControlProviders\Gitlab;
use App\StorageProviders\Dropbox; use App\StorageProviders\Dropbox;
@ -263,12 +263,12 @@
'site_types' => [ 'site_types' => [
\App\Enums\SiteType::PHP, \App\Enums\SiteType::PHP,
\App\Enums\SiteType::LARAVEL, \App\Enums\SiteType::LARAVEL,
// \App\Enums\SiteType::WORDPRESS, \App\Enums\SiteType::WORDPRESS,
], ],
'site_types_class' => [ 'site_types_class' => [
\App\Enums\SiteType::PHP => PHPSite::class, \App\Enums\SiteType::PHP => PHPSite::class,
\App\Enums\SiteType::LARAVEL => Laravel::class, \App\Enums\SiteType::LARAVEL => Laravel::class,
// \App\Enums\SiteType::WORDPRESS => Wordpress::class, \App\Enums\SiteType::WORDPRESS => Wordpress::class,
], ],
/* /*
@ -284,7 +284,6 @@
'github' => Github::class, 'github' => Github::class,
'gitlab' => Gitlab::class, 'gitlab' => Gitlab::class,
'bitbucket' => Bitbucket::class, 'bitbucket' => Bitbucket::class,
'custom' => Custom::class,
], ],
/* /*
@ -292,11 +291,12 @@
*/ */
'php_extensions' => [ 'php_extensions' => [
'imagick', 'imagick',
'geoip',
'exif', 'exif',
'gmagick', 'gmagick',
'gmp', 'gmp',
'intl', 'intl',
'sqlite3',
'opcache',
], ],
/* /*

View File

@ -0,0 +1,29 @@
<?php
namespace Database\Factories;
use App\Models\GitHook;
use App\Models\Site;
use App\Models\SourceControl;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Carbon;
class GitHookFactory extends Factory
{
protected $model = GitHook::class;
public function definition(): array
{
return [
'secret' => $this->faker->word(),
'events' => $this->faker->words(),
'actions' => $this->faker->words(),
'hook_id' => $this->faker->word(),
'hook_response' => $this->faker->words(),
'created_at' => Carbon::now(),
'updated_at' => Carbon::now(),
'site_id' => Site::factory(),
'source_control_id' => SourceControl::factory(),
];
}
}

View File

@ -13,7 +13,6 @@ class SourceControlFactory extends Factory
public function definition(): array public function definition(): array
{ {
return [ return [
'provider' => $this->faker->randomElement(\App\Enums\SourceControl::getValues()),
'access_token' => Str::random(10), 'access_token' => Str::random(10),
]; ];
} }
@ -44,13 +43,4 @@ public function bitbucket(): Factory
]; ];
}); });
} }
public function custom(): Factory
{
return $this->state(function (array $attributes) {
return [
'provider' => \App\Enums\SourceControl::CUSTOM,
];
});
}
} }

36
package-lock.json generated
View File

@ -10,10 +10,10 @@
"@tailwindcss/typography": "^0.5.9", "@tailwindcss/typography": "^0.5.9",
"alpinejs": "^3.4.2", "alpinejs": "^3.4.2",
"autoprefixer": "^10.4.2", "autoprefixer": "^10.4.2",
"axios": "^1.1.2", "axios": "^1.6.0",
"laravel-echo": "^1.15.0", "laravel-echo": "^1.15.0",
"laravel-vite-plugin": "^0.7.2", "laravel-vite-plugin": "^0.7.2",
"postcss": "^8.4.6", "postcss": "^8.4.31",
"pusher-js": "^4.3.1", "pusher-js": "^4.3.1",
"tailwindcss": "^3.1.0", "tailwindcss": "^3.1.0",
"toastr": "^2.1.4", "toastr": "^2.1.4",
@ -596,9 +596,9 @@
} }
}, },
"node_modules/axios": { "node_modules/axios": {
"version": "1.3.5", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.3.5.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.0.tgz",
"integrity": "sha512-glL/PvG/E+xCWwV8S6nCHcrfg1exGx7vxyUIivIA1iL7BIh6bePylCfVHwp6k13ao7SATxB6imau2kqY+I67kw==", "integrity": "sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"follow-redirects": "^1.15.0", "follow-redirects": "^1.15.0",
@ -1380,9 +1380,9 @@
} }
}, },
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.4.21", "version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
"integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==", "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -1392,10 +1392,14 @@
{ {
"type": "tidelift", "type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss" "url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
} }
], ],
"dependencies": { "dependencies": {
"nanoid": "^3.3.4", "nanoid": "^3.3.6",
"picocolors": "^1.0.0", "picocolors": "^1.0.0",
"source-map-js": "^1.0.2" "source-map-js": "^1.0.2"
}, },
@ -2284,9 +2288,9 @@
} }
}, },
"axios": { "axios": {
"version": "1.3.5", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.3.5.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.0.tgz",
"integrity": "sha512-glL/PvG/E+xCWwV8S6nCHcrfg1exGx7vxyUIivIA1iL7BIh6bePylCfVHwp6k13ao7SATxB6imau2kqY+I67kw==", "integrity": "sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg==",
"dev": true, "dev": true,
"requires": { "requires": {
"follow-redirects": "^1.15.0", "follow-redirects": "^1.15.0",
@ -2852,12 +2856,12 @@
"dev": true "dev": true
}, },
"postcss": { "postcss": {
"version": "8.4.21", "version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
"integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==", "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"nanoid": "^3.3.4", "nanoid": "^3.3.6",
"picocolors": "^1.0.0", "picocolors": "^1.0.0",
"source-map-js": "^1.0.2" "source-map-js": "^1.0.2"
} }

View File

@ -12,10 +12,10 @@
"@tailwindcss/typography": "^0.5.9", "@tailwindcss/typography": "^0.5.9",
"alpinejs": "^3.4.2", "alpinejs": "^3.4.2",
"autoprefixer": "^10.4.2", "autoprefixer": "^10.4.2",
"axios": "^1.1.2", "axios": "^1.6.0",
"laravel-echo": "^1.15.0", "laravel-echo": "^1.15.0",
"laravel-vite-plugin": "^0.7.2", "laravel-vite-plugin": "^0.7.2",
"postcss": "^8.4.6", "postcss": "^8.4.31",
"pusher-js": "^4.3.1", "pusher-js": "^4.3.1",
"tailwindcss": "^3.1.0", "tailwindcss": "^3.1.0",
"toastr": "^2.1.4", "toastr": "^2.1.4",

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-46ad72d7.css", "file": "assets/app-328222da.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-dfd48f80.js", "file": "assets/app-9aa488bb.js",
"isEntry": true, "isEntry": true,
"src": "resources/js/app.js" "src": "resources/js/app.js"
} }

View File

@ -38,7 +38,8 @@
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"> 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 <x-application-logo class="w-10 h-10 rounded-md"/>
<span class="ml-1">Deploy</span>
</div> </div>
</div> </div>
@ -160,45 +161,7 @@ class="min-h-screen w-64 flex-none border-r border-gray-200 bg-white dark:border
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
{{-- Search --}} {{-- Search --}}
</div> </div>
{{-- Dark Mode Toggle Button section --}} @include('layouts.partials.color-scheme')
<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">

View File

@ -0,0 +1,37 @@
<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>

View File

@ -1,3 +1,4 @@
@php use App\Enums\SiteFeature; @endphp
<x-app-layout :server="$site->server"> <x-app-layout :server="$site->server">
@if(isset($pageTitle)) @if(isset($pageTitle))
<x-slot name="pageTitle">{{ $site->domain }} - {{ $pageTitle }}</x-slot> <x-slot name="pageTitle">{{ $site->domain }} - {{ $pageTitle }}</x-slot>
@ -58,10 +59,12 @@
<x-secondary-sidebar-link :href="route('servers.sites.show', ['server' => $site->server, 'site' => $site])" :active="request()->routeIs('servers.sites.show')"> <x-secondary-sidebar-link :href="route('servers.sites.show', ['server' => $site->server, 'site' => $site])" :active="request()->routeIs('servers.sites.show')">
{{ __('Application') }} {{ __('Application') }}
</x-secondary-sidebar-link> </x-secondary-sidebar-link>
@if($site->status == \App\Enums\SiteStatus::READY) @if($site->isReady() && $site->hasFeature(SiteFeature::SSL))
<x-secondary-sidebar-link :href="route('servers.sites.ssl', ['server' => $site->server, 'site' => $site])" :active="request()->routeIs('servers.sites.ssl')"> <x-secondary-sidebar-link :href="route('servers.sites.ssl', ['server' => $site->server, 'site' => $site])" :active="request()->routeIs('servers.sites.ssl')">
{{ __('SSL') }} {{ __('SSL') }}
</x-secondary-sidebar-link> </x-secondary-sidebar-link>
@endif
@if($site->isReady() && $site->hasFeature(SiteFeature::QUEUES))
<x-secondary-sidebar-link :href="route('servers.sites.queues', ['server' => $site->server, 'site' => $site])" :active="request()->routeIs('servers.sites.queues')"> <x-secondary-sidebar-link :href="route('servers.sites.queues', ['server' => $site->server, 'site' => $site])" :active="request()->routeIs('servers.sites.queues')">
{{ __('Queues') }} {{ __('Queues') }}
</x-secondary-sidebar-link> </x-secondary-sidebar-link>

View File

@ -0,0 +1,32 @@
<div>
@if($site->deploymentScript?->content)
<x-dropdown>
<x-slot name="trigger">
<x-secondary-button>
{{ __('Auto Deployment') }}
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 ml-1">
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 15L12 18.75 15.75 15m-7.5-6L12 5.25 15.75 9" />
</svg>
</x-secondary-button>
</x-slot>
<x-slot name="content">
<x-dropdown-link class="cursor-pointer" wire:click="enable">
{{ __("Enable") }}
@if($site->auto_deployment)
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 ml-1 text-green-600">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
@endif
</x-dropdown-link>
<x-dropdown-link class="cursor-pointer" wire:click="disable">
{{ __("Disable") }}
@if(!$site->auto_deployment)
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 ml-1 text-green-600">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
@endif
</x-dropdown-link>
</x-slot>
</x-dropdown>
@endif
</div>

View File

@ -1,5 +1,4 @@
<div x-data=""> <div x-data="">
<x-secondary-button x-on:click="$dispatch('open-modal', 'change-branch')">{{ __("Branch") }}</x-secondary-button>
<x-modal name="change-branch"> <x-modal name="change-branch">
<form wire:submit.prevent="change" class="p-6"> <form wire:submit.prevent="change" class="p-6">
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100"> <h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">

View File

@ -1,5 +1,4 @@
<div x-data=""> <div x-data="">
<x-secondary-button x-on:click="$dispatch('open-modal', 'deployment-script')">{{ __("Deployment Script") }}</x-secondary-button>
<x-modal name="deployment-script"> <x-modal name="deployment-script">
<form wire:submit.prevent="save" class="p-6"> <form wire:submit.prevent="save" class="p-6">
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100"> <h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">

View File

@ -1,5 +1,4 @@
<div x-data=""> <div x-data="">
<x-secondary-button x-on:click="$dispatch('open-modal', 'update-env')">{{ __(".env") }}</x-secondary-button>
<x-modal name="update-env"> <x-modal name="update-env">
<form wire:submit.prevent="save" class="p-6"> <form wire:submit.prevent="save" class="p-6">
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100"> <h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">

View File

@ -5,17 +5,29 @@
<x-slot name="aside"> <x-slot name="aside">
<div class="flex items-center"> <div class="flex items-center">
<div class="mr-2"> <div class="mr-2">
<livewire:application.change-branch :site="$site" />
</div>
<div class="mr-2">
<livewire:application.deployment-script :site="$site" />
</div>
<div class="mr-2">
<livewire:application.env :site="$site" />
</div>
<div>
<livewire:application.deploy :site="$site" /> <livewire:application.deploy :site="$site" />
</div> </div>
<div class="mr-2">
<livewire:application.auto-deployment :site="$site" />
</div>
<x-dropdown>
<x-slot name="trigger">
<x-secondary-button>
{{ __('Manage') }}
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 ml-1">
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 15L12 18.75 15.75 15m-7.5-6L12 5.25 15.75 9" />
</svg>
</x-secondary-button>
</x-slot>
<x-slot name="content">
<x-dropdown-link class="cursor-pointer" x-on:click="$dispatch('open-modal', 'change-branch')">{{ __("Branch") }}</x-dropdown-link>
<x-dropdown-link class="cursor-pointer" x-on:click="$dispatch('open-modal', 'deployment-script')">{{ __("Deployment Script") }}</x-dropdown-link>
<x-dropdown-link class="cursor-pointer" x-on:click="$dispatch('open-modal', 'update-env')">{{ __(".env") }}</x-dropdown-link>
</x-slot>
</x-dropdown>
<livewire:application.change-branch :site="$site" />
<livewire:application.deployment-script :site="$site" />
<livewire:application.env :site="$site" />
</div> </div>
</x-slot> </x-slot>
</x-card-header> </x-card-header>

View File

@ -1,3 +1,6 @@
<div> <div>
<x-simple-card class="flex items-center justify-between">
<span>{{ __("Your Wordpress site is installed and ready to use! ") }}</span>
<x-secondary-button :href="$site->url" target="_blank">{{ __("Open Website") }}</x-secondary-button>
</x-simple-card>
</div> </div>

View File

@ -28,9 +28,9 @@
</x-secondary-button> </x-secondary-button>
</x-slot> </x-slot>
<x-slot name="content"> <x-slot name="content">
{{--<x-dropdown-link class="cursor-pointer">--}} <x-dropdown-link class="cursor-pointer" x-on:click="$wire.extensionId = {{ $php->id }}; $dispatch('open-modal', 'install-extension')">
{{-- {{ __("Install Extension") }}--}} {{ __("Install Extension") }}
{{--</x-dropdown-link>--}} </x-dropdown-link>
<x-dropdown-link class="cursor-pointer" x-on:click="$dispatch('open-modal', 'update-php-ini')" wire:click="loadIni({{ $php->id }})"> <x-dropdown-link class="cursor-pointer" x-on:click="$dispatch('open-modal', 'update-php-ini')" wire:click="loadIni({{ $php->id }})">
{{ __("Edit php.ini") }} {{ __("Edit php.ini") }}
</x-dropdown-link> </x-dropdown-link>
@ -52,6 +52,7 @@
</div> </div>
@include('livewire.php.partials.uninstall-php') @include('livewire.php.partials.uninstall-php')
@include('livewire.php.partials.update-php-ini') @include('livewire.php.partials.update-php-ini')
@include('livewire.php.partials.install-extension')
@else @else
<x-simple-card> <x-simple-card>
<div class="text-center"> <div class="text-center">

View File

@ -0,0 +1,36 @@
<x-modal name="install-extension">
<form wire:submit.prevent="installExtension" class="p-6">
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
{{ __('Install Extension') }}
</h2>
<div class="mt-6">
<x-input-label for="extension" value="Name" />
<x-select-input wire:model.defer="extension" name="extension" class="mt-1 w-full">
<option value="" selected>{{ __("Select") }}</option>
@foreach(config('core.php_extensions') as $extension)
<option value="{{ $extension }}" @if(in_array($extension, $installedExtensions)) disabled @endif>
{{ $extension }} @if(in_array($extension, $installedExtensions)) ({{ __("Installed") }}) @endif
</option>
@endforeach
</x-select-input>
@error('name')
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
<div class="mt-6 flex items-center justify-end">
@if (session('status') === 'started-installation')
<p class="mr-2">{{ __('Installation Started!') }}</p>
@endif
<x-secondary-button type="button" x-on:click="$dispatch('close')">
{{ __('Cancel') }}
</x-secondary-button>
<x-primary-button class="ml-3">
{{ __('Install') }}
</x-primary-button>
</div>
</form>
</x-modal>

View File

@ -5,10 +5,10 @@
<form id="create-site" wire:submit.prevent="create" class="space-y-6"> <form id="create-site" wire:submit.prevent="create" class="space-y-6">
<div> <div>
<x-input-label>{{ __("Select site type") }}</x-input-label> <x-input-label>{{ __("Select site type") }}</x-input-label>
<x-select-input wire:model="type" id="type" name="type" class="mt-1 w-full"> <x-select-input wire:model="inputs.type" id="type" name="type" class="mt-1 w-full">
<option value="" selected disabled>{{ __("Select") }}</option> <option value="" selected disabled>{{ __("Select") }}</option>
@foreach(config('core.site_types') as $t) @foreach(config('core.site_types') as $t)
<option value="{{ $t }}" @if($t === $type) selected @endif> <option value="{{ $t }}" @if($t === $inputs['type']) selected @endif>
{{ $t }} {{ $t }}
</option> </option>
@endforeach @endforeach
@ -20,7 +20,7 @@
<div> <div>
<x-input-label for="domain" :value="__('Domain')" /> <x-input-label for="domain" :value="__('Domain')" />
<x-text-input wire:model.defer="domain" id="domain" name="domain" type="text" class="mt-1 block w-full" autocomplete="domain" placeholder="example.com" /> <x-text-input wire:model.defer="inputs.domain" id="domain" name="domain" type="text" class="mt-1 block w-full" autocomplete="domain" placeholder="example.com" />
@error('domain') @error('domain')
<x-input-error class="mt-2" :messages="$message" /> <x-input-error class="mt-2" :messages="$message" />
@enderror @enderror
@ -28,75 +28,15 @@
<div> <div>
<x-input-label for="alias" :value="__('Alias')" /> <x-input-label for="alias" :value="__('Alias')" />
<x-text-input wire:model.defer="alias" id="alias" name="alias" type="text" class="mt-1 block w-full" autocomplete="alias" placeholder="www.example.com" /> <x-text-input wire:model.defer="inputs.alias" id="alias" name="alias" type="text" class="mt-1 block w-full" autocomplete="alias" placeholder="www.example.com" />
@error('alias') @error('alias')
<x-input-error class="mt-2" :messages="$message" /> <x-input-error class="mt-2" :messages="$message" />
@enderror @enderror
</div> </div>
<div> @if (isset($inputs['type']) && $inputs['type'])
<x-input-label for="php_version" :value="__('PHP Version')" /> @include('livewire.sites.partials.create.' . $inputs['type'])
<x-select-input wire:model.defer="php_version" id="php_version" name="php_version" class="mt-1 w-full"> @endif
<option value="" selected disabled>{{ __("Select") }}</option>
@foreach($server->installedPHPVersions() as $version)
<option value="{{ $version }}" @if($version === $php_version) selected @endif>
PHP {{ $version }}
</option>
@endforeach
</x-select-input>
@error('php_version')
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
<div>
<x-input-label for="web_directory" :value="__('Web Directory')" />
<x-text-input wire:model.defer="web_directory" id="web_directory" name="web_directory" type="text" class="mt-1 block w-full" autocomplete="web_directory" />
@error('web_directory')
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
<div>
<x-input-label for="source_control" :value="__('Source Control')" />
<div class="flex items-center mt-1">
<x-select-input wire:model="source_control" id="source_control" name="source_control" class="mt-1 w-full">
<option value="" selected disabled>{{ __("Select") }}</option>
@foreach($sourceControls as $sourceControl)
<option value="{{ $sourceControl->id }}" @if($sourceControl->id === $source_control) selected @endif>
{{ $sourceControl->profile }} ({{ $sourceControl->provider }})
</option>
@endforeach
</x-select-input>
<x-secondary-button :href="route('source-controls', ['redirect' => request()->url()])" class="flex-none ml-2">{{ __('Connect') }}</x-secondary-button>
</div>
@error('source_control')
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
<div>
<x-input-label for="repository" :value="__('Repository')" />
<x-text-input wire:model.defer="repository" id="repository" name="repository" type="text" class="mt-1 block w-full" autocomplete="repository" placeholder="organization/repository" />
@error('repository')
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
<div>
<x-input-label for="branch" :value="__('Branch')" />
<x-text-input wire:model.defer="branch" id="branch" name="branch" type="text" class="mt-1 block w-full" autocomplete="branch" placeholder="main" />
@error('branch')
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
<div class="mt-6">
<label for="composer" class="inline-flex items-center">
<input id="composer" wire:model.defer="composer" 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="composer">
<span class="ml-2 text-sm text-gray-600 dark:text-gray-400">{{ __('Run `composer install --no-dev`') }}</span>
</label>
</div>
</form> </form>
<x-slot name="actions"> <x-slot name="actions">
<x-primary-button form="create-site" wire:loading.attr="disabled">{{ __('Create') }}</x-primary-button> <x-primary-button form="create-site" wire:loading.attr="disabled">{{ __('Create') }}</x-primary-button>

View File

@ -0,0 +1,7 @@
<div>
<x-input-label for="branch" :value="__('Branch')" />
<x-text-input wire:model.defer="inputs.branch" id="branch" name="branch" type="text" class="mt-1 block w-full" autocomplete="branch" placeholder="main" />
@error('branch')
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>

View File

@ -0,0 +1,6 @@
<div class="mt-6">
<label for="composer" class="inline-flex items-center">
<input id="composer" wire:model.defer="inputs.composer" 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="composer">
<span class="ml-2 text-sm text-gray-600 dark:text-gray-400">{{ __('Run `composer install --no-dev`') }}</span>
</label>
</div>

View File

@ -0,0 +1,17 @@
@php
/* @var \App\Models\Server $server */
@endphp
<div>
<x-input-label for="php_version" :value="__('PHP Version')" />
<x-select-input wire:model.defer="inputs.php_version" id="php_version" name="php_version" class="mt-1 w-full">
<option value="" selected>{{ __("Select") }}</option>
@foreach($server->installedPHPVersions() as $version)
<option value="{{ $version }}" @if($version === $inputs['php_version']) selected @endif>
PHP {{ $version }}
</option>
@endforeach
</x-select-input>
@error('php_version')
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>

View File

@ -0,0 +1,7 @@
<div>
<x-input-label for="repository" :value="__('Repository')" />
<x-text-input wire:model.defer="inputs.repository" id="repository" name="repository" type="text" class="mt-1 block w-full" autocomplete="repository" placeholder="organization/repository" />
@error('repository')
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>

View File

@ -0,0 +1,17 @@
<div>
<x-input-label for="source_control" :value="__('Source Control')" />
<div class="flex items-center mt-1">
<x-select-input wire:model="inputs.source_control" id="source_control" name="source_control" class="mt-1 w-full">
<option value="" selected>{{ __("Select") }}</option>
@foreach($sourceControls as $sourceControl)
<option value="{{ $sourceControl->id }}" @if($sourceControl->id === $inputs['source_control']) selected @endif>
{{ $sourceControl->profile }} ({{ $sourceControl->provider }})
</option>
@endforeach
</x-select-input>
<x-secondary-button :href="route('source-controls', ['redirect' => request()->url()])" class="flex-none ml-2">{{ __('Connect') }}</x-secondary-button>
</div>
@error('source_control')
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>

View File

@ -0,0 +1,10 @@
<div>
<x-input-label for="web_directory" :value="__('Web Directory')" />
<x-text-input wire:model.defer="inputs.web_directory" id="web_directory" name="web_directory" type="text" class="mt-1 block w-full" autocomplete="web_directory" />
<x-input-help>
{{ __("For root, leave this blank") }}
</x-input-help>
@error('web_directory')
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>

View File

@ -0,0 +1,11 @@
@include('livewire.sites.partials.create.fields.php-version')
@include('livewire.sites.partials.create.fields.web-directory')
@include('livewire.sites.partials.create.fields.source-control')
@include('livewire.sites.partials.create.fields.repository')
@include('livewire.sites.partials.create.fields.branch')
@include('livewire.sites.partials.create.fields.composer')

View File

@ -0,0 +1,11 @@
@include('livewire.sites.partials.create.fields.php-version')
@include('livewire.sites.partials.create.fields.web-directory')
@include('livewire.sites.partials.create.fields.source-control')
@include('livewire.sites.partials.create.fields.repository')
@include('livewire.sites.partials.create.fields.branch')
@include('livewire.sites.partials.create.fields.composer')

View File

@ -0,0 +1,63 @@
@include('livewire.sites.partials.create.fields.php-version')
<div>
<x-input-label for="title" :value="__('Title')" />
<x-text-input wire:model.defer="inputs.title" id="title" name="title" type="text" class="mt-1 block w-full" autocomplete="branch" />
@error('title')
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-5">
<div>
<x-input-label for="email" :value="__('WP Admin Email')" />
<x-text-input wire:model.defer="inputs.email" id="email" name="email" type="email" class="mt-1 block w-full" autocomplete="email" />
@error('email')
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
<div>
<x-input-label for="username" :value="__('WP Admin Username')" />
<x-text-input wire:model.defer="inputs.username" id="username" name="username" type="text" class="mt-1 block w-full" autocomplete="username" />
@error('username')
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
<div>
<x-input-label for="password" :value="__('WP Admin Password')" />
<x-text-input wire:model.defer="inputs.password" id="password" name="password" type="text" class="mt-1 block w-full" />
@error('title')
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-5">
<div>
<x-input-label for="database" :value="__('Database Name')" />
<x-text-input wire:model.defer="inputs.database" id="database" name="database" type="text" class="mt-1 block w-full" autocomplete="database" />
<x-input-help>{{ __("It will create a database with this name") }}</x-input-help>
@error('database')
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
<div>
<x-input-label for="database" :value="__('Database User')" />
<x-text-input wire:model.defer="inputs.database_user" id="database_user" name="database_user" type="text" class="mt-1 block w-full" autocomplete="database_user" />
<x-input-help>{{ __("It will create a database user with this username") }}</x-input-help>
@error('database_user')
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
<div>
<x-input-label for="password" :value="__('Database Password')" />
<x-text-input wire:model.defer="inputs.database_password" id="database_password" name="database_password" type="text" class="mt-1 block w-full" />
@error('database_password')
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
</div>

View File

@ -0,0 +1,27 @@
<x-card>
<x-slot name="title">{{ __("Update Source Control") }}</x-slot>
<x-slot name="description">{{ __("You can change the source control provider for this site") }}</x-slot>
<form id="update-source-control" wire:submit.prevent="update" class="space-y-6">
<div>
<x-input-label for="provider" :value="__('Source Control')" />
<x-select-input wire:model.defer="source_control" id="source_control" name="source_control" class="mt-1 w-full">
<option value="" selected>{{ __("Select") }}</option>
@foreach(\App\Models\SourceControl::all() as $sourceControl)
<option value="{{ $sourceControl->id }}" @if($sourceControl->id === $source_control) selected @endif>{{ $sourceControl->profile }} ({{ $sourceControl->provider }})</option>
@endforeach
</x-select-input>
@error('source_control')
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
</form>
<x-slot name="actions">
@if (session('status') === 'source-control-updated')
<p class="mr-2">{{ __('Saved') }}</p>
@endif
<x-primary-button form="update-source-control" wire:loading.attr="disabled">{{ __('Save') }}</x-primary-button>
</x-slot>
</x-card>

View File

@ -3,6 +3,8 @@
<livewire:sites.change-php-version :site="$site"/> <livewire:sites.change-php-version :site="$site"/>
<livewire:sites.update-source-control-provider :site="$site"/>
<x-card> <x-card>
<x-slot name="title">{{ __("Delete Site") }}</x-slot> <x-slot name="title">{{ __("Delete Site") }}</x-slot>
<x-slot name="description">{{ __("Permanently delete the site from server") }}</x-slot> <x-slot name="description">{{ __("Permanently delete the site from server") }}</x-slot>

View File

@ -1,19 +1,7 @@
<?php <?php
use Illuminate\Http\Request; // git hook
use App\Http\Controllers\GitHookController;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
/* Route::any('git-hooks', GitHookController::class)->name('git-hooks');
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider and all of them will
| be assigned to the "api" middleware group. Make something great!
|
*/
Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
return $request->user();
});

View File

@ -2,13 +2,16 @@
namespace Tests\Feature\Http; namespace Tests\Feature\Http;
use App\Http\Livewire\Application\AutoDeployment;
use App\Http\Livewire\Application\ChangeBranch; use App\Http\Livewire\Application\ChangeBranch;
use App\Http\Livewire\Application\Deploy; use App\Http\Livewire\Application\Deploy;
use App\Http\Livewire\Application\DeploymentScript; use App\Http\Livewire\Application\DeploymentScript;
use App\Http\Livewire\Application\LaravelApp; use App\Http\Livewire\Application\LaravelApp;
use App\Jobs\Site\UpdateBranch; use App\Jobs\Site\UpdateBranch;
use App\Models\GitHook;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Bus; use Illuminate\Support\Facades\Bus;
use Illuminate\Support\Facades\Http;
use Livewire\Livewire; use Livewire\Livewire;
use Tests\TestCase; use Tests\TestCase;
@ -66,4 +69,45 @@ public function test_change_branch()
Bus::assertDispatched(UpdateBranch::class); Bus::assertDispatched(UpdateBranch::class);
} }
public function test_enable_auto_deployment()
{
Http::fake([
'github.com/*' => Http::response([
'id' => '123',
], 201),
]);
$this->actingAs($this->user);
Livewire::test(AutoDeployment::class, ['site' => $this->site])
->call('enable')
->assertSuccessful();
$this->site->refresh();
$this->assertTrue($this->site->auto_deployment);
}
public function test_disable_auto_deployment()
{
Http::fake([
'github.com/*' => Http::response([], 204),
]);
$this->actingAs($this->user);
GitHook::factory()->create([
'site_id' => $this->site->id,
'source_control_id' => $this->site->source_control_id,
]);
Livewire::test(AutoDeployment::class, ['site' => $this->site])
->call('disable')
->assertSuccessful();
$this->site->refresh();
$this->assertFalse($this->site->auto_deployment);
}
} }

View File

@ -7,6 +7,7 @@
use App\Http\Livewire\Php\InstalledVersions; use App\Http\Livewire\Php\InstalledVersions;
use App\Jobs\Installation\InstallPHP; use App\Jobs\Installation\InstallPHP;
use App\Jobs\Installation\UninstallPHP; use App\Jobs\Installation\UninstallPHP;
use App\Jobs\PHP\InstallPHPExtension;
use App\Jobs\PHP\SetDefaultCli; use App\Jobs\PHP\SetDefaultCli;
use App\Models\Service; use App\Models\Service;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
@ -68,4 +69,42 @@ public function test_change_default_php_cli(): void
Bus::assertDispatched(SetDefaultCli::class); Bus::assertDispatched(SetDefaultCli::class);
} }
public function test_install_extension(): void
{
Bus::fake();
$this->actingAs($this->user);
Livewire::test(InstalledVersions::class, ['server' => $this->server])
->set('extensionId', $this->server->php('8.2')?->id)
->set('extension', 'gmp')
->call('installExtension')
->assertSuccessful();
Bus::assertDispatched(InstallPHPExtension::class);
}
public function test_extension_already_installed(): void
{
Bus::fake();
$this->actingAs($this->user);
$this->server->php('8.2')->update([
'type_data' => [
'extensions' => [
'gmp',
],
],
]);
Livewire::test(InstalledVersions::class, ['server' => $this->server])
->set('extensionId', $this->server->php('8.2')?->id)
->set('extension', 'gmp')
->call('installExtension')
->assertSuccessful();
Bus::assertNotDispatched(InstallPHPExtension::class);
}
} }

View File

@ -9,6 +9,7 @@
use App\Http\Livewire\Sites\CreateSite; use App\Http\Livewire\Sites\CreateSite;
use App\Http\Livewire\Sites\DeleteSite; use App\Http\Livewire\Sites\DeleteSite;
use App\Http\Livewire\Sites\SitesList; use App\Http\Livewire\Sites\SitesList;
use App\Http\Livewire\Sites\UpdateSourceControlProvider;
use App\Jobs\Site\CreateVHost; use App\Jobs\Site\CreateVHost;
use App\Models\Site; use App\Models\Site;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
@ -21,7 +22,10 @@ class SitesTest extends TestCase
{ {
use RefreshDatabase; use RefreshDatabase;
public function test_create_site(): void /**
* @dataProvider create_data
*/
public function test_create_site(array $inputs): void
{ {
Bus::fake(); Bus::fake();
@ -35,15 +39,8 @@ public function test_create_site(): void
]); ]);
Livewire::test(CreateSite::class, ['server' => $this->server]) Livewire::test(CreateSite::class, ['server' => $this->server])
->set('type', SiteType::LARAVEL) ->fill($inputs)
->set('domain', 'example.com') ->set('inputs.source_control', $sourceControl->id)
->set('alias', 'www.example.com')
->set('php_version', '8.2')
->set('web_directory', 'public')
->set('source_control', $sourceControl->id)
->set('repository', 'test/test')
->set('branch', 'main')
->set('composer', true)
->call('create') ->call('create')
->assertSuccessful() ->assertSuccessful()
->assertHasNoErrors(); ->assertHasNoErrors();
@ -109,4 +106,54 @@ public function test_change_php_version(): void
Bus::assertDispatched(\App\Jobs\Site\ChangePHPVersion::class); Bus::assertDispatched(\App\Jobs\Site\ChangePHPVersion::class);
} }
public function test_update_source_control(): void
{
$this->actingAs($this->user);
/** @var \App\Models\SourceControl $gitlab */
$gitlab = \App\Models\SourceControl::factory()->gitlab()->create();
Livewire::test(UpdateSourceControlProvider::class, ['site' => $this->site])
->set('source_control', $gitlab->id)
->call('update')
->assertSuccessful();
$this->site->refresh();
$this->assertEquals($gitlab->id, $this->site->source_control_id);
}
public static function create_data(): array
{
return [
[
[
'inputs.type' => SiteType::LARAVEL,
'inputs.domain' => 'example.com',
'inputs.alias' => 'www.example.com',
'inputs.php_version' => '8.2',
'inputs.web_directory' => 'public',
'inputs.repository' => 'test/test',
'inputs.branch' => 'main',
'inputs.composer' => true,
],
],
[
[
'inputs.type' => SiteType::WORDPRESS,
'inputs.domain' => 'example.com',
'inputs.alias' => 'www.example.com',
'inputs.php_version' => '8.2',
'inputs.title' => 'Example',
'inputs.username' => 'example',
'inputs.email' => 'email@example.com',
'inputs.password' => 'password',
'inputs.database' => 'example',
'inputs.database_user' => 'example',
'inputs.database_password' => 'password',
],
],
];
}
} }

View File

@ -61,7 +61,7 @@ public function test_delete_provider(string $provider): void
->assertSuccessful(); ->assertSuccessful();
$this->assertDatabaseMissing('source_controls', [ $this->assertDatabaseMissing('source_controls', [
'provider' => $provider, 'id' => $sourceControl->id,
]); ]);
} }

View File

@ -7,6 +7,7 @@
use App\Enums\Webserver; use App\Enums\Webserver;
use App\Models\Server; use App\Models\Server;
use App\Models\Site; use App\Models\Site;
use App\Models\SourceControl;
use App\Models\User; use App\Models\User;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase; use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\File;
@ -53,8 +54,12 @@ private function setupServer(): void
private function setupSite(): void private function setupSite(): void
{ {
/** @var SourceControl $sourceControl */
$sourceControl = SourceControl::factory()->github()->create();
$this->site = Site::factory()->create([ $this->site = Site::factory()->create([
'server_id' => $this->server->id, 'server_id' => $this->server->id,
'source_control_id' => $sourceControl->id,
'repository' => 'organization/repository',
]); ]);
} }