Initial Ladill Hosting app with Gitea deploy pipeline.
Deploy Ladill Hosting / deploy (push) Failing after 17s

Shared web hosting extracted from the platform monolith, with CI deploy
to /var/www/ladill-hosting matching Bird/Domains/Email.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-06-06 16:24:20 +00:00
co-authored by Cursor
commit e251a4cf60
367 changed files with 66268 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
*.sqlite*
+45
View File
@@ -0,0 +1,45 @@
<?php
namespace Database\Factories;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
/**
* @extends Factory<User>
*/
class UserFactory extends Factory
{
/**
* The current password being used by the factory.
*/
protected static ?string $password;
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'name' => fake()->name(),
'email' => fake()->unique()->safeEmail(),
'email_verified_at' => now(),
'password' => static::$password ??= Hash::make('password'),
'remember_token' => Str::random(10),
];
}
/**
* Indicate that the model's email address should be unverified.
*/
public function unverified(): static
{
return $this->state(fn (array $attributes) => [
'email_verified_at' => null,
]);
}
}
@@ -0,0 +1,52 @@
<?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
{
// Local mirror of the platform identity, keyed by public_id (OIDC sub).
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->uuid('public_id')->unique();
$table->string('name')->nullable();
$table->string('email')->unique();
$table->string('avatar_url')->nullable();
$table->timestamp('email_verified_at')->nullable();
$table->string('password')->nullable();
$table->rememberToken();
$table->timestamps();
});
Schema::create('password_reset_tokens', function (Blueprint $table) {
$table->string('email')->primary();
$table->string('token');
$table->timestamp('created_at')->nullable();
});
Schema::create('sessions', function (Blueprint $table) {
$table->string('id')->primary();
$table->foreignId('user_id')->nullable()->index();
$table->string('ip_address', 45)->nullable();
$table->text('user_agent')->nullable();
$table->longText('payload');
$table->integer('last_activity')->index();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('users');
Schema::dropIfExists('password_reset_tokens');
Schema::dropIfExists('sessions');
}
};
@@ -0,0 +1,35 @@
<?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::create('cache', function (Blueprint $table) {
$table->string('key')->primary();
$table->mediumText('value');
$table->integer('expiration')->index();
});
Schema::create('cache_locks', function (Blueprint $table) {
$table->string('key')->primary();
$table->string('owner');
$table->integer('expiration')->index();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('cache');
Schema::dropIfExists('cache_locks');
}
};
@@ -0,0 +1,57 @@
<?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::create('jobs', function (Blueprint $table) {
$table->id();
$table->string('queue')->index();
$table->longText('payload');
$table->unsignedTinyInteger('attempts');
$table->unsignedInteger('reserved_at')->nullable();
$table->unsignedInteger('available_at');
$table->unsignedInteger('created_at');
});
Schema::create('job_batches', function (Blueprint $table) {
$table->string('id')->primary();
$table->string('name');
$table->integer('total_jobs');
$table->integer('pending_jobs');
$table->integer('failed_jobs');
$table->longText('failed_job_ids');
$table->mediumText('options')->nullable();
$table->integer('cancelled_at')->nullable();
$table->integer('created_at');
$table->integer('finished_at')->nullable();
});
Schema::create('failed_jobs', function (Blueprint $table) {
$table->id();
$table->string('uuid')->unique();
$table->text('connection');
$table->text('queue');
$table->longText('payload');
$table->longText('exception');
$table->timestamp('failed_at')->useCurrent();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('jobs');
Schema::dropIfExists('job_batches');
Schema::dropIfExists('failed_jobs');
}
};
@@ -0,0 +1,55 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
/**
* Registry of domains linked to hosting accounts (no website FK hosting app
* does not own the site builder). Must run before hosting_orders.
*/
return new class extends Migration
{
public function up(): void
{
Schema::create('domains', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
$table->unsignedBigInteger('website_id')->nullable();
$table->unsignedBigInteger('hosting_account_id')->nullable();
$table->unsignedBigInteger('email_domain_id')->nullable();
$table->string('host')->unique();
$table->string('type')->default('custom');
$table->string('source')->default('hosting');
$table->string('onboarding_mode')->default('ns_auto');
$table->string('verification_token')->nullable();
$table->timestamp('verified_at')->nullable();
$table->string('status')->default('pending');
$table->string('ssl_status')->default('pending');
$table->string('onboarding_state')->default('pending_ns');
$table->string('dns_mode')->nullable();
$table->json('ns_expected')->nullable();
$table->json('ns_observed')->nullable();
$table->timestamp('ns_checked_at')->nullable();
$table->timestamp('connected_at')->nullable();
$table->timestamp('mail_ready_at')->nullable();
$table->timestamp('active_at')->nullable();
$table->timestamp('manual_dns_verified_at')->nullable();
$table->json('verification_meta')->nullable();
$table->timestamp('ssl_provisioned_at')->nullable();
$table->timestamp('ssl_expires_at')->nullable();
$table->string('registrar')->nullable();
$table->string('registrar_order_id')->nullable();
$table->timestamps();
$table->index(['user_id', 'host']);
$table->index(['source']);
$table->index(['hosting_account_id']);
});
}
public function down(): void
{
Schema::dropIfExists('domains');
}
};
@@ -0,0 +1,37 @@
<?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::create('hosting_orders', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->foreignId('domain_id')->nullable()->constrained()->nullOnDelete();
$table->string('domain_name');
$table->string('rc_order_id')->nullable()->index();
$table->string('rc_customer_id')->nullable();
$table->string('plan_name')->nullable();
$table->string('hosting_type')->default('single_domain');
$table->string('status')->default('pending');
$table->string('ip_address')->nullable();
$table->string('cpanel_url')->nullable();
$table->string('cpanel_username')->nullable();
$table->unsignedInteger('bandwidth_mb')->nullable();
$table->unsignedInteger('disk_space_mb')->nullable();
$table->timestamp('expires_at')->nullable();
$table->timestamp('provisioned_at')->nullable();
$table->json('meta')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('hosting_orders');
}
};
@@ -0,0 +1,245 @@
<?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
{
// Hosting Nodes - Physical/virtual servers that host accounts
Schema::create('hosting_nodes', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('hostname');
$table->string('ip_address');
$table->string('ipv6_address')->nullable();
$table->enum('type', ['shared', 'vps', 'dedicated'])->default('shared');
$table->enum('provider', ['contabo', 'manual', 'legacy_rc', 'local'])->default('contabo');
$table->string('provider_instance_id')->nullable();
$table->string('region')->nullable();
$table->string('datacenter')->nullable();
$table->unsignedInteger('cpu_cores')->nullable();
$table->unsignedInteger('ram_mb')->nullable();
$table->unsignedInteger('disk_gb')->nullable();
$table->unsignedInteger('bandwidth_tb')->nullable();
$table->unsignedInteger('max_accounts')->nullable();
$table->unsignedInteger('current_accounts')->default(0);
$table->enum('status', ['provisioning', 'active', 'maintenance', 'full', 'decommissioned'])->default('provisioning');
$table->json('features')->nullable();
$table->json('installed_software')->nullable();
$table->string('ssh_port')->default('22');
$table->text('ssh_private_key')->nullable();
$table->string('control_panel')->nullable();
$table->timestamp('last_health_check_at')->nullable();
$table->json('health_status')->nullable();
$table->timestamps();
$table->softDeletes();
$table->index(['type', 'status']);
$table->index('provider_instance_id');
});
// Hosting Plans - Product offerings
Schema::create('hosting_plans', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('slug')->unique();
$table->text('description')->nullable();
$table->enum('type', ['shared', 'vps', 'dedicated', 'email'])->default('shared');
$table->decimal('price_monthly', 10, 2);
$table->decimal('price_yearly', 10, 2)->nullable();
$table->string('currency', 3)->default('USD');
$table->unsignedInteger('disk_gb');
$table->unsignedInteger('bandwidth_gb')->nullable();
$table->unsignedInteger('cpu_cores')->nullable();
$table->unsignedInteger('ram_mb')->nullable();
$table->unsignedInteger('max_domains')->nullable();
$table->unsignedInteger('max_databases')->nullable();
$table->unsignedInteger('max_email_accounts')->nullable();
$table->unsignedInteger('max_ftp_accounts')->nullable();
$table->boolean('ssl_included')->default(true);
$table->boolean('backups_included')->default(true);
$table->unsignedInteger('backup_retention_days')->default(7);
$table->json('features')->nullable();
$table->json('php_versions')->nullable();
$table->boolean('is_active')->default(true);
$table->boolean('is_featured')->default(false);
$table->unsignedInteger('sort_order')->default(0);
$table->string('contabo_product_id')->nullable();
$table->timestamps();
$table->softDeletes();
$table->index(['type', 'is_active']);
});
// Hosting Accounts - Customer hosting instances
Schema::create('hosting_accounts', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->foreignId('hosting_plan_id')->constrained()->restrictOnDelete();
$table->foreignId('hosting_node_id')->nullable()->constrained()->nullOnDelete();
$table->string('username', 32)->unique();
$table->string('primary_domain')->nullable();
$table->enum('type', ['shared', 'vps', 'dedicated'])->default('shared');
$table->enum('status', ['pending', 'provisioning', 'active', 'suspended', 'terminated', 'failed'])->default('pending');
$table->string('provider_account_id')->nullable();
$table->string('home_directory')->nullable();
$table->unsignedBigInteger('disk_used_bytes')->default(0);
$table->unsignedBigInteger('bandwidth_used_bytes')->default(0);
$table->timestamp('bandwidth_reset_at')->nullable();
$table->string('php_version')->default('8.2');
$table->json('features_enabled')->nullable();
$table->json('resource_limits')->nullable();
$table->text('suspension_reason')->nullable();
$table->timestamp('suspended_at')->nullable();
$table->timestamp('provisioned_at')->nullable();
$table->timestamp('expires_at')->nullable();
$table->json('metadata')->nullable();
$table->timestamps();
$table->softDeletes();
$table->index(['user_id', 'status']);
$table->index('hosting_node_id');
$table->index('primary_domain');
});
// Hosted Sites - Individual websites on hosting accounts
Schema::create('hosted_sites', function (Blueprint $table) {
$table->id();
$table->foreignId('hosting_account_id')->constrained()->cascadeOnDelete();
$table->foreignId('domain_id')->nullable()->constrained()->nullOnDelete();
$table->string('domain');
$table->string('document_root');
$table->enum('type', ['primary', 'addon', 'subdomain', 'parked'])->default('addon');
$table->enum('status', ['pending', 'active', 'suspended', 'disabled'])->default('pending');
$table->string('php_version')->nullable();
$table->boolean('ssl_enabled')->default(false);
$table->timestamp('ssl_expires_at')->nullable();
$table->string('installed_app')->nullable();
$table->string('installed_app_version')->nullable();
$table->json('app_config')->nullable();
$table->timestamps();
$table->softDeletes();
$table->unique(['hosting_account_id', 'domain']);
$table->index('domain');
});
// Hosted Databases
Schema::create('hosted_databases', function (Blueprint $table) {
$table->id();
$table->foreignId('hosting_account_id')->constrained()->cascadeOnDelete();
$table->foreignId('hosted_site_id')->nullable()->constrained()->nullOnDelete();
$table->string('name');
$table->string('username');
$table->string('password_encrypted')->nullable();
$table->enum('type', ['mysql', 'mariadb', 'postgresql'])->default('mysql');
$table->unsignedBigInteger('size_bytes')->default(0);
$table->enum('status', ['active', 'suspended', 'deleted'])->default('active');
$table->timestamps();
$table->softDeletes();
$table->unique(['hosting_account_id', 'name']);
});
// Provisioning Jobs - Track async provisioning operations
Schema::create('provisioning_jobs', function (Blueprint $table) {
$table->id();
$table->uuid('uuid')->unique();
$table->string('type');
$table->morphs('provisionable');
$table->enum('provider', ['contabo', 'shared_node', 'mail', 'dns', 'ssl']);
$table->enum('status', ['pending', 'running', 'completed', 'failed', 'cancelled'])->default('pending');
$table->json('payload')->nullable();
$table->json('result')->nullable();
$table->text('error_message')->nullable();
$table->unsignedInteger('attempts')->default(0);
$table->unsignedInteger('max_attempts')->default(3);
$table->timestamp('started_at')->nullable();
$table->timestamp('completed_at')->nullable();
$table->timestamp('next_retry_at')->nullable();
$table->timestamps();
$table->index(['status', 'next_retry_at']);
});
// VPS Instances - For managed VPS products
Schema::create('vps_instances', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->foreignId('hosting_plan_id')->constrained()->restrictOnDelete();
$table->string('name');
$table->string('hostname');
$table->string('provider')->default('contabo');
$table->string('provider_instance_id')->nullable();
$table->string('ip_address')->nullable();
$table->string('ipv6_address')->nullable();
$table->string('region')->nullable();
$table->string('datacenter')->nullable();
$table->string('image')->nullable();
$table->unsignedInteger('cpu_cores')->nullable();
$table->unsignedInteger('ram_mb')->nullable();
$table->unsignedInteger('disk_gb')->nullable();
$table->enum('status', ['pending', 'provisioning', 'running', 'stopped', 'suspended', 'terminated', 'failed'])->default('pending');
$table->enum('power_status', ['on', 'off', 'unknown'])->default('unknown');
$table->string('root_password_encrypted')->nullable();
$table->text('ssh_public_key')->nullable();
$table->json('tags')->nullable();
$table->json('metadata')->nullable();
$table->timestamp('provisioned_at')->nullable();
$table->timestamp('expires_at')->nullable();
$table->timestamps();
$table->softDeletes();
$table->index(['user_id', 'status']);
$table->index('provider_instance_id');
});
// VPS Snapshots
Schema::create('vps_snapshots', function (Blueprint $table) {
$table->id();
$table->foreignId('vps_instance_id')->constrained()->cascadeOnDelete();
$table->string('name');
$table->string('provider_snapshot_id')->nullable();
$table->unsignedBigInteger('size_bytes')->nullable();
$table->enum('status', ['creating', 'available', 'deleting', 'failed'])->default('creating');
$table->text('description')->nullable();
$table->timestamps();
$table->index('provider_snapshot_id');
});
// App Installations - Track installed applications
Schema::create('app_installations', function (Blueprint $table) {
$table->id();
$table->foreignId('hosted_site_id')->constrained()->cascadeOnDelete();
$table->string('app_type');
$table->string('app_version');
$table->string('admin_path')->nullable();
$table->string('admin_username')->nullable();
$table->string('admin_email')->nullable();
$table->json('config')->nullable();
$table->enum('status', ['installing', 'active', 'updating', 'failed', 'removed'])->default('installing');
$table->timestamp('installed_at')->nullable();
$table->timestamp('last_updated_at')->nullable();
$table->timestamps();
$table->index(['app_type', 'status']);
});
}
public function down(): void
{
Schema::dropIfExists('app_installations');
Schema::dropIfExists('vps_snapshots');
Schema::dropIfExists('vps_instances');
Schema::dropIfExists('provisioning_jobs');
Schema::dropIfExists('hosted_databases');
Schema::dropIfExists('hosted_sites');
Schema::dropIfExists('hosting_accounts');
Schema::dropIfExists('hosting_plans');
Schema::dropIfExists('hosting_nodes');
}
};
@@ -0,0 +1,174 @@
<?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
{
// Hosting Products - Admin-managed pricing for all hosting types
Schema::create('hosting_products', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('slug')->unique();
$table->text('description')->nullable();
$table->enum('category', ['shared', 'vps', 'dedicated', 'email']);
$table->enum('type', ['single_domain', 'multi_domain', 'wordpress', 'vps', 'dedicated', 'email_standalone']);
$table->decimal('price_monthly', 10, 2);
$table->decimal('price_quarterly', 10, 2)->nullable();
$table->decimal('price_yearly', 10, 2)->nullable();
$table->decimal('price_biennial', 10, 2)->nullable();
$table->decimal('setup_fee', 10, 2)->default(0);
$table->string('currency', 3)->default('USD');
// Resource limits
$table->integer('disk_gb')->nullable();
$table->integer('bandwidth_gb')->nullable();
$table->integer('cpu_cores')->nullable();
$table->integer('ram_mb')->nullable();
$table->integer('max_domains')->default(1);
$table->integer('max_databases')->nullable();
$table->integer('max_email_accounts')->nullable();
$table->integer('max_ftp_accounts')->nullable();
// Features
$table->boolean('ssl_included')->default(true);
$table->boolean('backups_included')->default(false);
$table->integer('backup_retention_days')->nullable();
$table->json('features')->nullable();
$table->json('php_versions')->nullable();
// Contabo mapping for VPS/Dedicated
$table->string('contabo_product_id')->nullable();
$table->string('contabo_region')->nullable();
// Display
$table->boolean('is_active')->default(true);
$table->boolean('is_featured')->default(false);
$table->boolean('is_visible')->default(true);
$table->integer('sort_order')->default(0);
$table->timestamps();
$table->softDeletes();
$table->index(['category', 'is_active']);
$table->index(['type', 'is_active']);
});
// Customer Hosting Orders - New unified order system
Schema::create('customer_hosting_orders', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->foreignId('hosting_product_id')->constrained()->onDelete('restrict');
$table->foreignId('domain_id')->nullable()->constrained()->onDelete('set null');
$table->string('domain_name');
$table->enum('billing_cycle', ['monthly', 'quarterly', 'yearly', 'biennial'])->default('yearly');
$table->decimal('amount_paid', 10, 2);
$table->string('currency', 3)->default('USD');
$table->string('payment_reference')->nullable();
// Order status with approval workflow
$table->enum('status', [
'pending_payment',
'pending_approval',
'approved',
'provisioning',
'active',
'suspended',
'cancelled',
'expired',
'failed'
])->default('pending_payment');
// Approval tracking
$table->foreignId('approved_by')->nullable()->constrained('users')->onDelete('set null');
$table->timestamp('approved_at')->nullable();
$table->text('approval_notes')->nullable();
// Provisioning details
$table->foreignId('hosting_node_id')->nullable()->constrained()->onDelete('set null');
$table->foreignId('hosting_account_id')->nullable()->constrained()->onDelete('set null');
$table->foreignId('vps_instance_id')->nullable()->constrained()->onDelete('set null');
$table->string('server_username')->nullable();
$table->string('server_ip')->nullable();
$table->string('control_panel_url')->nullable();
$table->timestamp('provisioned_at')->nullable();
$table->timestamp('expires_at')->nullable();
$table->timestamp('suspended_at')->nullable();
$table->timestamp('cancelled_at')->nullable();
$table->json('meta')->nullable();
$table->timestamps();
$table->softDeletes();
$table->index(['user_id', 'status']);
$table->index(['status', 'created_at']);
$table->index('domain_name');
});
// Node Capacity Alerts - Track when new shared hosting nodes are needed
Schema::create('node_capacity_alerts', function (Blueprint $table) {
$table->id();
$table->foreignId('hosting_node_id')->constrained()->onDelete('cascade');
$table->enum('alert_type', ['capacity_warning', 'capacity_critical', 'resource_high', 'needs_expansion']);
$table->integer('current_accounts');
$table->integer('max_accounts');
$table->decimal('capacity_percent', 5, 2);
$table->json('resource_usage')->nullable();
$table->text('message');
$table->boolean('is_resolved')->default(false);
$table->foreignId('resolved_by')->nullable()->constrained('users')->onDelete('set null');
$table->timestamp('resolved_at')->nullable();
$table->text('resolution_notes')->nullable();
$table->timestamps();
$table->index(['is_resolved', 'created_at']);
$table->index(['hosting_node_id', 'is_resolved']);
});
// Provisioning Queue - Track all provisioning jobs
Schema::create('provisioning_queue', function (Blueprint $table) {
$table->id();
$table->foreignId('customer_hosting_order_id')->constrained()->onDelete('cascade');
$table->enum('status', ['queued', 'processing', 'completed', 'failed', 'cancelled'])->default('queued');
$table->enum('provider', ['shared_node', 'contabo_vps', 'contabo_dedicated']);
$table->integer('attempts')->default(0);
$table->integer('max_attempts')->default(3);
$table->timestamp('started_at')->nullable();
$table->timestamp('completed_at')->nullable();
$table->text('error_message')->nullable();
$table->json('provisioning_log')->nullable();
$table->timestamps();
$table->index(['status', 'created_at']);
});
// Add approval fields to existing hosting_orders table for legacy RC orders
if (Schema::hasTable('hosting_orders')) {
Schema::table('hosting_orders', function (Blueprint $table) {
if (!Schema::hasColumn('hosting_orders', 'is_legacy_rc')) {
$table->boolean('is_legacy_rc')->default(true)->after('meta');
}
});
}
}
public function down(): void
{
Schema::dropIfExists('provisioning_queue');
Schema::dropIfExists('node_capacity_alerts');
Schema::dropIfExists('customer_hosting_orders');
Schema::dropIfExists('hosting_products');
if (Schema::hasTable('hosting_orders') && Schema::hasColumn('hosting_orders', 'is_legacy_rc')) {
Schema::table('hosting_orders', function (Blueprint $table) {
$table->dropColumn('is_legacy_rc');
});
}
}
};
@@ -0,0 +1,29 @@
<?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('hosting_accounts', function (Blueprint $table) {
$table->foreignId('hosting_product_id')->nullable()->after('user_id')->constrained()->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('hosting_accounts', function (Blueprint $table) {
$table->dropForeign(['hosting_product_id']);
$table->dropColumn('hosting_product_id');
});
}
};
@@ -0,0 +1,29 @@
<?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('hosting_accounts', function (Blueprint $table) {
$table->dropForeign(['hosting_plan_id']);
$table->dropColumn('hosting_plan_id');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('hosting_accounts', function (Blueprint $table) {
$table->foreignId('hosting_plan_id')->nullable()->after('hosting_product_id')->constrained()->onDelete('cascade');
});
}
};
@@ -0,0 +1,35 @@
<?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('app_installations', function (Blueprint $table) {
$table->text('admin_password_encrypted')->nullable()->after('admin_email');
$table->string('database_name')->nullable()->after('admin_password_encrypted');
$table->string('database_username')->nullable()->after('database_name');
$table->text('database_password_encrypted')->nullable()->after('database_username');
$table->string('install_url')->nullable()->after('database_password_encrypted');
});
}
public function down(): void
{
Schema::table('app_installations', function (Blueprint $table) {
$table->dropColumn([
'admin_password_encrypted',
'database_name',
'database_username',
'database_password_encrypted',
'install_url',
]);
});
}
};
@@ -0,0 +1,26 @@
<?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('app_installations', function (Blueprint $table) {
$table->unsignedTinyInteger('progress')->default(0)->after('status');
$table->string('progress_message')->nullable()->after('progress');
$table->string('current_step')->nullable()->after('progress_message');
$table->unsignedTinyInteger('total_steps')->default(5)->after('current_step');
$table->text('error_message')->nullable()->after('total_steps');
});
}
public function down(): void
{
Schema::table('app_installations', function (Blueprint $table) {
$table->dropColumn(['progress', 'progress_message', 'current_step', 'total_steps', 'error_message']);
});
}
};
@@ -0,0 +1,27 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('customer_hosting_orders', function (Blueprint $table) {
$table->string('billing_cycle', 20)->default('yearly')->change();
});
}
public function down(): void
{
DB::table('customer_hosting_orders')
->where('billing_cycle', 'semiannual')
->update(['billing_cycle' => 'quarterly']);
Schema::table('customer_hosting_orders', function (Blueprint $table) {
$table->enum('billing_cycle', ['monthly', 'quarterly', 'yearly', 'biennial'])->default('yearly')->change();
});
}
};
@@ -0,0 +1,38 @@
<?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::create('server_agents', function (Blueprint $table) {
$table->id();
$table->foreignId('customer_hosting_order_id')->constrained()->cascadeOnDelete();
$table->uuid('uuid')->unique();
$table->string('status', 40)->default('pending_registration');
$table->string('hostname')->nullable();
$table->string('agent_version', 120)->nullable();
$table->string('platform', 120)->nullable();
$table->json('capabilities')->nullable();
$table->json('metadata')->nullable();
$table->string('bootstrap_token_hash', 64)->nullable();
$table->string('access_token_hash', 64)->nullable();
$table->timestamp('access_token_issued_at')->nullable();
$table->timestamp('registered_at')->nullable();
$table->timestamp('last_heartbeat_at')->nullable();
$table->string('last_seen_ip', 64)->nullable();
$table->timestamps();
$table->unique('customer_hosting_order_id');
$table->index(['status', 'last_heartbeat_at']);
});
}
public function down(): void
{
Schema::dropIfExists('server_agents');
}
};
@@ -0,0 +1,39 @@
<?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::create('server_tasks', function (Blueprint $table) {
$table->id();
$table->foreignId('customer_hosting_order_id')->constrained()->cascadeOnDelete();
$table->foreignId('server_agent_id')->nullable()->constrained()->nullOnDelete();
$table->string('type', 80);
$table->string('status', 40)->default('queued');
$table->json('payload');
$table->json('result')->nullable();
$table->unsignedTinyInteger('progress')->default(0);
$table->unsignedInteger('attempt_count')->default(0);
$table->timestamp('queued_at')->nullable();
$table->timestamp('started_at')->nullable();
$table->timestamp('completed_at')->nullable();
$table->timestamp('failed_at')->nullable();
$table->timestamp('last_heartbeat_at')->nullable();
$table->text('error_message')->nullable();
$table->timestamps();
$table->index(['customer_hosting_order_id', 'status']);
$table->index(['server_agent_id', 'status']);
$table->index(['type', 'status']);
});
}
public function down(): void
{
Schema::dropIfExists('server_tasks');
}
};
@@ -0,0 +1,30 @@
<?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('customer_hosting_orders', function (Blueprint $table) {
$table->string('guest_email')->nullable()->after('user_id');
$table->index('guest_email');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('customer_hosting_orders', function (Blueprint $table) {
$table->dropIndex(['guest_email']);
$table->dropColumn('guest_email');
});
}
};
@@ -0,0 +1,25 @@
<?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('customer_hosting_orders', function (Blueprint $table) {
$table->foreignId('user_id')->nullable()->change();
});
}
public function down(): void
{
Schema::table('customer_hosting_orders', function (Blueprint $table) {
$table->foreignId('user_id')->nullable(false)->change();
});
}
};
@@ -0,0 +1,151 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
public function up(): void
{
$this->upsertStarterPlan();
$this->reorderExistingSharedPlans();
$this->updateProPlan();
}
public function down(): void
{
DB::table('hosting_products')
->where('slug', 'starter-plan')
->delete();
DB::table('hosting_products')
->where('slug', 'basic-plan')
->update([
'sort_order' => 1,
'updated_at' => now(),
]);
DB::table('hosting_products')
->where('slug', 'plus-plan')
->update([
'sort_order' => 2,
'updated_at' => now(),
]);
DB::table('hosting_products')
->where('slug', 'growth-plan')
->update([
'sort_order' => 3,
'updated_at' => now(),
]);
DB::table('hosting_products')
->where('slug', 'pro-plan')
->update([
'description' => 'Unlimited websites with unmetered SSD storage and bandwidth, 25 free email accounts, free SSL + CDN, LiteSpeed servers, AI tools + Site builders, Softaculous installer, daily backups, SSH access, unlimited FTP users, and unlimited MySQL databases.',
'price_monthly' => 75.00,
'price_yearly' => 900.00,
'disk_gb' => null,
'sort_order' => 4,
'updated_at' => now(),
]);
}
private function upsertStarterPlan(): void
{
$existing = DB::table('hosting_products')->where('slug', 'starter-plan')->first();
$payload = [
'name' => 'Starter Plan',
'description' => 'Entry-level single-domain hosting with 5 GB SSD storage, unmetered bandwidth, 1 free email account, free SSL + CDN, LiteSpeed servers, AI Website Builder, Softaculous installer, weekly backups, 2 FTP users, and 2 MySQL databases.',
'category' => 'shared',
'type' => 'single_domain',
'price_monthly' => 1.00,
'price_quarterly' => null,
'price_yearly' => 12.00,
'price_biennial' => null,
'setup_fee' => 0.00,
'currency' => 'GHS',
'disk_gb' => 5,
'bandwidth_gb' => null,
'cpu_cores' => null,
'ram_mb' => null,
'max_domains' => 1,
'max_databases' => 2,
'max_email_accounts' => 1,
'max_ftp_accounts' => 2,
'ssl_included' => true,
'backups_included' => true,
'backup_retention_days' => 7,
'features' => json_encode([
'LiteSpeed servers',
'AI Website Builder',
'Softaculous installer',
'Free SSL',
'Free CDN',
'Weekly backups',
], JSON_THROW_ON_ERROR),
'php_versions' => null,
'contabo_product_id' => null,
'contabo_region' => null,
'is_active' => true,
'is_featured' => false,
'is_visible' => true,
'sort_order' => 1,
'deleted_at' => null,
'updated_at' => now(),
];
if ($existing) {
DB::table('hosting_products')
->where('slug', 'starter-plan')
->update($payload);
return;
}
DB::table('hosting_products')->insert($payload + [
'slug' => 'starter-plan',
'created_at' => now(),
]);
}
private function reorderExistingSharedPlans(): void
{
DB::table('hosting_products')
->where('slug', 'basic-plan')
->update([
'sort_order' => 2,
'updated_at' => now(),
]);
DB::table('hosting_products')
->where('slug', 'plus-plan')
->update([
'sort_order' => 3,
'updated_at' => now(),
]);
DB::table('hosting_products')
->where('slug', 'growth-plan')
->update([
'sort_order' => 4,
'updated_at' => now(),
]);
}
private function updateProPlan(): void
{
DB::table('hosting_products')
->where('slug', 'pro-plan')
->update([
'description' => 'Unlimited websites with 150 GB SSD storage and unmetered bandwidth, 25 free email accounts, free SSL + CDN, LiteSpeed servers, AI tools + Site builders, Softaculous installer, daily backups, SSH access, unlimited FTP users, and unlimited MySQL databases.',
'price_monthly' => 175.00,
'price_yearly' => 2100.00,
'disk_gb' => 150,
'sort_order' => 5,
'deleted_at' => null,
'updated_at' => now(),
]);
}
};
@@ -0,0 +1,239 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
private const BYTES_PER_GB = 1073741824;
private const HOSTING_NODES_SEGMENT_STATUS_INDEX = 'hosting_nodes_segment_status_index';
public function up(): void
{
$this->addColumnIfMissing('hosting_nodes', 'segment', function (Blueprint $table): void {
$table->string('segment')->default('general')->after('type');
});
$this->addColumnIfMissing('hosting_nodes', 'oversell_ratio', function (Blueprint $table): void {
$table->decimal('oversell_ratio', 5, 2)->default(3)->after('disk_gb');
});
$this->addColumnIfMissing('hosting_nodes', 'allocated_disk_gb', function (Blueprint $table): void {
$table->unsignedInteger('allocated_disk_gb')->default(0)->after('oversell_ratio');
});
$this->addColumnIfMissing('hosting_nodes', 'used_disk_gb', function (Blueprint $table): void {
$table->unsignedInteger('used_disk_gb')->default(0)->after('allocated_disk_gb');
});
$this->addColumnIfMissing('hosting_nodes', 'current_load_percent', function (Blueprint $table): void {
$table->decimal('current_load_percent', 5, 2)->default(0)->after('current_accounts');
});
$this->addIndexIfMissing('hosting_nodes', ['segment', 'status'], self::HOSTING_NODES_SEGMENT_STATUS_INDEX);
$this->addColumnIfMissing('hosting_accounts', 'allocated_disk_gb', function (Blueprint $table): void {
$table->unsignedInteger('allocated_disk_gb')->default(0)->after('home_directory');
});
$this->addColumnIfMissing('hosting_accounts', 'inode_count', function (Blueprint $table): void {
$table->unsignedInteger('inode_count')->default(0)->after('disk_used_bytes');
});
$this->addColumnIfMissing('hosting_accounts', 'cpu_usage_percent', function (Blueprint $table): void {
$table->decimal('cpu_usage_percent', 8, 2)->nullable()->after('inode_count');
});
$this->addColumnIfMissing('hosting_accounts', 'last_usage_sync_at', function (Blueprint $table): void {
$table->timestamp('last_usage_sync_at')->nullable()->after('cpu_usage_percent');
});
$this->backfillAccountAllocations();
$this->backfillNodeCapacities();
}
public function down(): void
{
$this->dropColumnsIfPresent('hosting_accounts', [
'allocated_disk_gb',
'inode_count',
'cpu_usage_percent',
'last_usage_sync_at',
]);
$this->dropIndexIfPresent('hosting_nodes', self::HOSTING_NODES_SEGMENT_STATUS_INDEX);
$this->dropColumnsIfPresent('hosting_nodes', [
'segment',
'oversell_ratio',
'allocated_disk_gb',
'used_disk_gb',
'current_load_percent',
]);
}
private function backfillAccountAllocations(): void
{
if (! Schema::hasTable('hosting_accounts') || ! Schema::hasColumn('hosting_accounts', 'allocated_disk_gb')) {
return;
}
$productDiskById = Schema::hasTable('hosting_products')
? DB::table('hosting_products')->pluck('disk_gb', 'id')
: collect();
$planDiskById = Schema::hasTable('hosting_plans')
? DB::table('hosting_plans')->pluck('disk_gb', 'id')
: collect();
$select = ['id', 'disk_used_bytes'];
if (Schema::hasColumn('hosting_accounts', 'hosting_product_id')) {
$select[] = 'hosting_product_id';
}
if (Schema::hasColumn('hosting_accounts', 'hosting_plan_id')) {
$select[] = 'hosting_plan_id';
}
DB::table('hosting_accounts')
->select($select)
->orderBy('id')
->chunkById(100, function ($accounts) use ($productDiskById, $planDiskById): void {
$hasLastUsageSyncAt = Schema::hasColumn('hosting_accounts', 'last_usage_sync_at');
foreach ($accounts as $account) {
$allocated = (int) ($productDiskById[$account->hosting_product_id] ?? 0);
if ($allocated <= 0) {
$allocated = (int) ($planDiskById[$account->hosting_plan_id] ?? 0);
}
$updates = [
'allocated_disk_gb' => $allocated,
];
if ($hasLastUsageSyncAt) {
$updates['last_usage_sync_at'] = now();
}
DB::table('hosting_accounts')
->where('id', $account->id)
->update($updates);
}
});
}
private function backfillNodeCapacities(): void
{
if (! Schema::hasTable('hosting_nodes') || ! Schema::hasTable('hosting_accounts')) {
return;
}
DB::table('hosting_nodes')
->select(['id', 'disk_gb', 'max_accounts', 'status'])
->orderBy('id')
->chunkById(100, function ($nodes): void {
foreach ($nodes as $node) {
$aggregate = DB::table('hosting_accounts')
->where('hosting_node_id', $node->id)
->selectRaw('COUNT(*) as account_count')
->selectRaw('COALESCE(SUM(allocated_disk_gb), 0) as allocated_disk_gb')
->selectRaw('COALESCE(SUM(disk_used_bytes), 0) as used_disk_bytes')
->first();
$currentAccounts = (int) ($aggregate->account_count ?? 0);
$allocatedDiskGb = (int) ($aggregate->allocated_disk_gb ?? 0);
$usedDiskGb = (int) ceil(((int) ($aggregate->used_disk_bytes ?? 0)) / self::BYTES_PER_GB);
$logicalCapacity = $node->disk_gb ? ((float) $node->disk_gb * 3) : 0;
$accountPercent = $node->max_accounts ? ($currentAccounts / $node->max_accounts) * 100 : 0;
$logicalPercent = $logicalCapacity > 0 ? ($allocatedDiskGb / $logicalCapacity) * 100 : 0;
$physicalPercent = $node->disk_gb ? ($usedDiskGb / $node->disk_gb) * 100 : 0;
$loadPercent = round(max($accountPercent, $logicalPercent, $physicalPercent), 2);
DB::table('hosting_nodes')
->where('id', $node->id)
->update([
'allocated_disk_gb' => $allocatedDiskGb,
'used_disk_gb' => $usedDiskGb,
'current_accounts' => $currentAccounts,
'current_load_percent' => $loadPercent,
'status' => in_array($node->status, ['active', 'full'], true)
? (($logicalCapacity > 0 && $allocatedDiskGb >= $logicalCapacity) || ($node->max_accounts && $currentAccounts >= $node->max_accounts) ? 'full' : 'active')
: $node->status,
]);
}
});
}
private function addColumnIfMissing(string $table, string $column, callable $callback): void
{
if (! Schema::hasTable($table) || Schema::hasColumn($table, $column)) {
return;
}
Schema::table($table, $callback);
}
private function addIndexIfMissing(string $table, array $columns, string $indexName): void
{
if (! Schema::hasTable($table) || $this->hasIndex($table, $indexName)) {
return;
}
Schema::table($table, function (Blueprint $table) use ($columns, $indexName): void {
$table->index($columns, $indexName);
});
}
private function dropColumnsIfPresent(string $table, array $columns): void
{
if (! Schema::hasTable($table)) {
return;
}
$presentColumns = array_values(array_filter($columns, fn (string $column): bool => Schema::hasColumn($table, $column)));
if ($presentColumns === []) {
return;
}
Schema::table($table, function (Blueprint $table) use ($presentColumns): void {
$table->dropColumn($presentColumns);
});
}
private function dropIndexIfPresent(string $table, string $indexName): void
{
if (! Schema::hasTable($table) || ! $this->hasIndex($table, $indexName)) {
return;
}
Schema::table($table, function (Blueprint $table) use ($indexName): void {
$table->dropIndex($indexName);
});
}
private function hasIndex(string $table, string $indexName): bool
{
if (! Schema::hasTable($table)) {
return false;
}
$driver = DB::getDriverName();
if ($driver === 'sqlite') {
$rows = DB::select(sprintf("PRAGMA index_list('%s')", str_replace("'", "''", $table)));
foreach ($rows as $row) {
if (($row->name ?? null) === $indexName) {
return true;
}
}
return false;
}
if ($driver === 'mysql') {
return DB::table('information_schema.statistics')
->where('table_schema', DB::getDatabaseName())
->where('table_name', $table)
->where('index_name', $indexName)
->exists();
}
return false;
}
};
@@ -0,0 +1,244 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
private const HOSTING_ACCOUNTS_RESOURCE_STATUS_INDEX = 'hosting_accounts_resource_status_status_index';
private const HOSTING_ACCOUNTS_FLAGGED_WARNING_INDEX = 'hosting_accounts_is_flagged_warning_count_index';
public function up(): void
{
$this->addColumnIfMissing('hosting_accounts', 'memory_used_mb', function (Blueprint $table): void {
$table->unsignedInteger('memory_used_mb')->default(0)->after('cpu_usage_percent');
});
$this->addColumnIfMissing('hosting_accounts', 'process_count', function (Blueprint $table): void {
$table->unsignedInteger('process_count')->default(0)->after('memory_used_mb');
});
$this->addColumnIfMissing('hosting_accounts', 'io_usage_mb', function (Blueprint $table): void {
$table->decimal('io_usage_mb', 8, 2)->nullable()->after('process_count');
});
$this->addColumnIfMissing('hosting_accounts', 'cpu_limit_percent', function (Blueprint $table): void {
$table->unsignedInteger('cpu_limit_percent')->nullable()->after('php_version');
});
$this->addColumnIfMissing('hosting_accounts', 'memory_limit_mb', function (Blueprint $table): void {
$table->unsignedInteger('memory_limit_mb')->nullable()->after('cpu_limit_percent');
});
$this->addColumnIfMissing('hosting_accounts', 'process_limit', function (Blueprint $table): void {
$table->unsignedInteger('process_limit')->nullable()->after('memory_limit_mb');
});
$this->addColumnIfMissing('hosting_accounts', 'io_limit_mb', function (Blueprint $table): void {
$table->unsignedInteger('io_limit_mb')->nullable()->after('process_limit');
});
$this->addColumnIfMissing('hosting_accounts', 'inode_limit', function (Blueprint $table): void {
$table->unsignedInteger('inode_limit')->nullable()->after('io_limit_mb');
});
$this->addColumnIfMissing('hosting_accounts', 'resource_status', function (Blueprint $table): void {
$table->string('resource_status')->default('active')->after('inode_limit');
});
$this->addColumnIfMissing('hosting_accounts', 'uploads_restricted', function (Blueprint $table): void {
$table->boolean('uploads_restricted')->default(false)->after('resource_status');
});
$this->addColumnIfMissing('hosting_accounts', 'is_flagged', function (Blueprint $table): void {
$table->boolean('is_flagged')->default(false)->after('uploads_restricted');
});
$this->addColumnIfMissing('hosting_accounts', 'warning_count', function (Blueprint $table): void {
$table->unsignedInteger('warning_count')->default(0)->after('is_flagged');
});
$this->addColumnIfMissing('hosting_accounts', 'cpu_breach_streak', function (Blueprint $table): void {
$table->unsignedInteger('cpu_breach_streak')->default(0)->after('warning_count');
});
$this->addColumnIfMissing('hosting_accounts', 'memory_breach_streak', function (Blueprint $table): void {
$table->unsignedInteger('memory_breach_streak')->default(0)->after('cpu_breach_streak');
});
$this->addColumnIfMissing('hosting_accounts', 'process_breach_streak', function (Blueprint $table): void {
$table->unsignedInteger('process_breach_streak')->default(0)->after('memory_breach_streak');
});
$this->addColumnIfMissing('hosting_accounts', 'io_breach_streak', function (Blueprint $table): void {
$table->unsignedInteger('io_breach_streak')->default(0)->after('process_breach_streak');
});
$this->addColumnIfMissing('hosting_accounts', 'inode_breach_streak', function (Blueprint $table): void {
$table->unsignedInteger('inode_breach_streak')->default(0)->after('io_breach_streak');
});
$this->addColumnIfMissing('hosting_accounts', 'last_warning_at', function (Blueprint $table): void {
$table->timestamp('last_warning_at')->nullable()->after('inode_breach_streak');
});
$this->addColumnIfMissing('hosting_accounts', 'last_resource_breach_at', function (Blueprint $table): void {
$table->timestamp('last_resource_breach_at')->nullable()->after('last_warning_at');
});
$this->addColumnIfMissing('hosting_accounts', 'throttled_at', function (Blueprint $table): void {
$table->timestamp('throttled_at')->nullable()->after('last_resource_breach_at');
});
$this->addIndexIfMissing('hosting_accounts', ['resource_status', 'status'], self::HOSTING_ACCOUNTS_RESOURCE_STATUS_INDEX);
$this->addIndexIfMissing('hosting_accounts', ['is_flagged', 'warning_count'], self::HOSTING_ACCOUNTS_FLAGGED_WARNING_INDEX);
if (! Schema::hasTable('hosting_account_alerts')) {
Schema::create('hosting_account_alerts', function (Blueprint $table) {
$table->id();
$table->foreignId('hosting_account_id')->constrained()->cascadeOnDelete();
$table->string('alert_type');
$table->string('severity')->default('warning');
$table->text('message');
$table->json('resource_usage')->nullable();
$table->boolean('is_resolved')->default(false);
$table->timestamp('resolved_at')->nullable();
$table->timestamps();
$table->index(['hosting_account_id', 'is_resolved']);
$table->index(['alert_type', 'is_resolved']);
});
}
$this->backfillResourcePolicyDefaults();
}
public function down(): void
{
Schema::dropIfExists('hosting_account_alerts');
$this->dropIndexIfPresent('hosting_accounts', self::HOSTING_ACCOUNTS_RESOURCE_STATUS_INDEX);
$this->dropIndexIfPresent('hosting_accounts', self::HOSTING_ACCOUNTS_FLAGGED_WARNING_INDEX);
$this->dropColumnsIfPresent('hosting_accounts', [
'memory_used_mb',
'process_count',
'io_usage_mb',
'cpu_limit_percent',
'memory_limit_mb',
'process_limit',
'io_limit_mb',
'inode_limit',
'resource_status',
'uploads_restricted',
'is_flagged',
'warning_count',
'cpu_breach_streak',
'memory_breach_streak',
'process_breach_streak',
'io_breach_streak',
'inode_breach_streak',
'last_warning_at',
'last_resource_breach_at',
'throttled_at',
]);
}
private function backfillResourcePolicyDefaults(): void
{
if (! Schema::hasTable('hosting_accounts')) {
return;
}
$products = Schema::hasTable('hosting_products')
? DB::table('hosting_products')->get()->keyBy('id')
: collect();
DB::table('hosting_accounts')
->select(['id', 'hosting_product_id', 'type', 'status'])
->orderBy('id')
->chunkById(100, function ($accounts) use ($products): void {
foreach ($accounts as $account) {
$product = $products->get($account->hosting_product_id);
$type = $product->type ?? null;
$defaults = match ($type) {
'wordpress' => [75, 768, 15, 8, 100000],
'multi_domain' => [100, 1024, 20, 10, 150000],
default => [50, 512, 10, 5, 50000],
};
DB::table('hosting_accounts')
->where('id', $account->id)
->update([
'cpu_limit_percent' => $defaults[0],
'memory_limit_mb' => $defaults[1],
'process_limit' => $defaults[2],
'io_limit_mb' => $defaults[3],
'inode_limit' => $defaults[4],
'resource_status' => $account->status === 'suspended' ? 'suspended' : 'active',
]);
}
});
}
private function addColumnIfMissing(string $table, string $column, callable $callback): void
{
if (! Schema::hasTable($table) || Schema::hasColumn($table, $column)) {
return;
}
Schema::table($table, $callback);
}
private function addIndexIfMissing(string $table, array $columns, string $indexName): void
{
if (! Schema::hasTable($table) || $this->hasIndex($table, $indexName)) {
return;
}
Schema::table($table, function (Blueprint $table) use ($columns, $indexName): void {
$table->index($columns, $indexName);
});
}
private function dropColumnsIfPresent(string $table, array $columns): void
{
if (! Schema::hasTable($table)) {
return;
}
$presentColumns = array_values(array_filter($columns, fn (string $column): bool => Schema::hasColumn($table, $column)));
if ($presentColumns === []) {
return;
}
Schema::table($table, function (Blueprint $table) use ($presentColumns): void {
$table->dropColumn($presentColumns);
});
}
private function dropIndexIfPresent(string $table, string $indexName): void
{
if (! Schema::hasTable($table) || ! $this->hasIndex($table, $indexName)) {
return;
}
Schema::table($table, function (Blueprint $table) use ($indexName): void {
$table->dropIndex($indexName);
});
}
private function hasIndex(string $table, string $indexName): bool
{
if (! Schema::hasTable($table)) {
return false;
}
$driver = DB::getDriverName();
if ($driver === 'sqlite') {
$rows = DB::select(sprintf("PRAGMA index_list('%s')", str_replace("'", "''", $table)));
foreach ($rows as $row) {
if (($row->name ?? null) === $indexName) {
return true;
}
}
return false;
}
if ($driver === 'mysql') {
return DB::table('information_schema.statistics')
->where('table_schema', DB::getDatabaseName())
->where('table_name', $table)
->where('index_name', $indexName)
->exists();
}
return false;
}
};
@@ -0,0 +1,85 @@
<?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
{
if (! Schema::hasTable('hosting_plan_changes')) {
Schema::create('hosting_plan_changes', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->foreignId('hosting_account_id')->constrained()->cascadeOnDelete();
$table->foreignId('from_hosting_product_id')->constrained('hosting_products')->restrictOnDelete();
$table->foreignId('to_hosting_product_id')->constrained('hosting_products')->restrictOnDelete();
$table->string('direction');
$table->string('billing_cycle')->default('monthly');
$table->unsignedSmallInteger('cycle_months')->default(1);
$table->unsignedSmallInteger('remaining_days')->default(0);
$table->decimal('proration_ratio', 8, 4)->default(0);
$table->decimal('old_cycle_price', 10, 2)->default(0);
$table->decimal('new_cycle_price', 10, 2)->default(0);
$table->decimal('charge_amount', 10, 2)->default(0);
$table->decimal('credit_amount', 10, 2)->default(0);
$table->decimal('net_amount', 10, 2)->default(0);
$table->string('currency', 3)->default('GHS');
$table->string('status')->default('applied');
$table->timestamp('applied_at')->nullable();
$table->json('metadata')->nullable();
$table->timestamps();
$table->index(['hosting_account_id', 'created_at']);
$table->index(['user_id', 'created_at']);
$table->index(['direction', 'status']);
});
}
if (! Schema::hasTable('hosting_billing_invoices')) {
Schema::create('hosting_billing_invoices', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->foreignId('hosting_account_id')->constrained()->cascadeOnDelete();
$table->foreignId('hosting_plan_change_id')->nullable()->constrained()->nullOnDelete();
$table->string('invoice_type');
$table->string('status')->default('pending');
$table->string('currency', 3)->default('GHS');
$table->decimal('subtotal_amount', 10, 2)->default(0);
$table->decimal('credit_amount', 10, 2)->default(0);
$table->decimal('total_amount', 10, 2)->default(0);
$table->string('description')->nullable();
$table->string('provider_reference')->nullable();
$table->timestamp('paid_at')->nullable();
$table->json('line_items')->nullable();
$table->json('metadata')->nullable();
$table->timestamps();
$table->index(['hosting_account_id', 'invoice_type']);
$table->index(['user_id', 'status']);
$table->index(['invoice_type', 'created_at']);
});
}
if (Schema::hasTable('mailboxes') && ! Schema::hasColumn('mailboxes', 'hosting_account_id')) {
Schema::table('mailboxes', function (Blueprint $table) {
$table->foreignId('hosting_account_id')->nullable()->after('website_id')->constrained()->nullOnDelete();
$table->index(['hosting_account_id', 'status']);
});
}
}
public function down(): void
{
if (Schema::hasTable('mailboxes') && Schema::hasColumn('mailboxes', 'hosting_account_id')) {
Schema::table('mailboxes', function (Blueprint $table) {
$table->dropIndex(['hosting_account_id', 'status']);
$table->dropConstrainedForeignId('hosting_account_id');
});
}
Schema::dropIfExists('hosting_billing_invoices');
Schema::dropIfExists('hosting_plan_changes');
}
};
@@ -0,0 +1,29 @@
<?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::create('hosting_account_members', function (Blueprint $table) {
$table->id();
$table->foreignId('hosting_account_id')->constrained('hosting_accounts')->cascadeOnDelete();
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
$table->foreignId('invited_by_user_id')->nullable()->constrained('users')->nullOnDelete();
$table->string('role')->default('developer');
$table->timestamp('invited_at')->nullable();
$table->timestamps();
$table->unique(['hosting_account_id', 'user_id']);
$table->index(['user_id', 'role']);
});
}
public function down(): void
{
Schema::dropIfExists('hosting_account_members');
}
};
@@ -0,0 +1,23 @@
<?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('hosting_account_members', function (Blueprint $table) {
$table->text('ssh_public_key')->nullable()->after('invited_at');
$table->timestamp('ssh_key_installed_at')->nullable()->after('ssh_public_key');
});
}
public function down(): void
{
Schema::table('hosting_account_members', function (Blueprint $table) {
$table->dropColumn(['ssh_public_key', 'ssh_key_installed_at']);
});
}
};
@@ -0,0 +1,30 @@
<?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('hosted_sites', function (Blueprint $table) {
$table->string('ssl_status')->nullable()->after('ssl_enabled');
$table->text('ssl_error')->nullable()->after('ssl_status');
$table->timestamp('ssl_provisioned_at')->nullable()->after('ssl_expires_at');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('hosted_sites', function (Blueprint $table) {
$table->dropColumn(['ssl_status', 'ssl_error', 'ssl_provisioned_at']);
});
}
};
@@ -0,0 +1,82 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
if (! Schema::hasTable('hosting_products') || ! Schema::hasTable('hosting_accounts')) {
return;
}
$targets = [
'starter-plan' => 150000,
'basic-plan' => 300000,
'plus-plan' => 300000,
'growth-plan' => 500000,
'pro-plan' => 800000,
'wp-starter' => 300000,
'wp-business' => 300000,
];
foreach ($targets as $slug => $inodeLimit) {
$productId = (int) (DB::table('hosting_products')->where('slug', $slug)->value('id') ?? 0);
if ($productId > 0) {
$this->setAccountsForProduct($productId, $inodeLimit);
}
}
}
public function down(): void
{
if (! Schema::hasTable('hosting_products') || ! Schema::hasTable('hosting_accounts')) {
return;
}
$fallbacks = [
'starter-plan' => 50000,
'basic-plan' => 100000,
'plus-plan' => 100000,
'growth-plan' => 300000,
'pro-plan' => 600000,
'wp-starter' => 100000,
'wp-business' => 100000,
];
foreach ($fallbacks as $slug => $inodeLimit) {
$productId = (int) (DB::table('hosting_products')->where('slug', $slug)->value('id') ?? 0);
if ($productId > 0) {
$this->setAccountsForProduct($productId, $inodeLimit);
}
}
}
private function setAccountsForProduct(int $productId, int $inodeLimit): void
{
DB::table('hosting_accounts')
->where('hosting_product_id', $productId)
->orderBy('id')
->chunkById(100, function ($accounts) use ($inodeLimit): void {
foreach ($accounts as $account) {
$resourceLimits = json_decode((string) ($account->resource_limits ?? '[]'), true);
if (! is_array($resourceLimits)) {
$resourceLimits = [];
}
$resourceLimits['inode_limit'] = $inodeLimit;
DB::table('hosting_accounts')
->where('id', $account->id)
->update([
'inode_limit' => $inodeLimit,
'resource_limits' => json_encode($resourceLimits, JSON_UNESCAPED_UNICODE),
]);
}
});
}
};
@@ -0,0 +1,169 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
private array $plans = [
'starter-plan' => [
'description' => 'Entry-level single-domain hosting with 8 GB SSD storage, unmetered bandwidth, 2 free email accounts, free SSL + CDN, LiteSpeed servers, AI Website Builder, Softaculous installer, weekly backups, 3 FTP users, and 3 MySQL databases.',
'disk_gb' => 8,
'max_domains' => 1,
'max_databases' => 3,
'max_email_accounts' => 2,
'max_ftp_accounts' => 3,
'limits' => [75, 768, 15, 8, 225000],
],
'basic-plan' => [
'description' => 'Perfect for individuals & small business websites. Includes 15 GB SSD storage, unmetered bandwidth, 8 free email accounts, free SSL + CDN, LiteSpeed servers, AI Website Builder, Softaculous installer, weekly backups, 15 FTP users, and 15 MySQL databases.',
'disk_gb' => 15,
'max_domains' => 1,
'max_databases' => 15,
'max_email_accounts' => 8,
'max_ftp_accounts' => 15,
'limits' => [75, 768, 15, 8, 450000],
],
'plus-plan' => [
'description' => 'Enhanced single-domain hosting with 30 GB SSD storage, unmetered bandwidth, 15 free email accounts, free SSL + CDN, LiteSpeed servers, AI Website Builder + WordPress AI, Softaculous installer, twice-weekly backups, SSH access, 30 FTP users, and 30 MySQL databases.',
'disk_gb' => 30,
'max_domains' => 1,
'max_databases' => 30,
'max_email_accounts' => 15,
'max_ftp_accounts' => 30,
'limits' => [75, 768, 15, 8, 450000],
],
'growth-plan' => [
'description' => 'For businesses managing multiple websites. Host up to 8 websites with 45 GB SSD storage, unmetered bandwidth, 23 free email accounts, free SSL + CDN, LiteSpeed servers, AI tools + Site builders, Softaculous installer, daily backups, SSH access, 75 FTP users, and 75 MySQL databases.',
'disk_gb' => 45,
'max_domains' => 8,
'max_databases' => 75,
'max_email_accounts' => 23,
'max_ftp_accounts' => 75,
'limits' => [150, 1536, 30, 15, 750000],
],
'pro-plan' => [
'description' => 'Unlimited websites with 225 GB SSD storage and unmetered bandwidth, 38 free email accounts, free SSL + CDN, LiteSpeed servers, AI tools + Site builders, Softaculous installer, daily backups, SSH access, unlimited FTP users, and unlimited MySQL databases.',
'disk_gb' => 225,
'max_domains' => -1,
'max_databases' => -1,
'max_email_accounts' => 38,
'max_ftp_accounts' => -1,
'limits' => [150, 1536, 30, 15, 1200000],
],
'wp-starter' => [
'description' => 'Optimized WordPress hosting with 23 GB SSD storage, unmetered bandwidth, 8 free email accounts, free SSL + CDN, LiteSpeed + WordPress optimization, AI for WordPress, 1-click WordPress install, automatic updates, daily backups, and SSH access.',
'disk_gb' => 23,
'max_domains' => 1,
'max_databases' => 2,
'max_email_accounts' => 8,
'max_ftp_accounts' => null,
'limits' => [113, 1152, 23, 12, 450000],
],
'wp-business' => [
'description' => 'Premium WordPress hosting with unlimited WordPress sites, 75 GB SSD storage, unmetered bandwidth, 38 free email accounts, advanced security (Imunify360-level), free SSL + CDN, LiteSpeed + advanced caching, AI for WordPress, staging environment, daily backups + restore points, SSH access, and priority performance.',
'disk_gb' => 75,
'max_domains' => -1,
'max_databases' => -1,
'max_email_accounts' => 38,
'max_ftp_accounts' => null,
'limits' => [113, 1152, 23, 12, 450000],
],
];
public function up(): void
{
if (! Schema::hasTable('hosting_products')) {
return;
}
foreach ($this->plans as $slug => $plan) {
$product = DB::table('hosting_products')->where('slug', $slug)->first();
if (! $product) {
continue;
}
DB::table('hosting_products')
->where('id', $product->id)
->update(array_filter([
'description' => $plan['description'],
'disk_gb' => $plan['disk_gb'],
'max_domains' => $plan['max_domains'],
'max_databases' => $plan['max_databases'],
'max_email_accounts' => $plan['max_email_accounts'],
'max_ftp_accounts' => $plan['max_ftp_accounts'],
'updated_at' => now(),
], fn ($value) => $value !== null));
$this->updateAccountsForProduct((int) $product->id, $plan);
}
}
public function down(): void
{
// Do not reduce customer quotas automatically on rollback.
}
private function updateAccountsForProduct(int $productId, array $plan): void
{
if (! Schema::hasTable('hosting_accounts')) {
return;
}
[$cpuLimit, $memoryLimit, $processLimit, $ioLimit, $inodeLimit] = $plan['limits'];
DB::table('hosting_accounts')
->where('hosting_product_id', $productId)
->orderBy('id')
->chunkById(100, function ($accounts) use ($plan, $cpuLimit, $memoryLimit, $processLimit, $ioLimit, $inodeLimit): void {
foreach ($accounts as $account) {
$resourceLimits = json_decode((string) ($account->resource_limits ?? '[]'), true);
if (! is_array($resourceLimits)) {
$resourceLimits = [];
}
$resourceLimits['disk_gb'] = max((int) ($resourceLimits['disk_gb'] ?? 0), (int) $plan['disk_gb']);
$resourceLimits['cpu_limit_percent'] = max((int) ($resourceLimits['cpu_limit_percent'] ?? 0), $cpuLimit);
$resourceLimits['memory_limit_mb'] = max((int) ($resourceLimits['memory_limit_mb'] ?? 0), $memoryLimit);
$resourceLimits['process_limit'] = max((int) ($resourceLimits['process_limit'] ?? 0), $processLimit);
$resourceLimits['io_limit_mb'] = max((int) ($resourceLimits['io_limit_mb'] ?? 0), $ioLimit);
$resourceLimits['inode_limit'] = max((int) ($resourceLimits['inode_limit'] ?? 0), $inodeLimit);
$this->raiseFiniteResourceLimit($resourceLimits, 'max_domains', $plan['max_domains'] ?? null);
$this->raiseFiniteResourceLimit($resourceLimits, 'max_databases', $plan['max_databases'] ?? null);
$this->raiseFiniteResourceLimit($resourceLimits, 'max_email_accounts', $plan['max_email_accounts'] ?? null);
$this->raiseFiniteResourceLimit($resourceLimits, 'max_ftp_accounts', $plan['max_ftp_accounts'] ?? null);
$updates = [
'allocated_disk_gb' => max((int) ($account->allocated_disk_gb ?? 0), (int) $plan['disk_gb']),
'cpu_limit_percent' => max((int) ($account->cpu_limit_percent ?? 0), $cpuLimit),
'memory_limit_mb' => max((int) ($account->memory_limit_mb ?? 0), $memoryLimit),
'process_limit' => max((int) ($account->process_limit ?? 0), $processLimit),
'io_limit_mb' => max((int) ($account->io_limit_mb ?? 0), $ioLimit),
'inode_limit' => max((int) ($account->inode_limit ?? 0), $inodeLimit),
'resource_limits' => json_encode($resourceLimits, JSON_UNESCAPED_UNICODE),
'updated_at' => now(),
];
DB::table('hosting_accounts')
->where('id', $account->id)
->update($updates);
}
});
}
private function raiseFiniteResourceLimit(array &$resourceLimits, string $key, mixed $newValue): void
{
if ($newValue === null) {
return;
}
if ((int) $newValue === -1 || (int) ($resourceLimits[$key] ?? 0) === -1) {
$resourceLimits[$key] = -1;
return;
}
$resourceLimits[$key] = max((int) ($resourceLimits[$key] ?? 0), (int) $newValue);
}
};
@@ -0,0 +1,110 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
if (! Schema::hasTable('hosting_products')) {
return;
}
foreach ($this->serverProducts() as $slug => $payload) {
DB::table('hosting_products')
->where('slug', $slug)
->update($payload + [
'currency' => 'GHS',
'price_monthly' => 0,
'price_quarterly' => 0,
'price_yearly' => 0,
'price_biennial' => 0,
'deleted_at' => null,
'updated_at' => now(),
]);
}
$this->clearPricingCaches();
}
public function down(): void
{
if (! Schema::hasTable('hosting_products')) {
return;
}
$legacy = [
'vps-s' => ['contabo_product_id' => 'V10', 'price_monthly' => 75, 'price_quarterly' => 215, 'price_yearly' => 720],
'vps-m' => ['contabo_product_id' => 'V20', 'price_monthly' => 90, 'price_quarterly' => 255, 'price_yearly' => 865],
'vps-l' => ['contabo_product_id' => 'V30', 'price_monthly' => 120, 'price_quarterly' => 340, 'price_yearly' => 1150],
'vps-xl' => ['contabo_product_id' => 'V40', 'price_monthly' => 150, 'price_quarterly' => 430, 'price_yearly' => 1440],
];
foreach ($legacy as $slug => $payload) {
DB::table('hosting_products')
->where('slug', $slug)
->update($payload + [
'updated_at' => now(),
]);
}
$this->clearPricingCaches();
}
/**
* @return array<string, array<string, mixed>>
*/
private function serverProducts(): array
{
return [
'vps-s' => [
'description' => 'Entry-level VPS with 3 vCPU cores, 8 GB RAM, 75 GB NVMe SSD, and unlimited bandwidth. Perfect for small projects and development.',
'disk_gb' => 75,
'cpu_cores' => 3,
'ram_mb' => 8192,
'contabo_product_id' => 'V91',
'contabo_region' => 'EU',
'sort_order' => 10,
],
'vps-m' => [
'description' => 'Mid-range VPS with 6 vCPU cores, 12 GB RAM, 100 GB NVMe SSD, and unlimited bandwidth. Ideal for growing applications and medium traffic sites.',
'disk_gb' => 100,
'cpu_cores' => 6,
'ram_mb' => 12288,
'contabo_product_id' => 'V94',
'contabo_region' => 'EU',
'sort_order' => 11,
],
'vps-l' => [
'description' => 'High-performance VPS with 8 vCPU cores, 24 GB RAM, 200 GB NVMe SSD, and unlimited bandwidth. Built for demanding workloads and high-traffic applications.',
'disk_gb' => 200,
'cpu_cores' => 8,
'ram_mb' => 24576,
'contabo_product_id' => 'V97',
'contabo_region' => 'EU',
'sort_order' => 12,
],
'vps-xl' => [
'description' => 'Enterprise VPS with 12 vCPU cores, 48 GB RAM, 250 GB NVMe SSD, and unlimited bandwidth. Maximum performance for resource-intensive applications.',
'disk_gb' => 250,
'cpu_cores' => 12,
'ram_mb' => 49152,
'contabo_product_id' => 'V100',
'contabo_region' => 'EU',
'sort_order' => 13,
],
];
}
private function clearPricingCaches(): void
{
Cache::forget('contabo_products');
foreach (['V10', 'V20', 'V30', 'V40', 'V91', 'V94', 'V97', 'V100'] as $productId) {
Cache::forget("contabo_product_price_{$productId}");
}
}
};
@@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
if (Schema::hasColumn('hosting_nodes', 'role')) {
return;
}
Schema::table('hosting_nodes', function (Blueprint $table) {
$table->string('role', 20)->default('primary')->after('type');
$table->foreignId('primary_hosting_node_id')
->nullable()
->after('role')
->constrained('hosting_nodes')
->nullOnDelete();
$table->index(['role', 'primary_hosting_node_id']);
});
DB::table('hosting_nodes')->update(['role' => 'primary']);
}
public function down(): void
{
Schema::table('hosting_nodes', function (Blueprint $table) {
$table->dropForeign(['primary_hosting_node_id']);
$table->dropIndex(['role', 'primary_hosting_node_id']);
$table->dropColumn(['role', 'primary_hosting_node_id']);
});
}
};
@@ -0,0 +1,49 @@
<?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
{
if (Schema::hasTable('contabo_infrastructure_provisions')) {
return;
}
Schema::create('contabo_infrastructure_provisions', function (Blueprint $table) {
$table->id();
$table->string('purpose');
$table->string('status')->default('pending');
$table->string('name');
$table->string('contabo_product_id');
$table->string('region');
$table->string('image_id')->nullable();
$table->string('contabo_instance_id')->nullable();
$table->string('completion_token', 64);
$table->json('payload')->nullable();
$table->json('log')->nullable();
$table->text('ssh_public_key')->nullable();
$table->text('ssh_private_key')->nullable();
$table->text('generated_mail_db_password')->nullable();
$table->foreignId('hosting_node_id')->nullable()->constrained()->nullOnDelete();
$table->unsignedBigInteger('email_server_id')->nullable();
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
$table->text('error_message')->nullable();
$table->timestamp('instance_created_at')->nullable();
$table->timestamp('ip_assigned_at')->nullable();
$table->timestamp('completed_at')->nullable();
$table->timestamp('failed_at')->nullable();
$table->timestamps();
$table->index(['status', 'purpose']);
$table->index('completion_token');
});
}
public function down(): void
{
Schema::dropIfExists('contabo_infrastructure_provisions');
}
};
@@ -0,0 +1,23 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
public function up(): void
{
DB::table('hosting_nodes')
->where('provider', 'local')
->where('disk_gb', 100)
->update(['disk_gb' => 150]);
}
public function down(): void
{
DB::table('hosting_nodes')
->where('provider', 'local')
->where('disk_gb', 150)
->update(['disk_gb' => 100]);
}
};
@@ -0,0 +1,32 @@
<?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
{
// Team membership: links a user to an account (owner) whose mailboxes &
// domains they can manage. Invited by email; linked on first SSO login.
Schema::create('email_team_members', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('account_id')->index(); // owner user id
$table->unsignedBigInteger('user_id')->nullable()->index(); // member user (set on accept)
$table->string('email');
$table->string('role', 20)->default('member'); // admin|member
$table->string('status', 20)->default('invited'); // invited|active
$table->string('token', 64)->nullable();
$table->timestamp('accepted_at')->nullable();
$table->timestamps();
$table->unique(['account_id', 'email']);
});
}
public function down(): void
{
Schema::dropIfExists('email_team_members');
}
};
@@ -0,0 +1,26 @@
<?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
{
// Per-account preferences for the Email product.
Schema::create('email_settings', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->unique()->constrained()->cascadeOnDelete();
$table->unsignedInteger('default_quota_mb')->nullable(); // default new-mailbox quota
$table->string('notify_email')->nullable(); // where billing/alerts go
$table->boolean('product_updates')->default(true);
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('email_settings');
}
};
@@ -0,0 +1,33 @@
<?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::create('personal_access_tokens', function (Blueprint $table) {
$table->id();
$table->morphs('tokenable');
$table->text('name');
$table->string('token', 64)->unique();
$table->text('abilities')->nullable();
$table->timestamp('last_used_at')->nullable();
$table->timestamp('expires_at')->nullable()->index();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('personal_access_tokens');
}
};
@@ -0,0 +1,32 @@
<?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
{
// Team membership: links a user to an account (owner) whose mailboxes &
// domains they can manage. Invited by email; linked on first SSO login.
Schema::create('hosting_team_members', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('account_id')->index(); // owner user id
$table->unsignedBigInteger('user_id')->nullable()->index(); // member user (set on accept)
$table->string('email');
$table->string('role', 20)->default('member'); // admin|member
$table->string('status', 20)->default('invited'); // invited|active
$table->string('token', 64)->nullable();
$table->timestamp('accepted_at')->nullable();
$table->timestamps();
$table->unique(['account_id', 'email']);
});
}
public function down(): void
{
Schema::dropIfExists('hosting_team_members');
}
};
@@ -0,0 +1,42 @@
<?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
{
foreach ([
'hosting_accounts',
'customer_hosting_orders',
'hosting_orders',
'hosting_billing_invoices',
'hosting_plan_changes',
] as $table) {
if (Schema::hasTable($table) && ! Schema::hasColumn($table, 'platform_id')) {
Schema::table($table, function (Blueprint $table): void {
$table->unsignedBigInteger('platform_id')->nullable()->unique()->after('id');
});
}
}
}
public function down(): void
{
foreach ([
'hosting_accounts',
'customer_hosting_orders',
'hosting_orders',
'hosting_billing_invoices',
'hosting_plan_changes',
] as $table) {
if (Schema::hasTable($table) && Schema::hasColumn($table, 'platform_id')) {
Schema::table($table, function (Blueprint $table): void {
$table->dropColumn('platform_id');
});
}
}
}
};
+29
View File
@@ -0,0 +1,29 @@
<?php
namespace Database\Seeders;
use App\Models\User;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use Database\Seeders\HostingProductSeeder;
class DatabaseSeeder extends Seeder
{
use WithoutModelEvents;
/**
* Seed the application's database.
*/
public function run(): void
{
$this->call([
HostingProductSeeder::class,
]);
// User::factory(10)->create();
User::factory()->create([
'name' => 'Test User',
'email' => 'test@example.com',
]);
}
}
+40
View File
@@ -0,0 +1,40 @@
<?php
namespace Database\Seeders;
use App\Models\HostingNode;
use Illuminate\Database\Seeder;
class HostingNodeSeeder extends Seeder
{
public function run(): void
{
HostingNode::firstOrCreate(
['provider' => 'local', 'ip_address' => '127.0.0.1'],
[
'name' => 'Primary Server',
'hostname' => gethostname() ?: 'localhost',
'ip_address' => '127.0.0.1',
'ipv6_address' => '::1',
'type' => 'shared',
'segment' => 'general',
'provider' => 'local',
'cpu_cores' => 4,
'ram_mb' => 8192,
'disk_gb' => 150,
'oversell_ratio' => 3,
'bandwidth_tb' => 10,
'max_accounts' => 100,
'current_accounts' => 0,
'status' => 'active',
'ssh_port' => 22,
'features' => ['php', 'mysql', 'nginx', 'ssl'],
'installed_software' => [
'php' => ['8.1', '8.2', '8.3'],
'mysql' => '8.0',
'nginx' => '1.24',
],
]
);
}
}
+280
View File
@@ -0,0 +1,280 @@
<?php
namespace Database\Seeders;
use App\Models\HostingProduct;
use Illuminate\Database\Seeder;
class HostingProductSeeder extends Seeder
{
public function run(): void
{
$products = [
// ============================================
// SINGLE DOMAIN HOSTING
// Perfect for individuals & small business websites
// ============================================
[
'name' => 'Starter Plan',
'slug' => 'starter-plan',
'description' => 'Entry-level single-domain hosting with 8 GB SSD storage, unmetered bandwidth, 2 free email accounts, free SSL + CDN, LiteSpeed servers, AI Website Builder, Softaculous installer, weekly backups, 3 FTP users, and 3 MySQL databases.',
'category' => 'shared',
'type' => 'single_domain',
'price_monthly' => 1.00,
'price_yearly' => 12.00, // 1 × 12
'currency' => 'GHS',
'disk_gb' => 8,
'bandwidth_gb' => null, // Unmetered
'max_domains' => 1,
'max_databases' => 3,
'max_email_accounts' => 2,
'max_ftp_accounts' => 3,
'ssl_included' => true,
'backups_included' => true,
'backup_retention_days' => 7, // Weekly backups
'features' => [
'LiteSpeed servers',
'AI Website Builder',
'Softaculous installer',
'Free SSL',
'Free CDN',
'Weekly backups',
],
'is_active' => true,
'is_featured' => false,
'sort_order' => 1,
],
[
'name' => 'Basic Plan',
'slug' => 'basic-plan',
'description' => 'Perfect for individuals & small business websites. Includes 15 GB SSD storage, unmetered bandwidth, 8 free email accounts, free SSL + CDN, LiteSpeed servers, AI Website Builder, Softaculous installer, weekly backups, 15 FTP users, and 15 MySQL databases.',
'category' => 'shared',
'type' => 'single_domain',
'price_monthly' => 25.00,
'price_yearly' => 300.00, // 25 × 12
'currency' => 'GHS',
'disk_gb' => 15,
'bandwidth_gb' => null, // Unmetered
'max_domains' => 1,
'max_databases' => 15,
'max_email_accounts' => 8,
'max_ftp_accounts' => 15,
'ssl_included' => true,
'backups_included' => true,
'backup_retention_days' => 7, // Weekly backups
'features' => [
'LiteSpeed servers',
'AI Website Builder',
'Softaculous installer',
'Free SSL',
'Free CDN',
'Weekly backups',
],
'is_active' => true,
'is_featured' => false,
'sort_order' => 2,
],
[
'name' => 'Plus Plan',
'slug' => 'plus-plan',
'description' => 'Enhanced single-domain hosting with 30 GB SSD storage, unmetered bandwidth, 15 free email accounts, free SSL + CDN, LiteSpeed servers, AI Website Builder + WordPress AI, Softaculous installer, twice-weekly backups, SSH access, 30 FTP users, and 30 MySQL databases.',
'category' => 'shared',
'type' => 'single_domain',
'price_monthly' => 35.00,
'price_yearly' => 420.00, // 35 × 12
'currency' => 'GHS',
'disk_gb' => 30,
'bandwidth_gb' => null, // Unmetered
'max_domains' => 1,
'max_databases' => 30,
'max_email_accounts' => 15,
'max_ftp_accounts' => 30,
'ssl_included' => true,
'backups_included' => true,
'backup_retention_days' => 4, // Twice-weekly backups
'features' => [
'LiteSpeed servers',
'AI Website Builder',
'WordPress AI',
'Softaculous installer',
'Free SSL',
'Free CDN',
'Twice-weekly backups',
'SSH access',
],
'is_active' => true,
'is_featured' => true,
'sort_order' => 3,
],
// ============================================
// MULTI-DOMAIN HOSTING
// For businesses managing multiple websites
// ============================================
[
'name' => 'Growth Plan',
'slug' => 'growth-plan',
'description' => 'For businesses managing multiple websites. Host up to 8 websites with 45 GB SSD storage, unmetered bandwidth, 23 free email accounts, free SSL + CDN, LiteSpeed servers, AI tools + Site builders, Softaculous installer, daily backups, SSH access, 75 FTP users, and 75 MySQL databases.',
'category' => 'shared',
'type' => 'multi_domain',
'price_monthly' => 55.00,
'price_yearly' => 660.00, // 55 × 12
'currency' => 'GHS',
'disk_gb' => 45,
'bandwidth_gb' => null, // Unmetered
'max_domains' => 8,
'max_databases' => 75,
'max_email_accounts' => 23,
'max_ftp_accounts' => 75,
'ssl_included' => true,
'backups_included' => true,
'backup_retention_days' => 7, // Daily backups
'features' => [
'LiteSpeed servers',
'AI tools',
'Site builders',
'Softaculous installer',
'Free SSL',
'Free CDN',
'Daily backups',
'SSH access',
],
'is_active' => true,
'is_featured' => true,
'sort_order' => 4,
],
[
'name' => 'Pro Plan',
'slug' => 'pro-plan',
'description' => 'Unlimited websites with 225 GB SSD storage and unmetered bandwidth, 38 free email accounts, free SSL + CDN, LiteSpeed servers, AI tools + Site builders, Softaculous installer, daily backups, SSH access, unlimited FTP users, and unlimited MySQL databases.',
'category' => 'shared',
'type' => 'multi_domain',
'price_monthly' => 175.00,
'price_yearly' => 2100.00, // 175 × 12
'currency' => 'GHS',
'disk_gb' => 225,
'bandwidth_gb' => null, // Unmetered
'max_domains' => -1, // Unlimited
'max_databases' => -1, // Unlimited
'max_email_accounts' => 38,
'max_ftp_accounts' => -1, // Unlimited
'ssl_included' => true,
'backups_included' => true,
'backup_retention_days' => 7, // Daily backups
'features' => [
'LiteSpeed servers',
'AI tools',
'Site builders',
'Softaculous installer',
'Free SSL',
'Free CDN',
'Daily backups',
'SSH access',
'Unlimited websites',
'Unlimited FTP users',
'Unlimited MySQL databases',
],
'is_active' => true,
'is_featured' => false,
'sort_order' => 5,
],
// ============================================
// WORDPRESS HOSTING
// Optimized for WordPress performance & automation
// ============================================
[
'name' => 'WP Starter',
'slug' => 'wp-starter',
'description' => 'Optimized WordPress hosting with 23 GB SSD storage, unmetered bandwidth, 8 free email accounts, free SSL + CDN, LiteSpeed + WordPress optimization, AI for WordPress, 1-click WordPress install, automatic updates, daily backups, and SSH access.',
'category' => 'shared',
'type' => 'wordpress',
'price_monthly' => 40.00,
'price_yearly' => 480.00, // 40 × 12
'currency' => 'GHS',
'disk_gb' => 23,
'bandwidth_gb' => null, // Unmetered
'max_domains' => 1,
'max_databases' => 2,
'max_email_accounts' => 8,
'ssl_included' => true,
'backups_included' => true,
'backup_retention_days' => 7, // Daily backups
'features' => [
'LiteSpeed servers',
'WordPress optimization',
'AI for WordPress',
'1-click WordPress install',
'Automatic updates',
'Free SSL',
'Free CDN',
'Daily backups',
'SSH access',
],
'is_active' => true,
'is_featured' => true,
'sort_order' => 5,
],
[
'name' => 'WP Business',
'slug' => 'wp-business',
'description' => 'Premium WordPress hosting with unlimited WordPress sites, 75 GB SSD storage, unmetered bandwidth, 38 free email accounts, advanced security (Imunify360-level), free SSL + CDN, LiteSpeed + advanced caching, AI for WordPress, staging environment, daily backups + restore points, SSH access, and priority performance.',
'category' => 'shared',
'type' => 'wordpress',
'price_monthly' => 85.00,
'price_yearly' => 1020.00, // 85 × 12
'currency' => 'GHS',
'disk_gb' => 75,
'bandwidth_gb' => null, // Unmetered
'max_domains' => -1, // Unlimited WordPress sites
'max_databases' => -1, // Unlimited
'max_email_accounts' => 38,
'ssl_included' => true,
'backups_included' => true,
'backup_retention_days' => 30, // Daily backups + restore points
'features' => [
'LiteSpeed servers',
'Advanced caching',
'WordPress optimization',
'AI for WordPress',
'Staging environment',
'Advanced security (Imunify360-level)',
'Free SSL',
'Free CDN',
'Daily backups',
'Restore points',
'SSH access',
'Priority performance',
'Unlimited WordPress sites',
],
'is_active' => true,
'is_featured' => false,
'sort_order' => 6,
],
// VPS & dedicated → Ladill Servers (servers.ladill.com). See ServerProductSeeder there.
];
foreach ($products as $product) {
HostingProduct::updateOrCreate(
['slug' => $product['slug']],
$product
);
}
// Clean up old hosting products that are no longer in the new pricing structure
$newSlugs = array_column($products, 'slug');
$oldSlugsToRemove = [
'starter-hosting',
'basic-hosting',
'business-hosting',
'pro-hosting',
'wordpress-starter',
'wordpress-business',
];
HostingProduct::whereIn('slug', $oldSlugsToRemove)
->whereNotIn('slug', $newSlugs)
->delete();
}
}