-
Notifications
You must be signed in to change notification settings - Fork 714
Stage #9289
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
* feat(plugin-subscription-access): add access control and user assignment
Implement a comprehensive system for managing plugin subscription access,
including hierarchical subscriptions, user assignment, and revocation.
This introduces:
- Parent-child subscription relationships in PluginSubscription entity,
enabling organization/tenant-level subscriptions to manage individual
user access.
- Commands, handlers, DTOs, and API endpoints for assigning and revoking
plugin access to users.
- A dedicated PluginSubscriptionAccessService for centralized validation
of user access, assignment permissions, and subscription details.
- A PluginSubscriptionAccessGuard for protecting plugin routes based on
subscription status.
- Refinements to the subscription purchase logic to automatically set
USER scope for free plans and handle initial statuses (ACTIVE, PENDING, TRIAL).
* feat(plugins-access): implement plugin subscription access management
Introduce a robust system for managing and controlling access to plugins based on subscriptions. This includes:
- New Akita state management (store, query, facade) to track plugin and user-specific access.
- Dedicated service for API interactions (check, assign, revoke access, bulk checks).
- UI dialogs (PluginUserAssignmentDialogComponent, PluginUserRevocationDialogComponent) for managing user assignments and revocations.
- An Angular guard (PluginAccessGuard) to protect plugin routes and prompt for subscription if needed.
- Updates to the plugin marketplace item to display access levels and enable user assignment actions.
- Extend plugin subscription plans to support various access scopes (user, organization, tenant, global).
- Add component for displaying plugin subscription hierarchy.
* feat: Implement subscription management for plugin installations
- Refactored plugin installation logic to handle subscription-based plugins.
- Introduced strategies for free and subscription-based plugin installations.
- Added validation chain for installation prerequisites, including access and subscription checks.
- Created a facade for managing installation processes and strategies.
- Implemented access guard to verify user permissions for plugin installations.
- Enhanced UI components to reflect subscription requirements during installation.
- Added validators for plugin status and subscription requirements.
- Improved error handling and user notifications for subscription-related actions.
* feat(plugin-tenant): add PluginTenantService for managing plugin-tenant relationships
* refactor(plugin-marketplace): remove analytics tracking from various actions
* feat(plugin-subscription): add ability to upgrade and downgrade plans
Introduce new functionality for users to change their active plugin
subscription plans. This includes new Redux actions and effects for
managing the upgrade and downgrade processes, alongside new API
endpoints to handle these requests on the backend.
The plugin subscription selection dialog has been enhanced to:
* Detect and display the user's current active subscription plan.
* Visually distinguish the current plan, making it unselectable.
* Dynamically update the primary action button to reflect "Upgrade
Plan", "Downgrade Plan", or "Current Plan" based on the selected
plan and the user's existing subscription status.
* Implement client-side logic to determine the appropriate plan
change type (upgrade or downgrade) based on plan hierarchy.
* feat(plugin-installation): streamline plugin removal by simplifying parameters
* feat(plugin-marketplace): implement lazy loaded detail tabs
Refactor plugin marketplace item detail page to use NbRouteTabset with lazy-loaded child routes.
This improves initial load performance by only loading tab content when activated.
New tabs introduced:
* Settings: Configure plugin-specific options (permission-gated).
* User Management: Assign and unassign users to the plugin (permission-gated).
Existing "Overview" and "Source Code" tab content has been extracted into dedicated lazy-loaded components to align with this new structure.
Tabs are dynamically rendered based on user permissions for configuration and assignment.
* feat(plugin-user-management): implement user assignment facade and dialogs
Introduce PluginUserAssignmentFacade to centralize business logic and
provide a simplified API for managing plugin user assignments. This
enhances testability, maintainability, and improves reactive data flow.
Refactor PluginUserAssignmentEffects to include:
- Comprehensive loading state management for all assignment operations.
- Translateable success and error notifications using ToastrNotificationService.
- Direct store updates on success (add, remove, set assignments).
- Flexible loading of assignments for a plugin or specific installation.
Add showAssignmentDialog, hideAssignmentDialog, showRevocationDialog, and
hideRevocationDialog effects in PluginSubscriptionAccessEffects to
orchestrate UI dialogs for user assignment and revocation.
Update UserManagementTabComponent to consume the new
PluginUserAssignmentFacade via a consolidated view model. This reduces
component complexity, simplifies template logic, and ensures proper
handling of loading and error states in the UI.
* feat(plugin-installation): return structured response for install and uninstall
The `create` and `remove` endpoints for plugin installation and
uninstallation now return a structured JSON response. Previously,
these methods returned `void`, providing no body on success.
This change enhances client feedback by including a descriptive
message and the HTTP status code in the response body.
* feat(plugins-registry): return paginated results for plugin user assignments
Update query handlers and service methods related to plugin user assignments
to return `IPagination<PluginUserAssignment>` instead of a plain array.
This standardizes list retrieval operations by providing pagination metadata.
Also, correct the instantiation of `TypeOrmPluginUserAssignmentRepository`
to properly inject and utilize the underlying TypeORM repository.
BREAKING CHANGE: Plugin user assignment query handlers and service methods
now return an IPagination object instead of a plain array. Consumers
expecting an array will need to adapt to the new return structure (e.g.,
accessing `result.items`).
* feat(plugins): add infinite scroll and pagination for user assignments
This commit introduces infinite scroll functionality to the plugin user assignment lists, significantly enhancing the user experience and performance when dealing with a large number of assigned users.
Key changes include:
- A new `InfiniteScrollDirective` in `desktop-ui-lib` to detect scroll events and emit `scrolled` events.
- Implementation of `take` and `skip` based pagination across plugin user assignment APIs (frontend and backend) and state management (Akita store, query, facade, effects). This forms the foundation for efficient data loading.
- Refactoring of plugin user assignment, unassignment, and access check mechanisms to utilize a subscription-based access control model. This involves changes to service methods and API endpoints, promoting a more scalable and consistent approach to managing plugin access.
The combined changes allow for lazy loading of user assignments, reducing initial load times and memory footprint.
* refactor(plugin-user-assignment): update user interface and improve state selectors
* feat(plugin-user-management): implement available users state management and infinite scroll
Introduces a dedicated Akita state management module (`_available-users` store, query, effects) for handling the list of users available for plugin assignment. This refactors the user fetching, filtering, and selection logic, improving separation of concerns and maintainability.
The `PluginUserManagementComponent` has been refactored to utilize a new `UserManagementFacade`, which orchestrates interactions between the available users and plugin user assignment state.
Key changes include:
- Added `AvailableUsersActions`, `AvailableUsersEffects`, `AvailableUsersQuery`, `AvailableUsersStore` for robust state management.
- Created `UserManagementFacade` to provide a high-level, component-agnostic API for user management.
- Refactored `PluginUserManagementComponent` to delegate all business logic to the `UserManagementFacade`.
- Implemented infinite scroll for the "Available Users" list, enhancing UX for large user bases.
- Updated `plugins.module` to include new state management providers and the `InfiniteScrollDirective`.
- Adjusted `UserManagementTabComponent` to clear and reload assignments after unassignment for improved data consistency.
* refactor(plugin-user-management): reorder initialization methods in ngOnInit for better clarity
* chore(plugin-marketplace): update store config names
Rename store configuration names to use snake_case instead of
kebab-case. This change ensures consistent naming conventions
across the application's state management modules. Additionally,
the 'plugin-user-assignment' store name has been pluralized to
'plugin_user_assignments' for improved semantic clarity.
* fix(plugin-marketplace): correct total count for available users
Remove client-side pagination logic and derive the total count
for available users directly from the API response. This ensures
that the displayed total accurately reflects the backend's
pagination and overall count, preventing data inconsistencies.
Also add a debug log to `loadAssignmentsSuccess` for better visibility.
* refactor: streamline effects in plugin subscription access and user assignment
- Updated effects in `plugin-subscription-access.effects.ts` for better readability and consistency.
- Added dispatch options to effects where necessary.
- Removed unused imports in `plugin-subscription.effect.ts`.
- Refactored loading and assignment effects in `plugin-user-assignment.effects.ts` for clarity and efficiency.
- Improved error handling messages for user assignment actions.
- Cleaned up module imports in `plugins.module.ts` for better organization.
* feat(i18n): enhance plugin user management with new loading states and action messages
* feat(plugins): enhance plugin subscription access and activation flow
Introduce `IPluginAccess` contract and `PluginScope` enum to standardize plugin access details across the platform.
Implement server-side validation for plugin activation and deactivation, ensuring that actions are authorized before local execution. This improves security and adherence to subscription rules.
Refine `PluginSubscriptionAccessService.getSubscriptionDetails` to provide comprehensive access information, including:
- Determining if a plugin requires a subscription.
- Calculating `canAssign` and `canActivate` permissions based on subscription ownership and user assignments.
- Validating subscription parent relationships.
Consolidate `PluginScope` enum definition into the `@gauzy/contracts` package for better shared model management.
* feat(plugin-installation): implement multi-step workflow with installation ID
Introduce a unique installation ID and a comprehensive multi-step workflow
for managing plugin installations. This enhancement provides a robust mechanism
to track plugins across the desktop application and the server-side registry,
improving reliability and traceability of plugin lifecycle events.
Key changes include:
- Add `installationId` column to the `plugins` database table via new migration.
- Update plugin metadata interfaces, services, and the `plugin-manager` to
support storing and managing the `installationId`.
- Refactor the desktop UI's plugin installation process into distinct
steps: download, server-side registration, local completion, and activation.
- Utilize a Command Pattern for each installation step in the UI, enabling
better separation of concerns, explicit error handling, and potential for
rollback mechanisms.
- Server-side plugin installation now returns the generated `installationId`.
- New IPC channel and Electron service method `completeInstallation` added
to sync the `installationId` locally after server registration.
- Update plugin activation, deactivation, and uninstallation logic across
desktop-lib and desktop-ui-lib to explicitly use the `installationId`
for server communication.
* feat(plugins): enhance plugin lifecycle authorization and flow
This commit introduces several improvements to the plugin management
lifecycle, primarily focusing on enhanced security, authorization,
and a more robust client-side installation experience.
Key changes include:
- Enforce employee-specific authorization for plugin installation,
activation, deactivation, and uninstallation, ensuring actions are
tied to the installing employee.
- The `ActivatePluginCommand` now explicitly includes `pluginId`
alongside `installationId` for more precise control over activation.
- Client-side plugin installation completion now utilizes an event-driven
progress observable, improving monitoring and responsiveness.
- The `RequestContext.currentOrganizationId()` helper can now retrieve
the organization ID from the `organization-id` request header.
- The legacy `PluginInstallHandler` and `PluginUninstallHandler` have
been removed.
- The `PluginSubscriptionAccessGuard` now performs stricter validation
for missing plugin IDs.
* fix(plugin-marketplace): reset installing flag after plugin installation
Ensure the 'installing' flag in the plugin installation store is
explicitly set to false when an installation process finalizes.
Previously, only `completingInstallation` was reset, which could
lead to an incorrect UI state where a plugin appeared to be
stuck in an 'installing' state after completion or failure.
* feat(i18n): add strings for categories, plugin management, and user assignment
Add comprehensive internationalization strings to support new and enhanced
UI features. This includes:
* New success, error, and info messages for managing plugin categories
(CRUD, load operations).
* Messages for successful assignment/unassignment of users to plugins,
including bulk actions, and related error states.
* Expanded plugin lifecycle messages covering subscription, download, and
server installation progress.
* New confirmation and error messages for plugin source deletion and
restoration.
* Improved parameter naming for plugin version toasts (from `number` to
`version`).
* Additional generic error messages for plugin forms and data.
* refactor(plugins): streamline billing and subscription entities
Removed direct billing, pricing, invoice, and payment retry-related fields from
PluginBilling and PluginSubscription entities.
Shifted responsibility for subscription pricing, type, and billing period
to PluginSubscriptionPlan, referenced by a new planId on PluginSubscription.
Introduced computed properties (isOverdue, isPending, daysUntilDue) on
PluginBilling for derived status based on existing data.
Reimplemented nextBillingDate and isBillingDue as computed properties
on PluginSubscription, deriving their state from associated PluginBilling
records rather than stored fields.
Updated related DTOs and interfaces (IPluginBillingCreateInput,
IPluginSubscriptionCreateInput, IPluginSubscriptionPurchaseInput, etc.) to
reflect the entity simplifications and removal of redundant fields.
This refactor reduces data redundancy, improves data consistency by
centralizing billing plan details, and better encapsulates runtime state
through computed properties.
* refactor(plugins): simplify user assignments and refine setting management
Remove the dedicated `PluginUserAssignment` entity and its corresponding interfaces,
consolidating user assignment logic within the plugin subscription domain.
Update plugin user assignment handlers and services to utilize `subscriptionId`
instead of `pluginInstallationId` for clarity and consistency with the new structure.
Rework the `PluginSetting` entity:
- Store setting `value` as plain `text` instead of `jsonb`, simplifying data handling.
- Introduce a unique index on `pluginId`, `key`, and `pluginTenantId` to prevent
duplicate configurations.
- Add an `updatedById` field to track the user who last modified a setting.
- Enhance validation rules and `ApiProperty` decorations for existing fields.
- Add `ColumnIndex` decorations for improved query performance.
* refactor(plugins): simplify user assignment and subscription management
Removed `PluginUserAssignment` entity, its repositories, and related models.
Refactored plugin user assignment commands, queries, and handlers to use
`pluginId` directly instead of `pluginInstallationId`. Replaced complex
assignment logic in `PluginUserAssignmentService` with placeholder
implementations, indicating a future re-design.
Updated plugin subscription logic to utilize a `planId` for managing
subscription details (type, billing period, trial days) instead of direct
fields. Streamlined `PluginBillingService` by removing internal total amount
calculation and retry logic.
Restructured plugin user assignment API controllers and DTOs to align with
the new, simplified model, removing `installationId` from endpoints and DTOs.
This change is a preparatory step for a more robust and flexible plugin
management system.
* feat(plugins/billing): implement comprehensive plugin billing system
Introduce a new billing system for plugin subscriptions using a CQRS and
event-driven architecture. This includes:
* **Billing Commands:** Commands for creating billing records and processing
payments.
* **Command Handlers:** Logic to handle billing creation and payment
processing, integrating with a new `PluginBillingFactory`.
* **Billing Events:** Domain events for billing lifecycle states (created,
paid, failed, overdue).
* **Event Handlers:** React to billing events to update subscription metadata,
change status (e.g., suspend on multiple failures), and trigger
notifications (future implementation).
* **PluginBillingFactory:** Encapsulates business logic for creating various
types of billing records (initial, renewal, upgrade).
The `PluginSubscriptionService` is updated to utilize this new billing flow for
purchasing, renewing, and upgrading subscriptions, promoting a cleaner
separation of concerns and a more robust billing lifecycle management.
It also introduces new helper methods to determine subscription scope and
initial status based on plan type.
* feat(billing): formalize subscription billing calculations
Refactor subscription billing logic in PluginBillingFactory to extract complex
calculations into a new private method, `calculateBillingAmount`.
This centralizes the application of base price, discounts, and setup fees,
ensuring consistency and clarity. The `createForRenewal` and `createInitialBilling`
methods now accept the full plan object, simplifying their signatures and
enabling richer metadata capture.
Billing records are enhanced with detailed metadata including `basePrice`,
`discountAmount`, `setupFee`, `effectivePrice`, and a comprehensive breakdown
string, improving traceability and auditability of charges.
Explicitly clarifies that discounts and setup fees apply only to initial billing.
Extensive inline documentation has been added to the factory to outline these rules.
* feat(plugin-subscription): add subscriber for entity lifecycle management
Implement a TypeORM subscriber for `PluginSubscription` entities.
This subscriber's `afterLoad` hook automatically updates the in-memory
subscription status to `EXPIRED` if its `endDate` has passed, ensuring
the entity's state is consistent upon loading. It also logs the current
state of the subscription for debugging and monitoring.
Additionally, refactor `PluginSubscription` entity to use the
`PluginBillingStatus` enum for billing status checks, enhancing type
safety and readability.
* fix(plugin-system): always deactivate plugin before deletion
Ensure plugins are always cleanly shut down before their files and metadata are
removed, preventing potential resource leaks or orphaned processes.
Previously, deactivation was conditional on the plugin's `isActivate` metadata
flag, which could lead to improper cleanup if the metadata was out of sync or
the plugin was in an inconsistent state.
* refactor(plugin-subscriptions): consolidate domain logic and access
Transition the plugin subscription management to a rich domain model by encapsulating business rules and state changes directly within the `PluginSubscription` entity.
Introduce `PluginSubscriptionAccessService` to centralize complex access validation and permissions, leveraging Strategy and Factory patterns for modularity. This service now orchestrates access checks across different subscription scopes (user, organization, tenant).
Update the `PluginSubscriptionService` to delegate business logic to the `PluginSubscription` entity's new domain methods for operations like cancellation, renewal, upgrade, downgrade, and trial extension.
Remove the `PluginUserAssignmentService` as its responsibilities are now integrated into the centralized subscription access architecture.
Adjust consuming components, including `PluginAccessGuard`, `PluginInstallationAccessGuard`, `PluginSubscriptionGuard`, and `CheckPluginAccessQueryHandler`, to utilize the new `PluginSubscriptionAccessService` for consistent and robust access control.
This refactor improves maintainability, testability, and adherence to domain-driven design principles. Additionally, trial subscriptions are now correctly identified as active during access checks.
* feat(plugins-registry): add plugin user assignment service
Introduce a lightweight stub for the PluginUserAssignmentService. This
service provides an interface for managing user assignments to plugins,
including methods to assign, unassign, and check user access.
The current implementation is minimal, designed primarily to satisfy
imports and provide a consistent API for command and query handlers.
Full business logic and persistence will be implemented in a subsequent
iteration.
* feat(plugin-registry): implement plugin queries and handlers
- Add GetPluginQuery for fetching a plugin by ID.
- Implement CheckPluginAccessQueryHandler to validate plugin access.
- Create GetPluginQueryHandler to retrieve plugin details.
- Introduce ListPluginsQuery and ListPluginsQueryHandler for paginated plugin listing.
- Add SearchPluginsQuery and SearchPluginsQueryHandler for advanced plugin searching.
- Establish a new index for plugin queries and handlers.
- Refactor plugin subscription strategies for creating and updating subscription plans.
- Implement PluginSubscriptionGuard to ensure valid subscriptions before plugin installation.
- Update shared DTOs and models for better organization and access.
- Enhance infrastructure with new controllers, guards, and storage strategies.
* refactor: reorganize imports and exports
This commit refactors the import and export statements across
multiple files within the plugin registry package. The goal is
to improve code organization, reduce redundancy, and clarify
dependencies.
Specifically, it addresses the following:
- Consolidates exports within the `index.ts` files of the
`application`, `domain`, `infrastructure`, and `shared`
directories. This makes it easier to understand the public API
of each module.
- Reorganizes imports to use relative paths for internal modules and
absolute paths for external dependencies.
- Refactors the structure to leverage the index.ts for exporting,
reducing the lines of code.
This change doesn't introduce any functional changes but aims to
improve maintainability and readability.
* feat: Add validation for plugin settings and subscription plans
This commit enhances the plugin registry by:
- Adding validation rules for plugin setting creation and updates.
- Implementing validation and defaults for plugin subscription plans.
- Ensuring date fields in plugin subscriptions are properly converted.
- Adding default activation status for subscription plans and subscriptions.
- Allowing stringified JSON for validation rules.
* refactor: Remove unused imports and reorder imports
Removes unused imports and reorders the imports to improve
code readability and maintainability across multiple files.
* refactor: Add business logic methods to entities
Adds business logic methods to `Plugin`, `PluginVersion`,
`PluginSource`, and `PluginInstallation` entities. Also adds static
methods and interfaces to the models. These methods encapsulate
plugin-related operations such as status checks, version
comparisons, and download counts.
* refactor: use Plugin.create to instantiate plugin
Refactor the plugin instantiation to use `Plugin.create` method
instead of `Object.assign`. This aligns with the project's
intended object creation pattern.
* feat: implement plugin tenant management
This commit introduces a new feature for managing plugin tenants,
allowing for tenant-specific configurations and access controls.
The changes include:
- Added commands and queries for creating, updating, deleting,
enabling, disabling, and approving plugin tenants.
- Implemented command handlers and query handlers to process the
respective commands and queries.
- Added a controller for managing plugin tenants through API endpoints.
- Introduced DTOs for data transfer and validation.
- Updated the plugin model to remove the GLOBAL plugin scope.
- Exported plugin tenant modules.
* fix: apply fixes and improvements across plugins registry
This commit addresses several issues within the plugins registry:
- Removes redundant error message for GLOBAL plugin scope.
- Fixes module import for plugin tenants.
- Improves logic for determining subscription plan operation type.
- Ensures planId is correctly passed during subscription plan updates.
- Adds repository url to CreatePluginDTO and boolean transformation for requiresSubscription
* refactor(plugins): introduce dedicated DTO and remove client-side category counting
Introduce a new `PluginQueryOptions` DTO to provide strongly-typed
query parameters for fetching a single plugin. This replaces the
generic `BaseQueryDTO` in the `PluginController` and `GetPluginQuery`,
improving type safety and API documentation.
In the desktop UI, the client-side logic for calculating plugin category
counts (`updateCategoryCounts`) has been removed from the marketplace
filter component. This simplifies the frontend, as category counts are
now handled and delivered by the backend.
* refactor(plugins): simplify PluginSubscriptionService by removing unused methods and dependencies
* feat(subscription): add computed properties and helper methods to IPluginSubscriptionPlan interface
* refactor(plugin-subscription): move business logic into domain models
Refactor the command handlers for subscription actions (cancel, upgrade, downgrade, renew) to use a richer domain model approach.
Business logic and validation rules, previously handled within the service layer, are now encapsulated as methods directly on the subscription domain entity. Command handlers are now responsible for fetching the entity, invoking its business methods, and persisting the result.
This change aligns with Domain-Driven Design (DDD) principles, making the application layer thinner and centralizing business rules within the domain. This improves maintainability, testability, and clarity of the subscription lifecycle logic.
Additionally, the IPluginTenant interface has been extended with business logic method signatures to support this pattern more broadly.
* refactor(plugin-subscription): refactor trial extension command handler
Relocate the business logic for extending a trial subscription from the `PluginSubscriptionService` directly into the `ExtendTrialSubscriptionCommandHandler`.
This refactoring aligns with CQRS best practices by making the command handler more self-contained and responsible for its specific unit of work. The handler now fetches the subscription using the correct security context, validates the action using new domain entity methods (`canExtendTrial`, `extendTrial`), and persists the result.
This approach improves clarity, testability, and adherence to domain-driven design principles by placing business rules within the domain model itself.
As part of this cleanup, the implementation for the deprecated `PurchasePluginSubscriptionCommandHandler` has been removed.
* feat(plugin-subscription): implement plugin subscription purchase logic
This commit implements the complete logic for the
PurchasePluginSubscriptionCommandHandler. It handles the creation of
plugin subscriptions based on different scenarios, including free, trial,
and paid plans.
The handler now:
- Validates required input like plugin and tenant IDs.
- Ensures a `PluginTenant` relationship exists, creating one if needed.
- Differentiates subscription creation based on the plan type:
- Free plans result in an immediate active subscription.
- Plans with a trial period create a trial subscription.
- Paid plans create a pending subscription with a calculated end date.
- Defaults to a free subscription if no plan is specified.
- Sets additional properties like auto-renew and metadata from the DTO.
* feat(plugin-tenant): support scope on creation and set defaults
The `PluginTenantService.findOrCreate` method is updated to accept an
optional `scope` parameter. This allows callers, such as the plugin
purchase handler, to specify the installation scope (tenant,
organization, or user).
When a new `PluginTenant` relationship is created, it is now populated
with sensible defaults, including `requiresApproval: true` and
`autoInstall: false`. The `approvedById` is set to the current user
initiating the action to ensure a secure initial state.
BREAKING CHANGE: The default scope for a newly created PluginTenant
now defaults to `USER` if not explicitly provided. Previously, the scope
was inferred as `TENANT` or `ORGANIZATION` based on the presence of an
`organizationId`.
* refactor(plugin-subscription): overhaul plugin subscription and access control service
This commit completely refactors the plugin subscription and access control logic to follow Domain-Driven Design (DDD) and SOLID principles. The previous implementation was becoming difficult to maintain and extend.
Key changes include:
- Re-architected `PluginSubscriptionAccessService` to act as a true domain service.
- Introduced the Specification pattern for validating access rules (e.g., plugin enabled, subscription active, quota limits), encapsulating business logic into small, reusable components.
- Implemented a composite `AccessValidator` to orchestrate validation specifications.
- Refactored subscription finders to use a `SubscriptionFinderRegistry` with a clear Chain of Responsibility pattern (User > Organization > Tenant).
- Integrated `PluginTenant` service to manage tenant-specific configurations like installation/user quotas, approvals, and access lists.
- Rewrote multiple CQRS query handlers for subscriptions to use explicit TypeORM queries instead of high-level service methods, improving clarity and error handling. They now return empty results instead of throwing exceptions on failure.
- Simplified the `CreatePluginSubscriptionPlanCommand` to only require the DTO and the acting user's ID.
BREAKING CHANGE: The `GetPluginSubscriptionsByPluginIdQuery` and `GetPluginSubscriptionsBySubscriberIdQuery` handlers now return a paginated object `IPagination<IPluginSubscription>` instead of an array `IPluginSubscription[]`.
BREAKING CHANGE: The `CreatePluginSubscriptionPlanCommand` constructor signature has been changed, removing `tenantId` and `organizationId`.
* fix(plugin-marketplace): prevent runtime errors from non-array subscription data
The plugin marketplace could encounter runtime errors if API responses or
the internal state for subscriptions and plans were not arrays. This could
happen during initial load or with inconsistent API payloads, leading to
crashes.
This commit introduces defensive checks across the data flow:
- `PluginSubscriptionService` now normalizes API responses to ensure
methods always return an array, handling various payload structures.
- `PluginSubscriptionStore` and `PluginSubscriptionQuery` are updated to
safely handle non-array state, defaulting to an empty array for
operations like map, filter, and find.
This makes the subscription management feature more robust and resilient to
unexpected data shapes.
* feat(plugin-registry): implement plugin user access and assignment logic
This commit introduces the full implementation for the plugin user assignment feature, replacing the previous stub and placeholder logic. It provides a complete system for managing which users have access to specific plugins within a tenant and organization.
Key changes include:
- The `PluginUserAssignmentService` is fully implemented. It now manages user assignments by interacting with the `PluginTenantService` and `RoleService` to enforce access control based on user lists, roles, and quotas.
- All query handlers related to user assignments are implemented, providing real data for endpoints that list plugin assignments for a user or user assignments for a plugin.
- Pagination (`skip`, `take`) is added to all relevant query handlers and controller endpoints for fetching user assignment lists.
- Controllers are updated to use `RequestContext` to derive the current tenant and organization, ensuring operations are correctly scoped.
- `PluginSubscriptionService` now includes `createChildSubscriptions` and `revokeChildSubscriptions` methods to manage individual user subscriptions tied to a parent tenant/organization subscription.
- The `ProcessBillingCommandHandler` is simplified to use the `PluginBillingService` directly.
* feat(plugin-tenant): add pagination to get plugin-tenants queries
Adds support for pagination to the queries that retrieve plugin-tenant
relationships by either plugin ID or tenant ID.
The `GetPluginTenantsByPluginQuery` and `GetPluginTenantsByTenantQuery`
now accept optional `skip` and `take` parameters. The corresponding
controller endpoints have been updated to accept these as query
parameters, allowing clients to paginate through large result sets.
The query handlers and service methods are updated to handle these
new parameters and return a paginated response.
BREAKING CHANGE: The `/by-plugin/:pluginId` and `/by-tenant/:tenantId` endpoints now return a paginated response object of type `IPagination<IPluginTenant>` instead of a simple array.
* feat(plugin-tenant): apply plan limitations during creation
Refactor the `PluginTenantService.findOrCreate` method to accept a single
input object instead of multiple arguments. This improves readability
and makes the method easier to extend.
When purchasing a new plugin subscription, if the selected plan includes
limitations such as maximum users or installations, these values are now
passed during PluginTenant creation and stored accordingly.
Additionally, the user initiating the creation of the PluginTenant
relationship is now automatically granted access to the plugin.
* feat(plugin): set default isActive to true during plugin creation
* fix(registry): correct plugin approval and subscription checks
The `isApproved` method on the PluginTenant entity is updated to also check for the presence of `approvedById`. This makes the approval check more robust, ensuring a plugin is considered approved if it has an approver, even if the `approvedAt` timestamp is missing.
The logic for determining if a plugin requires a subscription has been refined. A new `isSubscriptionRequired` method is introduced in the plan service, which checks for active paid plans (price > 0) or if the plugin is explicitly flagged as requiring a subscription. This replaces a less accurate check and prevents free plugins from being misidentified.
* feat(plugin-subscription): implement contextual upgrade and downgrade flow
Introduce a comprehensive, state-managed subscription management flow that provides a contextual experience for users with existing subscriptions.
This change refactors the subscription selection process to be aware of the user's current plan, enabling intelligent upgrade, downgrade, and new subscription actions.
Key additions:
- A new `PlanComparisonService` encapsulates the business logic for determining action types (upgrade, downgrade), calculating prorations, and validating plan changes.
- Akita state is expanded to track the plan comparison process, confirmation steps, and current subscription/plans on a per-plugin basis.
- A new `SubscriptionConfirmationComponent` provides a clear summary of changes before the user commits.
- The subscription selection UI is significantly enhanced with:
- Skeleton loaders and improved error states.
- Visual badges and styling for upgrade/downgrade actions.
- Improved accessibility and keyboard shortcuts.
* feat(plugins): introduce subscription scope and auto-renew
This commit refactors the plugin subscription system to introduce subscription scopes (User, Organization, Tenant) and an auto-renew option. This provides more flexible subscription models and improves system robustness.
The subscription creation API payload is updated to include `scope` and `autoRenew`, while moving `billingPeriod` into the metadata object.
Frontend changes:
- Add a read-only section to display the plan's scope in the subscription dialog.
- Add an auto-renew checkbox to the subscription form.
- Automatically determine and set the subscription scope based on the selected plan type.
Backend changes:
- Add a unique database constraint (`pluginId`, `subscriberId`) to the `PluginSubscription` entity to prevent multiple subscriptions for the same user and plugin.
- Update subscription creation logic to handle pre-existing cancelled or expired subscriptions by deleting them before creating a new one.
- Refactor the `PluginSubscriptionPlan` entity with a builder pattern and static factory methods for more consistent plan creation.
* style(plugin-marketplace): enhance subscription plan selection UI
Overhauls the visual design of the plugin subscription selection screen to create a more modern, polished, and engaging user experience.
This update refines the styling for both the plan selection cards and the subscription preview component.
Key changes include:
- Redesigned `PlanCardComponent` with enhanced hover, focus, and selection states, incorporating subtle scaling, improved shadows, and smoother animations.
- Updated `SubscriptionPreviewComponent` with a cleaner layout, improved typography, and distinct styling for summary items.
- Replaced the feature list with a responsive grid of styled tags for better readability.
- Added responsive styles for improved usability on smaller screens.
- Implemented dark theme support for visual consistency.
* feat(subscription): add plan comparison and formatting services
- Implemented PlanComparisonService to handle logic for comparing subscription plans, including actions for new subscriptions, upgrades, downgrades, and current plans.
- Added methods for calculating proration amounts, checking upgrade/downgrade eligibility, and generating user-friendly action descriptions.
- Introduced PlanFormatterService for formatting subscription plan data for display, including transformation to view models and formatting prices and billing periods.
- Enhanced user experience with clear action descriptions and button variants based on plan actions.
* feat(subscription): enhance plan card comparison and update module exports
* refactor(plugins): introduce shared components and services for subscription management
This commit modularizes the plugin subscription management UI by extracting shared logic and presentation into reusable components and services. This improves code reuse, maintainability, and adherence to SOLID principles.
New shared modules have been created:
- **Shared Components:**
- `SubscriptionPlanCardComponent`: A reusable presentational component for displaying subscription plan details.
- `SubscriptionBillingFormComponent`: A component encapsulating the subscription billing form logic and UI.
- `SubscriptionStatusBadgeComponent`: A component for consistently displaying subscription status badges.
- **Shared Services:**
- `SubscriptionPlanService`: Centralizes business logic for plan comparison (upgrade/downgrade), price formatting, and savings calculations.
- `SubscriptionFormService`: Manages the creation and validation logic for the subscription form.
- `SubscriptionStatusService`: Provides helpers for mapping subscription statuses to UI elements (badges, icons).
The `PluginSubscriptionManagerComponent` has been significantly refactored to consume these new shared modules, removing duplicated logic and mock data. It is now connected to the `PluginSubscriptionFacade` to handle real subscription state changes.
Additionally, `PluginSubscriptionPlanSelectionComponent` and `PluginSubscriptionHierarchyComponent` are updated to leverage the new shared services, simplifying their implementations.
* feat: Refactor subscription plugin installation strategy and dialog routing
- Updated SubscriptionPluginInstallationStrategy to utilize SubscriptionDialogRouterService for managing subscription dialogs.
- Enhanced SubscriptionBillingFormComponent with improved UI elements and accessibility features.
- Introduced SubscriptionDialogRouterService to handle routing to appropriate subscription dialogs based on user status.
- Improved PluginAccessGuard to utilize the new dialog routing service for better user experience.
- Updated styles in subscription billing form for a more modern look and feel.
- Added animations and improved error handling in various components.
- Removed deprecated SubscriptionPlanCardComponent in favor of new plan card implementation.
* refactor(subscription): streamline subscription update input and improve upgrade/downgrade methods
* refactor: update plugin subscription plan data structure and comparison logic
* refactor(plugin-marketplace): streamline state management and improve loading states
- Updated PluginSourceEffects and PluginVersionEffects to use new state management methods for setting loading, creating, updating, deleting, and restoring states.
- Refactored PluginSourceStore and PluginVersionStore to include new methods for managing sources and versions, enhancing clarity and maintainability.
- Improved error handling and reset functionality across various stores.
- Enhanced the plugin subscription components to correctly display and manage subscription plans, including upgrades and downgrades.
- Cleaned up HTML templates for better readability and maintainability.
* refactor(plugin-marketplace): centralize state logic and add validation chain
Move state mutation logic from effects into the corresponding Akita stores. This centralizes the responsibility for merging and sorting collections of plugin versions and sources, making the effects cleaner and more focused on orchestration. The stores now handle maintaining a unique, sorted list of items.
Introduce an `InstallationValidationChainBuilder` to create a formal, extensible validation process before a plugin is installed or updated. This replaces scattered validation logic with a centralized chain of responsibility, improving robustness and maintainability. This new validation service is now used in both the item and detail components.
Additionally, this commit includes minor fixes such as:
- Using the async pipe for subscription checks in templates.
- Adding guards for potentially null values.
- Simplifying store reset logic.
* fix(subscription-manager): correct plan type display logic in subscription info
* feat(plugin): treat pending subscriptions as active and add tenant archiving
Subscriptions in a `PENDING` state are now consistently treated as active across the application. This prevents users from creating duplicate subscriptions while a payment is processing and allows them to manage their plan before it is fully activated.
This change ensures that pending subscriptions are correctly recognized when checking for an existing active subscription, calculating counts, and determining available actions like upgrades.
This also fixes a bug in the purchase handler where an existing subscription could be missed if its scope did not match the new purchase request.
Additionally, this commit introduces the ability to archive and restore plugin tenant configurations, providing a soft-delete mechanism.
* refactor(subscription-finders): update subscription retrieval logic to handle failures and adjust active status flag
* refactor(plugin-marketplace): decouple plan state from subscription state
The state management for plugin subscriptions previously handled
subscriptions, plans, and plan comparisons within a single, monolithic
Akita store. This made the state complex and difficult to maintain.
This refactoring separates these concerns into three distinct state
slices:
- PluginSubscription
- PluginPlan
- PluginPlanComparison
Each slice now has its own dedicated store, actions, effects, and
query files. The `PluginSubscriptionFacade` has been updated to act as
an orchestrator for these new, more focused state modules.
This change improves separation of concerns, enhances modularity, and
clarifies the responsibilities of each part of the plugin state.
* style(plugins): standardize code formatting and import order
Apply consistent code formatting, including indentation and import statement order, across various files in the plugin marketplace feature.
This improves code readability and maintainability without altering any functionality.
* refactor(plugin-marketplace): centralize plugin deletion confirmation logic in effect
Move the plugin deletion confirmation dialog from the components to the PluginMarketplaceEffects.
Previously, both the `PluginMarketplaceDetailComponent` and the `PluginMarketplaceItemComponent` contained identical logic to prompt the user before deleting a plugin. This refactoring centralizes this logic into a single effect.
Now, components simply dispatch the `delete` action, and the effect is responsible for showing the confirmation dialog before calling the delete service. This improves code reuse and separates concerns more cleanly.
* refactor(plugin): centralize and improve uninstallation process
The plugin uninstallation flow is overhauled to be more robust and to centralize logic within the `PluginInstallationEffects`.
The backend `uninstallPlugin` method is enhanced to find plugins by various criteria (ID, marketplace ID) and returns the `installationId`.
The frontend effect now:
- Presents a confirmation dialog to the user before proceeding.
- Uses the returned `installationId` to determine if a server-side API call is required, correctly handling both local-only and marketplace plugins.
- Centralizes the uninstallation logic, removing it from individual components.
This refactoring makes the process more reliable, improves code structure, and provides a better user experience. A new effect to check plugin installation status has also been added.
* refactor(plugin-marketplace): centralize plugin update dialog logic into effect
Move the responsibility for opening the PluginMarketplaceUploadComponent
dialog from individual components into the `update$` effect.
This simplifies `PluginMarketplaceDetailComponent` and
`PluginMarketplaceItemComponent` by removing duplicated dialog-handling
logic. Components now only dispatch the `update` action, and the effect
orchestrates the entire workflow, including user interaction and the
subsequent API call.
The `PluginMarketplaceActions.update` action signature is updated to
accept the full plugin object to facilitate this change.
* feat(plugin-marketplace): pre-select version and source on plugin update
When a user initiates an update for a plugin, the context for that
plugin (version and source) is now set in the state stores.
This ensures that when the update dialog opens, it is pre-populated
with the correct information, improving the user experience and reducing
the chance of errors.
* refactor(plugin-installation): centralize per-plugin toggle state
The previous implementation for managing a plugin's installation toggle state was flawed. It relied on a single global state object in the Akita store and additional local state within components. This could lead to state inconsistencies, especially when multiple plugin components are rendered simultaneously.
This refactoring addresses these issues by:
- Modifying the PluginInstallationStore to manage a collection of toggle states, one for each plugin, identified by `pluginId`.
- Removing local state management from `PluginMarketplaceDetailComponent` and `PluginMarketplaceItemComponent`.
- Components now query the central store for the installation status of their specific plugin.
- Encapsulating the installation check logic within an NgRx Effect, triggered by a `check` action for a more robust state flow.
- Updating actions to use `pluginId`, making state updates more precise.
- Ensuring that installation failures correctly reset the UI toggle for the specific plugin that failed.
* refactor(plugin-marketplace): centralize utility functions and dialog logic
Introduce a new `PluginMarketplaceUtilsService` to consolidate duplicated helper methods for labels, badges, and formatting across marketplace components. This eliminates code repetition and improves maintainability.
The logic for handling the creation of new plugin versions and sources is moved from the `PluginMarketplaceItemComponent` into the corresponding NgRx Effects. Effects now manage opening the creation dialogs, which centralizes side-effect handling and simplifies the component. The related `add` actions are updated to accept the `IPlugin` object directly.
* refactor(plugins): centralize PluginScope enum and remove GLOBAL scope
Move the PluginScope enum from its local definition within the
plugin-subscription-access service to the shared @gauzy/contracts
package. This ensures a single source of truth for plugin scopes across
the platform.
As part of this refactoring, the unused `PluginScope.GLOBAL` has been
completely removed from all related logic, including labels, status
indicators, and helper functions.
Additionally, this change improves type safety in the `OverviewTabComponent`
by correctly importing and using the `PluginType` enum.
* style(plugins): add explicit public access modifiers
Enforce explicit `public` access modifiers for methods and getters in plugin marketplace components and services to improve code clarity and maintain a consistent style.
Additionally, the unused `ngOnInit` lifecycle hook is removed from the `OverviewTabComponent` as part of this code cleanup.
* refactor(plugins): centralize marketplace logic into effects
Move complex business logic from plugin marketplace components into NgNeat effects to improve state management and code maintainability.
Components previously handled dialog orchestration, installation validation, and subscription checks directly. This logic is now centralized within dedicated effects, making the components simpler and more focused on presentation.
Key changes:
- Installation, subscription, settings, and user management workflows are
now triggered by dispatching intent-based actions.
- Effects orchestrate the entire process, including validation chains and
opening necessary dialogs.
- The `SubscriptionDialogRouterService` has been removed as its logic is
now managed by effects.
- Plugin components (`-item`, `-detail`) are significantly simplified,
only dispatching actions.
Additionally, `PluginMetadataService.findOne` no longer throws an error on not found, returning null instead to allow for cleaner handling in effects.
* fix(plugins): stabilize plugin installation and subscription handling
This commit addresses several issues related to the reliability and UI feedback of plugin installation and subscription management in the marketplace.
- Use `concatMap` in effects to prevent race conditions during installation status checks, ensuring actions are processed in order.
- Improve the installation toggle UI by setting its state immediately when an installation starts and resetting it correctly if the user cancels the confirmation dialog.
- Add a new backend endpoint to fetch the current user's active subscription for a specific plugin, which is then consumed by the UI.
- Centralize subscription state logic by using the `accessFacade` and storing the full subscription object in the access store for consistent UI display.
- Correct a typo in the `getCurrentSubscription` API endpoint URL.
- After plugin activation, dispatch an action to re-check the installation status, ensuring the marketplace UI is up-to-date.
- Refactor the installation store's `setToggle` method for more robust state updates.
* refactor(plugins): move plugin action dispatching to effects
Centralize the dispatching of secondary actions for the plugin marketplace into the `getOne$` effect. This removes scattered logic from components and adheres more closely to state management best practices.
The `getOne$` effect now dispatches actions to check installation status, select the plugin version, and select the plugin source upon successfully fetching a plugin. This logic was previously handled within the `PluginMarketplaceItemComponent`.
Additionally, the `SettingsTabComponent` now dispatches an action to open the settings dialog instead of calling the dialog service directly, further decoupling the component from implementation details.
* refactor(plugins): simplify plugin installation status checking
Remove the manual installation check and UI toggle state management.
Previously, a dedicated action/effect (`checkInstallation`) and a
'toggles' array in the store were used to track whether a marketplace
plugin was installed. This approach was complex and could lead to state
synchronization issues.
This change introduces a more robust method by deriving the installation
status directly from the single source of truth: the list of locally
installed plugins. A new `installed$` query has been added for this
purpose.
Effects for installation, uninstallation, and failures now simply
dispatch a refresh action, ensuring the UI always reflects the correct
state reactively.
* feat(plugins): implement optimistic UI for installation toggle
Introduce a new state management slice for the plugin installation toggle to provide immediate visual feedback for user actions in the marketplace. This implements an optimistic UI pattern.
Previously, the UI would wait for the entire installation or uninstallation process to complete before updating the toggle's state, leading to a sluggish user experience.
This change introduces `PluginToggleStore` and related effects/actions:
- The toggle state now updates instantly when a user initiates an install or uninstall action.
- If the backend operation fails or is canceled, the toggle reverts to its previous state.
- On success, the toggle's new state is confirmed.
Additionally, this resolves a UI bug where loading spinners would activate on all plugin items simultaneously. Spinners are now correctly scoped to only the specific plugin being installed, updated, or uninstalled.
* fix(plugins): correctly sync plugin toggle state after installation
The plugin toggle in the marketplace detail view did not automatically
enable after a successful installation, nor did it consistently reflect
the plugin's true enabled state.
This was caused by two issues:
1. The `activationCompleted` effect was using an incorrect ID to dispatch
the toggle action. It now correctly uses `marketplaceId`.
2. The component's toggle state was not reactively driven by the actual
installation status.
This change ensures the toggle state is updated by subscribing to the
installation query, providing a reliable single source of truth for the UI.
* fix(plugins): revert plugin toggle state on uninstall failure
The plugin uninstall effect would immediately toggle the plugin to disabled
in the UI. If the uninstall operation failed, the toggle would remain
in the disabled state, which was inconsistent with the plugin's actual
installed state.
The uninstall handlers now return a boolean indicating success or failure.
The main effect uses this result to correctly set the toggle's state. If
the uninstall fails, the toggle is reverted back to its enabled state,
ensuring UI consistency.
* refactor(plugins): enhance installation state handling in marketplace components
* refactor(plugins): track operation states on a per-plugin basis
The previous state management for plugin operations used global flags for
states like `installing`, `updating`, and `deleting`. This led to a UI
bug where performing an action on a single plugin would incorrectly
display a loading state for all plugins in the marketplace.
This commit refactors the plugin state stores (installation, marketplace,
version, source) to manage these states in records keyed by the plugin
ID.
- State properties are changed from `boolean` to `Record<string, boolean>`.
- Stores, queries, and effects are updated to operate on specific plugin
IDs.
- UI components are modified to consume this new per-plugin state,
ensuring that loading indicators and disabled states are isolated to
the correct plugin item.
Additionally, the installation validation chain has been hardened with
more robust error handling and improved subscription management.
* fix(plugins): correctly handle installation flow after plan selection
The plugin installation process after the subscription plan selection
dialog was flawed. Canceling the dialog left the plugin toggle enabled,
and proceeding did not trigger the installation.
This commit adjusts the plan selection effect to correctly dispatch
actions based on the dialog's outcome:
- Revert the plugin toggle to 'off' if the user cancels.
- Trigger the plugin installation if the user proceeds.
Additionally, state queries for `installing$`, `uninstalling$`, and
`uploading$` are refactored. They can now check the global status (i.e.,
if *any* plugin is undergoing an operation), which simplifies disabling
UI controls during these processes.
* refactor(plugins): remove unused DataSource dependency and transaction handling in delete plugin command
* feat(plugin-marketplace): revert plugin toggle when subscription flow is cancelled
When a user toggles a plugin that requires a subscription, the
subscription management dialog is shown. If the user closes this dialog
without completing the process, the toggle previously remained in its
new, misleading state.
This change introduces logic to automatically revert the toggle state if
the subscription flow was initiated by the toggle action and then
cancelled. A `clicked` flag distinguishes between manual and automatic
dialog openings to ensure the toggle is only reverted in the latter case.
* feat(cdn-download): enhance plugin download strategy with streaming support and improved error handling
* fix(plugin-installation): add unique index for plugin installation fields
* feat(plugin): implement soft delete for plugins
Replace hard deletion with a soft delete mechanism. This change prevents permanent data loss and preserves plugin history for auditing or recovery purposes.
The `DeletePluginCommandHandler` now calls `pluginService.softDelete` and returns a success message with a status code upon completion.
BREAKING CHANGE: The `DeletePluginCommandHandler` now returns an object `{ message: string, status: number }` instead of `void`. Consumers of this command must be updated to handle the new return value.
* feat(plugins): open settings and user management dialogs via effects
Create NgNeat effects to handle the opening of plugin management dialogs.
The `openSettings$` effect in `PluginSettingsEffects` listens for the
`openSettings` action and opens the `PluginSettingsManagementComponent`.
Similarly, the `openUserManagementDialog$` effect in
`PluginUserAssignmentEffects` listens for the `manageUsers` action to
open the `PluginUserManagementComponent`.
This approach centralizes the dialog opening logic within the state
management layer.
* feat(i18n): add translations for plugin management and validation
Adds a comprehensive set of new English translation strings to support
upcoming enhancements to the plugin marketplace and management features.
This includes:
- A new `VALIDATION` section for various plugin lifecycle errors.
- More specific error messages for the "Assign Users" modal.
- A new "View Subscription" button label.
- Improved wording for plugin-related empty state messages for clarity.
* fix(registry): correctly load tenant relations for assignment check
The `determineAssignmentPermissions` method previously accepted a `pluginTenant` object, which could lead to incorrect permission calculations if the required user and role relations were not pre-loaded by the caller.
This change refactors the method to accept a `pluginTenantId` instead. It now explicitly fetches the `pluginTenant` entity with its `allowedUsers`, `deniedUsers`, and `allowedRoles` relations, ensuring that the permission logic always has the necessary data to operate correctly.
* refactor(plugins): decouple user assignments from plugin installations
This refactor removes the tight coupling between plugin user assignments
and the `PluginInstallation` entity. User assignments are now tracked
at a higher level, abstracting away the specific installation instance.
On the backend, a new `PluginTenant` entity is introduced to manage the
relationship between a plugin and a tenant/organization, serving as the
anchor for user assignments.
This change simplifies the overall data model and the public API for
managing user access to plugins.
Key changes include:
- Replaced `installationId` with `subscriptionId` or removed it
entirely from frontend state management and service calls.
- Renamed `pluginInstallationId` to `pluginSubscriptionId` in data models.
- Simplified API endpoints for fetching user assignments, e.g., from
`.../installations/:id/users` to `/plugins/:pluginId/users`.
- Updated backend command handlers to use the new `PluginTenant` service
for assignment and revocation logic.
BREAKING CHANGE: The API and frontend service layer for plugin user
assignments have changed. `installationId` is no longer used to scope
user assignments. Methods like `loadAssignmentsForInstallation` have been
removed, and signatures for many other methods in facades and services
have been updated.
* feat(plugins): implement plugin tenant based user management
This refactors the entire plugin user assignment system to use a new "Plugin Tenant" access control model, replacing the previous subscription-based logic. Plugin tenants now directly manage user access through explicit `allowedUsers` and `deniedUsers` lists.
Frontend Changes:
- New NgRx actions, effects, and facade methods are introduced for managing users within a plugin tenant context (e.g., `loadAllowedUsersForPlugin`, `allowUsersToPluginTenant`, `removeAllowedUsersFromPluginTenant`).
- The component logic is updated to first resolve a `pluginTenantId` for a given plugin and then perform user management operations against that tenant.
- State management is updated to store the `currentPluginTenantId`.
Backend Changes:
- New API endpoints are added to manage plugin tenant users (`POST /plugin-tenants/:id/users`) and to resolve a plugin tenant from a plugin ID (`GET /plugins/:id/tenant`).
- CQRS commands (`ManagePluginTenantUsersCommand`) and queries (`GetPluginTenantUsersQuery`) are implemented to handle the new ACL logic.
This provides a more robust and explicit mechanism for controlling user access to plugins.
BREAKING CHANGE: The plugin user assignment system has been completely overhauled. The previous subscription-based user access model is replaced by a new Plugin Tenant access control list (ACL) model. API endpoints and frontend state management for user assignment are not backward compatible.
* refactor(plugin): simplify subscription finding and access logic
Remove the complex Strategy and Registry patterns for locating plugin
subscriptions. The multiple finder classes and registry are replaced with a
single private `findSubscriptionByContext` method within the service.
This consolidation simplifies the architecture, reduces boilerplate, and
makes the subscription lookup process more direct and maintainable.
Additionally, this commit includes the following fixes:
- Allow access for subscriptions with `TRIAL` or `PENDING` statuses,
not just `ACTIVE`.
- Grant access by default when a plugin has no `PluginTenant`
configuration, treating it as a non-restricted plugin.
- Add null-safety checks in the access context factory to prevent
runtime errors.
* refactor(plugins): simplify user loading in user management
The logic for loading available users relied on subscribing to the
`selectedOrganization$` observable to get the current organization and
tenant IDs.
This approach was unnecessarily complex as these values are available
directly on the store. This commit refactors the component to access
`store.organizationId` and `store.tenantId` directly.
This removes the verbose observable pipeline, resulting in cleaner and
more straightforward code.
* feat(subscription): link plugin u…
| .pipe(unzipper.Extract({ path: extractDir })) | ||
| .pipe(unzipper.Parse()) | ||
| .on('entry', (entry: any) => { | ||
| const { path: entryPath, type, size } = entry; |
Check failure
Code scanning / CodeQL
Arbitrary file access during archive extraction ("Zip Slip") High
file system operation
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 24 days ago
In general, to fix Zip Slip issues you must never trust archive entry paths directly. Instead, compute the target path with path.join/path.resolve, normalize it, and then verify that the resulting path is still under the intended extraction directory. Reject or skip any entry whose resolved path does not start with the normalized extraction root. This check must be done on the final path passed to fs operations like createWriteStream.
For this specific code, the best fix without changing existing functionality is:
- After joining
extractDirandentryPathto formfullPath, compute its normalized absolute path (e.g.,path.resolve(extractDir, entryPath)). - Compute the normalized absolute path of
extractDironce. - Check that the resolved file path starts with the normalized extraction directory path (taking care to include the path separator to avoid prefix tricks, e.g.,
/tmp/outvs/tmp/outside). - If the check fails, log a warning, drain the entry, and skip writing.
- Otherwise, proceed to create directories as needed (if that is already handled elsewhere) and write the file.
Because we can’t rely on the (unshown) isSafePath implementation, the simplest and most robust approach is to move the safety check to use the resolved fullPath directly in this method. Concretely, we will:
- Replace the call to
this.isSafePath(extractDir, entryPath)with an inline safety check usingpath.resolveand a normalized root. - Compute
const fullPath = path.resolve(extractDir, entryPath);(usingresolveinstead of a separatejoin). - Verify that
fullPathstarts with the normalizedextractDirpath (e.g.,extractRootWithSep) and skip the entry if not. - Maintain all existing logging, size checks, and behavior for valid entries.
All changes occur within unzip in packages/desktop-lib/src/lib/plugin-system/data-access/strategies/cdn-download.strategy.ts; no new imports are needed because path is already imported.
-
Copy modified lines R299-R303 -
Copy modified lines R309-R311
| @@ -296,13 +296,19 @@ | ||
| let totalSize = 0; | ||
|
|
||
| await new Promise<void>((resolve, reject) => { | ||
| const extractRoot = path.resolve(extractDir); | ||
| const extractRootWithSep = extractRoot.endsWith(path.sep) | ||
| ? extractRoot | ||
| : extractRoot + path.sep; | ||
|
|
||
| createReadStream(filePath) | ||
| .pipe(unzipper.Parse()) | ||
| .on('entry', (entry: any) => { | ||
| const { path: entryPath, type, size } = entry; | ||
|
|
||
| // Security: Check for path traversal | ||
| if (!this.isSafePath(extractDir, entryPath)) { | ||
| // Security: Resolve and validate final extraction path to prevent path traversal | ||
| const fullPath = path.resolve(extractRoot, entryPath); | ||
| if (!fullPath.startsWith(extractRootWithSep)) { | ||
| logger.warn(`Suspicious path detected, skipping: ${entryPath}`); | ||
| entry.autodrain(); | ||
| return; | ||
| @@ -326,8 +327,6 @@ | ||
| return; | ||
| } | ||
|
|
||
| const fullPath = path.join(extractDir, entryPath); | ||
|
|
||
| if (type === 'Directory') { | ||
| entry.autodrain(); | ||
| } else { |
| response | ||
| .body!.pipe(unzipper.Parse()) | ||
| .on('entry', async (entry: any) => { | ||
| const { path: filePath, type, size } = entry; |
Check failure
Code scanning / CodeQL
Arbitrary file access during archive extraction ("Zip Slip") High
file system operation
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 24 days ago
In general, the fix is to ensure that any path taken from a ZIP entry is validated and normalized before being used for file system operations. We should reject archive paths that attempt directory traversal (e.g., containing .. segments), are absolute, or that end up resolving outside the intended extraction directory after normalization. We should base all file system writes on a validated path, not on the raw entry.path.
The best targeted fix here is:
- Add a private helper method
isSafePath(root: string, entryPath: string): boolean(and optionally agetSafePathhelper) toCdnDownloadStrategyin this file. This method should:- Use
path.isAbsolute(entryPath)to reject absolute paths. - Use
path.normalize(entryPath)and reject anything containing..segments or path separators at the start that would move above the root. - Build
fullPath = path.join(root, entryPath)and then comparepath.resolve(fullPath)topath.resolve(root)to ensurefullPathis still insideroot(e.g.,startsWith(rootResolved + path.sep)or equalsrootResolved).
- Use
- Use that helper when processing ZIP entries:
- Maintain the existing check
if (!this.isSafePath(extractPath, filePath)) { ... return; }. - After passing this check, compute
fullPathin a way consistent with the validation (using the same normalized path used or implied byisSafePath).
- Maintain the existing check
- Ensure that we still allow directories and files to be created within
extractPathas before, so existing functionality remains the same except that malicious paths are skipped.
Because the snippet already calls this.isSafePath, we will implement that method within the CdnDownloadStrategy class (no external dependencies needed beyond Node’s built-in path module, which is already imported). We will also slightly adjust the fullPath computation to use path.resolve(extractPath, filePath) to align with the validation logic, which makes the relationship between the sanitized path and the write operation obvious.
-
Copy modified lines R12-R43 -
Copy modified line R528
| @@ -9,6 +9,38 @@ | ||
| export class CdnDownloadStrategy implements IPluginDownloadStrategy { | ||
| private static readonly MAX_RETRIES = 3; | ||
| private static readonly RETRY_DELAY_MS = 1000; | ||
|
|
||
| /** | ||
| * Validates that a path from an archive entry will resolve inside the given root directory. | ||
| * Protects against Zip Slip / directory traversal and absolute-path extraction. | ||
| */ | ||
| private isSafePath(rootDir: string, entryPath: string): boolean { | ||
| if (!entryPath) { | ||
| return false; | ||
| } | ||
|
|
||
| // Reject absolute paths outright | ||
| if (path.isAbsolute(entryPath)) { | ||
| return false; | ||
| } | ||
|
|
||
| // Normalize the path and remove any leading path separators | ||
| const normalizedEntry = path.normalize(entryPath).replace(/^([/\\])+/, ''); | ||
|
|
||
| // Quickly reject paths that still contain traversal segments | ||
| if (normalizedEntry.split(/[/\\]+/).includes('..')) { | ||
| return false; | ||
| } | ||
|
|
||
| // Resolve against the root directory and ensure the result stays within it | ||
| const rootResolved = path.resolve(rootDir); | ||
| const fullResolved = path.resolve(rootDir, normalizedEntry); | ||
|
|
||
| return ( | ||
| fullResolved === rootResolved || | ||
| fullResolved.startsWith(rootResolved + path.sep) | ||
| ); | ||
| } | ||
| private static readonly VALID_EXTENSION = '.zip'; | ||
| private static readonly MAX_FILE_SIZE = 500 * 1024 * 1024; // 500MB per file | ||
| private static readonly MAX_TOTAL_SIZE = 1024 * 1024 * 1024; // 1GB total extraction limit | ||
| @@ -493,7 +525,7 @@ | ||
| return; | ||
| } | ||
|
|
||
| const fullPath = path.join(extractPath, filePath); | ||
| const fullPath = path.resolve(extractPath, filePath); | ||
|
|
||
| if (type === 'Directory') { | ||
| await fs.mkdir(fullPath, { recursive: true }); |
| let baseSlug = name | ||
| .toLowerCase() | ||
| .replace(/[^a-z0-9\s-]/g, '') | ||
| .replace(/\s+/g, '-') | ||
| .replace(/-+/g, '-') | ||
| .replace(/^-+|-+$/g, ''); |
Check failure
Code scanning / CodeQL
Polynomial regular expression used on uncontrolled data High
regular expression
library input
| return count > 0; | ||
| } catch (error) { | ||
| // If there's an error checking, default to false (treat as free plugin) | ||
| console.error(`Error checking if plugin ${pluginId} requires subscription:`, error); |
Check failure
Code scanning / CodeQL
Use of externally-controlled format string High
user-provided value
Format string depends on a
user-provided value
Format string depends on a
user-provided value
Format string depends on a
user-provided value
Format string depends on a
user-provided value
Format string depends on a
user-provided value
Format string depends on a
user-provided value
Format string depends on a
user-provided value
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 24 days ago
In general, to fix externally-controlled format string issues with console.log/console.error/util.format, you should avoid putting untrusted data directly in the format string position. Instead, use a constant format string with %s placeholders (or string concatenation) and pass the untrusted data as subsequent arguments, or log a structured object.
For this specific case in plugin-subscription-plan.service.ts, we should remove the template literal interpolation of pluginId and instead either: (a) use a constant format string with %s and pass pluginId as an argument, or (b) concatenate strings so the first argument no longer contains % from untrusted data. Since the code already passes a second argument (error), the safest and most idiomatic fix is to keep using multiple arguments but make the first one a fixed string and move the plugin ID into an additional argument. This preserves logging behavior (message + error object) while eliminating the possibility that pluginId changes how error is formatted.
Concretely, in packages/plugins/registry/src/lib/domain/services/plugin-subscription-plan.service.ts, within the isSubscriptionRequired method’s catch block, change:
console.error(`Error checking if plugin ${pluginId} requires subscription:`, error);to something like:
console.error('Error checking if plugin requires subscription for pluginId:', pluginId, error);Now the first argument is a static string; pluginId is a separate argument and cannot affect formatting. No new imports or helper methods are needed, and this single change addresses all alert variants because they all flow into this same sink.
-
Copy modified line R372
| @@ -369,7 +369,7 @@ | ||
| return count > 0; | ||
| } catch (error) { | ||
| // If there's an error checking, default to false (treat as free plugin) | ||
| console.error(`Error checking if plugin ${pluginId} requires subscription:`, error); | ||
| console.error('Error checking if plugin requires subscription for pluginId:', pluginId, error); | ||
| return false; | ||
| } | ||
| } |
|
Skipped: This PR changes more files than the configured file change limit: ( |
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the ✨ Finishing touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
10 issues found across 717 files
Note: This PR contains a large number of files. cubic only reviews up to 75 files per PR, so some files may not have been reviewed.
Prompt for AI agents (all issues)
Check if these issues are valid — if so, understand the root cause of each and fix them.
<file name=".cspell.json">
<violation number="1" location=".cspell.json:805">
P0: Missing comma after `"Liskov"` creates invalid JSON. This will cause the cspell configuration file to fail parsing.</violation>
</file>
<file name="packages/desktop-ui-lib/src/lib/settings/plugins/component/plugin-marketplace/+state/effects/plugin-user-assignment.effects.ts">
<violation number="1" location="packages/desktop-ui-lib/src/lib/settings/plugins/component/plugin-marketplace/+state/effects/plugin-user-assignment.effects.ts:110">
P1: Incrementing skip before the HTTP request completes causes pagination inconsistency. If `switchMap` cancels the request (due to rapid successive calls), skip will be incremented but data won't be loaded. Move `incrementSkip()` to the success handler, or use `exhaustMap` instead of `switchMap` to prevent request cancellation.</violation>
</file>
<file name="packages/desktop-ui-lib/src/lib/settings/plugins/component/plugin-marketplace/+state/effects/plugin-marketplace.effect.ts">
<violation number="1" location="packages/desktop-ui-lib/src/lib/settings/plugins/component/plugin-marketplace/+state/effects/plugin-marketplace.effect.ts:109">
P2: Debug `console.log` statement should be removed before merging to production code.</violation>
</file>
<file name="packages/desktop-lib/src/lib/plugin-system/data-access/plugin-manager.ts">
<violation number="1" location="packages/desktop-lib/src/lib/plugin-system/data-access/plugin-manager.ts:109">
P2: Missing null coalescing for `installationId` - other optional fields (`marketplaceId`, `versionId`) use `? ... : null` pattern to ensure consistent database storage. Consider applying the same pattern here.</violation>
</file>
<file name="packages/core/src/lib/core/context/request-context.ts">
<violation number="1" location="packages/core/src/lib/core/context/request-context.ts:195">
P1: Security concern: This method falls back to reading `organization-id` from untrusted HTTP headers when the user's employee organization ID is not available. Unlike similar methods (`currentTenantId`, `currentUserId`, `currentRoleId`) which only read from the authenticated user object, this pattern allows clients to potentially spoof organization context by setting the header. If this value is used for authorization or data filtering, it could lead to unauthorized cross-organization access. Consider validating the header value against the user's allowed organizations or removing the header fallback entirely.</violation>
</file>
<file name="packages/desktop-ui-lib/src/lib/settings/plugins/component/plugin-marketplace/+state/effects/plugin-category.effect.ts">
<violation number="1" location="packages/desktop-ui-lib/src/lib/settings/plugins/component/plugin-marketplace/+state/effects/plugin-category.effect.ts:35">
P1: Stale state bug: `state` is captured before `incrementPage()` is called, so `state.pagination.page` will have the old page number, not the incremented value. This will cause the same page to be fetched repeatedly.</violation>
</file>
<file name="packages/desktop-ui-lib/src/lib/settings/plugins/component/plugin-marketplace/+state/effects/plugin-installation.effect.ts">
<violation number="1" location="packages/desktop-ui-lib/src/lib/settings/plugins/component/plugin-marketplace/+state/effects/plugin-installation.effect.ts:297">
P1: Missing `pluginId` in `serverInstallationFailed` action dispatch. The `handleServerInstallationFailure$` effect checks `if (pluginId)` before cleanup and rollback, so without it, state won't be reset and `revertFailedInstallation` won't be called.</violation>
<violation number="2" location="packages/desktop-ui-lib/src/lib/settings/plugins/component/plugin-marketplace/+state/effects/plugin-installation.effect.ts:353">
P1: Missing `pluginId` in `installationFailed` action dispatch. The `handleInstallationFailure$` effect checks `if (pluginId)` before cleanup, so without it, installation state won't be properly reset.</violation>
<violation number="3" location="packages/desktop-ui-lib/src/lib/settings/plugins/component/plugin-marketplace/+state/effects/plugin-installation.effect.ts:388">
P1: Missing `marketplaceId` in `activationFailed` action dispatch. The `handleActivationFailure$` effect checks `if (pluginId)` before cleanup, so without it, state won't be properly reset. Note: `executeActivation$` correctly passes it, but this effect does not.</violation>
</file>
<file name="packages/desktop-ui-lib/src/lib/settings/plugins/component/plugin-marketplace/+state/effects/available-users.effects.ts">
<violation number="1" location="packages/desktop-ui-lib/src/lib/settings/plugins/component/plugin-marketplace/+state/effects/available-users.effects.ts:174">
P1: Missing `{ dispatch: false }` on effect. This effect only performs side effects via `tap()` and doesn't dispatch a new action. Following the pattern in plugin-settings.effects.ts, add `{ dispatch: false }` as the second argument to `createEffect()`.</violation>
</file>
Reply to cubic to teach it or ask questions. Re-run a review with @cubic-dev-ai review this PR
| "prepopulate", | ||
| "prepopulates", | ||
| "prepopulation", | ||
| "Liskov" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P0: Missing comma after "Liskov" creates invalid JSON. This will cause the cspell configuration file to fail parsing.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .cspell.json, line 805:
<comment>Missing comma after `"Liskov"` creates invalid JSON. This will cause the cspell configuration file to fail parsing.</comment>
<file context>
@@ -658,6 +658,151 @@
+ "prepopulate",
+ "prepopulates",
+ "prepopulation",
+ "Liskov"
"Zrdm"
],
</file context>
| "Liskov" | |
| "Liskov", |
| ofType(PluginUserAssignmentActions.loadMoreAssignments), | ||
| tap(() => { | ||
| this.store.setLoadingMore(true); | ||
| this.store.incrementSkip(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P1: Incrementing skip before the HTTP request completes causes pagination inconsistency. If switchMap cancels the request (due to rapid successive calls), skip will be incremented but data won't be loaded. Move incrementSkip() to the success handler, or use exhaustMap instead of switchMap to prevent request cancellation.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/desktop-ui-lib/src/lib/settings/plugins/component/plugin-marketplace/+state/effects/plugin-user-assignment.effects.ts, line 110:
<comment>Incrementing skip before the HTTP request completes causes pagination inconsistency. If `switchMap` cancels the request (due to rapid successive calls), skip will be incremented but data won't be loaded. Move `incrementSkip()` to the success handler, or use `exhaustMap` instead of `switchMap` to prevent request cancellation.</comment>
<file context>
@@ -0,0 +1,794 @@
+ ofType(PluginUserAssignmentActions.loadMoreAssignments),
+ tap(() => {
+ this.store.setLoadingMore(true);
+ this.store.incrementSkip();
+ }),
+ switchMap(({ pluginId }) => {
</file context>
| })) | ||
| ), | ||
| tap((response) => { | ||
| console.log('API Response:', response); // Debug log |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: Debug console.log statement should be removed before merging to production code.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/desktop-ui-lib/src/lib/settings/plugins/component/plugin-marketplace/+state/effects/plugin-marketplace.effect.ts, line 109:
<comment>Debug `console.log` statement should be removed before merging to production code.</comment>
<file context>
@@ -58,14 +105,30 @@ export class PluginMarketplaceEffects {
- }))
- ),
+ tap((response) => {
+ console.log('API Response:', response); // Debug log
+ const items = Array.isArray(response?.items)
+ ? response.items
</file context>
| version: pluginMetadata.version, | ||
| description: pluginMetadata.description, | ||
| main: pluginMetadata.main, | ||
| installationId: pluginMetadata.installationId, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: Missing null coalescing for installationId - other optional fields (marketplaceId, versionId) use ? ... : null pattern to ensure consistent database storage. Consider applying the same pattern here.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/desktop-lib/src/lib/plugin-system/data-access/plugin-manager.ts, line 109:
<comment>Missing null coalescing for `installationId` - other optional fields (`marketplaceId`, `versionId`) use `? ... : null` pattern to ensure consistent database storage. Consider applying the same pattern here.</comment>
<file context>
@@ -100,6 +106,7 @@ export class PluginManager implements IPluginManager {
version: pluginMetadata.version,
description: pluginMetadata.description,
main: pluginMetadata.main,
+ installationId: pluginMetadata.installationId,
marketplaceId: pluginMetadata.marketplaceId ? pluginMetadata.marketplaceId : null,
versionId: pluginMetadata.versionId ? pluginMetadata.versionId : null,
</file context>
| const user: IUser | null = RequestContext.currentUser(); | ||
| return ( | ||
| user?.employee?.organizationId || | ||
| (RequestContext.currentRequest()?.headers['organization-id'] as ID) || |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P1: Security concern: This method falls back to reading organization-id from untrusted HTTP headers when the user's employee organization ID is not available. Unlike similar methods (currentTenantId, currentUserId, currentRoleId) which only read from the authenticated user object, this pattern allows clients to potentially spoof organization context by setting the header. If this value is used for authorization or data filtering, it could lead to unauthorized cross-organization access. Consider validating the header value against the user's allowed organizations or removing the header fallback entirely.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/core/src/lib/core/context/request-context.ts, line 195:
<comment>Security concern: This method falls back to reading `organization-id` from untrusted HTTP headers when the user's employee organization ID is not available. Unlike similar methods (`currentTenantId`, `currentUserId`, `currentRoleId`) which only read from the authenticated user object, this pattern allows clients to potentially spoof organization context by setting the header. If this value is used for authorization or data filtering, it could lead to unauthorized cross-organization access. Consider validating the header value against the user's allowed organizations or removing the header fallback entirely.</comment>
<file context>
@@ -182,6 +182,21 @@ export class RequestContext {
+ const user: IUser | null = RequestContext.currentUser();
+ return (
+ user?.employee?.organizationId ||
+ (RequestContext.currentRequest()?.headers['organization-id'] as ID) ||
+ null
+ );
</file context>
| }), | ||
| switchMap(({ params = {} }) => { | ||
| const state = this.pluginCategoryStore.getValue(); | ||
| const paginationParams = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P1: Stale state bug: state is captured before incrementPage() is called, so state.pagination.page will have the old page number, not the incremented value. This will cause the same page to be fetched repeatedly.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/desktop-ui-lib/src/lib/settings/plugins/component/plugin-marketplace/+state/effects/plugin-category.effect.ts, line 35:
<comment>Stale state bug: `state` is captured before `incrementPage()` is called, so `state.pagination.page` will have the old page number, not the incremented value. This will cause the same page to be fetched repeatedly.</comment>
<file context>
@@ -0,0 +1,576 @@
+ }),
+ switchMap(({ params = {} }) => {
+ const state = this.pluginCategoryStore.getValue();
+ const paginationParams = {
+ ...(params as object),
+ page: state.pagination.page,
</file context>
| PluginInstallationActions.activationFailed( | ||
| error?.message || 'Failed to find installed plugin' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P1: Missing marketplaceId in activationFailed action dispatch. The handleActivationFailure$ effect checks if (pluginId) before cleanup, so without it, state won't be properly reset. Note: executeActivation$ correctly passes it, but this effect does not.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/desktop-ui-lib/src/lib/settings/plugins/component/plugin-marketplace/+state/effects/plugin-installation.effect.ts, line 388:
<comment>Missing `marketplaceId` in `activationFailed` action dispatch. The `handleActivationFailure$` effect checks `if (pluginId)` before cleanup, so without it, state won't be properly reset. Note: `executeActivation$` correctly passes it, but this effect does not.</comment>
<file context>
@@ -22,167 +47,743 @@ export class PluginInstallationEffects {
+ ),
+ catchError((error) =>
+ of(
+ PluginInstallationActions.activationFailed(
+ error?.message || 'Failed to find installed plugin'
+ )
</file context>
| PluginInstallationActions.activationFailed( | |
| error?.message || 'Failed to find installed plugin' | |
| PluginInstallationActions.activationFailed( | |
| error?.message || 'Failed to find installed plugin', | |
| marketplaceId |
| PluginInstallationActions.installationFailed( | ||
| error?.message || | ||
| this.translateService.instant('PLUGIN.TOASTR.ERROR.COMPLETE_INSTALLATION') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P1: Missing pluginId in installationFailed action dispatch. The handleInstallationFailure$ effect checks if (pluginId) before cleanup, so without it, installation state won't be properly reset.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/desktop-ui-lib/src/lib/settings/plugins/component/plugin-marketplace/+state/effects/plugin-installation.effect.ts, line 353:
<comment>Missing `pluginId` in `installationFailed` action dispatch. The `handleInstallationFailure$` effect checks `if (pluginId)` before cleanup, so without it, installation state won't be properly reset.</comment>
<file context>
@@ -22,167 +47,743 @@ export class PluginInstallationEffects {
- catchError((error) => this.handleError(error))
+ catchError((error) =>
+ of(
+ PluginInstallationActions.installationFailed(
+ error?.message ||
+ this.translateService.instant('PLUGIN.TOASTR.ERROR.COMPLETE_INSTALLATION')
</file context>
| PluginInstallationActions.installationFailed( | |
| error?.message || | |
| this.translateService.instant('PLUGIN.TOASTR.ERROR.COMPLETE_INSTALLATION') | |
| PluginInstallationActions.installationFailed( | |
| error?.message || | |
| this.translateService.instant('PLUGIN.TOASTR.ERROR.COMPLETE_INSTALLATION'), | |
| marketplaceId |
| PluginInstallationActions.serverInstallationFailed( | ||
| error?.message || 'Server installation failed' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P1: Missing pluginId in serverInstallationFailed action dispatch. The handleServerInstallationFailure$ effect checks if (pluginId) before cleanup and rollback, so without it, state won't be reset and revertFailedInstallation won't be called.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/desktop-ui-lib/src/lib/settings/plugins/component/plugin-marketplace/+state/effects/plugin-installation.effect.ts, line 297:
<comment>Missing `pluginId` in `serverInstallationFailed` action dispatch. The `handleServerInstallationFailure$` effect checks `if (pluginId)` before cleanup and rollback, so without it, state won't be reset and `revertFailedInstallation` won't be called.</comment>
<file context>
@@ -22,167 +47,743 @@ export class PluginInstallationEffects {
+ finalize(() => this.pluginInstallationStore.setServerInstalling(pluginId, false)),
+ catchError((error) =>
+ of(
+ PluginInstallationActions.serverInstallationFailed(
+ error?.message || 'Server installation failed'
+ )
</file context>
| PluginInstallationActions.serverInstallationFailed( | |
| error?.message || 'Server installation failed' | |
| PluginInstallationActions.serverInstallationFailed( | |
| error?.message || 'Server installation failed', | |
| pluginId |
| * Handle load users success | ||
| * Updates store with loaded users | ||
| */ | ||
| handleLoadUsersSuccess$ = createEffect(() => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P1: Missing { dispatch: false } on effect. This effect only performs side effects via tap() and doesn't dispatch a new action. Following the pattern in plugin-settings.effects.ts, add { dispatch: false } as the second argument to createEffect().
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/desktop-ui-lib/src/lib/settings/plugins/component/plugin-marketplace/+state/effects/available-users.effects.ts, line 174:
<comment>Missing `{ dispatch: false }` on effect. This effect only performs side effects via `tap()` and doesn't dispatch a new action. Following the pattern in plugin-settings.effects.ts, add `{ dispatch: false }` as the second argument to `createEffect()`.</comment>
<file context>
@@ -0,0 +1,402 @@
+ * Handle load users success
+ * Updates store with loaded users
+ */
+ handleLoadUsersSuccess$ = createEffect(() => {
+ return this.actions$.pipe(
+ ofType(AvailableUsersActions.loadUsersSuccess),
</file context>
PR
Please note: we will close your PR without comment if you do not check the boxes above and provide ALL requested information.
Summary by cubic
Introduces a full plugin marketplace with subscriptions, user access control, and a multi‑step installation flow tracked by installationId. Also improves desktop UI/state, download reliability, and streamlines CI builds.
New Features
Migration
Written for commit 28628e2. Summary will update automatically on new commits.