Compare commits

...

19 Commits
0.4.0 ... 0.9.1

Author SHA1 Message Date
55bf8b8ecf fix ipv6 (#96) 2024-01-27 21:34:36 +01:00
0420babdef Bump vite from 4.2.3 to 4.5.2 (#95)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 4.2.3 to 4.5.2.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v4.5.2/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v4.5.2/packages/vite)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-27 21:09:10 +01:00
1c3d78a5ed installation without domain 2024-01-21 21:53:26 +01:00
8665435bc4 add blank php site (#94)
* add blank php site

* fix frontend

* fix source control check
2024-01-18 19:45:58 +01:00
0ec6a9dea2 fix db transaction usage 2024-01-14 12:56:25 +01:00
bdfda05398 update Roadmap URLs (#91) 2024-01-14 08:57:00 +01:00
919cdc6892 Bump follow-redirects from 1.15.2 to 1.15.5 (#90)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.2 to 1.15.5.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.2...v1.15.5)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-13 19:36:05 +01:00
902548e463 Fix roadmap link in README (#89) 2024-01-10 20:41:18 +01:00
2462b31f3b typo fix 2024-01-08 16:25:12 +01:00
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
151 changed files with 4189 additions and 1828 deletions

View File

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

View File

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

View File

@ -26,8 +26,8 @@ REDIS_PASSWORD=null
REDIS_PORT=6379 REDIS_PORT=6379
MAIL_MAILER=smtp MAIL_MAILER=smtp
MAIL_HOST=mailhog MAIL_HOST=
MAIL_PORT=1025 MAIL_PORT=
MAIL_USERNAME=null MAIL_USERNAME=null
MAIL_PASSWORD=null MAIL_PASSWORD=null
MAIL_ENCRYPTION=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/

View File

@ -10,6 +10,14 @@ ## Documentation
https://vitodeploy.com https://vitodeploy.com
## Feedbacks
https://vitodeploy.featurebase.app
## Roadmap
https://vitodeploy.featurebase.app/roadmap
## Contribution ## Contribution
Please read the contribution guide [Here](/CONTRIBUTING.md) Please read the contribution guide [Here](/CONTRIBUTING.md)

View File

@ -21,16 +21,25 @@ public function add(User $user, array $input): void
'label' => $input['label'], 'label' => $input['label'],
]); ]);
$this->validateType($channel, $input); $this->validateType($channel, $input);
$channel->data = $channel->provider()->data($input); $channel->data = $channel->provider()->createData($input);
$channel->save(); $channel->save();
if (! $channel->provider()->connect()) { if (! $channel->provider()->connect()) {
$channel->delete(); $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([ throw ValidationException::withMessages([
'provider' => __('Could not connect'), '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 protected function validateType(NotificationChannel $channel, array $input): void
{ {
Validator::make($input, $channel->provider()->validationRules()) Validator::make($input, $channel->provider()->createRules($input))
->validate(); ->validate();
} }
} }

View File

@ -17,9 +17,12 @@ public function update(Service $service, string $ini): void
{ {
$tmpName = Str::random(10).strtotime('now'); $tmpName = Str::random(10).strtotime('now');
try { 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( $service->server->ssh('root')->upload(
Storage::disk('local')->path($tmpName), $storageDisk->path($tmpName),
"/etc/php/$service->version/cli/php.ini" "/etc/php/$service->version/cli/php.ini"
); );
$this->deleteTempFile($tmpName); $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); $this->validateInputs($input);
$server = new Server([ $server = new Server([
'project_id' => $creator->currentProject->id,
'user_id' => $creator->id, 'user_id' => $creator->id,
'name' => $input['name'], 'name' => $input['name'],
'ssh_user' => config('core.server_providers_default_user')[$input['provider']][$input['os']], 'ssh_user' => config('core.server_providers_default_user')[$input['provider']][$input['os']],
@ -43,9 +44,8 @@ public function create(User $creator, array $input): Server
'progress_step' => 'Initializing', 'progress_step' => 'Initializing',
]); ]);
DB::beginTransaction();
try { try {
DB::beginTransaction();
if ($server->provider != 'custom') { if ($server->provider != 'custom') {
$server->provider_id = $input['server_provider']; $server->provider_id = $input['server_provider'];
} }
@ -118,7 +118,6 @@ private function validateInputs(array $input): void
if ($input['provider'] == 'custom') { if ($input['provider'] == 'custom') {
$rules['ip'] = [ $rules['ip'] = [
'required', 'required',
'ip',
new RestrictedIPAddressesRule(), new RestrictedIPAddressesRule(),
]; ];
$rules['port'] = [ $rules['port'] = [

View File

@ -23,9 +23,8 @@ public function create(Server $server, array $input): Site
{ {
$this->validateInputs($server, $input); $this->validateInputs($server, $input);
DB::beginTransaction();
try { try {
DB::beginTransaction();
$site = new Site([ $site = new Site([
'server_id' => $server->id, 'server_id' => $server->id,
'type' => $input['type'], 'type' => $input['type'],

View File

@ -2,9 +2,17 @@
namespace App\Contracts; namespace App\Contracts;
use Illuminate\Notifications\Messages\MailMessage;
interface Notification 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 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 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 ?int $siteId = null
): void; ): 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; 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 data(array $input): array;
public function connect(array $credentials = null): bool; public function connect(?array $credentials = null): bool;
public function plans(): array; public function plans(): array;

View File

@ -6,7 +6,7 @@ interface SourceControlProvider
{ {
public function connect(): bool; 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; public function fullRepoUrl(string $repo, string $key): string;

View File

@ -9,7 +9,9 @@ interface Webserver
{ {
public function createVHost(Site $site): void; 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; public function deleteSite(Site $site): void;

View File

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

View File

@ -8,6 +8,8 @@ final class SiteType extends Enum
{ {
const PHP = 'php'; const PHP = 'php';
const PHP_BLANK = 'php-blank';
const LARAVEL = 'laravel'; const LARAVEL = 'laravel';
const WORDPRESS = 'wordpress'; const WORDPRESS = 'wordpress';

View File

@ -7,7 +7,7 @@
class SourceControlIsNotConnected extends Exception class SourceControlIsNotConnected extends Exception
{ {
public function __construct(protected SourceControl|string|null $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');
} }

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; 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->connection = null;
$this->log = null; $this->log = null;
@ -64,11 +64,15 @@ public function setLog(string $logType, $siteId = null): void
*/ */
public function connect(bool $sftp = false): void public function connect(bool $sftp = false): void
{ {
$ip = $this->server->ip;
if (str($ip)->contains(':')) {
$ip = '['.$ip.']';
}
try { try {
if ($sftp) { if ($sftp) {
$this->connection = new SFTP($this->server->ip, $this->server->port); $this->connection = new SFTP($ip, $this->server->port);
} else { } else {
$this->connection = new SSH2($this->server->ip, $this->server->port); $this->connection = new SSH2($ip, $this->server->port);
} }
$login = $this->connection->login($this->user, $this->privateKey); $login = $this->connection->login($this->user, $this->privateKey);
@ -87,7 +91,7 @@ public function connect(bool $sftp = false): void
/** /**
* @throws Throwable * @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) { if ($log) {
$this->setLog($log, $siteId); $this->setLog($log, $siteId);

View File

@ -3,8 +3,11 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Exceptions\SourceControlIsNotConnected; use App\Exceptions\SourceControlIsNotConnected;
use App\Facades\Notifier;
use App\Models\GitHook; use App\Models\GitHook;
use App\Notifications\SourceControlDisconnected;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Throwable; use Throwable;
class GitHookController extends Controller class GitHookController extends Controller
@ -25,7 +28,7 @@ public function __invoke(Request $request)
try { try {
$gitHook->site->deploy(); $gitHook->site->deploy();
} catch (SourceControlIsNotConnected) { } catch (SourceControlIsNotConnected) {
// TODO: send notification Notifier::send($gitHook->sourceControl, new SourceControlDisconnected($gitHook->sourceControl));
} catch (Throwable $e) { } catch (Throwable $e) {
Log::error('git-hook-exception', (array) $e); Log::error('git-hook-exception', (array) $e);
} }

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

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

View File

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

View File

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

View File

@ -0,0 +1,19 @@
<?php
namespace App\Http\Livewire\Application;
use App\Models\Site;
use App\Traits\RefreshComponentOnBroadcast;
use Livewire\Component;
class PhpBlankApp extends Component
{
use RefreshComponentOnBroadcast;
public Site $site;
public function render()
{
return view('livewire.application.php-blank-app');
}
}

View File

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

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 class LogsList extends Component
{ {
use RefreshComponentOnBroadcast;
use HasCustomPaginationView; use HasCustomPaginationView;
use RefreshComponentOnBroadcast;
public ?int $count = null; public ?int $count = null;

View File

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

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 class SslsList extends Component
{ {
use RefreshComponentOnBroadcast;
use HasToast; use HasToast;
use RefreshComponentOnBroadcast;
public Site $site; public Site $site;

View File

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

View File

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

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

@ -69,8 +69,8 @@ public function deployHook(): void
*/ */
public function destroyHook(): void public function destroyHook(): void
{ {
DB::beginTransaction();
try { try {
DB::beginTransaction();
$this->sourceControl->provider()->destroyHook($this->site->repository, $this->hook_id); $this->sourceControl->provider()->destroyHook($this->site->repository, $this->hook_id);
$this->delete(); $this->delete();
DB::commit(); DB::commit();

View File

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

View File

@ -34,6 +34,17 @@ class ServerLog extends AbstractModel
'site_id' => 'integer', '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 public function getRouteKey(): string
{ {
return 'log'; return 'log';

View File

@ -8,10 +8,13 @@
use App\Enums\SslStatus; use App\Enums\SslStatus;
use App\Events\Broadcast; use App\Events\Broadcast;
use App\Exceptions\SourceControlIsNotConnected; use App\Exceptions\SourceControlIsNotConnected;
use App\Facades\Notifier;
use App\Jobs\Site\ChangePHPVersion; use App\Jobs\Site\ChangePHPVersion;
use App\Jobs\Site\Deploy; use App\Jobs\Site\Deploy;
use App\Jobs\Site\DeployEnv; use App\Jobs\Site\DeployEnv;
use App\Jobs\Site\UpdateBranch; use App\Jobs\Site\UpdateBranch;
use App\Notifications\SiteInstallationFailed;
use App\Notifications\SiteInstallationSucceed;
use Exception; use Exception;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
@ -354,8 +357,8 @@ public function enableAutoDeployment(): void
throw new SourceControlIsNotConnected($this->source_control); throw new SourceControlIsNotConnected($this->source_control);
} }
DB::beginTransaction();
try { try {
DB::beginTransaction();
$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,
@ -406,7 +409,7 @@ public function installationFinished(): void
'site' => $this, 'site' => $this,
]) ])
); );
/** @todo notify */ Notifier::send($this, new SiteInstallationSucceed($this));
} }
/** /**
@ -422,7 +425,7 @@ public function installationFailed(Throwable $e): void
'site' => $this, 'site' => $this,
]) ])
); );
/** @todo notify */ Notifier::send($this, new SiteInstallationFailed($this));
Log::error('install-site-error', [ Log::error('install-site-error', [
'error' => (string) $e, 'error' => (string) $e,
]); ]);

View File

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

View File

@ -28,6 +28,9 @@
* @property Collection $tokens * @property Collection $tokens
* @property string $profile_photo_url * @property string $profile_photo_url
* @property string $timezone * @property string $timezone
* @property int $current_project_id
* @property Project $currentProject
* @property Collection<Project> $projects
*/ */
class User extends Authenticatable class User extends Authenticatable
{ {
@ -41,6 +44,7 @@ class User extends Authenticatable
'email', 'email',
'password', 'password',
'timezone', 'timezone',
'current_project_id',
]; ];
protected $hidden = [ protected $hidden = [
@ -53,6 +57,20 @@ class User extends Authenticatable
protected $appends = [ 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 public function sshKeys(): HasMany
{ {
return $this->hasMany(SshKey::class); return $this->hasMany(SshKey::class);
@ -105,4 +123,36 @@ public function connectedSourceControls(): array
return $connectedSourceControls; 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; namespace App\NotificationChannels;
use App\Contracts\Notification;
use Illuminate\Support\Facades\Http; 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 [ return [
'webhook_url' => 'required|url', 'webhook_url' => 'required|url',
]; ];
} }
public function data(array $input): array public function createData(array $input): array
{ {
return [ 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( $connect = $this->checkConnection(
__('Congratulations! 🎉'), __('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". __('Manage your notification channels')."\n".
route('notification-channels') route('notification-channels')
); );
if (! $connect) { if (! $connect) {
$this->notificationChannel->delete();
return false; return false;
} }
$this->notificationChannel->connected = true;
$this->notificationChannel->save();
return true; 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 private function checkConnection(string $subject, string $text): bool
{ {
$data = $this->notificationChannel->data; $connect = Http::post($this->data()['webhook_url'], [
$connect = Http::post($data['webhook_url'], [
'content' => '*'.$subject.'*'."\n".$text, 'content' => '*'.$subject.'*'."\n".$text,
]); ]);
return $connect->ok(); 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; 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 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 [ return [
'email' => 'required|email', 'email' => 'required|email',
]; ];
} }
public function data(array $input): array public function createData(array $input): array
{ {
return [ return [
'email' => $input['email'], 'email' => $input['email'],
]; ];
} }
public function data(): array
{
return [
'email' => $this->notificationChannel->data['email'] ?? '',
];
}
public function connect(): bool public function connect(): bool
{ {
$this->notificationChannel->connected = true; try {
$this->notificationChannel->save(); Mail::to($this->data()['email'])->send(
new NotificationMail('Test VitoDeploy', 'This is a test email!')
);
} catch (Throwable) {
return false;
}
return true; return true;
} }
public function sendMessage(string $subject, mixed $text): void public function send(object $notifiable, Notification $notification): void
{ {
$data = $this->notificationChannel->data; /** @var NotificationChannel $notifiable */
Mail::to($data['email'])->send(new NotificationChannelMessage($subject, $text)); $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; namespace App\NotificationChannels;
use App\Contracts\Notification;
use Illuminate\Support\Facades\Http; 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 [ return [
'webhook_url' => 'required|url', 'webhook_url' => 'required|url',
]; ];
} }
public function data(array $input): array public function createData(array $input): array
{ {
return [ 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( $connect = $this->checkConnection(
__('Congratulations! 🎉'), __('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". __('Manage your notification channels')."\n".
route('notification-channels') route('notification-channels')
); );
if (! $connect) { if (! $connect) {
$this->notificationChannel->delete();
return false; return false;
} }
$this->notificationChannel->connected = true;
$this->notificationChannel->save();
return true; 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 private function checkConnection(string $subject, string $text): bool
{ {
$data = $this->notificationChannel->data; $connect = Http::post($this->data()['webhook_url'], [
$connect = Http::post($data['webhook_url'], [
'text' => '*'.$subject.'*'."\n".$text, 'text' => '*'.$subject.'*'."\n".$text,
]); ]);
return $connect->ok(); 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; namespace App\Notifications;
use App\Contracts\Notification;
use App\Models\Server; use App\Models\Server;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
class FailedToDeleteServerFromProvider implements Notification class FailedToDeleteServerFromProvider extends AbstractNotification
{ {
use Queueable; use Queueable;
@ -18,26 +17,18 @@ public function __construct(Server $server)
$this->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", [ return __("We couldn't delete [:server] \nfrom :provider \nPlease check your provider and delete it manually", [
'server' => $this->server->name, 'server' => $this->server->name,
'provider' => $this->server->provider, 'provider' => $this->server->provider,
]); ]);
} }
public function mail(): MailMessage public function toMail(object $notifiable): MailMessage
{ {
return (new 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("We couldn't delete [".$this->server->name.'] from '.$this->server->provider)
->line('Please check your provider and delete it manually'); ->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; namespace App\Notifications;
use App\Contracts\Notification;
use App\Models\Server; use App\Models\Server;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
class ServerDisconnected implements Notification class ServerDisconnected extends AbstractNotification
{ {
protected Server $server; protected Server $server;
@ -15,25 +14,17 @@ public function __construct(Server $server)
$this->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]", [ return __("We've disconnected from your server [:server]", [
'server' => $this->server->name, 'server' => $this->server->name,
]); ]);
} }
public function mail(): MailMessage public function toMail(object $notifiable): MailMessage
{ {
return (new MailMessage) return (new MailMessage)
->subject(__('Server disconnected!'))
->line("We've disconnected from your server [".$this->server->name.'].') ->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'); ->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; namespace App\Notifications;
use App\Contracts\Notification;
use App\Models\Server; use App\Models\Server;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
class ServerInstallationFailed implements Notification class ServerInstallationFailed extends AbstractNotification
{ {
protected Server $server; protected Server $server;
@ -15,26 +14,18 @@ public function __construct(Server $server)
$this->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", [ return __("Installation failed for server [:server] \nCheck your server's logs \n:logs", [
'server' => $this->server->name, 'server' => $this->server->name,
'logs' => url('/servers/'.$this->server->id.'/logs'), 'logs' => url('/servers/'.$this->server->id.'/logs'),
]); ]);
} }
private function mail(): MailMessage public function toMail(object $notifiable): MailMessage
{ {
return (new MailMessage) return (new MailMessage)
->subject(__('Server installation failed!'))
->line('Your server ['.$this->server->name.'] installation has been failed.') ->line('Your server ['.$this->server->name.'] installation has been failed.')
->line('Check your server logs') ->line('Check your server logs')
->action('View Logs', url('/servers/'.$this->server->id.'/logs')); ->action('View Logs', url('/servers/'.$this->server->id.'/logs'));

View File

@ -2,11 +2,10 @@
namespace App\Notifications; namespace App\Notifications;
use App\Contracts\Notification;
use App\Models\Server; use App\Models\Server;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
class ServerInstallationStarted implements Notification class ServerInstallationStarted extends AbstractNotification
{ {
protected Server $server; protected Server $server;
@ -15,26 +14,18 @@ public function __construct(Server $server)
$this->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", [ 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, 'server' => $this->server->name,
'progress' => url('/servers/'.$this->server->id), 'progress' => url('/servers/'.$this->server->id),
]); ]);
} }
public function mail(): MailMessage public function toMail(object $notifiable): MailMessage
{ {
return (new MailMessage) return (new MailMessage)
->subject(__('Server installation started!'))
->line('Your server\'s ['.$this->server->name.'] installation has been 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("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.') ->line('As soon as it finishes, We will notify you through this channel.')

View File

@ -2,11 +2,10 @@
namespace App\Notifications; namespace App\Notifications;
use App\Contracts\Notification;
use App\Models\Server; use App\Models\Server;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
class ServerInstallationSucceed implements Notification class ServerInstallationSucceed extends AbstractNotification
{ {
protected Server $server; protected Server $server;
@ -20,14 +19,10 @@ public function subject(): string
return __('Server installation succeed!'); return __('Server installation succeed!');
} }
public function message(bool $mail = false): mixed public function rawText(): string
{ {
$this->server->refresh(); $this->server->refresh();
if ($mail) {
return $this->mail();
}
return __("Installation succeed for server [:server] \nServer IP: :ip \nUser: :user\nPassword: :password\n:link", [ return __("Installation succeed for server [:server] \nServer IP: :ip \nUser: :user\nPassword: :password\n:link", [
'server' => $this->server->name, 'server' => $this->server->name,
'ip' => $this->server->ip, '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(); $this->server->refresh();
return (new MailMessage) return (new MailMessage)
->subject(__('Server installation succeed!'))
->line('Your server ['.$this->server->name.'] has been installed successfully.') ->line('Your server ['.$this->server->name.'] has been installed successfully.')
->line('Server IP: '.$this->server->ip) ->line('Server IP: '.$this->server->ip)
->line('User: '.$this->server->authentication['user']) ->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; namespace App\Notifications;
use Illuminate\Bus\Queueable; use App\Models\SourceControl;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class SourceControlDisconnected extends Notification implements ShouldQueue class SourceControlDisconnected extends AbstractNotification
{ {
use Queueable; public function __construct(protected SourceControl $sourceControl)
protected string $sourceControl;
public function __construct(string $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) return (new MailMessage)
->subject('Lost connection to your '.$this->sourceControl) ->subject(__('Source control disconnected!'))
->line("We've lost connection to your $this->sourceControl account.") ->line($this->rawText());
->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 [
//
];
} }
} }

View File

@ -2,6 +2,7 @@
namespace App\Providers; namespace App\Providers;
use App\Helpers\Notifier;
use App\Helpers\SSH; use App\Helpers\SSH;
use App\Support\SocialiteProviders\DropboxProvider; use App\Support\SocialiteProviders\DropboxProvider;
use Illuminate\Contracts\Container\BindingResolutionException; use Illuminate\Contracts\Container\BindingResolutionException;
@ -29,6 +30,9 @@ public function boot(): void
$this->app->bind('ssh', function () { $this->app->bind('ssh', function () {
return new SSH; return new SSH;
}); });
$this->app->bind('notifier', function () {
return new Notifier;
});
$this->extendSocialite(); $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\Enums\OperatingSystem;
use App\Exceptions\CouldNotConnectToProvider; use App\Exceptions\CouldNotConnectToProvider;
use App\Facades\Notifier;
use App\Notifications\FailedToDeleteServerFromProvider;
use Aws\Ec2\Ec2Client; use Aws\Ec2\Ec2Client;
use Aws\EC2InstanceConnect\EC2InstanceConnectClient; use Aws\EC2InstanceConnect\EC2InstanceConnectClient;
use Exception; use Exception;
use Illuminate\Filesystem\FilesystemAdapter;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Throwable;
class AWS extends AbstractProvider class AWS extends AbstractProvider
{ {
@ -63,7 +67,7 @@ public function data(array $input): array
/** /**
* @throws CouldNotConnectToProvider * @throws CouldNotConnectToProvider
*/ */
public function connect(array $credentials = null): bool public function connect(?array $credentials = null): bool
{ {
try { try {
$this->connectToEc2ClientTest($credentials); $this->connectToEc2ClientTest($credentials);
@ -125,9 +129,8 @@ public function delete(): void
$this->ec2Client->terminateInstances([ $this->ec2Client->terminateInstances([
'InstanceIds' => [$this->server->provider_data['instance_id']], 'InstanceIds' => [$this->server->provider_data['instance_id']],
]); ]);
} catch (Exception) { } catch (Throwable) {
/** @todo notify */ Notifier::send($this->server, new FailedToDeleteServerFromProvider($this->server));
// $this->server->team->notify(new FailedToDeleteServerFromProvider($this->server));
} }
} }
} }
@ -164,10 +167,12 @@ private function createKeyPair(): void
$result = $this->ec2Client->createKeyPair([ $result = $this->ec2Client->createKeyPair([
'KeyName' => $keyName, '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( generate_public_key(
Storage::disk(config('core.key_pairs_disk'))->path((string) $this->server->id), $storageDisk->path((string) $this->server->id),
Storage::disk(config('core.key_pairs_disk'))->path($this->server->id.'.pub'), $storageDisk->path($this->server->id.'.pub'),
); );
} }

View File

@ -10,13 +10,15 @@ abstract class AbstractProvider implements ServerProvider
{ {
protected ?Server $server; protected ?Server $server;
public function __construct(Server $server = null) public function __construct(?Server $server = null)
{ {
$this->server = $server; $this->server = $server;
} }
protected function generateKeyPair(): void 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

@ -14,7 +14,6 @@ public function createValidationRules(array $input): array
return [ return [
'ip' => [ 'ip' => [
'required', 'required',
'ip',
Rule::unique('servers', 'ip'), Rule::unique('servers', 'ip'),
new RestrictedIPAddressesRule(), new RestrictedIPAddressesRule(),
], ],
@ -42,7 +41,7 @@ public function data(array $input): array
return []; return [];
} }
public function connect(array $credentials = null): bool public function connect(?array $credentials = null): bool
{ {
return true; return true;
} }
@ -59,13 +58,15 @@ public function regions(): array
public function create(): void public function create(): void
{ {
/** @var \Illuminate\Filesystem\FilesystemAdapter $storageDisk */
$storageDisk = Storage::disk(config('core.key_pairs_disk'));
File::copy( File::copy(
storage_path(config('core.ssh_private_key_name')), 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( File::copy(
storage_path(config('core.ssh_public_key_name')), 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\CouldNotConnectToProvider;
use App\Exceptions\ServerProviderError; use App\Exceptions\ServerProviderError;
use App\Facades\Notifier;
use App\Notifications\FailedToDeleteServerFromProvider;
use Exception; use Exception;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
@ -58,7 +60,7 @@ public function data(array $input): array
/** /**
* @throws CouldNotConnectToProvider * @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'); $connect = Http::withToken($credentials['token'])->get($this->apiUrl.'/account');
if (! $connect->ok()) { if (! $connect->ok()) {
@ -148,10 +150,9 @@ public function delete(): void
$delete = Http::withToken($this->server->serverProvider->credentials['token']) $delete = Http::withToken($this->server->serverProvider->credentials['token'])
->delete($this->apiUrl.'/droplets/'.$this->server->provider_data['droplet_id']); ->delete($this->apiUrl.'/droplets/'.$this->server->provider_data['droplet_id']);
/** @todo notify */ if (! $delete->ok()) {
// if (! $delete->ok()) { Notifier::send($this->server, new FailedToDeleteServerFromProvider($this->server));
// $this->server->team->notify(new FailedToDeleteServerFromProvider($this->server)); }
// }
} }
} }
} }

View File

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

View File

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

View File

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

View File

@ -16,7 +16,7 @@ public function __construct(Server $server)
$this->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) { return function () use ($percentage, $step) {
$this->server->progress = $percentage; $this->server->progress = $percentage;

View File

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

View File

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

View File

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

View File

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

View File

@ -22,11 +22,6 @@ public function delete(): void
dispatch(new DeleteSite($this->site))->onConnection('ssh'); dispatch(new DeleteSite($this->site))->onConnection('ssh');
} }
public function install(): void
{
// TODO: Implement install() method.
}
protected function progress(int $percentage): Closure protected function progress(int $percentage): Closure
{ {
return function () use ($percentage) { return function () use ($percentage) {

67
app/SiteTypes/PHPBlank.php Executable file
View File

@ -0,0 +1,67 @@
<?php
namespace App\SiteTypes;
use App\Enums\SiteFeature;
use App\Jobs\Site\CreateVHost;
use Illuminate\Support\Facades\Bus;
use Illuminate\Validation\Rule;
use Throwable;
class PHPBlank extends PHPSite
{
public function supportedFeatures(): array
{
return [
SiteFeature::DEPLOYMENT,
SiteFeature::ENV,
SiteFeature::SSL,
SiteFeature::QUEUES,
];
}
public function createValidationRules(array $input): array
{
return [
'php_version' => [
'required',
Rule::in($this->site->server->installedPHPVersions()),
],
];
}
public function createFields(array $input): array
{
return [
'web_directory' => $input['web_directory'] ?? '',
'php_version' => $input['php_version'] ?? '',
];
}
public function data(array $input): array
{
return [];
}
public function install(): void
{
$chain = [
new CreateVHost($this->site),
$this->progress(65),
function () {
$this->site->php()?->restart();
},
];
$chain[] = function () {
$this->site->installationFinished();
};
Bus::chain($chain)
->catch(function (Throwable $e) {
$this->site->installationFailed($e);
})
->onConnection('ssh-long')
->dispatch();
}
}

View File

@ -52,17 +52,17 @@ 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'] ?? '',
'php_version' => $input['php_version'] ?? '',
]; ];
} }
public function data(array $input): array public function data(array $input): array
{ {
return [ return [
'composer' => (bool) $input['composer'], 'composer' => isset($input['composer']) && $input['composer'],
'php_version' => $input['php_version'],
]; ];
} }

View File

@ -24,7 +24,7 @@ public function connect(): bool
/** /**
* @throws Exception * @throws Exception
*/ */
public function getRepo(string $repo = null): mixed public function getRepo(?string $repo = null): mixed
{ {
$res = Http::withToken($this->sourceControl->access_token) $res = Http::withToken($this->sourceControl->access_token)
->get($this->apiUrl."/repositories/$repo"); ->get($this->apiUrl."/repositories/$repo");

View File

@ -25,7 +25,7 @@ public function connect(): bool
/** /**
* @throws Exception * @throws Exception
*/ */
public function getRepo(string $repo = null): mixed public function getRepo(?string $repo = null): mixed
{ {
if ($repo) { if ($repo) {
$url = $this->apiUrl.'/repos/'.$repo; $url = $this->apiUrl.'/repos/'.$repo;

View File

@ -25,7 +25,7 @@ public function connect(): bool
/** /**
* @throws Exception * @throws Exception
*/ */
public function getRepo(string $repo = null): mixed public function getRepo(?string $repo = null): mixed
{ {
$repository = $repo ? urlencode($repo) : null; $repository = $repo ? urlencode($repo) : null;
$res = Http::withToken($this->sourceControl->access_token) $res = Http::withToken($this->sourceControl->access_token)

View File

@ -15,7 +15,7 @@ class SSHFake
protected string $output = ''; protected string $output = '';
public function init(Server $server, string $asUser = null): self public function init(Server $server, ?string $asUser = null): self
{ {
return $this; return $this;
} }
@ -47,7 +47,7 @@ public function assertExecuted(array|string $commands): void
PHPUnit::assertTrue(true, $allExecuted); PHPUnit::assertTrue(true, $allExecuted);
} }
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 (! is_array($commands)) { if (! is_array($commands)) {
$commands = [$commands]; $commands = [$commands];

View File

@ -1,5 +1,6 @@
<?php <?php
use Illuminate\Contracts\Database\Query\Expression;
use Illuminate\Support\Str; use Illuminate\Support\Str;
function random_color(): string function random_color(): string
@ -63,3 +64,10 @@ function date_with_timezone($date, $timezone): string
return $dt->format('Y-m-d H:i:s'); return $dt->format('Y-m-d H:i:s');
} }
function cast_to_json(array $json): Illuminate\Database\Query\Expression|Expression
{
$json = addslashes(json_encode($json));
return DB::raw("CAST('{$json}' AS JSON)");
}

View File

@ -6,17 +6,22 @@
"license": "MIT", "license": "MIT",
"require": { "require": {
"php": "^8.1", "php": "^8.1",
"ext-ftp": "*",
"aws/aws-sdk-php": "^3.158", "aws/aws-sdk-php": "^3.158",
"bensampo/laravel-enum": "^6.3", "bensampo/laravel-enum": "^6.3",
"blade-ui-kit/blade-heroicons": "^2.2",
"davidhsianturi/blade-bootstrap-icons": "^1.4",
"guzzlehttp/guzzle": "^7.2", "guzzlehttp/guzzle": "^7.2",
"khatabwedaa/blade-css-icons": "^1.3",
"laravel/fortify": "^1.17", "laravel/fortify": "^1.17",
"laravel/framework": "^10.0", "laravel/framework": "^10.0",
"laravel/sanctum": "^3.2", "laravel/sanctum": "^3.2",
"laravel/socialite": "^5.2", "laravel/socialite": "^5.2",
"laravel/tinker": "^2.8", "laravel/tinker": "^2.8",
"livewire/livewire": "^2.12", "livewire/livewire": "^2.12",
"phpseclib/phpseclib": "~3.0", "opcodesio/log-viewer": "^2.5",
"ext-ftp": "*" "owenvoke/blade-fontawesome": "^2.5",
"phpseclib/phpseclib": "~3.0"
}, },
"require-dev": { "require-dev": {
"fakerphp/faker": "^1.9.1", "fakerphp/faker": "^1.9.1",
@ -24,7 +29,6 @@
"laravel/sail": "^1.18", "laravel/sail": "^1.18",
"mockery/mockery": "^1.4.4", "mockery/mockery": "^1.4.4",
"nunomaduro/collision": "^7.0", "nunomaduro/collision": "^7.0",
"opcodesio/log-viewer": "^2.5",
"phpunit/phpunit": "^10.0", "phpunit/phpunit": "^10.0",
"spatie/laravel-ignition": "^2.0" "spatie/laravel-ignition": "^2.0"
}, },

2669
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -13,6 +13,7 @@
use App\NotificationChannels\Discord; use App\NotificationChannels\Discord;
use App\NotificationChannels\Email; use App\NotificationChannels\Email;
use App\NotificationChannels\Slack; use App\NotificationChannels\Slack;
use App\NotificationChannels\Telegram;
use App\ServerProviders\AWS; use App\ServerProviders\AWS;
use App\ServerProviders\DigitalOcean; use App\ServerProviders\DigitalOcean;
use App\ServerProviders\Hetzner; use App\ServerProviders\Hetzner;
@ -24,6 +25,7 @@
use App\ServiceHandlers\ProcessManager\Supervisor; use App\ServiceHandlers\ProcessManager\Supervisor;
use App\ServiceHandlers\Webserver\Nginx; use App\ServiceHandlers\Webserver\Nginx;
use App\SiteTypes\Laravel; use App\SiteTypes\Laravel;
use App\SiteTypes\PHPBlank;
use App\SiteTypes\PHPSite; use App\SiteTypes\PHPSite;
use App\SiteTypes\Wordpress; use App\SiteTypes\Wordpress;
use App\SourceControlProviders\Bitbucket; use App\SourceControlProviders\Bitbucket;
@ -39,8 +41,8 @@
'ssh_user' => env('SSH_USER', 'vito'), 'ssh_user' => env('SSH_USER', 'vito'),
'ssh_public_key_name' => env('SSH_PUBLIC_KEY_NAME', 'ssh-public.key'), 'ssh_public_key_name' => env('SSH_PUBLIC_KEY_NAME', 'ssh-public.key'),
'ssh_private_key_name' => env('SSH_PRIVATE_KEY_NAME', 'ssh-private.pem'), 'ssh_private_key_name' => env('SSH_PRIVATE_KEY_NAME', 'ssh-private.pem'),
'logs_disk' => env('SERVER_LOGS_DISK', 'server-logs-local'), 'logs_disk' => env('SERVER_LOGS_DISK', 'server-logs-local'), // should to be FilesystemAdapter storage
'key_pairs_disk' => env('KEY_PAIRS_DISK', 'key-pairs-local'), 'key_pairs_disk' => env('KEY_PAIRS_DISK', 'key-pairs-local'), // should to be FilesystemAdapter storage
/* /*
* General * General
@ -262,11 +264,13 @@
*/ */
'site_types' => [ 'site_types' => [
\App\Enums\SiteType::PHP, \App\Enums\SiteType::PHP,
\App\Enums\SiteType::PHP_BLANK,
\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::PHP_BLANK => PHPBlank::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,
], ],
@ -343,11 +347,13 @@
\App\Enums\NotificationChannel::SLACK, \App\Enums\NotificationChannel::SLACK,
\App\Enums\NotificationChannel::DISCORD, \App\Enums\NotificationChannel::DISCORD,
\App\Enums\NotificationChannel::EMAIL, \App\Enums\NotificationChannel::EMAIL,
\App\Enums\NotificationChannel::TELEGRAM,
], ],
'notification_channels_providers_class' => [ 'notification_channels_providers_class' => [
\App\Enums\NotificationChannel::SLACK => Slack::class, \App\Enums\NotificationChannel::SLACK => Slack::class,
\App\Enums\NotificationChannel::DISCORD => Discord::class, \App\Enums\NotificationChannel::DISCORD => Discord::class,
\App\Enums\NotificationChannel::EMAIL => Email::class, \App\Enums\NotificationChannel::EMAIL => Email::class,
\App\Enums\NotificationChannel::TELEGRAM => Telegram::class,
], ],
/* /*

View File

@ -30,6 +30,7 @@
'disks' => [ 'disks' => [
// should be FilesystemAdapter
'local' => [ 'local' => [
'driver' => 'local', 'driver' => 'local',
'root' => storage_path('app'), 'root' => storage_path('app'),

217
config/log-viewer.php Normal file
View File

@ -0,0 +1,217 @@
<?php
use Opcodes\LogViewer\Level;
return [
/*
|--------------------------------------------------------------------------
| Log Viewer
|--------------------------------------------------------------------------
| Log Viewer can be disabled, so it's no longer accessible via browser.
|
*/
'enabled' => env('LOG_VIEWER_ENABLED', true),
/*
|--------------------------------------------------------------------------
| Log Viewer Domain
|--------------------------------------------------------------------------
| You may change the domain where Log Viewer should be active.
| If the domain is empty, all domains will be valid.
|
*/
'route_domain' => null,
/*
|--------------------------------------------------------------------------
| Log Viewer Route
|--------------------------------------------------------------------------
| Log Viewer will be available under this URL.
|
*/
'route_path' => 'log-viewer',
/*
|--------------------------------------------------------------------------
| Back to system URL
|--------------------------------------------------------------------------
| When set, displays a link to easily get back to this URL.
| Set to `null` to hide this link.
|
| Optional label to display for the above URL.
|
*/
'back_to_system_url' => config('app.url', null),
'back_to_system_label' => null, // Displayed by default: "Back to {{ app.name }}"
/*
|--------------------------------------------------------------------------
| Log Viewer time zone.
|--------------------------------------------------------------------------
| The time zone in which to display the times in the UI. Defaults to
| the application's timezone defined in config/app.php.
|
*/
'timezone' => null,
/*
|--------------------------------------------------------------------------
| Log Viewer route middleware.
|--------------------------------------------------------------------------
| Optional middleware to use when loading the initial Log Viewer page.
|
*/
'middleware' => [
'web',
\Opcodes\LogViewer\Http\Middleware\AuthorizeLogViewer::class,
'auth',
],
/*
|--------------------------------------------------------------------------
| Log Viewer API middleware.
|--------------------------------------------------------------------------
| Optional middleware to use on every API request. The same API is also
| used from within the Log Viewer user interface.
|
*/
'api_middleware' => [
\Opcodes\LogViewer\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
\Opcodes\LogViewer\Http\Middleware\AuthorizeLogViewer::class,
],
/*
|--------------------------------------------------------------------------
| Log Viewer Remote hosts.
|--------------------------------------------------------------------------
| Log Viewer supports viewing Laravel logs from remote hosts. They must
| be running Log Viewer as well. Below you can define the hosts you
| would like to show in this Log Viewer instance.
|
*/
'hosts' => [
'local' => [
'name' => ucfirst(env('APP_ENV', 'local')),
],
// 'staging' => [
// 'name' => 'Staging',
// 'host' => 'https://staging.example.com/log-viewer',
// 'auth' => [ // Example of HTTP Basic auth
// 'username' => 'username',
// 'password' => 'password',
// ],
// ],
//
// 'production' => [
// 'name' => 'Production',
// 'host' => 'https://example.com/log-viewer',
// 'auth' => [ // Example of Bearer token auth
// 'token' => env('LOG_VIEWER_PRODUCTION_TOKEN'),
// ],
// 'headers' => [
// 'X-Foo' => 'Bar',
// ],
// ],
],
/*
|--------------------------------------------------------------------------
| Include file patterns
|--------------------------------------------------------------------------
|
*/
'include_files' => [
'*.log',
'**/*.log',
// '/absolute/paths/supported',
],
/*
|--------------------------------------------------------------------------
| Exclude file patterns.
|--------------------------------------------------------------------------
| This will take precedence over included files.
|
*/
'exclude_files' => [
// 'my_secret.log'
],
/*
|--------------------------------------------------------------------------
| Shorter stack trace filters.
|--------------------------------------------------------------------------
| Lines containing any of these strings will be excluded from the full log.
| This setting is only active when the function is enabled via the user interface.
|
*/
'shorter_stack_trace_excludes' => [
'/vendor/symfony/',
'/vendor/laravel/framework/',
'/vendor/barryvdh/laravel-debugbar/',
],
/*
|--------------------------------------------------------------------------
| Log matching patterns
|--------------------------------------------------------------------------
| Regexes for matching log files
|
*/
'patterns' => [
'laravel' => [
'log_matching_regex' => '/^\[(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}\.?(\d{6}([\+-]\d\d:\d\d)?)?)\].*/',
/**
* This pattern, used for processing Laravel logs, returns these results:
* $matches[0] - the full log line being tested.
* $matches[1] - full timestamp between the square brackets (includes microseconds and timezone offset)
* $matches[2] - timestamp microseconds, if available
* $matches[3] - timestamp timezone offset, if available
* $matches[4] - contents between timestamp and the severity level
* $matches[5] - environment (local, production, etc)
* $matches[6] - log severity (info, debug, error, etc)
* $matches[7] - the log text, the rest of the text.
*/
'log_parsing_regex' => '/^\[(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}\.?(\d{6}([\+-]\d\d:\d\d)?)?)\](.*?(\w+)\.|.*?)('
.implode('|', array_filter(Level::caseValues()))
.')?: (.*?)( in [\/].*?:[0-9]+)?$/is',
],
],
/*
|--------------------------------------------------------------------------
| Cache driver
|--------------------------------------------------------------------------
| Cache driver to use for storing the log indices. Indices are used to speed up
| log navigation. Defaults to your application's default cache driver.
|
*/
'cache_driver' => env('LOG_VIEWER_CACHE_DRIVER', null),
/*
|--------------------------------------------------------------------------
| Chunk size when scanning log files lazily
|--------------------------------------------------------------------------
| The size in MB of files to scan before updating the progress bar when searching across all files.
|
*/
'lazy_scan_chunk_size_in_mb' => 50,
];

View File

@ -0,0 +1,25 @@
<?php
namespace Database\Factories;
use App\Models\Project;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Carbon;
/**
* @extends Factory<Project>
*/
class ProjectFactory extends Factory
{
protected $model = Project::class;
public function definition(): array
{
return [
'user_id' => $this->faker->randomNumber(),
'name' => $this->faker->name(),
'created_at' => Carbon::now(),
'updated_at' => Carbon::now(),
];
}
}

View File

@ -0,0 +1,23 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('projects', function (Blueprint $table) {
$table->id();
$table->bigInteger('user_id');
$table->string('name');
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('projects');
}
};

View File

@ -0,0 +1,22 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('servers', function (Blueprint $table) {
$table->unsignedBigInteger('project_id')->nullable()->after('id');
});
}
public function down(): void
{
Schema::table('servers', function (Blueprint $table) {
$table->dropColumn('project_id');
});
}
};

View File

@ -0,0 +1,27 @@
<?php
use App\Models\User;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->unsignedBigInteger('current_project_id')->nullable()->after('timezone');
});
User::query()->each(function (User $user) {
$project = $user->createDefaultProject();
$user->servers()->update(['project_id' => $project->id]);
});
}
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('current_project_id');
});
}
};

View File

@ -22,6 +22,7 @@ public function run(): void
]); ]);
$server = Server::factory()->create([ $server = Server::factory()->create([
'user_id' => $user->id, 'user_id' => $user->id,
'project_id' => $user->currentProject->id,
]); ]);
$server->services()->create([ $server->services()->create([
'type' => 'database', 'type' => 'database',

View File

@ -4,11 +4,19 @@ export DEBIAN_FRONTEND=noninteractive
export NEEDRESTART_MODE=a export NEEDRESTART_MODE=a
export V_USERNAME=vito export V_USERNAME=vito
export V_PASSWORD=$(openssl rand -base64 12) export V_PASSWORD=$(openssl rand -base64 12)
export V_IP_ADDRESS=$(curl https://freeipapi.com --silent)
export V_IS_DOMAIN=1
echo "Enter the domain you want to install Vito? (your-domain.com)" echo "Enter the domain you want to install Vito? (your-domain.com)"
echo "Hit enter to use your IP address (${V_IP_ADDRESS}):"
read V_DOMAIN read V_DOMAIN
if [[ -z "${V_DOMAIN}" ]]; then
export V_DOMAIN=${V_IP_ADDRESS}
export V_IS_DOMAIN=0
fi
echo "Enter your email address:" echo "Enter your email address:"
read V_ADMIN_EMAIL read V_ADMIN_EMAIL
@ -233,6 +241,11 @@ chmod +x /home/${V_USERNAME}/${V_DOMAIN}/update.sh
# cleanup # cleanup
chown -R ${V_USERNAME}:${V_USERNAME} /home/${V_USERNAME} chown -R ${V_USERNAME}:${V_USERNAME} /home/${V_USERNAME}
# cache
php artisan config:cache
php artisan icons:cache
# print info
echo "🎉 Congratulations!" echo "🎉 Congratulations!"
echo "✅ SSH User: ${V_USERNAME}" echo "✅ SSH User: ${V_USERNAME}"
echo "✅ SSH Password: ${V_PASSWORD}" echo "✅ SSH Password: ${V_PASSWORD}"

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