initial commit

This commit is contained in:
TheOnlyMace
2026-05-17 13:26:14 +02:00
commit 75299b723d
176 changed files with 20327 additions and 0 deletions

1
database/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
*.sqlite*

View File

@@ -0,0 +1,48 @@
<?php
namespace Database\Factories;
use App\Enums\UserRole;
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),
'role' => UserRole::Customer,
'is_active' => true,
];
}
/**
* Indicate that the model's email address should be unverified.
*/
public function unverified(): static
{
return $this->state(fn (array $attributes) => [
'email_verified_at' => null,
]);
}
}

View File

@@ -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
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$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');
}
};

View File

@@ -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->bigInteger('expiration')->index();
});
Schema::create('cache_locks', function (Blueprint $table) {
$table->string('key')->primary();
$table->string('owner');
$table->bigInteger('expiration')->index();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('cache');
Schema::dropIfExists('cache_locks');
}
};

View File

@@ -0,0 +1,59 @@
<?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->unsignedSmallInteger('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->string('connection');
$table->string('queue');
$table->longText('payload');
$table->longText('exception');
$table->timestamp('failed_at')->useCurrent();
$table->index(['connection', 'queue', 'failed_at']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('jobs');
Schema::dropIfExists('job_batches');
Schema::dropIfExists('failed_jobs');
}
};

View File

@@ -0,0 +1,34 @@
<?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('customers', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('domain')->unique();
$table->unsignedInteger('vmid')->nullable()->unique();
$table->string('ip_address', 45)->nullable();
$table->unsignedTinyInteger('cpu')->default(2);
$table->unsignedInteger('ram')->default(2048);
$table->unsignedInteger('disk')->default(32);
$table->enum('status', ['pending', 'active', 'failed'])->default('pending');
$table->string('provisioning_step')->nullable();
$table->text('error_message')->nullable();
$table->timestamps();
$table->index('status');
$table->index('ip_address');
});
}
public function down(): void
{
Schema::dropIfExists('customers');
}
};

View File

@@ -0,0 +1,67 @@
<?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('users', function (Blueprint $table) {
$table->string('role', 20)->default('customer')->after('email');
$table->boolean('is_active')->default(true)->after('role');
});
Schema::create('ip_pools', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->enum('type', ['private', 'public'])->default('private');
$table->string('start_ip', 45);
$table->string('end_ip', 45);
$table->string('gateway', 45)->nullable();
$table->unsignedTinyInteger('cidr')->default(24);
$table->text('description')->nullable();
$table->boolean('is_active')->default(true);
$table->timestamps();
$table->index(['type', 'is_active']);
});
Schema::table('customers', function (Blueprint $table) {
$table->foreignId('user_id')->nullable()->after('id')->constrained()->nullOnDelete();
$table->foreignId('ip_pool_id')->nullable()->after('user_id')->constrained('ip_pools')->nullOnDelete();
$table->string('public_ip', 45)->nullable()->after('ip_address');
$table->boolean('behind_traefik')->default(true)->after('public_ip');
});
Schema::create('vm_devices', function (Blueprint $table) {
$table->id();
$table->foreignId('customer_id')->constrained()->cascadeOnDelete();
$table->string('type', 32);
$table->string('slot')->nullable();
$table->json('config');
$table->unsignedSmallInteger('sort_order')->default(0);
$table->timestamps();
$table->index(['customer_id', 'type']);
});
}
public function down(): void
{
Schema::dropIfExists('vm_devices');
Schema::table('customers', function (Blueprint $table) {
$table->dropForeign(['user_id']);
$table->dropForeign(['ip_pool_id']);
$table->dropColumn(['user_id', 'ip_pool_id', 'public_ip', 'behind_traefik']);
});
Schema::dropIfExists('ip_pools');
Schema::table('users', function (Blueprint $table) {
$table->dropColumn(['role', 'is_active']);
});
}
};

View File

@@ -0,0 +1,40 @@
<?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('customers', function (Blueprint $table) {
$table->string('attached_iso')->nullable()->after('disk');
$table->string('proxmox_status', 32)->nullable()->after('attached_iso');
$table->unsignedBigInteger('proxmox_uptime')->nullable()->after('proxmox_status');
$table->timestamp('proxmox_status_at')->nullable()->after('proxmox_uptime');
});
Schema::create('vm_activity_logs', function (Blueprint $table) {
$table->id();
$table->foreignId('customer_id')->constrained()->cascadeOnDelete();
$table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
$table->string('action', 64);
$table->string('status', 20)->default('success');
$table->text('message')->nullable();
$table->json('meta')->nullable();
$table->timestamps();
$table->index(['customer_id', 'created_at']);
});
}
public function down(): void
{
Schema::dropIfExists('vm_activity_logs');
Schema::table('customers', function (Blueprint $table) {
$table->dropColumn(['attached_iso', 'proxmox_status', 'proxmox_uptime', 'proxmox_status_at']);
});
}
};

View File

@@ -0,0 +1,92 @@
<?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_plans', function (Blueprint $table) {
$table->id();
$table->string('slug')->unique();
$table->string('name');
$table->unsignedTinyInteger('cpu');
$table->unsignedInteger('ram');
$table->unsignedInteger('disk');
$table->unsignedTinyInteger('max_backups')->default(4);
$table->boolean('allow_public_ip')->default(false);
$table->boolean('allow_iso_upload')->default(true);
$table->boolean('is_active')->default(true);
$table->unsignedInteger('whmcs_product_id')->nullable()->index();
$table->timestamps();
});
Schema::create('vmid_reservations', function (Blueprint $table) {
$table->id();
$table->unsignedInteger('vmid')->unique();
$table->foreignId('customer_id')->nullable()->constrained()->nullOnDelete();
$table->enum('status', ['reserved', 'active', 'pending_release', 'released'])->default('reserved');
$table->timestamp('release_at')->nullable();
$table->timestamp('released_at')->nullable();
$table->timestamps();
$table->index(['status', 'release_at']);
});
Schema::create('whmcs_services', function (Blueprint $table) {
$table->id();
$table->unsignedInteger('whmcs_service_id')->unique();
$table->unsignedInteger('whmcs_client_id')->index();
$table->unsignedInteger('whmcs_order_id')->nullable();
$table->foreignId('customer_id')->nullable()->constrained()->nullOnDelete();
$table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
$table->foreignId('hosting_plan_id')->nullable()->constrained()->nullOnDelete();
$table->string('status', 32)->default('pending');
$table->json('config')->nullable();
$table->timestamps();
});
Schema::create('customer_iso_uploads', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->string('filename');
$table->string('volid');
$table->unsignedBigInteger('size_bytes');
$table->timestamp('expires_at');
$table->timestamps();
$table->index(['user_id', 'expires_at']);
});
Schema::table('users', function (Blueprint $table) {
$table->unsignedInteger('whmcs_client_id')->nullable()->unique()->after('id');
$table->boolean('two_factor_required')->default(false)->after('is_active');
});
Schema::table('customers', function (Blueprint $table) {
$table->foreignId('hosting_plan_id')->nullable()->after('user_id')->constrained()->nullOnDelete();
$table->foreignId('whmcs_service_id')->nullable()->after('hosting_plan_id')->constrained('whmcs_services')->nullOnDelete();
$table->string('provision_mode', 20)->default('template')->after('behind_traefik');
});
}
public function down(): void
{
Schema::table('customers', function (Blueprint $table) {
$table->dropForeign(['hosting_plan_id']);
$table->dropForeign(['whmcs_service_id']);
$table->dropColumn(['hosting_plan_id', 'whmcs_service_id', 'provision_mode']);
});
Schema::table('users', function (Blueprint $table) {
$table->dropColumn(['whmcs_client_id', 'two_factor_required']);
});
Schema::dropIfExists('customer_iso_uploads');
Schema::dropIfExists('whmcs_services');
Schema::dropIfExists('vmid_reservations');
Schema::dropIfExists('hosting_plans');
}
};

View File

@@ -0,0 +1,95 @@
<?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('users', function (Blueprint $table) {
$table->text('two_factor_secret')->nullable()->after('password');
$table->text('two_factor_recovery_codes')->nullable()->after('two_factor_secret');
$table->timestamp('two_factor_confirmed_at')->nullable()->after('two_factor_recovery_codes');
});
Schema::create('vm_templates', function (Blueprint $table) {
$table->id();
$table->string('slug')->unique();
$table->string('name');
$table->unsignedInteger('proxmox_template_vmid');
$table->string('os_family', 32)->nullable();
$table->boolean('is_active')->default(true);
$table->timestamps();
});
Schema::create('vm_snapshots', function (Blueprint $table) {
$table->id();
$table->foreignId('customer_id')->constrained()->cascadeOnDelete();
$table->string('name');
$table->string('proxmox_snapshot_id');
$table->boolean('auto_created')->default(false);
$table->timestamp('expires_at')->nullable();
$table->timestamps();
$table->index(['customer_id', 'expires_at']);
});
Schema::create('vm_backups', function (Blueprint $table) {
$table->id();
$table->foreignId('customer_id')->constrained()->cascadeOnDelete();
$table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
$table->string('storage', 64);
$table->string('volume_id')->nullable();
$table->string('status', 32)->default('running');
$table->unsignedBigInteger('size_bytes')->nullable();
$table->timestamp('completed_at')->nullable();
$table->timestamps();
$table->index(['customer_id', 'status']);
});
Schema::create('vm_firewall_rules', function (Blueprint $table) {
$table->id();
$table->foreignId('customer_id')->constrained()->cascadeOnDelete();
$table->string('direction', 8)->default('in');
$table->string('action', 8)->default('ACCEPT');
$table->string('protocol', 8)->default('tcp');
$table->string('port')->nullable();
$table->string('source')->nullable();
$table->boolean('is_active')->default(true);
$table->unsignedSmallInteger('sort_order')->default(0);
$table->timestamps();
});
Schema::create('vm_metrics', function (Blueprint $table) {
$table->id();
$table->foreignId('customer_id')->constrained()->cascadeOnDelete();
$table->float('cpu')->default(0);
$table->unsignedBigInteger('mem')->default(0);
$table->unsignedBigInteger('maxmem')->default(0);
$table->unsignedBigInteger('disk')->default(0);
$table->unsignedBigInteger('maxdisk')->default(0);
$table->timestamp('recorded_at');
$table->index(['customer_id', 'recorded_at']);
});
Schema::create('system_settings', function (Blueprint $table) {
$table->string('key')->primary();
$table->text('value')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('system_settings');
Schema::dropIfExists('vm_metrics');
Schema::dropIfExists('vm_firewall_rules');
Schema::dropIfExists('vm_backups');
Schema::dropIfExists('vm_snapshots');
Schema::dropIfExists('vm_templates');
Schema::table('users', function (Blueprint $table) {
$table->dropColumn(['two_factor_secret', 'two_factor_recovery_codes', 'two_factor_confirmed_at']);
});
}
};

View File

@@ -0,0 +1,16 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
use WithoutModelEvents;
public function run(): void
{
$this->call(HostingSeeder::class);
}
}

View File

@@ -0,0 +1,70 @@
<?php
namespace Database\Seeders;
use App\Enums\IpPoolType;
use App\Enums\UserRole;
use App\Models\HostingPlan;
use App\Models\IpPool;
use App\Models\User;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Hash;
class HostingSeeder extends Seeder
{
public function run(): void
{
User::query()->updateOrCreate(
['email' => 'admin@hexahost.local'],
[
'name' => 'Administrator',
'password' => Hash::make('admin1234'),
'role' => UserRole::Admin,
'is_active' => true,
],
);
$plans = [
['slug' => 'small', 'name' => 'Small', 'cpu' => 2, 'ram' => 4096, 'disk' => 40],
['slug' => 'medium', 'name' => 'Medium', 'cpu' => 4, 'ram' => 8192, 'disk' => 80],
['slug' => 'large', 'name' => 'Large', 'cpu' => 8, 'ram' => 16384, 'disk' => 160],
];
foreach ($plans as $plan) {
HostingPlan::query()->updateOrCreate(
['slug' => $plan['slug']],
[
...$plan,
'max_backups' => (int) config('hosting.backups.max_per_customer', 4),
'allow_public_ip' => true,
'allow_iso_upload' => true,
'is_active' => true,
],
);
}
IpPool::query()->updateOrCreate(
['name' => 'Privat 10.32.0.0/24', 'type' => IpPoolType::Private],
[
'start_ip' => config('hosting.network.ip_pool_start', '10.32.0.10'),
'end_ip' => config('hosting.network.ip_pool_end', '10.32.0.254'),
'gateway' => config('hosting.network.gateway', '10.32.0.1'),
'cidr' => (int) config('hosting.network.cidr', 24),
'description' => 'Internes VM-Netz (Hyperion)',
'is_active' => true,
],
);
IpPool::query()->updateOrCreate(
['name' => 'Öffentlich 185.45.149.x', 'type' => IpPoolType::Public],
[
'start_ip' => config('hosting.public_network.ip_pool_start', '185.45.149.246'),
'end_ip' => config('hosting.public_network.ip_pool_end', '185.45.149.252'),
'gateway' => config('hosting.public_network.gateway', '185.45.149.241'),
'cidr' => (int) config('hosting.public_network.cidr', 28),
'description' => 'Öffentliche IPs /28',
'is_active' => true,
],
);
}
}