2023-09-20 13:42:41 +00:00
< ? php
2023-12-07 18:06:32 +00:00
namespace App\Livewire\Project\Service ;
2023-09-20 13:42:41 +00:00
2026-01-02 15:29:48 +00:00
use App\Actions\Database\StartDatabaseProxy ;
use App\Actions\Database\StopDatabaseProxy ;
use App\Models\Server ;
2023-09-20 13:42:41 +00:00
use App\Models\Service ;
2024-01-07 15:23:41 +00:00
use App\Models\ServiceApplication ;
use App\Models\ServiceDatabase ;
2025-08-26 08:27:31 +00:00
use Illuminate\Foundation\Auth\Access\AuthorizesRequests ;
2024-01-07 15:23:41 +00:00
use Illuminate\Support\Collection ;
2026-01-02 15:29:48 +00:00
use Illuminate\Support\Facades\DB ;
2023-09-20 13:42:41 +00:00
use Livewire\Component ;
2026-01-02 15:29:48 +00:00
use Spatie\Url\Url ;
2023-09-20 13:42:41 +00:00
class Index extends Component
{
2025-08-26 08:27:31 +00:00
use AuthorizesRequests ;
2024-04-08 09:16:20 +00:00
public ? Service $service = null ;
2024-06-10 20:43:34 +00:00
2024-01-07 15:23:41 +00:00
public ? ServiceApplication $serviceApplication = null ;
2024-06-10 20:43:34 +00:00
2024-01-07 15:23:41 +00:00
public ? ServiceDatabase $serviceDatabase = null ;
2024-06-10 20:43:34 +00:00
2026-01-02 15:29:48 +00:00
public ? string $resourceType = null ;
public ? string $currentRoute = null ;
2023-09-20 13:42:41 +00:00
public array $parameters ;
2024-06-10 20:43:34 +00:00
2023-09-20 13:42:41 +00:00
public array $query ;
2024-06-10 20:43:34 +00:00
2024-01-07 15:23:41 +00:00
public Collection $services ;
2024-06-10 20:43:34 +00:00
2024-01-07 15:23:41 +00:00
public $s3s ;
2026-01-02 15:29:48 +00:00
public ? Server $server = null ;
// Database-specific properties
public ? string $db_url_public = null ;
public $fileStorages ;
public ? string $humanName = null ;
public ? string $description = null ;
public ? string $image = null ;
public bool $excludeFromStatus = false ;
public ? int $publicPort = null ;
2026-02-27 05:07:09 +00:00
public ? int $publicPortTimeout = 3600 ;
2026-01-02 15:29:48 +00:00
public bool $isPublic = false ;
public bool $isLogDrainEnabled = false ;
public bool $isImportSupported = false ;
// Application-specific properties
public $docker_cleanup = true ;
public $delete_volumes = true ;
public $domainConflicts = [];
public $showDomainConflictModal = false ;
public $forceSaveDomains = false ;
public $showPortWarningModal = false ;
public $forceRemovePort = false ;
public $requiredPort = null ;
public ? string $fqdn = null ;
public bool $isGzipEnabled = false ;
public bool $isStripprefixEnabled = false ;
protected $listeners = [ 'generateDockerCompose' , 'refreshScheduledBackups' => '$refresh' , 'refreshFileStorages' ];
protected $rules = [
'humanName' => 'nullable' ,
'description' => 'nullable' ,
'image' => 'required' ,
'excludeFromStatus' => 'required|boolean' ,
'publicPort' => 'nullable|integer' ,
2026-03-10 08:59:19 +00:00
'publicPortTimeout' => 'nullable|integer|min:1' ,
2026-01-02 15:29:48 +00:00
'isPublic' => 'required|boolean' ,
'isLogDrainEnabled' => 'required|boolean' ,
// Application-specific rules
'fqdn' => 'nullable' ,
'isGzipEnabled' => 'nullable|boolean' ,
'isStripprefixEnabled' => 'nullable|boolean' ,
];
2024-01-07 15:23:41 +00:00
2023-09-30 13:08:40 +00:00
public function mount ()
{
2024-01-07 15:23:41 +00:00
try {
$this -> services = collect ([]);
$this -> parameters = get_route_parameters ();
$this -> query = request () -> query ();
2026-01-02 15:29:48 +00:00
$this -> currentRoute = request () -> route () -> getName ();
2024-04-08 09:16:20 +00:00
$this -> service = Service :: whereUuid ( $this -> parameters [ 'service_uuid' ]) -> first ();
2024-06-10 20:43:34 +00:00
if ( ! $this -> service ) {
2024-04-08 09:16:20 +00:00
return redirect () -> route ( 'dashboard' );
}
2025-08-26 08:27:31 +00:00
$this -> authorize ( 'view' , $this -> service );
2024-02-11 14:44:02 +00:00
$service = $this -> service -> applications () -> whereUuid ( $this -> parameters [ 'stack_service_uuid' ]) -> first ();
2024-01-07 15:23:41 +00:00
if ( $service ) {
$this -> serviceApplication = $service ;
2026-01-02 15:29:48 +00:00
$this -> resourceType = 'application' ;
2024-01-07 15:23:41 +00:00
$this -> serviceApplication -> getFilesFromServer ();
2026-01-02 15:29:48 +00:00
$this -> initializeApplicationProperties ();
2024-01-07 15:23:41 +00:00
} else {
2024-02-11 14:44:02 +00:00
$this -> serviceDatabase = $this -> service -> databases () -> whereUuid ( $this -> parameters [ 'stack_service_uuid' ]) -> first ();
2026-01-02 12:46:53 +00:00
if ( ! $this -> serviceDatabase ) {
return redirect () -> route ( 'project.service.configuration' , [
'project_uuid' => $this -> parameters [ 'project_uuid' ],
'environment_uuid' => $this -> parameters [ 'environment_uuid' ],
'service_uuid' => $this -> parameters [ 'service_uuid' ],
]);
}
2026-01-02 15:29:48 +00:00
$this -> resourceType = 'database' ;
2024-01-07 15:23:41 +00:00
$this -> serviceDatabase -> getFilesFromServer ();
2026-01-02 15:29:48 +00:00
$this -> initializeDatabaseProperties ();
2024-01-07 15:23:41 +00:00
}
$this -> s3s = currentTeam () -> s3s ;
2024-06-10 20:43:34 +00:00
} catch ( \Throwable $e ) {
2024-01-07 15:23:41 +00:00
return handleError ( $e , $this );
}
2023-09-30 13:08:40 +00:00
}
2024-06-10 20:43:34 +00:00
2026-01-02 15:29:48 +00:00
private function initializeDatabaseProperties () : void
{
$this -> server = $this -> serviceDatabase -> service -> destination -> server ;
if ( $this -> serviceDatabase -> is_public ) {
$this -> db_url_public = $this -> serviceDatabase -> getServiceDatabaseUrl ();
}
$this -> refreshFileStorages ();
$this -> syncDatabaseData ( false );
// Check if import is supported for this database type
$dbType = $this -> serviceDatabase -> databaseType ();
$supportedTypes = [ 'mysql' , 'mariadb' , 'postgres' , 'mongo' ];
$this -> isImportSupported = collect ( $supportedTypes ) -> contains ( fn ( $type ) => str_contains ( $dbType , $type ));
}
private function syncDatabaseData ( bool $toModel = false ) : void
{
if ( $toModel ) {
$this -> serviceDatabase -> human_name = $this -> humanName ;
$this -> serviceDatabase -> description = $this -> description ;
$this -> serviceDatabase -> image = $this -> image ;
$this -> serviceDatabase -> exclude_from_status = $this -> excludeFromStatus ;
$this -> serviceDatabase -> public_port = $this -> publicPort ;
2026-02-27 05:07:09 +00:00
$this -> serviceDatabase -> public_port_timeout = $this -> publicPortTimeout ;
2026-01-02 15:29:48 +00:00
$this -> serviceDatabase -> is_public = $this -> isPublic ;
$this -> serviceDatabase -> is_log_drain_enabled = $this -> isLogDrainEnabled ;
} else {
$this -> humanName = $this -> serviceDatabase -> human_name ;
$this -> description = $this -> serviceDatabase -> description ;
$this -> image = $this -> serviceDatabase -> image ;
$this -> excludeFromStatus = $this -> serviceDatabase -> exclude_from_status ? ? false ;
$this -> publicPort = $this -> serviceDatabase -> public_port ;
2026-02-27 05:07:09 +00:00
$this -> publicPortTimeout = $this -> serviceDatabase -> public_port_timeout ;
2026-01-02 15:29:48 +00:00
$this -> isPublic = $this -> serviceDatabase -> is_public ? ? false ;
$this -> isLogDrainEnabled = $this -> serviceDatabase -> is_log_drain_enabled ? ? false ;
}
}
2024-01-07 15:23:41 +00:00
public function generateDockerCompose ()
2023-09-30 13:08:40 +00:00
{
2025-08-26 08:27:31 +00:00
try {
$this -> authorize ( 'update' , $this -> service );
$this -> service -> parse ();
} catch ( \Throwable $e ) {
return handleError ( $e , $this );
}
2023-09-26 12:45:52 +00:00
}
2024-06-10 20:43:34 +00:00
2026-01-02 15:29:48 +00:00
// Database-specific methods
public function refreshFileStorages ()
{
if ( $this -> serviceDatabase ) {
$this -> fileStorages = $this -> serviceDatabase -> fileStorages () -> get ();
}
}
2026-03-11 14:04:45 +00:00
public function deleteDatabase ( $password , $selectedActions = [])
2026-01-02 15:29:48 +00:00
{
try {
$this -> authorize ( 'delete' , $this -> serviceDatabase );
if ( ! verifyPasswordConfirmation ( $password , $this )) {
2026-03-11 14:04:45 +00:00
return 'The provided password is incorrect.' ;
2026-01-02 15:29:48 +00:00
}
$this -> serviceDatabase -> delete ();
$this -> dispatch ( 'success' , 'Database deleted.' );
return redirectRoute ( $this , 'project.service.configuration' , $this -> parameters );
} catch ( \Throwable $e ) {
return handleError ( $e , $this );
}
}
public function instantSaveExclude ()
{
try {
$this -> authorize ( 'update' , $this -> serviceDatabase );
$this -> submitDatabase ();
} catch ( \Throwable $e ) {
return handleError ( $e , $this );
}
}
public function instantSaveLogDrain ()
{
try {
$this -> authorize ( 'update' , $this -> serviceDatabase );
if ( ! $this -> serviceDatabase -> service -> destination -> server -> isLogDrainEnabled ()) {
$this -> isLogDrainEnabled = false ;
$this -> dispatch ( 'error' , 'Log drain is not enabled on the server. Please enable it first.' );
return ;
}
$this -> submitDatabase ();
$this -> dispatch ( 'success' , 'You need to restart the service for the changes to take effect.' );
} catch ( \Throwable $e ) {
return handleError ( $e , $this );
}
}
public function convertToApplication ()
{
try {
$this -> authorize ( 'update' , $this -> serviceDatabase );
$service = $this -> serviceDatabase -> service ;
$serviceDatabase = $this -> serviceDatabase ;
// Check if application with same name already exists
if ( $service -> applications () -> where ( 'name' , $serviceDatabase -> name ) -> exists ()) {
throw new \Exception ( 'An application with this name already exists.' );
}
// Create new parameters removing database_uuid
$redirectParams = collect ( $this -> parameters )
-> except ( 'database_uuid' )
-> all ();
DB :: transaction ( function () use ( $service , $serviceDatabase ) {
$service -> applications () -> create ([
'name' => $serviceDatabase -> name ,
'human_name' => $serviceDatabase -> human_name ,
'description' => $serviceDatabase -> description ,
'exclude_from_status' => $serviceDatabase -> exclude_from_status ,
'is_log_drain_enabled' => $serviceDatabase -> is_log_drain_enabled ,
'image' => $serviceDatabase -> image ,
'service_id' => $service -> id ,
'is_migrated' => true ,
]);
$serviceDatabase -> delete ();
});
return redirectRoute ( $this , 'project.service.configuration' , $redirectParams );
} catch ( \Throwable $e ) {
return handleError ( $e , $this );
}
}
public function instantSave ()
{
try {
$this -> authorize ( 'update' , $this -> serviceDatabase );
if ( $this -> isPublic && ! $this -> publicPort ) {
$this -> dispatch ( 'error' , 'Public port is required.' );
$this -> isPublic = false ;
return ;
}
$this -> syncDatabaseData ( true );
if ( $this -> serviceDatabase -> is_public ) {
if ( ! str ( $this -> serviceDatabase -> status ) -> startsWith ( 'running' )) {
$this -> dispatch ( 'error' , 'Database must be started to be publicly accessible.' );
$this -> isPublic = false ;
$this -> serviceDatabase -> is_public = false ;
return ;
}
StartDatabaseProxy :: run ( $this -> serviceDatabase );
$this -> db_url_public = $this -> serviceDatabase -> getServiceDatabaseUrl ();
$this -> dispatch ( 'success' , 'Database is now publicly accessible.' );
} else {
StopDatabaseProxy :: run ( $this -> serviceDatabase );
$this -> db_url_public = null ;
$this -> dispatch ( 'success' , 'Database is no longer publicly accessible.' );
}
} catch ( \Throwable $e ) {
return handleError ( $e , $this );
}
}
public function submitDatabase ()
{
try {
$this -> authorize ( 'update' , $this -> serviceDatabase );
$this -> validate ();
$this -> syncDatabaseData ( true );
$this -> serviceDatabase -> save ();
$this -> serviceDatabase -> refresh ();
$this -> syncDatabaseData ( false );
updateCompose ( $this -> serviceDatabase );
$this -> dispatch ( 'success' , 'Database saved.' );
} catch ( \Throwable $e ) {
return handleError ( $e , $this );
} finally {
$this -> dispatch ( 'generateDockerCompose' );
}
}
// Application-specific methods
private function initializeApplicationProperties () : void
{
$this -> requiredPort = $this -> serviceApplication -> getRequiredPort ();
$this -> syncApplicationData ( false );
}
private function syncApplicationData ( bool $toModel = false ) : void
{
if ( $toModel ) {
$this -> serviceApplication -> human_name = $this -> humanName ;
$this -> serviceApplication -> description = $this -> description ;
$this -> serviceApplication -> fqdn = $this -> fqdn ;
$this -> serviceApplication -> image = $this -> image ;
$this -> serviceApplication -> exclude_from_status = $this -> excludeFromStatus ;
$this -> serviceApplication -> is_log_drain_enabled = $this -> isLogDrainEnabled ;
$this -> serviceApplication -> is_gzip_enabled = $this -> isGzipEnabled ;
$this -> serviceApplication -> is_stripprefix_enabled = $this -> isStripprefixEnabled ;
} else {
$this -> humanName = $this -> serviceApplication -> human_name ;
$this -> description = $this -> serviceApplication -> description ;
$this -> fqdn = $this -> serviceApplication -> fqdn ;
$this -> image = $this -> serviceApplication -> image ;
$this -> excludeFromStatus = data_get ( $this -> serviceApplication , 'exclude_from_status' , false );
$this -> isLogDrainEnabled = data_get ( $this -> serviceApplication , 'is_log_drain_enabled' , false );
$this -> isGzipEnabled = data_get ( $this -> serviceApplication , 'is_gzip_enabled' , true );
$this -> isStripprefixEnabled = data_get ( $this -> serviceApplication , 'is_stripprefix_enabled' , true );
}
}
public function instantSaveApplication ()
{
try {
$this -> authorize ( 'update' , $this -> serviceApplication );
$this -> submitApplication ();
} catch ( \Throwable $e ) {
return handleError ( $e , $this );
}
}
public function instantSaveApplicationSettings ()
{
try {
$this -> authorize ( 'update' , $this -> serviceApplication );
$this -> serviceApplication -> is_gzip_enabled = $this -> isGzipEnabled ;
$this -> serviceApplication -> is_stripprefix_enabled = $this -> isStripprefixEnabled ;
$this -> serviceApplication -> exclude_from_status = $this -> excludeFromStatus ;
$this -> serviceApplication -> save ();
$this -> dispatch ( 'success' , 'Settings saved.' );
} catch ( \Throwable $e ) {
return handleError ( $e , $this );
}
}
public function instantSaveApplicationAdvanced ()
{
try {
$this -> authorize ( 'update' , $this -> serviceApplication );
if ( ! $this -> serviceApplication -> service -> destination -> server -> isLogDrainEnabled ()) {
$this -> isLogDrainEnabled = false ;
$this -> dispatch ( 'error' , 'Log drain is not enabled on the server. Please enable it first.' );
return ;
}
$this -> syncApplicationData ( true );
$this -> serviceApplication -> save ();
$this -> dispatch ( 'success' , 'You need to restart the service for the changes to take effect.' );
} catch ( \Throwable $e ) {
return handleError ( $e , $this );
}
}
2026-03-11 14:04:45 +00:00
public function deleteApplication ( $password , $selectedActions = [])
2026-01-02 15:29:48 +00:00
{
try {
$this -> authorize ( 'delete' , $this -> serviceApplication );
if ( ! verifyPasswordConfirmation ( $password , $this )) {
2026-03-11 14:04:45 +00:00
return 'The provided password is incorrect.' ;
2026-01-02 15:29:48 +00:00
}
$this -> serviceApplication -> delete ();
$this -> dispatch ( 'success' , 'Application deleted.' );
return redirect () -> route ( 'project.service.configuration' , $this -> parameters );
} catch ( \Throwable $e ) {
return handleError ( $e , $this );
}
}
public function convertToDatabase ()
{
try {
$this -> authorize ( 'update' , $this -> serviceApplication );
$service = $this -> serviceApplication -> service ;
$serviceApplication = $this -> serviceApplication ;
if ( $service -> databases () -> where ( 'name' , $serviceApplication -> name ) -> exists ()) {
throw new \Exception ( 'A database with this name already exists.' );
}
$redirectParams = collect ( $this -> parameters )
-> except ( 'database_uuid' )
-> all ();
DB :: transaction ( function () use ( $service , $serviceApplication ) {
$service -> databases () -> create ([
'name' => $serviceApplication -> name ,
'human_name' => $serviceApplication -> human_name ,
'description' => $serviceApplication -> description ,
'exclude_from_status' => $serviceApplication -> exclude_from_status ,
'is_log_drain_enabled' => $serviceApplication -> is_log_drain_enabled ,
'image' => $serviceApplication -> image ,
'service_id' => $service -> id ,
'is_migrated' => true ,
]);
$serviceApplication -> delete ();
});
return redirect () -> route ( 'project.service.configuration' , $redirectParams );
} catch ( \Throwable $e ) {
return handleError ( $e , $this );
}
}
public function confirmDomainUsage ()
{
$this -> forceSaveDomains = true ;
$this -> showDomainConflictModal = false ;
$this -> submitApplication ();
}
public function confirmRemovePort ()
{
$this -> forceRemovePort = true ;
$this -> showPortWarningModal = false ;
$this -> submitApplication ();
}
public function cancelRemovePort ()
{
$this -> showPortWarningModal = false ;
$this -> syncApplicationData ( false );
}
public function submitApplication ()
{
try {
$this -> authorize ( 'update' , $this -> serviceApplication );
$this -> fqdn = str ( $this -> fqdn ) -> replaceEnd ( ',' , '' ) -> trim () -> toString ();
$this -> fqdn = str ( $this -> fqdn ) -> replaceStart ( ',' , '' ) -> trim () -> toString ();
$domains = str ( $this -> fqdn ) -> trim () -> explode ( ',' ) -> map ( function ( $domain ) {
$domain = trim ( $domain );
Url :: fromString ( $domain , [ 'http' , 'https' ]);
return str ( $domain ) -> lower ();
});
$this -> fqdn = $domains -> unique () -> implode ( ',' );
$warning = sslipDomainWarning ( $this -> fqdn );
if ( $warning ) {
$this -> dispatch ( 'warning' , __ ( 'warning.sslipdomain' ));
}
$this -> syncApplicationData ( true );
if ( ! $this -> forceSaveDomains ) {
$result = checkDomainUsage ( resource : $this -> serviceApplication );
if ( $result [ 'hasConflicts' ]) {
$this -> domainConflicts = $result [ 'conflicts' ];
$this -> showDomainConflictModal = true ;
return ;
}
} else {
$this -> forceSaveDomains = false ;
}
if ( ! $this -> forceRemovePort ) {
$requiredPort = $this -> serviceApplication -> getRequiredPort ();
if ( $requiredPort !== null ) {
$fqdns = str ( $this -> fqdn ) -> trim () -> explode ( ',' );
$missingPort = false ;
foreach ( $fqdns as $fqdn ) {
$fqdn = trim ( $fqdn );
if ( empty ( $fqdn )) {
continue ;
}
$port = ServiceApplication :: extractPortFromUrl ( $fqdn );
if ( $port === null ) {
$missingPort = true ;
break ;
}
}
if ( $missingPort ) {
$this -> requiredPort = $requiredPort ;
$this -> showPortWarningModal = true ;
return ;
}
}
} else {
$this -> forceRemovePort = false ;
}
$this -> validate ();
$this -> serviceApplication -> save ();
$this -> serviceApplication -> refresh ();
$this -> syncApplicationData ( false );
updateCompose ( $this -> serviceApplication );
if ( str ( $this -> serviceApplication -> fqdn ) -> contains ( ',' )) {
$this -> dispatch ( 'warning' , 'Some services do not support multiple domains, which can lead to problems and is NOT RECOMMENDED.<br><br>Only use multiple domains if you know what you are doing.' );
} else {
! $warning && $this -> dispatch ( 'success' , 'Service saved.' );
}
$this -> dispatch ( 'generateDockerCompose' );
} catch ( \Throwable $e ) {
$originalFqdn = $this -> serviceApplication -> getOriginal ( 'fqdn' );
if ( $originalFqdn !== $this -> serviceApplication -> fqdn ) {
$this -> serviceApplication -> fqdn = $originalFqdn ;
$this -> syncApplicationData ( false );
}
return handleError ( $e , $this );
}
}
2024-01-07 15:23:41 +00:00
public function render ()
2023-09-26 12:45:52 +00:00
{
2024-01-07 15:23:41 +00:00
return view ( 'livewire.project.service.index' );
2023-09-26 12:45:52 +00:00
}
2023-09-20 13:42:41 +00:00
}