authorizeManage($actor, $workspace); $definition = $this->requireDefinition($domain, $key); $normalizedValue = $this->validatedValue($definition, $value); $existing = WorkspaceSetting::query() ->where('workspace_id', (int) $workspace->getKey()) ->where('domain', $domain) ->where('key', $key) ->first(); $beforeValue = $existing instanceof WorkspaceSetting ? $this->decodeStoredValue($existing->getAttribute('value')) : null; $setting = WorkspaceSetting::query()->updateOrCreate([ 'workspace_id' => (int) $workspace->getKey(), 'domain' => $domain, 'key' => $key, ], [ 'value' => $normalizedValue, 'updated_by_user_id' => (int) $actor->getKey(), ]); $this->resolver->clearCache(); $afterValue = $this->resolver->resolveValue($workspace, $domain, $key); $this->auditLogger->log( workspace: $workspace, action: AuditActionId::WorkspaceSettingUpdated->value, context: [ 'metadata' => [ 'scope' => 'workspace', 'domain' => $domain, 'key' => $key, 'before_value' => $beforeValue, 'after_value' => $afterValue, ], ], actor: $actor, resourceType: 'workspace_setting', resourceId: $domain.'.'.$key, ); return $setting; } public function resetWorkspaceSetting(User $actor, Workspace $workspace, string $domain, string $key): void { $this->authorizeManage($actor, $workspace); $this->requireDefinition($domain, $key); $existing = WorkspaceSetting::query() ->where('workspace_id', (int) $workspace->getKey()) ->where('domain', $domain) ->where('key', $key) ->first(); $beforeValue = $existing instanceof WorkspaceSetting ? $this->decodeStoredValue($existing->getAttribute('value')) : null; if ($existing instanceof WorkspaceSetting) { $existing->delete(); } $this->resolver->clearCache(); $afterValue = $this->resolver->resolveValue($workspace, $domain, $key); $this->auditLogger->log( workspace: $workspace, action: AuditActionId::WorkspaceSettingReset->value, context: [ 'metadata' => [ 'scope' => 'workspace', 'domain' => $domain, 'key' => $key, 'before_value' => $beforeValue, 'after_value' => $afterValue, ], ], actor: $actor, resourceType: 'workspace_setting', resourceId: $domain.'.'.$key, ); } public function updateTenantSetting(User $actor, Workspace $workspace, Tenant $tenant, string $domain, string $key, mixed $value): TenantSetting { $this->authorizeManage($actor, $workspace); $this->assertTenantBelongsToWorkspace($workspace, $tenant); $definition = $this->requireDefinition($domain, $key); $normalizedValue = $this->validatedValue($definition, $value); $setting = TenantSetting::query()->updateOrCreate([ 'workspace_id' => (int) $workspace->getKey(), 'tenant_id' => (int) $tenant->getKey(), 'domain' => $domain, 'key' => $key, ], [ 'value' => $normalizedValue, 'updated_by_user_id' => (int) $actor->getKey(), ]); $this->resolver->clearCache(); return $setting; } public function resetTenantSetting(User $actor, Workspace $workspace, Tenant $tenant, string $domain, string $key): void { $this->authorizeManage($actor, $workspace); $this->assertTenantBelongsToWorkspace($workspace, $tenant); $this->requireDefinition($domain, $key); TenantSetting::query() ->where('workspace_id', (int) $workspace->getKey()) ->where('tenant_id', (int) $tenant->getKey()) ->where('domain', $domain) ->where('key', $key) ->delete(); $this->resolver->clearCache(); } private function requireDefinition(string $domain, string $key): SettingDefinition { $definition = $this->registry->find($domain, $key); if ($definition instanceof SettingDefinition) { return $definition; } throw ValidationException::withMessages([ 'key' => [sprintf('Unknown setting key: %s.%s', $domain, $key)], ]); } private function validatedValue(SettingDefinition $definition, mixed $value): mixed { $validator = Validator::make( data: ['value' => $value], rules: ['value' => $definition->rules], ); if ($validator->fails()) { throw ValidationException::withMessages($validator->errors()->toArray()); } return $definition->normalize($validator->validated()['value']); } private function authorizeManage(User $actor, Workspace $workspace): void { if (! $this->workspaceCapabilityResolver->isMember($actor, $workspace)) { throw new NotFoundHttpException('Workspace not found.'); } if (! $this->workspaceCapabilityResolver->can($actor, $workspace, Capabilities::WORKSPACE_SETTINGS_MANAGE)) { throw new AuthorizationException('Missing workspace settings manage capability.'); } } private function assertTenantBelongsToWorkspace(Workspace $workspace, Tenant $tenant): void { if ((int) $tenant->workspace_id !== (int) $workspace->getKey()) { throw new NotFoundHttpException('Tenant is outside the selected workspace scope.'); } } private function decodeStoredValue(mixed $value): mixed { if (! is_string($value)) { return $value; } $decoded = json_decode($value, true); return json_last_error() === JSON_ERROR_NONE ? $decoded : $value; } }