mirror of
https://github.com/vitodeploy/vito.git
synced 2025-07-01 05:56:16 +00:00
Compare commits
26 Commits
Author | SHA1 | Date | |
---|---|---|---|
ce085879c1 | |||
8a49003e9e | |||
dcc4276f09 | |||
f089779045 | |||
f1efb9a6c8 | |||
a7d472fb45 | |||
d01d406d3d | |||
c66c50835a | |||
b6179d6693 | |||
9244e69fd8 | |||
a7ba095919 | |||
807ae01646 | |||
cc896d82e9 | |||
d16d3c1385 | |||
3946cf6b34 | |||
165212fed2 | |||
f6b36dfefc | |||
33594f2dba | |||
f68d6c7ca2 | |||
d504588f95 | |||
ca0e33be2f | |||
4d051330d6 | |||
ab2d6f64f3 | |||
d9a56f95dd | |||
884f18db63 | |||
536df65fc6 |
@ -12,5 +12,5 @@ MAIL_PORT=
|
||||
MAIL_USERNAME=null
|
||||
MAIL_PASSWORD=null
|
||||
MAIL_ENCRYPTION=null
|
||||
MAIL_FROM_ADDRESS=null
|
||||
MAIL_FROM_ADDRESS="noreply@${APP_NAME}"
|
||||
MAIL_FROM_NAME="${APP_NAME}"
|
||||
|
@ -12,5 +12,5 @@ MAIL_PORT=
|
||||
MAIL_USERNAME=null
|
||||
MAIL_PASSWORD=null
|
||||
MAIL_ENCRYPTION=null
|
||||
MAIL_FROM_ADDRESS=null
|
||||
MAIL_FROM_ADDRESS="noreply@${APP_NAME}"
|
||||
MAIL_FROM_NAME="${APP_NAME}"
|
||||
|
@ -13,5 +13,5 @@ MAIL_PORT=
|
||||
MAIL_USERNAME=null
|
||||
MAIL_PASSWORD=null
|
||||
MAIL_ENCRYPTION=null
|
||||
MAIL_FROM_ADDRESS=null
|
||||
MAIL_FROM_ADDRESS="noreply@${APP_NAME}"
|
||||
MAIL_FROM_NAME="${APP_NAME}"
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -7,6 +7,8 @@
|
||||
/storage/test-key
|
||||
/storage/test-key.pub
|
||||
/vendor
|
||||
/storage/database.sqlite
|
||||
/storage/database-test.sqlite
|
||||
.env
|
||||
.env.backup
|
||||
.env.production
|
||||
|
@ -1,23 +1,5 @@
|
||||
# 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
|
||||
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.
|
||||
https://vitodeploy.com/introduction/contribution-guide.html
|
||||
|
@ -37,7 +37,7 @@ ## Useful Links
|
||||
- [Feedbacks](https://vitodeploy.featurebase.app)
|
||||
- [Roadmap](https://vitodeploy.featurebase.app/roadmap)
|
||||
- [Video Demo](https://youtu.be/rLRHIyEfON8)
|
||||
- [Discord](https://discord.gg/dcUWA5DV)
|
||||
- [Discord](https://discord.gg/uZeeHZZnm5)
|
||||
- [Contribution](/CONTRIBUTING.md)
|
||||
- [Security](/SECURITY.md)
|
||||
|
||||
@ -54,3 +54,4 @@ ## Credits
|
||||
- Prettier
|
||||
- Postcss
|
||||
- Flowbite
|
||||
- svgrepo.com
|
||||
|
55
app/Actions/Service/Create.php
Normal file
55
app/Actions/Service/Create.php
Normal 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();
|
||||
}
|
||||
}
|
@ -10,7 +10,9 @@ final class Database
|
||||
|
||||
const MYSQL80 = 'mysql80';
|
||||
|
||||
const MARIADB = 'mariadb';
|
||||
const MARIADB103 = 'mariadb103';
|
||||
|
||||
const MARIADB104 = 'mariadb104';
|
||||
|
||||
const POSTGRESQL12 = 'postgresql12';
|
||||
|
||||
|
@ -11,4 +11,6 @@ final class SiteType
|
||||
const LARAVEL = 'laravel';
|
||||
|
||||
const WORDPRESS = 'wordpress';
|
||||
|
||||
const PHPMYADMIN = 'phpmyadmin';
|
||||
}
|
||||
|
@ -12,7 +12,7 @@
|
||||
* @method static init(Server $server, string $asUser = null)
|
||||
* @method static setLog(string $logType, int $siteId = null)
|
||||
* @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 assertExecutedContains(string $command)
|
||||
* @method static disconnect()
|
||||
|
@ -96,7 +96,7 @@ public function connect(bool $sftp = false): void
|
||||
* @throws SSHCommandError
|
||||
* @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) {
|
||||
$this->setLog($log, $siteId);
|
||||
@ -112,18 +112,34 @@ public function exec(string|array $commands, string $log = '', ?int $siteId = nu
|
||||
throw new SSHConnectionError($e->getMessage());
|
||||
}
|
||||
|
||||
if (! is_array($commands)) {
|
||||
$commands = [$commands];
|
||||
}
|
||||
|
||||
try {
|
||||
$result = '';
|
||||
foreach ($commands as $command) {
|
||||
$result .= $this->executeCommand($command);
|
||||
if ($this->asUser) {
|
||||
$command = 'sudo su - '.$this->asUser.' -c '.'"'.addslashes($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) {
|
||||
throw $e;
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
43
app/Http/Controllers/ConsoleController.php
Normal file
43
app/Http/Controllers/ConsoleController.php
Normal 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',
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
@ -71,7 +71,9 @@ public function updateIni(Server $server, Request $request): RedirectResponse
|
||||
|
||||
Toast::success('PHP ini updated!');
|
||||
|
||||
return back();
|
||||
return back()->with([
|
||||
'ini' => $request->input('ini'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function uninstall(Server $server, Request $request): RedirectResponse
|
||||
|
@ -2,11 +2,14 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Actions\Service\Create;
|
||||
use App\Facades\Toast;
|
||||
use App\Helpers\HtmxResponse;
|
||||
use App\Models\Server;
|
||||
use App\Models\Service;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ServiceController extends Controller
|
||||
{
|
||||
@ -62,4 +65,13 @@ public function disable(Server $server, Service $service): RedirectResponse
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
@ -7,13 +7,14 @@
|
||||
use App\Facades\Toast;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
|
||||
class HandleSSHErrors
|
||||
{
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
$res = $next($request);
|
||||
if ($res->exception) {
|
||||
if ($res instanceof Response && $res->exception) {
|
||||
if ($res->exception instanceof SSHConnectionError || $res->exception instanceof SSHCommandError) {
|
||||
Toast::error($res->exception->getMessage());
|
||||
|
||||
|
@ -4,6 +4,7 @@
|
||||
|
||||
use App\Actions\Service\Manage;
|
||||
use App\Exceptions\ServiceInstallationFailed;
|
||||
use App\SSH\Services\AddOnServices\AbstractAddOnService;
|
||||
use App\SSH\Services\Database\Database as DatabaseHandler;
|
||||
use App\SSH\Services\Firewall\Firewall as FirewallHandler;
|
||||
use App\SSH\Services\PHP\PHP as PHPHandler;
|
||||
@ -53,7 +54,9 @@ public static function boot(): void
|
||||
parent::boot();
|
||||
|
||||
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(
|
||||
): PHPHandler|WebserverHandler|DatabaseHandler|FirewallHandler|ProcessManagerHandler|RedisHandler {
|
||||
): PHPHandler|WebserverHandler|DatabaseHandler|FirewallHandler|ProcessManagerHandler|RedisHandler|AbstractAddOnService {
|
||||
$handler = config('core.service_handlers')[$this->name];
|
||||
|
||||
return new $handler($this);
|
||||
@ -81,26 +84,26 @@ public function validateInstall($result): void
|
||||
|
||||
public function start(): void
|
||||
{
|
||||
app(Manage::class)->start($this);
|
||||
$this->unit && app(Manage::class)->start($this);
|
||||
}
|
||||
|
||||
public function stop(): void
|
||||
{
|
||||
app(Manage::class)->stop($this);
|
||||
$this->unit && app(Manage::class)->stop($this);
|
||||
}
|
||||
|
||||
public function restart(): void
|
||||
{
|
||||
app(Manage::class)->restart($this);
|
||||
$this->unit && app(Manage::class)->restart($this);
|
||||
}
|
||||
|
||||
public function enable(): void
|
||||
{
|
||||
app(Manage::class)->enable($this);
|
||||
$this->unit && app(Manage::class)->enable($this);
|
||||
}
|
||||
|
||||
public function disable(): void
|
||||
{
|
||||
app(Manage::class)->disable($this);
|
||||
$this->unit && app(Manage::class)->disable($this);
|
||||
}
|
||||
}
|
||||
|
@ -38,7 +38,7 @@ public function boot(): void
|
||||
return new Toast;
|
||||
});
|
||||
|
||||
if (str(config('app.url'))->startsWith('https://')) {
|
||||
if (str(request()->url())->startsWith('https://')) {
|
||||
URL::forceScheme('https');
|
||||
}
|
||||
}
|
||||
|
@ -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->getScript('edit-file.sh', [
|
||||
'path' => $path,
|
||||
'content' => $content,
|
||||
'content' => $content ?? '',
|
||||
]),
|
||||
);
|
||||
}
|
||||
@ -131,4 +131,21 @@ public function runScript(string $path, string $script, ?int $siteId = null): Se
|
||||
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
3
app/SSH/OS/scripts/download.sh
Normal file
3
app/SSH/OS/scripts/download.sh
Normal file
@ -0,0 +1,3 @@
|
||||
if ! wget __url__ -O __path__; then
|
||||
echo 'VITO_SSH_ERROR' && exit 1
|
||||
fi
|
23
app/SSH/PHPMyAdmin/PHPMyAdmin.php
Normal file
23
app/SSH/PHPMyAdmin/PHPMyAdmin.php
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
25
app/SSH/PHPMyAdmin/scripts/install.sh
Executable file
25
app/SSH/PHPMyAdmin/scripts/install.sh
Executable 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!"
|
18
app/SSH/Services/AddOnServices/AbstractAddOnService.php
Normal file
18
app/SSH/Services/AddOnServices/AbstractAddOnService.php
Normal 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
49
app/SiteTypes/PHPMyAdmin.php
Executable 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);
|
||||
}
|
||||
}
|
@ -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) {
|
||||
$this->setLog($log, $siteId);
|
||||
@ -42,21 +42,19 @@ public function exec(string|array $commands, string $log = '', ?int $siteId = nu
|
||||
$this->log = null;
|
||||
}
|
||||
|
||||
if (! is_array($commands)) {
|
||||
$commands = [$commands];
|
||||
}
|
||||
|
||||
foreach ($commands as $command) {
|
||||
if (is_string($command)) {
|
||||
$this->commands[] = $command;
|
||||
} else {
|
||||
$this->commands[] = get_class($command);
|
||||
}
|
||||
}
|
||||
$this->commands[] = $command;
|
||||
|
||||
$output = $this->output ?? 'fake output';
|
||||
$this->log?->write($output);
|
||||
|
||||
if ($stream) {
|
||||
echo $output;
|
||||
ob_flush();
|
||||
flush();
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
|
@ -13,6 +13,7 @@
|
||||
use App\ServerProviders\Vultr;
|
||||
use App\SiteTypes\Laravel;
|
||||
use App\SiteTypes\PHPBlank;
|
||||
use App\SiteTypes\PHPMyAdmin;
|
||||
use App\SiteTypes\PHPSite;
|
||||
use App\SiteTypes\Wordpress;
|
||||
use App\SourceControlProviders\Bitbucket;
|
||||
@ -177,6 +178,9 @@
|
||||
'ufw' => Ufw::class,
|
||||
'supervisor' => Supervisor::class,
|
||||
],
|
||||
'add_on_services' => [
|
||||
// add-on services
|
||||
],
|
||||
'service_units' => [
|
||||
'nginx' => [
|
||||
'ubuntu_18' => [
|
||||
@ -320,12 +324,14 @@
|
||||
\App\Enums\SiteType::PHP_BLANK,
|
||||
\App\Enums\SiteType::LARAVEL,
|
||||
\App\Enums\SiteType::WORDPRESS,
|
||||
\App\Enums\SiteType::PHPMYADMIN,
|
||||
],
|
||||
'site_types_class' => [
|
||||
\App\Enums\SiteType::PHP => PHPSite::class,
|
||||
\App\Enums\SiteType::PHP_BLANK => PHPBlank::class,
|
||||
\App\Enums\SiteType::LARAVEL => Laravel::class,
|
||||
\App\Enums\SiteType::WORDPRESS => Wordpress::class,
|
||||
\App\Enums\SiteType::PHPMYADMIN => PHPMyAdmin::class,
|
||||
],
|
||||
|
||||
/*
|
||||
|
1
public/build/assets/app-2c6e7578.css
Normal file
1
public/build/assets/app-2c6e7578.css
Normal file
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
@ -1,6 +1,6 @@
|
||||
{
|
||||
"resources/css/app.css": {
|
||||
"file": "assets/app-986a0fb7.css",
|
||||
"file": "assets/app-2c6e7578.css",
|
||||
"isEntry": true,
|
||||
"src": "resources/css/app.css"
|
||||
},
|
||||
@ -12,7 +12,7 @@
|
||||
"css": [
|
||||
"assets/app-a1ae07b3.css"
|
||||
],
|
||||
"file": "assets/app-e823d2ab.js",
|
||||
"file": "assets/app-5f99a92f.js",
|
||||
"isEntry": true,
|
||||
"src": "resources/js/app.js"
|
||||
}
|
||||
|
@ -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 |
@ -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
@ -26,7 +26,9 @@ document.body.addEventListener('htmx:configRequest', (event) => {
|
||||
if (window.getSelection) { window.getSelection().removeAllRanges(); }
|
||||
else if (document.selection) { document.selection.empty(); }
|
||||
});
|
||||
let activeElement = null;
|
||||
document.body.addEventListener('htmx:beforeRequest', (event) => {
|
||||
activeElement = document.activeElement;
|
||||
let targetElements = event.target.querySelectorAll('[hx-disable]');
|
||||
for (let i = 0; i < targetElements.length; i++) {
|
||||
targetElements[i].disabled = true;
|
||||
@ -38,6 +40,18 @@ document.body.addEventListener('htmx:afterRequest', (event) => {
|
||||
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';
|
||||
window.toastr = toastr;
|
||||
@ -49,13 +63,6 @@ window.toastr.options = {
|
||||
|
||||
import tippy from 'tippy.js';
|
||||
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]', {
|
||||
content(reference) {
|
||||
return reference.getAttribute('data-tooltip');
|
||||
|
6
resources/views/application/phpmyadmin-app.blade.php
Normal file
6
resources/views/application/phpmyadmin-app.blade.php
Normal 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>
|
@ -3,7 +3,7 @@
|
||||
<span>
|
||||
{{ __("Your Wordpress site is installed and ready to use! ") }}
|
||||
</span>
|
||||
<x-secondary-button :href="$site->url" target="_blank">
|
||||
<x-secondary-button :href="$site->getUrl()" target="_blank">
|
||||
{{ __("Open Website") }}
|
||||
</x-secondary-button>
|
||||
</x-simple-card>
|
||||
|
@ -11,31 +11,27 @@
|
||||
disabled: @js($disabled),
|
||||
lang: @js($lang),
|
||||
init() {
|
||||
document.body.addEventListener('htmx:afterSettle', (event) => {
|
||||
let editor = null
|
||||
let theme =
|
||||
document.documentElement.className === 'dark'
|
||||
? 'one-dark'
|
||||
: 'github'
|
||||
editor = window.ace.edit(this.editorId)
|
||||
let contentElement = document.getElementById(
|
||||
`text-${this.editorId}`,
|
||||
)
|
||||
editor.setValue(contentElement.innerText, 1)
|
||||
if (this.disabled) {
|
||||
editor.setReadOnly(true)
|
||||
}
|
||||
editor.getSession().setMode(`ace/mode/${this.lang}`)
|
||||
let editor = null
|
||||
let theme =
|
||||
document.documentElement.className === 'dark'
|
||||
? 'one-dark'
|
||||
: 'github'
|
||||
editor = window.ace.edit(this.editorId, {})
|
||||
let contentElement = document.getElementById(`text-${this.editorId}`)
|
||||
editor.setValue(contentElement.innerText, 1)
|
||||
if (this.disabled) {
|
||||
editor.setReadOnly(true)
|
||||
}
|
||||
editor.getSession().setMode(`ace/mode/${this.lang}`)
|
||||
editor.setTheme(`ace/theme/${theme}`)
|
||||
editor.setFontSize('15px')
|
||||
editor.setShowPrintMargin(false)
|
||||
editor.on('change', () => {
|
||||
contentElement.innerHTML = editor.getValue()
|
||||
})
|
||||
document.body.addEventListener('color-scheme-changed', (event) => {
|
||||
theme = event.detail.theme === 'dark' ? 'one-dark' : 'github'
|
||||
editor.setTheme(`ace/theme/${theme}`)
|
||||
editor.setFontSize('15px')
|
||||
editor.setShowPrintMargin(false)
|
||||
editor.on('change', () => {
|
||||
contentElement.innerHTML = editor.getValue()
|
||||
})
|
||||
document.body.addEventListener('color-scheme-changed', (event) => {
|
||||
theme = event.detail.theme === 'dark' ? 'one-dark' : 'github'
|
||||
editor.setTheme(`ace/theme/${theme}`)
|
||||
})
|
||||
})
|
||||
},
|
||||
}"
|
||||
|
@ -1,5 +1,5 @@
|
||||
<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 }}
|
||||
</div>
|
||||
|
@ -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 |
14
resources/views/components/heroicons/o-no-symbol.blade.php
Normal file
14
resources/views/components/heroicons/o-no-symbol.blade.php
Normal 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 |
@ -1,5 +1,6 @@
|
||||
@props([
|
||||
"href",
|
||||
"disabled" => false,
|
||||
])
|
||||
|
||||
@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";
|
||||
@endphp
|
||||
|
||||
@if (isset($href))
|
||||
@if (isset($href) && ! $disabled)
|
||||
<a href="{{ $href }}" {{ $attributes->merge(["class" => $class]) }}>
|
||||
{{ $slot }}
|
||||
</a>
|
||||
@else
|
||||
<button {{ $attributes->merge(["type" => "submit", "class" => $class]) }}>
|
||||
<button {{ $attributes->merge(["type" => "submit", "class" => $class, "disabled" => $disabled]) }}>
|
||||
{{ $slot }}
|
||||
</button>
|
||||
@endif
|
||||
|
@ -1,5 +1,5 @@
|
||||
<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 }}
|
||||
</div>
|
||||
|
@ -1,20 +1,17 @@
|
||||
@props([
|
||||
"interval" => "30s",
|
||||
"interval" => "7s",
|
||||
"id",
|
||||
"target" => null,
|
||||
])
|
||||
|
||||
<div
|
||||
id="{{ $id }}"
|
||||
hx-get="{{ request()->getUri() }}"
|
||||
hx-trigger="every {{ $interval }}"
|
||||
{{ $attributes->merge(["interval" => $interval, "id" => $id, "hx-get" => request()->getUri(), "hx-trigger" => "every " . $interval, "hx-swap" => "outerHTML"]) }}
|
||||
@if ($target)
|
||||
hx-target="{{ $target }}"
|
||||
hx-select="{{ $target }}"
|
||||
@else
|
||||
hx-select="#{{ $id }}"
|
||||
@endif
|
||||
hx-swap="outerHTML"
|
||||
>
|
||||
{{ $slot }}
|
||||
</div>
|
||||
|
@ -7,7 +7,7 @@
|
||||
|
||||
@php
|
||||
$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
|
||||
|
||||
@if (isset($href))
|
||||
|
104
resources/views/console/index.blade.php
Normal file
104
resources/views/console/index.blade.php
Normal 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>
|
@ -1,5 +1,5 @@
|
||||
<div data-tooltip="Project" class="cursor-pointer">
|
||||
<x-dropdown align="left">
|
||||
<x-dropdown width="full">
|
||||
<x-slot:trigger>
|
||||
<div>
|
||||
<div
|
||||
|
@ -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>
|
||||
</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>
|
||||
<x-sidebar-link
|
||||
:href="route('servers.settings', ['server' => $server])"
|
||||
|
@ -6,45 +6,47 @@
|
||||
</x-slot>
|
||||
</x-card-header>
|
||||
|
||||
<a class="block">
|
||||
<x-item-card>
|
||||
<div class="flex items-start justify-center">
|
||||
<span class="mr-2">PHP {{ $defaultPHP->version }}</span>
|
||||
@include("services.partials.status", ["status" => $defaultPHP->status])
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="inline">
|
||||
<x-dropdown>
|
||||
<x-slot name="trigger">
|
||||
<x-secondary-button>
|
||||
{{ __("Change") }}
|
||||
<x-heroicon name="o-chevron-up-down" class="ml-1 h-5 w-5" />
|
||||
</x-secondary-button>
|
||||
</x-slot>
|
||||
<x-slot name="content">
|
||||
@foreach ($phps as $php)
|
||||
@if ($php->version != $defaultPHP->version)
|
||||
<x-dropdown-link
|
||||
id="php-{{ $php->id }}-default-cli"
|
||||
class="cursor-pointer"
|
||||
hx-post="{{ route('servers.php.default-cli', ['server' => $server, 'version' => $php->version]) }}"
|
||||
hx-swap="outerHTML"
|
||||
hx-select="#php-{{ $php->id }}-default-cli"
|
||||
>
|
||||
PHP {{ $php->version }}
|
||||
<x-live id="php-default-cli">
|
||||
<a class="block">
|
||||
<x-item-card>
|
||||
<div class="flex items-start justify-center">
|
||||
<span class="mr-2">PHP {{ $defaultPHP->version }}</span>
|
||||
@include("services.partials.status", ["status" => $defaultPHP->status])
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="inline">
|
||||
<x-dropdown>
|
||||
<x-slot name="trigger">
|
||||
<x-secondary-button>
|
||||
{{ __("Change") }}
|
||||
<x-heroicon name="o-chevron-up-down" class="ml-1 h-5 w-5" />
|
||||
</x-secondary-button>
|
||||
</x-slot>
|
||||
<x-slot name="content">
|
||||
@foreach ($phps as $php)
|
||||
@if ($php->version != $defaultPHP->version)
|
||||
<x-dropdown-link
|
||||
id="php-{{ $php->id }}-default-cli"
|
||||
class="cursor-pointer"
|
||||
hx-post="{{ route('servers.php.default-cli', ['server' => $server, 'version' => $php->version]) }}"
|
||||
hx-swap="outerHTML"
|
||||
hx-select="#php-{{ $php->id }}-default-cli"
|
||||
>
|
||||
PHP {{ $php->version }}
|
||||
</x-dropdown-link>
|
||||
@endif
|
||||
@endforeach
|
||||
|
||||
@if (count($phps) == 1)
|
||||
<x-dropdown-link>
|
||||
{{ __("No other versions") }}
|
||||
</x-dropdown-link>
|
||||
@endif
|
||||
@endforeach
|
||||
|
||||
@if (count($phps) == 1)
|
||||
<x-dropdown-link>
|
||||
{{ __("No other versions") }}
|
||||
</x-dropdown-link>
|
||||
@endif
|
||||
</x-slot>
|
||||
</x-dropdown>
|
||||
</x-slot>
|
||||
</x-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-item-card>
|
||||
</a>
|
||||
</x-item-card>
|
||||
</a>
|
||||
</x-live>
|
||||
</div>
|
||||
|
@ -39,7 +39,7 @@ class="cursor-pointer"
|
||||
</x-dropdown-link>
|
||||
<x-dropdown-link
|
||||
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-swap="outerHTML"
|
||||
hx-target="#update-php-ini-form"
|
||||
|
@ -2,4 +2,6 @@
|
||||
<x-slot name="pageTitle">{{ __("Services") }}</x-slot>
|
||||
|
||||
@include("services.partials.services-list")
|
||||
|
||||
{{-- @include("services.partials.add-ons") --}}
|
||||
</x-server-layout>
|
||||
|
@ -0,0 +1 @@
|
||||
@include("services.partials.unit-actions")
|
@ -0,0 +1 @@
|
||||
@include("services.partials.unit-actions")
|
@ -0,0 +1 @@
|
||||
@include("services.partials.unit-actions")
|
1
resources/views/services/partials/actions/php.blade.php
Normal file
1
resources/views/services/partials/actions/php.blade.php
Normal file
@ -0,0 +1 @@
|
||||
@include("services.partials.unit-actions")
|
@ -0,0 +1 @@
|
||||
@include("services.partials.unit-actions")
|
@ -0,0 +1 @@
|
||||
@include("services.partials.unit-actions")
|
@ -0,0 +1 @@
|
||||
@include("services.partials.unit-actions")
|
1
resources/views/services/partials/actions/ufw.blade.php
Normal file
1
resources/views/services/partials/actions/ufw.blade.php
Normal file
@ -0,0 +1 @@
|
||||
@include("services.partials.unit-actions")
|
33
resources/views/services/partials/add-ons.blade.php
Normal file
33
resources/views/services/partials/add-ons.blade.php
Normal 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>
|
@ -1,80 +1,42 @@
|
||||
<div>
|
||||
<x-card-header>
|
||||
<x-slot name="title">{{ __("Services") }}</x-slot>
|
||||
<x-slot name="description">
|
||||
{{ __("All services that we installed on your server are here") }}
|
||||
</x-slot>
|
||||
<x-slot name="title">Installed Services</x-slot>
|
||||
<x-slot name="description">All services that we installed on your server are here</x-slot>
|
||||
<x-slot name="aside"></x-slot>
|
||||
</x-card-header>
|
||||
|
||||
<x-live id="live-services" interval="5s">
|
||||
<div class="space-y-3">
|
||||
<x-live id="live-services">
|
||||
<div class="grid grid-cols-1 gap-5 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||
@foreach ($services as $service)
|
||||
<x-item-card>
|
||||
<div class="flex-none">
|
||||
<img src="{{ asset("static/images/" . $service->name . ".svg") }}" class="h-10 w-10" alt="" />
|
||||
<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="absolute right-3 top-3">
|
||||
@include("services.partials.status", ["status" => $service->status])
|
||||
</div>
|
||||
<div class="ml-3 flex flex-grow flex-col items-start justify-center">
|
||||
<div class="flex items-center">
|
||||
<div class="mr-2">{{ $service->name }}:{{ $service->version }}</div>
|
||||
@include("services.partials.status", ["status" => $service->status])
|
||||
<div class="space-y-3 p-5">
|
||||
<div class="flex items-center justify-center">
|
||||
<img
|
||||
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 class="flex items-center">
|
||||
<x-dropdown>
|
||||
<x-slot name="trigger">
|
||||
<x-secondary-button>
|
||||
{{ __("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
|
||||
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.actions." . $service->name)
|
||||
</div>
|
||||
</x-item-card>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</x-live>
|
||||
|
42
resources/views/services/partials/unit-actions.blade.php
Normal file
42
resources/views/services/partials/unit-actions.blade.php
Normal 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>
|
13
resources/views/sites/partials/create/phpmyadmin.blade.php
Normal file
13
resources/views/sites/partials/create/phpmyadmin.blade.php
Normal 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>
|
@ -1,6 +1,7 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Controllers\ApplicationController;
|
||||
use App\Http\Controllers\ConsoleController;
|
||||
use App\Http\Controllers\CronjobController;
|
||||
use App\Http\Controllers\DatabaseBackupController;
|
||||
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}/enable', [ServiceController::class, 'enable'])->name('servers.services.enable');
|
||||
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
|
||||
|
@ -23,7 +23,7 @@ public function test_visit_application()
|
||||
'site' => $this->site,
|
||||
])
|
||||
)
|
||||
->assertOk()
|
||||
->assertSuccessful()
|
||||
->assertSee($this->site->domain);
|
||||
}
|
||||
|
||||
@ -88,7 +88,7 @@ public function test_deploy(): void
|
||||
'server' => $this->server,
|
||||
'site' => $this->site,
|
||||
]))
|
||||
->assertOk()
|
||||
->assertSuccessful()
|
||||
->assertSee('test commit message');
|
||||
|
||||
$deployment = $this->site->deployments()->first();
|
||||
|
46
tests/Feature/ConsoleTest.php
Normal file
46
tests/Feature/ConsoleTest.php
Normal 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');
|
||||
}
|
||||
}
|
@ -22,6 +22,7 @@ public function test_see_cronjobs_list()
|
||||
]);
|
||||
|
||||
$this->get(route('servers.cronjobs', $this->server))
|
||||
->assertSuccessful()
|
||||
->assertSeeText($cronjob->frequencyLabel());
|
||||
}
|
||||
|
||||
|
@ -98,6 +98,7 @@ public function test_see_backups_list(): void
|
||||
]);
|
||||
|
||||
$this->get(route('servers.databases.backups', [$this->server, $backup]))
|
||||
->assertSuccessful()
|
||||
->assertSee($backup->database->name);
|
||||
}
|
||||
|
||||
|
@ -66,6 +66,7 @@ public function test_see_databases_list(): void
|
||||
]);
|
||||
|
||||
$this->get(route('servers.databases', $this->server))
|
||||
->assertSuccessful()
|
||||
->assertSee($database->name);
|
||||
}
|
||||
|
||||
|
@ -58,6 +58,7 @@ public function test_see_database_users_list(): void
|
||||
]);
|
||||
|
||||
$this->get(route('servers.databases', $this->server))
|
||||
->assertSuccessful()
|
||||
->assertSee($databaseUser->username);
|
||||
}
|
||||
|
||||
|
@ -41,6 +41,7 @@ public function test_see_firewall_rules(): void
|
||||
]);
|
||||
|
||||
$this->get(route('servers.firewall', $this->server))
|
||||
->assertSuccessful()
|
||||
->assertSee($rule->source)
|
||||
->assertSee($rule->port);
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ public function test_see_logs()
|
||||
]);
|
||||
|
||||
$this->get(route('servers.logs', $this->server))
|
||||
->assertSuccessful()
|
||||
->assertSeeText($log->type);
|
||||
}
|
||||
}
|
||||
|
@ -194,6 +194,7 @@ public function test_see_channels_list(): void
|
||||
$channel = \App\Models\NotificationChannel::factory()->create();
|
||||
|
||||
$this->get(route('notification-channels'))
|
||||
->assertSuccessful()
|
||||
->assertSee($channel->provider);
|
||||
}
|
||||
|
||||
|
@ -16,6 +16,7 @@ public function test_profile_page_is_displayed(): void
|
||||
|
||||
$this
|
||||
->get(route('profile'))
|
||||
->assertSuccessful()
|
||||
->assertSee('Profile Information')
|
||||
->assertSee('Update Password')
|
||||
->assertSee('Two Factor Authentication');
|
||||
|
@ -32,6 +32,7 @@ public function test_see_projects_list(): void
|
||||
]);
|
||||
|
||||
$this->get(route('projects'))
|
||||
->assertSuccessful()
|
||||
->assertSee($project->name);
|
||||
}
|
||||
|
||||
|
@ -27,7 +27,7 @@ public function test_see_queues()
|
||||
'site' => $this->site,
|
||||
])
|
||||
)
|
||||
->assertOk()
|
||||
->assertSuccessful()
|
||||
->assertSee($queue->command);
|
||||
}
|
||||
|
||||
|
@ -27,6 +27,7 @@ public function test_see_server_keys()
|
||||
]);
|
||||
|
||||
$this->get(route('servers.ssh-keys', $this->server))
|
||||
->assertSuccessful()
|
||||
->assertSeeText('My first key');
|
||||
}
|
||||
|
||||
|
@ -70,6 +70,7 @@ public function test_see_providers_list(): void
|
||||
]);
|
||||
|
||||
$this->get(route('server-providers'))
|
||||
->assertSuccessful()
|
||||
->assertSee($provider->profile);
|
||||
}
|
||||
|
||||
|
@ -16,6 +16,8 @@ public function test_see_services_list(): void
|
||||
$this->actingAs($this->user);
|
||||
|
||||
$this->get(route('servers.services', $this->server))
|
||||
->assertSuccessful()
|
||||
->assertSee('mysql')
|
||||
->assertSee('nginx')
|
||||
->assertSee('php')
|
||||
->assertSee('supervisor')
|
||||
@ -242,6 +244,7 @@ public static function data(): array
|
||||
['redis'],
|
||||
['ufw'],
|
||||
['php'],
|
||||
['mysql'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -57,7 +57,7 @@ public function test_see_sites_list(): void
|
||||
$this->get(route('servers.sites', [
|
||||
'server' => $this->server,
|
||||
]))
|
||||
->assertOk()
|
||||
->assertSuccessful()
|
||||
->assertSee($site->domain);
|
||||
}
|
||||
|
||||
@ -158,6 +158,15 @@ public static function create_data(): array
|
||||
'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,
|
||||
'site' => $this->site,
|
||||
]))
|
||||
->assertOk()
|
||||
->assertSuccessful()
|
||||
->assertSee('Logs');
|
||||
}
|
||||
}
|
||||
|
@ -29,6 +29,7 @@ public function test_get_public_keys_list(): void
|
||||
]);
|
||||
|
||||
$this->get(route('ssh-keys'))
|
||||
->assertSuccessful()
|
||||
->assertSee($key->name);
|
||||
}
|
||||
|
||||
|
@ -25,7 +25,7 @@ public function test_see_ssls_list()
|
||||
'server' => $this->server,
|
||||
'site' => $this->site,
|
||||
]))
|
||||
->assertOk()
|
||||
->assertSuccessful()
|
||||
->assertSee($ssl->type);
|
||||
}
|
||||
|
||||
@ -37,7 +37,7 @@ public function test_see_ssls_list_with_no_ssls()
|
||||
'server' => $this->server,
|
||||
'site' => $this->site,
|
||||
]))
|
||||
->assertOk()
|
||||
->assertSuccessful()
|
||||
->assertSeeText(__("You don't have any SSL certificates yet!"));
|
||||
}
|
||||
|
||||
|
@ -41,6 +41,7 @@ public function test_see_providers_list(): void
|
||||
]);
|
||||
|
||||
$this->get(route('storage-providers'))
|
||||
->assertSuccessful()
|
||||
->assertSee($provider->profile);
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user