Merge remote-tracking branch 'origin/next' into feat/railpack

This commit is contained in:
Andras Bacsai committed 2026-04-30 11:47:06 +02:00
commit 8e91d627a3
8 files changed
+216 -17

No files matched your search

+1 -5
View File
@@ -4,7 +4,6 @@ namespace App\Actions\Server;
use App\Events\SentinelRestarted;
use App\Models\Server;
use App\Models\ServerSetting;
use Lorisleiva\Actions\Concerns\AsAction;
class StartSentinel
@@ -23,10 +22,7 @@ class StartSentinel
$metricsHistory = data_get($server, 'settings.sentinel_metrics_history_days');
$refreshRate = data_get($server, 'settings.sentinel_metrics_refresh_rate_seconds');
$pushInterval = data_get($server, 'settings.sentinel_push_interval_seconds');
$token = data_get($server, 'settings.sentinel_token');
if (! ServerSetting::isValidSentinelToken($token)) {
throw new \RuntimeException('Invalid sentinel token format. Token must contain only alphanumeric characters, dots, hyphens, and underscores.');
}
$token = $server->settings->ensureValidSentinelToken();
$endpoint = data_get($server, 'settings.sentinel_custom_url');
$debug = data_get($server, 'settings.is_sentinel_debug_enabled');
$mountDir = '/data/coolify/sentinel';
@@ -29,6 +29,7 @@ class UploadController extends BaseController
'archive.gz',
'bz2',
'xz',
'dmp',
];
public function upload(Request $request)
+1 -1
View File
@@ -45,7 +45,7 @@ class Email extends Component
public ?string $smtpPort = null;
#[Validate(['nullable', 'string', 'in:starttls,tls,none'])]
public ?string $smtpEncryption = null;
public ?string $smtpEncryption = 'starttls';
#[Validate(['nullable', 'string'])]
public ?string $smtpUsername = null;
+41 -5
View File
@@ -2,6 +2,7 @@
namespace App\Models;
use Illuminate\Contracts\Encryption\DecryptException;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Log;
@@ -144,19 +145,54 @@ class ServerSetting extends Model
* Validate that a sentinel token contains only safe characters.
* Prevents OS command injection when the token is interpolated into shell commands.
*/
public static function isValidSentinelToken(string $token): bool
public static function isValidSentinelToken(?string $token): bool
{
if ($token === null) {
return false;
}
return (bool) preg_match('/\A[a-zA-Z0-9._\-+=\/]+\z/', $token);
}
public function generateSentinelToken(bool $save = true, bool $ignoreEvent = false)
/**
* Returns a valid sentinel token, regenerating it if the stored value is
* empty, undecryptable, or otherwise invalid. Throws only when regeneration
* still fails to produce a valid token.
*/
public function ensureValidSentinelToken(): string
{
try {
$token = $this->sentinel_token;
} catch (DecryptException) {
$token = null;
}
if (! self::isValidSentinelToken($token)) {
// Clear undecryptable raw value so Eloquent's dirty-check won't try to
// decrypt the bad original during save().
$attrs = $this->getAttributes();
$attrs['sentinel_token'] = null;
$this->setRawAttributes($attrs, true);
$this->generateSentinelToken(save: true, ignoreEvent: true);
$this->refresh();
$token = $this->sentinel_token;
}
if (! self::isValidSentinelToken($token)) {
throw new \RuntimeException('Sentinel token invalid after regeneration. Allowed characters: a-z, A-Z, 0-9, dot, underscore, hyphen, plus, slash, equals.');
}
return $token;
}
public function generateSentinelToken(bool $save = true, bool $ignoreEvent = false): string
{
$data = [
'server_uuid' => $this->server->uuid,
];
$token = json_encode($data);
$encrypted = encrypt($token);
$this->sentinel_token = $encrypted;
$token = encrypt(json_encode($data));
$this->sentinel_token = $token;
if ($save) {
if ($ignoreEvent) {
$this->saveQuietly();
+23
View File
@@ -3,9 +3,12 @@
namespace App\Providers;
use App\Contracts\CustomJobRepositoryInterface;
use App\Exceptions\DeploymentException;
use App\Models\ApplicationDeploymentQueue;
use App\Models\User;
use App\Repositories\CustomJobRepository;
use Illuminate\Queue\Events\JobFailed;
use Illuminate\Queue\TimeoutExceededException;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Gate;
use Laravel\Horizon\Contracts\JobRepository;
@@ -48,6 +51,26 @@ class HorizonServiceProvider extends HorizonApplicationServiceProvider
]);
}
});
Event::listen(function (JobFailed $event) {
if (! isCloud()) {
return;
}
$exception = $event->exception;
if (! ($exception instanceof DeploymentException) && ! ($exception instanceof TimeoutExceededException)) {
return;
}
try {
$uuid = $event->job->uuid();
if ($uuid) {
app(JobRepository::class)->deleteFailed($uuid);
}
} catch (\Throwable $e) {
// Best-effort scrub; never mask the original failure.
}
});
}
protected function gate(): void
+14 -6
View File
@@ -2,7 +2,9 @@
namespace App\Traits;
use App\Models\ServerSetting;
use App\Models\Server;
use Illuminate\Contracts\Encryption\DecryptException;
use Illuminate\Support\Facades\Log;
trait HasMetrics
{
@@ -28,9 +30,15 @@ trait HasMetrics
$from = now()->subMinutes($mins)->toIso8601ZuluString();
$endpoint = $this->getMetricsEndpoint($type, $from);
$token = $server->settings->sentinel_token;
if (! ServerSetting::isValidSentinelToken($token)) {
throw new \Exception('Invalid sentinel token format. Please regenerate the token.');
$previousToken = null;
try {
$previousToken = $server->settings->sentinel_token;
} catch (DecryptException) {
// fall through to ensureValidSentinelToken which will regenerate
}
$token = $server->settings->ensureValidSentinelToken();
if ($token !== $previousToken) {
Log::warning('Regenerated sentinel token during metrics read; sentinel container restart required', ['server_id' => $server->id]);
}
$response = instant_remote_process(
@@ -61,10 +69,10 @@ trait HasMetrics
private function isServerMetrics(): bool
{
return $this instanceof \App\Models\Server;
return $this instanceof Server;
}
private function getMetricsServer(): \App\Models\Server
private function getMetricsServer(): Server
{
return $this->isServerMetrics() ? $this : $this->destination->server;
}