targets()->with('mikrotik:id,tenant_id,name,connection_type,host,connection_status')->orderBy('id')->get(); } public function saveTarget(NasPackageProfile $profile, array $data): NasPackageProfileTarget { $nas = NasMikrotik::with('tenant.nasSetting')->whereKey($data['nas_mikrotik_id'])->where('tenant_id', $profile->tenant_id)->firstOrFail(); $backendMode = $nas->effectiveBackendMode(); return NasPackageProfileTarget::updateOrCreate( ['nas_package_profile_id' => $profile->id, 'nas_mikrotik_id' => $nas->id], ['tenant_id' => $profile->tenant_id, 'backend_type' => in_array($backendMode, ['platform_radius','external_radius','hybrid'], true) ? 'radius' : 'routeros_api', 'external_profile_name' => $data['external_profile_name'], 'sync_status' => 'not_deployed'] ); } public function sync(NasPackageProfileTarget $target, string $mode): NasPackageProfileTarget { $target->load(['packageProfile.scripts.versions', 'mikrotik']); $desired = $this->desiredSnapshot($target->packageProfile); try { $remote = $target->backend_type === 'routeros_api' ? (in_array($target->packageProfile->service_type, ['static_ip', 'dynamic_ip'], true) ? ['managed_per_customer' => true, 'network' => $desired['network']] : $this->routerOs->syncProfile($target->mikrotik, $target->packageProfile->service_type, $target->external_profile_name, $desired['routeros'], $mode)) : ['radius_attributes' => $desired['radius']]; $checksum = hash('sha256', json_encode($desired)); $target->update(['sync_status' => 'synced', 'desired_snapshot' => $desired, 'remote_snapshot' => $remote, 'remote_id' => $remote['.id'] ?? null, 'deployed_checksum' => $checksum, 'last_checked_at' => now(), 'last_synced_at' => now(), 'last_error' => null]); } catch (Throwable $e) { $target->update(['sync_status' => 'error', 'desired_snapshot' => $desired, 'last_checked_at' => now(), 'last_error' => $e instanceof ValidationException ? collect($e->errors())->flatten()->first() : $e->getMessage()]); throw $e; } return $target->fresh('mikrotik'); } public function assertSynced(NasPackageProfile $profile, NasMikrotik $nas): NasPackageProfileTarget { $target = $profile->targets()->where('nas_mikrotik_id', $nas->id)->where('sync_status', 'synced')->first(); if (! $target) throw ValidationException::withMessages(['package_profile_id' => 'Profile paket belum disinkronkan ke NAS yang dipilih.']); return $target; } private function desiredSnapshot(NasPackageProfile $profile): array { $scripts = $profile->scripts->keyBy(fn ($policy) => $policy->pivot->event); $script = fn (string $event) => $this->scriptTemplate($scripts->get($event), $profile); $rate = ($profile->upload_kbps ? ($profile->upload_kbps.'k') : '0').'/'.($profile->download_kbps ? ($profile->download_kbps.'k') : '0'); $common = ['rate-limit' => $rate, 'session-timeout' => $profile->session_timeout ? $profile->session_timeout.'s' : null, 'idle-timeout' => $profile->idle_timeout ? $profile->idle_timeout.'s' : null]; $router = $profile->service_type === 'hotspot' ? [...$common, 'shared-users' => (string) $profile->shared_users, 'on-login' => $script('on_login'), 'on-logout' => $script('on_logout')] : ($profile->service_type === 'ppp' ? [...$common, 'local-address' => $profile->local_address, 'remote-address' => $profile->remote_address_pool, 'on-up' => $script('on_up'), 'on-down' => $script('on_down')] : []); return ['routeros' => $router, 'radius' => ['Mikrotik-Rate-Limit' => $rate, 'Session-Timeout' => $profile->session_timeout, 'Idle-Timeout' => $profile->idle_timeout, 'Simultaneous-Use' => $profile->shared_users], 'network' => $profile->only(['mac_lock_required','ip_pool','subnet_cidr','gateway','dns_servers','vlan_id']), 'profile' => $profile->only(['profile_code','service_type','usage_mode','validity_start','validity_value','validity_unit','uptime_limit_seconds','data_limit_bytes'])]; } private function scriptTemplate($policy, NasPackageProfile $profile): ?string { if (! $policy || ! $policy->pivot->enabled) return null; $versionId = $policy->pivot->nas_script_policy_version_id; $version = $versionId ? $policy->versions->firstWhere('id', $versionId) : $policy->versions->sortByDesc('version')->first(); if (! $version) return null; return strtr($version->script_template, [ '{{ username }}' => '$user', '{{username}}' => '$user', '{{ package_code }}' => (string) $profile->profile_code, '{{package_code}}' => (string) $profile->profile_code, '{{ tenant_code }}' => (string) optional($profile->tenant)->tenant_code, '{{tenant_code}}' => (string) optional($profile->tenant)->tenant_code, ]); } }