mirror of
https://github.com/vitodeploy/vito.git
synced 2025-07-01 14:06:15 +00:00
Compare commits
20 Commits
3.0.0-alph
...
3.0.0-beta
Author | SHA1 | Date | |
---|---|---|---|
1da1d5d413 | |||
f3df6b8673 | |||
f532b56abb | |||
194143d7ef | |||
73c836bfe7 | |||
ad5af0cd9e | |||
15d8cb1705 | |||
f4414a931e | |||
a593525939 | |||
a2841f673b | |||
231e90076f | |||
dc7fa6b55c | |||
5689e751af | |||
e59448d30a | |||
346bd03f4f | |||
085504605b | |||
d1a7eb24a7 | |||
c10a3ee4bb | |||
dd14e69239 | |||
736e27fa4e |
15
README.md
15
README.md
@ -1,5 +1,5 @@
|
||||
<p align="center">
|
||||
<img src="https://github.com/user-attachments/assets/57f77bd5-bd3f-4367-84c0-aff6ecd392b4" alt="VitoDeploy>
|
||||
<img src="https://github.com/user-attachments/assets/b06531f3-2066-436e-a0e3-0a5f1b7d5118" alt="VitoDeploy>
|
||||
<p align="center">
|
||||
<a href="https://github.com/vitodeploy/vito/actions"><img alt="GitHub Workflow Status" src="https://github.com/vitodeploy/vito/workflows/tests/badge.svg"></a>
|
||||
</p>
|
||||
@ -14,10 +14,18 @@ ## About Vito
|
||||
|
||||
## Quick Start
|
||||
|
||||
Version 3 (Alpha):
|
||||
|
||||
```sh
|
||||
bash <(curl -Ls https://raw.githubusercontent.com/vitodeploy/vito/3.x/scripts/install.sh)
|
||||
```
|
||||
|
||||
Version 2:
|
||||
|
||||
```sh
|
||||
bash <(curl -Ls https://raw.githubusercontent.com/vitodeploy/vito/2.x/scripts/install.sh)
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- Provisions and Manages the server
|
||||
@ -30,6 +38,8 @@ ## Features
|
||||
- Deploy your SSH Keys to the server
|
||||
- Create and Manage cron jobs on the server
|
||||
- API
|
||||
- Plugins
|
||||
- Export and Import
|
||||
|
||||
## Useful Links
|
||||
|
||||
@ -46,6 +56,7 @@ ## Credits
|
||||
|
||||
- Laravel
|
||||
- InertiaJS
|
||||
- ReactJS
|
||||
- Shadcn UI
|
||||
- PHPSecLib
|
||||
- PHPUnit
|
||||
@ -53,3 +64,5 @@ ## Credits
|
||||
- Vite
|
||||
- Prettier
|
||||
- Spatie
|
||||
- Opcodesio log viewer
|
||||
- Tightenco
|
||||
|
11
SECURITY.md
11
SECURITY.md
@ -2,11 +2,12 @@ # Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
| Version | New Features | Bug Fixes | Security Fixes |
|
||||
|---------|--------------|-----------|----------------------|
|
||||
| 0.x | ❌ | ❌ | ❌ |
|
||||
| 1.x | ❌ | ❌ | ✅ (Until March 2025) |
|
||||
| 2.x | ✅ | ✅ | ✅ |
|
||||
| Version | New Features | Bug Fixes | Security Fixes |
|
||||
|---------|--------------|-----------|----------------|
|
||||
| 0.x | ❌ | ❌ | ❌ |
|
||||
| 1.x | ❌ | ❌ | ❌ |
|
||||
| 2.x | ❌ | ✅ | ✅ |
|
||||
| 3.x | ✅ | ✅ | ✅ |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
|
@ -65,7 +65,7 @@ public function delete(Backup $backup): void
|
||||
}
|
||||
|
||||
$backup->delete();
|
||||
});
|
||||
})->onQueue('ssh');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -30,6 +30,6 @@ public function delete(BackupFile $file): void
|
||||
|
||||
dispatch(function () use ($file): void {
|
||||
$file->deleteFile();
|
||||
});
|
||||
})->onQueue('ssh');
|
||||
}
|
||||
}
|
||||
|
@ -37,7 +37,7 @@ public function restore(BackupFile $backupFile, array $input): void
|
||||
})->catch(function () use ($backupFile): void {
|
||||
$backupFile->status = BackupFileStatus::RESTORE_FAILED;
|
||||
$backupFile->save();
|
||||
})->onConnection('ssh');
|
||||
})->onQueue('ssh');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -39,7 +39,7 @@ public function run(Backup $backup): BackupFile
|
||||
$backup->save();
|
||||
$file->status = BackupFileStatus::FAILED;
|
||||
$file->save();
|
||||
})->onConnection('ssh');
|
||||
})->onQueue('ssh');
|
||||
|
||||
return $file;
|
||||
}
|
||||
|
@ -34,7 +34,7 @@ public function create(Server $server, array $input): FirewallRule
|
||||
|
||||
$rule->save();
|
||||
|
||||
dispatch(fn () => $this->applyRule($rule));
|
||||
dispatch(fn () => $this->applyRule($rule))->onQueue('ssh');
|
||||
|
||||
return $rule;
|
||||
}
|
||||
@ -58,7 +58,7 @@ public function update(FirewallRule $rule, array $input): FirewallRule
|
||||
'status' => FirewallRuleStatus::UPDATING,
|
||||
]);
|
||||
|
||||
dispatch(fn () => $this->applyRule($rule));
|
||||
dispatch(fn () => $this->applyRule($rule))->onQueue('ssh');
|
||||
|
||||
return $rule;
|
||||
}
|
||||
@ -68,7 +68,7 @@ public function delete(FirewallRule $rule): void
|
||||
$rule->status = FirewallRuleStatus::DELETING;
|
||||
$rule->save();
|
||||
|
||||
dispatch(fn () => $this->applyRule($rule));
|
||||
dispatch(fn () => $this->applyRule($rule))->onQueue('ssh');
|
||||
}
|
||||
|
||||
protected function applyRule(FirewallRule $rule): void
|
||||
|
@ -44,7 +44,7 @@ function () use ($service, $input): void {
|
||||
$typeData['extensions'] = array_values(array_diff($typeData['extensions'], [$input['extension']]));
|
||||
$service->type_data = $typeData;
|
||||
$service->save();
|
||||
})->onConnection('ssh');
|
||||
})->onQueue('ssh-unique');
|
||||
|
||||
return $service;
|
||||
}
|
||||
|
@ -43,7 +43,7 @@ public function create(Site $site, array $input): Redirect
|
||||
$redirect->status = RedirectStatus::FAILED;
|
||||
$redirect->save();
|
||||
})
|
||||
->onConnection('ssh');
|
||||
->onQueue('ssh-unique');
|
||||
|
||||
return $redirect->refresh();
|
||||
}
|
||||
|
@ -27,6 +27,6 @@ public function delete(Site $site, Redirect $redirect): void
|
||||
})->catch(function () use ($redirect): void {
|
||||
$redirect->status = RedirectStatus::FAILED;
|
||||
$redirect->save();
|
||||
})->onConnection('ssh');
|
||||
})->onQueue('ssh-unique');
|
||||
}
|
||||
}
|
||||
|
@ -60,7 +60,7 @@ public function create(Site $site, array $input): void
|
||||
})->catch(function () use ($ssl): void {
|
||||
$ssl->status = SslStatus::FAILED;
|
||||
$ssl->save();
|
||||
})->onConnection('ssh');
|
||||
})->onQueue('ssh-unique');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -62,7 +62,7 @@ public function execute(Script $script, User $user, array $input): ScriptExecuti
|
||||
})->catch(function () use ($execution): void {
|
||||
$execution->status = ScriptExecutionStatus::FAILED;
|
||||
$execution->save();
|
||||
})->onConnection('ssh');
|
||||
})->onQueue('ssh');
|
||||
|
||||
return $execution;
|
||||
}
|
||||
|
@ -81,7 +81,7 @@ public function create(User $creator, Project $project, array $input): Server
|
||||
'error' => (string) $e,
|
||||
]);
|
||||
})
|
||||
->onConnection('ssh');
|
||||
->onQueue('ssh');
|
||||
|
||||
return $this->server;
|
||||
} catch (Exception $e) {
|
||||
@ -198,30 +198,10 @@ public function createFirewallRules(Server $server): void
|
||||
private function createServices(): void
|
||||
{
|
||||
$this->server->services()->forceDelete();
|
||||
$this->addSupervisor();
|
||||
$this->addRedis();
|
||||
$this->addUfw();
|
||||
$this->addMonitoring();
|
||||
}
|
||||
|
||||
private function addSupervisor(): void
|
||||
{
|
||||
$this->server->services()->create([
|
||||
'type' => 'process_manager',
|
||||
'name' => 'supervisor',
|
||||
'version' => 'latest',
|
||||
]);
|
||||
}
|
||||
|
||||
private function addRedis(): void
|
||||
{
|
||||
$this->server->services()->create([
|
||||
'type' => 'memory_database',
|
||||
'name' => 'redis',
|
||||
'version' => 'latest',
|
||||
]);
|
||||
}
|
||||
|
||||
private function addUfw(): void
|
||||
{
|
||||
$this->server->services()->create([
|
||||
|
@ -20,6 +20,6 @@ public function update(Server $server): void
|
||||
})->catch(function () use ($server): void {
|
||||
Notifier::send($server, new ServerUpdateFailed($server));
|
||||
$server->checkConnection();
|
||||
})->onConnection('ssh');
|
||||
})->onQueue('ssh-unique');
|
||||
}
|
||||
}
|
||||
|
@ -46,7 +46,7 @@ public function install(Server $server, array $input): Service
|
||||
})->catch(function () use ($service): void {
|
||||
$service->status = ServiceStatus::INSTALLATION_FAILED;
|
||||
$service->save();
|
||||
})->onConnection('ssh');
|
||||
})->onQueue('ssh-unique');
|
||||
|
||||
return $service;
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ public function start(Service $service): void
|
||||
$service->status = ServiceStatus::FAILED;
|
||||
}
|
||||
$service->save();
|
||||
})->onConnection('ssh');
|
||||
})->onQueue('ssh');
|
||||
}
|
||||
|
||||
public function stop(Service $service): void
|
||||
@ -37,7 +37,7 @@ public function stop(Service $service): void
|
||||
$service->status = ServiceStatus::FAILED;
|
||||
}
|
||||
$service->save();
|
||||
})->onConnection('ssh');
|
||||
})->onQueue('ssh');
|
||||
}
|
||||
|
||||
public function restart(Service $service): void
|
||||
@ -53,7 +53,7 @@ public function restart(Service $service): void
|
||||
$service->status = ServiceStatus::FAILED;
|
||||
}
|
||||
$service->save();
|
||||
})->onConnection('ssh');
|
||||
})->onQueue('ssh');
|
||||
}
|
||||
|
||||
public function enable(Service $service): void
|
||||
@ -69,7 +69,7 @@ public function enable(Service $service): void
|
||||
$service->status = ServiceStatus::FAILED;
|
||||
}
|
||||
$service->save();
|
||||
})->onConnection('ssh');
|
||||
})->onQueue('ssh');
|
||||
}
|
||||
|
||||
public function disable(Service $service): void
|
||||
@ -85,7 +85,7 @@ public function disable(Service $service): void
|
||||
$service->status = ServiceStatus::FAILED;
|
||||
}
|
||||
$service->save();
|
||||
})->onConnection('ssh');
|
||||
})->onQueue('ssh');
|
||||
}
|
||||
|
||||
private function validate(Service $service): void
|
||||
|
@ -26,6 +26,6 @@ public function uninstall(Service $service): void
|
||||
})->catch(function () use ($service): void {
|
||||
$service->status = ServiceStatus::FAILED;
|
||||
$service->save();
|
||||
})->onConnection('ssh');
|
||||
})->onQueue('ssh-unique');
|
||||
}
|
||||
}
|
||||
|
@ -92,7 +92,7 @@ public function create(Server $server, array $input): Site
|
||||
$site->status = SiteStatus::INSTALLATION_FAILED;
|
||||
$site->save();
|
||||
Notifier::send($site, new SiteInstallationFailed($site));
|
||||
})->onConnection('ssh');
|
||||
})->onQueue('ssh-unique');
|
||||
|
||||
DB::commit();
|
||||
|
||||
|
@ -57,7 +57,7 @@ public function run(Site $site): Deployment
|
||||
$deployment->status = DeploymentStatus::FAILED;
|
||||
$deployment->save();
|
||||
Notifier::send($site, new DeploymentCompleted($deployment, $site));
|
||||
})->onConnection('ssh');
|
||||
})->onQueue('ssh-unique');
|
||||
|
||||
return $deployment;
|
||||
}
|
||||
|
@ -55,7 +55,7 @@ public function execute(Command $command, User $user, array $input): CommandExec
|
||||
})->catch(function () use ($execution): void {
|
||||
$execution->status = CommandExecutionStatus::FAILED;
|
||||
$execution->save();
|
||||
})->onConnection('ssh');
|
||||
})->onQueue('ssh');
|
||||
|
||||
return $execution;
|
||||
}
|
||||
|
@ -17,12 +17,15 @@ public function update(Site $site, array $input): void
|
||||
{
|
||||
Validator::make($input, [
|
||||
'env' => ['required', 'string'],
|
||||
'path' => ['required', 'string'],
|
||||
])->validate();
|
||||
|
||||
$site->server->os()->write(
|
||||
$site->path.'/.env',
|
||||
$input['path'],
|
||||
trim((string) $input['env']),
|
||||
$site->user,
|
||||
);
|
||||
|
||||
$site->jsonUpdate('type_data', 'env_path', $input['path']);
|
||||
}
|
||||
}
|
||||
|
@ -55,7 +55,7 @@ public function create(Server $server, array $input, ?Site $site = null): void
|
||||
$worker->save();
|
||||
})->catch(function () use ($worker): void {
|
||||
$worker->delete();
|
||||
})->onConnection('ssh');
|
||||
})->onQueue('ssh');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -55,7 +55,7 @@ public function edit(Worker $worker, array $input): void
|
||||
})->catch(function () use ($worker): void {
|
||||
$worker->status = WorkerStatus::FAILED;
|
||||
$worker->save();
|
||||
})->onConnection('ssh');
|
||||
})->onQueue('ssh');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -21,7 +21,7 @@ public function start(Worker $worker): void
|
||||
$handler->start($worker->id, $worker->site_id);
|
||||
$worker->status = WorkerStatus::RUNNING;
|
||||
$worker->save();
|
||||
})->onConnection('ssh');
|
||||
})->onQueue('ssh');
|
||||
}
|
||||
|
||||
public function stop(Worker $worker): void
|
||||
@ -36,7 +36,7 @@ public function stop(Worker $worker): void
|
||||
$handler->stop($worker->id, $worker->site_id);
|
||||
$worker->status = WorkerStatus::STOPPED;
|
||||
$worker->save();
|
||||
})->onConnection('ssh');
|
||||
})->onQueue('ssh');
|
||||
}
|
||||
|
||||
public function restart(Worker $worker): void
|
||||
@ -51,6 +51,6 @@ public function restart(Worker $worker): void
|
||||
$handler->restart($worker->id, $worker->site_id);
|
||||
$worker->status = WorkerStatus::RUNNING;
|
||||
$worker->save();
|
||||
})->onConnection('ssh');
|
||||
})->onQueue('ssh');
|
||||
}
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ public function handle(): void
|
||||
return;
|
||||
}
|
||||
$server->checkConnection();
|
||||
})->onConnection('ssh');
|
||||
})->onQueue('ssh');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ class Agent extends MobileDetect
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
protected static $additionalOperatingSystems = [
|
||||
protected static array $additionalOperatingSystems = [
|
||||
'Windows' => 'Windows',
|
||||
'Windows NT' => 'Windows NT',
|
||||
'OS X' => 'Mac OS X',
|
||||
@ -29,7 +29,7 @@ class Agent extends MobileDetect
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
protected static $additionalBrowsers = [
|
||||
protected static array $additionalBrowsers = [
|
||||
'Opera Mini' => 'Opera Mini',
|
||||
'Opera' => 'Opera|OPR',
|
||||
'Edge' => 'Edge|Edg',
|
||||
@ -50,14 +50,12 @@ class Agent extends MobileDetect
|
||||
*
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
protected $store = [];
|
||||
protected array $store = [];
|
||||
|
||||
/**
|
||||
* Get the platform name from the User Agent.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function platform()
|
||||
public function platform(): ?string
|
||||
{
|
||||
return $this->retrieveUsingCacheOrResolve('paymently.platform', fn () => $this->findDetectionRulesAgainstUserAgent(
|
||||
$this->mergeRules(MobileDetect::getOperatingSystems(), static::$additionalOperatingSystems)
|
||||
|
@ -49,6 +49,18 @@ public function store(Request $request): RedirectResponse
|
||||
RateLimiter::clear($this->throttleKey());
|
||||
Session::regenerate();
|
||||
|
||||
if (user()->two_factor_secret) {
|
||||
$request->session()->invalidate();
|
||||
$request->session()->regenerateToken();
|
||||
|
||||
$request->session()->put([
|
||||
'login.id' => user()->id,
|
||||
'login.remember' => $request->boolean('remember'),
|
||||
]);
|
||||
|
||||
return redirect()->route('two-factor.login');
|
||||
}
|
||||
|
||||
return redirect()->intended(route('servers', absolute: false));
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\Auth\StatefulGuard;
|
||||
use Illuminate\Http\Exceptions\HttpResponseException;
|
||||
use Illuminate\Routing\Controller;
|
||||
use Inertia\Inertia;
|
||||
use Laravel\Fortify\Contracts\FailedTwoFactorLoginResponse;
|
||||
use Laravel\Fortify\Contracts\TwoFactorLoginResponse;
|
||||
use Laravel\Fortify\Events\RecoveryCodeReplaced;
|
||||
use Laravel\Fortify\Events\TwoFactorAuthenticationFailed;
|
||||
use Laravel\Fortify\Events\ValidTwoFactorAuthenticationCodeProvided;
|
||||
use Laravel\Fortify\Http\Requests\TwoFactorLoginRequest;
|
||||
use Spatie\RouteAttributes\Attributes\Get;
|
||||
use Spatie\RouteAttributes\Attributes\Post;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class TwoFactorAuthenticatedSessionController extends Controller
|
||||
{
|
||||
protected StatefulGuard $guard;
|
||||
|
||||
public function __construct(StatefulGuard $guard)
|
||||
{
|
||||
$this->guard = $guard;
|
||||
}
|
||||
|
||||
#[Get('two-factor', name: 'two-factor.login')]
|
||||
public function create(TwoFactorLoginRequest $request): \Inertia\Response
|
||||
{
|
||||
if (! $request->hasChallengedUser()) {
|
||||
throw new HttpResponseException(redirect()->route('login'));
|
||||
}
|
||||
|
||||
return Inertia::render('auth/two-factor');
|
||||
}
|
||||
|
||||
#[Post('two-factor', name: 'two-factor.store')]
|
||||
public function store(TwoFactorLoginRequest $request): TwoFactorLoginResponse|Response
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = $request->challengedUser();
|
||||
|
||||
if ($code = $request->validRecoveryCode()) {
|
||||
$user->replaceRecoveryCode($code);
|
||||
|
||||
event(new RecoveryCodeReplaced($user, $code));
|
||||
} elseif (! $request->hasValidCode()) {
|
||||
event(new TwoFactorAuthenticationFailed($user));
|
||||
|
||||
return app(FailedTwoFactorLoginResponse::class)->toResponse($request);
|
||||
}
|
||||
|
||||
event(new ValidTwoFactorAuthenticationCodeProvided($user));
|
||||
|
||||
$this->guard->login($user, $request->remember());
|
||||
|
||||
$request->session()->regenerate();
|
||||
|
||||
return redirect()->intended(route('servers', absolute: false));
|
||||
}
|
||||
}
|
@ -36,6 +36,10 @@ public function index(): Response
|
||||
#[Post('/install', name: 'plugins.install')]
|
||||
public function install(Request $request): RedirectResponse
|
||||
{
|
||||
if (config('app.demo')) {
|
||||
return back()->with('error', 'Plugins are disabled in demo mode.');
|
||||
}
|
||||
|
||||
$this->validate($request, [
|
||||
'url' => 'required|url',
|
||||
]);
|
||||
@ -54,8 +58,7 @@ public function install(Request $request): RedirectResponse
|
||||
}
|
||||
|
||||
Plugins::cleanup();
|
||||
})
|
||||
->onConnection('default');
|
||||
})->onQueue('default');
|
||||
|
||||
return back()->with('info', 'Plugin is being installed...');
|
||||
}
|
||||
@ -63,6 +66,10 @@ public function install(Request $request): RedirectResponse
|
||||
#[Delete('/uninstall', name: 'plugins.uninstall')]
|
||||
public function uninstall(Request $request): RedirectResponse
|
||||
{
|
||||
if (config('app.demo')) {
|
||||
return back()->with('error', 'Plugins are disabled in demo mode.');
|
||||
}
|
||||
|
||||
$this->validate($request, [
|
||||
'name' => 'required|string',
|
||||
]);
|
||||
@ -81,8 +88,7 @@ public function uninstall(Request $request): RedirectResponse
|
||||
}
|
||||
|
||||
Plugins::cleanup();
|
||||
})
|
||||
->onConnection('default');
|
||||
})->onQueue('default');
|
||||
|
||||
return back()->with('warning', 'Plugin is being uninstalled...');
|
||||
}
|
||||
|
@ -11,9 +11,12 @@
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
use Laravel\Fortify\Actions\DisableTwoFactorAuthentication;
|
||||
use Laravel\Fortify\Actions\EnableTwoFactorAuthentication;
|
||||
use Spatie\RouteAttributes\Attributes\Get;
|
||||
use Spatie\RouteAttributes\Attributes\Middleware;
|
||||
use Spatie\RouteAttributes\Attributes\Patch;
|
||||
use Spatie\RouteAttributes\Attributes\Post;
|
||||
use Spatie\RouteAttributes\Attributes\Prefix;
|
||||
use Spatie\RouteAttributes\Attributes\Put;
|
||||
|
||||
@ -65,4 +68,30 @@ public function password(Request $request): RedirectResponse
|
||||
|
||||
return to_route('profile');
|
||||
}
|
||||
|
||||
#[Post('/enable-two-factor', name: 'profile.enable-two-factor')]
|
||||
public function enableTwoFactor(): RedirectResponse
|
||||
{
|
||||
$user = user();
|
||||
|
||||
app(EnableTwoFactorAuthentication::class)($user);
|
||||
|
||||
return back()
|
||||
->with('success', 'Two factor authentication enabled.')
|
||||
->with('data', [
|
||||
'qr_code' => $user->twoFactorQrCodeSvg(),
|
||||
'qr_code_url' => $user->twoFactorQrCodeUrl(),
|
||||
'recovery_codes' => $user->recoveryCodes(),
|
||||
]);
|
||||
}
|
||||
|
||||
#[Post('/disable-two-factor', name: 'profile.disable-two-factor')]
|
||||
public function disableTwoFactor(): RedirectResponse
|
||||
{
|
||||
$user = user();
|
||||
|
||||
app(DisableTwoFactorAuthentication::class)($user);
|
||||
|
||||
return back()->with('success', 'Two factor authentication disabled.');
|
||||
}
|
||||
}
|
||||
|
@ -2,8 +2,10 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Spatie\RouteAttributes\Attributes\Get;
|
||||
use Spatie\RouteAttributes\Attributes\Middleware;
|
||||
use Spatie\RouteAttributes\Attributes\Prefix;
|
||||
@ -21,8 +23,65 @@ public function search(Request $request): JsonResponse
|
||||
|
||||
$query = $request->input('query');
|
||||
|
||||
$projects = DB::table('projects')
|
||||
->select(
|
||||
DB::raw('projects.id as id'),
|
||||
DB::raw('null as parent_id'),
|
||||
DB::raw('projects.name as label'),
|
||||
DB::raw('"project" as type')
|
||||
)
|
||||
->where(function (Builder $query) {
|
||||
if (! user()->isAdmin()) {
|
||||
$query
|
||||
->join('user_project', 'projects.id', '=', 'user_project.project_id')
|
||||
->where('user_project.user_id', user()->id);
|
||||
}
|
||||
})
|
||||
->where('projects.name', 'like', "%{$query}%");
|
||||
|
||||
$servers = DB::table('servers')
|
||||
->select(
|
||||
DB::raw('servers.id as id'),
|
||||
DB::raw('null as parent_id'),
|
||||
DB::raw('servers.name as label'),
|
||||
DB::raw('"server" as type')
|
||||
)
|
||||
->join('projects', 'servers.project_id', '=', 'projects.id')
|
||||
->where(function (Builder $query) {
|
||||
if (! user()->isAdmin()) {
|
||||
$query
|
||||
->join('user_project', 'projects.id', '=', 'user_project.project_id')
|
||||
->where('user_project.user_id', user()->id);
|
||||
}
|
||||
})
|
||||
->where('servers.name', 'like', "%{$query}%");
|
||||
|
||||
$sites = DB::table('sites')
|
||||
->select(
|
||||
DB::raw('sites.id as id'),
|
||||
DB::raw('sites.server_id as parent_id'),
|
||||
DB::raw('sites.domain as label'),
|
||||
DB::raw('"site" as type')
|
||||
)
|
||||
->join('servers', 'sites.server_id', '=', 'servers.id')
|
||||
->join('projects', 'servers.project_id', '=', 'projects.id')
|
||||
->where(function (Builder $query) {
|
||||
if (! user()->isAdmin()) {
|
||||
$query
|
||||
->join('user_project', 'projects.id', '=', 'user_project.project_id')
|
||||
->where('user_project.user_id', user()->id);
|
||||
}
|
||||
})
|
||||
->where('sites.domain', 'like', "%{$query}%");
|
||||
|
||||
// Combine with unionAll
|
||||
$results = $projects
|
||||
->unionAll($servers)
|
||||
->unionAll($sites)
|
||||
->get();
|
||||
|
||||
$results = [
|
||||
'data' => [], // Replace with actual search results
|
||||
'data' => $results, // Replace with actual search results
|
||||
];
|
||||
|
||||
return response()->json($results);
|
||||
|
@ -14,7 +14,6 @@
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
@ -93,18 +92,6 @@ public function switch(Server $server): RedirectResponse
|
||||
{
|
||||
$this->authorize('view', $server);
|
||||
|
||||
$previousUrl = URL::previous();
|
||||
$previousRequest = Request::create($previousUrl);
|
||||
$previousRoute = app('router')->getRoutes()->match($previousRequest);
|
||||
|
||||
if ($previousRoute->hasParameter('server')) {
|
||||
if (count($previousRoute->parameters()) > 1) {
|
||||
return redirect()->route('servers.show', ['server' => $server->id]);
|
||||
}
|
||||
|
||||
return redirect()->route($previousRoute->getName(), ['server' => $server]);
|
||||
}
|
||||
|
||||
return redirect()->route('servers.show', ['server' => $server->id]);
|
||||
}
|
||||
|
||||
|
@ -85,6 +85,10 @@ private function export(string $zipFileName): string
|
||||
#[Post('/import', name: 'vito-settings.import')]
|
||||
public function import(Request $request): RedirectResponse
|
||||
{
|
||||
if (config('app.demo')) {
|
||||
return back()->with('error', 'Import is disabled in demo mode.');
|
||||
}
|
||||
|
||||
// set session driver to file
|
||||
config(['session.driver' => 'file']);
|
||||
|
||||
|
@ -2,8 +2,10 @@
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Http\Resources\ProjectResource;
|
||||
use App\Http\Resources\ServerResource;
|
||||
use App\Http\Resources\SiteResource;
|
||||
use App\Http\Resources\UserResource;
|
||||
use App\Models\Server;
|
||||
use App\Models\Site;
|
||||
use App\Models\User;
|
||||
@ -55,12 +57,17 @@ public function share(Request $request): array
|
||||
|
||||
$data = [];
|
||||
if ($request->route('server')) {
|
||||
$data['server'] = ServerResource::make($request->route('server'));
|
||||
/** @var Server $server */
|
||||
$server = $request->route('server');
|
||||
if ($user && $user->can('view', $server) && $user->current_project_id !== $server->project_id) {
|
||||
$user->current_project_id = $server->project_id;
|
||||
$user->save();
|
||||
}
|
||||
|
||||
$data['server'] = ServerResource::make($server);
|
||||
|
||||
// sites
|
||||
$sites = [];
|
||||
/** @var Server $server */
|
||||
$server = $request->route('server');
|
||||
if ($user && $user->can('viewAny', [Site::class, $server])) {
|
||||
$sites = SiteResource::collection($server->sites);
|
||||
}
|
||||
@ -76,12 +83,14 @@ public function share(Request $request): array
|
||||
...parent::share($request),
|
||||
...$data,
|
||||
'name' => config('app.name'),
|
||||
'version' => config('app.version'),
|
||||
'demo' => config('app.demo'),
|
||||
'quote' => ['message' => trim($message), 'author' => trim($author)],
|
||||
'auth' => [
|
||||
'user' => $user,
|
||||
'projects' => $user?->allProjects()->get(),
|
||||
'currentProject' => $user?->currentProject,
|
||||
],
|
||||
'auth' => $user ? [
|
||||
'user' => UserResource::make($user->load('projects')),
|
||||
'projects' => ProjectResource::collection($user->allProjects()->get()),
|
||||
'currentProject' => ProjectResource::make($user->currentProject),
|
||||
] : null,
|
||||
'public_key_text' => __('servers.create.public_key_text', ['public_key' => get_public_key_content()]),
|
||||
'project_servers' => $servers,
|
||||
'configs' => [
|
||||
|
@ -19,6 +19,7 @@ public function toArray(Request $request): array
|
||||
'name' => $this->name,
|
||||
'email' => $this->email,
|
||||
'role' => $this->role,
|
||||
'two_factor_enabled' => (bool) $this->two_factor_secret,
|
||||
'created_at' => $this->created_at,
|
||||
'updated_at' => $this->updated_at,
|
||||
'projects' => ProjectResource::collection($this->whenLoaded('projects')),
|
||||
|
@ -8,6 +8,7 @@
|
||||
use App\Models\PersonalAccessToken;
|
||||
use App\Plugins\Plugins;
|
||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Laravel\Fortify\Fortify;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
@ -33,5 +34,12 @@ public function boot(): void
|
||||
$this->app->bind('plugins', fn (): Plugins => new Plugins);
|
||||
|
||||
Sanctum::usePersonalAccessTokenModel(PersonalAccessToken::class);
|
||||
Fortify::twoFactorChallengeView(function () {
|
||||
return view('app');
|
||||
});
|
||||
|
||||
if (config('app.force_https')) {
|
||||
URL::forceHttps();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
35
app/Providers/HorizonServiceProvider.php
Normal file
35
app/Providers/HorizonServiceProvider.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Horizon\Horizon;
|
||||
use Laravel\Horizon\HorizonApplicationServiceProvider;
|
||||
|
||||
class HorizonServiceProvider extends HorizonApplicationServiceProvider
|
||||
{
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
// Horizon::routeSmsNotificationsTo('15556667777');
|
||||
// Horizon::routeMailNotificationsTo('example@example.com');
|
||||
// Horizon::routeSlackNotificationsTo('slack-webhook-url', '#channel');
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the Horizon gate.
|
||||
*
|
||||
* This gate determines who can access Horizon in non-local environments.
|
||||
*/
|
||||
protected function gate(): void
|
||||
{
|
||||
Gate::define('viewHorizon', function (?User $user = null) {
|
||||
return $user?->isAdmin();
|
||||
});
|
||||
}
|
||||
}
|
@ -65,6 +65,7 @@ private function databases(): void
|
||||
->label('PostgreSQL')
|
||||
->handler(Postgresql::class)
|
||||
->versions([
|
||||
'17',
|
||||
'16',
|
||||
'15',
|
||||
'14',
|
||||
|
@ -55,7 +55,7 @@ public function data(array $input): array
|
||||
public function connect(array $credentials): bool
|
||||
{
|
||||
try {
|
||||
$connect = Http::withToken($credentials['token'])->get($this->apiUrl.'/account');
|
||||
$connect = Http::withToken($credentials['token'])->get($this->apiUrl.'/droplets');
|
||||
} catch (Exception) {
|
||||
throw new CouldNotConnectToProvider('DigitalOcean');
|
||||
}
|
||||
|
@ -34,15 +34,12 @@ public function createRules(array $input): array
|
||||
public function connect(): bool
|
||||
{
|
||||
try {
|
||||
ds($this->getApiUrl());
|
||||
$res = Http::withToken($this->data()['token'])
|
||||
->get($this->getApiUrl().'/version');
|
||||
} catch (Exception) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ds($res->status());
|
||||
|
||||
return $res->successful();
|
||||
}
|
||||
|
||||
|
@ -13,9 +13,9 @@
|
||||
"ext-intl": "*",
|
||||
"aws/aws-sdk-php": "^3.158",
|
||||
"inertiajs/inertia-laravel": "^2.0",
|
||||
"laradumps/laradumps": "^4.2",
|
||||
"laravel/fortify": "^1.17",
|
||||
"laravel/framework": "^12.0",
|
||||
"laravel/horizon": "^5.33",
|
||||
"laravel/sanctum": "^4.0",
|
||||
"laravel/tinker": "^2.8",
|
||||
"mobiledetect/mobiledetectlib": "^4.8",
|
||||
|
493
composer.lock
generated
493
composer.lock
generated
@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "fc8ee6c13042c4df2cd39b59c4d80ac1",
|
||||
"content-hash": "b7e394815fa2fc2447c357d65eda06e7",
|
||||
"packages": [
|
||||
{
|
||||
"name": "aws/aws-crt-php",
|
||||
@ -1377,147 +1377,6 @@
|
||||
},
|
||||
"time": "2025-04-10T15:08:36+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laradumps/laradumps",
|
||||
"version": "v4.2.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/laradumps/laradumps.git",
|
||||
"reference": "0cdd5fe9e20efc71c280a97a6aca3a05c19469f6"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/laradumps/laradumps/zipball/0cdd5fe9e20efc71c280a97a6aca3a05c19469f6",
|
||||
"reference": "0cdd5fe9e20efc71c280a97a6aca3a05c19469f6",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"illuminate/mail": "^10.0|^11.0|^12.0",
|
||||
"illuminate/support": "^10.0|^11.0|^12.0",
|
||||
"laradumps/laradumps-core": "^3.2.2",
|
||||
"nunomaduro/termwind": "^1.15.1|^2.0.1",
|
||||
"php": "^8.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"larastan/larastan": "^2.0|^3.0",
|
||||
"laravel/framework": "^10.0|^11.0|^12.0",
|
||||
"laravel/pint": "^1.17.2",
|
||||
"livewire/livewire": "^3.5.6",
|
||||
"mockery/mockery": "^1.6.12",
|
||||
"orchestra/testbench-core": "^8.0|^9.4|^10.0",
|
||||
"pestphp/pest": "^2.35.1|^3.7.0",
|
||||
"symfony/var-dumper": "^6.4.0|^7.1.3"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"LaraDumps\\LaraDumps\\LaraDumpsServiceProvider"
|
||||
]
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"src/functions.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"LaraDumps\\LaraDumps\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Luan Freitas",
|
||||
"email": "luanfreitas10@protonmail.com",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "LaraDumps is a friendly app designed to boost your Laravel PHP coding and debugging experience.",
|
||||
"homepage": "https://github.com/laradumps/laradumps",
|
||||
"support": {
|
||||
"issues": "https://github.com/laradumps/laradumps/issues",
|
||||
"source": "https://github.com/laradumps/laradumps/tree/v4.2.1"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/luanfreitasdev",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2025-06-12T13:38:57+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laradumps/laradumps-core",
|
||||
"version": "v3.2.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/laradumps/laradumps-core.git",
|
||||
"reference": "eb0e15805ac4061a524c43ed7c1e7796ef3326d8"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/laradumps/laradumps-core/zipball/eb0e15805ac4061a524c43ed7c1e7796ef3326d8",
|
||||
"reference": "eb0e15805ac4061a524c43ed7c1e7796ef3326d8",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-curl": "*",
|
||||
"nunomaduro/termwind": "^1.15|^2.0",
|
||||
"php": "^8.1",
|
||||
"ramsey/uuid": "^4.7.5",
|
||||
"spatie/backtrace": "^1.5",
|
||||
"symfony/console": "^5.4|^6.4|^7.0",
|
||||
"symfony/finder": "^5.4|^6.4|^7.0",
|
||||
"symfony/process": "^5.4|^6.4|^7.0",
|
||||
"symfony/var-dumper": "^5.4|^6.4|^7.0",
|
||||
"symfony/yaml": "^5.4|^6.4|^7.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"illuminate/support": "^10.46",
|
||||
"laravel/pint": "^1.13.7",
|
||||
"pestphp/pest": "^2.0|^3.0",
|
||||
"phpstan/phpstan": "^1.10.50"
|
||||
},
|
||||
"bin": [
|
||||
"bin/laradumps"
|
||||
],
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"files": [
|
||||
"src/functions.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"LaraDumps\\LaraDumpsCore\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Luan Freitas",
|
||||
"email": "luanfreitas10@protonmail.com",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "LaraDumps is a friendly app designed to boost your Laravel / PHP coding and debugging experience.",
|
||||
"homepage": "https://github.com/laradumps/laradumps-core",
|
||||
"support": {
|
||||
"issues": "https://github.com/laradumps/laradumps-core/issues",
|
||||
"source": "https://github.com/laradumps/laradumps-core/tree/v3.2.4"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/luanfreitasdev",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2025-05-16T14:48:30+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/fortify",
|
||||
"version": "v1.27.0",
|
||||
@ -1798,6 +1657,86 @@
|
||||
},
|
||||
"time": "2025-06-18T12:56:23+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/horizon",
|
||||
"version": "v5.33.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/laravel/horizon.git",
|
||||
"reference": "50057bca1f1dcc9fbd5ff6d65143833babd784b3"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/laravel/horizon/zipball/50057bca1f1dcc9fbd5ff6d65143833babd784b3",
|
||||
"reference": "50057bca1f1dcc9fbd5ff6d65143833babd784b3",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-json": "*",
|
||||
"ext-pcntl": "*",
|
||||
"ext-posix": "*",
|
||||
"illuminate/contracts": "^9.21|^10.0|^11.0|^12.0",
|
||||
"illuminate/queue": "^9.21|^10.0|^11.0|^12.0",
|
||||
"illuminate/support": "^9.21|^10.0|^11.0|^12.0",
|
||||
"nesbot/carbon": "^2.17|^3.0",
|
||||
"php": "^8.0",
|
||||
"ramsey/uuid": "^4.0",
|
||||
"symfony/console": "^6.0|^7.0",
|
||||
"symfony/error-handler": "^6.0|^7.0",
|
||||
"symfony/polyfill-php83": "^1.28",
|
||||
"symfony/process": "^6.0|^7.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"mockery/mockery": "^1.0",
|
||||
"orchestra/testbench": "^7.0|^8.0|^9.0|^10.0",
|
||||
"phpstan/phpstan": "^1.10",
|
||||
"phpunit/phpunit": "^9.0|^10.4|^11.5",
|
||||
"predis/predis": "^1.1|^2.0"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-redis": "Required to use the Redis PHP driver.",
|
||||
"predis/predis": "Required when not using the Redis PHP driver (^1.1|^2.0)."
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"aliases": {
|
||||
"Horizon": "Laravel\\Horizon\\Horizon"
|
||||
},
|
||||
"providers": [
|
||||
"Laravel\\Horizon\\HorizonServiceProvider"
|
||||
]
|
||||
},
|
||||
"branch-alias": {
|
||||
"dev-master": "6.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Laravel\\Horizon\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Taylor Otwell",
|
||||
"email": "taylor@laravel.com"
|
||||
}
|
||||
],
|
||||
"description": "Dashboard and code-driven configuration for Laravel queues.",
|
||||
"keywords": [
|
||||
"laravel",
|
||||
"queue"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/laravel/horizon/issues",
|
||||
"source": "https://github.com/laravel/horizon/tree/v5.33.1"
|
||||
},
|
||||
"time": "2025-06-16T13:48:30+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/prompts",
|
||||
"version": "v0.3.5",
|
||||
@ -4465,69 +4404,6 @@
|
||||
},
|
||||
"time": "2025-06-01T06:28:46+00:00"
|
||||
},
|
||||
{
|
||||
"name": "spatie/backtrace",
|
||||
"version": "1.7.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/spatie/backtrace.git",
|
||||
"reference": "cd37a49fce7137359ac30ecc44ef3e16404cccbe"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/spatie/backtrace/zipball/cd37a49fce7137359ac30ecc44ef3e16404cccbe",
|
||||
"reference": "cd37a49fce7137359ac30ecc44ef3e16404cccbe",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.3 || ^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"ext-json": "*",
|
||||
"laravel/serializable-closure": "^1.3 || ^2.0",
|
||||
"phpunit/phpunit": "^9.3 || ^11.4.3",
|
||||
"spatie/phpunit-snapshot-assertions": "^4.2 || ^5.1.6",
|
||||
"symfony/var-dumper": "^5.1 || ^6.0 || ^7.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Spatie\\Backtrace\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Freek Van de Herten",
|
||||
"email": "freek@spatie.be",
|
||||
"homepage": "https://spatie.be",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "A better backtrace",
|
||||
"homepage": "https://github.com/spatie/backtrace",
|
||||
"keywords": [
|
||||
"Backtrace",
|
||||
"spatie"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/spatie/backtrace/tree/1.7.4"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/sponsors/spatie",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://spatie.be/open-source/support-us",
|
||||
"type": "other"
|
||||
}
|
||||
],
|
||||
"time": "2025-05-08T15:41:09+00:00"
|
||||
},
|
||||
{
|
||||
"name": "spatie/laravel-route-attributes",
|
||||
"version": "1.25.2",
|
||||
@ -6834,78 +6710,6 @@
|
||||
],
|
||||
"time": "2025-04-27T18:39:23+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/yaml",
|
||||
"version": "v7.3.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/yaml.git",
|
||||
"reference": "cea40a48279d58dc3efee8112634cb90141156c2"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/yaml/zipball/cea40a48279d58dc3efee8112634cb90141156c2",
|
||||
"reference": "cea40a48279d58dc3efee8112634cb90141156c2",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.2",
|
||||
"symfony/deprecation-contracts": "^2.5|^3.0",
|
||||
"symfony/polyfill-ctype": "^1.8"
|
||||
},
|
||||
"conflict": {
|
||||
"symfony/console": "<6.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/console": "^6.4|^7.0"
|
||||
},
|
||||
"bin": [
|
||||
"Resources/bin/yaml-lint"
|
||||
],
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\Yaml\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Loads and dumps YAML files",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/yaml/tree/v7.3.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-04-04T10:10:33+00:00"
|
||||
},
|
||||
{
|
||||
"name": "tightenco/ziggy",
|
||||
"version": "v2.5.3",
|
||||
@ -9854,6 +9658,69 @@
|
||||
],
|
||||
"time": "2024-02-20T11:51:46+00:00"
|
||||
},
|
||||
{
|
||||
"name": "spatie/backtrace",
|
||||
"version": "1.7.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/spatie/backtrace.git",
|
||||
"reference": "cd37a49fce7137359ac30ecc44ef3e16404cccbe"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/spatie/backtrace/zipball/cd37a49fce7137359ac30ecc44ef3e16404cccbe",
|
||||
"reference": "cd37a49fce7137359ac30ecc44ef3e16404cccbe",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.3 || ^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"ext-json": "*",
|
||||
"laravel/serializable-closure": "^1.3 || ^2.0",
|
||||
"phpunit/phpunit": "^9.3 || ^11.4.3",
|
||||
"spatie/phpunit-snapshot-assertions": "^4.2 || ^5.1.6",
|
||||
"symfony/var-dumper": "^5.1 || ^6.0 || ^7.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Spatie\\Backtrace\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Freek Van de Herten",
|
||||
"email": "freek@spatie.be",
|
||||
"homepage": "https://spatie.be",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "A better backtrace",
|
||||
"homepage": "https://github.com/spatie/backtrace",
|
||||
"keywords": [
|
||||
"Backtrace",
|
||||
"spatie"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/spatie/backtrace/tree/1.7.4"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/sponsors/spatie",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://spatie.be/open-source/support-us",
|
||||
"type": "other"
|
||||
}
|
||||
],
|
||||
"time": "2025-05-08T15:41:09+00:00"
|
||||
},
|
||||
{
|
||||
"name": "spatie/data-transfer-object",
|
||||
"version": "3.9.1",
|
||||
@ -10364,6 +10231,78 @@
|
||||
],
|
||||
"time": "2025-05-15T09:04:05+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/yaml",
|
||||
"version": "v7.3.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/yaml.git",
|
||||
"reference": "cea40a48279d58dc3efee8112634cb90141156c2"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/yaml/zipball/cea40a48279d58dc3efee8112634cb90141156c2",
|
||||
"reference": "cea40a48279d58dc3efee8112634cb90141156c2",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.2",
|
||||
"symfony/deprecation-contracts": "^2.5|^3.0",
|
||||
"symfony/polyfill-ctype": "^1.8"
|
||||
},
|
||||
"conflict": {
|
||||
"symfony/console": "<6.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/console": "^6.4|^7.0"
|
||||
},
|
||||
"bin": [
|
||||
"Resources/bin/yaml-lint"
|
||||
],
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\Yaml\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Loads and dumps YAML files",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/yaml/tree/v7.3.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-04-04T10:10:33+00:00"
|
||||
},
|
||||
{
|
||||
"name": "theseer/tokenizer",
|
||||
"version": "1.2.3",
|
||||
|
@ -200,6 +200,7 @@
|
||||
App\Providers\SourceControlServiceProvider::class,
|
||||
App\Providers\NotificationChannelServiceProvider::class,
|
||||
App\Providers\ServiceTypeServiceProvider::class,
|
||||
App\Providers\HorizonServiceProvider::class,
|
||||
],
|
||||
|
||||
/*
|
||||
@ -217,7 +218,9 @@
|
||||
// 'ExampleClass' => App\Example\ExampleClass::class,
|
||||
])->toArray(),
|
||||
|
||||
'version' => '3.0.0-alpha-1',
|
||||
'version' => '3.0.0-beta-1',
|
||||
|
||||
'demo' => env('APP_DEMO', false),
|
||||
|
||||
'force_https' => env('FORCE_HTTPS', false),
|
||||
];
|
||||
|
242
config/horizon.php
Normal file
242
config/horizon.php
Normal file
@ -0,0 +1,242 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Horizon Domain
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This is the subdomain where Horizon will be accessible from. If this
|
||||
| setting is null, Horizon will reside under the same domain as the
|
||||
| application. Otherwise, this value will serve as the subdomain.
|
||||
|
|
||||
*/
|
||||
|
||||
'domain' => env('HORIZON_DOMAIN'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Horizon Path
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This is the URI path where Horizon will be accessible from. Feel free
|
||||
| to change this path to anything you like. Note that the URI will not
|
||||
| affect the paths of its internal API that aren't exposed to users.
|
||||
|
|
||||
*/
|
||||
|
||||
'path' => env('HORIZON_PATH', 'horizon'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Horizon Redis Connection
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This is the name of the Redis connection where Horizon will store the
|
||||
| meta information required for it to function. It includes the list
|
||||
| of supervisors, failed jobs, job metrics, and other information.
|
||||
|
|
||||
*/
|
||||
|
||||
'use' => 'default',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Horizon Redis Prefix
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This prefix will be used when storing all Horizon data in Redis. You
|
||||
| may modify the prefix when you are running multiple installations
|
||||
| of Horizon on the same server so that they don't have problems.
|
||||
|
|
||||
*/
|
||||
|
||||
'prefix' => env(
|
||||
'HORIZON_PREFIX',
|
||||
Str::slug(env('APP_NAME', 'laravel'), '_').'_horizon:'
|
||||
),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Horizon Route Middleware
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These middleware will get attached onto each Horizon route, giving you
|
||||
| the chance to add your own middleware to this list or change any of
|
||||
| the existing middleware. Or, you can simply stick with this list.
|
||||
|
|
||||
*/
|
||||
|
||||
'middleware' => [
|
||||
'web',
|
||||
\App\Http\Middleware\MustBeAdminMiddleware::class,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Queue Wait Time Thresholds
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option allows you to configure when the LongWaitDetected event
|
||||
| will be fired. Every connection / queue combination may have its
|
||||
| own, unique threshold (in seconds) before this event is fired.
|
||||
|
|
||||
*/
|
||||
|
||||
'waits' => [
|
||||
'redis:default' => 60,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Job Trimming Times
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you can configure for how long (in minutes) you desire Horizon to
|
||||
| persist the recent and failed jobs. Typically, recent jobs are kept
|
||||
| for one hour while all failed jobs are stored for an entire week.
|
||||
|
|
||||
*/
|
||||
|
||||
'trim' => [
|
||||
'recent' => 60,
|
||||
'pending' => 60,
|
||||
'completed' => 60,
|
||||
'recent_failed' => 10080,
|
||||
'failed' => 10080,
|
||||
'monitored' => 10080,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Silenced Jobs
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Silencing a job will instruct Horizon to not place the job in the list
|
||||
| of completed jobs within the Horizon dashboard. This setting may be
|
||||
| used to fully remove any noisy jobs from the completed jobs list.
|
||||
|
|
||||
*/
|
||||
|
||||
'silenced' => [
|
||||
// App\Jobs\ExampleJob::class,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Metrics
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you can configure how many snapshots should be kept to display in
|
||||
| the metrics graph. This will get used in combination with Horizon's
|
||||
| `horizon:snapshot` schedule to define how long to retain metrics.
|
||||
|
|
||||
*/
|
||||
|
||||
'metrics' => [
|
||||
'trim_snapshots' => [
|
||||
'job' => 24,
|
||||
'queue' => 24,
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Fast Termination
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When this option is enabled, Horizon's "terminate" command will not
|
||||
| wait on all of the workers to terminate unless the --wait option
|
||||
| is provided. Fast termination can shorten deployment delay by
|
||||
| allowing a new instance of Horizon to start while the last
|
||||
| instance will continue to terminate each of its workers.
|
||||
|
|
||||
*/
|
||||
|
||||
'fast_termination' => false,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Memory Limit (MB)
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value describes the maximum amount of memory the Horizon master
|
||||
| supervisor may consume before it is terminated and restarted. For
|
||||
| configuring these limits on your workers, see the next section.
|
||||
|
|
||||
*/
|
||||
|
||||
'memory_limit' => 64,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Queue Worker Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may define the queue worker settings used by your application
|
||||
| in all environments. These supervisors and settings handle all your
|
||||
| queued jobs and will be provisioned by Horizon during deployment.
|
||||
|
|
||||
*/
|
||||
|
||||
'defaults' => [
|
||||
'default' => [
|
||||
'connection' => 'redis',
|
||||
'queue' => ['default'],
|
||||
'balance' => 'auto',
|
||||
'autoScalingStrategy' => 'time',
|
||||
'maxProcesses' => 1,
|
||||
'maxTime' => 0,
|
||||
'maxJobs' => 0,
|
||||
'memory' => 128,
|
||||
'tries' => 1,
|
||||
'timeout' => 90,
|
||||
'nice' => 0,
|
||||
],
|
||||
|
||||
'ssh' => [
|
||||
'connection' => 'redis',
|
||||
'queue' => ['ssh'],
|
||||
'balance' => 'auto',
|
||||
'autoScalingStrategy' => 'time',
|
||||
'maxProcesses' => 1,
|
||||
'maxTime' => 0,
|
||||
'maxJobs' => 0,
|
||||
'memory' => 128,
|
||||
'tries' => 1,
|
||||
'timeout' => 600,
|
||||
'nice' => 0,
|
||||
],
|
||||
|
||||
'ssh-unique' => [
|
||||
'connection' => 'redis',
|
||||
'queue' => ['ssh-unique'],
|
||||
'balance' => 'auto',
|
||||
'autoScalingStrategy' => 'time',
|
||||
'maxProcesses' => 1,
|
||||
'maxTime' => 0,
|
||||
'maxJobs' => 0,
|
||||
'memory' => 128,
|
||||
'tries' => 1,
|
||||
'timeout' => 600,
|
||||
'nice' => 0,
|
||||
],
|
||||
],
|
||||
|
||||
'environments' => [
|
||||
'*' => [
|
||||
'default' => [
|
||||
'maxProcesses' => 3,
|
||||
],
|
||||
'ssh' => [
|
||||
'maxProcesses' => 3,
|
||||
],
|
||||
'ssh-unique' => [
|
||||
'maxProcesses' => 1,
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
@ -10,7 +10,7 @@ user=root
|
||||
autostart=1
|
||||
autorestart=1
|
||||
numprocs=1
|
||||
command=/usr/bin/php /var/www/html/artisan queue:work --sleep=3 --backoff=0 --queue=default,ssh,ssh-long --timeout=3600 --tries=1
|
||||
command=/usr/bin/php /var/www/html/artisan horizon
|
||||
redirect_stderr=true
|
||||
stdout_logfile=/var/www/html/storage/logs/worker.log
|
||||
stopwaitsecs=3600
|
||||
|
@ -7,38 +7,37 @@ import typescript from 'typescript-eslint';
|
||||
|
||||
/** @type {import('eslint').Linter.Config[]} */
|
||||
export default [
|
||||
js.configs.recommended,
|
||||
...typescript.configs.recommended,
|
||||
{
|
||||
...react.configs.flat.recommended,
|
||||
...react.configs.flat['jsx-runtime'], // Required for React 17+
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
'react/prop-types': 'off',
|
||||
'react/no-unescaped-entities': 'off',
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
version: 'detect',
|
||||
},
|
||||
},
|
||||
js.configs.recommended,
|
||||
...typescript.configs.recommended,
|
||||
{
|
||||
...react.configs.flat.recommended,
|
||||
...react.configs.flat['jsx-runtime'], // Required for React 17+
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
},
|
||||
},
|
||||
{
|
||||
plugins: {
|
||||
'react-hooks': reactHooks,
|
||||
},
|
||||
rules: {
|
||||
'react-hooks/rules-of-hooks': 'error',
|
||||
'react-hooks/exhaustive-deps': 'warn',
|
||||
},
|
||||
rules: {
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
'react/prop-types': 'off',
|
||||
'react/no-unescaped-entities': 'off',
|
||||
},
|
||||
{
|
||||
ignores: ['vendor', 'node_modules', 'public', 'bootstrap/ssr', 'tailwind.config.js'],
|
||||
settings: {
|
||||
react: {
|
||||
version: 'detect',
|
||||
},
|
||||
},
|
||||
prettier, // Turn off all rules that might conflict with Prettier
|
||||
},
|
||||
{
|
||||
plugins: {
|
||||
'react-hooks': reactHooks,
|
||||
},
|
||||
rules: {
|
||||
'react-hooks/rules-of-hooks': 'error',
|
||||
},
|
||||
},
|
||||
{
|
||||
ignores: ['vendor', 'node_modules', 'public', 'bootstrap/ssr', 'tailwind.config.js'],
|
||||
},
|
||||
prettier, // Turn off all rules that might conflict with Prettier
|
||||
];
|
||||
|
@ -4,7 +4,6 @@ includes:
|
||||
parameters:
|
||||
paths:
|
||||
- app
|
||||
- config
|
||||
- bootstrap
|
||||
- database/factories
|
||||
level: 7
|
||||
|
@ -3,9 +3,25 @@ import { useEffect, useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { CommandIcon, SearchIcon } from 'lucide-react';
|
||||
import CreateServer from '@/pages/servers/components/create-server';
|
||||
import ProjectForm from '@/pages/projects/components/project-form';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import axios from 'axios';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { router } from '@inertiajs/react';
|
||||
|
||||
type SearchResult = {
|
||||
id: number;
|
||||
parent_id?: number;
|
||||
label: string;
|
||||
type: 'server' | 'project' | 'site';
|
||||
};
|
||||
|
||||
export default function AppCommand() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [openServer, setOpenServer] = useState(false);
|
||||
const [openProject, setOpenProject] = useState(false);
|
||||
const [queryText, setQueryText] = useState('');
|
||||
const [selected, setSelected] = useState<string>('create-server');
|
||||
|
||||
useEffect(() => {
|
||||
const down = (e: KeyboardEvent) => {
|
||||
@ -19,9 +35,47 @@ export default function AppCommand() {
|
||||
return () => document.removeEventListener('keydown', down);
|
||||
}, []);
|
||||
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
setOpen(open);
|
||||
if (!open) {
|
||||
setOpenServer(false);
|
||||
setOpenProject(false);
|
||||
}
|
||||
};
|
||||
|
||||
const query = useQuery<SearchResult[]>({
|
||||
queryKey: ['search'],
|
||||
queryFn: async () => {
|
||||
const response = await axios.get(route('search', { query: queryText }));
|
||||
return response.data.data;
|
||||
},
|
||||
retry: false,
|
||||
enabled: false,
|
||||
refetchInterval: false,
|
||||
refetchIntervalInBackground: false,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (query.data && query.data.length > 0) {
|
||||
setSelected(`result-0`);
|
||||
} else {
|
||||
setSelected('create-server');
|
||||
}
|
||||
}, [query.data]);
|
||||
|
||||
useEffect(() => {
|
||||
if (queryText !== '' && queryText.length >= 3) {
|
||||
const timeoutId = setTimeout(() => {
|
||||
query.refetch();
|
||||
}, 300);
|
||||
|
||||
return () => clearTimeout(timeoutId);
|
||||
}
|
||||
}, [queryText]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Button className="px-1!" variant="outline" size="sm" onClick={() => setOpen(true)}>
|
||||
<Button className="hidden px-1! lg:flex" variant="outline" size="sm" onClick={() => setOpen(true)}>
|
||||
<span className="sr-only">Open command menu</span>
|
||||
<SearchIcon className="ml-1 size-3" />
|
||||
Search...
|
||||
@ -29,15 +83,54 @@ export default function AppCommand() {
|
||||
<CommandIcon className="mr-1 size-3" /> K
|
||||
</span>
|
||||
</Button>
|
||||
<CommandDialog open={open} onOpenChange={setOpen}>
|
||||
<CommandInput placeholder="Type a command or search..." />
|
||||
<Button className="lg:hidden" variant="outline" size="sm" onClick={() => setOpen(true)}>
|
||||
<CommandIcon className="mr-1 size-3" /> K
|
||||
</Button>
|
||||
<CommandDialog open={open} onOpenChange={handleOpenChange} shouldFilter={false} value={selected}>
|
||||
<CommandInput placeholder="Type a command or search..." onValueChange={setQueryText} />
|
||||
<CommandList>
|
||||
<CommandEmpty>No results found.</CommandEmpty>
|
||||
<CommandGroup heading="Suggestions">
|
||||
<CreateServer>
|
||||
<CommandItem>Create server</CommandItem>
|
||||
{query.isFetching && <p className="text-muted-foreground p-4 text-sm">Searching...</p>}
|
||||
{query.data && query.data?.length > 0 && (
|
||||
<CommandGroup heading="Search Results">
|
||||
{query.data.map((result, index) => (
|
||||
<CommandItem
|
||||
key={`search-result-${result.id}`}
|
||||
className="flex items-center justify-between"
|
||||
value={`result-${index}`}
|
||||
onSelect={() => {
|
||||
if (result.type === 'server') {
|
||||
router.post(route('servers.switch', { server: result.id }));
|
||||
} else if (result.type === 'project') {
|
||||
router.patch(
|
||||
route('projects.switch', {
|
||||
project: result.id,
|
||||
currentPath: window.location.pathname,
|
||||
}),
|
||||
);
|
||||
} else if (result.type === 'site') {
|
||||
router.post(route('sites.switch', { server: result.parent_id, site: result.id }));
|
||||
}
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
{result.label}
|
||||
<Badge variant="outline">{result.type}</Badge>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
)}
|
||||
<CommandGroup heading="Commands">
|
||||
<CreateServer defaultOpen={openServer} onOpenChange={setOpenServer}>
|
||||
<CommandItem value="create-server" key="cmd-create-server" onSelect={() => setOpenServer(true)}>
|
||||
Create server
|
||||
</CommandItem>
|
||||
</CreateServer>
|
||||
<CommandItem>Create project</CommandItem>
|
||||
<ProjectForm defaultOpen={openProject} onOpenChange={setOpenProject}>
|
||||
<CommandItem value="create-project" key="cmd-create-project" onSelect={() => setOpenProject(true)}>
|
||||
Create project
|
||||
</CommandItem>
|
||||
</ProjectForm>
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</CommandDialog>
|
||||
|
@ -6,6 +6,7 @@ import AppCommand from '@/components/app-command';
|
||||
import { SiteSwitch } from '@/components/site-switch';
|
||||
import { usePage } from '@inertiajs/react';
|
||||
import { SharedData } from '@/types';
|
||||
import Refresh from '@/components/refresh';
|
||||
|
||||
export function AppHeader() {
|
||||
const page = usePage<SharedData>();
|
||||
@ -18,7 +19,7 @@ export function AppHeader() {
|
||||
<ProjectSwitch />
|
||||
<SlashIcon className="size-3" />
|
||||
<ServerSwitch />
|
||||
{page.props.server && (
|
||||
{page.props.server && page.props.server.services['webserver'] && (
|
||||
<>
|
||||
<SlashIcon className="size-3" />
|
||||
<SiteSwitch />
|
||||
@ -26,7 +27,10 @@ export function AppHeader() {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<AppCommand />
|
||||
<div className="flex items-center gap-2">
|
||||
<AppCommand />
|
||||
<Refresh />
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
@ -12,12 +12,13 @@ import {
|
||||
SidebarMenuSub,
|
||||
SidebarMenuSubItem,
|
||||
} from '@/components/ui/sidebar';
|
||||
import { type NavItem } from '@/types';
|
||||
import { Link } from '@inertiajs/react';
|
||||
import { BookOpen, ChevronRightIcon, CogIcon, Folder, MousePointerClickIcon, ServerIcon, ZapIcon } from 'lucide-react';
|
||||
import { type NavItem, SharedData } from '@/types';
|
||||
import { Link, usePage } from '@inertiajs/react';
|
||||
import { BookOpen, ChevronRightIcon, CogIcon, Folder, ListEndIcon, LogsIcon, MousePointerClickIcon, ServerIcon, ZapIcon } from 'lucide-react';
|
||||
import AppLogo from './app-logo';
|
||||
import { Icon } from '@/components/icon';
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
|
||||
const mainNavItems: NavItem[] = [
|
||||
{
|
||||
@ -43,6 +44,16 @@ const mainNavItems: NavItem[] = [
|
||||
];
|
||||
|
||||
const footerNavItems: NavItem[] = [
|
||||
{
|
||||
title: 'Horizon Dashboard',
|
||||
href: route('horizon.index'),
|
||||
icon: ListEndIcon,
|
||||
},
|
||||
{
|
||||
title: 'Vito Logs',
|
||||
href: route('log-viewer.index'),
|
||||
icon: LogsIcon,
|
||||
},
|
||||
{
|
||||
title: 'Repository',
|
||||
href: 'https://github.com/vitodeploy/vito',
|
||||
@ -56,6 +67,8 @@ const footerNavItems: NavItem[] = [
|
||||
];
|
||||
|
||||
export function AppSidebar({ secondNavItems, secondNavTitle }: { secondNavItems?: NavItem[]; secondNavTitle?: string }) {
|
||||
const page = usePage<SharedData>();
|
||||
|
||||
return (
|
||||
<Sidebar collapsible="icon" className="overflow-hidden [&>[data-sidebar=sidebar]]:flex-row">
|
||||
{/* This is the first sidebar */}
|
||||
@ -67,7 +80,12 @@ export function AppSidebar({ secondNavItems, secondNavTitle }: { secondNavItems?
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton size="lg" asChild className="md:h-8 md:p-0">
|
||||
<Link href={route('servers')} prefetch>
|
||||
<AppLogo />
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<AppLogo />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">{page.props.version}</TooltipContent>
|
||||
</Tooltip>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
|
70
resources/js/components/refresh.tsx
Normal file
70
resources/js/components/refresh.tsx
Normal file
@ -0,0 +1,70 @@
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ChevronDownIcon, RefreshCwIcon } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { router } from '@inertiajs/react';
|
||||
|
||||
export default function Refresh() {
|
||||
const [poll, setPoll] = useState<{
|
||||
stop: VoidFunction;
|
||||
start: VoidFunction;
|
||||
}>();
|
||||
const [polling, setPolling] = useState(false);
|
||||
const storedInterval = (localStorage.getItem('refresh_interval') as '5' | '10' | '30' | '60' | '0') || '10';
|
||||
const [refreshInterval, setRefreshInterval] = useState<5 | 10 | 30 | 60 | 0>(
|
||||
storedInterval === '0' ? 0 : (parseInt(storedInterval) as 5 | 10 | 30 | 60),
|
||||
);
|
||||
const intervalLabels = {
|
||||
5: '5s',
|
||||
10: '10s',
|
||||
30: '30s',
|
||||
60: '1m',
|
||||
0: 'OFF',
|
||||
};
|
||||
|
||||
const refresh = () => {
|
||||
router.reload({
|
||||
onStart: () => {
|
||||
setPolling(true);
|
||||
},
|
||||
onFinish: () => {
|
||||
setPolling(false);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
poll?.stop();
|
||||
if (refreshInterval > 0) {
|
||||
setPoll(router.poll(refreshInterval * 1000));
|
||||
} else {
|
||||
poll?.stop();
|
||||
setPoll(undefined);
|
||||
}
|
||||
localStorage.setItem('refresh_interval', refreshInterval.toString());
|
||||
}, [refreshInterval]);
|
||||
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<Button variant="outline" size="sm" className="md:rounded-r-none" onClick={refresh} disabled={polling}>
|
||||
{polling ? <RefreshCwIcon className="animate-spin" /> : <RefreshCwIcon className="lg:hidden" />}
|
||||
<span className="hidden md:block">Refresh</span>
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="hidden rounded-l-none border-l-0 md:flex">
|
||||
{intervalLabels[refreshInterval] || 'Unknown'}
|
||||
<ChevronDownIcon />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onSelect={() => setRefreshInterval(5)}>5s</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={() => setRefreshInterval(10)}>10s</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={() => setRefreshInterval(30)}>30s</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={() => setRefreshInterval(60)}>1m</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={() => setRefreshInterval(0)}>OFF</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -30,6 +30,7 @@ export function SiteSwitch() {
|
||||
|
||||
if (storedSite && page.props.server_sites && !page.props.server_sites.find((site) => site.id === storedSite.id)) {
|
||||
siteHelper.storeSite();
|
||||
setSelectedSite(null);
|
||||
}
|
||||
|
||||
const handleSiteChange = (site: Site) => {
|
||||
|
@ -18,11 +18,15 @@ function Command({ className, ...props }: React.ComponentProps<typeof CommandPri
|
||||
function CommandDialog({
|
||||
title = 'Command Palette',
|
||||
description = 'Search for a command to run...',
|
||||
shouldFilter = true,
|
||||
value,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Dialog> & {
|
||||
title?: string;
|
||||
description?: string;
|
||||
shouldFilter?: boolean;
|
||||
value?: string;
|
||||
}) {
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
@ -31,7 +35,11 @@ function CommandDialog({
|
||||
<DialogDescription>{description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogContent className="overflow-hidden p-0">
|
||||
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||
<Command
|
||||
shouldFilter={shouldFilter}
|
||||
value={value}
|
||||
className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5"
|
||||
>
|
||||
{children}
|
||||
</Command>
|
||||
</DialogContent>
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { AppSidebar } from '@/components/app-sidebar';
|
||||
import { AppHeader } from '@/components/app-header';
|
||||
import { type BreadcrumbItem, NavItem, SharedData } from '@/types';
|
||||
import { type PropsWithChildren } from 'react';
|
||||
import { type PropsWithChildren, useEffect } from 'react';
|
||||
import { SidebarInset, SidebarProvider } from '@/components/ui/sidebar';
|
||||
import { usePage } from '@inertiajs/react';
|
||||
import { Toaster } from '@/components/ui/sonner';
|
||||
@ -20,38 +20,40 @@ export default function Layout({
|
||||
}>) {
|
||||
const page = usePage<SharedData>();
|
||||
|
||||
if (page.props.flash && page.props.flash.success) {
|
||||
toast(
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle2Icon className="text-success size-5" />
|
||||
{page.props.flash.success}
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
if (page.props.flash && page.props.flash.error) {
|
||||
toast(
|
||||
<div className="flex items-center gap-2">
|
||||
<CircleXIcon className="text-destructive size-5" />
|
||||
{page.props.flash.error}
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
if (page.props.flash && page.props.flash.warning) {
|
||||
toast(
|
||||
<div className="flex items-center gap-2">
|
||||
<TriangleAlertIcon className="text-warning size-5" />
|
||||
{page.props.flash.warning}
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
if (page.props.flash && page.props.flash.info) {
|
||||
toast(
|
||||
<div className="flex items-center gap-2">
|
||||
<InfoIcon className="text-info size-5" />
|
||||
{page.props.flash.info}
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
useEffect(() => {
|
||||
if (page.props.flash && page.props.flash.success) {
|
||||
toast(
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle2Icon className="text-success size-5" />
|
||||
{page.props.flash.success}
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
if (page.props.flash && page.props.flash.error) {
|
||||
toast(
|
||||
<div className="flex items-center gap-2">
|
||||
<CircleXIcon className="text-destructive size-5" />
|
||||
{page.props.flash.error}
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
if (page.props.flash && page.props.flash.warning) {
|
||||
toast(
|
||||
<div className="flex items-center gap-2">
|
||||
<TriangleAlertIcon className="text-warning size-5" />
|
||||
{page.props.flash.warning}
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
if (page.props.flash && page.props.flash.info) {
|
||||
toast(
|
||||
<div className="flex items-center gap-2">
|
||||
<InfoIcon className="text-info size-5" />
|
||||
{page.props.flash.info}
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
}, [page.props.flash]);
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import AppLogoIcon from '@/components/app-logo-icon';
|
||||
import { Link } from '@inertiajs/react';
|
||||
import { Link, usePage } from '@inertiajs/react';
|
||||
import { type PropsWithChildren } from 'react';
|
||||
import { SharedData } from '@/types';
|
||||
|
||||
interface AuthLayoutProps {
|
||||
name?: string;
|
||||
@ -9,6 +10,7 @@ interface AuthLayoutProps {
|
||||
}
|
||||
|
||||
export default function AuthLayout({ children, title, description }: PropsWithChildren<AuthLayoutProps>) {
|
||||
const page = usePage<SharedData>();
|
||||
return (
|
||||
<div className="bg-background flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10">
|
||||
<div className="w-full max-w-sm">
|
||||
@ -27,6 +29,16 @@ export default function AuthLayout({ children, title, description }: PropsWithCh
|
||||
</div>
|
||||
</div>
|
||||
{children}
|
||||
<div className="text-muted-foreground/50 text-center text-xs">
|
||||
VitoDeploy{' '}
|
||||
<a
|
||||
href={`https://github.com/vitodeploy/vito/releases/tag/${page.props.version}`}
|
||||
className="hover:text-primary cursor-pointer"
|
||||
target="_blank"
|
||||
>
|
||||
{page.props.version}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -26,15 +26,13 @@ import { ReactNode } from 'react';
|
||||
import { Server } from '@/types/server';
|
||||
import ServerHeader from '@/pages/servers/components/header';
|
||||
import Layout from '@/layouts/app/layout';
|
||||
import { usePage, usePoll } from '@inertiajs/react';
|
||||
import { usePage } from '@inertiajs/react';
|
||||
import { Site } from '@/types/site';
|
||||
import PHPIcon from '@/icons/php';
|
||||
import NodeIcon from '@/icons/node';
|
||||
import siteHelper from '@/lib/site-helper';
|
||||
|
||||
export default function ServerLayout({ children }: { children: ReactNode }) {
|
||||
usePoll(7000);
|
||||
|
||||
const page = usePage<{
|
||||
server: Server;
|
||||
site?: Site;
|
||||
|
@ -1,18 +1,5 @@
|
||||
import { type BreadcrumbItem, type NavItem } from '@/types';
|
||||
import {
|
||||
BellIcon,
|
||||
CloudIcon,
|
||||
CodeIcon,
|
||||
CommandIcon,
|
||||
DatabaseIcon,
|
||||
KeyIcon,
|
||||
ListIcon,
|
||||
LogsIcon,
|
||||
PlugIcon,
|
||||
TagIcon,
|
||||
UserIcon,
|
||||
UsersIcon,
|
||||
} from 'lucide-react';
|
||||
import { BellIcon, CloudIcon, CodeIcon, CommandIcon, DatabaseIcon, KeyIcon, ListIcon, PlugIcon, TagIcon, UserIcon, UsersIcon } from 'lucide-react';
|
||||
import { ReactNode } from 'react';
|
||||
import Layout from '@/layouts/app/layout';
|
||||
import VitoIcon from '@/icons/vito';
|
||||
@ -78,12 +65,6 @@ const sidebarNavItems: NavItem[] = [
|
||||
href: route('vito-settings'),
|
||||
icon: VitoIcon,
|
||||
},
|
||||
{
|
||||
title: 'Vito Logs',
|
||||
href: route('log-viewer.index'),
|
||||
icon: LogsIcon,
|
||||
external: true,
|
||||
},
|
||||
];
|
||||
|
||||
export default function SettingsLayout({ children, breadcrumbs }: { children: ReactNode; breadcrumbs?: BreadcrumbItem[] }) {
|
||||
|
16
resources/js/lib/event-bus.tsx
Normal file
16
resources/js/lib/event-bus.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
type Callback = (data: unknown) => void;
|
||||
|
||||
const events: Record<string, Callback[]> = {};
|
||||
|
||||
export const EventBus = {
|
||||
on(event: string, callback: Callback) {
|
||||
if (!events[event]) events[event] = [];
|
||||
events[event].push(callback);
|
||||
},
|
||||
off(event: string, callback: Callback) {
|
||||
events[event] = events[event]?.filter((cb) => cb !== callback) || [];
|
||||
},
|
||||
emit(event: string, data?: unknown) {
|
||||
events[event]?.forEach((cb) => cb(data));
|
||||
},
|
||||
};
|
@ -11,14 +11,17 @@ import { LoaderCircleIcon } from 'lucide-react';
|
||||
import { registerDotEnvLanguage } from '@/lib/editor';
|
||||
import { Site } from '@/types/site';
|
||||
import { useAppearance } from '@/hooks/use-appearance';
|
||||
import { Input } from '@/components/ui/input';
|
||||
|
||||
export default function Env({ site, children }: { site: Site; children: ReactNode }) {
|
||||
const { getActualAppearance } = useAppearance();
|
||||
const [open, setOpen] = useState(false);
|
||||
const form = useForm<{
|
||||
env: string;
|
||||
path: string;
|
||||
}>({
|
||||
env: '',
|
||||
path: site.type_data.env_path || `${site.path}/.env`,
|
||||
});
|
||||
|
||||
const submit = (e: FormEvent) => {
|
||||
@ -55,7 +58,15 @@ export default function Env({ site, children }: { site: Site; children: ReactNod
|
||||
<SheetTrigger asChild>{children}</SheetTrigger>
|
||||
<SheetContent className="sm:max-w-5xl">
|
||||
<SheetHeader>
|
||||
<SheetTitle>Edit .env</SheetTitle>
|
||||
<SheetTitle>
|
||||
<Input
|
||||
name="path"
|
||||
value={form.data.path}
|
||||
onChange={(e) => form.setData('path', e.target.value)}
|
||||
autoFocus={false}
|
||||
className="max-w-[80%]"
|
||||
/>
|
||||
</SheetTitle>
|
||||
<SheetDescription className="sr-only">Edit .env file</SheetDescription>
|
||||
</SheetHeader>
|
||||
<Form id="update-env-form" className="h-full" onSubmit={submit}>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Head, useForm } from '@inertiajs/react';
|
||||
import { Head, useForm, usePage } from '@inertiajs/react';
|
||||
import { LoaderCircle } from 'lucide-react';
|
||||
import { FormEventHandler } from 'react';
|
||||
|
||||
@ -9,6 +9,7 @@ import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import AuthLayout from '@/layouts/auth/layout';
|
||||
import { SharedData } from '@/types';
|
||||
|
||||
type LoginForm = {
|
||||
email: string;
|
||||
@ -22,9 +23,11 @@ interface LoginProps {
|
||||
}
|
||||
|
||||
export default function Login({ status, canResetPassword }: LoginProps) {
|
||||
const page = usePage<SharedData>();
|
||||
|
||||
const { data, setData, post, processing, errors, reset } = useForm<Required<LoginForm>>({
|
||||
email: '',
|
||||
password: '',
|
||||
email: page.props.demo ? 'demo@vitodeploy.com' : '',
|
||||
password: page.props.demo ? 'password' : '',
|
||||
remember: false,
|
||||
});
|
||||
|
||||
|
76
resources/js/pages/auth/two-factor.tsx
Normal file
76
resources/js/pages/auth/two-factor.tsx
Normal file
@ -0,0 +1,76 @@
|
||||
import { Head, Link, useForm } from '@inertiajs/react';
|
||||
import { LoaderCircle } from 'lucide-react';
|
||||
import { FormEventHandler } from 'react';
|
||||
|
||||
import InputError from '@/components/ui/input-error';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import AuthLayout from '@/layouts/auth/layout';
|
||||
import { Form, FormField, FormFields } from '@/components/ui/form';
|
||||
|
||||
export default function TwoFactor() {
|
||||
const form = useForm<Required<{ code: string; recovery_code: string }>>({
|
||||
code: '',
|
||||
recovery_code: '',
|
||||
});
|
||||
|
||||
const submit: FormEventHandler = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
form.post(route('two-factor.store'), {
|
||||
onFinish: () => form.reset(),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthLayout title="Two factor challenge" description="Please enter the two-factor authentication code to continue.">
|
||||
<Head title="Confirm password" />
|
||||
|
||||
<Form onSubmit={submit}>
|
||||
<FormFields>
|
||||
<FormField>
|
||||
<Label htmlFor="code">Code</Label>
|
||||
<Input
|
||||
id="code"
|
||||
type="text"
|
||||
name="code"
|
||||
placeholder="Two factor code"
|
||||
value={form.data.code}
|
||||
autoFocus
|
||||
onChange={(e) => form.setData('code', e.target.value)}
|
||||
/>
|
||||
|
||||
<InputError message={form.errors.code} />
|
||||
</FormField>
|
||||
|
||||
<FormField>
|
||||
<Label htmlFor="recovery_code">Recovery Code</Label>
|
||||
<Input
|
||||
id="recovery_code"
|
||||
type="text"
|
||||
name="recovery_code"
|
||||
placeholder="Or enter your recovery code"
|
||||
value={form.data.recovery_code}
|
||||
onChange={(e) => form.setData('recovery_code', e.target.value)}
|
||||
/>
|
||||
|
||||
<InputError message={form.errors.recovery_code} />
|
||||
</FormField>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Button className="w-full" disabled={form.processing}>
|
||||
{form.processing && <LoaderCircle className="h-4 w-4 animate-spin" />}
|
||||
Confirm
|
||||
</Button>
|
||||
<Button variant="ghost" asChild>
|
||||
<Link className="block w-full" method="post" href={route('logout')}>
|
||||
Back to login
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</FormFields>
|
||||
</Form>
|
||||
</AuthLayout>
|
||||
);
|
||||
}
|
139
resources/js/pages/profile/components/two-factor.tsx
Normal file
139
resources/js/pages/profile/components/two-factor.tsx
Normal file
@ -0,0 +1,139 @@
|
||||
import { useForm, usePage } from '@inertiajs/react';
|
||||
import type { SharedData } from '@/types';
|
||||
import { FormEventHandler, ReactNode, useState } from 'react';
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { CheckCircle2Icon, LoaderCircleIcon, XCircleIcon } from 'lucide-react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { FormField, FormFields } from '@/components/ui/form';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
|
||||
function Disable(): ReactNode {
|
||||
const [open, setOpen] = useState(false);
|
||||
const form = useForm();
|
||||
|
||||
const submit = () => {
|
||||
form.post(route('profile.disable-two-factor'), {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => setOpen(false),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="destructive">Disable Two Factor</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Disable two factor</DialogTitle>
|
||||
<DialogDescription className="sr-only">Disable two factor</DialogDescription>
|
||||
</DialogHeader>
|
||||
<p className="p-4">Are you sure you want to enable two factor authentication?</p>
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
</DialogClose>
|
||||
<Button onClick={submit} variant="destructive" disabled={form.processing}>
|
||||
{form.processing && <LoaderCircleIcon className="animate-spin" />}
|
||||
Disable
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function Enable() {
|
||||
const form = useForm();
|
||||
|
||||
const submit: FormEventHandler = (e) => {
|
||||
e.preventDefault();
|
||||
form.post(route('profile.enable-two-factor'));
|
||||
};
|
||||
|
||||
return (
|
||||
<Button onClick={submit} disabled={form.processing}>
|
||||
{form.processing && <LoaderCircleIcon className="animate-spin" />}
|
||||
Enable Two Factor
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export default function TwoFactor() {
|
||||
const page = usePage<
|
||||
SharedData & {
|
||||
flash: {
|
||||
data?: {
|
||||
qr_code?: string;
|
||||
qr_code_url?: string;
|
||||
recovery_codes?: string[];
|
||||
};
|
||||
};
|
||||
}
|
||||
>();
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Two factor authentication</CardTitle>
|
||||
<CardDescription>Enable or Disable two factor authentication</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2 p-4">
|
||||
{page.props.flash.data?.qr_code && (
|
||||
<FormFields>
|
||||
<FormField>
|
||||
<Label htmlFor="qr-code">Scan this QR code with your authenticator app</Label>
|
||||
<div className="flex max-h-[400px] items-center">
|
||||
<div dangerouslySetInnerHTML={{ __html: page.props.flash.data.qr_code }}></div>
|
||||
</div>
|
||||
</FormField>
|
||||
<FormField>
|
||||
<Label htmlFor="qr-code-url">QR Code URL</Label>
|
||||
<Input id="qr-code-url" value={page.props.flash.data.qr_code_url} disabled />
|
||||
</FormField>
|
||||
<FormField>
|
||||
<Label htmlFor="recovery-codes">Recovery Codes</Label>
|
||||
<Textarea id="recovery-codes" value={page.props.flash.data.recovery_codes?.join('\n') || ''} disabled rows={5} />
|
||||
</FormField>
|
||||
</FormFields>
|
||||
)}
|
||||
{page.props.auth.user.two_factor_enabled ? (
|
||||
<Alert>
|
||||
<AlertDescription>
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle2Icon className="text-success size-4" />
|
||||
<p>Two factor authentication is enabled</p>
|
||||
</div>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : (
|
||||
<Alert>
|
||||
<AlertDescription>
|
||||
<div className="flex items-center gap-2">
|
||||
<XCircleIcon className="text-danger size-4" />
|
||||
Two factor authentication is <strong>not</strong> enabled
|
||||
</div>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</CardContent>
|
||||
<CardFooter className="gap-2">
|
||||
{!page.props.auth.user.two_factor_enabled && <Enable />}
|
||||
{page.props.auth.user.two_factor_enabled && <Disable />}
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
}
|
@ -4,15 +4,34 @@ import Container from '@/components/container';
|
||||
import UpdatePassword from '@/pages/profile/components/update-password';
|
||||
import UpdateProfile from '@/pages/profile/components/update-profile';
|
||||
import Heading from '@/components/heading';
|
||||
import TwoFactor from '@/pages/profile/components/two-factor';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { useState } from 'react';
|
||||
|
||||
export default function Profile() {
|
||||
const [tab, setTab] = useState('info');
|
||||
return (
|
||||
<SettingsLayout>
|
||||
<Head title="Profile settings" />
|
||||
<Container className="max-w-5xl">
|
||||
<Heading title="Profile settings" description="Manage your profile settings." />
|
||||
<UpdateProfile />
|
||||
<UpdatePassword />
|
||||
|
||||
<Tabs defaultValue={tab} onValueChange={setTab}>
|
||||
<TabsList>
|
||||
<TabsTrigger value="info">Info</TabsTrigger>
|
||||
<TabsTrigger value="password">Password</TabsTrigger>
|
||||
<TabsTrigger value="two_factor">Two Factor</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="info">
|
||||
<UpdateProfile />
|
||||
</TabsContent>
|
||||
<TabsContent value="password">
|
||||
<UpdatePassword />
|
||||
</TabsContent>
|
||||
<TabsContent value="two_factor">
|
||||
<TwoFactor />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</Container>
|
||||
</SettingsLayout>
|
||||
);
|
||||
|
@ -8,7 +8,7 @@ import {
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@/components/ui/dialog';
|
||||
import { FormEventHandler, ReactNode, useState } from 'react';
|
||||
import { FormEventHandler, ReactNode, useEffect, useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { LoaderCircle } from 'lucide-react';
|
||||
import { useForm } from '@inertiajs/react';
|
||||
@ -19,8 +19,30 @@ import InputError from '@/components/ui/input-error';
|
||||
import { Project } from '@/types/project';
|
||||
import FormSuccessful from '@/components/form-successful';
|
||||
|
||||
export default function ProjectForm({ project, children }: { project?: Project; children: ReactNode }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
export default function ProjectForm({
|
||||
project,
|
||||
defaultOpen,
|
||||
onOpenChange,
|
||||
children,
|
||||
}: {
|
||||
project?: Project;
|
||||
defaultOpen?: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const [open, setOpen] = useState(defaultOpen || false);
|
||||
useEffect(() => {
|
||||
if (defaultOpen) {
|
||||
setOpen(defaultOpen);
|
||||
}
|
||||
}, [setOpen, defaultOpen]);
|
||||
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
setOpen(open);
|
||||
if (onOpenChange) {
|
||||
onOpenChange(open);
|
||||
}
|
||||
};
|
||||
|
||||
const form = useForm({
|
||||
name: project?.name || '',
|
||||
@ -42,7 +64,7 @@ export default function ProjectForm({ project, children }: { project?: Project;
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
|
@ -2,7 +2,7 @@ import { ClipboardCheckIcon, ClipboardIcon, LoaderCircle, TriangleAlert, WifiIco
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet';
|
||||
import { useForm, usePage } from '@inertiajs/react';
|
||||
import React, { FormEventHandler, useState } from 'react';
|
||||
import React, { FormEventHandler, useEffect, useState } from 'react';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import InputError from '@/components/ui/input-error';
|
||||
@ -25,9 +25,31 @@ type CreateServerForm = {
|
||||
plan: string;
|
||||
};
|
||||
|
||||
export default function CreateServer({ children }: { children: React.ReactNode }) {
|
||||
export default function CreateServer({
|
||||
defaultOpen,
|
||||
onOpenChange,
|
||||
children,
|
||||
}: {
|
||||
defaultOpen?: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const page = usePage<SharedData>();
|
||||
|
||||
const [open, setOpen] = useState(defaultOpen || false);
|
||||
useEffect(() => {
|
||||
if (defaultOpen) {
|
||||
setOpen(defaultOpen);
|
||||
}
|
||||
}, [defaultOpen]);
|
||||
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
setOpen(open);
|
||||
if (onOpenChange) {
|
||||
onOpenChange(open);
|
||||
}
|
||||
};
|
||||
|
||||
const form = useForm<Required<CreateServerForm>>({
|
||||
provider: 'custom',
|
||||
server_provider: 0,
|
||||
@ -97,7 +119,7 @@ export default function CreateServer({ children }: { children: React.ReactNode }
|
||||
};
|
||||
|
||||
return (
|
||||
<Sheet>
|
||||
<Sheet open={open} onOpenChange={handleOpenChange} modal>
|
||||
<SheetTrigger asChild>{children}</SheetTrigger>
|
||||
<SheetContent className="w-full lg:max-w-4xl">
|
||||
<SheetHeader>
|
||||
|
@ -5,7 +5,7 @@ import HeaderContainer from '@/components/header-container';
|
||||
import Heading from '@/components/heading';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import ServerLayout from '@/layouts/server/layout';
|
||||
import { BookOpenIcon, MoreVerticalIcon } from 'lucide-react';
|
||||
import { MoreVerticalIcon } from 'lucide-react';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardRow, CardTitle } from '@/components/ui/card';
|
||||
import React from 'react';
|
||||
import { Site, SiteFeature } from '@/types/site';
|
||||
@ -29,14 +29,6 @@ export default function SiteFeatures() {
|
||||
<Container className="max-w-5xl">
|
||||
<HeaderContainer>
|
||||
<Heading title="Features" description="Your site has some features enabled by Vito or other plugins" />
|
||||
<div className="flex items-center gap-2">
|
||||
<a href="https://vitodeploy.com/docs/sites/features" target="_blank">
|
||||
<Button variant="outline">
|
||||
<BookOpenIcon />
|
||||
<span className="hidden lg:block">Docs</span>
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
</HeaderContainer>
|
||||
|
||||
<Card>
|
||||
|
2
resources/js/types/index.d.ts
vendored
2
resources/js/types/index.d.ts
vendored
@ -104,6 +104,8 @@ export interface Configs {
|
||||
|
||||
export interface SharedData {
|
||||
name: string;
|
||||
version: string;
|
||||
demo: boolean;
|
||||
quote: { message: string; author: string };
|
||||
auth: Auth;
|
||||
ziggy: Config & { location: string };
|
||||
|
1
resources/js/types/site.d.ts
vendored
1
resources/js/types/site.d.ts
vendored
@ -9,6 +9,7 @@ export interface Site {
|
||||
type: string;
|
||||
type_data: {
|
||||
method?: 'round-robin' | 'least-connections' | 'ip-hash';
|
||||
env_path?: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
domain: string;
|
||||
|
1
resources/js/types/user.d.ts
vendored
1
resources/js/types/user.d.ts
vendored
@ -10,6 +10,7 @@ export interface User {
|
||||
updated_at: string;
|
||||
timezone: string;
|
||||
projects?: Project[];
|
||||
two_factor_enabled: boolean;
|
||||
role: string;
|
||||
[key: string]: unknown; // This allows for additional properties...
|
||||
}
|
||||
|
@ -0,0 +1,11 @@
|
||||
sudo sh -c 'echo "deb https://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
|
||||
|
||||
wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -
|
||||
|
||||
sudo DEBIAN_FRONTEND=noninteractive apt-get update -y
|
||||
|
||||
sudo DEBIAN_FRONTEND=noninteractive apt-get install postgresql-17 -y
|
||||
|
||||
systemctl status postgresql
|
||||
|
||||
sudo -u postgres psql -c "SELECT version();"
|
@ -209,7 +209,7 @@ chown -R vito:vito /home/vito
|
||||
export V_WORKER_CONFIG="
|
||||
[program:worker]
|
||||
process_name=%(program_name)s_%(process_num)02d
|
||||
command=php /home/vito/vito/artisan queue:work --sleep=3 --backoff=0 --queue=default,ssh,ssh-long --timeout=3600 --tries=1
|
||||
command=php /home/vito/vito/artisan horizon
|
||||
autostart=1
|
||||
autorestart=1
|
||||
user=vito
|
||||
|
@ -19,6 +19,7 @@ sudo service php8.4-fpm restart
|
||||
sudo sed -i "s/memory_limit = .*/memory_limit = 1G/" /etc/php/8.4/fpm/php.ini
|
||||
sudo sed -i "s/upload_max_filesize = .*/upload_max_filesize = 1G/" /etc/php/8.4/fpm/php.ini
|
||||
sudo sed -i "s/post_max_size = .*/post_max_size = 1G/" /etc/php/8.4/fpm/php.ini
|
||||
sudo sed -i '/location ~ \\\.php\$ {/a \ fastcgi_buffers 16 16k;\n fastcgi_buffer_size 32k;' /etc/nginx/sites-available/vito
|
||||
|
||||
echo "Installing Redis"
|
||||
sudo apt install redis-server -y
|
||||
@ -34,6 +35,10 @@ sudo sed -i "s/php8.2-fpm.sock/php8.4-fpm.sock/g" /etc/nginx/sites-available/vit
|
||||
sudo sed -i '/server\s*{.*/a \ client_max_body_size 100M;' /etc/nginx/sites-available/vito
|
||||
sudo service nginx restart
|
||||
|
||||
echo "Update supervisor configuration"
|
||||
sudo sed -i 's/command=php \/home\/vito\/vito\/artisan queue:work --sleep=3 --backoff=0 --queue=default,ssh,ssh-long --timeout=3600 --tries=1/command=php \/home\/vito\/vito\/artisan horizon/' /etc/supervisor/conf.d/worker.conf
|
||||
sudo service supervisor restart
|
||||
|
||||
echo "Fetching the latest release"
|
||||
git fetch
|
||||
git checkout 3.x
|
||||
|
@ -381,6 +381,7 @@ public function test_update_env(): void
|
||||
'site' => $site,
|
||||
]), [
|
||||
'env' => $envContent,
|
||||
'path' => '/home/vito/some-path/.env',
|
||||
])
|
||||
->assertSuccessful()
|
||||
->assertJsonFragment([
|
||||
|
@ -147,8 +147,13 @@ public function test_update_env_file(): void
|
||||
'site' => $this->site,
|
||||
]), [
|
||||
'env' => 'APP_ENV="production"',
|
||||
'path' => '/home/vito/some-path/.env',
|
||||
])
|
||||
->assertSessionDoesntHaveErrors();
|
||||
|
||||
$this->site->refresh();
|
||||
|
||||
$this->assertEquals('/home/vito/some-path/.env', data_get($this->site->type_data, 'env_path'));
|
||||
}
|
||||
|
||||
/**
|
||||
|
Reference in New Issue
Block a user