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;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Activity extends Model
{
use HasFactory;
use HasFactory, HasUuids;
protected $fillable = [
'user_id', 'type', 'level',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,13 +2,14 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class TeamMember extends Model
{
use HasFactory;
use HasFactory, HasUuids;
protected $fillable = [
'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 Database\Factories\UserFactory;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
@ -11,7 +12,7 @@ use Illuminate\Notifications\Notifiable;
class User extends Authenticatable
{
/** @use HasFactory<UserFactory> */
use HasFactory, Notifiable;
use HasFactory, HasUuids, Notifiable;
/**
* The attributes that are mass assignable.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,10 +8,10 @@ return new class extends Migration {
public function up(): void
{
Schema::create('invitations', function (Blueprint $table) {
$table->id();
$table->uuid('id')->primary();
$table->string('email');
$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->timestamp('sent_at')->nullable();
$table->timestamp('accepted_at')->nullable();

View File

@ -8,8 +8,8 @@ return new class extends Migration {
public function up(): void
{
Schema::create('security_events', function (Blueprint $table) {
$table->id();
$table->foreignId('site_id')->nullable()->constrained()->cascadeOnDelete();
$table->uuid('id')->primary();
$table->foreignUuid('site_id')->nullable()->constrained()->cascadeOnDelete();
$table->enum('type', ['brute_force', 'tor_exit', 'malware', 'cert_expiring', 'firewall', 'scan'])
->default('firewall');
$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
{
Schema::create('certificates', function (Blueprint $table) {
$table->id();
$table->foreignId('site_id')->constrained()->cascadeOnDelete();
$table->uuid('id')->primary();
$table->foreignUuid('site_id')->constrained()->cascadeOnDelete();
$table->string('domain');
$table->string('issuer', 64)->default("Let's Encrypt");
$table->timestamp('issued_at')->nullable();

View File

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

View File

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