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

129
app/Models/Customer.php Normal file
View File

@@ -0,0 +1,129 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Customer extends Model
{
protected $table = 'customers';
protected $fillable = [
'user_id',
'ip_pool_id',
'name',
'domain',
'vmid',
'ip_address',
'public_ip',
'behind_traefik',
'provision_mode',
'hosting_plan_id',
'whmcs_service_id',
'cpu',
'ram',
'disk',
'attached_iso',
'proxmox_status',
'proxmox_uptime',
'proxmox_status_at',
'status',
'provisioning_step',
'error_message',
];
protected function casts(): array
{
return [
'vmid' => 'integer',
'cpu' => 'integer',
'ram' => 'integer',
'disk' => 'integer',
'behind_traefik' => 'boolean',
'proxmox_uptime' => 'integer',
'proxmox_status_at' => 'datetime',
];
}
public function owner(): BelongsTo
{
return $this->belongsTo(User::class, 'user_id');
}
public function ipPool(): BelongsTo
{
return $this->belongsTo(IpPool::class, 'ip_pool_id');
}
public function plan(): BelongsTo
{
return $this->belongsTo(HostingPlan::class, 'hosting_plan_id');
}
public function whmcsService(): BelongsTo
{
return $this->belongsTo(WhmcsService::class, 'whmcs_service_id');
}
public function vmidReservation(): \Illuminate\Database\Eloquent\Relations\HasOne
{
return $this->hasOne(VmidReservation::class);
}
public function devices(): HasMany
{
return $this->hasMany(VmDevice::class)->orderBy('sort_order');
}
public function snapshots(): HasMany
{
return $this->hasMany(VmSnapshot::class);
}
public function backups(): HasMany
{
return $this->hasMany(VmBackup::class);
}
public function firewallRules(): HasMany
{
return $this->hasMany(VmFirewallRule::class)->orderBy('sort_order');
}
public function metrics(): HasMany
{
return $this->hasMany(VmMetric::class)->orderByDesc('recorded_at');
}
public function activityLogs(): HasMany
{
return $this->hasMany(VmActivityLog::class)->latest();
}
public function isRunning(): bool
{
return $this->proxmox_status === 'running';
}
public function scopeForUser(Builder $query, User $user): Builder
{
if ($user->isAdmin()) {
return $query;
}
return $query->where('user_id', $user->id);
}
public function isProvisionable(): bool
{
return in_array($this->status, ['pending', 'failed'], true);
}
public function displayName(): string
{
return $this->name.($this->vmid ? " (#{$this->vmid})" : '');
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class CustomerIsoUpload extends Model
{
protected $fillable = [
'user_id',
'filename',
'volid',
'size_bytes',
'expires_at',
];
protected function casts(): array
{
return [
'size_bytes' => 'integer',
'expires_at' => 'datetime',
];
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class HostingPlan extends Model
{
protected $fillable = [
'slug',
'name',
'cpu',
'ram',
'disk',
'max_backups',
'allow_public_ip',
'allow_iso_upload',
'is_active',
'whmcs_product_id',
];
protected function casts(): array
{
return [
'cpu' => 'integer',
'ram' => 'integer',
'disk' => 'integer',
'max_backups' => 'integer',
'allow_public_ip' => 'boolean',
'allow_iso_upload' => 'boolean',
'is_active' => 'boolean',
'whmcs_product_id' => 'integer',
];
}
public function customers(): HasMany
{
return $this->hasMany(Customer::class, 'hosting_plan_id');
}
}

74
app/Models/IpPool.php Normal file
View File

@@ -0,0 +1,74 @@
<?php
namespace App\Models;
use App\Enums\IpPoolType;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class IpPool extends Model
{
protected $fillable = [
'name',
'type',
'start_ip',
'end_ip',
'gateway',
'cidr',
'description',
'is_active',
];
protected function casts(): array
{
return [
'type' => IpPoolType::class,
'cidr' => 'integer',
'is_active' => 'boolean',
];
}
public function vms(): HasMany
{
return $this->hasMany(Customer::class, 'ip_pool_id');
}
public function totalIps(): int
{
$start = ip2long($this->start_ip);
$end = ip2long($this->end_ip);
if ($start === false || $end === false || $end < $start) {
return 0;
}
return (int) ($end - $start + 1);
}
public function usedIpsCount(): int
{
$query = Customer::query()
->where('ip_pool_id', $this->id)
->whereIn('status', ['pending', 'active']);
if ($this->type === IpPoolType::Public) {
return $query->whereNotNull('public_ip')->count();
}
return $query->whereNotNull('ip_address')->count();
}
public function freeIpsCount(): int
{
return max(0, $this->totalIps() - $this->usedIpsCount());
}
public function containsIp(string $ip): bool
{
$long = ip2long($ip);
$start = ip2long($this->start_ip);
$end = ip2long($this->end_ip);
return $long !== false && $long >= $start && $long <= $end;
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Cache;
class SystemSetting extends Model
{
protected $primaryKey = 'key';
public $incrementing = false;
protected $keyType = 'string';
protected $fillable = ['key', 'value'];
public static function get(string $key, mixed $default = null): mixed
{
return Cache::remember("setting.{$key}", 300, function () use ($key, $default) {
$row = static::query()->find($key);
return $row?->value ?? $default;
});
}
public static function set(string $key, mixed $value): void
{
static::query()->updateOrCreate(['key' => $key], ['value' => (string) $value]);
Cache::forget("setting.{$key}");
}
}

46
app/Models/User.php Normal file
View File

@@ -0,0 +1,46 @@
<?php
namespace App\Models;
use App\Enums\UserRole;
use Database\Factories\UserFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\Hidden;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
#[Fillable(['name', 'email', 'password', 'role', 'is_active', 'two_factor_secret', 'two_factor_recovery_codes', 'two_factor_confirmed_at'])]
#[Hidden(['password', 'remember_token', 'two_factor_secret', 'two_factor_recovery_codes'])]
class User extends Authenticatable
{
/** @use HasFactory<UserFactory> */
use HasFactory, Notifiable;
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
'role' => UserRole::class,
'is_active' => 'boolean',
'two_factor_confirmed_at' => 'datetime',
];
}
public function vms(): HasMany
{
return $this->hasMany(Customer::class, 'user_id');
}
public function isAdmin(): bool
{
return $this->role === UserRole::Admin;
}
public function isCustomer(): bool
{
return $this->role === UserRole::Customer;
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class VmActivityLog extends Model
{
protected $fillable = [
'customer_id',
'user_id',
'action',
'status',
'message',
'meta',
];
protected function casts(): array
{
return [
'meta' => 'array',
];
}
public function vm(): BelongsTo
{
return $this->belongsTo(Customer::class, 'customer_id');
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}

26
app/Models/VmBackup.php Normal file
View File

@@ -0,0 +1,26 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class VmBackup extends Model
{
protected $fillable = [
'customer_id', 'user_id', 'storage', 'volume_id', 'status', 'size_bytes', 'completed_at',
];
protected function casts(): array
{
return [
'size_bytes' => 'integer',
'completed_at' => 'datetime',
];
}
public function vm(): BelongsTo
{
return $this->belongsTo(Customer::class, 'customer_id');
}
}

58
app/Models/VmDevice.php Normal file
View File

@@ -0,0 +1,58 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class VmDevice extends Model
{
public const TYPE_DISK = 'disk';
public const TYPE_NETWORK = 'network';
public const TYPE_USB = 'usb';
public const TYPE_PCI = 'pci';
public static function types(): array
{
return [
self::TYPE_DISK => 'Zusätzliche Festplatte',
self::TYPE_NETWORK => 'Netzwerk-Interface',
self::TYPE_USB => 'USB-Gerät',
self::TYPE_PCI => 'PCI Passthrough',
];
}
public static function typesFor(?\App\Models\User $user = null): array
{
$types = self::types();
if ($user && ! $user->isAdmin()) {
return array_intersect_key($types, array_flip([self::TYPE_DISK, self::TYPE_NETWORK]));
}
return $types;
}
protected $fillable = [
'customer_id',
'type',
'slot',
'config',
'sort_order',
];
protected function casts(): array
{
return [
'config' => 'array',
'sort_order' => 'integer',
];
}
public function vm(): BelongsTo
{
return $this->belongsTo(Customer::class, 'customer_id');
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class VmFirewallRule extends Model
{
protected $fillable = [
'customer_id', 'direction', 'action', 'protocol', 'port', 'source', 'is_active', 'sort_order',
];
protected function casts(): array
{
return [
'is_active' => 'boolean',
'sort_order' => 'integer',
];
}
public function vm(): BelongsTo
{
return $this->belongsTo(Customer::class, 'customer_id');
}
}

32
app/Models/VmMetric.php Normal file
View File

@@ -0,0 +1,32 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class VmMetric extends Model
{
public $timestamps = false;
protected $fillable = [
'customer_id', 'cpu', 'mem', 'maxmem', 'disk', 'maxdisk', 'recorded_at',
];
protected function casts(): array
{
return [
'cpu' => 'float',
'mem' => 'integer',
'maxmem' => 'integer',
'disk' => 'integer',
'maxdisk' => 'integer',
'recorded_at' => 'datetime',
];
}
public function vm(): BelongsTo
{
return $this->belongsTo(Customer::class, 'customer_id');
}
}

26
app/Models/VmSnapshot.php Normal file
View File

@@ -0,0 +1,26 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class VmSnapshot extends Model
{
protected $fillable = [
'customer_id', 'name', 'proxmox_snapshot_id', 'auto_created', 'expires_at',
];
protected function casts(): array
{
return [
'auto_created' => 'boolean',
'expires_at' => 'datetime',
];
}
public function vm(): BelongsTo
{
return $this->belongsTo(Customer::class, 'customer_id');
}
}

20
app/Models/VmTemplate.php Normal file
View File

@@ -0,0 +1,20 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class VmTemplate extends Model
{
protected $fillable = [
'slug', 'name', 'proxmox_template_vmid', 'os_family', 'is_active',
];
protected function casts(): array
{
return [
'proxmox_template_vmid' => 'integer',
'is_active' => 'boolean',
];
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class VmidReservation extends Model
{
protected $fillable = [
'vmid',
'customer_id',
'status',
'release_at',
'released_at',
];
protected function casts(): array
{
return [
'vmid' => 'integer',
'release_at' => 'datetime',
'released_at' => 'datetime',
];
}
public function customer(): BelongsTo
{
return $this->belongsTo(Customer::class);
}
public function isBlocking(int $vmid): bool
{
return in_array($this->status, ['reserved', 'active', 'pending_release'], true);
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class WhmcsService extends Model
{
protected $fillable = [
'whmcs_service_id',
'whmcs_client_id',
'whmcs_order_id',
'customer_id',
'user_id',
'hosting_plan_id',
'status',
'config',
];
protected function casts(): array
{
return [
'whmcs_service_id' => 'integer',
'whmcs_client_id' => 'integer',
'whmcs_order_id' => 'integer',
'config' => 'array',
];
}
public function customer(): BelongsTo
{
return $this->belongsTo(Customer::class);
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function plan(): BelongsTo
{
return $this->belongsTo(HostingPlan::class, 'hosting_plan_id');
}
}