Compare commits

...

32 Commits
1.0.0 ... 1.2.0

Author SHA1 Message Date
bce05d3171 Merge pull request #148 from vitodeploy/versioning
show current version
2024-04-01 20:50:03 +02:00
929dd1dbaa show version a bit trasparent on mobile 2024-04-01 00:06:29 +02:00
2bcd145bea docker 2024-03-31 23:58:45 +02:00
c0f903d4ca show current version 2024-03-31 23:29:22 +02:00
cca4ab7ae3 fix code editor 2024-03-29 18:40:20 +01:00
51e7325d3d fix trusted procies 2024-03-29 18:25:14 +01:00
ce085879c1 Merge pull request #144 from vitodeploy/fix-env-update
empty content on editing file
2024-03-29 12:29:21 +01:00
8a49003e9e fix focus issue 2024-03-29 12:21:33 +01:00
dcc4276f09 fix spacing in the editor 2024-03-29 10:07:14 +01:00
f089779045 empty content on editing file 2024-03-29 00:42:36 +01:00
f1efb9a6c8 make project dropdown full width #132 2024-03-28 18:59:37 +01:00
a7d472fb45 update discord link 2024-03-27 22:33:24 +01:00
d01d406d3d Merge pull request #137 from vitodeploy/phpmyadmin
add phpmyadmin
2024-03-27 13:34:40 +01:00
c66c50835a fix tests 2024-03-27 13:32:25 +01:00
b6179d6693 build 2024-03-27 11:49:48 +01:00
9244e69fd8 add phpmyadmin 2024-03-27 11:41:29 +01:00
a7ba095919 build 2024-03-25 23:02:06 +01:00
807ae01646 Merge pull request #135 from vitodeploy/console
headless console
2024-03-25 22:57:56 +01:00
cc896d82e9 Merge branch 'console' of github.com:vitodeploy/vito into console 2024-03-25 22:55:48 +01:00
d16d3c1385 test 2024-03-25 22:52:45 +01:00
3946cf6b34 Merge branch '1.x' into console 2024-03-25 22:20:39 +01:00
165212fed2 add stop button 2024-03-25 22:20:21 +01:00
f6b36dfefc Update CONTRIBUTING.md 2024-03-25 22:19:30 +01:00
33594f2dba headless console 2024-03-24 21:58:48 +01:00
f68d6c7ca2 Merge pull request #133 from vitodeploy/fix-php-ini-update
fix php ini update bug
2024-03-24 15:19:06 +01:00
d504588f95 fix php ini update bug 2024-03-24 15:16:49 +01:00
ca0e33be2f Merge branch 'main' into 1.x 2024-03-24 09:58:50 +01:00
4d051330d6 Merge (#127) 2024-03-24 09:56:34 +01:00
ab2d6f64f3 Merge branch 'main' into 1.x 2024-03-24 09:56:07 +01:00
d9a56f95dd minify ace.js 2024-03-24 09:01:18 +01:00
884f18db63 Update README.md 2024-03-23 18:13:16 +01:00
536df65fc6 AGPL-3.0 2024-03-23 10:34:51 +01:00
91 changed files with 849 additions and 21631 deletions

View File

@ -12,5 +12,5 @@ MAIL_PORT=
MAIL_USERNAME=null MAIL_USERNAME=null
MAIL_PASSWORD=null MAIL_PASSWORD=null
MAIL_ENCRYPTION=null MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS=null MAIL_FROM_ADDRESS="noreply@${APP_NAME}"
MAIL_FROM_NAME="${APP_NAME}" MAIL_FROM_NAME="${APP_NAME}"

View File

@ -12,5 +12,5 @@ MAIL_PORT=
MAIL_USERNAME=null MAIL_USERNAME=null
MAIL_PASSWORD=null MAIL_PASSWORD=null
MAIL_ENCRYPTION=null MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS=null MAIL_FROM_ADDRESS="noreply@${APP_NAME}"
MAIL_FROM_NAME="${APP_NAME}" MAIL_FROM_NAME="${APP_NAME}"

View File

@ -13,5 +13,5 @@ MAIL_PORT=
MAIL_USERNAME=null MAIL_USERNAME=null
MAIL_PASSWORD=null MAIL_PASSWORD=null
MAIL_ENCRYPTION=null MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS=null MAIL_FROM_ADDRESS="noreply@${APP_NAME}"
MAIL_FROM_NAME="${APP_NAME}" MAIL_FROM_NAME="${APP_NAME}"

2
.gitignore vendored
View File

@ -7,6 +7,8 @@
/storage/test-key /storage/test-key
/storage/test-key.pub /storage/test-key.pub
/vendor /vendor
/storage/database.sqlite
/storage/database-test.sqlite
.env .env
.env.backup .env.backup
.env.production .env.production

View File

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

View File

@ -37,7 +37,7 @@ ## Useful Links
- [Feedbacks](https://vitodeploy.featurebase.app) - [Feedbacks](https://vitodeploy.featurebase.app)
- [Roadmap](https://vitodeploy.featurebase.app/roadmap) - [Roadmap](https://vitodeploy.featurebase.app/roadmap)
- [Video Demo](https://youtu.be/rLRHIyEfON8) - [Video Demo](https://youtu.be/rLRHIyEfON8)
- [Discord](https://discord.gg/dcUWA5DV) - [Discord](https://discord.gg/uZeeHZZnm5)
- [Contribution](/CONTRIBUTING.md) - [Contribution](/CONTRIBUTING.md)
- [Security](/SECURITY.md) - [Security](/SECURITY.md)
@ -54,3 +54,4 @@ ## Credits
- Prettier - Prettier
- Postcss - Postcss
- Flowbite - Flowbite
- svgrepo.com

View File

@ -0,0 +1,55 @@
<?php
namespace App\Actions\Service;
use App\Enums\ServiceStatus;
use App\Models\Server;
use App\Models\Service;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
class Create
{
public function create(Server $server, array $input): Service
{
$this->validate($server, $input);
$service = new Service([
'name' => $input['type'],
'type' => $input['type'],
'version' => $input['version'],
'status' => ServiceStatus::INSTALLING,
]);
Validator::make($input, $service->handler()->creationRules($input))->validate();
$service->type_data = $service->handler()->creationData($input);
$service->save();
$service->handler()->create();
dispatch(function () use ($service) {
$service->handler()->install();
$service->status = ServiceStatus::READY;
$service->save();
})->catch(function () use ($service) {
$service->handler()->delete();
$service->delete();
})->onConnection('ssh');
return $service;
}
private function validate(Server $server, array $input): void
{
Validator::make($input, [
'type' => [
'required',
Rule::in(config('core.add_on_services')),
Rule::unique('services', 'type')->where('server_id', $server->id),
],
'version' => 'required',
])->validate();
}
}

View File

@ -10,7 +10,9 @@ final class Database
const MYSQL80 = 'mysql80'; const MYSQL80 = 'mysql80';
const MARIADB = 'mariadb'; const MARIADB103 = 'mariadb103';
const MARIADB104 = 'mariadb104';
const POSTGRESQL12 = 'postgresql12'; const POSTGRESQL12 = 'postgresql12';

View File

@ -11,4 +11,6 @@ final class SiteType
const LARAVEL = 'laravel'; const LARAVEL = 'laravel';
const WORDPRESS = 'wordpress'; const WORDPRESS = 'wordpress';
const PHPMYADMIN = 'phpmyadmin';
} }

View File

@ -12,7 +12,7 @@
* @method static init(Server $server, string $asUser = null) * @method static init(Server $server, string $asUser = null)
* @method static setLog(string $logType, int $siteId = null) * @method static setLog(string $logType, int $siteId = null)
* @method static connect() * @method static connect()
* @method static string exec(string $command, string $log = '', int $siteId = null) * @method static string exec(string $command, string $log = '', int $siteId = null, ?bool $stream = false)
* @method static string assertExecuted(array|string $commands) * @method static string assertExecuted(array|string $commands)
* @method static string assertExecutedContains(string $command) * @method static string assertExecutedContains(string $command)
* @method static disconnect() * @method static disconnect()

View File

@ -96,7 +96,7 @@ public function connect(bool $sftp = false): void
* @throws SSHCommandError * @throws SSHCommandError
* @throws SSHConnectionError * @throws SSHConnectionError
*/ */
public function exec(string|array $commands, string $log = '', ?int $siteId = null): string public function exec(string $command, string $log = '', ?int $siteId = null, ?bool $stream = false): string
{ {
if ($log) { if ($log) {
$this->setLog($log, $siteId); $this->setLog($log, $siteId);
@ -112,18 +112,34 @@ public function exec(string|array $commands, string $log = '', ?int $siteId = nu
throw new SSHConnectionError($e->getMessage()); throw new SSHConnectionError($e->getMessage());
} }
if (! is_array($commands)) {
$commands = [$commands];
}
try { try {
$result = ''; if ($this->asUser) {
foreach ($commands as $command) { $command = 'sudo su - '.$this->asUser.' -c '.'"'.addslashes($command).'"';
$result .= $this->executeCommand($command);
} }
return $result; $this->connection->setTimeout(0);
if ($stream) {
$this->connection->exec($command, function ($output) {
$this->log?->write($output);
echo $output;
ob_flush();
flush();
});
return '';
} else {
$output = $this->connection->exec($command);
$this->log?->write($output);
if (Str::contains($output, 'VITO_SSH_ERROR')) {
throw new Exception('SSH command failed with an error');
}
return $output;
}
} catch (Throwable $e) { } catch (Throwable $e) {
throw $e;
throw new SSHCommandError($e->getMessage()); throw new SSHCommandError($e->getMessage());
} }
} }
@ -141,28 +157,6 @@ public function upload(string $local, string $remote): void
$this->connection->put($remote, $local, SFTP::SOURCE_LOCAL_FILE); $this->connection->put($remote, $local, SFTP::SOURCE_LOCAL_FILE);
} }
/**
* @throws Exception
*/
protected function executeCommand(string $command): string
{
if ($this->asUser) {
$command = 'sudo su - '.$this->asUser.' -c '.'"'.addslashes($command).'"';
}
$this->connection->setTimeout(0);
$output = $this->connection->exec($command);
$this->log?->write($output);
if (Str::contains($output, 'VITO_SSH_ERROR')) {
throw new Exception('SSH command failed with an error');
}
return $output;
}
/** /**
* @throws Exception * @throws Exception
*/ */

View File

@ -1,10 +1,11 @@
<?php <?php
namespace App\Http\Controllers; namespace App\Http\Controllers\API;
use App\Actions\Site\Deploy; use App\Actions\Site\Deploy;
use App\Exceptions\SourceControlIsNotConnected; use App\Exceptions\SourceControlIsNotConnected;
use App\Facades\Notifier; use App\Facades\Notifier;
use App\Http\Controllers\Controller;
use App\Models\GitHook; use App\Models\GitHook;
use App\Models\ServerLog; use App\Models\ServerLog;
use App\Notifications\SourceControlDisconnected; use App\Notifications\SourceControlDisconnected;

View File

@ -0,0 +1,16 @@
<?php
namespace App\Http\Controllers\API;
use App\Http\Controllers\Controller;
class HealthController extends Controller
{
public function __invoke()
{
return response()->json([
'success' => true,
'version' => vito_version(),
]);
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace App\Http\Controllers;
use App\Models\Server;
use Illuminate\Contracts\View\View;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
class ConsoleController extends Controller
{
public function index(Server $server): View
{
return view('console.index', [
'server' => $server,
]);
}
public function run(Server $server, Request $request)
{
$this->validate($request, [
'user' => [
'required',
Rule::in(['root', $server->ssh_user]),
],
'command' => 'required|string',
]);
return response()->stream(
function () use ($server, $request) {
$ssh = $server->ssh($request->user);
$log = 'console-'.time();
$ssh->exec(command: $request->command, log: $log, stream: true);
},
200,
[
'Cache-Control' => 'no-cache',
'X-Accel-Buffering' => 'no',
'Content-Type' => 'text/event-stream',
]
);
}
}

View File

@ -71,7 +71,9 @@ public function updateIni(Server $server, Request $request): RedirectResponse
Toast::success('PHP ini updated!'); Toast::success('PHP ini updated!');
return back(); return back()->with([
'ini' => $request->input('ini'),
]);
} }
public function uninstall(Server $server, Request $request): RedirectResponse public function uninstall(Server $server, Request $request): RedirectResponse

View File

@ -2,11 +2,14 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Actions\Service\Create;
use App\Facades\Toast; use App\Facades\Toast;
use App\Helpers\HtmxResponse;
use App\Models\Server; use App\Models\Server;
use App\Models\Service; use App\Models\Service;
use Illuminate\Contracts\View\View; use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class ServiceController extends Controller class ServiceController extends Controller
{ {
@ -62,4 +65,13 @@ public function disable(Server $server, Service $service): RedirectResponse
return back(); return back();
} }
public function install(Server $server, Request $request): HtmxResponse
{
app(Create::class)->create($server, $request->input());
Toast::success('Service is being uninstalled!');
return htmx()->back();
}
} }

View File

@ -7,13 +7,14 @@
use App\Facades\Toast; use App\Facades\Toast;
use Closure; use Closure;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Response;
class HandleSSHErrors class HandleSSHErrors
{ {
public function handle(Request $request, Closure $next) public function handle(Request $request, Closure $next)
{ {
$res = $next($request); $res = $next($request);
if ($res->exception) { if ($res instanceof Response && $res->exception) {
if ($res->exception instanceof SSHConnectionError || $res->exception instanceof SSHCommandError) { if ($res->exception instanceof SSHConnectionError || $res->exception instanceof SSHCommandError) {
Toast::error($res->exception->getMessage()); Toast::error($res->exception->getMessage());

View File

@ -12,7 +12,7 @@ class TrustProxies extends Middleware
* *
* @var array<int, string>|string|null * @var array<int, string>|string|null
*/ */
protected $proxies; protected $proxies = '*';
/** /**
* The headers that should be used to detect proxies. * The headers that should be used to detect proxies.
@ -20,7 +20,7 @@ class TrustProxies extends Middleware
* @var int * @var int
*/ */
protected $headers = protected $headers =
Request::HEADER_X_FORWARDED_FOR | Request::HEADER_X_FORWARDED_FOR |
Request::HEADER_X_FORWARDED_HOST | Request::HEADER_X_FORWARDED_HOST |
Request::HEADER_X_FORWARDED_PORT | Request::HEADER_X_FORWARDED_PORT |
Request::HEADER_X_FORWARDED_PROTO | Request::HEADER_X_FORWARDED_PROTO |

View File

@ -4,6 +4,7 @@
use App\Actions\Service\Manage; use App\Actions\Service\Manage;
use App\Exceptions\ServiceInstallationFailed; use App\Exceptions\ServiceInstallationFailed;
use App\SSH\Services\AddOnServices\AbstractAddOnService;
use App\SSH\Services\Database\Database as DatabaseHandler; use App\SSH\Services\Database\Database as DatabaseHandler;
use App\SSH\Services\Firewall\Firewall as FirewallHandler; use App\SSH\Services\Firewall\Firewall as FirewallHandler;
use App\SSH\Services\PHP\PHP as PHPHandler; use App\SSH\Services\PHP\PHP as PHPHandler;
@ -53,7 +54,9 @@ public static function boot(): void
parent::boot(); parent::boot();
static::creating(function (Service $service) { static::creating(function (Service $service) {
$service->unit = config('core.service_units')[$service->name][$service->server->os][$service->version]; if (array_key_exists($service->name, config('core.service_units'))) {
$service->unit = config('core.service_units')[$service->name][$service->server->os][$service->version];
}
}); });
} }
@ -63,7 +66,7 @@ public function server(): BelongsTo
} }
public function handler( public function handler(
): PHPHandler|WebserverHandler|DatabaseHandler|FirewallHandler|ProcessManagerHandler|RedisHandler { ): PHPHandler|WebserverHandler|DatabaseHandler|FirewallHandler|ProcessManagerHandler|RedisHandler|AbstractAddOnService {
$handler = config('core.service_handlers')[$this->name]; $handler = config('core.service_handlers')[$this->name];
return new $handler($this); return new $handler($this);
@ -81,26 +84,26 @@ public function validateInstall($result): void
public function start(): void public function start(): void
{ {
app(Manage::class)->start($this); $this->unit && app(Manage::class)->start($this);
} }
public function stop(): void public function stop(): void
{ {
app(Manage::class)->stop($this); $this->unit && app(Manage::class)->stop($this);
} }
public function restart(): void public function restart(): void
{ {
app(Manage::class)->restart($this); $this->unit && app(Manage::class)->restart($this);
} }
public function enable(): void public function enable(): void
{ {
app(Manage::class)->enable($this); $this->unit && app(Manage::class)->enable($this);
} }
public function disable(): void public function disable(): void
{ {
app(Manage::class)->disable($this); $this->unit && app(Manage::class)->disable($this);
} }
} }

View File

@ -7,7 +7,6 @@
use App\Helpers\Toast; use App\Helpers\Toast;
use Illuminate\Contracts\Container\BindingResolutionException; use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Http\Resources\Json\ResourceCollection; use Illuminate\Http\Resources\Json\ResourceCollection;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider class AppServiceProvider extends ServiceProvider
@ -37,9 +36,5 @@ public function boot(): void
$this->app->bind('toast', function () { $this->app->bind('toast', function () {
return new Toast; return new Toast;
}); });
if (str(config('app.url'))->startsWith('https://')) {
URL::forceScheme('https');
}
} }
} }

View File

@ -98,12 +98,12 @@ public function reboot(): void
); );
} }
public function editFile(string $path, string $content): void public function editFile(string $path, ?string $content = null): void
{ {
$this->server->ssh()->exec( $this->server->ssh()->exec(
$this->getScript('edit-file.sh', [ $this->getScript('edit-file.sh', [
'path' => $path, 'path' => $path,
'content' => $content, 'content' => $content ?? '',
]), ]),
); );
} }
@ -131,4 +131,21 @@ public function runScript(string $path, string $script, ?int $siteId = null): Se
return $ssh->log; return $ssh->log;
} }
public function download(string $url, string $path): string
{
return $this->server->ssh()->exec(
$this->getScript('download.sh', [
'url' => $url,
'path' => $path,
])
);
}
public function unzip(string $path): string
{
return $this->server->ssh()->exec(
'unzip '.$path
);
}
} }

View File

@ -0,0 +1,3 @@
if ! wget __url__ -O __path__; then
echo 'VITO_SSH_ERROR' && exit 1
fi

View File

@ -0,0 +1,23 @@
<?php
namespace App\SSH\PHPMyAdmin;
use App\Models\Site;
use App\SSH\HasScripts;
class PHPMyAdmin
{
use HasScripts;
public function install(Site $site): void
{
$site->server->ssh()->exec(
$this->getScript('install.sh', [
'version' => $site->type_data['version'],
'path' => $site->path,
]),
'install-phpmyadmin',
$site->id
);
}
}

View File

@ -0,0 +1,25 @@
sudo rm -rf phpmyadmin
sudo rm -rf __path__
if ! wget https://files.phpmyadmin.net/phpMyAdmin/__version__/phpMyAdmin-__version__-all-languages.zip; then
echo 'VITO_SSH_ERROR' && exit 1
fi
if ! unzip phpMyAdmin-__version__-all-languages.zip; then
echo 'VITO_SSH_ERROR' && exit 1
fi
if ! rm -rf phpMyAdmin-__version__-all-languages.zip; then
echo 'VITO_SSH_ERROR' && exit 1
fi
if ! mv phpMyAdmin-__version__-all-languages __path__; then
echo 'VITO_SSH_ERROR' && exit 1
fi
if ! mv __path__/config.sample.inc.php __path__/config.inc.php; then
echo 'VITO_SSH_ERROR' && exit 1
fi
echo "PHPMyAdmin installed!"

View File

@ -0,0 +1,18 @@
<?php
namespace App\SSH\Services\AddOnServices;
use App\SSH\Services\ServiceInterface;
abstract class AbstractAddOnService implements ServiceInterface
{
abstract public function creationRules(array $input): array;
abstract public function creationData(array $input): array;
abstract public function create(): void;
abstract public function delete(): void;
abstract public function data(): array;
}

49
app/SiteTypes/PHPMyAdmin.php Executable file
View File

@ -0,0 +1,49 @@
<?php
namespace App\SiteTypes;
use Illuminate\Validation\Rule;
class PHPMyAdmin extends PHPSite
{
public function supportedFeatures(): array
{
return [
//
];
}
public function createRules(array $input): array
{
return [
'php_version' => [
'required',
Rule::in($this->site->server->installedPHPVersions()),
],
'version' => 'required|string',
];
}
public function createFields(array $input): array
{
return [
'web_directory' => '',
'php_version' => $input['php_version'] ?? '',
];
}
public function data(array $input): array
{
return [
'version' => $input['version'],
];
}
public function install(): void
{
$this->site->server->webserver()->handler()->createVHost($this->site);
$this->progress(30);
app(\App\SSH\PHPMyAdmin\PHPMyAdmin::class)->install($this->site);
$this->progress(65);
}
}

View File

@ -34,7 +34,7 @@ public function connect(bool $sftp = false): void
} }
} }
public function exec(string|array $commands, string $log = '', ?int $siteId = null): string public function exec(string $command, string $log = '', ?int $siteId = null, ?bool $stream = false): string
{ {
if ($log) { if ($log) {
$this->setLog($log, $siteId); $this->setLog($log, $siteId);
@ -42,21 +42,19 @@ public function exec(string|array $commands, string $log = '', ?int $siteId = nu
$this->log = null; $this->log = null;
} }
if (! is_array($commands)) { $this->commands[] = $command;
$commands = [$commands];
}
foreach ($commands as $command) {
if (is_string($command)) {
$this->commands[] = $command;
} else {
$this->commands[] = get_class($command);
}
}
$output = $this->output ?? 'fake output'; $output = $this->output ?? 'fake output';
$this->log?->write($output); $this->log?->write($output);
if ($stream) {
echo $output;
ob_flush();
flush();
return '';
}
return $output; return $output;
} }

View File

@ -29,3 +29,8 @@ function htmx(): HtmxResponse
{ {
return new HtmxResponse(); return new HtmxResponse();
} }
function vito_version(): string
{
return exec('git describe --tags');
}

View File

@ -13,6 +13,7 @@
use App\ServerProviders\Vultr; use App\ServerProviders\Vultr;
use App\SiteTypes\Laravel; use App\SiteTypes\Laravel;
use App\SiteTypes\PHPBlank; use App\SiteTypes\PHPBlank;
use App\SiteTypes\PHPMyAdmin;
use App\SiteTypes\PHPSite; use App\SiteTypes\PHPSite;
use App\SiteTypes\Wordpress; use App\SiteTypes\Wordpress;
use App\SourceControlProviders\Bitbucket; use App\SourceControlProviders\Bitbucket;
@ -177,6 +178,9 @@
'ufw' => Ufw::class, 'ufw' => Ufw::class,
'supervisor' => Supervisor::class, 'supervisor' => Supervisor::class,
], ],
'add_on_services' => [
// add-on services
],
'service_units' => [ 'service_units' => [
'nginx' => [ 'nginx' => [
'ubuntu_18' => [ 'ubuntu_18' => [
@ -320,12 +324,14 @@
\App\Enums\SiteType::PHP_BLANK, \App\Enums\SiteType::PHP_BLANK,
\App\Enums\SiteType::LARAVEL, \App\Enums\SiteType::LARAVEL,
\App\Enums\SiteType::WORDPRESS, \App\Enums\SiteType::WORDPRESS,
\App\Enums\SiteType::PHPMYADMIN,
], ],
'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::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,
\App\Enums\SiteType::PHPMYADMIN => PHPMyAdmin::class,
], ],
/* /*

View File

@ -27,9 +27,9 @@ COPY docker/php.ini /etc/php/8.2/cli/conf.d/99-vito.ini
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
# app # app
COPY . /var/www/html RUN rm -rf /var/www/html
RUN rm -rf /var/www/html/vendor RUN git clone -b 1.x https://github.com/vitodeploy/vito.git /var/www/html
RUN rm -rf /var/www/html/.env RUN git checkout $(git tag -l --merged 1.x --sort=-v:refname | head -n 1)
RUN composer install --no-dev --prefer-dist RUN composer install --no-dev --prefer-dist
RUN chown -R www-data:www-data /var/www/html \ RUN chown -R www-data:www-data /var/www/html \
&& chmod -R 755 /var/www/html/storage /var/www/html/bootstrap/cache && chmod -R 755 /var/www/html/storage /var/www/html/bootstrap/cache

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
{ {
"resources/css/app.css": { "resources/css/app.css": {
"file": "assets/app-986a0fb7.css", "file": "assets/app-e3775b0a.css",
"isEntry": true, "isEntry": true,
"src": "resources/css/app.css" "src": "resources/css/app.css"
}, },
@ -12,7 +12,7 @@
"css": [ "css": [
"assets/app-a1ae07b3.css" "assets/app-a1ae07b3.css"
], ],
"file": "assets/app-e823d2ab.js", "file": "assets/app-5f99a92f.js",
"isEntry": true, "isEntry": true,
"src": "resources/js/app.js" "src": "resources/js/app.js"
} }

View File

@ -1 +1,9 @@
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="list" class="svg-inline--fa fa-list fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M80 368H16a16 16 0 0 0-16 16v64a16 16 0 0 0 16 16h64a16 16 0 0 0 16-16v-64a16 16 0 0 0-16-16zm0-320H16A16 16 0 0 0 0 64v64a16 16 0 0 0 16 16h64a16 16 0 0 0 16-16V64a16 16 0 0 0-16-16zm0 160H16a16 16 0 0 0-16 16v64a16 16 0 0 0 16 16h64a16 16 0 0 0 16-16v-64a16 16 0 0 0-16-16zm416 176H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zm0-320H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16V80a16 16 0 0 0-16-16zm0 160H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16z"></path></svg> <?xml version="1.0" encoding="utf-8"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="48" height="48" fill="white" fill-opacity="0.01" />
<rect x="4" y="8" width="40" height="32" rx="2" fill="#2F88FF" stroke="#000000" stroke-width="4"
stroke-linejoin="round" />
<path d="M12 18L19 24L12 30" stroke="white" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" />
<path d="M23 32H36" stroke="white" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" />
</svg>

Before

Width:  |  Height:  |  Size: 786 B

After

Width:  |  Height:  |  Size: 654 B

View File

@ -1 +1,9 @@
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="fire" class="svg-inline--fa fa-fire fa-w-12" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><path fill="currentColor" d="M216 23.86c0-23.8-30.65-32.77-44.15-13.04C48 191.85 224 200 224 288c0 35.63-29.11 64.46-64.85 63.99-35.17-.45-63.15-29.77-63.15-64.94v-85.51c0-21.7-26.47-32.23-41.43-16.5C27.8 213.16 0 261.33 0 320c0 105.87 86.13 192 192 192s192-86.13 192-192c0-170.29-168-193-168-296.14z"></path></svg> <?xml version="1.0" encoding="utf-8"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 14 14" role="img" focusable="false" aria-hidden="true"
xmlns="http://www.w3.org/2000/svg">
<path fill="olive"
d="M12.123424 3.09708C9.1750899 1.36849 7.0055019 1 7.0055019 1s-.0023.00035-.0055.00096c-.0033-.00061-.0055-.00096-.0055-.00096s-2.169706.36849-5.117921 2.09708c0 0-.390942 8.72672 5.117921 9.90292.0019-.00035.0036-.00096.0055-.001.0019.00035.0036.00096.0055.001 5.5087451-1.1762 5.1179221-9.90292 5.1179221-9.90292z" />
<path fill="#ff0"
d="M7.0058749 1.74098c.482647.10437 2.177431.53826 4.3878041 1.77816-.0096 1.61164-.285353 7.71006-4.3878041 8.73494V1.74098z" />
</svg>

Before

Width:  |  Height:  |  Size: 499 B

After

Width:  |  Height:  |  Size: 781 B

File diff suppressed because one or more lines are too long

View File

@ -26,7 +26,9 @@ document.body.addEventListener('htmx:configRequest', (event) => {
if (window.getSelection) { window.getSelection().removeAllRanges(); } if (window.getSelection) { window.getSelection().removeAllRanges(); }
else if (document.selection) { document.selection.empty(); } else if (document.selection) { document.selection.empty(); }
}); });
let activeElement = null;
document.body.addEventListener('htmx:beforeRequest', (event) => { document.body.addEventListener('htmx:beforeRequest', (event) => {
activeElement = document.activeElement;
let targetElements = event.target.querySelectorAll('[hx-disable]'); let targetElements = event.target.querySelectorAll('[hx-disable]');
for (let i = 0; i < targetElements.length; i++) { for (let i = 0; i < targetElements.length; i++) {
targetElements[i].disabled = true; targetElements[i].disabled = true;
@ -38,6 +40,18 @@ document.body.addEventListener('htmx:afterRequest', (event) => {
targetElements[i].disabled = false; targetElements[i].disabled = false;
} }
}); });
document.body.addEventListener('htmx:afterSwap', (event) => {
tippy('[data-tooltip]', {
content(reference) {
return reference.getAttribute('data-tooltip');
},
});
if (activeElement) {
activeElement.blur();
activeElement.focus();
activeElement = null;
}
});
import toastr from 'toastr'; import toastr from 'toastr';
window.toastr = toastr; window.toastr = toastr;
@ -49,13 +63,6 @@ window.toastr.options = {
import tippy from 'tippy.js'; import tippy from 'tippy.js';
import 'tippy.js/dist/tippy.css'; import 'tippy.js/dist/tippy.css';
document.body.addEventListener('htmx:afterSettle', (event) => {
tippy('[data-tooltip]', {
content(reference) {
return reference.getAttribute('data-tooltip');
},
});
});
tippy('[data-tooltip]', { tippy('[data-tooltip]', {
content(reference) { content(reference) {
return reference.getAttribute('data-tooltip'); return reference.getAttribute('data-tooltip');

View File

@ -0,0 +1,6 @@
<div>
<x-simple-card class="flex items-center justify-between">
<span>PHPMyAdmin is installed and ready to use!</span>
<x-secondary-button :href="$site->getUrl()" target="_blank">Open</x-secondary-button>
</x-simple-card>
</div>

View File

@ -3,7 +3,7 @@
<span> <span>
{{ __("Your Wordpress site is installed and ready to use! ") }} {{ __("Your Wordpress site is installed and ready to use! ") }}
</span> </span>
<x-secondary-button :href="$site->url" target="_blank"> <x-secondary-button :href="$site->getUrl()" target="_blank">
{{ __("Open Website") }} {{ __("Open Website") }}
</x-secondary-button> </x-secondary-button>
</x-simple-card> </x-simple-card>

View File

@ -17,7 +17,7 @@
document.documentElement.className === 'dark' document.documentElement.className === 'dark'
? 'one-dark' ? 'one-dark'
: 'github' : 'github'
editor = window.ace.edit(this.editorId) editor = window.ace.edit(this.editorId, {})
let contentElement = document.getElementById( let contentElement = document.getElementById(
`text-${this.editorId}`, `text-${this.editorId}`,
) )

View File

@ -1,5 +1,5 @@
<div <div
class="h-[500px] w-full overflow-auto whitespace-pre-line rounded-md border border-gray-200 bg-gray-900 p-5 text-gray-50 dark:border-gray-700" {{ $attributes->merge(["class" => "relative h-[500px] w-full overflow-auto whitespace-pre-line rounded-md border border-gray-200 bg-black p-5 text-gray-50 dark:border-gray-700"]) }}
> >
{{ $slot }} {{ $slot }}
</div> </div>

View File

@ -0,0 +1,14 @@
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
{{ $attributes }}
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m6.75 7.5 3 2.25-3 2.25m4.5 0h3m-9 8.25h13.5A2.25 2.25 0 0 0 21 18V6a2.25 2.25 0 0 0-2.25-2.25H5.25A2.25 2.25 0 0 0 3 6v12a2.25 2.25 0 0 0 2.25 2.25Z"
/>
</svg>

After

Width:  |  Height:  |  Size: 406 B

View File

@ -0,0 +1,14 @@
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
{{ $attributes }}
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M18.364 18.364A9 9 0 0 0 5.636 5.636m12.728 12.728A9 9 0 0 1 5.636 5.636m12.728 12.728L5.636 5.636"
/>
</svg>

After

Width:  |  Height:  |  Size: 355 B

View File

@ -1,5 +1,6 @@
@props([ @props([
"href", "href",
"disabled" => false,
]) ])
@php @php
@ -7,12 +8,12 @@
"inline-flex w-max items-center justify-center px-2 py-1 font-semibold capitalize outline-0 transition hover:opacity-50 focus:ring focus:ring-primary-200 disabled:opacity-25 dark:focus:ring-primary-700 dark:focus:ring-opacity-40"; "inline-flex w-max items-center justify-center px-2 py-1 font-semibold capitalize outline-0 transition hover:opacity-50 focus:ring focus:ring-primary-200 disabled:opacity-25 dark:focus:ring-primary-700 dark:focus:ring-opacity-40";
@endphp @endphp
@if (isset($href)) @if (isset($href) && ! $disabled)
<a href="{{ $href }}" {{ $attributes->merge(["class" => $class]) }}> <a href="{{ $href }}" {{ $attributes->merge(["class" => $class]) }}>
{{ $slot }} {{ $slot }}
</a> </a>
@else @else
<button {{ $attributes->merge(["type" => "submit", "class" => $class]) }}> <button {{ $attributes->merge(["type" => "submit", "class" => $class, "disabled" => $disabled]) }}>
{{ $slot }} {{ $slot }}
</button> </button>
@endif @endif

View File

@ -1,5 +1,5 @@
<div <div
class="flex h-20 items-center justify-between rounded-b-md rounded-t-md border border-gray-200 bg-white p-7 text-center dark:border-gray-700 dark:bg-gray-800" {{ $attributes->merge(["class" => "flex h-20 items-center justify-between rounded-b-md rounded-t-md border border-gray-200 bg-white p-7 text-center dark:border-gray-700 dark:bg-gray-800"]) }}
> >
{{ $slot }} {{ $slot }}
</div> </div>

View File

@ -1,20 +1,17 @@
@props([ @props([
"interval" => "30s", "interval" => "7s",
"id", "id",
"target" => null, "target" => null,
]) ])
<div <div
id="{{ $id }}" {{ $attributes->merge(["interval" => $interval, "id" => $id, "hx-get" => request()->getUri(), "hx-trigger" => "every " . $interval, "hx-swap" => "outerHTML"]) }}
hx-get="{{ request()->getUri() }}"
hx-trigger="every {{ $interval }}"
@if ($target) @if ($target)
hx-target="{{ $target }}" hx-target="{{ $target }}"
hx-select="{{ $target }}" hx-select="{{ $target }}"
@else @else
hx-select="#{{ $id }}" hx-select="#{{ $id }}"
@endif @endif
hx-swap="outerHTML"
> >
{{ $slot }} {{ $slot }}
</div> </div>

View File

@ -7,7 +7,7 @@
@php @php
$class = $class =
"inline-flex h-9 min-w-max items-center rounded-md border border-gray-300 bg-white px-4 py-1 font-semibold text-gray-700 shadow-sm transition duration-150 ease-in-out hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 disabled:opacity-25 dark:border-gray-500 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700 dark:focus:ring-offset-gray-800"; "inline-flex h-9 w-max min-w-max items-center justify-center rounded-md border border-gray-300 bg-white px-4 py-1 font-semibold text-gray-700 shadow-sm transition duration-150 ease-in-out hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 disabled:opacity-25 dark:border-gray-500 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700 dark:focus:ring-offset-gray-800";
@endphp @endphp
@if (isset($href)) @if (isset($href))

View File

@ -0,0 +1,104 @@
<x-server-layout :server="$server">
<x-slot name="pageTitle">{{ $server->name }} - Console</x-slot>
<div
x-data="{
user: '{{ $server->ssh_user }}',
running: false,
command: '',
output: '',
runUrl: '{{ route("servers.console.run", ["server" => $server]) }}',
async run() {
this.running = true
this.output = 'Running...\n'
const fetchOptions = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}',
},
body: JSON.stringify({
user: this.user,
command: this.command,
}),
}
const response = await fetch(this.runUrl, fetchOptions)
const reader = response.body.getReader()
const decoder = new TextDecoder('utf-8')
while (true) {
if (! this.running) {
reader.cancel()
this.output += '\nStopped!'
break
}
const { value, done } = await reader.read()
if (done) break
const textChunk = decoder.decode(value, { stream: true })
this.output += textChunk
document.getElementById('console-output').scrollTop =
document.getElementById('console-output').scrollHeight
}
this.output += '\nDone!'
this.running = false
},
stop() {
this.running = false
},
}"
>
<x-card-header>
<x-slot name="title">Headless Console</x-slot>
<x-slot name="description">
Here you can run ssh commands on your server and see the result right away.
<br />
<b>Note that</b>
this is a headless console, it doesn't keep the current path. it will always run from the home path of
the selected user.
</x-slot>
</x-card-header>
<div class="space-y-3">
<x-console-view id="console-output">
<div class="w-full" x-text="output"></div>
</x-console-view>
<form onsubmit="return false" id="console-form" class="flex items-center justify-between">
<x-select-input x-model="user" id="user" name="user" class="flex-none" data-tooltip="User">
<option value="{{ $server->ssh_user }}">{{ $server->ssh_user }}</option>
<option value="root">root</option>
</x-select-input>
<x-text-input
id="command"
name="command"
x-model="command"
type="text"
placeholder="Type your command here..."
class="mx-1 flex-grow"
autocomplete="off"
/>
<x-secondary-button
type="button"
id="btn-stop"
x-on:click="stop"
class="mr-1 h-[40px]"
x-bind:disabled="!running"
>
Stop
</x-secondary-button>
<x-primary-button
type="submit"
id="btn-run"
x-on:click="run"
class="h-[40px]"
x-bind:disabled="running || command === ''"
>
Run
</x-primary-button>
</form>
</div>
</div>
</x-server-layout>

View File

@ -1,26 +1,34 @@
<nav <nav
class="fixed top-0 z-50 flex h-[64px] w-full items-center border-b border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800" class="fixed top-0 z-50 flex h-[64px] w-full items-center border-b border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800"
> >
<div class="w-full px-3 py-3 lg:px-5 lg:pl-3"> <div class="w-full">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="flex items-center justify-start"> <div class="flex items-center justify-start">
<button <div
data-drawer-target="logo-sidebar" class="flex items-center justify-start border-r border-gray-200 px-3 py-3 dark:border-gray-700 md:w-64"
data-drawer-toggle="logo-sidebar"
aria-controls="logo-sidebar"
type="button"
class="inline-flex items-center rounded-md p-2 text-sm text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-600 sm:hidden"
> >
<span class="sr-only">Open sidebar</span> <button
<x-heroicon name="o-bars-3-center-left" class="h-6 w-6" /> data-drawer-target="logo-sidebar"
</button> data-drawer-toggle="logo-sidebar"
<a href="/" class="ms-2 flex md:me-24"> aria-controls="logo-sidebar"
<div class="flex items-center justify-start text-3xl font-extrabold"> type="button"
<x-application-logo class="h-9 w-9 rounded-md" /> class="inline-flex items-center rounded-md p-2 text-sm text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-600 sm:hidden"
<span class="ml-1 hidden sm:block">Deploy</span> >
</div> <span class="sr-only">Open sidebar</span>
</a> <x-heroicon name="o-bars-3-center-left" class="h-6 w-6" />
<div class="h-[64px] w-1 border-r border-gray-200 px-3 dark:border-gray-700 md:px-0"></div> </button>
<a href="/" class="ms-2 flex md:me-24">
<div class="relative flex items-center justify-start text-3xl font-extrabold">
<x-application-logo class="h-9 w-9 rounded-md" />
<span class="ml-1 hidden md:block">Deploy</span>
<span
class="absolute bottom-0 left-0 right-0 rounded-b-md bg-gray-700/60 text-center text-xs text-white md:relative md:ml-1 md:block md:bg-inherit md:text-inherit"
>
{{ vito_version() }}
</span>
</div>
</a>
</div>
<div class="ml-5 cursor-pointer" x-data=""> <div class="ml-5 cursor-pointer" x-data="">
<div <div
class="flex w-full items-center rounded-md border border-gray-200 bg-gray-100 px-4 py-2 text-sm text-gray-900 focus:ring-4 focus:ring-gray-300 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300 dark:focus:ring-gray-600" class="flex w-full items-center rounded-md border border-gray-200 bg-gray-100 px-4 py-2 text-sm text-gray-900 focus:ring-4 focus:ring-gray-300 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300 dark:focus:ring-gray-600"
@ -31,7 +39,7 @@ class="flex w-full items-center rounded-md border border-gray-200 bg-gray-100 px
</div> </div>
</div> </div>
</div> </div>
<div class="flex items-center"> <div class="flex items-center px-3 py-3">
<div class="mr-3"> <div class="mr-3">
@include("layouts.partials.color-scheme") @include("layouts.partials.color-scheme")
</div> </div>

View File

@ -1,5 +1,5 @@
<div data-tooltip="Project" class="cursor-pointer"> <div data-tooltip="Project" class="cursor-pointer">
<x-dropdown align="left"> <x-dropdown width="full">
<x-slot:trigger> <x-slot:trigger>
<div> <div>
<div <div

View File

@ -137,8 +137,8 @@
x-on:click="close" x-on:click="close"
class="fixed inset-0 bottom-0 left-0 right-0 top-0 z-[1000] items-center bg-gray-500 opacity-75 dark:bg-gray-900" class="fixed inset-0 bottom-0 left-0 right-0 top-0 z-[1000] items-center bg-gray-500 opacity-75 dark:bg-gray-900"
></div> ></div>
<div class="absolute z-[1000] mt-20 lg:scale-110"> <div class="absolute left-1 right-1 z-[1000] mt-20 md:left-auto md:right-auto lg:scale-110">
<div class="w-[500px]"> <div class="w-full px-10 md:w-[500px]">
<x-text-input <x-text-input
id="search-input" id="search-input"
x-ref="input" x-ref="input"

View File

@ -116,6 +116,18 @@ class="fixed left-0 top-0 z-40 h-screen w-64 -translate-x-full border-r border-g
</x-sidebar-link> </x-sidebar-link>
</li> </li>
<li>
<x-sidebar-link
:href="route('servers.console', ['server' => $server])"
:active="request()->routeIs('servers.console')"
>
<x-heroicon name="o-command-line" class="h-6 w-6" />
<span class="ml-2">
{{ __("Console") }}
</span>
</x-sidebar-link>
</li>
<li> <li>
<x-sidebar-link <x-sidebar-link
:href="route('servers.settings', ['server' => $server])" :href="route('servers.settings', ['server' => $server])"

View File

@ -6,45 +6,47 @@
</x-slot> </x-slot>
</x-card-header> </x-card-header>
<a class="block"> <x-live id="php-default-cli">
<x-item-card> <a class="block">
<div class="flex items-start justify-center"> <x-item-card>
<span class="mr-2">PHP {{ $defaultPHP->version }}</span> <div class="flex items-start justify-center">
@include("services.partials.status", ["status" => $defaultPHP->status]) <span class="mr-2">PHP {{ $defaultPHP->version }}</span>
</div> @include("services.partials.status", ["status" => $defaultPHP->status])
<div class="flex items-center"> </div>
<div class="inline"> <div class="flex items-center">
<x-dropdown> <div class="inline">
<x-slot name="trigger"> <x-dropdown>
<x-secondary-button> <x-slot name="trigger">
{{ __("Change") }} <x-secondary-button>
<x-heroicon name="o-chevron-up-down" class="ml-1 h-5 w-5" /> {{ __("Change") }}
</x-secondary-button> <x-heroicon name="o-chevron-up-down" class="ml-1 h-5 w-5" />
</x-slot> </x-secondary-button>
<x-slot name="content"> </x-slot>
@foreach ($phps as $php) <x-slot name="content">
@if ($php->version != $defaultPHP->version) @foreach ($phps as $php)
<x-dropdown-link @if ($php->version != $defaultPHP->version)
id="php-{{ $php->id }}-default-cli" <x-dropdown-link
class="cursor-pointer" id="php-{{ $php->id }}-default-cli"
hx-post="{{ route('servers.php.default-cli', ['server' => $server, 'version' => $php->version]) }}" class="cursor-pointer"
hx-swap="outerHTML" hx-post="{{ route('servers.php.default-cli', ['server' => $server, 'version' => $php->version]) }}"
hx-select="#php-{{ $php->id }}-default-cli" hx-swap="outerHTML"
> hx-select="#php-{{ $php->id }}-default-cli"
PHP {{ $php->version }} >
PHP {{ $php->version }}
</x-dropdown-link>
@endif
@endforeach
@if (count($phps) == 1)
<x-dropdown-link>
{{ __("No other versions") }}
</x-dropdown-link> </x-dropdown-link>
@endif @endif
@endforeach </x-slot>
</x-dropdown>
@if (count($phps) == 1) </div>
<x-dropdown-link>
{{ __("No other versions") }}
</x-dropdown-link>
@endif
</x-slot>
</x-dropdown>
</div> </div>
</div> </x-item-card>
</x-item-card> </a>
</a> </x-live>
</div> </div>

View File

@ -39,7 +39,7 @@ class="cursor-pointer"
</x-dropdown-link> </x-dropdown-link>
<x-dropdown-link <x-dropdown-link
class="cursor-pointer" class="cursor-pointer"
x-on:click="$dispatch('open-modal', 'update-php-ini'); document.getElementById('ini').value = 'Loading...';" x-on:click="version = '{{ $php->version }}'; $dispatch('open-modal', 'update-php-ini'); document.getElementById('ini').value = 'Loading...';"
hx-get="{{ route('servers.php.get-ini', ['server' => $server, 'version' => $php->version]) }}" hx-get="{{ route('servers.php.get-ini', ['server' => $server, 'version' => $php->version]) }}"
hx-swap="outerHTML" hx-swap="outerHTML"
hx-target="#update-php-ini-form" hx-target="#update-php-ini-form"

View File

@ -2,4 +2,6 @@
<x-slot name="pageTitle">{{ __("Services") }}</x-slot> <x-slot name="pageTitle">{{ __("Services") }}</x-slot>
@include("services.partials.services-list") @include("services.partials.services-list")
{{-- @include("services.partials.add-ons") --}}
</x-server-layout> </x-server-layout>

View File

@ -0,0 +1 @@
@include("services.partials.unit-actions")

View File

@ -0,0 +1 @@
@include("services.partials.unit-actions")

View File

@ -0,0 +1 @@
@include("services.partials.unit-actions")

View File

@ -0,0 +1 @@
@include("services.partials.unit-actions")

View File

@ -0,0 +1 @@
@include("services.partials.unit-actions")

View File

@ -0,0 +1 @@
@include("services.partials.unit-actions")

View File

@ -0,0 +1 @@
@include("services.partials.unit-actions")

View File

@ -0,0 +1 @@
@include("services.partials.unit-actions")

View File

@ -0,0 +1,33 @@
<div>
<x-card-header>
<x-slot name="title">Supported Services</x-slot>
<x-slot name="description">Here you can find the supported services to install</x-slot>
<x-slot name="aside"></x-slot>
</x-card-header>
<div class="grid grid-cols-1 gap-5 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
@foreach (config("core.add_on_services") as $addOn)
<div
class="relative flex h-auto flex-col items-center justify-between space-y-3 rounded-b-md rounded-t-md border border-gray-200 bg-white text-center dark:border-gray-700 dark:bg-gray-800"
>
<div class="space-y-3 p-5">
<div class="flex items-center justify-center">
<img src="{{ asset("static/images/" . $addOn . ".svg") }}" class="h-20 w-20" alt="" />
</div>
<div class="flex flex-grow flex-col items-start justify-center">
<div class="flex items-center">
<div class="flex items-center text-lg">
{{ $addOn }}
</div>
</div>
</div>
</div>
<div
class="flex w-full items-center justify-between rounded-b-md border-t border-t-gray-200 bg-gray-50 p-2 dark:border-t-gray-600 dark:bg-gray-700"
>
@include("services.partials.add-on-installers." . $addOn)
</div>
</div>
@endforeach
</div>
</div>

View File

@ -1,80 +1,42 @@
<div> <div>
<x-card-header> <x-card-header>
<x-slot name="title">{{ __("Services") }}</x-slot> <x-slot name="title">Installed Services</x-slot>
<x-slot name="description"> <x-slot name="description">All services that we installed on your server are here</x-slot>
{{ __("All services that we installed on your server are here") }}
</x-slot>
<x-slot name="aside"></x-slot> <x-slot name="aside"></x-slot>
</x-card-header> </x-card-header>
<x-live id="live-services" interval="5s"> <x-live id="live-services">
<div class="space-y-3"> <div class="grid grid-cols-1 gap-5 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
@foreach ($services as $service) @foreach ($services as $service)
<x-item-card> <div
<div class="flex-none"> class="relative flex h-auto flex-col items-center justify-between space-y-3 rounded-b-md rounded-t-md border border-gray-200 bg-white text-center dark:border-gray-700 dark:bg-gray-800"
<img src="{{ asset("static/images/" . $service->name . ".svg") }}" class="h-10 w-10" alt="" /> >
<div class="absolute right-3 top-3">
@include("services.partials.status", ["status" => $service->status])
</div> </div>
<div class="ml-3 flex flex-grow flex-col items-start justify-center"> <div class="space-y-3 p-5">
<div class="flex items-center"> <div class="flex items-center justify-center">
<div class="mr-2">{{ $service->name }}:{{ $service->version }}</div> <img
@include("services.partials.status", ["status" => $service->status]) src="{{ asset("static/images/" . $service->name . ".svg") }}"
class="h-20 w-20"
alt=""
/>
</div>
<div class="flex flex-grow flex-col items-start justify-center">
<div class="flex items-center">
<div class="flex items-center text-lg">
{{ $service->name }}
<x-status status="disabled" class="ml-1">{{ $service->version }}</x-status>
</div>
</div>
</div> </div>
</div> </div>
<div class="flex items-center"> <div
<x-dropdown> class="flex w-full items-center justify-between rounded-b-md border-t border-t-gray-200 bg-gray-50 p-2 dark:border-t-gray-600 dark:bg-gray-700"
<x-slot name="trigger"> >
<x-secondary-button> @include("services.partials.actions." . $service->name)
{{ __("Actions") }}
</x-secondary-button>
</x-slot>
<x-slot name="content">
@if ($service->unit)
@if ($service->status == \App\Enums\ServiceStatus::STOPPED)
<x-dropdown-link
class="cursor-pointer"
href="{{ route('servers.services.start', ['server' => $server, 'service' => $service]) }}"
>
{{ __("Start") }}
</x-dropdown-link>
@endif
@if ($service->status == \App\Enums\ServiceStatus::READY)
<x-dropdown-link
class="cursor-pointer"
href="{{ route('servers.services.stop', ['server' => $server, 'service' => $service]) }}"
>
{{ __("Stop") }}
</x-dropdown-link>
@endif
<x-dropdown-link
class="cursor-pointer"
href="{{ route('servers.services.restart', ['server' => $server, 'service' => $service]) }}"
>
{{ __("Restart") }}
</x-dropdown-link>
@if ($service->status == \App\Enums\ServiceStatus::DISABLED)
<x-dropdown-link
class="cursor-pointer"
href="{{ route('servers.services.enable', ['server' => $server, 'service' => $service]) }}"
>
{{ __("Enable") }}
</x-dropdown-link>
@endif
<x-dropdown-link
class="cursor-pointer"
href="{{ route('servers.services.disable', ['server' => $server, 'service' => $service]) }}"
>
{{ __("Disable") }}
</x-dropdown-link>
@endif
</x-slot>
</x-dropdown>
</div> </div>
</x-item-card> </div>
@endforeach @endforeach
</div> </div>
</x-live> </x-live>

View File

@ -0,0 +1,42 @@
<x-icon-button
data-tooltip="Restart Service"
class="cursor-pointer"
href="{{ route('servers.services.restart', ['server' => $server, 'service' => $service]) }}"
>
<x-heroicon name="o-arrow-path" class="h-5 w-5" />
</x-icon-button>
<x-icon-button
:disabled="$service->status != \App\Enums\ServiceStatus::STOPPED"
data-tooltip="Start Service"
class="cursor-pointer"
href="{{ route('servers.services.start', ['server' => $server, 'service' => $service]) }}"
>
<x-heroicon name="o-play" class="h-5 w-5 text-green-400" />
</x-icon-button>
<x-icon-button
data-tooltip="Stop Service"
:disabled="$service->status != \App\Enums\ServiceStatus::READY"
class="cursor-pointer"
href="{{ route('servers.services.stop', ['server' => $server, 'service' => $service]) }}"
>
<x-heroicon name="o-stop" class="h-5 w-5 text-red-400" />
</x-icon-button>
<x-icon-button
:disabled="$service->status != \App\Enums\ServiceStatus::DISABLED"
data-tooltip="Enable Service"
class="cursor-pointer"
href="{{ route('servers.services.enable', ['server' => $server, 'service' => $service]) }}"
>
<x-heroicon name="o-check" class="h-5 w-5" />
</x-icon-button>
<x-icon-button
data-tooltip="Disable Service"
class="cursor-pointer"
href="{{ route('servers.services.disable', ['server' => $server, 'service' => $service]) }}"
>
<x-heroicon name="o-no-symbol" class="h-5 w-5" />
</x-icon-button>

View File

@ -0,0 +1,13 @@
@include("sites.partials.create.fields.php-version")
<div>
<x-input-label for="version" :value="__('Version')" />
<x-select-input id="version" name="version" class="mt-1 w-full">
<option value="" selected>{{ __("Select") }}</option>
<option value="5.1.2" @if(old('version') == '5.1.2') selected @endif>PHPMyAdmin 5.1.2</option>
<option value="4.9.11" @if(old('version') == '4.9.11') selected @endif>PHPMyAdmin 4.9.11</option>
</x-select-input>
@error("version")
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>

View File

@ -1,7 +1,9 @@
<?php <?php
// git hook // git hook
use App\Http\Controllers\GitHookController; use App\Http\Controllers\API\GitHookController;
use App\Http\Controllers\API\HealthController;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
Route::any('git-hooks', GitHookController::class)->name('git-hooks'); Route::get('health', HealthController::class)->name('api.health');
Route::any('git-hooks', GitHookController::class)->name('api.git-hooks');

View File

@ -1,6 +1,7 @@
<?php <?php
use App\Http\Controllers\ApplicationController; use App\Http\Controllers\ApplicationController;
use App\Http\Controllers\ConsoleController;
use App\Http\Controllers\CronjobController; use App\Http\Controllers\CronjobController;
use App\Http\Controllers\DatabaseBackupController; use App\Http\Controllers\DatabaseBackupController;
use App\Http\Controllers\DatabaseController; use App\Http\Controllers\DatabaseController;
@ -123,6 +124,11 @@
Route::get('/{server}/services/{service}/restart', [ServiceController::class, 'restart'])->name('servers.services.restart'); Route::get('/{server}/services/{service}/restart', [ServiceController::class, 'restart'])->name('servers.services.restart');
Route::get('/{server}/services/{service}/enable', [ServiceController::class, 'enable'])->name('servers.services.enable'); Route::get('/{server}/services/{service}/enable', [ServiceController::class, 'enable'])->name('servers.services.enable');
Route::get('/{server}/services/{service}/disable', [ServiceController::class, 'disable'])->name('servers.services.disable'); Route::get('/{server}/services/{service}/disable', [ServiceController::class, 'disable'])->name('servers.services.disable');
Route::post('/{server}/services/install', [ServiceController::class, 'install'])->name('servers.services.install');
// console
Route::get('/{server}/console', [ConsoleController::class, 'index'])->name('servers.console');
Route::post('/{server}/console', [ConsoleController::class, 'run'])->name('servers.console.run');
}); });
// settings // settings

View File

@ -1,5 +1,6 @@
#!/bin/bash #!/bin/bash
export VITO_VERSION="1.x"
export DEBIAN_FRONTEND=noninteractive export DEBIAN_FRONTEND=noninteractive
export NEEDRESTART_MODE=a export NEEDRESTART_MODE=a
@ -151,11 +152,13 @@ ln -s /etc/nginx/sites-available/vito /etc/nginx/sites-enabled/
service nginx restart service nginx restart
rm -rf /home/${V_USERNAME}/vito rm -rf /home/${V_USERNAME}/vito
git config --global core.fileMode false git config --global core.fileMode false
git clone -b 1.x ${V_REPO} /home/${V_USERNAME}/vito git clone -b ${VITO_VERSION} ${V_REPO} /home/${V_USERNAME}/vito
find /home/${V_USERNAME}/vito -type d -exec chmod 755 {} \; find /home/${V_USERNAME}/vito -type d -exec chmod 755 {} \;
find /home/${V_USERNAME}/vito -type f -exec chmod 644 {} \; find /home/${V_USERNAME}/vito -type f -exec chmod 644 {} \;
cd /home/${V_USERNAME}/vito && git config core.fileMode false cd /home/${V_USERNAME}/vito && git config core.fileMode false
cd /home/${V_USERNAME}/vito && composer install --no-dev cd /home/${V_USERNAME}/vito
git checkout $(git tag -l --merged ${VITO_VERSION} --sort=-v:refname | head -n 1)
composer install --no-dev
cp .env.prod .env cp .env.prod .env
touch /home/${V_USERNAME}/vito/storage/database.sqlite touch /home/${V_USERNAME}/vito/storage/database.sqlite
php artisan key:generate php artisan key:generate

View File

@ -4,7 +4,9 @@ cd /home/vito/vito
php artisan down php artisan down
git pull git fetch --all
git checkout $(git tag -l --merged 1.x --sort=-v:refname | head -n 1)
composer install --no-dev composer install --no-dev

View File

@ -23,7 +23,7 @@ public function test_visit_application()
'site' => $this->site, 'site' => $this->site,
]) ])
) )
->assertOk() ->assertSuccessful()
->assertSee($this->site->domain); ->assertSee($this->site->domain);
} }
@ -88,7 +88,7 @@ public function test_deploy(): void
'server' => $this->server, 'server' => $this->server,
'site' => $this->site, 'site' => $this->site,
])) ]))
->assertOk() ->assertSuccessful()
->assertSee('test commit message'); ->assertSee('test commit message');
$deployment = $this->site->deployments()->first(); $deployment = $this->site->deployments()->first();
@ -212,7 +212,7 @@ public function test_git_hook_deployment(): void
'content' => 'git pull', 'content' => 'git pull',
]); ]);
$this->post(route('git-hooks'), [ $this->post(route('api.git-hooks'), [
'secret' => 'secret', 'secret' => 'secret',
])->assertSessionDoesntHaveErrors(); ])->assertSessionDoesntHaveErrors();
@ -240,7 +240,7 @@ public function test_git_hook_deployment_invalid_secret(): void
'content' => 'git pull', 'content' => 'git pull',
]); ]);
$this->post(route('git-hooks'), [ $this->post(route('api.git-hooks'), [
'secret' => 'invalid-secret', 'secret' => 'invalid-secret',
])->assertNotFound(); ])->assertNotFound();

View File

@ -0,0 +1,46 @@
<?php
namespace Tests\Feature;
use App\Facades\SSH;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class ConsoleTest extends TestCase
{
use RefreshDatabase;
public function test_see_console(): void
{
$this->actingAs($this->user);
$this->get(route('servers.console', $this->server))
->assertSuccessful()
->assertSeeText('Headless Console');
}
public function test_run(): void
{
SSH::fake('fake output');
$this->actingAs($this->user);
$this->post(route('servers.console.run', $this->server), [
'user' => 'vito',
'command' => 'ls -la',
])->assertStreamedContent('fake output');
}
public function test_run_validation_error(): void
{
$this->actingAs($this->user);
$this->post(route('servers.console.run', $this->server), [
'user' => 'vito',
])->assertSessionHasErrors('command');
$this->post(route('servers.console.run', $this->server), [
'command' => 'ls -la',
])->assertSessionHasErrors('user');
}
}

View File

@ -22,6 +22,7 @@ public function test_see_cronjobs_list()
]); ]);
$this->get(route('servers.cronjobs', $this->server)) $this->get(route('servers.cronjobs', $this->server))
->assertSuccessful()
->assertSeeText($cronjob->frequencyLabel()); ->assertSeeText($cronjob->frequencyLabel());
} }

View File

@ -98,6 +98,7 @@ public function test_see_backups_list(): void
]); ]);
$this->get(route('servers.databases.backups', [$this->server, $backup])) $this->get(route('servers.databases.backups', [$this->server, $backup]))
->assertSuccessful()
->assertSee($backup->database->name); ->assertSee($backup->database->name);
} }

View File

@ -66,6 +66,7 @@ public function test_see_databases_list(): void
]); ]);
$this->get(route('servers.databases', $this->server)) $this->get(route('servers.databases', $this->server))
->assertSuccessful()
->assertSee($database->name); ->assertSee($database->name);
} }

View File

@ -58,6 +58,7 @@ public function test_see_database_users_list(): void
]); ]);
$this->get(route('servers.databases', $this->server)) $this->get(route('servers.databases', $this->server))
->assertSuccessful()
->assertSee($databaseUser->username); ->assertSee($databaseUser->username);
} }

View File

@ -41,6 +41,7 @@ public function test_see_firewall_rules(): void
]); ]);
$this->get(route('servers.firewall', $this->server)) $this->get(route('servers.firewall', $this->server))
->assertSuccessful()
->assertSee($rule->source) ->assertSee($rule->source)
->assertSee($rule->port); ->assertSee($rule->port);
} }

View File

@ -20,6 +20,7 @@ public function test_see_logs()
]); ]);
$this->get(route('servers.logs', $this->server)) $this->get(route('servers.logs', $this->server))
->assertSuccessful()
->assertSeeText($log->type); ->assertSeeText($log->type);
} }
} }

View File

@ -194,6 +194,7 @@ public function test_see_channels_list(): void
$channel = \App\Models\NotificationChannel::factory()->create(); $channel = \App\Models\NotificationChannel::factory()->create();
$this->get(route('notification-channels')) $this->get(route('notification-channels'))
->assertSuccessful()
->assertSee($channel->provider); ->assertSee($channel->provider);
} }

View File

@ -16,6 +16,7 @@ public function test_profile_page_is_displayed(): void
$this $this
->get(route('profile')) ->get(route('profile'))
->assertSuccessful()
->assertSee('Profile Information') ->assertSee('Profile Information')
->assertSee('Update Password') ->assertSee('Update Password')
->assertSee('Two Factor Authentication'); ->assertSee('Two Factor Authentication');

View File

@ -32,6 +32,7 @@ public function test_see_projects_list(): void
]); ]);
$this->get(route('projects')) $this->get(route('projects'))
->assertSuccessful()
->assertSee($project->name); ->assertSee($project->name);
} }

View File

@ -27,7 +27,7 @@ public function test_see_queues()
'site' => $this->site, 'site' => $this->site,
]) ])
) )
->assertOk() ->assertSuccessful()
->assertSee($queue->command); ->assertSee($queue->command);
} }

View File

@ -27,6 +27,7 @@ public function test_see_server_keys()
]); ]);
$this->get(route('servers.ssh-keys', $this->server)) $this->get(route('servers.ssh-keys', $this->server))
->assertSuccessful()
->assertSeeText('My first key'); ->assertSeeText('My first key');
} }

View File

@ -70,6 +70,7 @@ public function test_see_providers_list(): void
]); ]);
$this->get(route('server-providers')) $this->get(route('server-providers'))
->assertSuccessful()
->assertSee($provider->profile); ->assertSee($provider->profile);
} }

View File

@ -16,6 +16,8 @@ public function test_see_services_list(): void
$this->actingAs($this->user); $this->actingAs($this->user);
$this->get(route('servers.services', $this->server)) $this->get(route('servers.services', $this->server))
->assertSuccessful()
->assertSee('mysql')
->assertSee('nginx') ->assertSee('nginx')
->assertSee('php') ->assertSee('php')
->assertSee('supervisor') ->assertSee('supervisor')
@ -242,6 +244,7 @@ public static function data(): array
['redis'], ['redis'],
['ufw'], ['ufw'],
['php'], ['php'],
['mysql'],
]; ];
} }
} }

View File

@ -57,7 +57,7 @@ public function test_see_sites_list(): void
$this->get(route('servers.sites', [ $this->get(route('servers.sites', [
'server' => $this->server, 'server' => $this->server,
])) ]))
->assertOk() ->assertSuccessful()
->assertSee($site->domain); ->assertSee($site->domain);
} }
@ -158,6 +158,15 @@ public static function create_data(): array
'web_directory' => 'public', 'web_directory' => 'public',
], ],
], ],
[
[
'type' => SiteType::PHPMYADMIN,
'domain' => 'example.com',
'alias' => 'www.example.com',
'php_version' => '8.2',
'version' => '5.1.2',
],
],
]; ];
} }
@ -169,7 +178,7 @@ public function test_see_logs(): void
'server' => $this->server, 'server' => $this->server,
'site' => $this->site, 'site' => $this->site,
])) ]))
->assertOk() ->assertSuccessful()
->assertSee('Logs'); ->assertSee('Logs');
} }
} }

View File

@ -29,6 +29,7 @@ public function test_get_public_keys_list(): void
]); ]);
$this->get(route('ssh-keys')) $this->get(route('ssh-keys'))
->assertSuccessful()
->assertSee($key->name); ->assertSee($key->name);
} }

View File

@ -25,7 +25,7 @@ public function test_see_ssls_list()
'server' => $this->server, 'server' => $this->server,
'site' => $this->site, 'site' => $this->site,
])) ]))
->assertOk() ->assertSuccessful()
->assertSee($ssl->type); ->assertSee($ssl->type);
} }
@ -37,7 +37,7 @@ public function test_see_ssls_list_with_no_ssls()
'server' => $this->server, 'server' => $this->server,
'site' => $this->site, 'site' => $this->site,
])) ]))
->assertOk() ->assertSuccessful()
->assertSeeText(__("You don't have any SSL certificates yet!")); ->assertSeeText(__("You don't have any SSL certificates yet!"));
} }

View File

@ -41,6 +41,7 @@ public function test_see_providers_list(): void
]); ]);
$this->get(route('storage-providers')) $this->get(route('storage-providers'))
->assertSuccessful()
->assertSee($provider->profile); ->assertSee($provider->profile);
} }