Initial commit device-management
tests / ci (push) Has been cancelled

This commit is contained in:
Wian Drs
2026-08-25 09:42:00 +07:00
commit 1e80be180e
1159 changed files with 149545 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
*.sqlite*
+26
View File
@@ -0,0 +1,26 @@
<?php
namespace Database\Factories;
use App\Models\Tenant;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends Factory<Tenant>
*/
class TenantFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'name' => fake()->unique()->company(),
'slug' => fake()->unique()->slug(2),
'is_active' => true,
];
}
}
+64
View File
@@ -0,0 +1,64 @@
<?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 [
'uuid' => (string) Str::uuid(),
'tenant_id' => null,
'name' => fake()->name(),
'email' => fake()->unique()->safeEmail(),
'email_verified_at' => now(),
'password' => static::$password ??= Hash::make('password'),
'is_platform_admin' => false,
'is_active' => true,
'remember_token' => Str::random(10),
'two_factor_secret' => null,
'two_factor_recovery_codes' => null,
'two_factor_confirmed_at' => null,
];
}
/**
* Indicate that the model's email address should be unverified.
*/
public function unverified(): static
{
return $this->state(fn (array $attributes) => [
'email_verified_at' => null,
]);
}
/**
* Indicate that the model has two-factor authentication configured.
*/
public function withTwoFactor(): static
{
return $this->state(fn (array $attributes) => [
'two_factor_secret' => encrypt('secret'),
'two_factor_recovery_codes' => encrypt(json_encode(['recovery-code-1'])),
'two_factor_confirmed_at' => now(),
]);
}
}
@@ -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('tenants', function (Blueprint $table): void {
$table->id();
$table->uuid('uuid')->unique();
$table->string('name');
$table->string('slug')->unique();
$table->boolean('is_active')->default(true)->index();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('tenants');
}
};
@@ -0,0 +1,54 @@
<?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->uuid('uuid')->unique();
$table->foreignId('tenant_id')->nullable()->constrained()->restrictOnDelete();
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->boolean('is_platform_admin')->default(false)->index();
$table->boolean('is_active')->default(true)->index();
$table->rememberToken();
$table->timestamps();
$table->index(['tenant_id', 'is_active']);
});
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,34 @@
<?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('passkeys', function (Blueprint $table) {
$table->id();
$table->foreignId('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,34 @@
<?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,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->foreignId($columnNames['team_foreign_key'])->nullable()->constrained('tenants')->cascadeOnDelete();
$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->foreignId($columnNames['team_foreign_key'])->constrained('tenants')->cascadeOnDelete();
$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->foreignId($columnNames['team_foreign_key'])->constrained('tenants')->cascadeOnDelete();
$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,25 @@
<?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('permissions', function (Blueprint $table): void {
$table->boolean('is_system')->default(false)->index();
});
DB::table('permissions')->update(['is_system' => true]);
}
public function down(): void
{
Schema::table('permissions', function (Blueprint $table): void {
$table->dropColumn('is_system');
});
}
};
@@ -0,0 +1,68 @@
<?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('deployment_installations', function (Blueprint $table): void {
$table->id();
$table->uuid('uuid')->unique();
$table->foreignId('tenant_id')->nullable()->constrained()->cascadeOnDelete();
$table->string('name');
$table->string('deployment_type', 30)->index();
$table->string('domain')->nullable();
$table->string('instance_key_hash', 64)->unique();
$table->string('fingerprint_hash', 64)->nullable()->index();
$table->string('status', 30)->default('pending')->index();
$table->timestampTz('activated_at')->nullable();
$table->timestampTz('last_seen_at')->nullable()->index();
$table->timestampsTz();
$table->index(['tenant_id', 'status']);
});
Schema::create('licenses', function (Blueprint $table): void {
$table->id();
$table->uuid('uuid')->unique();
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
$table->foreignId('deployment_installation_id')->nullable()->constrained()->nullOnDelete();
$table->string('license_key_hash', 64)->unique();
$table->string('plan', 50);
$table->string('status', 30)->default('pending')->index();
$table->unsignedInteger('max_devices')->nullable();
$table->unsignedInteger('max_users')->nullable();
$table->jsonb('features')->nullable();
$table->text('signed_payload')->nullable();
$table->timestampTz('starts_at');
$table->timestampTz('expires_at')->index();
$table->timestampTz('grace_until')->nullable();
$table->foreignId('issued_by')->nullable()->constrained('users')->nullOnDelete();
$table->timestampsTz();
$table->index(['tenant_id', 'status', 'expires_at']);
});
Schema::create('license_events', function (Blueprint $table): void {
$table->id();
$table->uuid('uuid')->unique();
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
$table->foreignId('license_id')->constrained()->cascadeOnDelete();
$table->foreignId('deployment_installation_id')->nullable()->constrained()->nullOnDelete();
$table->foreignId('actor_id')->nullable()->constrained('users')->nullOnDelete();
$table->string('event_type', 50)->index();
$table->jsonb('metadata')->nullable();
$table->timestampTz('occurred_at')->index();
$table->timestampsTz();
$table->index(['tenant_id', 'occurred_at']);
});
}
public function down(): void
{
Schema::dropIfExists('license_events');
Schema::dropIfExists('licenses');
Schema::dropIfExists('deployment_installations');
}
};
@@ -0,0 +1,69 @@
<?php
use App\Enums\SystemRole;
use App\Models\User;
use App\Support\SystemRolePermissions;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
use Spatie\Permission\PermissionRegistrar;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
app(PermissionRegistrar::class)->forgetCachedPermissions();
setPermissionsTeamId(null);
$masterRole = Role::query()->whereNull('tenant_id')->firstOrCreate([
'name' => SystemRole::MasterAdmin->value,
'guard_name' => 'web',
]);
$masterRole->syncPermissions(Permission::all());
DB::table('tenants')->orderBy('id')->eachById(function (object $tenant): void {
setPermissionsTeamId($tenant->id);
$existingAdminIds = DB::table('model_has_roles')
->join('roles', 'roles.id', '=', 'model_has_roles.role_id')
->where('model_has_roles.tenant_id', $tenant->id)
->whereIn('roles.name', ['TENANT OWNER', SystemRole::TenantAdmin->value])
->pluck('model_has_roles.model_id');
$roles = [];
foreach (SystemRolePermissions::tenantRoles() as $name => $permissions) {
$role = Role::findOrCreate($name, 'web', $tenant->id);
$role->syncPermissions($permissions);
$roles[$name] = $role;
}
DB::table('model_has_roles')->where('tenant_id', $tenant->id)->delete();
User::withoutGlobalScope('tenant')->where('tenant_id', $tenant->id)->each(function (User $user) use ($existingAdminIds, $roles): void {
$role = $existingAdminIds->contains($user->id)
? $roles[SystemRole::TenantAdmin->value]
: $roles[SystemRole::TenantUser->value];
$user->assignRole($role);
});
Role::query()
->where('tenant_id', $tenant->id)
->whereNotIn('name', [SystemRole::TenantAdmin->value, SystemRole::TenantUser->value])
->delete();
});
Role::query()->whereNull('tenant_id')->where('name', 'SUPER ADMIN')->delete();
app(PermissionRegistrar::class)->forgetCachedPermissions();
}
/**
* Reverse the migrations.
*/
public function down(): void
{
// Standardizing security roles is intentionally irreversible.
}
};
@@ -0,0 +1,111 @@
<?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('device_vendors', function (Blueprint $table) {
$table->id();
$table->string('name')->unique();
$table->string('slug')->unique();
$table->text('description')->nullable();
$table->boolean('is_active')->default(true)->index();
$table->timestamps();
});
Schema::create('device_types', function (Blueprint $table) {
$table->id();
$table->string('name')->unique();
$table->string('slug')->unique();
$table->boolean('is_active')->default(true)->index();
$table->timestamps();
});
Schema::create('device_models', function (Blueprint $table) {
$table->id();
$table->foreignId('device_vendor_id')->constrained()->cascadeOnDelete();
$table->foreignId('device_type_id')->constrained()->restrictOnDelete();
$table->string('name');
$table->string('model_code')->nullable();
$table->text('description')->nullable();
$table->boolean('is_active')->default(true)->index();
$table->timestamps();
$table->unique(['device_vendor_id', 'name']);
});
Schema::create('tenant_device_policies', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->unique()->constrained()->cascadeOnDelete();
$table->boolean('tenant_user_can_create')->default(true);
$table->boolean('tenant_user_can_update_own')->default(true);
$table->boolean('tenant_user_can_delete_own')->default(false);
$table->timestamps();
});
Schema::create('devices', function (Blueprint $table) {
$table->id();
$table->uuid('uuid')->unique();
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
$table->string('name');
$table->string('hostname')->nullable();
$table->foreignId('device_vendor_id')->constrained()->restrictOnDelete();
$table->foreignId('device_type_id')->constrained()->restrictOnDelete();
$table->foreignId('device_model_id')->nullable()->constrained()->nullOnDelete();
$table->string('serial_number')->nullable();
$table->string('management_address');
$table->string('connection_type', 32);
$table->unsignedSmallInteger('management_port');
$table->string('location')->nullable();
$table->decimal('latitude', 10, 7)->nullable();
$table->decimal('longitude', 10, 7)->nullable();
$table->string('firmware_version')->nullable();
$table->string('software_version')->nullable();
$table->string('status', 24)->default('unknown');
$table->timestampTz('last_seen_at')->nullable();
$table->text('notes')->nullable();
$table->boolean('is_active')->default(true);
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
$table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete();
$table->timestamps();
$table->unique(['tenant_id', 'management_address', 'management_port']);
$table->index(['tenant_id', 'status']);
$table->index(['tenant_id', 'device_vendor_id']);
$table->index(['tenant_id', 'device_type_id']);
});
Schema::create('device_credentials', function (Blueprint $table) {
$table->id();
$table->uuid('uuid')->unique();
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
$table->foreignId('device_id')->constrained()->cascadeOnDelete();
$table->string('name');
$table->string('username');
$table->text('password');
$table->string('connection_type', 32);
$table->string('privilege_type', 32)->default('custom');
$table->boolean('is_master')->default(false);
$table->boolean('is_active')->default(true);
$table->timestampTz('last_verified_at')->nullable();
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
$table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete();
$table->timestamps();
$table->unique(['device_id', 'name']);
$table->index(['tenant_id', 'device_id', 'is_active']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('device_credentials');
Schema::dropIfExists('devices');
Schema::dropIfExists('tenant_device_policies');
Schema::dropIfExists('device_models');
Schema::dropIfExists('device_types');
Schema::dropIfExists('device_vendors');
}
};
@@ -0,0 +1,41 @@
<?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('tenant_device_settings', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->unique()->constrained()->cascadeOnDelete();
$table->string('base_username');
$table->text('base_password');
$table->boolean('use_tls')->default(false);
$table->boolean('verify_tls')->default(true);
$table->unsignedSmallInteger('connection_timeout')->default(10);
$table->timestamps();
});
Schema::table('devices', function (Blueprint $table) {
$table->string('activation_status', 24)->default('inactive')->index();
$table->timestampTz('activated_at')->nullable();
$table->text('activation_message')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('devices', function (Blueprint $table) {
$table->dropColumn(['activation_status', 'activated_at', 'activation_message']);
});
Schema::dropIfExists('tenant_device_settings');
}
};
@@ -0,0 +1,43 @@
<?php
use App\Services\Encryption\TenantEnvelopeEncryption;
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
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('tenant_encryption_keys', function (Blueprint $table) {
$table->id();
$table->uuid('uuid')->unique();
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
$table->unsignedInteger('version');
$table->text('wrapped_key');
$table->boolean('is_active')->default(true);
$table->string('source', 16)->default('generated');
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
$table->timestampTz('retired_at')->nullable();
$table->timestamps();
$table->unique(['tenant_id', 'version']);
$table->index(['tenant_id', 'is_active']);
});
DB::table('tenants')->orderBy('id')->pluck('id')->each(
fn (int $tenantId) => app(TenantEnvelopeEncryption::class)->ensureKey($tenantId),
);
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('tenant_encryption_keys');
}
};
@@ -0,0 +1,60 @@
<?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('device_access_users', function (Blueprint $table) {
$table->id();
$table->uuid('uuid')->unique();
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
$table->string('display_name');
$table->string('username');
$table->text('password');
$table->boolean('is_enabled')->default(true);
$table->text('notes')->nullable();
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
$table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete();
$table->timestamps();
$table->softDeletes();
$table->unique(['tenant_id', 'username']);
$table->index(['tenant_id', 'is_enabled']);
});
Schema::create('device_user_assignments', function (Blueprint $table) {
$table->id();
$table->uuid('uuid')->unique();
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
$table->foreignId('device_access_user_id')->constrained()->cascadeOnDelete();
$table->foreignId('device_id')->constrained()->cascadeOnDelete();
$table->string('group_name', 64);
$table->string('desired_operation', 24)->default('upsert');
$table->string('sync_status', 24)->default('pending');
$table->string('remote_id')->nullable();
$table->string('error_code', 64)->nullable();
$table->text('message')->nullable();
$table->unsignedInteger('attempts')->default(0);
$table->timestampTz('last_attempted_at')->nullable();
$table->timestampTz('last_synced_at')->nullable();
$table->timestamps();
$table->unique(['device_access_user_id', 'device_id']);
$table->index(['tenant_id', 'sync_status']);
$table->index(['device_id', 'sync_status']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('device_user_assignments');
Schema::dropIfExists('device_access_users');
}
};
@@ -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
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('devices', function (Blueprint $table) {
$table->boolean('remove_legacy_users_on_activation')->default(false);
$table->jsonb('device_facts')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('devices', function (Blueprint $table) {
$table->dropColumn(['remove_legacy_users_on_activation', 'device_facts']);
});
}
};
@@ -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
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('device_user_assignments', function (Blueprint $table) {
$table->string('remote_username')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('device_user_assignments', function (Blueprint $table) {
$table->dropColumn('remote_username');
});
}
};
@@ -0,0 +1,27 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
DB::table('device_user_assignments')->where('group_name', 'RADIQ-MANAGER')->update([
'group_name' => 'RADIQ-NOC',
'sync_status' => 'pending',
'message' => 'Menunggu sinkronisasi ulang setelah RADIQ-MANAGER diganti menjadi RADIQ-NOC.',
]);
}
/**
* Reverse the migrations.
*/
public function down(): void
{
// The original manager assignments cannot be distinguished safely.
}
};
@@ -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
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('devices', function (Blueprint $table) {
$table->string('base_sync_status', 24)->default('not_synced')->index();
$table->string('base_sync_error_code', 64)->nullable();
$table->text('base_sync_message')->nullable();
$table->timestampTz('base_synced_at')->nullable();
$table->timestampTz('base_sync_attempted_at')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('devices', function (Blueprint $table) {
$table->dropColumn(['base_sync_status', 'base_sync_error_code', 'base_sync_message', 'base_synced_at', 'base_sync_attempted_at']);
});
}
};
@@ -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
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('device_credentials', function (Blueprint $table) {
$table->text('enable_password')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('device_credentials', function (Blueprint $table) {
$table->dropColumn('enable_password');
});
}
};
@@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
$zteDeviceIds = DB::table('devices')
->join('device_vendors', 'device_vendors.id', '=', 'devices.device_vendor_id')
->where('device_vendors.slug', 'zte')
->where('devices.connection_type', 'ssh')
->select('devices.id');
DB::table('device_user_assignments')
->whereIn('device_id', $zteDeviceIds)
->where('sync_status', 'unsupported')
->where('error_code', 'DRIVER_NOT_AVAILABLE')
->update([
'sync_status' => 'pending',
'error_code' => null,
'message' => 'Driver ZTE tersedia; menunggu sinkronisasi ulang.',
'updated_at' => now(),
]);
}
/**
* Reverse the migrations.
*/
public function down(): void
{
//
}
};
@@ -0,0 +1,33 @@
<?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('devices', function (Blueprint $table) {
$table->json('connection_ports')->nullable()->after('management_port');
});
DB::table('devices')->orderBy('id')->each(function ($device): void {
$vendor = DB::table('device_vendors')->where('id', $device->device_vendor_id)->value('slug');
$ports = match ($vendor) {
'mikrotik' => ['routeros_api' => (int) $device->management_port],
'zte' => ['ssh' => (int) $device->management_port],
'hsgq' => ['ssh' => (int) $device->management_port, 'telnet' => 23],
'hisfocus' => ['telnet' => (int) $device->management_port, 'web_http' => max(1, (int) $device->management_port - 1)],
default => [$device->connection_type => (int) $device->management_port],
};
DB::table('devices')->where('id', $device->id)->update(['connection_ports' => json_encode($ports)]);
});
}
public function down(): void
{
Schema::table('devices', fn (Blueprint $table) => $table->dropColumn('connection_ports'));
}
};
+20
View File
@@ -0,0 +1,20 @@
<?php
namespace Database\Seeders;
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
{
$this->call(RolePermissionSeeder::class);
$this->call(DeviceCatalogSeeder::class);
}
}
+33
View File
@@ -0,0 +1,33 @@
<?php
namespace Database\Seeders;
use App\Models\DeviceModel;
use App\Models\DeviceType;
use App\Models\DeviceVendor;
use Illuminate\Database\Seeder;
class DeviceCatalogSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
foreach (['MikroTik', 'ZTE', 'C-Data', 'HSGQ', 'HiOSO', 'Hisfocus'] as $name) {
DeviceVendor::updateOrCreate(['slug' => str($name)->slug()], ['name' => $name, 'is_active' => true]);
}
foreach (['Router', 'OLT', 'Switch', 'Access Point', 'Server', 'Other'] as $name) {
DeviceType::updateOrCreate(['slug' => str($name)->slug()], ['name' => $name, 'is_active' => true]);
}
$zte = DeviceVendor::where('slug', 'zte')->firstOrFail();
$olt = DeviceType::where('slug', 'olt')->firstOrFail();
foreach (['C300', 'C320'] as $name) {
DeviceModel::updateOrCreate(
['device_vendor_id' => $zte->id, 'name' => $name],
['device_type_id' => $olt->id, 'model_code' => $name, 'is_active' => true],
);
}
}
}
+40
View File
@@ -0,0 +1,40 @@
<?php
namespace Database\Seeders;
use App\Enums\SystemRole;
use Illuminate\Database\Seeder;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
use Spatie\Permission\PermissionRegistrar;
class RolePermissionSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
app(PermissionRegistrar::class)->forgetCachedPermissions();
$permissions = [
'tenant.view', 'tenant.create', 'tenant.update', 'tenant.delete', 'tenant.access-any',
'user.view', 'user.create', 'user.update', 'user.delete',
'device.view', 'device.create', 'device.update', 'device.delete', 'device.connect',
'device.credential.view', 'device.credential.create', 'device.credential.update', 'device.credential.delete',
'device.user.view', 'device.user.create', 'device.user.update', 'device.user.delete', 'device.user.mass_update',
'device.master_account.manage', 'device.command.execute', 'device.command.mass_execute',
'device.config.view', 'device.config.deploy', 'device.backup.create', 'device.backup.download',
'device.backup.restore', 'device.monitoring.view', 'audit.view',
'license.view', 'license.manage', 'installation.view', 'installation.manage',
];
foreach ($permissions as $permission) {
$model = Permission::findOrCreate($permission, 'web');
$model->forceFill(['is_system' => true])->save();
}
setPermissionsTeamId(null);
Role::findOrCreate(SystemRole::MasterAdmin->value, 'web')->syncPermissions(Permission::all());
}
}