Skip to content
2 changes: 1 addition & 1 deletion app/Console/Server/ServerAddCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$this->displayServerDeets($server);

//
// Get server info (verifies SSH connection and validates distribution)
// Get server info (verifies SSH connection and validates distribution & permissions)
// ----

$info = $this->getServerInfo($server);
Expand Down
2 changes: 1 addition & 1 deletion app/Console/Server/ServerInfoCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$this->displayServerDeets($server);

//
// Get server info (verifies SSH connection and validates distribution)
// Get server info (verifies SSH connection and validates distribution & permissions)
// ----

$info = $this->getServerInfo($server);
Expand Down
66 changes: 33 additions & 33 deletions app/Console/Server/ServerInstallCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ class ServerInstallCommand extends BaseCommand
use PlaybooksTrait;
use ServersTrait;

// ---- Configuration
// ----
// Configuration
// ----

protected function configure(): void
Expand All @@ -34,7 +35,7 @@ protected function configure(): void
$this->addOption('server', null, InputOption::VALUE_REQUIRED, 'Server name');
}

//
// ----
// Execution
// ----

Expand All @@ -57,7 +58,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$this->displayServerDeets($server);

//
// Get server info (verifies SSH connection and validates distribution)
// Get server info (verifies SSH connection and validates distribution & permissions)
// ----

$info = $this->getServerInfo($server);
Expand All @@ -66,40 +67,26 @@ protected function execute(InputInterface $input, OutputInterface $output): int
return $info;
}

//
// Validate server info
// ----
[
'distro' => $distro,
'permissions' => $permissions,
] = $info;

/** @var string $distro */
$distro = $info['distro'] ?? 'unknown';
$distribution = Distribution::tryFrom($distro);
if ($distribution === null) {
$this->nay("Distribution validation failed: {$distro}");

return Command::FAILURE;
}

$permissions = $info['permissions'] ?? null;
if (!is_string($permissions) || !in_array($permissions, ['root', 'sudo'])) {
$this->nay('Server requires root or sudo permissions to install software');

return Command::FAILURE;
}

$family = $distribution->family()->value;
/** @var string $permissions */

//
// Execute installation playbook
// ---
// ----

$result = $this->executePlaybook(
$server,
'server-install',
'Installing server...',
[
'DEPLOYER_DISTRO' => $distro,
'DEPLOYER_FAMILY' => $family,
'DEPLOYER_PERMS' => $permissions,
'DEPLOYER_SERVER_NAME' => $server->name,
],
true
);
Expand All @@ -116,12 +103,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int
// Setup demo site
// ----

/** @var string $permissions */
$demoResult = $this->executePlaybook(
$server,
'demo-site',
'Setting up demo site...',
[
'DEPLOYER_FAMILY' => $family,
'DEPLOYER_PERMS' => $permissions,
],
true
Expand All @@ -140,8 +127,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int
// ----

$url = 'http://' . $server->host;
$deployKey = isset($result['deploy_public_key']) && is_string($result['deploy_public_key']) && $result['deploy_public_key'] !== 'unknown'
? $result['deploy_public_key']
: null;

$verification = $this->io->promptSpin(
fn () => $this->verifyInstallation($url),
fn () => $this->verifyInstallation($url, $deployKey),
'Verifying installation...'
);

Expand Down Expand Up @@ -175,7 +166,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
*
* @return array{status: 'success'|'warning', message: string, lines: array<int, string>}
*/
private function verifyInstallation(string $url): array
private function verifyInstallation(string $url, ?string $deployKey): array
{
try {
$client = new Client([
Expand Down Expand Up @@ -203,15 +194,24 @@ private function verifyInstallation(string $url): array
];
}

$nextSteps = [
'Next steps:',
' • Caddy running at <fg=cyan>' . $url . '</>',
' • Run <fg=cyan>site:add</> to deploy your first application',
];

if ($deployKey !== null) {
$nextSteps[] = ' • Add this key to your Git provider (GitHub, GitLab, etc.) to enable deployments:';
$nextSteps[] = '';
$nextSteps[] = '<fg=cyan>' . $deployKey . '</>';
}

$nextSteps[] = '';

return [
'status' => 'success',
'message' => 'Server installation completed successfully',
'lines' => [
'Next steps:',
' • Caddy running at <fg=cyan>' . $url . '</>',
' • Run <fg=cyan>site:add</> to deploy your first application',
'',
],
'lines' => $nextSteps,
];
} catch (\Throwable $e) {
return [
Expand Down
2 changes: 1 addition & 1 deletion app/Console/Server/ServerLogsCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$this->displayServerDeets($server);

//
// Get server info (verifies SSH connection and validates distribution)
// Get server info (verifies SSH connection and validates distribution & permissions)
// ----

$info = $this->getServerInfo($server);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int

$this->displayServerDeets($server);

// Get server info (verifies SSH connection and validates distribution)
// Get server info (verifies SSH connection and validates distribution & permissions)
$info = $this->getServerInfo($server);

if (is_int($info)) {
Expand Down
3 changes: 1 addition & 2 deletions app/Traits/PlaybooksTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,7 @@ trait PlaybooksTrait
*
* Standard playbook environment variables:
* - DEPLOYER_OUTPUT_FILE: Output file path (provided automatically)
* - DEPLOYER_DISTRO: Exact distribution - caller must provide via $playbookVars
* - DEPLOYER_FAMILY: Distribution family - caller must provide via $playbookVars
* - DEPLOYER_DISTRO: Exact distribution (ubuntu|debian) - caller must provide via $playbookVars
* - DEPLOYER_PERMS: User permissions (root|sudo|none) - caller must provide via $playbookVars
*
* @param string $playbookName Playbook name without .sh extension (e.g., 'server-info', 'install-php', etc)
Expand Down
37 changes: 35 additions & 2 deletions app/Traits/ServersTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ trait ServersTrait
/**
* Get server information by executing server-info playbook.
*
* Automatically displays server info and validates that the server is running a supported distribution (Debian/Ubuntu).
* Automatically displays server info and validates that the server is running a supported distribution (Debian/Ubuntu)
* and has sufficient permissions (root or sudo).
*
* @param ServerDTO $server Server to get information for
* @return array<string, mixed>|int Returns parsed server info or failure code on failure
Expand All @@ -58,7 +59,15 @@ protected function getServerInfo(ServerDTO $server): array|int
// Display server information before validation
$this->displayServerInfo($info);

return $this->validateServerDistribution($info);
// Validate server distribution and permissions
$distroResult = $this->validateServerDistribution($info);
$permissionsResult = $this->validateServerPermissions($info);

if (is_int($distroResult) || is_int($permissionsResult)) {
return Command::FAILURE;
}

return $info;
}

/**
Expand Down Expand Up @@ -90,6 +99,25 @@ protected function validateServerDistribution(array $info): array|int
return $info;
}

/**
* Validate that server has sufficient permissions (root or sudo).
*
* @param array<string, mixed> $info Server information array from server-info playbook
* @return array<string, mixed>|int Returns validated server info or failure code
*/
protected function validateServerPermissions(array $info): array|int
{
$permissions = $info['permissions'] ?? null;

if (!is_string($permissions) || !in_array($permissions, ['root', 'sudo'])) {
$this->nay('Server requires root or sudo permissions');

return Command::FAILURE;
}

return $info;
}

/**
* Display formatted server information.
*
Expand Down Expand Up @@ -319,6 +347,11 @@ protected function validateServerName(mixed $name): ?string
return 'Server name cannot be empty';
}

// Validate format: alphanumeric, hyphens, underscores only
if (!preg_match('/^[a-zA-Z0-9_-]+$/', $name)) {
return 'Server name can only contain letters, numbers, hyphens, and underscores';
}

// Check uniqueness
$existing = $this->servers->findByName($name);
if ($existing !== null) {
Expand Down
Loading