Compare commits

...

26 Commits
0.2.0 ... 0.8.0

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

* discord channel

* build assets

* pint
2024-01-07 09:54:08 +01:00
f06b8f7d20 update vhost and bug fix (#87) 2024-01-05 22:07:45 +01:00
f120a570e8 Update issue templates 2024-01-05 20:38:48 +01:00
2d7f225ff2 define storage instances with phpdoc 2024-01-03 22:31:50 +01:00
31bd146239 small typo fix 2024-01-02 20:18:25 +01:00
10a6bb57a8 adding Projects feature (#85) 2024-01-02 19:50:49 +01:00
fd2244d382 update composer (#84)
* update composer
log viewer
code style format

* fix composer
2024-01-01 22:05:31 +01:00
551f1ce40e fix issue with php site creation 2024-01-01 21:47:05 +01:00
1ce92d9361 fix issue with php site creation 2024-01-01 21:44:49 +01:00
ec6e55e30c Update README.md 2024-01-01 19:49:51 +01:00
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
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
202 changed files with 4931 additions and 1947 deletions

View File

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

View File

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

View File

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

27
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,27 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Additional context**
Add any other context about the problem here.

View File

@ -0,0 +1,12 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: feature
assignees: ''
---
To request a feature or suggest an idea please add it to the feedback boards
https://features.vitodeploy.com/

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

@ -10,9 +10,21 @@ ## Documentation
https://vitodeploy.com
## Feedbacks
https://features.vitodeploy.com
## Roadmap
https://https://features.vitodeploy.com/roadmap
## 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

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

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

View File

@ -17,9 +17,12 @@ public function update(Service $service, string $ini): void
{
$tmpName = Str::random(10).strtotime('now');
try {
Storage::disk('local')->put($tmpName, $ini);
/** @var \Illuminate\Filesystem\FilesystemAdapter $storageDisk */
$storageDisk = Storage::disk('local');
$storageDisk->put($tmpName, $ini);
$service->server->ssh('root')->upload(
Storage::disk('local')->path($tmpName),
$storageDisk->path($tmpName),
"/etc/php/$service->version/cli/php.ini"
);
$this->deleteTempFile($tmpName);

View File

@ -0,0 +1,36 @@
<?php
namespace App\Actions\Projects;
use App\Models\Project;
use App\Models\User;
use Illuminate\Support\Facades\Validator;
class CreateProject
{
public function create(User $user, array $input): Project
{
$this->validate($user, $input);
$project = new Project([
'user_id' => $user->id,
'name' => $input['name'],
]);
$project->save();
return $project;
}
private function validate(User $user, array $input): void
{
Validator::make($input, [
'name' => [
'required',
'string',
'max:255',
'unique:projects,name,NULL,id,user_id,'.$user->id,
],
])->validate();
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace App\Actions\Projects;
use App\Models\Project;
use App\Models\User;
use Illuminate\Validation\ValidationException;
class DeleteProject
{
public function delete(User $user, int $projectId): void
{
/** @var Project $project */
$project = $user->projects()->findOrFail($projectId);
if ($user->projects()->count() === 1) {
throw ValidationException::withMessages([
'project' => __('Cannot delete the last project.'),
]);
}
if ($user->current_project_id == $project->id) {
/** @var Project $randomProject */
$randomProject = $user->projects()->where('id', '!=', $project->id)->first();
$user->current_project_id = $randomProject->id;
$user->save();
}
$project->delete();
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace App\Actions\Projects;
use App\Models\Project;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
class UpdateProject
{
public function update(Project $project, array $input): Project
{
$this->validate($project, $input);
$project->name = $input['name'];
$project->save();
return $project;
}
private function validate(Project $project, array $input): void
{
Validator::make($input, [
'name' => [
'required',
'string',
'max:255',
Rule::unique('projects')->ignore($project->id),
],
])->validate();
}
}

View File

@ -26,6 +26,7 @@ public function create(User $creator, array $input): Server
$this->validateInputs($input);
$server = new Server([
'project_id' => $creator->currentProject->id,
'user_id' => $creator->id,
'name' => $input['name'],
'ssh_user' => config('core.server_providers_default_user')[$input['provider']][$input['os']],

View File

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

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
$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

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

View File

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

View File

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

View File

@ -15,13 +15,13 @@ public function create(
?int $siteId = null
): void;
public function delete(int $id, int $siteId = null): void;
public function delete(int $id, ?int $siteId = null): void;
public function restart(int $id, int $siteId = null): void;
public function restart(int $id, ?int $siteId = null): void;
public function stop(int $id, int $siteId = null): void;
public function stop(int $id, ?int $siteId = null): void;
public function start(int $id, int $siteId = null): void;
public function start(int $id, ?int $siteId = null): void;
public function getLogs(string $logPath): string;
}

View File

@ -12,7 +12,7 @@ public function credentialData(array $input): array;
public function data(array $input): array;
public function connect(array $credentials = null): bool;
public function connect(?array $credentials = null): bool;
public function plans(): array;

View File

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

View File

@ -6,7 +6,7 @@ interface SourceControlProvider
{
public function connect(): bool;
public function getRepo(string $repo = null): mixed;
public function getRepo(?string $repo = null): mixed;
public function fullRepoUrl(string $repo, string $key): string;

View File

@ -9,7 +9,9 @@ interface Webserver
{
public function createVHost(Site $site): void;
public function updateVHost(Site $site): void;
public function updateVHost(Site $site, bool $noSSL = false, ?string $vhost = null): void;
public function getVHost(Site $site): string;
public function deleteSite(Site $site): void;

View File

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

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 BITBUCKET = 'bitbucket';
const CUSTOM = 'custom';
}

View File

@ -7,7 +7,7 @@
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');
}

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

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

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

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

View File

@ -31,7 +31,7 @@ class SSH
protected PrivateKey $privateKey;
public function init(Server $server, string $asUser = null): self
public function init(Server $server, ?string $asUser = null): self
{
$this->connection = null;
$this->log = null;
@ -87,7 +87,7 @@ public function connect(bool $sftp = false): void
/**
* @throws Throwable
*/
public function exec(string|array|SSHCommand $commands, string $log = '', int $siteId = null): string
public function exec(string|array|SSHCommand $commands, string $log = '', ?int $siteId = null): string
{
if ($log) {
$this->setLog($log, $siteId);

View File

@ -0,0 +1,42 @@
<?php
namespace App\Http\Controllers;
use App\Exceptions\SourceControlIsNotConnected;
use App\Facades\Notifier;
use App\Models\GitHook;
use App\Notifications\SourceControlDisconnected;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Throwable;
class GitHookController extends Controller
{
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) {
Notifier::send($gitHook->sourceControl, new SourceControlDisconnected($gitHook->sourceControl));
} catch (Throwable $e) {
Log::error('git-hook-exception', (array) $e);
}
}
}
return response()->json([
'success' => true,
]);
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Http\Controllers;
use App\Models\Project;
use App\Models\User;
use Illuminate\Contracts\View\View;
class ProjectController extends Controller
{
public function index(): View
{
return view('projects.index');
}
public function switch($projectId)
{
/** @var User $user */
$user = auth()->user();
/** @var Project $project */
$project = $user->projects()->findOrFail($projectId);
$user->current_project_id = $project->id;
$user->save();
return redirect()->route('servers');
}
}

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 HasToast;
use RefreshComponentOnBroadcast;
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

@ -11,8 +11,8 @@
class Deploy extends Component
{
use RefreshComponentOnBroadcast;
use HasToast;
use RefreshComponentOnBroadcast;
public Site $site;

View File

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

View File

@ -10,8 +10,8 @@
class DeploymentsList extends Component
{
use RefreshComponentOnBroadcast;
use HasCustomPaginationView;
use RefreshComponentOnBroadcast;
public Site $site;

View File

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

View File

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

View File

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

View File

@ -0,0 +1,37 @@
<?php
namespace App\Http\Livewire\Projects;
use App\Traits\HasToast;
use App\Traits\RefreshComponentOnBroadcast;
use Illuminate\Contracts\View\View;
use Livewire\Component;
class CreateProject extends Component
{
use HasToast;
use RefreshComponentOnBroadcast;
public bool $open = false;
public array $inputs = [];
public function create(): void
{
app(\App\Actions\Projects\CreateProject::class)
->create(auth()->user(), $this->inputs);
$this->emitTo(ProjectsList::class, '$refresh');
$this->dispatchBrowserEvent('created', true);
}
public function render(): View
{
if (request()->query('create')) {
$this->open = true;
}
return view('livewire.projects.create-project');
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace App\Http\Livewire\Projects;
use App\Actions\Projects\UpdateProject;
use App\Models\Project;
use App\Traits\RefreshComponentOnBroadcast;
use Illuminate\Contracts\View\View;
use Livewire\Component;
class EditProject extends Component
{
use RefreshComponentOnBroadcast;
public Project $project;
public array $inputs = [];
public function save(): void
{
app(UpdateProject::class)->update($this->project, $this->inputs);
$this->redirect(route('projects'));
}
public function mount(): void
{
$this->inputs = [
'name' => $this->project->name,
];
}
public function render(): View
{
return view('livewire.projects.edit-project');
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace App\Http\Livewire\Projects;
use App\Actions\Projects\DeleteProject;
use App\Traits\HasToast;
use App\Traits\RefreshComponentOnBroadcast;
use Illuminate\Contracts\View\View;
use Illuminate\Validation\ValidationException;
use Livewire\Component;
class ProjectsList extends Component
{
use HasToast;
use RefreshComponentOnBroadcast;
protected $listeners = [
'$refresh',
];
public int $deleteId;
public function delete(): void
{
try {
app(DeleteProject::class)->delete(auth()->user(), $this->deleteId);
$this->redirect(route('projects'));
return;
} catch (ValidationException $e) {
$this->toast()->error($e->getMessage());
}
}
public function render(): View
{
return view('livewire.projects.projects-list', [
'projects' => auth()->user()->projects()->orderByDesc('id')->get(),
]);
}
}

View File

@ -11,8 +11,8 @@
class LogsList extends Component
{
use RefreshComponentOnBroadcast;
use HasCustomPaginationView;
use RefreshComponentOnBroadcast;
public ?int $count = null;

View File

@ -2,7 +2,7 @@
namespace App\Http\Livewire\Servers;
use App\Models\Server;
use App\Models\User;
use App\Traits\RefreshComponentOnBroadcast;
use Illuminate\Contracts\View\View;
use Livewire\Component;
@ -13,8 +13,12 @@ class ServersList extends Component
public function render(): View
{
/** @var User $user */
$user = auth()->user();
$servers = $user->currentProject->servers()->orderByDesc('created_at')->get();
return view('livewire.servers.servers-list', [
'servers' => Server::all(),
'servers' => $servers,
]);
}
}

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

View File

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

@ -0,0 +1,41 @@
<?php
namespace App\Http\Livewire\Sites;
use App\Models\Site;
use App\Traits\HasToast;
use App\Traits\RefreshComponentOnBroadcast;
use Illuminate\Contracts\View\View;
use Livewire\Component;
use Throwable;
class UpdateVHost extends Component
{
use HasToast;
use RefreshComponentOnBroadcast;
public Site $site;
public string $vHost = 'Loading...';
public function loadVHost(): void
{
$this->vHost = $this->site->server->webserver()->handler()->getVHost($this->site);
}
public function update(): void
{
try {
$this->site->server->webserver()->handler()->updateVHost($this->site, false, $this->vHost);
$this->toast()->success('VHost updated successfully!');
} catch (Throwable $e) {
$this->toast()->error($e->getMessage());
}
}
public function render(): View
{
return view('livewire.sites.update-v-host');
}
}

View File

@ -10,8 +10,8 @@
class SslsList extends Component
{
use RefreshComponentOnBroadcast;
use HasToast;
use RefreshComponentOnBroadcast;
public Site $site;

View File

@ -13,7 +13,7 @@ class Initialize extends InstallationJob
protected ?string $asUser;
public function __construct(Server $server, string $asUser = null)
public function __construct(Server $server, ?string $asUser = null)
{
$this->server = $server->refresh();
$this->asUser = $asUser;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -57,19 +57,19 @@ public function server(): BelongsTo
/**
* 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
*/
public function deleteFromServer(): void
public function deleteFromServer(string $queue = 'ssh'): void
{
$this->status = DatabaseStatus::DELETING;
$this->save();
dispatch(new DeleteFromServer($this))->onConnection('ssh');
dispatch(new DeleteFromServer($this))->onConnection($queue);
}
public function backups(): HasMany

View File

@ -54,17 +54,17 @@ public function scopeHasDatabase(Builder $query, string $databaseName): Builder
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->save();
dispatch(new DeleteFromServer($this))->onConnection('ssh');
dispatch(new DeleteFromServer($this))->onConnection($queue);
}
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

View File

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

View File

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

56
app/Models/Project.php Normal file
View File

@ -0,0 +1,56 @@
<?php
namespace App\Models;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
/**
* @property int $id
* @property int $user_id
* @property string $name
* @property Carbon $created_at
* @property Carbon $updated_at
* @property User $user
* @property Collection<Server> $servers
* @property Collection<NotificationChannel> $notificationChannels
*/
class Project extends Model
{
use HasFactory;
protected $fillable = [
'user_id',
'name',
];
public static function boot(): void
{
parent::boot();
static::deleting(function (Project $project) {
$project->servers()->each(function (Server $server) {
$server->delete();
});
});
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function servers(): HasMany
{
return $this->hasMany(Server::class);
}
public function notificationChannels(): HasMany
{
return $this->hasMany(NotificationChannel::class);
}
}

View File

@ -4,20 +4,24 @@
use App\Contracts\ServerType;
use App\Enums\ServerStatus;
use App\Facades\Notifier;
use App\Facades\SSH;
use App\Jobs\Installation\Upgrade;
use App\Jobs\Server\CheckConnection;
use App\Jobs\Server\RebootServer;
use App\Notifications\ServerInstallationStarted;
use App\Support\Testing\SSHFake;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Filesystem\FilesystemAdapter;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
/**
* @property int $project_id
* @property int $user_id
* @property string $name
* @property string $ssh_user
@ -38,6 +42,7 @@
* @property int $security_updates
* @property int $progress
* @property string $progress_step
* @property Project $project
* @property User $creator
* @property ServerProvider $serverProvider
* @property ServerLog[] $logs
@ -59,6 +64,7 @@ class Server extends AbstractModel
use HasFactory;
protected $fillable = [
'project_id',
'user_id',
'name',
'ssh_user',
@ -82,6 +88,7 @@ class Server extends AbstractModel
];
protected $casts = [
'project_id' => 'integer',
'user_id' => 'integer',
'type_data' => 'json',
'port' => 'integer',
@ -106,7 +113,9 @@ public static function boot(): void
$site->delete();
});
$server->provider()->delete();
$server->logs()->delete();
$server->logs()->each(function (ServerLog $log) {
$log->delete();
});
$server->services()->delete();
$server->databases()->delete();
$server->databaseUsers()->delete();
@ -125,6 +134,11 @@ public static function boot(): void
});
}
public function project(): BelongsTo
{
return $this->belongsTo(Project::class, 'project_id');
}
public function creator(): BelongsTo
{
return $this->belongsTo(User::class, 'user_id');
@ -230,10 +244,10 @@ public function getServiceByUnit($unit): ?Service
public function install(): void
{
$this->type()->install();
// $this->team->notify(new ServerInstallationStarted($this));
Notifier::send($this, new ServerInstallationStarted($this));
}
public function ssh(string $user = null): \App\Helpers\SSH|SSHFake
public function ssh(?string $user = null): \App\Helpers\SSH|SSHFake
{
return SSH::init($this, $user);
}
@ -263,7 +277,7 @@ public function provider(): \App\Contracts\ServerProvider
return new $providerClass($this);
}
public function webserver(string $version = null): ?Service
public function webserver(?string $version = null): ?Service
{
if (! $version) {
return $this->defaultService('webserver');
@ -272,7 +286,7 @@ public function webserver(string $version = null): ?Service
return $this->service('webserver', $version);
}
public function database(string $version = null): ?Service
public function database(?string $version = null): ?Service
{
if (! $version) {
return $this->defaultService('database');
@ -281,7 +295,7 @@ public function database(string $version = null): ?Service
return $this->service('database', $version);
}
public function firewall(string $version = null): ?Service
public function firewall(?string $version = null): ?Service
{
if (! $version) {
return $this->defaultService('firewall');
@ -290,7 +304,7 @@ public function firewall(string $version = null): ?Service
return $this->service('firewall', $version);
}
public function processManager(string $version = null): ?Service
public function processManager(?string $version = null): ?Service
{
if (! $version) {
return $this->defaultService('process_manager');
@ -299,7 +313,7 @@ public function processManager(string $version = null): ?Service
return $this->service('process_manager', $version);
}
public function php(string $version = null): ?Service
public function php(?string $version = null): ?Service
{
if (! $version) {
return $this->defaultService('php');
@ -334,10 +348,13 @@ public function sshKey(): array
];
}
/** @var FilesystemAdapter $storageDisk */
$storageDisk = Storage::disk(config('core.key_pairs_disk'));
return [
'public_key' => Str::replace("\n", '', Storage::disk(config('core.key_pairs_disk'))->get($this->id.'.pub')),
'public_key_path' => Storage::disk(config('core.key_pairs_disk'))->path($this->id.'.pub'),
'private_key_path' => Storage::disk(config('core.key_pairs_disk'))->path((string) $this->id),
'public_key_path' => $storageDisk->path($this->id.'.pub'),
'private_key_path' => $storageDisk->path((string) $this->id),
];
}

View File

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

View File

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

View File

@ -6,12 +6,15 @@
use App\Enums\DeploymentStatus;
use App\Enums\SiteStatus;
use App\Enums\SslStatus;
use App\Exceptions\FailedToDeployGitHook;
use App\Events\Broadcast;
use App\Exceptions\SourceControlIsNotConnected;
use App\Facades\Notifier;
use App\Jobs\Site\ChangePHPVersion;
use App\Jobs\Site\Deploy;
use App\Jobs\Site\DeployEnv;
use App\Jobs\Site\UpdateBranch;
use App\Notifications\SiteInstallationFailed;
use App\Notifications\SiteInstallationSucceed;
use Exception;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
@ -19,7 +22,8 @@
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Throwable;
/**
@ -341,22 +345,16 @@ public function getWebDirectoryPathAttribute(): string
/**
* @throws SourceControlIsNotConnected
* @throws ValidationException
* @throws FailedToDeployGitHook
* @throws Throwable
*/
public function enableAutoDeployment(): void
{
if ($this->gitHook) {
throw ValidationException::withMessages([
'auto_deployment' => __('Auto deployment already enabled'),
])->errorBag('auto_deployment');
return;
}
if (! $this->sourceControl()) {
throw ValidationException::withMessages([
'auto_deployment' => __('Your application does not use any source controls'),
])->errorBag('auto_deployment');
throw new SourceControlIsNotConnected($this->source_control);
}
try {
@ -364,7 +362,7 @@ public function enableAutoDeployment(): void
$gitHook = new GitHook([
'site_id' => $this->id,
'source_control_id' => $this->sourceControl()->id,
'secret' => generate_uid(),
'secret' => Str::uuid()->toString(),
'actions' => ['deploy'],
'events' => ['push'],
]);
@ -399,4 +397,49 @@ public function getSshKeyNameAttribute(): string
{
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,
])
);
Notifier::send($this, new SiteInstallationSucceed($this));
}
/**
* @throws Throwable
*/
public function installationFailed(Throwable $e): void
{
$this->update([
'status' => SiteStatus::INSTALLATION_FAILED,
]);
event(
new Broadcast('install-site-failed', [
'site' => $this,
])
);
Notifier::send($this, new SiteInstallationFailed($this));
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

@ -33,7 +33,7 @@ public function provider(): SourceControlProvider
return new $providerClass($this);
}
public function getRepo(string $repo = null): ?array
public function getRepo(?string $repo = null): ?array
{
return $this->provider()->getRepo($repo);
}

View File

@ -28,6 +28,9 @@
* @property Collection $tokens
* @property string $profile_photo_url
* @property string $timezone
* @property int $current_project_id
* @property Project $currentProject
* @property Collection<Project> $projects
*/
class User extends Authenticatable
{
@ -41,6 +44,7 @@ class User extends Authenticatable
'email',
'password',
'timezone',
'current_project_id',
];
protected $hidden = [
@ -53,6 +57,20 @@ class User extends Authenticatable
protected $appends = [
];
public static function boot(): void
{
parent::boot();
static::created(function (User $user) {
$user->createDefaultProject();
});
}
public function servers(): HasMany
{
return $this->hasMany(Server::class);
}
public function sshKeys(): HasMany
{
return $this->hasMany(SshKey::class);
@ -105,4 +123,36 @@ public function connectedSourceControls(): array
return $connectedSourceControls;
}
public function projects(): HasMany
{
return $this->hasMany(Project::class);
}
public function currentProject(): HasOne
{
return $this->HasOne(Project::class, 'id', 'current_project_id');
}
public function isMemberOfProject(Project $project): bool
{
return $project->user_id === $this->id;
}
public function createDefaultProject(): Project
{
$project = $this->projects()->first();
if (! $project) {
$project = new Project();
$project->user_id = $this->id;
$project->name = 'Default';
$project->save();
}
$this->current_project_id = $project->id;
$this->save();
return $project;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,26 @@
<?php
namespace App\SSHCommands\Nginx;
use App\SSHCommands\Command;
use Illuminate\Support\Facades\File;
class GetNginxVHostCommand extends Command
{
public function __construct(
protected string $domain
) {
}
public function file(): string
{
return File::get(resource_path('commands/webserver/nginx/get-vhost.sh'));
}
public function content(): string
{
return str($this->file())
->replace('__domain__', $this->domain)
->toString();
}
}

View File

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

View File

@ -10,13 +10,15 @@ abstract class AbstractProvider implements ServerProvider
{
protected ?Server $server;
public function __construct(Server $server = null)
public function __construct(?Server $server = null)
{
$this->server = $server;
}
protected function generateKeyPair(): void
{
generate_key_pair(Storage::disk(config('core.key_pairs_disk'))->path((string) $this->server->id));
/** @var \Illuminate\Filesystem\FilesystemAdapter $storageDisk */
$storageDisk = Storage::disk(config('core.key_pairs_disk'));
generate_key_pair($storageDisk->path((string) $this->server->id));
}
}

View File

@ -42,7 +42,7 @@ public function data(array $input): array
return [];
}
public function connect(array $credentials = null): bool
public function connect(?array $credentials = null): bool
{
return true;
}
@ -59,13 +59,15 @@ public function regions(): array
public function create(): void
{
/** @var \Illuminate\Filesystem\FilesystemAdapter $storageDisk */
$storageDisk = Storage::disk(config('core.key_pairs_disk'));
File::copy(
storage_path(config('core.ssh_private_key_name')),
Storage::disk(config('core.key_pairs_disk'))->path($this->server->id)
$storageDisk->path($this->server->id)
);
File::copy(
storage_path(config('core.ssh_public_key_name')),
Storage::disk(config('core.key_pairs_disk'))->path($this->server->id.'.pub')
$storageDisk->path($this->server->id.'.pub')
);
}

View File

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

View File

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

View File

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

View File

@ -4,6 +4,8 @@
use App\Exceptions\CouldNotConnectToProvider;
use App\Exceptions\ServerProviderError;
use App\Facades\Notifier;
use App\Notifications\FailedToDeleteServerFromProvider;
use Exception;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
@ -59,7 +61,7 @@ public function data(array $input): array
/**
* @throws CouldNotConnectToProvider
*/
public function connect(array $credentials = null): bool
public function connect(?array $credentials = null): bool
{
$connect = Http::withToken($credentials['token'])->get($this->apiUrl.'/account');
if (! $connect->ok()) {
@ -85,7 +87,9 @@ public function regions(): array
public function create(): void
{
// generate key pair
generate_key_pair(Storage::disk(config('core.key_pairs_disk'))->path((string) $this->server->id));
/** @var \Illuminate\Filesystem\FilesystemAdapter $storageDisk */
$storageDisk = Storage::disk(config('core.key_pairs_disk'));
generate_key_pair($storageDisk->path((string) $this->server->id));
$createSshKey = Http::withToken($this->server->serverProvider->credentials['token'])
->post($this->apiUrl.'/ssh-keys', [
@ -142,10 +146,9 @@ public function delete(): void
$delete = Http::withToken($this->server->serverProvider->credentials['token'])
->delete($this->apiUrl.'/instances/'.$this->server->provider_data['instance_id']);
/** @todo notify */
// if (! $delete->ok()) {
// $this->server->team->notify(new FailedToDeleteServerFromProvider($this->server));
// }
if (! $delete->ok()) {
Notifier::send($this->server, new FailedToDeleteServerFromProvider($this->server));
}
}
}
}

View File

@ -16,7 +16,7 @@ public function __construct(Server $server)
$this->server = $server;
}
protected function progress(int $percentage, string $step = null): Closure
protected function progress(int $percentage, ?string $step = null): Closure
{
return function () use ($percentage, $step) {
$this->server->progress = $percentage;

View File

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

View File

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

View File

@ -47,7 +47,7 @@ public function create(
/**
* @throws Throwable
*/
public function delete(int $id, int $siteId = null): void
public function delete(int $id, ?int $siteId = null): void
{
$this->service->server->ssh()->exec(
new DeleteWorkerCommand($id),
@ -59,7 +59,7 @@ public function delete(int $id, int $siteId = null): void
/**
* @throws Throwable
*/
public function restart(int $id, int $siteId = null): void
public function restart(int $id, ?int $siteId = null): void
{
$this->service->server->ssh()->exec(
new RestartWorkerCommand($id),
@ -71,7 +71,7 @@ public function restart(int $id, int $siteId = null): void
/**
* @throws Throwable
*/
public function stop(int $id, int $siteId = null): void
public function stop(int $id, ?int $siteId = null): void
{
$this->service->server->ssh()->exec(
new StopWorkerCommand($id),
@ -83,7 +83,7 @@ public function stop(int $id, int $siteId = null): void
/**
* @throws Throwable
*/
public function start(int $id, int $siteId = null): void
public function start(int $id, ?int $siteId = null): void
{
$this->service->server->ssh()->exec(
new StartWorkerCommand($id),

View File

@ -9,6 +9,7 @@
use App\SSHCommands\Nginx\ChangeNginxPHPVersionCommand;
use App\SSHCommands\Nginx\CreateNginxVHostCommand;
use App\SSHCommands\Nginx\DeleteNginxSiteCommand;
use App\SSHCommands\Nginx\GetNginxVHostCommand;
use App\SSHCommands\Nginx\UpdateNginxRedirectsCommand;
use App\SSHCommands\Nginx\UpdateNginxVHostCommand;
use App\SSHCommands\SSL\CreateCustomSSLCommand;
@ -39,19 +40,30 @@ public function createVHost(Site $site): void
/**
* @throws Throwable
*/
public function updateVHost(Site $site, bool $noSSL = false): void
public function updateVHost(Site $site, bool $noSSL = false, ?string $vhost = null): void
{
$this->service->server->ssh()->exec(
new UpdateNginxVHostCommand(
$site->domain,
$site->path,
$this->generateVhost($site, $noSSL)
$vhost ?? $this->generateVhost($site, $noSSL)
),
'update-vhost',
$site->id
);
}
public function getVHost(Site $site): string
{
return $this->service->server->ssh()->exec(
new GetNginxVHostCommand(
$site->domain
),
'get-vhost',
$site->id
);
}
/**
* @throws Throwable
*/

Some files were not shown because too many files have changed in this diff Show More