Migrate all primary keys + foreign keys to UUID (HasUuids trait)

Per user request: switch from auto-increment IDs to UUID v4 across all
models.

Models — added HasUuids trait:
- User, Cluster, Server, Site, Backup, TeamMember, Invitation,
  SecurityEvent, Certificate, Activity, Cron.

Migrations — $table->id() → $table->uuid('id')->primary(),
$table->foreignId(...) → $table->foreignUuid(...) across stock laravel
users/sessions/jobs migrations + all 10 domain migrations.

Routes: sites/{site} (and any other implicit route-model binding) keeps
working since Laravel resolves by primary key as a string. Tested:
clicking a site row → URL now /sites/019e800c-520a-7022-badd-2ef9d9ed5c61.

Re-seeded: 6 users, 87 sites, 6 servers, 128 security events, 30
activities, 102 backups, 47 crons, 87 certs, 5 team members, 3 invites.

Full Pest regression: 47/47 green. Browser-verified login + sites
index + site details + WP/PHP/Files tabs with template-exact chrome.
master
Boban Blaskovic 2026-06-01 00:00:44 +02:00
parent 933f641e58
commit 29622eff77
23 changed files with 45 additions and 34 deletions

View File

@ -2,13 +2,14 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Activity extends Model class Activity extends Model
{ {
use HasFactory; use HasFactory, HasUuids;
protected $fillable = [ protected $fillable = [
'user_id', 'type', 'level', 'user_id', 'type', 'level',

View File

@ -2,13 +2,14 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Backup extends Model class Backup extends Model
{ {
use HasFactory; use HasFactory, HasUuids;
protected $fillable = [ protected $fillable = [
'site_id', 'type', 'size_bytes', 'status', 'site_id', 'type', 'size_bytes', 'status',

View File

@ -2,13 +2,14 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Certificate extends Model class Certificate extends Model
{ {
use HasFactory; use HasFactory, HasUuids;
protected $fillable = [ protected $fillable = [
'site_id', 'domain', 'issuer', 'site_id', 'domain', 'issuer',

View File

@ -2,13 +2,14 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
class Cluster extends Model class Cluster extends Model
{ {
use HasFactory; use HasFactory, HasUuids;
protected $fillable = [ protected $fillable = [
'name', 'slug', 'region', 'env', 'monthly_price_cents', 'name', 'slug', 'region', 'env', 'monthly_price_cents',

View File

@ -2,13 +2,14 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Cron extends Model class Cron extends Model
{ {
use HasFactory; use HasFactory, HasUuids;
protected $fillable = [ protected $fillable = [
'site_id', 'name', 'expression', 'command', 'site_id', 'name', 'expression', 'command',

View File

@ -2,6 +2,7 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -9,7 +10,7 @@ use Illuminate\Support\Str;
class Invitation extends Model class Invitation extends Model
{ {
use HasFactory; use HasFactory, HasUuids;
protected $fillable = [ protected $fillable = [
'email', 'role', 'invited_by_user_id', 'email', 'role', 'invited_by_user_id',

View File

@ -2,13 +2,14 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
class SecurityEvent extends Model class SecurityEvent extends Model
{ {
use HasFactory; use HasFactory, HasUuids;
protected $fillable = [ protected $fillable = [
'site_id', 'type', 'severity', 'blocked', 'site_id', 'type', 'severity', 'blocked',

View File

@ -2,6 +2,7 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -9,7 +10,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
class Server extends Model class Server extends Model
{ {
use HasFactory; use HasFactory, HasUuids;
protected $fillable = [ protected $fillable = [
'cluster_id', 'name', 'region', 'country_flag', 'env', 'cluster_id', 'name', 'region', 'country_flag', 'env',

View File

@ -2,6 +2,7 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -10,7 +11,7 @@ use Illuminate\Database\Eloquent\Relations\HasOne;
class Site extends Model class Site extends Model
{ {
use HasFactory; use HasFactory, HasUuids;
protected $fillable = [ protected $fillable = [
'server_id', 'domain', 'name', 'server_id', 'domain', 'name',

View File

@ -2,13 +2,14 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
class TeamMember extends Model class TeamMember extends Model
{ {
use HasFactory; use HasFactory, HasUuids;
protected $fillable = [ protected $fillable = [
'user_id', 'role', 'scope', 'two_fa_enabled', 'last_active_at', 'user_id', 'role', 'scope', 'two_fa_enabled', 'last_active_at',

View File

@ -4,6 +4,7 @@ namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail; // use Illuminate\Contracts\Auth\MustVerifyEmail;
use Database\Factories\UserFactory; use Database\Factories\UserFactory;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable; use Illuminate\Notifications\Notifiable;
@ -11,7 +12,7 @@ use Illuminate\Notifications\Notifiable;
class User extends Authenticatable class User extends Authenticatable
{ {
/** @use HasFactory<UserFactory> */ /** @use HasFactory<UserFactory> */
use HasFactory, Notifiable; use HasFactory, HasUuids, Notifiable;
/** /**
* The attributes that are mass assignable. * The attributes that are mass assignable.

View File

@ -12,7 +12,7 @@ return new class extends Migration
public function up(): void public function up(): void
{ {
Schema::create('users', function (Blueprint $table) { Schema::create('users', function (Blueprint $table) {
$table->id(); $table->uuid('id')->primary();
$table->string('name'); $table->string('name');
$table->string('email')->unique(); $table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable(); $table->timestamp('email_verified_at')->nullable();
@ -29,7 +29,7 @@ return new class extends Migration
Schema::create('sessions', function (Blueprint $table) { Schema::create('sessions', function (Blueprint $table) {
$table->string('id')->primary(); $table->string('id')->primary();
$table->foreignId('user_id')->nullable()->index(); $table->foreignUuid('user_id')->nullable()->index();
$table->string('ip_address', 45)->nullable(); $table->string('ip_address', 45)->nullable();
$table->text('user_agent')->nullable(); $table->text('user_agent')->nullable();
$table->longText('payload'); $table->longText('payload');

View File

@ -12,7 +12,7 @@ return new class extends Migration
public function up(): void public function up(): void
{ {
Schema::create('jobs', function (Blueprint $table) { Schema::create('jobs', function (Blueprint $table) {
$table->id(); $table->uuid('id')->primary();
$table->string('queue')->index(); $table->string('queue')->index();
$table->longText('payload'); $table->longText('payload');
$table->unsignedTinyInteger('attempts'); $table->unsignedTinyInteger('attempts');
@ -35,7 +35,7 @@ return new class extends Migration
}); });
Schema::create('failed_jobs', function (Blueprint $table) { Schema::create('failed_jobs', function (Blueprint $table) {
$table->id(); $table->uuid('id')->primary();
$table->string('uuid')->unique(); $table->string('uuid')->unique();
$table->text('connection'); $table->text('connection');
$table->text('queue'); $table->text('queue');

View File

@ -8,7 +8,7 @@ return new class extends Migration {
public function up(): void public function up(): void
{ {
Schema::create('clusters', function (Blueprint $table) { Schema::create('clusters', function (Blueprint $table) {
$table->id(); $table->uuid('id')->primary();
$table->string('name'); $table->string('name');
$table->string('slug')->unique(); $table->string('slug')->unique();
$table->string('region', 32); // e.g. eu-central-1 $table->string('region', 32); // e.g. eu-central-1

View File

@ -8,8 +8,8 @@ return new class extends Migration {
public function up(): void public function up(): void
{ {
Schema::create('servers', function (Blueprint $table) { Schema::create('servers', function (Blueprint $table) {
$table->id(); $table->uuid('id')->primary();
$table->foreignId('cluster_id')->nullable()->constrained()->nullOnDelete(); $table->foreignUuid('cluster_id')->nullable()->constrained()->nullOnDelete();
$table->string('name', 64); // eu-prod-01 $table->string('name', 64); // eu-prod-01
$table->string('region', 32); // eu-central-1 $table->string('region', 32); // eu-central-1
$table->string('country_flag', 4)->default('🇩🇪'); // emoji $table->string('country_flag', 4)->default('🇩🇪'); // emoji

View File

@ -8,8 +8,8 @@ return new class extends Migration {
public function up(): void public function up(): void
{ {
Schema::create('sites', function (Blueprint $table) { Schema::create('sites', function (Blueprint $table) {
$table->id(); $table->uuid('id')->primary();
$table->foreignId('server_id')->constrained()->cascadeOnDelete(); $table->foreignUuid('server_id')->constrained()->cascadeOnDelete();
$table->string('domain')->unique(); $table->string('domain')->unique();
$table->string('name')->nullable(); $table->string('name')->nullable();
$table->string('wp_version', 16)->default('6.4'); $table->string('wp_version', 16)->default('6.4');

View File

@ -8,8 +8,8 @@ return new class extends Migration {
public function up(): void public function up(): void
{ {
Schema::create('backups', function (Blueprint $table) { Schema::create('backups', function (Blueprint $table) {
$table->id(); $table->uuid('id')->primary();
$table->foreignId('site_id')->constrained()->cascadeOnDelete(); $table->foreignUuid('site_id')->constrained()->cascadeOnDelete();
$table->enum('type', ['hourly', 'daily', 'manual', 'snapshot'])->default('daily'); $table->enum('type', ['hourly', 'daily', 'manual', 'snapshot'])->default('daily');
$table->unsignedBigInteger('size_bytes')->default(0); $table->unsignedBigInteger('size_bytes')->default(0);
$table->enum('status', ['queued', 'running', 'ok', 'failed'])->default('queued'); $table->enum('status', ['queued', 'running', 'ok', 'failed'])->default('queued');

View File

@ -8,8 +8,8 @@ return new class extends Migration {
public function up(): void public function up(): void
{ {
Schema::create('team_members', function (Blueprint $table) { Schema::create('team_members', function (Blueprint $table) {
$table->id(); $table->uuid('id')->primary();
$table->foreignId('user_id')->constrained()->cascadeOnDelete(); $table->foreignUuid('user_id')->constrained()->cascadeOnDelete();
$table->enum('role', ['owner', 'admin', 'developer', 'viewer'])->default('viewer'); $table->enum('role', ['owner', 'admin', 'developer', 'viewer'])->default('viewer');
$table->string('scope')->nullable(); // e.g. "All clusters", "eu-prod-*" $table->string('scope')->nullable(); // e.g. "All clusters", "eu-prod-*"
$table->boolean('two_fa_enabled')->default(false); $table->boolean('two_fa_enabled')->default(false);

View File

@ -8,10 +8,10 @@ return new class extends Migration {
public function up(): void public function up(): void
{ {
Schema::create('invitations', function (Blueprint $table) { Schema::create('invitations', function (Blueprint $table) {
$table->id(); $table->uuid('id')->primary();
$table->string('email'); $table->string('email');
$table->enum('role', ['admin', 'developer', 'viewer'])->default('developer'); $table->enum('role', ['admin', 'developer', 'viewer'])->default('developer');
$table->foreignId('invited_by_user_id')->nullable()->constrained('users')->nullOnDelete(); $table->foreignUuid('invited_by_user_id')->nullable()->constrained('users')->nullOnDelete();
$table->string('token', 64)->unique(); $table->string('token', 64)->unique();
$table->timestamp('sent_at')->nullable(); $table->timestamp('sent_at')->nullable();
$table->timestamp('accepted_at')->nullable(); $table->timestamp('accepted_at')->nullable();

View File

@ -8,8 +8,8 @@ return new class extends Migration {
public function up(): void public function up(): void
{ {
Schema::create('security_events', function (Blueprint $table) { Schema::create('security_events', function (Blueprint $table) {
$table->id(); $table->uuid('id')->primary();
$table->foreignId('site_id')->nullable()->constrained()->cascadeOnDelete(); $table->foreignUuid('site_id')->nullable()->constrained()->cascadeOnDelete();
$table->enum('type', ['brute_force', 'tor_exit', 'malware', 'cert_expiring', 'firewall', 'scan']) $table->enum('type', ['brute_force', 'tor_exit', 'malware', 'cert_expiring', 'firewall', 'scan'])
->default('firewall'); ->default('firewall');
$table->enum('severity', ['info', 'low', 'medium', 'high', 'critical'])->default('low'); $table->enum('severity', ['info', 'low', 'medium', 'high', 'critical'])->default('low');

View File

@ -8,8 +8,8 @@ return new class extends Migration {
public function up(): void public function up(): void
{ {
Schema::create('certificates', function (Blueprint $table) { Schema::create('certificates', function (Blueprint $table) {
$table->id(); $table->uuid('id')->primary();
$table->foreignId('site_id')->constrained()->cascadeOnDelete(); $table->foreignUuid('site_id')->constrained()->cascadeOnDelete();
$table->string('domain'); $table->string('domain');
$table->string('issuer', 64)->default("Let's Encrypt"); $table->string('issuer', 64)->default("Let's Encrypt");
$table->timestamp('issued_at')->nullable(); $table->timestamp('issued_at')->nullable();

View File

@ -8,8 +8,8 @@ return new class extends Migration {
public function up(): void public function up(): void
{ {
Schema::create('activities', function (Blueprint $table) { Schema::create('activities', function (Blueprint $table) {
$table->id(); $table->uuid('id')->primary();
$table->foreignId('user_id')->nullable()->constrained()->nullOnDelete(); $table->foreignUuid('user_id')->nullable()->constrained()->nullOnDelete();
$table->enum('type', ['deploy', 'backup', 'update', 'security', 'system', 'login'])->default('system'); $table->enum('type', ['deploy', 'backup', 'update', 'security', 'system', 'login'])->default('system');
$table->enum('level', ['info', 'success', 'warning', 'error'])->default('info'); $table->enum('level', ['info', 'success', 'warning', 'error'])->default('info');
$table->string('message'); $table->string('message');

View File

@ -8,8 +8,8 @@ return new class extends Migration {
public function up(): void public function up(): void
{ {
Schema::create('crons', function (Blueprint $table) { Schema::create('crons', function (Blueprint $table) {
$table->id(); $table->uuid('id')->primary();
$table->foreignId('site_id')->constrained()->cascadeOnDelete(); $table->foreignUuid('site_id')->constrained()->cascadeOnDelete();
$table->string('name'); $table->string('name');
$table->string('expression', 64)->default('0 * * * *'); // cron expr $table->string('expression', 64)->default('0 * * * *'); // cron expr
$table->string('command'); $table->string('command');