Compare commits

...

15 Commits
1.5.0 ... 1.6.2

Author SHA1 Message Date
0cfb938320 fix missing ubuntu 24 providers (#220) 2024-05-25 12:03:11 +02:00
dd4a3d30c0 Use Site PHP Version to Run Composer Install (#218) 2024-05-22 10:37:16 +02:00
2b849c888e fix update bug 2024-05-15 22:55:35 +02:00
d9a791755e fix updater and add post-update (#213) 2024-05-15 22:49:07 +02:00
e3ea8f975f fix project deletion 404 error (#208) 2024-05-15 13:41:33 +02:00
de468ae1ba Manage site aliases (#206)
* manage site aliases

* build assets

* fix tests
2024-05-15 11:23:24 +02:00
30ef8ad5eb fix letsencrypt for aliases & blank php deployment fix (#204) 2024-05-13 00:37:51 +02:00
88223a61f9 enable/disable cronjobs (#203) 2024-05-11 13:19:19 +02:00
1067a5fd33 build assets 2024-05-11 11:10:15 +02:00
4361305206 build assets 2024-05-11 11:05:53 +02:00
fe331fd2b3 server updates (#202)
* server updates

* add last update check
2024-05-11 10:09:46 +02:00
bbe3ca802d Add Ubuntu 24.04 support (#199)
* ubuntu 24
* updated aws regions and images
2024-05-10 19:10:33 +02:00
765ac21916 restart php after installing phpmyadmin (#198) 2024-05-09 12:52:28 +02:00
016886f307 fix new user bug (#197) 2024-05-09 00:55:52 +02:00
179aefefac source-controls (#193)
* edit source control
* assign project after creation
* global and project scoped source controls
2024-05-08 00:07:11 +02:00
92 changed files with 1532 additions and 267 deletions

View File

@ -2,6 +2,7 @@
namespace App\Actions\CronJob;
use App\Enums\CronjobStatus;
use App\Models\CronJob;
use App\Models\Server;
@ -10,7 +11,9 @@ class DeleteCronJob
public function delete(Server $server, CronJob $cronJob): void
{
$user = $cronJob->user;
$cronJob->delete();
$cronJob->status = CronjobStatus::DELETING;
$cronJob->save();
$server->cron()->update($cronJob->user, CronJob::crontab($server, $user));
$cronJob->delete();
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace App\Actions\CronJob;
use App\Enums\CronjobStatus;
use App\Models\CronJob;
use App\Models\Server;
class DisableCronJob
{
public function disable(Server $server, CronJob $cronJob): void
{
$cronJob->status = CronjobStatus::DISABLING;
$cronJob->save();
$server->cron()->update($cronJob->user, CronJob::crontab($server, $cronJob->user));
$cronJob->status = CronjobStatus::DISABLED;
$cronJob->save();
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace App\Actions\CronJob;
use App\Enums\CronjobStatus;
use App\Models\CronJob;
use App\Models\Server;
class EnableCronJob
{
public function enable(Server $server, CronJob $cronJob): void
{
$cronJob->status = CronjobStatus::ENABLING;
$cronJob->save();
$server->cron()->update($cronJob->user, CronJob::crontab($server, $cronJob->user));
$cronJob->status = CronjobStatus::READY;
$cronJob->save();
}
}

View File

@ -14,7 +14,7 @@ public function create(User $user, array $input): Project
$input['name'] = strtolower($input['name']);
}
$this->validate($user, $input);
$this->validate($input);
$project = new Project([
'name' => $input['name'],
@ -22,10 +22,12 @@ public function create(User $user, array $input): Project
$project->save();
$project->users()->attach($user);
return $project;
}
private function validate(User $user, array $input): void
private function validate(array $input): void
{
Validator::make($input, [
'name' => [

View File

@ -17,11 +17,15 @@ public function delete(User $user, Project $project): void
}
if ($user->current_project_id == $project->id) {
throw ValidationException::withMessages([
'project' => __('Cannot delete your current project.'),
]);
}
/** @var Project $randomProject */
$randomProject = $user->projects()->where('id', '!=', $project->id)->first();
$randomProject = $user->projects()->where('project_id', '!=', $project->id)->first();
$user->current_project_id = $randomProject->id;
$user->save();
}
$project->delete();
}

View File

@ -6,6 +6,7 @@
use App\Enums\SslType;
use App\Models\Site;
use App\Models\Ssl;
use App\SSH\Services\Webserver\Webserver;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
@ -27,14 +28,23 @@ public function create(Site $site, array $input): void
'expires_at' => $input['type'] === SslType::LETSENCRYPT ? now()->addMonths(3) : $input['expires_at'],
'status' => SslStatus::CREATING,
]);
$ssl->domains = [$site->domain];
if (isset($input['aliases']) && $input['aliases']) {
$ssl->domains = array_merge($ssl->domains, $site->aliases);
}
$ssl->save();
dispatch(function () use ($site, $ssl) {
$site->server->webserver()->handler()->setupSSL($ssl);
/** @var Webserver $webserver */
$webserver = $site->server->webserver()->handler();
$webserver->setupSSL($ssl);
$ssl->status = SslStatus::CREATED;
$ssl->save();
$site->type()->edit();
});
})->catch(function () use ($ssl) {
$ssl->status = SslStatus::FAILED;
$ssl->save();
})->onConnection('ssh');
}
/**

View File

@ -2,6 +2,7 @@
namespace App\Actions\Server;
use App\Enums\ServerStatus;
use App\Facades\Notifier;
use App\Models\Server;
use App\Notifications\ServerDisconnected;
@ -15,12 +16,12 @@ public function check(Server $server): Server
try {
$server->ssh()->connect();
$server->refresh();
if ($status == 'disconnected') {
$server->status = 'ready';
if (in_array($status, [ServerStatus::DISCONNECTED, ServerStatus::UPDATING])) {
$server->status = ServerStatus::READY;
$server->save();
}
} catch (Throwable) {
$server->status = 'disconnected';
$server->status = ServerStatus::DISCONNECTED;
$server->save();
Notifier::send($server, new ServerDisconnected($server));
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Actions\Server;
use App\Enums\ServerStatus;
use App\Facades\Notifier;
use App\Models\Server;
use App\Notifications\ServerUpdateFailed;
class Update
{
public function update(Server $server): void
{
$server->status = ServerStatus::UPDATING;
$server->save();
dispatch(function () use ($server) {
$server->os()->upgrade();
$server->checkConnection();
$server->checkForUpdates();
})->catch(function () use ($server) {
Notifier::send($server, new ServerUpdateFailed($server));
$server->checkConnection();
})->onConnection('ssh');
}
}

View File

@ -21,7 +21,7 @@
class CreateSite
{
/**
* @throws ValidationException
* @throws SourceControlIsNotConnected
*/
public function create(Server $server, array $input): Site
{
@ -33,7 +33,7 @@ public function create(Server $server, array $input): Site
'server_id' => $server->id,
'type' => $input['type'],
'domain' => $input['domain'],
'aliases' => isset($input['alias']) ? [$input['alias']] : [],
'aliases' => $input['aliases'] ?? [],
'path' => '/home/'.$server->getSshUser().'/'.$input['domain'],
'status' => SiteStatus::INSTALLING,
]);
@ -115,7 +115,7 @@ private function validateInputs(Server $server, array $input): void
return $query->where('server_id', $server->id);
}),
],
'alias' => [
'aliases.*' => [
new DomainRule(),
],
];

View File

@ -30,7 +30,7 @@ public function run(Site $site): Deployment
'deployment_script_id' => $site->deploymentScript->id,
'status' => DeploymentStatus::DEPLOYING,
]);
$lastCommit = $site->sourceControl()->provider()->getLastCommit($site->repository, $site->branch);
$lastCommit = $site->sourceControl()?->provider()?->getLastCommit($site->repository, $site->branch);
if ($lastCommit) {
$deployment->commit_id = $lastCommit['commit_id'];
$deployment->commit_data = $lastCommit['commit_data'];

View File

@ -0,0 +1,33 @@
<?php
namespace App\Actions\Site;
use App\Models\Site;
use App\SSH\Services\Webserver\Webserver;
use App\ValidationRules\DomainRule;
use Illuminate\Support\Facades\Validator;
class UpdateAliases
{
public function update(Site $site, array $input): void
{
$this->validate($input);
$site->aliases = $input['aliases'] ?? [];
/** @var Webserver $webserver */
$webserver = $site->server->webserver()->handler();
$webserver->updateVHost($site, ! $site->hasSSL());
$site->save();
}
private function validate(array $input): void
{
Validator::make($input, [
'aliases.*' => [
new DomainRule(),
],
])->validate();
}
}

View File

@ -3,6 +3,7 @@
namespace App\Actions\SourceControl;
use App\Models\SourceControl;
use App\Models\User;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
@ -10,7 +11,7 @@
class ConnectSourceControl
{
public function connect(array $input): void
public function connect(User $user, array $input): void
{
$this->validate($input);
@ -18,6 +19,7 @@ public function connect(array $input): void
'provider' => $input['provider'],
'profile' => $input['name'],
'url' => Arr::has($input, 'url') ? $input['url'] : null,
'project_id' => isset($input['global']) && $input['global'] ? null : $user->current_project_id,
]);
$this->validateProvider($sourceControl, $input);

View File

@ -0,0 +1,54 @@
<?php
namespace App\Actions\SourceControl;
use App\Models\SourceControl;
use App\Models\User;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
class EditSourceControl
{
public function edit(SourceControl $sourceControl, User $user, array $input): void
{
$this->validate($input);
$sourceControl->profile = $input['name'];
$sourceControl->url = isset($input['url']) ? $input['url'] : null;
$sourceControl->project_id = isset($input['global']) && $input['global'] ? null : $user->current_project_id;
$this->validateProvider($sourceControl, $input);
$sourceControl->provider_data = $sourceControl->provider()->createData($input);
if (! $sourceControl->provider()->connect()) {
throw ValidationException::withMessages([
'token' => __('Cannot connect to :provider or invalid token!', ['provider' => $sourceControl->provider]
),
]);
}
$sourceControl->save();
}
/**
* @throws ValidationException
*/
private function validate(array $input): void
{
$rules = [
'name' => [
'required',
],
];
Validator::make($input, $rules)->validate();
}
/**
* @throws ValidationException
*/
private function validateProvider(SourceControl $sourceControl, array $input): void
{
Validator::make($input, $sourceControl->provider()->createRules($input))->validate();
}
}

View File

@ -9,4 +9,10 @@ final class CronjobStatus
const READY = 'ready';
const DELETING = 'deleting';
const ENABLING = 'enabling';
const DISABLING = 'disabling';
const DISABLED = 'disabled';
}

View File

@ -9,4 +9,6 @@ final class OperatingSystem
const UBUNTU20 = 'ubuntu_20';
const UBUNTU22 = 'ubuntu_22';
const UBUNTU24 = 'ubuntu_24';
}

View File

@ -11,4 +11,6 @@ final class ServerStatus
const INSTALLATION_FAILED = 'installation_failed';
const DISCONNECTED = 'disconnected';
const UPDATING = 'updating';
}

View File

@ -94,12 +94,6 @@ public function connect(bool $sftp = false): void
public function exec(string $command, string $log = '', ?int $siteId = null, ?bool $stream = false): string
{
if (! $this->log && $log) {
$this->log = $this->server->logs()->create([
'site_id' => $siteId,
'name' => $this->server->id.'-'.strtotime('now').'-'.$log.'.log',
'type' => $log,
'disk' => config('core.logs_disk'),
]);
$this->log = ServerLog::make($this->server, $log);
if ($siteId) {
$this->log->forSite($siteId);

View File

@ -4,6 +4,8 @@
use App\Actions\CronJob\CreateCronJob;
use App\Actions\CronJob\DeleteCronJob;
use App\Actions\CronJob\DisableCronJob;
use App\Actions\CronJob\EnableCronJob;
use App\Facades\Toast;
use App\Helpers\HtmxResponse;
use App\Models\CronJob;
@ -45,4 +47,26 @@ public function destroy(Server $server, CronJob $cronJob): RedirectResponse
return back();
}
public function enable(Server $server, CronJob $cronJob): RedirectResponse
{
$this->authorize('manage', $server);
app(EnableCronJob::class)->enable($server, $cronJob);
Toast::success('Cronjob enabled successfully.');
return back();
}
public function disable(Server $server, CronJob $cronJob): RedirectResponse
{
$this->authorize('manage', $server);
app(DisableCronJob::class)->disable($server, $cronJob);
Toast::success('Cronjob disabled successfully.');
return back();
}
}

View File

@ -4,6 +4,7 @@
use App\Actions\Server\EditServer;
use App\Actions\Server\RebootServer;
use App\Actions\Server\Update;
use App\Facades\Toast;
use App\Helpers\HtmxResponse;
use App\Models\Server;
@ -64,4 +65,24 @@ public function edit(Request $request, Server $server): RedirectResponse
return back();
}
public function checkUpdates(Server $server): RedirectResponse
{
$this->authorize('manage', $server);
$server->checkForUpdates();
return back();
}
public function update(Server $server): HtmxResponse
{
$this->authorize('manage', $server);
app(Update::class)->update($server);
Toast::info('Updating server. This may take a few minutes.');
return htmx()->back();
}
}

View File

@ -19,6 +19,8 @@ class ProjectController extends Controller
{
public function index(): View
{
$this->authorize('viewAny', Project::class);
return view('settings.projects.index', [
'projects' => Project::all(),
]);
@ -26,6 +28,8 @@ public function index(): View
public function create(Request $request): HtmxResponse
{
$this->authorize('create', Project::class);
app(CreateProject::class)->create($request->user(), $request->input());
Toast::success('Project created.');
@ -35,8 +39,7 @@ public function create(Request $request): HtmxResponse
public function update(Request $request, Project $project): HtmxResponse
{
/** @var Project $project */
$project = $request->user()->projects()->findOrFail($project->id);
$this->authorize('update', $project);
app(UpdateProject::class)->update($project, $request->input());
@ -47,12 +50,11 @@ public function update(Request $request, Project $project): HtmxResponse
public function delete(Project $project): RedirectResponse
{
$this->authorize('delete', $project);
/** @var User $user */
$user = auth()->user();
/** @var Project $project */
$project = $user->projects()->findOrFail($project->id);
try {
app(DeleteProject::class)->delete($user, $project);
} catch (ValidationException $e) {

View File

@ -4,6 +4,7 @@
use App\Actions\SourceControl\ConnectSourceControl;
use App\Actions\SourceControl\DeleteSourceControl;
use App\Actions\SourceControl\EditSourceControl;
use App\Facades\Toast;
use App\Helpers\HtmxResponse;
use App\Http\Controllers\Controller;
@ -14,16 +15,23 @@
class SourceControlController extends Controller
{
public function index(): View
public function index(Request $request): View
{
return view('settings.source-controls.index', [
'sourceControls' => SourceControl::query()->orderByDesc('id')->get(),
]);
$data = [
'sourceControls' => SourceControl::getByCurrentProject(),
];
if ($request->has('edit')) {
$data['editSourceControl'] = SourceControl::find($request->input('edit'));
}
return view('settings.source-controls.index', $data);
}
public function connect(Request $request): HtmxResponse
{
app(ConnectSourceControl::class)->connect(
$request->user(),
$request->input(),
);
@ -32,6 +40,19 @@ public function connect(Request $request): HtmxResponse
return htmx()->redirect(route('settings.source-controls'));
}
public function update(SourceControl $sourceControl, Request $request): HtmxResponse
{
app(EditSourceControl::class)->edit(
$sourceControl,
$request->user(),
$request->input(),
);
Toast::success('Source control updated.');
return htmx()->redirect(route('settings.source-controls'));
}
public function delete(SourceControl $sourceControl): RedirectResponse
{
try {

View File

@ -7,6 +7,7 @@
use App\Facades\Toast;
use App\Helpers\HtmxResponse;
use App\Http\Controllers\Controller;
use App\Models\Project;
use App\Models\User;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
@ -56,6 +57,18 @@ public function updateProjects(User $user, Request $request): HtmxResponse
$user->projects()->sync($request->projects);
if ($user->currentProject && ! $user->projects->contains($user->currentProject)) {
$user->current_project_id = null;
$user->save();
}
/** @var Project $firstProject */
$firstProject = $user->projects->first();
if (! $user->currentProject && $firstProject) {
$user->current_project_id = $firstProject->id;
$user->save();
}
Toast::success('Projects updated successfully');
return htmx()->redirect(route('settings.users.show', $user));

View File

@ -2,6 +2,7 @@
namespace App\Http\Controllers;
use App\Actions\Site\UpdateAliases;
use App\Actions\Site\UpdateSourceControl;
use App\Facades\Toast;
use App\Helpers\HtmxResponse;
@ -83,10 +84,21 @@ public function updateSourceControl(Server $server, Site $site, Request $request
{
$this->authorize('manage', $server);
$site = app(UpdateSourceControl::class)->update($site, $request->input());
app(UpdateSourceControl::class)->update($site, $request->input());
Toast::success('Source control updated successfully!');
return htmx()->back();
}
public function updateAliases(Server $server, Site $site, Request $request): HtmxResponse
{
$this->authorize('manage', $server);
app(UpdateAliases::class)->update($site, $request->input());
Toast::success('Aliases updated successfully!');
return htmx()->back();
}
}

View File

@ -69,5 +69,6 @@ class Kernel extends HttpKernel
'handle-ssh-errors' => HandleSSHErrors::class,
'select-current-project' => SelectCurrentProject::class,
'is-admin' => \App\Http\Middleware\IsAdmin::class,
'must-have-current-project' => \App\Http\Middleware\MustHaveCurrentProject::class,
];
}

View File

@ -0,0 +1,31 @@
<?php
namespace App\Http\Middleware;
use App\Facades\Toast;
use App\Models\User;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class MustHaveCurrentProject
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
/** @var User $user */
$user = $request->user();
if (! $user->currentProject) {
Toast::warning('Please select a project to continue');
return redirect()->route('profile');
}
return $next($request);
}
}

View File

@ -22,7 +22,7 @@ public function handle(Request $request, Closure $next): Response
/** @var User $user */
$user = $request->user();
if ($server->project_id != $user->current_project_id) {
if ($server->project_id != $user->current_project_id && $user->can('view', $server)) {
$user->current_project_id = $server->project_id;
$user->save();
}

View File

@ -2,6 +2,7 @@
namespace App\Models;
use App\Enums\CronjobStatus;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -41,7 +42,14 @@ public function server(): BelongsTo
public static function crontab(Server $server, string $user): string
{
$data = '';
$cronJobs = $server->cronJobs()->where('user', $user)->get();
$cronJobs = $server->cronJobs()
->where('user', $user)
->whereIn('status', [
CronjobStatus::READY,
CronjobStatus::CREATING,
CronjobStatus::ENABLING,
])
->get();
foreach ($cronJobs as $key => $cronJob) {
$data .= $cronJob->frequency.' '.$cronJob->command;
if ($key != count($cronJobs) - 1) {

View File

@ -19,6 +19,7 @@
* @property User $user
* @property Collection<Server> $servers
* @property Collection<NotificationChannel> $notificationChannels
* @property Collection<SourceControl> $sourceControls
*/
class Project extends Model
{
@ -59,4 +60,9 @@ public function users(): BelongsToMany
{
return $this->belongsToMany(User::class, 'user_project')->withTimestamps();
}
public function sourceControls(): HasMany
{
return $this->hasMany(SourceControl::class);
}
}

View File

@ -9,6 +9,7 @@
use App\SSH\Cron\Cron;
use App\SSH\OS\OS;
use App\SSH\Systemd\Systemd;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
@ -55,6 +56,8 @@
* @property Queue[] $daemons
* @property SshKey[] $sshKeys
* @property string $hostname
* @property int $updates
* @property Carbon $last_update_check
*/
class Server extends AbstractModel
{
@ -82,6 +85,8 @@ class Server extends AbstractModel
'security_updates',
'progress',
'progress_step',
'updates',
'last_update_check',
];
protected $casts = [
@ -95,6 +100,8 @@ class Server extends AbstractModel
'available_updates' => 'integer',
'security_updates' => 'integer',
'progress' => 'integer',
'updates' => 'integer',
'last_update_check' => 'datetime',
];
protected $hidden = [
@ -384,4 +391,11 @@ public function cron(): Cron
{
return new Cron($this);
}
public function checkForUpdates(): void
{
$this->updates = $this->os()->availableUpdates();
$this->last_update_check = now();
$this->save();
}
}

View File

@ -278,4 +278,9 @@ public function getEnv(): string
return '';
}
}
public function hasSSL(): bool
{
return $this->ssls->isNotEmpty();
}
}

View File

@ -3,7 +3,9 @@
namespace App\Models;
use App\SourceControlProviders\SourceControlProvider;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
/**
@ -12,6 +14,7 @@
* @property ?string $profile
* @property ?string $url
* @property string $access_token
* @property ?int $project_id
*/
class SourceControl extends AbstractModel
{
@ -23,11 +26,13 @@ class SourceControl extends AbstractModel
'profile',
'url',
'access_token',
'project_id',
];
protected $casts = [
'access_token' => 'encrypted',
'provider_data' => 'encrypted:array',
'project_id' => 'integer',
];
public function provider(): SourceControlProvider
@ -46,4 +51,16 @@ public function sites(): HasMany
{
return $this->hasMany(Site::class);
}
public function project(): BelongsTo
{
return $this->belongsTo(Project::class);
}
public static function getByCurrentProject(): Collection
{
return self::query()
->where('project_id', auth()->user()->current_project_id)
->orWhereNull('project_id')->get();
}
}

View File

@ -17,6 +17,7 @@
* @property string $status
* @property Site $site
* @property string $ca_path
* @property ?array $domains
*/
class Ssl extends AbstractModel
{
@ -30,6 +31,7 @@ class Ssl extends AbstractModel
'ca',
'expires_at',
'status',
'domains',
];
protected $casts = [
@ -38,6 +40,7 @@ class Ssl extends AbstractModel
'pk' => 'encrypted',
'ca' => 'encrypted',
'expires_at' => 'datetime',
'domains' => 'array',
];
public function site(): BelongsTo
@ -111,4 +114,16 @@ public function validateSetup(string $result): bool
return true;
}
public function getDomains(): array
{
if (! empty($this->domains) && is_array($this->domains)) {
return $this->domains;
}
$this->domains = [$this->site->domain];
$this->save();
return $this->domains;
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace App\Notifications;
use App\Models\Server;
use Illuminate\Notifications\Messages\MailMessage;
class ServerUpdateFailed extends AbstractNotification
{
protected Server $server;
public function __construct(Server $server)
{
$this->server = $server;
}
public function rawText(): string
{
return __("Update failed for server [:server] \nCheck your server's logs \n:logs", [
'server' => $this->server->name,
'logs' => url('/servers/'.$this->server->id.'/logs'),
]);
}
public function toEmail(object $notifiable): MailMessage
{
return (new MailMessage)
->subject(__('Server update failed!'))
->line('Your server ['.$this->server->name.'] update has been failed.')
->line('Check your server logs')
->action('View Logs', url('/servers/'.$this->server->id.'/logs'));
}
}

View File

@ -14,6 +14,7 @@ public function installDependencies(Site $site): void
$site->server->ssh()->exec(
$this->getScript('composer-install.sh', [
'path' => $site->path,
'php_version' => $site->php_version,
]),
'composer-install',
$site->id

View File

@ -2,6 +2,6 @@ if ! cd __path__; then
echo 'VITO_SSH_ERROR' && exit 1
fi
if ! composer install --no-interaction --prefer-dist --optimize-autoloader --no-dev; then
if ! php__php_version__ /usr/local/bin/composer install --no-interaction --prefer-dist --optimize-autoloader --no-dev; then
echo 'VITO_SSH_ERROR' && exit 1
fi

View File

@ -30,6 +30,19 @@ public function upgrade(): void
);
}
public function availableUpdates(): int
{
$result = $this->server->ssh()->exec(
$this->getScript('available-updates.sh'),
'check-available-updates'
);
// -1 because the first line is not a package
$availableUpdates = str($result)->after('Available updates:')->trim()->toInteger() - 1;
return max($availableUpdates, 0);
}
public function createUser(string $user, string $password, string $key): void
{
$this->server->ssh()->exec(

View File

@ -0,0 +1,5 @@
sudo DEBIAN_FRONTEND=noninteractive apt-get update
AVAILABLE_UPDATES=$(sudo DEBIAN_FRONTEND=noninteractive apt list --upgradable | wc -l)
echo "Available updates:$AVAILABLE_UPDATES"

View File

@ -1,3 +1,8 @@
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y software-properties-common curl zip unzip git gcc openssl
git config --global user.email "__email__"
git config --global user.name "__name__"
# Install Node.js
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -;
sudo DEBIAN_FRONTEND=noninteractive apt-get update
sudo DEBIAN_FRONTEND=noninteractive apt-get install nodejs -y

View File

@ -2,8 +2,3 @@ sudo DEBIAN_FRONTEND=noninteractive apt-get clean
sudo DEBIAN_FRONTEND=noninteractive apt-get update
sudo DEBIAN_FRONTEND=noninteractive apt-get upgrade -y
sudo DEBIAN_FRONTEND=noninteractive apt-get autoremove -y
# Install Node.js
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -;
sudo DEBIAN_FRONTEND=noninteractive apt-get update
sudo DEBIAN_FRONTEND=noninteractive apt-get install nodejs -y

View File

@ -117,9 +117,14 @@ public function changePHPVersion(Site $site, $version): void
*/
public function setupSSL(Ssl $ssl): void
{
$domains = '';
foreach ($ssl->getDomains() as $domain) {
$domains .= ' -d '.$domain;
}
$command = $this->getScript('nginx/create-letsencrypt-ssl.sh', [
'email' => $ssl->site->server->creator->email,
'domain' => $ssl->site->domain,
'domains' => $domains,
'web_directory' => $ssl->site->getWebDirectoryPath(),
]);
if ($ssl->type == 'custom') {

View File

@ -1,3 +1,3 @@
if ! sudo certbot certonly --force-renewal --nginx --noninteractive --agree-tos --cert-name __domain__ -m __email__ -d __domain__ --verbose; then
if ! sudo certbot certonly --force-renewal --nginx --noninteractive --agree-tos --cert-name __domain__ -m __email__ __domains__ --verbose; then
echo 'VITO_SSH_ERROR' && exit 1
fi

View File

@ -1,10 +1,9 @@
server {
listen 80;
listen 443 ssl;
server_name __domain__ www.__domain__;
server_name __domain__ __aliases__;
root __path__/__web_directory__;
ssl on;
ssl_certificate __certificate__;
ssl_certificate_key __private_key__;

View File

@ -1,6 +1,6 @@
server {
listen 80;
server_name __domain__ www.__domain__;
server_name __domain__ __aliases__;
root __path__/__web_directory__;
add_header X-Frame-Options "SAMEORIGIN";

View File

@ -1,31 +0,0 @@
server {
listen __port__;
server_name _;
root /home/vito/phpmyadmin;
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
index index.php;
charset utf-8;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }
error_page 404 /index.php;
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php__php_version__-fpm.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\.(?!well-known).* {
deny all;
}
}

View File

@ -4,7 +4,6 @@ server {
server_name __domain__ __aliases__;
root __path__;
ssl on;
ssl_certificate __certificate__;
ssl_certificate_key __private_key__;

View File

@ -4,7 +4,6 @@ server {
server_name __domain__ __aliases__;
root __path__/__web_directory__;
ssl on;
ssl_certificate __certificate__;
ssl_certificate_key __private_key__;

View File

@ -3,6 +3,7 @@
namespace App\SiteTypes;
use App\Enums\SiteFeature;
use App\SSH\Services\Webserver\Webserver;
use Illuminate\Validation\Rule;
class PHPBlank extends PHPSite
@ -42,7 +43,9 @@ public function data(array $input): array
public function install(): void
{
$this->site->server->webserver()->handler()->createVHost($this->site);
/** @var Webserver $webserver */
$webserver = $this->site->server->webserver()->handler();
$webserver->createVHost($this->site);
$this->progress(65);
$this->site->php()?->restart();
}

View File

@ -41,9 +41,12 @@ public function data(array $input): array
public function install(): void
{
$this->site->server->webserver()->handler()->createVHost($this->site);
/** @var Webserver $webserver */
$webserver = $this->site->server->webserver()->handler();
$webserver->createVHost($this->site);
$this->progress(30);
app(\App\SSH\PHPMyAdmin\PHPMyAdmin::class)->install($this->site);
$this->progress(65);
$this->site->php()?->restart();
}
}

View File

@ -68,7 +68,9 @@ public function data(array $input): array
*/
public function install(): void
{
$this->site->server->webserver()->handler()->createVHost($this->site);
/** @var Webserver $webserver */
$webserver = $this->site->server->webserver()->handler();
$webserver->createVHost($this->site);
$this->progress(15);
$this->deployKey();
$this->progress(30);

View File

@ -81,7 +81,9 @@ public function data(array $input): array
public function install(): void
{
$this->site->server->webserver()->handler()->createVHost($this->site);
/** @var Webserver $webserver */
$webserver = $this->site->server->webserver()->handler();
$webserver->createVHost($this->site);
$this->progress(30);
/** @var Database $database */
$database = app(CreateDatabase::class)->create($this->site->server, [

View File

@ -16,6 +16,7 @@
'operating_systems' => [
\App\Enums\OperatingSystem::UBUNTU20,
\App\Enums\OperatingSystem::UBUNTU22,
\App\Enums\OperatingSystem::UBUNTU24,
],
'webservers' => ['none', 'nginx'],
'php_versions' => [
@ -106,26 +107,32 @@
'custom' => [
\App\Enums\OperatingSystem::UBUNTU20 => 'root',
\App\Enums\OperatingSystem::UBUNTU22 => 'root',
\App\Enums\OperatingSystem::UBUNTU24 => 'root',
],
'aws' => [
\App\Enums\OperatingSystem::UBUNTU20 => 'ubuntu',
\App\Enums\OperatingSystem::UBUNTU22 => 'ubuntu',
\App\Enums\OperatingSystem::UBUNTU24 => 'ubuntu',
],
'linode' => [
\App\Enums\OperatingSystem::UBUNTU20 => 'root',
\App\Enums\OperatingSystem::UBUNTU22 => 'root',
\App\Enums\OperatingSystem::UBUNTU24 => 'root',
],
'digitalocean' => [
\App\Enums\OperatingSystem::UBUNTU20 => 'root',
\App\Enums\OperatingSystem::UBUNTU22 => 'root',
\App\Enums\OperatingSystem::UBUNTU24 => 'root',
],
'vultr' => [
\App\Enums\OperatingSystem::UBUNTU20 => 'root',
\App\Enums\OperatingSystem::UBUNTU22 => 'root',
\App\Enums\OperatingSystem::UBUNTU24 => 'root',
],
'hetzner' => [
\App\Enums\OperatingSystem::UBUNTU20 => 'root',
\App\Enums\OperatingSystem::UBUNTU22 => 'root',
\App\Enums\OperatingSystem::UBUNTU24 => 'root',
],
],
@ -164,6 +171,9 @@
\App\Enums\OperatingSystem::UBUNTU22 => [
'latest' => 'nginx',
],
\App\Enums\OperatingSystem::UBUNTU24 => [
'latest' => 'nginx',
],
],
'mysql' => [
\App\Enums\OperatingSystem::UBUNTU20 => [
@ -174,6 +184,10 @@
'5.7' => 'mysql',
'8.0' => 'mysql',
],
\App\Enums\OperatingSystem::UBUNTU24 => [
'5.7' => 'mysql',
'8.0' => 'mysql',
],
],
'mariadb' => [
\App\Enums\OperatingSystem::UBUNTU20 => [
@ -184,6 +198,10 @@
'10.3' => 'mariadb',
'10.4' => 'mariadb',
],
\App\Enums\OperatingSystem::UBUNTU24 => [
'10.3' => 'mariadb',
'10.4' => 'mariadb',
],
],
'postgresql' => [
\App\Enums\OperatingSystem::UBUNTU20 => [
@ -200,6 +218,13 @@
'15' => 'postgresql',
'16' => 'postgresql',
],
\App\Enums\OperatingSystem::UBUNTU24 => [
'12' => 'postgresql',
'13' => 'postgresql',
'14' => 'postgresql',
'15' => 'postgresql',
'16' => 'postgresql',
],
],
'php' => [
\App\Enums\OperatingSystem::UBUNTU20 => [
@ -225,6 +250,18 @@
'8.2' => 'php8.2-fpm',
'8.3' => 'php8.3-fpm',
],
\App\Enums\OperatingSystem::UBUNTU24 => [
'5.6' => 'php5.6-fpm',
'7.0' => 'php7.0-fpm',
'7.1' => 'php7.1-fpm',
'7.2' => 'php7.2-fpm',
'7.3' => 'php7.3-fpm',
'7.4' => 'php7.4-fpm',
'8.0' => 'php8.0-fpm',
'8.1' => 'php8.1-fpm',
'8.2' => 'php8.2-fpm',
'8.3' => 'php8.3-fpm',
],
],
'redis' => [
\App\Enums\OperatingSystem::UBUNTU20 => [
@ -233,6 +270,9 @@
\App\Enums\OperatingSystem::UBUNTU22 => [
'latest' => 'redis',
],
\App\Enums\OperatingSystem::UBUNTU24 => [
'latest' => 'redis',
],
],
'supervisor' => [
\App\Enums\OperatingSystem::UBUNTU20 => [
@ -241,6 +281,9 @@
\App\Enums\OperatingSystem::UBUNTU22 => [
'latest' => 'supervisor',
],
\App\Enums\OperatingSystem::UBUNTU24 => [
'latest' => 'supervisor',
],
],
'ufw' => [
\App\Enums\OperatingSystem::UBUNTU20 => [
@ -249,6 +292,9 @@
\App\Enums\OperatingSystem::UBUNTU22 => [
'latest' => 'ufw',
],
\App\Enums\OperatingSystem::UBUNTU24 => [
'latest' => 'ufw',
],
],
'vito-agent' => [
\App\Enums\OperatingSystem::UBUNTU20 => [
@ -257,6 +303,9 @@
\App\Enums\OperatingSystem::UBUNTU22 => [
'latest' => 'vito-agent',
],
\App\Enums\OperatingSystem::UBUNTU24 => [
'latest' => 'vito-agent',
],
],
],

View File

@ -58,206 +58,267 @@
],
'regions' => [
[
'title' => 'US East (N. Virginia) (us-east-1)',
'value' => 'us-east-1',
],
[
'title' => 'US East (Ohio) (us-east-2)',
'title' => 'US East (Ohio)',
'value' => 'us-east-2',
],
[
'title' => 'US West (N. California) (us-west-1)',
'title' => 'US East (Virginia)',
'value' => 'us-east-1',
],
[
'title' => 'US West (N. California)',
'value' => 'us-west-1',
],
[
'title' => 'US West (Oregon) (us-west-2)',
'title' => 'US West (Oregon)',
'value' => 'us-west-2',
],
[
'title' => 'Asia Pacific (Hong Kong) (ap-east-1)',
'title' => 'Africa (Cape Town)',
'value' => 'af-south-1',
],
[
'title' => 'Asia Pacific (Hong Kong)',
'value' => 'ap-east-1',
],
[
'title' => 'Asia Pacific (Mumbai) (ap-south-1)',
'title' => 'Asia Pacific (Hyderabad)',
'value' => 'ap-south-2',
],
[
'title' => 'Asia Pacific (Jakarta)',
'value' => 'ap-southeast-3',
],
[
'title' => 'Asia Pacific (Melbourne)',
'value' => 'ap-southeast-4',
],
[
'title' => 'Asia Pacific (Mumbai)',
'value' => 'ap-south-1',
],
[
'title' => 'Asia Pacific (Singapore) (ap-southeast-1',
'value' => 'ap-southeast-1',
'title' => 'Asia Pacific (Osaka)',
'value' => 'ap-northeast-3',
],
[
'title' => 'Asia Pacific (Seoul) (ap-northeast-2)',
'title' => 'Asia Pacific (Seoul)',
'value' => 'ap-northeast-2',
],
[
'title' => 'Asia Pacific (Tokyo) (ap-northeast-1)',
'value' => 'ap-northeast-1',
'title' => 'Asia Pacific (Singapore)',
'value' => 'ap-southeast-1',
],
[
'title' => 'Asia Pacific (Sydney) (ap-southeast-2)',
'title' => 'Asia Pacific (Sydney)',
'value' => 'ap-southeast-2',
],
[
'title' => 'Canada (Central) (ca-central-1)',
'title' => 'Asia Pacific (Tokyo)',
'value' => 'ap-northeast-1',
],
[
'title' => 'Canada (Central)',
'value' => 'ca-central-1',
],
[
'title' => 'Europe (Frankfurt) (eu-central-1)',
'title' => 'Canada West (Calgary)',
'value' => 'ca-west-1',
],
[
'title' => 'Europe (Frankfurt)',
'value' => 'eu-central-1',
],
[
'title' => 'Europe (Ireland) (eu-west-1)',
'title' => 'Europe (Ireland)',
'value' => 'eu-west-1',
],
[
'title' => 'Europe (London) (eu-west-2)',
'title' => 'Europe (London)',
'value' => 'eu-west-2',
],
[
'title' => 'Europe (Paris) (eu-west-3)',
'value' => 'eu-west-3',
],
[
'title' => 'Europe (Milan) (eu-south-1)',
'title' => 'Europe (Milan)',
'value' => 'eu-south-1',
],
[
'title' => 'Europe (Stockholm) (eu-north-1)',
'title' => 'Europe (Paris)',
'value' => 'eu-west-3',
],
[
'title' => 'Europe (Spain)',
'value' => 'eu-south-2',
],
[
'title' => 'Europe (Stockholm)',
'value' => 'eu-north-1',
],
[
'title' => 'Middle East (Bahrain) (me-south-1)',
'title' => 'Europe (Zurich)',
'value' => 'eu-central-2',
],
[
'title' => 'Israel (Tel Aviv)',
'value' => 'il-central-1',
],
[
'title' => 'Middle East (Bahrain)',
'value' => 'me-south-1',
],
[
'title' => 'South America (São Paulo) (sa-east-1)',
'value' => 'sa-east-1',
'title' => 'Middle East (UAE)',
'value' => 'me-central-1',
],
[
'title' => 'Africa (Cape Town) (af-south-1)',
'value' => 'af-south-1',
'title' => 'South America (São Paulo)',
'value' => 'sa-east-1',
],
],
'images' => [
'af-south-1' => [
'ubuntu_20' => 'ami-03684d4c2541e5333',
'ubuntu_22' => 'ami-05759acc7d8973892',
],
'ap-east-1' => [
'ubuntu_20' => 'ami-0b19a97bf326b4931',
'ubuntu_22' => 'ami-03490b1b7425e5fe3',
],
'ap-northeast-1' => [
'ubuntu_20' => 'ami-0e25df74d27e028e6',
'ubuntu_22' => 'ami-09a81b370b76de6a2',
],
'ap-northeast-2' => [
'ubuntu_20' => 'ami-003a709e1e4ce3729',
'ubuntu_22' => 'ami-086cae3329a3f7d75',
],
'ap-northeast-3' => [
'ubuntu_20' => 'ami-06c1367bd83de7d47',
'ubuntu_22' => 'ami-0690c54203f5f67da',
],
'ap-south-1' => [
'ubuntu_20' => 'ami-0b88997c830e88c76',
'ubuntu_22' => 'ami-0287a05f0ef0e9d9a',
],
'ap-south-2' => [
'ubuntu_20' => 'ami-049e2ae605332dba6',
'ubuntu_22' => 'ami-06fe902e167e67d33',
],
'ap-southeast-1' => [
'ubuntu_20' => 'ami-0a6461ddb52e9db63',
'ubuntu_22' => 'ami-078c1149d8ad719a7',
],
'ap-southeast-2' => [
'ubuntu_20' => 'ami-0a9fb81cc3289919c',
'ubuntu_22' => 'ami-0df4b2961410d4cff',
],
'ap-southeast-3' => [
'ubuntu_20' => 'ami-05ee5bed682a3fff0',
'ubuntu_22' => 'ami-0fb6d1fdeeea10488',
],
'ap-southeast-4' => [
'ubuntu_20' => 'ami-02f9759882b112414',
'ubuntu_22' => 'ami-043a030d3eeabec75',
],
'ca-central-1' => [
'ubuntu_20' => 'ami-0daaea212e620de87',
'ubuntu_22' => 'ami-06873c81b882339ac',
],
'cn-north-1' => [
'ubuntu_20' => 'ami-0c8bcac1fe3389a72',
'ubuntu_22' => 'ami-0728a1a4cc9e07753',
],
'cn-northwest-1' => [
'ubuntu_20' => 'ami-0415bfb3ea62e17c0',
'ubuntu_22' => 'ami-05529cf859783e600',
],
'eu-central-1' => [
'ubuntu_20' => 'ami-0b369586722023326',
'ubuntu_22' => 'ami-06dd92ecc74fdfb36',
],
'eu-central-2' => [
'ubuntu_20' => 'ami-070c78d5ed65f11c8',
'ubuntu_22' => 'ami-07cf963e6321c9e6a',
],
'eu-north-1' => [
'ubuntu_20' => 'ami-0c5863072fc83557e',
'ubuntu_22' => 'ami-0fe8bec493a81c7da',
],
'eu-south-1' => [
'ubuntu_20' => 'ami-0966ff128f1497260',
'ubuntu_22' => 'ami-0b03947fd0ce0eed2',
],
'eu-south-2' => [
'ubuntu_20' => 'ami-087296a5b46cb95ce',
'ubuntu_22' => 'ami-03486abd2962c176f',
'ubuntu_24' => 'ami-0a50f993202fe4f22',
'ubuntu_22' => 'ami-043e9941c6aec0f52',
'ubuntu_20' => 'ami-086f353893612e446',
],
'eu-west-1' => [
'ubuntu_20' => 'ami-0e3e7f215a53e2a86',
'ubuntu_22' => 'ami-0694d931cee176e7d',
'ubuntu_24' => 'ami-0776c814353b4814d',
'ubuntu_22' => 'ami-0d0fa503c811361ab',
'ubuntu_20' => 'ami-0008aa5cb0cde3400',
],
'af-south-1' => [
'ubuntu_24' => 'ami-0bfda59e8f84ff5ed',
'ubuntu_22' => 'ami-0d06a4031539a9be6',
'ubuntu_20' => 'ami-0ea465fccfaf199ce',
],
'eu-west-2' => [
'ubuntu_20' => 'ami-0b22eee5ba6bb6772',
'ubuntu_22' => 'ami-0505148b3591e4c07',
'ubuntu_24' => 'ami-053a617c6207ecc7b',
'ubuntu_22' => 'ami-0eb5c35d7b89f3488',
'ubuntu_20' => 'ami-0608dbf22649c0159',
],
'eu-west-3' => [
'ubuntu_20' => 'ami-0f14fa1f9c69f4111',
'ubuntu_22' => 'ami-00983e8a26e4c9bd9',
'eu-south-1' => [
'ubuntu_24' => 'ami-0355c99d0faba8847',
'ubuntu_22' => 'ami-0bdcf995dcfebf29c',
'ubuntu_20' => 'ami-034ea9dc86027e603',
],
'ap-south-1' => [
'ubuntu_24' => 'ami-0f58b397bc5c1f2e8',
'ubuntu_22' => 'ami-0f16c6c3de733b474',
'ubuntu_20' => 'ami-02f829375c976f810',
],
'il-central-1' => [
'ubuntu_20' => 'ami-0703881563bf5fab7',
'ubuntu_22' => 'ami-03869c813f5a2e20c',
'ubuntu_24' => 'ami-04a4b28d712600827',
'ubuntu_22' => 'ami-09cd8eea397932e88',
'ubuntu_20' => 'ami-03988803bd4e18212',
],
'eu-north-1' => [
'ubuntu_24' => 'ami-0705384c0b33c194c',
'ubuntu_22' => 'ami-0fff1012fc5cb9f25',
'ubuntu_20' => 'ami-07cca21629288f454',
],
'me-central-1' => [
'ubuntu_20' => 'ami-04a5bde3b044c7c21',
'ubuntu_22' => 'ami-02168d82d5c12118f',
'ubuntu_24' => 'ami-048798fd481c4c791',
'ubuntu_22' => 'ami-042fcc4c33a3b6429',
'ubuntu_20' => 'ami-0f98fff9d77968c80',
],
'ca-central-1' => [
'ubuntu_24' => 'ami-0c4596ce1e7ae3e68',
'ubuntu_22' => 'ami-04fea581fe25e2675',
'ubuntu_20' => 'ami-05690acfbddfbeaf6',
],
'eu-west-3' => [
'ubuntu_24' => 'ami-00ac45f3035ff009e',
'ubuntu_22' => 'ami-0b020d95f579c8f43',
'ubuntu_20' => 'ami-0130b7d3ec1d07e4f',
],
'ap-south-2' => [
'ubuntu_24' => 'ami-008616ec4a2c6975e',
'ubuntu_22' => 'ami-088e75eecea53e53e',
'ubuntu_20' => 'ami-0688d182e7c22ec3f',
],
'ca-west-1' => [
'ubuntu_24' => 'ami-07022089d2e36ace0',
'ubuntu_22' => 'ami-02e22cefcad05a835',
'ubuntu_20' => 'ami-03890126b7675fac8',
],
'eu-central-1' => [
'ubuntu_24' => 'ami-01e444924a2233b07',
'ubuntu_22' => 'ami-01a93368cab494eb5',
'ubuntu_20' => 'ami-07fd6b7604806e876',
],
'me-south-1' => [
'ubuntu_20' => 'ami-0165b692f5714e330',
'ubuntu_22' => 'ami-0f8d2a6080634ee69',
'ubuntu_24' => 'ami-087f3ec3fdda67295',
'ubuntu_22' => 'ami-03ae386fab11fa0a1',
'ubuntu_20' => 'ami-0f65a186b3552f348',
],
'sa-east-1' => [
'ubuntu_20' => 'ami-095ca107fb46b81e6',
'ubuntu_22' => 'ami-0b6c2d49148000cd5',
'ap-northeast-1' => [
'ubuntu_24' => 'ami-01bef798938b7644d',
'ubuntu_22' => 'ami-08e32db9e33e28876',
'ubuntu_20' => 'ami-0ed286a950292f370',
],
'us-east-1' => [
'ubuntu_20' => 'ami-0fe0238291c8e3f07',
'ubuntu_22' => 'ami-0fc5d935ebf8bc3bc',
],
'us-east-2' => [
'ubuntu_20' => 'ami-0b6968e5c7117349a',
'ubuntu_22' => 'ami-0e83be366243f524a',
'ap-southeast-1' => [
'ubuntu_24' => 'ami-003c463c8207b4dfa',
'ubuntu_22' => 'ami-084cab24460184bd3',
'ubuntu_20' => 'ami-081ee02c4cdf3917c',
],
'us-west-1' => [
'ubuntu_20' => 'ami-092efbcc9a2d2be8a',
'ubuntu_22' => 'ami-0cbd40f694b804622',
'ubuntu_24' => 'ami-08012c0a9ee8e21c4',
'ubuntu_22' => 'ami-023f8bebe991375fd',
'ubuntu_20' => 'ami-0344f34a6875de16a',
],
'ap-southeast-3' => [
'ubuntu_24' => 'ami-00c31062c5966e820',
'ubuntu_22' => 'ami-0fd547652d1673e30',
'ubuntu_20' => 'ami-0699dddffd3542faf',
],
'ap-northeast-2' => [
'ubuntu_24' => 'ami-0e6f2b2fa0ca704d0',
'ubuntu_22' => 'ami-0720c7fcba4b88b36',
'ubuntu_20' => 'ami-03ec7d02334d21d49',
],
'ap-southeast-2' => [
'ubuntu_24' => 'ami-080660c9757080771',
'ubuntu_22' => 'ami-0d9d3b991cfa8ac6e',
'ubuntu_20' => 'ami-06c7a70c38594fef6',
],
'us-east-1' => [
'ubuntu_24' => 'ami-04b70fa74e45c3917',
'ubuntu_22' => 'ami-0cfa2ad4242c3168d',
'ubuntu_20' => 'ami-0e3a6d8ff4c8fe246',
],
'us-west-2' => [
'ubuntu_20' => 'ami-0a55cdf919d10eac9',
'ubuntu_22' => 'ami-0efcece6bed30fd98',
'ubuntu_24' => 'ami-0cf2b4e024cdb6960',
'ubuntu_22' => 'ami-09c3a3c2cf6003f6c',
'ubuntu_20' => 'ami-091c4300a778841cc',
],
'ap-east-1' => [
'ubuntu_24' => 'ami-026789b06a607b9a5',
'ubuntu_22' => 'ami-0361acb22fef7522b',
'ubuntu_20' => 'ami-0c0665dcea29a292d',
],
'eu-central-2' => [
'ubuntu_24' => 'ami-053ea2f9d1d6ac54c',
'ubuntu_22' => 'ami-09407f9985de426af',
'ubuntu_20' => 'ami-00c9866441e3616dd',
],
'us-east-2' => [
'ubuntu_24' => 'ami-09040d770ffe2224f',
'ubuntu_22' => 'ami-0b986fc833876b42e',
'ubuntu_20' => 'ami-010e55fe08af05fa7',
],
'ap-northeast-3' => [
'ubuntu_24' => 'ami-0b9bc7dcdbcff394e',
'ubuntu_22' => 'ami-063600dcf13c07ebc',
'ubuntu_20' => 'ami-0b7108d627f57c7c8',
],
'sa-east-1' => [
'ubuntu_24' => 'ami-04716897be83e3f04',
'ubuntu_22' => 'ami-0e6dfcf4e0e4dfc52',
'ubuntu_20' => 'ami-050e1159c5a10dd81',
],
'ap-southeast-4' => [
'ubuntu_24' => 'ami-0396cf525fd0aa5c1',
'ubuntu_22' => 'ami-097638dc9b6250206',
'ubuntu_20' => 'ami-02d0fccf5cdcdd8c5',
],
],
],
@ -426,6 +487,7 @@
'ubuntu_18' => 'linode/ubuntu18.04',
'ubuntu_20' => 'linode/ubuntu20.04',
'ubuntu_22' => 'linode/ubuntu22.04',
'ubuntu_24' => 'linode/ubuntu24.04',
],
],
'digitalocean' => [
@ -557,6 +619,7 @@
'ubuntu_18' => '112929540',
'ubuntu_20' => '112929454',
'ubuntu_22' => '129211873',
'ubuntu_24' => '155133621',
],
],
'vultr' => [
@ -728,6 +791,7 @@
'ubuntu_18' => '270',
'ubuntu_20' => '387',
'ubuntu_22' => '1743',
'ubuntu_24' => '2284',
],
],
'hetzner' => [
@ -859,6 +923,7 @@
'ubuntu_18' => 'ubuntu-18.04',
'ubuntu_20' => 'ubuntu-20.04',
'ubuntu_22' => 'ubuntu-22.04',
'ubuntu_24' => 'ubuntu-24.04',
],
],
];

View File

@ -20,7 +20,7 @@ public function definition(): array
'ca' => $this->faker->word(),
'expires_at' => Carbon::now()->addDay(),
'status' => SslStatus::CREATED,
'domains' => 'example.com',
'domains' => ['example.com'],
];
}
}

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('source_controls', function (Blueprint $table) {
$table->unsignedBigInteger('project_id')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('source_controls', function (Blueprint $table) {
$table->dropColumn('project_id');
});
}
};

View File

@ -0,0 +1,24 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('servers', function (Blueprint $table) {
$table->integer('updates')->default(0);
$table->timestamp('last_update_check')->nullable();
});
}
public function down(): void
{
Schema::table('servers', function (Blueprint $table) {
$table->dropColumn('updates');
$table->dropColumn('last_update_check');
});
}
};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
{
"resources/css/app.css": {
"file": "assets/app-268661bd.css",
"file": "assets/app-f8a673af.css",
"isEntry": true,
"src": "resources/css/app.css"
},

View File

@ -1,4 +1,4 @@
<div class="mx-auto mb-10">
<div {{ $attributes->merge(["class" => "mx-auto mb-10"]) }}>
<x-card-header>
@if (isset($title))
<x-slot name="title">{{ $title }}</x-slot>

View File

@ -0,0 +1,19 @@
@props([
"disabled" => false,
"id",
"name",
"value",
])
<div class="flex items-center">
<input
id="{{ $id }}"
name="{{ $name }}"
type="checkbox"
value="{{ $value }}"
{{ $attributes->merge(["disabled" => $disabled, "class" => "rounded border-gray-300 text-indigo-600 shadow-sm focus:ring-indigo-500 dark:border-gray-700 dark:bg-gray-900 dark:focus:ring-indigo-600 dark:focus:ring-offset-gray-800"]) }}
/>
<label for="{{ $id }}" class="ms-2 text-sm font-medium text-gray-900 dark:text-gray-300">
{{ $slot }}
</label>
</div>

View File

@ -18,7 +18,8 @@
<div
x-data="{
show: @js($show),
forceShow: @js($show),
show: false,
focusables() {
// All focusable element types...
let selector = 'a, button, input:not([type=\'hidden\']), textarea, select, details, [tabindex]:not([tabindex=\'-1\'])'
@ -34,6 +35,7 @@
prevFocusableIndex() { return Math.max(0, this.focusables().indexOf(document.activeElement)) -1 },
}"
x-init="
setTimeout(() => (show = forceShow), 100)
$watch('show', (value) => {
if (value) {
document.body.classList.add('overflow-y-hidden')

View File

@ -4,11 +4,11 @@
@php
$class = [
"success" => "rounded-md border border-green-300 bg-green-50 px-2 py-1 text-xs uppercase text-green-500 dark:border-green-600 dark:bg-green-500 dark:bg-opacity-10",
"danger" => "rounded-md border border-red-300 bg-red-50 px-2 py-1 text-xs uppercase text-red-500 dark:border-red-600 dark:bg-red-500 dark:bg-opacity-10",
"warning" => "rounded-md border border-yellow-300 bg-yellow-50 px-2 py-1 text-xs uppercase text-yellow-500 dark:border-yellow-600 dark:bg-yellow-500 dark:bg-opacity-10",
"disabled" => "rounded-md border border-gray-300 bg-gray-50 px-2 py-1 text-xs uppercase text-gray-500 dark:border-gray-600 dark:bg-gray-500 dark:bg-opacity-30 dark:text-gray-400",
"info" => "rounded-md border border-primary-300 bg-primary-50 px-2 py-1 text-xs uppercase text-primary-500 dark:border-primary-600 dark:bg-primary-500 dark:bg-opacity-10",
"success" => "max-w-max rounded-md border border-green-300 bg-green-50 px-2 py-1 text-xs uppercase text-green-500 dark:border-green-600 dark:bg-green-500 dark:bg-opacity-10",
"danger" => "max-w-max rounded-md border border-red-300 bg-red-50 px-2 py-1 text-xs uppercase text-red-500 dark:border-red-600 dark:bg-red-500 dark:bg-opacity-10",
"warning" => "max-w-max rounded-md border border-yellow-300 bg-yellow-50 px-2 py-1 text-xs uppercase text-yellow-500 dark:border-yellow-600 dark:bg-yellow-500 dark:bg-opacity-10",
"disabled" => "max-w-max rounded-md border border-gray-300 bg-gray-50 px-2 py-1 text-xs uppercase text-gray-500 dark:border-gray-600 dark:bg-gray-500 dark:bg-opacity-30 dark:text-gray-400",
"info" => "max-w-max rounded-md border border-primary-300 bg-primary-50 px-2 py-1 text-xs uppercase text-primary-500 dark:border-primary-600 dark:bg-primary-500 dark:bg-opacity-10",
];
@endphp

View File

@ -1,5 +1,5 @@
<td
{!! $attributes->merge(["class" => "whitespace-nowrap px-6 py-4 text-gray-700 dark:text-gray-300 w-1"]) !!}
{!! $attributes->merge(["class" => "text-sm whitespace-nowrap px-6 py-4 text-gray-700 dark:text-gray-300 w-1"]) !!}
>
{{ $slot }}
</td>

View File

@ -432,7 +432,7 @@ class="-ml-1 mr-1.5 h-[18px] w-[18px]"
></path>
</svg>
<p
class="text-[13px] font-medium leading-none text-gray-800 dark:text-white"
class="text-sm font-medium leading-none text-gray-800 dark:text-white"
x-text="toast.message"
></p>
</div>

View File

@ -30,6 +30,11 @@ class="p-6"
type="text"
class="mt-1 w-full"
/>
<x-input-help class="mt-2">
<a href="https://vitodeploy.com/servers/cronjobs.html" target="_blank" class="text-primary-500">
How the command should look like?
</a>
</x-input-help>
@error("command")
<x-input-error class="mt-2" :messages="$message" />
@enderror

View File

@ -12,19 +12,50 @@
<div x-data="" class="space-y-3">
@if (count($cronjobs) > 0)
@foreach ($cronjobs as $cronjob)
<x-item-card>
<x-item-card id="cronjob-{{ $cronjob->id }}">
<div class="flex flex-grow flex-col items-start justify-center">
<span class="mb-1 flex items-center lowercase text-red-600">
<div class="mb-1 text-left text-xs lowercase text-red-600 md:text-sm">
{{ $cronjob->command }}
</span>
<span class="text-sm text-gray-400">
</div>
<div class="text-xs text-gray-400 md:text-sm">
{{ $cronjob->frequencyLabel() }}
</span>
</div>
</div>
<div class="flex items-center">
@include("cronjobs.partials.status", ["status" => $cronjob->status])
<div class="inline">
<div class="ml-1 inline">
@if ($cronjob->status == \App\Enums\CronjobStatus::READY)
<x-icon-button
id="disable-cronjob-{{ $cronjob->id }}"
data-tooltip="Disable Cronjob"
hx-post="{{ route('servers.cronjobs.disable', ['server' => $server, 'cronJob' => $cronjob]) }}"
hx-target="#cronjob-{{ $cronjob->id }}"
hx-select="#cronjob-{{ $cronjob->id }}"
hx-swap="outerHTML"
hx-ext="disable-element"
hx-disable-element="#disable-cronjob-{{ $cronjob->id }}"
>
<x-heroicon name="o-stop" class="h-5 w-5" />
</x-icon-button>
@endif
@if ($cronjob->status == \App\Enums\CronjobStatus::DISABLED)
<x-icon-button
id="enable-cronjob-{{ $cronjob->id }}"
data-tooltip="Enable Cronjob"
hx-post="{{ route('servers.cronjobs.enable', ['server' => $server, 'cronJob' => $cronjob]) }}"
hx-target="#cronjob-{{ $cronjob->id }}"
hx-select="#cronjob-{{ $cronjob->id }}"
hx-swap="outerHTML"
hx-ext="disable-element"
hx-disable-element="#enable-cronjob-{{ $cronjob->id }}"
>
<x-heroicon name="o-play" class="h-5 w-5" />
</x-icon-button>
@endif
<x-icon-button
data-tooltip="Delete Cronjob"
x-on:click="deleteAction = '{{ route('servers.cronjobs.destroy', ['server' => $server, 'cronJob' => $cronjob]) }}'; $dispatch('open-modal', 'delete-cronjob')"
>
<x-heroicon name="o-trash" class="h-5 w-5" />

View File

@ -6,6 +6,18 @@
<x-status status="warning">{{ $status }}</x-status>
@endif
@if ($status == \App\Enums\CronjobStatus::DISABLING)
<x-status status="warning">{{ $status }}</x-status>
@endif
@if ($status == \App\Enums\CronjobStatus::DISABLED)
<x-status status="disabled">{{ $status }}</x-status>
@endif
@if ($status == \App\Enums\CronjobStatus::ENABLING)
<x-status status="warning">{{ $status }}</x-status>
@endif
@if ($status == \App\Enums\CronjobStatus::DELETING)
<x-status status="danger">{{ $status }}</x-status>
@endif

View File

@ -1,4 +1,4 @@
<x-card>
<x-card id="server-details">
<x-slot name="title">{{ __("Details") }}</x-slot>
<x-slot name="description">
{{ __("More details about your server") }}
@ -14,6 +14,57 @@
<div class="border-t border-gray-200 dark:border-gray-700"></div>
</div>
</div>
<div class="flex items-center justify-between">
<div>{{ __("Last Update Checked") }}</div>
<div>
@if ($server->last_update_check)
<x-datetime :value="$server->last_update_check" />
@else
-
@endif
</div>
</div>
<div>
<div class="py-5">
<div class="border-t border-gray-200 dark:border-gray-700"></div>
</div>
</div>
<div id="available-updates" class="flex items-center justify-between">
<div>{{ __("Available Updates") }} ({{ $server->updates }})</div>
<div class="flex flex-col items-end md:flex-row md:items-center">
@if ($server->updates > 0)
<x-primary-button
id="btn-update-server"
hx-post="{{ route('servers.settings.update', $server) }}"
hx-swap="outerHTML"
hx-target="#server-details"
hx-select="#server-details"
hx-ext="disable-element"
hx-disable-element="#btn-update-server"
>
{{ __("Update") }}
</x-primary-button>
@endif
<x-secondary-button
id="btn-check-updates"
class="mt-2 md:ml-2 md:mt-0"
hx-post="{{ route('servers.settings.check-updates', $server) }}"
hx-swap="outerHTML"
hx-target="#server-details"
hx-select="#server-details"
hx-ext="disable-element"
hx-disable-element="#btn-check-updates"
>
{{ __("Check") }}
</x-secondary-button>
</div>
</div>
<div>
<div class="py-5">
<div class="border-t border-gray-200 dark:border-gray-700"></div>
</div>
</div>
<div class="flex items-center justify-between">
<div>{{ __("Provider") }}</div>
<div class="capitalize">{{ $server->provider }}</div>

View File

@ -183,7 +183,7 @@ class="mt-1 block w-full"
@foreach (config("core.operating_systems") as $operatingSystem)
<option
value="{{ $operatingSystem }}"
@if($operatingSystem == old('os', 'ubuntu_22')) selected @endif
@if($operatingSystem == old('os', \App\Enums\OperatingSystem::UBUNTU24)) selected @endif
>
{{ str($operatingSystem)->replace("_", " ")->ucfirst() }}
LTS

View File

@ -14,4 +14,8 @@
@if ($server->status == \App\Enums\ServerStatus::INSTALLATION_FAILED)
<x-status status="danger">{{ $server->status }}</x-status>
@endif
@if ($server->status == \App\Enums\ServerStatus::UPDATING)
<x-status status="warning">{{ $server->status }}</x-status>
@endif
</div>

View File

@ -131,6 +131,15 @@ class="text-primary-500"
</div>
</div>
<div class="mt-6">
<x-checkbox id="global" name="global" :checked="old('global')" value="1">
Is Global (Accessible in all projects)
</x-checkbox>
@error("global")
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
<div class="mt-6 flex justify-end">
<x-secondary-button type="button" x-on:click="$dispatch('close')">
{{ __("Cancel") }}

View File

@ -0,0 +1,130 @@
<x-modal name="edit-source-control" :show="true">
<form
id="edit-source-control-form"
hx-post="{{ route("settings.source-controls.update", ["sourceControl" => $sourceControl->id]) }}"
hx-swap="outerHTML"
hx-select="#edit-source-control-form"
hx-ext="disable-element"
hx-disable-element="#btn-edit-source-control"
class="p-6"
>
@csrf
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
{{ __("Edit Source Control") }}
</h2>
<div class="mt-6">
<x-input-label for="name" value="Name" />
<x-text-input value="{{ $sourceControl->profile }}" id="name" name="name" type="text" class="mt-1 w-full" />
@error("name")
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
@if ($sourceControl->provider == \App\Enums\SourceControl::GITLAB)
<div class="mt-6">
<x-input-label for="url" value="Url (optional)" />
<x-text-input
value="{{ old('url', $sourceControl->url) }}"
id="url"
name="url"
type="text"
class="mt-1 w-full"
placeholder="e.g. https://gitlab.example.com/"
/>
<x-input-help>
If you run a self-managed gitlab enter the url here, leave empty to use gitlab.com
</x-input-help>
@error("url")
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
@endif
@if (in_array($sourceControl->provider, [\App\Enums\SourceControl::GITLAB, \App\Enums\SourceControl::GITHUB]))
<div class="mt-6">
<x-input-label for="token" value="API Key" />
<x-text-input
value="{{ old('token', $sourceControl->provider()->data()['token']) }}"
id="token"
name="token"
type="text"
class="mt-1 w-full"
/>
@error("token")
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
@endif
@if ($sourceControl->provider == \App\Enums\SourceControl::BITBUCKET)
<div>
<div class="mt-6">
<x-input-label for="username" value="Username" />
<x-text-input
value="{{ old('username', $sourceControl->provider()->data()['username']) }}"
id="username"
name="username"
type="text"
class="mt-1 w-full"
/>
<x-input-help>Your Bitbucket username</x-input-help>
@error("username")
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
<div class="mt-6">
<x-input-label for="password" value="Password" />
<x-text-input
value="{{ old('password', $sourceControl->provider()->data()['password']) }}"
id="password"
name="password"
type="text"
class="mt-1 w-full"
/>
<x-input-help>
Create a new
<a
class="text-primary-500"
href="https://bitbucket.org/account/settings/app-passwords/new"
target="_blank"
>
App Password
</a>
in your Bitbucket account with write and admin access to Workspaces, Projects, Repositories and
Webhooks
</x-input-help>
@error("password")
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
</div>
@endif
<div class="mt-6">
<x-checkbox
id="global"
name="global"
:checked="old('global', $sourceControl->project_id === null ? 1 : null)"
value="1"
>
Is Global (Accessible in all projects)
</x-checkbox>
@error("global")
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
<div class="mt-6 flex justify-end">
<x-secondary-button type="button" x-on:click="$dispatch('close')">
{{ __("Cancel") }}
</x-secondary-button>
<x-primary-button id="btn-edit-source-control" class="ml-3">
{{ __("Save") }}
</x-primary-button>
</div>
</form>
</x-modal>

View File

@ -14,13 +14,29 @@
@include("settings.source-controls.partials.icons." . $sourceControl->provider . "-icon")
</div>
<div class="ml-3 flex flex-grow flex-col items-start justify-center">
<span class="mb-1">{{ $sourceControl->profile }}</span>
<div class="mb-1 flex items-center">
{{ $sourceControl->profile }}
@if (! $sourceControl->project_id)
<x-status status="disabled" class="ml-2">GLOBAL</x-status>
@endif
</div>
<span class="text-sm text-gray-400">
<x-datetime :value="$sourceControl->created_at" />
</span>
</div>
<div class="flex items-center">
<div class="inline">
<x-icon-button
id="edit-{{ $sourceControl->id }}"
hx-get="{{ route('settings.source-controls', ['edit' => $sourceControl->id]) }}"
hx-replace-url="true"
hx-select="#edit"
hx-target="#edit"
hx-ext="disable-element"
hx-disable-element="#edit-{{ $sourceControl->id }}"
>
<x-heroicon name="o-pencil" class="h-5 w-5" />
</x-icon-button>
<x-icon-button
x-on:click="deleteAction = '{{ route('settings.source-controls.delete', $sourceControl->id) }}'; $dispatch('open-modal', 'delete-source-control')"
>
@ -32,6 +48,12 @@
@endforeach
@include("settings.source-controls.partials.delete-source-control")
<div id="edit">
@if (isset($editSourceControl))
@include("settings.source-controls.partials.edit-source-control", ["sourceControl" => $editSourceControl])
@endif
</div>
@else
<x-simple-card>
<div class="text-center">

View File

@ -3,6 +3,8 @@
@include("site-settings.partials.change-php-version")
@include("site-settings.partials.update-aliases")
@if ($site->source_control_id)
@include("site-settings.partials.update-source-control")
@endif

View File

@ -0,0 +1,30 @@
<x-card>
<x-slot name="title">{{ __("Update Aliases") }}</x-slot>
<x-slot name="description">
{{ __("Add/Remove site aliases") }}
</x-slot>
<form
id="update-aliases"
hx-post="{{ route("servers.sites.settings.aliases", ["server" => $server, "site" => $site]) }}"
hx-swap="outerHTML"
hx-select="#update-aliases"
hx-ext="disable-element"
hx-disable-element="#btn-update-aliases"
class="space-y-6"
>
@include(
"sites.partials.create.fields.aliases",
[
"aliases" => $site->aliases,
]
)
</form>
<x-slot name="actions">
<x-primary-button id="btn-update-aliases" form="update-aliases" hx-disable>
{{ __("Save") }}
</x-primary-button>
</x-slot>
</x-card>

View File

@ -55,21 +55,7 @@ class="mt-1 block w-full"
@enderror
</div>
<div>
<x-input-label for="alias" :value="__('Alias')" />
<x-text-input
value="{{ old('alias') }}"
id="alias"
name="alias"
type="text"
class="mt-1 block w-full"
autocomplete="alias"
placeholder="www.example.com"
/>
@error("alias")
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
@include("sites.partials.create.fields.aliases")
@include("sites.partials.create." . $type)
</form>

View File

@ -0,0 +1,55 @@
<script>
let aliases = @json($aliases ?? []);
</script>
<div
x-data="{
aliasInput: '',
aliases: aliases,
removeAlias(alias) {
this.aliases = this.aliases.filter((a) => a !== alias)
},
addAlias() {
if (! this.aliasInput) {
return
}
if (this.aliases.includes(this.aliasInput)) {
return
}
this.aliases.push(this.aliasInput)
this.aliasInput = ''
},
}"
>
<x-input-label for="alias" :value="__('Alias')" />
<div class="flex items-center">
<x-text-input
value="{{ old('alias') }}"
id="alias"
x-model="aliasInput"
name="alias"
type="text"
class="mt-1 block w-full"
autocomplete="alias"
placeholder="www.example.com"
/>
<x-secondary-button type="button" class="ml-2 flex-none" x-on:click="addAlias()">
{{ __("Add") }}
</x-secondary-button>
</div>
<div class="mt-1">
<template x-for="alias in aliases">
<div class="mr-1 inline-flex">
<x-status status="info" class="flex items-center lowercase">
<span x-text="alias"></span>
<x-heroicon name="o-x-mark" class="ml-1 h-4 w-4 cursor-pointer" x-on:click="removeAlias(alias)" />
<input type="hidden" name="aliases[]" x-bind:value="alias" />
</x-status>
</div>
</template>
</div>
@error("aliases")
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>

View File

@ -3,7 +3,7 @@
<div class="mt-1 flex items-center">
<x-select-input id="source_control" name="source_control" class="mt-1 w-full">
<option value="" selected>{{ __("Select") }}</option>
@foreach ($sourceControls as $sourceControl)
@foreach (\App\Models\SourceControl::getByCurrentProject() as $sourceControl)
<option
value="{{ $sourceControl->id }}"
@if($sourceControl->id == old('source_control', isset($site) ? $site->source_control_id : null)) selected @endif

View File

@ -5,7 +5,6 @@
<x-select-input id="version" name="version" class="mt-1 w-full">
<option value="" selected>{{ __("Select") }}</option>
<option value="5.1.2" @if(old('version') == '5.1.2') selected @endif>PHPMyAdmin 5.1.2</option>
<option value="4.9.11" @if(old('version') == '4.9.11') selected @endif>PHPMyAdmin 4.9.11</option>
</x-select-input>
@error("version")
<x-input-error class="mt-2" :messages="$message" />

View File

@ -82,6 +82,12 @@ class="mt-1 w-full"
</div>
</div>
<div class="mt-6">
<x-checkbox id="aliases" name="aliases" :checked="old('aliases')" value="1">
Set SSL for site's aliases as well
</x-checkbox>
</div>
<div class="mt-6 flex justify-end">
<x-secondary-button type="button" x-on:click="$dispatch('close')">
{{ __("Cancel") }}

View File

@ -14,6 +14,7 @@
<x-table>
<x-tr>
<x-th>{{ __("Type") }}</x-th>
<x-th>{{ __("Domains") }}</x-th>
<x-th>{{ __("Created") }}</x-th>
<x-th>{{ __("Expires at") }}</x-th>
<x-th></x-th>
@ -21,6 +22,15 @@
@foreach ($ssls as $ssl)
<x-tr>
<x-td>{{ $ssl->type }}</x-td>
<x-td>
<div class="flex-col space-y-1">
@foreach ($ssl->getDomains() as $domain)
<x-status status="disabled" class="lowercase">
{{ $domain }}
</x-status>
@endforeach
</div>
</x-td>
<x-td>
<x-datetime :value="$ssl->created_at" />
</x-td>

View File

@ -67,6 +67,7 @@
Route::post('/{site}/settings/vhost', [SiteSettingController::class, 'updateVhost']);
Route::post('/{site}/settings/php', [SiteSettingController::class, 'updatePHPVersion'])->name('servers.sites.settings.php');
Route::post('/{site}/settings/source-control', [SiteSettingController::class, 'updateSourceControl'])->name('servers.sites.settings.source-control');
Route::post('/{site}/settings/update-aliases', [SiteSettingController::class, 'updateAliases'])->name('servers.sites.settings.aliases');
// site logs
Route::get('/{site}/logs', [SiteLogController::class, 'index'])->name('servers.sites.logs');
@ -113,6 +114,8 @@
Route::get('/{server}/cronjobs', [CronjobController::class, 'index'])->name('servers.cronjobs');
Route::post('/{server}/cronjobs', [CronjobController::class, 'store'])->name('servers.cronjobs.store');
Route::delete('/{server}/cronjobs/{cronJob}', [CronjobController::class, 'destroy'])->name('servers.cronjobs.destroy');
Route::post('/{server}/cronjobs/{cronJob}/enable', [CronjobController::class, 'enable'])->name('servers.cronjobs.enable');
Route::post('/{server}/cronjobs/{cronJob}/disable', [CronjobController::class, 'disable'])->name('servers.cronjobs.disable');
// ssh keys
Route::get('/{server}/ssh-keys', [SSHKeyController::class, 'index'])->name('servers.ssh-keys');
@ -145,6 +148,8 @@
Route::post('/check-connection', [ServerSettingController::class, 'checkConnection'])->name('servers.settings.check-connection');
Route::post('/reboot', [ServerSettingController::class, 'reboot'])->name('servers.settings.reboot');
Route::post('/edit', [ServerSettingController::class, 'edit'])->name('servers.settings.edit');
Route::post('/check-updates', [ServerSettingController::class, 'checkUpdates'])->name('servers.settings.check-updates');
Route::post('/update', [ServerSettingController::class, 'update'])->name('servers.settings.update');
});
// logs

View File

@ -38,6 +38,7 @@
Route::get('/', [SourceControlController::class, 'index'])->name('settings.source-controls');
Route::post('connect', [SourceControlController::class, 'connect'])->name('settings.source-controls.connect');
Route::delete('delete/{sourceControl}', [SourceControlController::class, 'delete'])->name('settings.source-controls.delete');
Route::post('edit/{sourceControl}', [SourceControlController::class, 'update'])->name('settings.source-controls.update');
});
// storage-providers

View File

@ -24,7 +24,7 @@
require __DIR__.'/settings.php';
});
Route::prefix('/servers')->group(function () {
Route::prefix('/servers')->middleware('must-have-current-project')->group(function () {
require __DIR__.'/server.php';
});

2
scripts/post-update.sh Normal file
View File

@ -0,0 +1,2 @@
# post-update script is here to cover extra commands in case of an update requires it.
echo "Running post-update script..."

View File

@ -1,23 +1,29 @@
#!/bin/bash
echo "Updating Vito..."
cd /home/vito/vito
php artisan down
echo "Pulling changes..."
git fetch --all
git checkout $(git tag -l --merged 1.x --sort=-v:refname | head -n 1)
echo "Checking out the latest tag..."
NEW_RELEASE=$(git tag -l --merged 1.x --sort=-v:refname | head -n 1)
git checkout "$NEW_RELEASE"
git pull origin "$NEW_RELEASE"
echo "Installing composer dependencies..."
composer install --no-dev
echo "Running migrations..."
php artisan migrate --force
php artisan config:clear
php artisan cache:clear
php artisan view:clear
php artisan config:cache
echo "Optimizing..."
php artisan optimize:clear
php artisan optimize
echo "Restarting workers..."
sudo supervisorctl restart worker:*
php artisan up
bash scripts/post-update.sh
echo "Vito updated successfully to $NEW_RELEASE! 🎉"

View File

@ -99,4 +99,60 @@ public function test_create_custom_cronjob()
SSH::assertExecutedContains("echo '* * * 1 1 ls -la' | sudo -u vito crontab -");
SSH::assertExecutedContains('sudo -u vito crontab -l');
}
public function test_enable_cronjob()
{
SSH::fake();
$this->actingAs($this->user);
/** @var CronJob $cronjob */
$cronjob = CronJob::factory()->create([
'server_id' => $this->server->id,
'user' => 'vito',
'command' => 'ls -la',
'frequency' => '* * * 1 1',
'status' => CronjobStatus::DISABLED,
]);
$this->post(route('servers.cronjobs.enable', [
'server' => $this->server,
'cronJob' => $cronjob,
]))->assertSessionDoesntHaveErrors();
$cronjob->refresh();
$this->assertEquals(CronjobStatus::READY, $cronjob->status);
SSH::assertExecutedContains("echo '* * * 1 1 ls -la' | sudo -u vito crontab -");
SSH::assertExecutedContains('sudo -u vito crontab -l');
}
public function test_disable_cronjob()
{
SSH::fake();
$this->actingAs($this->user);
/** @var CronJob $cronjob */
$cronjob = CronJob::factory()->create([
'server_id' => $this->server->id,
'user' => 'vito',
'command' => 'ls -la',
'frequency' => '* * * 1 1',
'status' => CronjobStatus::READY,
]);
$this->post(route('servers.cronjobs.disable', [
'server' => $this->server,
'cronJob' => $cronjob,
]))->assertSessionDoesntHaveErrors();
$cronjob->refresh();
$this->assertEquals(CronjobStatus::DISABLED, $cronjob->status);
SSH::assertExecutedContains("echo '' | sudo -u vito crontab -");
SSH::assertExecutedContains('sudo -u vito crontab -l');
}
}

View File

@ -280,4 +280,32 @@ public function test_edit_server_ip_address_and_disconnect(): void
'status' => ServerStatus::DISCONNECTED,
]);
}
public function test_check_updates(): void
{
SSH::fake('Available updates:10');
$this->actingAs($this->user);
$this->post(route('servers.settings.check-updates', $this->server))
->assertSessionDoesntHaveErrors();
$this->server->refresh();
$this->assertEquals(9, $this->server->updates);
}
public function test_update_server(): void
{
SSH::fake('Available updates:0');
$this->actingAs($this->user);
$this->post(route('servers.settings.update', $this->server))
->assertSessionDoesntHaveErrors();
$this->server->refresh();
$this->assertEquals(ServerStatus::READY, $this->server->status);
$this->assertEquals(0, $this->server->updates);
}
}

View File

@ -42,6 +42,7 @@ public function test_create_site(array $inputs): void
$this->assertDatabaseHas('sites', [
'domain' => 'example.com',
'aliases' => json_encode($inputs['aliases'] ?? []),
'status' => SiteStatus::READY,
]);
}
@ -54,7 +55,7 @@ public function test_create_site_failed_due_to_source_control(int $status): void
$inputs = [
'type' => SiteType::LARAVEL,
'domain' => 'example.com',
'alias' => 'www.example.com',
'aliases' => ['www.example.com'],
'php_version' => '8.2',
'web_directory' => 'public',
'repository' => 'test/test',
@ -220,7 +221,7 @@ public static function create_data(): array
[
'type' => SiteType::LARAVEL,
'domain' => 'example.com',
'alias' => 'www.example.com',
'aliases' => ['www.example.com', 'www2.example.com'],
'php_version' => '8.2',
'web_directory' => 'public',
'repository' => 'test/test',
@ -232,7 +233,7 @@ public static function create_data(): array
[
'type' => SiteType::WORDPRESS,
'domain' => 'example.com',
'alias' => 'www.example.com',
'aliases' => ['www.example.com'],
'php_version' => '8.2',
'title' => 'Example',
'username' => 'example',
@ -247,7 +248,7 @@ public static function create_data(): array
[
'type' => SiteType::PHP_BLANK,
'domain' => 'example.com',
'alias' => 'www.example.com',
'aliases' => ['www.example.com'],
'php_version' => '8.2',
'web_directory' => 'public',
],
@ -256,7 +257,7 @@ public static function create_data(): array
[
'type' => SiteType::PHPMYADMIN,
'domain' => 'example.com',
'alias' => 'www.example.com',
'aliases' => ['www.example.com'],
'php_version' => '8.2',
'version' => '5.1.2',
],

View File

@ -35,6 +35,20 @@ public function test_connect_provider(string $provider, ?string $customUrl, arra
'provider' => $provider,
'url' => $customUrl,
]);
if (isset($input['global'])) {
$this->assertDatabaseHas('source_controls', [
'provider' => $provider,
'url' => $customUrl,
'project_id' => null,
]);
} else {
$this->assertDatabaseHas('source_controls', [
'provider' => $provider,
'url' => $customUrl,
'project_id' => $this->user->current_project_id,
]);
}
}
/**
@ -85,10 +99,38 @@ public function test_cannot_delete_provider(string $provider): void
]);
}
/**
* @dataProvider data
*/
public function test_edit_source_control(string $provider, ?string $url, array $input): void
{
Http::fake();
$this->actingAs($this->user);
/** @var SourceControl $sourceControl */
$sourceControl = SourceControl::factory()->create([
'provider' => $provider,
'profile' => 'old-name',
'url' => $url,
]);
$this->post(route('settings.source-controls.update', $sourceControl->id), array_merge([
'name' => 'new-name',
'url' => $url,
], $input))->assertSessionDoesntHaveErrors();
$sourceControl->refresh();
$this->assertEquals('new-name', $sourceControl->profile);
$this->assertEquals($url, $sourceControl->url);
}
public static function data(): array
{
return [
['github', null, ['token' => 'test']],
['github', null, ['token' => 'test', 'global' => '1']],
['gitlab', null, ['token' => 'test']],
['gitlab', 'https://git.example.com/', ['token' => 'test']],
['bitbucket', null, ['username' => 'test', 'password' => 'test']],

View File

@ -58,6 +58,29 @@ public function test_letsencrypt_ssl()
'site_id' => $this->site->id,
'type' => SslType::LETSENCRYPT,
'status' => SslStatus::CREATED,
'domains' => json_encode([$this->site->domain]),
]);
}
public function test_letsencrypt_ssl_with_aliases()
{
SSH::fake('Successfully received certificate');
$this->actingAs($this->user);
$this->post(route('servers.sites.ssl.store', [
'server' => $this->server,
'site' => $this->site,
]), [
'type' => SslType::LETSENCRYPT,
'aliases' => '1',
])->assertSessionDoesntHaveErrors();
$this->assertDatabaseHas('ssls', [
'site_id' => $this->site->id,
'type' => SslType::LETSENCRYPT,
'status' => SslStatus::CREATED,
'domains' => json_encode(array_merge([$this->site->domain], $this->site->aliases)),
]);
}

161
tests/Feature/UserTest.php Normal file
View File

@ -0,0 +1,161 @@
<?php
namespace Tests\Feature;
use App\Enums\UserRole;
use App\Models\Project;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class UserTest extends TestCase
{
use RefreshDatabase;
public function test_create_user(): void
{
$this->actingAs($this->user);
$this->post(route('settings.users.store'), [
'name' => 'new user',
'email' => 'newuser@example.com',
'password' => 'password',
'role' => UserRole::USER,
])->assertSessionDoesntHaveErrors();
$this->assertDatabaseHas('users', [
'name' => 'new user',
'email' => 'newuser@example.com',
'role' => UserRole::USER,
]);
}
public function test_see_users_list(): void
{
$this->actingAs($this->user);
$user = User::factory()->create();
$this->get(route('settings.users.index'))
->assertSuccessful()
->assertSee($user->name);
}
public function test_must_be_admin_to_see_users_list(): void
{
$this->user->role = UserRole::USER;
$this->user->save();
$this->actingAs($this->user);
$user = User::factory()->create();
$this->get(route('settings.users.index'))
->assertForbidden();
}
public function test_delete_user(): void
{
$this->actingAs($this->user);
$user = User::factory()->create();
$this->delete(route('settings.users.delete', $user))
->assertSessionDoesntHaveErrors();
$this->assertDatabaseMissing('users', [
'id' => $user->id,
]);
}
public function test_cannot_delete_yourself(): void
{
$this->actingAs($this->user);
$this->delete(route('settings.users.delete', $this->user))
->assertSessionDoesntHaveErrors()
->assertSessionHas('toast.type', 'error');
$this->assertDatabaseHas('users', [
'id' => $this->user->id,
]);
}
public function test_see_user(): void
{
$this->actingAs($this->user);
$user = User::factory()->create();
$this->get(route('settings.users.show', $user))
->assertSuccessful()
->assertSee($user->name);
}
public function test_edit_user_info(): void
{
$this->actingAs($this->user);
$user = User::factory()->create();
$this->post(route('settings.users.update', $user), [
'name' => 'new-name',
'email' => 'newemail@example.com',
'timezone' => 'Europe/London',
'role' => UserRole::ADMIN,
])
->assertSessionDoesntHaveErrors();
$this->assertDatabaseHas('users', [
'id' => $user->id,
'name' => 'new-name',
'email' => 'newemail@example.com',
'timezone' => 'Europe/London',
'role' => UserRole::ADMIN,
]);
}
public function test_edit_user_projects(): void
{
$this->actingAs($this->user);
$user = User::factory()->create();
$project = Project::factory()->create();
$this->post(route('settings.users.update-projects', $user), [
'projects' => [$project->id],
])
->assertSessionDoesntHaveErrors();
$this->assertDatabaseHas('user_project', [
'user_id' => $user->id,
'project_id' => $project->id,
]);
}
public function test_edit_user_projects_with_current_project(): void
{
$this->actingAs($this->user);
$user = User::factory()->create();
$user->current_project_id = null;
$user->save();
$project = Project::factory()->create();
$this->post(route('settings.users.update-projects', $user), [
'projects' => [$project->id],
])
->assertSessionDoesntHaveErrors();
$this->assertDatabaseHas('user_project', [
'user_id' => $user->id,
'project_id' => $project->id,
]);
$this->assertDatabaseHas('users', [
'id' => $user->id,
'current_project_id' => $project->id,
]);
}
}