Initial commit: CABM-IT (ItGlue-style MSP documentation platform)

Laravel 13 + Livewire/Volt app for managing client organizations,
documents, passwords, flexible assets, virtual machines, contacts,
domains, todos, shortcuts, and role-based user administration.
This commit is contained in:
jeremy bayse
2026-08-16 13:28:12 +02:00
commit 8f22dca9c8
209 changed files with 23324 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,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');
}
};
@@ -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');
}
};
@@ -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');
}
};
@@ -0,0 +1,27 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateActivityLogTable extends Migration
{
public function up()
{
Schema::connection(config('activitylog.database_connection'))->create(config('activitylog.table_name'), function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('log_name')->nullable();
$table->text('description');
$table->nullableMorphs('subject', 'subject');
$table->nullableMorphs('causer', 'causer');
$table->json('properties')->nullable();
$table->timestamps();
$table->index('log_name');
});
}
public function down()
{
Schema::connection(config('activitylog.database_connection'))->dropIfExists(config('activitylog.table_name'));
}
}
@@ -0,0 +1,137 @@
<?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
{
$teams = config('permission.teams');
$tableNames = config('permission.table_names');
$columnNames = config('permission.column_names');
$pivotRole = $columnNames['role_pivot_key'] ?? 'role_id';
$pivotPermission = $columnNames['permission_pivot_key'] ?? 'permission_id';
throw_if(empty($tableNames), 'Error: config/permission.php not loaded. Run [php artisan config:clear] and try again.');
throw_if($teams && empty($columnNames['team_foreign_key'] ?? null), 'Error: team_foreign_key on config/permission.php not loaded. Run [php artisan config:clear] and try again.');
/**
* See `docs/prerequisites.md` for suggested lengths on 'name' and 'guard_name' if "1071 Specified key was too long" errors are encountered.
*/
Schema::create($tableNames['permissions'], static function (Blueprint $table) {
$table->id(); // permission id
$table->string('name');
$table->string('guard_name');
$table->timestamps();
$table->unique(['name', 'guard_name']);
});
/**
* See `docs/prerequisites.md` for suggested lengths on 'name' and 'guard_name' if "1071 Specified key was too long" errors are encountered.
*/
Schema::create($tableNames['roles'], static function (Blueprint $table) use ($teams, $columnNames) {
$table->id(); // role id
if ($teams || config('permission.testing')) { // permission.testing is a fix for sqlite testing
$table->unsignedBigInteger($columnNames['team_foreign_key'])->nullable();
$table->index($columnNames['team_foreign_key'], 'roles_team_foreign_key_index');
}
$table->string('name');
$table->string('guard_name');
$table->timestamps();
if ($teams || config('permission.testing')) {
$table->unique([$columnNames['team_foreign_key'], 'name', 'guard_name']);
} else {
$table->unique(['name', 'guard_name']);
}
});
Schema::create($tableNames['model_has_permissions'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotPermission, $teams) {
$table->unsignedBigInteger($pivotPermission);
$table->string('model_type');
$table->unsignedBigInteger($columnNames['model_morph_key']);
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_model_id_model_type_index');
$table->foreign($pivotPermission)
->references('id') // permission id
->on($tableNames['permissions'])
->cascadeOnDelete();
if ($teams) {
$table->unsignedBigInteger($columnNames['team_foreign_key']);
$table->index($columnNames['team_foreign_key'], 'model_has_permissions_team_foreign_key_index');
$table->primary([$columnNames['team_foreign_key'], $pivotPermission, $columnNames['model_morph_key'], 'model_type'],
'model_has_permissions_permission_model_type_primary');
} else {
$table->primary([$pivotPermission, $columnNames['model_morph_key'], 'model_type'],
'model_has_permissions_permission_model_type_primary');
}
});
Schema::create($tableNames['model_has_roles'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotRole, $teams) {
$table->unsignedBigInteger($pivotRole);
$table->string('model_type');
$table->unsignedBigInteger($columnNames['model_morph_key']);
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_roles_model_id_model_type_index');
$table->foreign($pivotRole)
->references('id') // role id
->on($tableNames['roles'])
->cascadeOnDelete();
if ($teams) {
$table->unsignedBigInteger($columnNames['team_foreign_key']);
$table->index($columnNames['team_foreign_key'], 'model_has_roles_team_foreign_key_index');
$table->primary([$columnNames['team_foreign_key'], $pivotRole, $columnNames['model_morph_key'], 'model_type'],
'model_has_roles_role_model_type_primary');
} else {
$table->primary([$pivotRole, $columnNames['model_morph_key'], 'model_type'],
'model_has_roles_role_model_type_primary');
}
});
Schema::create($tableNames['role_has_permissions'], static function (Blueprint $table) use ($tableNames, $pivotRole, $pivotPermission) {
$table->unsignedBigInteger($pivotPermission);
$table->unsignedBigInteger($pivotRole);
$table->foreign($pivotPermission)
->references('id') // permission id
->on($tableNames['permissions'])
->cascadeOnDelete();
$table->foreign($pivotRole)
->references('id') // role id
->on($tableNames['roles'])
->cascadeOnDelete();
$table->primary([$pivotPermission, $pivotRole], 'role_has_permissions_permission_id_role_id_primary');
});
app('cache')
->store(config('permission.cache.store') != 'default' ? config('permission.cache.store') : null)
->forget(config('permission.cache.key'));
}
/**
* Reverse the migrations.
*/
public function down(): void
{
$tableNames = config('permission.table_names');
throw_if(empty($tableNames), 'Error: config/permission.php not found and defaults could not be merged. Please publish the package configuration before proceeding, or drop the tables manually.');
Schema::dropIfExists($tableNames['role_has_permissions']);
Schema::dropIfExists($tableNames['model_has_roles']);
Schema::dropIfExists($tableNames['model_has_permissions']);
Schema::dropIfExists($tableNames['roles']);
Schema::dropIfExists($tableNames['permissions']);
}
};
@@ -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,22 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddEventColumnToActivityLogTable extends Migration
{
public function up()
{
Schema::connection(config('activitylog.database_connection'))->table(config('activitylog.table_name'), function (Blueprint $table) {
$table->string('event')->nullable()->after('subject_type');
});
}
public function down()
{
Schema::connection(config('activitylog.database_connection'))->table(config('activitylog.table_name'), function (Blueprint $table) {
$table->dropColumn('event');
});
}
}
@@ -0,0 +1,22 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddBatchUuidColumnToActivityLogTable extends Migration
{
public function up()
{
Schema::connection(config('activitylog.database_connection'))->table(config('activitylog.table_name'), function (Blueprint $table) {
$table->uuid('batch_uuid')->nullable()->after('properties');
});
}
public function down()
{
Schema::connection(config('activitylog.database_connection'))->table(config('activitylog.table_name'), function (Blueprint $table) {
$table->dropColumn('batch_uuid');
});
}
}
@@ -0,0 +1,28 @@
<?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('organizations', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('slug')->unique();
$table->string('org_type')->nullable(); // client, internal, vendor
$table->string('status')->default('active'); // active, inactive
$table->text('description')->nullable();
$table->json('allowed_ip_ranges')->nullable(); // CIDR list for IP access control
$table->timestamps();
$table->softDeletes();
});
}
public function down(): void
{
Schema::dropIfExists('organizations');
}
};
@@ -0,0 +1,31 @@
<?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('locations', function (Blueprint $table) {
$table->id();
$table->foreignId('organization_id')->constrained()->cascadeOnDelete();
$table->string('name');
$table->string('address_line_1')->nullable();
$table->string('address_line_2')->nullable();
$table->string('city')->nullable();
$table->string('region')->nullable();
$table->string('postal_code')->nullable();
$table->string('country')->nullable();
$table->string('phone')->nullable();
$table->boolean('is_primary')->default(false);
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('locations');
}
};
@@ -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::create('configuration_types', function (Blueprint $table) {
$table->id();
$table->string('name'); // Workstation, Server, Network Device, Telephone...
$table->string('icon')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('configuration_types');
}
};
@@ -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::create('configuration_statuses', function (Blueprint $table) {
$table->id();
$table->string('name'); // Active, Inactive, Retired...
$table->string('color')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('configuration_statuses');
}
};
@@ -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
{
Schema::create('configurations', function (Blueprint $table) {
$table->id();
$table->foreignId('organization_id')->constrained()->cascadeOnDelete();
$table->foreignId('configuration_type_id')->constrained()->restrictOnDelete();
$table->foreignId('configuration_status_id')->nullable()->constrained()->nullOnDelete();
$table->foreignId('location_id')->nullable()->constrained()->nullOnDelete();
$table->foreignId('contact_id')->nullable()->constrained()->nullOnDelete();
$table->string('name');
$table->string('hostname')->nullable();
$table->string('primary_ip')->nullable();
$table->string('mac_address')->nullable();
$table->string('serial_number')->nullable();
$table->string('asset_tag')->nullable();
$table->string('manufacturer')->nullable();
$table->string('model')->nullable();
$table->string('operating_system')->nullable();
$table->date('installed_at')->nullable();
$table->date('warranty_expires_at')->nullable();
$table->text('notes')->nullable();
$table->boolean('is_archived')->default(false);
$table->timestamps();
$table->softDeletes();
$table->index(['organization_id', 'name']);
});
}
public function down(): void
{
Schema::dropIfExists('configurations');
}
};
@@ -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
{
Schema::create('contacts', function (Blueprint $table) {
$table->id();
$table->foreignId('organization_id')->constrained()->cascadeOnDelete();
$table->foreignId('location_id')->nullable()->constrained()->nullOnDelete();
$table->string('first_name');
$table->string('last_name');
$table->string('title')->nullable();
$table->string('email')->nullable();
$table->string('phone')->nullable();
$table->string('mobile')->nullable();
$table->boolean('is_important')->default(false);
$table->text('notes')->nullable();
$table->timestamps();
$table->softDeletes();
});
}
public function down(): void
{
Schema::dropIfExists('contacts');
}
};
@@ -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('passwords', function (Blueprint $table) {
$table->id();
$table->foreignId('organization_id')->constrained()->cascadeOnDelete();
// optional link to the asset this credential belongs to (polymorphic: configuration, flexible_asset...)
$table->nullableMorphs('resource');
$table->string('name');
$table->string('username')->nullable();
$table->text('password_ciphertext'); // AES-256-GCM envelope-encrypted payload
$table->string('url')->nullable();
$table->text('notes')->nullable();
$table->string('otp_secret')->nullable(); // for OTP generator feature, also encrypted at rest via cast
$table->boolean('is_favorite')->default(false);
$table->timestamp('password_updated_at')->nullable();
$table->timestamp('rotation_due_at')->nullable(); // for scheduled rotation
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
$table->timestamps();
$table->softDeletes();
$table->index(['organization_id', 'name']);
});
}
public function down(): void
{
Schema::dropIfExists('passwords');
}
};
@@ -0,0 +1,27 @@
<?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
{
// Append-only. No updated_at, no soft deletes, no update/delete performed by app code.
Schema::create('password_audit_logs', function (Blueprint $table) {
$table->id();
$table->foreignId('password_id')->constrained()->cascadeOnDelete();
$table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
$table->string('action'); // viewed, revealed, copied, created, updated, deleted, rotated
$table->string('ip_address', 45)->nullable();
$table->string('user_agent')->nullable();
$table->timestamp('created_at')->useCurrent();
});
}
public function down(): void
{
Schema::dropIfExists('password_audit_logs');
}
};
@@ -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
{
public function up(): void
{
Schema::create('flexible_asset_types', function (Blueprint $table) {
$table->id();
$table->string('name'); // e.g. "SSL Certificate", "Wireless Network", "Warranty"
$table->string('icon')->nullable();
$table->text('description')->nullable();
$table->boolean('enabled_for_all_organizations')->default(true);
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('flexible_asset_types');
}
};
@@ -0,0 +1,31 @@
<?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('flexible_asset_fields', function (Blueprint $table) {
$table->id();
$table->foreignId('flexible_asset_type_id')->constrained()->cascadeOnDelete();
$table->string('name'); // machine key, e.g. "ip_address"
$table->string('label'); // display label, e.g. "IP Address"
$table->string('field_type'); // text, textarea, password, checkbox, date, tag, select, related_asset
$table->json('options')->nullable(); // select choices, related_asset target type, etc.
$table->boolean('is_required')->default(false);
$table->boolean('show_in_list')->default(false);
$table->unsignedInteger('sort_order')->default(0);
$table->timestamps();
$table->unique(['flexible_asset_type_id', 'name']);
});
}
public function down(): void
{
Schema::dropIfExists('flexible_asset_fields');
}
};
@@ -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
{
public function up(): void
{
Schema::create('flexible_assets', function (Blueprint $table) {
$table->id();
$table->foreignId('flexible_asset_type_id')->constrained()->restrictOnDelete();
$table->foreignId('organization_id')->constrained()->cascadeOnDelete();
$table->string('name'); // primary display value, denormalized from data for listing/search
$table->json('data'); // { field_name: value, ... } validated against flexible_asset_fields
$table->boolean('is_archived')->default(false);
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
$table->timestamps();
$table->softDeletes();
$table->index(['organization_id', 'flexible_asset_type_id']);
});
}
public function down(): void
{
Schema::dropIfExists('flexible_assets');
}
};
@@ -0,0 +1,28 @@
<?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
{
// Directional edge; UI queries both directions to show a symmetric "Related Items" panel.
Schema::create('related_items', function (Blueprint $table) {
$table->id();
$table->morphs('item'); // owning side
$table->morphs('related_item'); // linked side
$table->string('notes')->nullable();
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
$table->timestamps();
$table->unique(['item_type', 'item_id', 'related_item_type', 'related_item_id'], 'related_items_unique');
});
}
public function down(): void
{
Schema::dropIfExists('related_items');
}
};
@@ -0,0 +1,43 @@
<?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('documents', function (Blueprint $table) {
$table->id();
$table->foreignId('organization_id')->constrained()->cascadeOnDelete();
$table->foreignId('folder_id')->nullable(); // self-referencing folder tree, FK added after table exists
$table->string('name');
$table->longText('content')->nullable(); // rich text body (Tiptap JSON/HTML)
$table->string('file_path')->nullable(); // for uploaded (non rich-text) documents
$table->unsignedInteger('current_version')->default(1);
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
$table->timestamps();
$table->softDeletes();
});
Schema::table('documents', function (Blueprint $table) {
$table->foreign('folder_id')->references('id')->on('documents')->nullOnDelete();
});
Schema::create('document_versions', function (Blueprint $table) {
$table->id();
$table->foreignId('document_id')->constrained()->cascadeOnDelete();
$table->unsignedInteger('version');
$table->longText('content')->nullable();
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
$table->timestamp('created_at')->useCurrent();
});
}
public function down(): void
{
Schema::dropIfExists('document_versions');
Schema::dropIfExists('documents');
}
};
@@ -0,0 +1,27 @@
<?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('domains', function (Blueprint $table) {
$table->id();
$table->foreignId('organization_id')->constrained()->cascadeOnDelete();
$table->string('name')->unique(); // domain name
$table->string('registrar')->nullable();
$table->date('expires_at')->nullable();
$table->boolean('auto_renew')->default(false);
$table->timestamp('last_checked_at')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('domains');
}
};
@@ -0,0 +1,28 @@
<?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('ssl_certificates', function (Blueprint $table) {
$table->id();
$table->foreignId('organization_id')->constrained()->cascadeOnDelete();
$table->foreignId('domain_id')->nullable()->constrained()->nullOnDelete();
$table->string('name'); // hostname/CN
$table->string('issuer')->nullable();
$table->date('issued_at')->nullable();
$table->date('expires_at')->nullable();
$table->timestamp('last_checked_at')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('ssl_certificates');
}
};
@@ -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::table('users', function (Blueprint $table) {
// MSP staff account; null = client-portal user scoped only via organization_user pivot
$table->boolean('is_msp_staff')->default(true)->after('password');
// two_factor_* columns owned by Fortify's own migration (add_two_factor_columns_to_users_table)
$table->timestamp('last_login_at')->nullable()->after('is_msp_staff');
$table->string('last_login_ip', 45)->nullable()->after('last_login_at');
});
Schema::create('organization_user', function (Blueprint $table) {
$table->id();
$table->foreignId('organization_id')->constrained()->cascadeOnDelete();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->timestamps();
$table->unique(['organization_id', 'user_id']);
});
}
public function down(): void
{
Schema::dropIfExists('organization_user');
Schema::table('users', function (Blueprint $table) {
$table->dropColumn(['is_msp_staff', 'last_login_at', 'last_login_ip']);
});
}
};
@@ -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
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->text('two_factor_secret')
->after('password')
->nullable();
$table->text('two_factor_recovery_codes')
->after('two_factor_secret')
->nullable();
$table->timestamp('two_factor_confirmed_at')
->after('two_factor_recovery_codes')
->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn([
'two_factor_secret',
'two_factor_recovery_codes',
'two_factor_confirmed_at',
]);
});
}
};
@@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Laravel\Passkeys\Passkeys;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('passkeys', function (Blueprint $table) {
$table->id();
$table->foreignIdFor(Passkeys::userModel(), 'user_id')->constrained()->cascadeOnDelete();
$table->string('name');
$table->string('credential_id')->unique();
$table->json('credential');
$table->timestamp('last_used_at')->nullable();
$table->timestamps();
$table->index('user_id');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('passkeys');
}
};
@@ -0,0 +1,24 @@
<?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('documents', function (Blueprint $table) {
$table->string('type')->default('text')->after('name'); // text|office
$table->string('office_extension')->nullable()->after('file_path'); // docx|xlsx|pptx
$table->string('office_key')->nullable()->after('office_extension'); // OnlyOffice cache-busting version key
});
}
public function down(): void
{
Schema::table('documents', function (Blueprint $table) {
$table->dropColumn(['type', 'office_extension', 'office_key']);
});
}
};
@@ -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('domains', function (Blueprint $table) {
$table->text('nameservers')->nullable()->after('registrar'); // one per line
$table->text('notes')->nullable()->after('last_checked_at');
});
}
public function down(): void
{
Schema::table('domains', function (Blueprint $table) {
$table->dropColumn(['nameservers', 'notes']);
});
}
};
@@ -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('virtual_machines', function (Blueprint $table) {
$table->id();
$table->foreignId('organization_id')->constrained()->cascadeOnDelete();
$table->string('name');
$table->string('role')->nullable();
$table->unsignedInteger('memory_gb')->nullable();
$table->unsignedInteger('cpu_count')->nullable();
$table->string('operating_system')->nullable();
$table->text('notes')->nullable();
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
$table->timestamps();
$table->softDeletes();
});
Schema::create('virtual_machine_logs', function (Blueprint $table) {
$table->id();
$table->foreignId('virtual_machine_id')->constrained()->cascadeOnDelete();
$table->text('note');
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
$table->timestamp('created_at')->useCurrent();
});
}
public function down(): void
{
Schema::dropIfExists('virtual_machine_logs');
Schema::dropIfExists('virtual_machines');
}
};
@@ -0,0 +1,24 @@
<?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('flexible_asset_logs', function (Blueprint $table) {
$table->id();
$table->foreignId('flexible_asset_id')->constrained()->cascadeOnDelete();
$table->text('note');
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
$table->timestamp('created_at')->useCurrent();
});
}
public function down(): void
{
Schema::dropIfExists('flexible_asset_logs');
}
};
@@ -0,0 +1,28 @@
<?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('documents', function (Blueprint $table) {
$table->boolean('is_folder')->default(false)->after('type');
});
DB::table('documents')
->whereNull('content')
->whereNull('file_path')
->update(['is_folder' => true]);
}
public function down(): void
{
Schema::table('documents', function (Blueprint $table) {
$table->dropColumn('is_folder');
});
}
};
@@ -0,0 +1,66 @@
<?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
{
// No-op when the base permission tables were already created without the
// team column (e.g. a fresh migrate with permission.teams already false).
if (! Schema::hasColumn('roles', 'organization_id')) {
return;
}
Schema::table('roles', function (Blueprint $table) {
$table->dropUnique(['organization_id', 'name', 'guard_name']);
$table->dropIndex('roles_team_foreign_key_index');
$table->dropColumn('organization_id');
$table->unique(['name', 'guard_name']);
});
Schema::table('model_has_permissions', function (Blueprint $table) {
$table->dropPrimary('model_has_permissions_permission_model_type_primary');
$table->dropIndex('model_has_permissions_team_foreign_key_index');
$table->dropColumn('organization_id');
$table->primary(['permission_id', 'model_id', 'model_type'], 'model_has_permissions_permission_model_type_primary');
});
Schema::table('model_has_roles', function (Blueprint $table) {
$table->dropPrimary('model_has_roles_role_model_type_primary');
$table->dropIndex('model_has_roles_team_foreign_key_index');
$table->dropColumn('organization_id');
$table->primary(['role_id', 'model_id', 'model_type'], 'model_has_roles_role_model_type_primary');
});
}
public function down(): void
{
if (Schema::hasColumn('roles', 'organization_id')) {
return;
}
Schema::table('roles', function (Blueprint $table) {
$table->dropUnique(['name', 'guard_name']);
$table->unsignedBigInteger('organization_id')->nullable();
$table->index('organization_id', 'roles_team_foreign_key_index');
$table->unique(['organization_id', 'name', 'guard_name']);
});
Schema::table('model_has_permissions', function (Blueprint $table) {
$table->dropPrimary('model_has_permissions_permission_model_type_primary');
$table->unsignedBigInteger('organization_id');
$table->index('organization_id', 'model_has_permissions_team_foreign_key_index');
$table->primary(['organization_id', 'permission_id', 'model_id', 'model_type'], 'model_has_permissions_permission_model_type_primary');
});
Schema::table('model_has_roles', function (Blueprint $table) {
$table->dropPrimary('model_has_roles_role_model_type_primary');
$table->unsignedBigInteger('organization_id');
$table->index('organization_id', 'model_has_roles_team_foreign_key_index');
$table->primary(['organization_id', 'role_id', 'model_id', 'model_type'], 'model_has_roles_role_model_type_primary');
});
}
};
@@ -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
{
public function up(): void
{
Schema::create('organization_log_entries', function (Blueprint $table) {
$table->id();
$table->foreignId('organization_id')->constrained()->cascadeOnDelete();
$table->string('category');
$table->text('note');
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
$table->timestamp('created_at')->useCurrent();
});
}
public function down(): void
{
Schema::dropIfExists('organization_log_entries');
}
};
@@ -0,0 +1,58 @@
<?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
{
// "Configuration" duplicated the "FlexibleAsset" system (a generic,
// admin-configurable IT inventory) with a fixed set of fields — merged
// into flexible asset types instead of keeping two parallel models.
Schema::dropIfExists('configurations');
Schema::dropIfExists('configuration_statuses');
Schema::dropIfExists('configuration_types');
}
public function down(): void
{
Schema::create('configuration_types', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->timestamps();
});
Schema::create('configuration_statuses', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('color')->nullable();
$table->timestamps();
});
Schema::create('configurations', function (Blueprint $table) {
$table->id();
$table->foreignId('organization_id')->constrained()->cascadeOnDelete();
$table->foreignId('configuration_type_id')->constrained()->restrictOnDelete();
$table->foreignId('configuration_status_id')->nullable()->constrained()->nullOnDelete();
$table->foreignId('location_id')->nullable()->constrained()->nullOnDelete();
$table->foreignId('contact_id')->nullable()->constrained()->nullOnDelete();
$table->string('name');
$table->string('hostname')->nullable();
$table->string('primary_ip')->nullable();
$table->string('mac_address')->nullable();
$table->string('serial_number')->nullable();
$table->string('asset_tag')->nullable();
$table->string('manufacturer')->nullable();
$table->string('model')->nullable();
$table->string('operating_system')->nullable();
$table->date('installed_at')->nullable();
$table->date('warranty_expires_at')->nullable();
$table->text('notes')->nullable();
$table->boolean('is_archived')->default(false);
$table->timestamps();
$table->softDeletes();
});
}
};
@@ -0,0 +1,22 @@
<?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('organizations', function (Blueprint $table) {
$table->string('logo_path')->nullable()->after('name');
});
}
public function down(): void
{
Schema::table('organizations', function (Blueprint $table) {
$table->dropColumn('logo_path');
});
}
};
@@ -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('todos', function (Blueprint $table) {
$table->id();
$table->foreignId('organization_id')->constrained()->cascadeOnDelete();
$table->string('title');
$table->text('description')->nullable();
$table->string('category'); // infra|support|metier|autre
$table->string('criticality')->default('medium'); // low|medium|high|urgent
$table->date('due_date')->nullable();
$table->foreignId('assigned_to')->nullable()->constrained('users')->nullOnDelete();
$table->boolean('is_done')->default(false);
$table->timestamp('completed_at')->nullable();
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
$table->timestamps();
$table->softDeletes();
$table->index(['organization_id', 'is_done']);
});
}
public function down(): void
{
Schema::dropIfExists('todos');
}
};
@@ -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('shortcuts', function (Blueprint $table) {
$table->id();
// Null organization_id means a global shortcut, visible from every organization.
$table->foreignId('organization_id')->nullable()->constrained()->cascadeOnDelete();
$table->string('label');
$table->string('url');
$table->string('icon')->nullable();
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
$table->timestamps();
$table->index('organization_id');
});
}
public function down(): void
{
Schema::dropIfExists('shortcuts');
}
};
@@ -0,0 +1,64 @@
<?php
namespace Database\Seeders;
use App\Models\FlexibleAssetType;
use Illuminate\Database\Seeder;
class AssetTypeLookupSeeder extends Seeder
{
/**
* Ready-to-use asset types covering common IT inventory categories,
* with the fields a "Configuration" record used to carry.
*/
const TYPES = [
['name' => 'Poste de travail', 'icon' => '💻'],
['name' => 'Serveur', 'icon' => '🖧'],
['name' => 'Équipement réseau', 'icon' => '📡'],
['name' => 'Téléphone', 'icon' => '📱'],
['name' => 'NAS', 'icon' => '🗄️'],
['name' => 'Switch', 'icon' => '🔀'],
['name' => 'Wifi', 'icon' => '📶'],
['name' => 'Sauvegarde', 'icon' => '💾'],
['name' => 'Autre', 'icon' => '🔧'],
];
const FIELDS = [
['label' => 'Nom d\'hôte', 'field_type' => 'text', 'show_in_list' => true],
['label' => 'Adresse IP', 'field_type' => 'text', 'show_in_list' => true],
['label' => 'Adresse MAC', 'field_type' => 'text'],
['label' => 'Numéro de série', 'field_type' => 'text'],
['label' => 'Fabricant', 'field_type' => 'text'],
['label' => 'Modèle', 'field_type' => 'text'],
['label' => 'Système d\'exploitation', 'field_type' => 'text'],
['label' => 'Statut', 'field_type' => 'select', 'options' => ['choices' => ['Actif', 'Inactif', 'Retiré']], 'show_in_list' => true],
['label' => 'Site', 'field_type' => 'related_asset', 'options' => ['target' => 'location']],
['label' => 'Contact', 'field_type' => 'related_asset', 'options' => ['target' => 'contact']],
['label' => 'Date d\'installation', 'field_type' => 'date'],
['label' => 'Fin de garantie', 'field_type' => 'date'],
['label' => 'Notes', 'field_type' => 'textarea'],
];
public function run(): void
{
foreach (self::TYPES as $typeAttributes) {
$type = FlexibleAssetType::firstOrCreate(['name' => $typeAttributes['name']], $typeAttributes);
if ($type->fields()->exists()) {
continue;
}
foreach (self::FIELDS as $index => $field) {
$type->fields()->create([
'name' => \Illuminate\Support\Str::snake($field['label']),
'label' => $field['label'],
'field_type' => $field['field_type'],
'options' => $field['options'] ?? null,
'is_required' => false,
'show_in_list' => $field['show_in_list'] ?? false,
'sort_order' => $index,
]);
}
}
}
}
+30
View File
@@ -0,0 +1,30 @@
<?php
namespace Database\Seeders;
use App\Models\User;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
use WithoutModelEvents;
/**
* Seed the application's database.
*/
public function run(): void
{
// User::factory(10)->create();
$this->call(RolesAndPermissionsSeeder::class);
$admin = User::factory()->create([
'name' => 'Administrateur',
'email' => 'administrateur@cabmit.local',
]);
$admin->assignRole('Administrateur');
$this->call(AssetTypeLookupSeeder::class);
}
}
@@ -0,0 +1,34 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
class RolesAndPermissionsSeeder extends Seeder
{
public function run(): void
{
$permissions = [
'manage-users',
'manage-asset-types',
'view-content',
'edit-content',
'delete-content',
];
foreach ($permissions as $permission) {
Permission::findOrCreate($permission);
}
$admin = Role::findOrCreate('Administrateur');
$admin->syncPermissions($permissions);
$technician = Role::findOrCreate('Technicien');
$technician->syncPermissions(['view-content', 'edit-content', 'delete-content']);
$readOnly = Role::findOrCreate('Lecture seule');
$readOnly->syncPermissions(['view-content']);
}
}