Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .env.prod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ APP_URL=

FILESYSTEM_DRIVER=local

VITO_MODE=local
VITO_PORT=54331
VITO_SSL=false
WS_PORT=54332

MAIL_MAILER=smtp
MAIL_HOST=
MAIL_PORT=
Expand Down
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,8 @@ AGENTS.md
boost.json
opencode.json
.claude/

**/caddy
Caddyfile
frankenphp
frankenphp-worker.php
71 changes: 71 additions & 0 deletions app/Actions/Admin/UpdateVitoSSL.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php

namespace App\Actions\Admin;

use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;

class UpdateVitoSSL
{
/**
* @param array<string, mixed> $input
*
* @throws ValidationException
*/
public function update(array $input): void
{
$this->validate($input);

$ssl = (bool) ($input['ssl'] ?? false);
$domain = $input['domain'] ?? '';
$email = $input['email'] ?? '';

$this->updateEnv('VITO_SSL', $ssl ? 'true' : 'false');
$this->updateEnv('VITO_DOMAIN', $domain);

if ($ssl && $domain) {
$scheme = 'https';
$this->updateEnv('APP_URL', "{$scheme}://{$domain}");
}

Artisan::call('vito:generate-caddyfile');

exec('sudo supervisorctl restart octane 2>/dev/null');
}

/**
* @param array<string, mixed> $input
*
* @throws ValidationException
*/
private function validate(array $input): void
{
$rules = [
'ssl' => ['required', 'boolean'],
'domain' => ['required_if:ssl,true', 'nullable', 'string'],
'email' => ['required_if:ssl,true', 'nullable', 'email'],
];

Validator::make($input, $rules)->validate();
}

private function updateEnv(string $key, string $value): void
{
$envPath = base_path('.env');

if (! file_exists($envPath)) {
return;
}

$content = file_get_contents($envPath);

if (preg_match("/^{$key}=.*/m", $content)) {
$content = preg_replace("/^{$key}=.*/m", "{$key}={$value}", $content);
} else {
$content .= "\n{$key}={$value}";
}

file_put_contents($envPath, $content);
}
}
1 change: 1 addition & 0 deletions app/Actions/Server/CreateServer.php
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ private function validate(Project $project, array $input): void
'provider' => [
'required',
Rule::in(array_keys(config('server-provider.providers'))),
Rule::notIn(['local']),
],
'name' => [
'required',
Expand Down
7 changes: 7 additions & 0 deletions app/Actions/Server/RebootServer.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,19 @@

use App\Enums\ServerStatus;
use App\Models\Server;
use Illuminate\Validation\ValidationException;
use Throwable;

class RebootServer
{
public function reboot(Server $server): Server
{
if ($server->isLocal()) {
throw ValidationException::withMessages([
'server' => 'Cannot reboot the local server as it would stop Vito itself.',
]);
}

try {
$server->os()->reboot();
$server->status = ServerStatus::DISCONNECTED;
Expand Down
6 changes: 6 additions & 0 deletions app/Actions/Server/TransferServer.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ class TransferServer
*/
public function transfer(User $user, Server $server, array $input): Server
{
if ($server->isLocal()) {
throw ValidationException::withMessages([
'server' => 'Cannot transfer the local server.',
]);
}

$this->validate($user, $input);

$server->project_id = $input['project_id'];
Expand Down
6 changes: 6 additions & 0 deletions app/Actions/Service/Manage.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ public function disable(Service $service): void

private function validate(Service $service): void
{
if ($service->is_readonly) {
throw ValidationException::withMessages([
'service' => __('Cannot modify :service as it is required by Vito.', ['service' => $service->name]),
]);
}

if (! $service->handler()->unit()) {
throw ValidationException::withMessages([
'service' => __('This service does not have a systemd unit configured.'),
Expand Down
7 changes: 7 additions & 0 deletions app/Actions/Service/Uninstall.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use App\Jobs\Service\UninstallJob;
use App\Models\Service;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;

class Uninstall
{
Expand All @@ -14,6 +15,12 @@ class Uninstall
*/
public function uninstall(Service $service): void
{
if ($service->is_readonly) {
throw ValidationException::withMessages([
'service' => 'Cannot uninstall '.$service->name.' as it is required by Vito.',
]);
}

Validator::make([
'service' => $service->id,
], $service->handler()->deletionRules())->validate();
Expand Down
38 changes: 38 additions & 0 deletions app/Console/Commands/GenerateCaddyfileCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\View;

class GenerateCaddyfileCommand extends Command
{
protected $signature = 'vito:generate-caddyfile';

protected $description = 'Generate the Caddyfile for FrankenPHP from config';

public function handle(): int
{
$ssl = (bool) config('core.vito_ssl');
$domain = config('core.vito_domain', '');
$port = config('core.vito_port', 54331);
$wsPort = config('core.ws_port', 54332);
$root = base_path('public');

$content = View::make('ssh.caddyfile', [
'ssl' => $ssl,
'domain' => $domain,
'port' => $port,
'wsPort' => $wsPort,
'root' => $root,
'email' => '',
])->render();

File::put(base_path('Caddyfile'), $content);

$this->info('Caddyfile generated successfully.');

return self::SUCCESS;
}
}
133 changes: 133 additions & 0 deletions app/Console/Commands/SetupLocalServerCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
<?php

namespace App\Console\Commands;

use App\Enums\OperatingSystem;
use App\Enums\ServerStatus;
use App\Enums\ServiceStatus;
use App\Models\Server;
use App\Models\User;
use Illuminate\Console\Command;
use Illuminate\Support\Str;

class SetupLocalServerCommand extends Command
{
protected $signature = 'server:setup-local';

protected $description = 'Bootstrap the local server as a managed server';

public function handle(): int
{
if (Server::query()->where('provider', 'local')->exists()) {
$this->info('Local server already exists. Skipping.');

return self::SUCCESS;
}

/** @var ?User $user */
$user = User::query()->first();

if (! $user) {
$this->error('No admin user found. Please create a user first.');

return self::FAILURE;
}

/** @var ?\App\Models\Project $project */
$project = $user->currentProject ?? $user->allProjects()->first();

if (! $project) {
$this->error('No project found. Please create a project first.');

return self::FAILURE;
}

$os = $this->detectOS();

$server = new Server([
'project_id' => $project->id,
'user_id' => $user->id,
'name' => 'Vito',
'ssh_user' => 'vito',
'ip' => '127.0.0.1',
'port' => 22,
'os' => $os,
'provider' => 'local',
'authentication' => [
'user' => 'vito',
'pass' => Str::random(15),
'root_pass' => Str::random(15),
],
'status' => ServerStatus::READY,
'progress' => 100,
]);
$server->save();

$server->provider()->create();

if (file_exists('/home/vito/.ssh/id_rsa.pub')) {
$server->public_key = trim(file_get_contents('/home/vito/.ssh/id_rsa.pub'));
$server->save();
}

$this->createServices($server);

$this->info('Local server has been set up successfully.');

return self::SUCCESS;
}

private function detectOS(): OperatingSystem
{
if (file_exists('/etc/os-release')) {
$content = file_get_contents('/etc/os-release');
if (str_contains($content, '24.04')) {
return OperatingSystem::UBUNTU24;
}
if (str_contains($content, '22.04')) {
return OperatingSystem::UBUNTU22;
}
if (str_contains($content, '20.04')) {
return OperatingSystem::UBUNTU20;
}
if (str_contains($content, '18.04')) {
return OperatingSystem::UBUNTU18;
}
}

return OperatingSystem::UBUNTU24;
}

private function createServices(Server $server): void
{
$services = [
[
'type' => 'webserver',
'name' => 'nginx',
'version' => 'latest',
'status' => ServiceStatus::READY,
'is_default' => true,
],
[
'type' => 'memory_database',
'name' => 'redis',
'version' => 'latest',
'status' => ServiceStatus::READY,
'is_default' => true,
'is_readonly' => true,
],
[
'type' => 'process_manager',
'name' => 'supervisor',
'version' => 'latest',
'status' => ServiceStatus::READY,
'is_default' => true,
'is_readonly' => true,
],
];

foreach ($services as $service) {
$server->services()->create($service);
}
}
}
4 changes: 2 additions & 2 deletions app/Console/Commands/WebSocketServeCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@ class WebSocketServeCommand extends Command
{
protected $signature = 'ws:serve
{--host=127.0.0.1 : The host to listen on}
{--port=8085 : The port to listen on}
{--port=54332 : The port to listen on}
{--max-connections=50 : Maximum concurrent WebSocket connections}';

protected $description = 'Start the WebSocket server';

public function handle(): void
{
$host = $this->option('host') ?? config('core.ws_host', '127.0.0.1');
$port = $this->option('port') ?? config('core.ws_port', '8085');
$port = $this->option('port') ?? config('core.ws_port', '54332');
$maxConnections = (int) $this->option('max-connections');

$loop = Loop::get();
Expand Down
18 changes: 17 additions & 1 deletion app/Http/Controllers/Admin/VitoSettingController.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace App\Http\Controllers\Admin;

use App\Actions\Admin\UpdateVitoSSL;
use App\Http\Controllers\Controller;
use Exception;
use Illuminate\Http\RedirectResponse;
Expand Down Expand Up @@ -38,7 +39,22 @@ class VitoSettingController extends Controller
#[Get('/', name: 'vito-settings')]
public function index(): Response
{
return Inertia::render('vito-settings/index');
return Inertia::render('vito-settings/index', [
'vito_ssl' => (bool) config('core.vito_ssl'),
'vito_domain' => config('core.vito_domain', ''),
'vito_mode' => config('core.vito_mode'),
]);
}

/**
* @throws ValidationException
*/
#[Post('/ssl', name: 'vito-settings.ssl')]
public function updateSSL(Request $request): RedirectResponse
{
app(UpdateVitoSSL::class)->update($request->all());

return back()->with('success', 'SSL settings updated successfully.');
}

/**
Expand Down
2 changes: 1 addition & 1 deletion app/Http/Controllers/ConsoleController.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public function token(Server $server, Request $request): JsonResponse
$port = $appUrl['port'] ?? ($isSecure ? 443 : 80);

if (app()->environment('local')) {
$wsPort = config('core.ws_port', 8085);
$wsPort = config('core.ws_port', 54332);
$result['url'] = "{$wsProtocol}://{$host}:{$wsPort}/ws/terminal";
} else {
$portSuffix = (($isSecure && $port == 443) || (! $isSecure && $port == 80)) ? '' : ":{$port}";
Expand Down
Loading
Loading