@flusys/ng-iam
v3.0.1
Published
Identity and Access Management (IAM) for Angular - Independent from ng-auth through Provider Interface Pattern
Downloads
1,449
Readme
@flusys/ng-iam Package Guide
Overview
@flusys/ng-iam provides Identity and Access Management (IAM) for FLUSYS applications. It includes permission state management, role-based access control (RBAC), action management with AND/OR prerequisite logic, and dynamic menu filtering based on user permissions.
Key Principle: ng-iam is 100% independent from ng-auth through the Provider Interface Pattern.
| Item | Value |
|------|-------|
| Package | @flusys/ng-iam |
| Version | 3.0.1 |
| Dependencies | ng-core, ng-shared, ng-layout (IMenuItem only) |
| CSS Framework | Tailwind CSS (not PrimeFlex) |
| Build | npm run build:ng-iam |
Architecture
ng-shared (defines interfaces + tokens)
↓
Your Auth System (implements interfaces with adapters)
↓
ng-iam (consumes interfaces via DI)ng-iam resolves all auth-related data at runtime via DI tokens defined in ng-shared.
Provider Interfaces
| Token | Source | Purpose | Required? |
|-------|--------|---------|-----------|
| USER_PROVIDER | ng-shared | User list for role/action assignment | Yes |
| LAYOUT_AUTH_STATE | ng-layout | Current company/branch context | Yes (if company enabled) |
| COMPANY_API_PROVIDER | ng-shared | Company list for whitelisting | Optional |
| USER_PERMISSION_PROVIDER | ng-shared | User branch permissions | Optional |
// Provider usage
const companyId = inject(LAYOUT_AUTH_STATE).currentCompanyInfo()?.id;
const response = await inject(USER_PERMISSION_PROVIDER).getUserBranchPermissions(userId).toPromise();Integration Options
With FLUSYS ng-auth (Recommended)
import { provideAuthProviders } from '@flusys/ng-auth';
export const appConfig: ApplicationConfig = {
providers: [provideAuthProviders()], // Registers all adapters
};With Custom Auth System
providers: [
{ provide: USER_PROVIDER, useClass: YourUserProvider },
{ provide: LAYOUT_AUTH_STATE, useClass: YourCompanyContextProvider },
{ provide: COMPANY_API_PROVIDER, useClass: YourCompanyApiProvider }, // Optional
{ provide: USER_PERMISSION_PROVIDER, useClass: YourUserPermissionProvider }, // Optional
]| Feature | Providers Needed | |---------|-----------------| | Basic role assignments | USER_PROVIDER only | | Company-scoped permissions | + LAYOUT_AUTH_STATE, COMPANY_API_PROVIDER | | Branch-scoped permissions | + LAYOUT_AUTH_STATE, USER_PERMISSION_PROVIDER |
Permission Modes
Configured via APP_CONFIG.iam.permissionMode:
| Mode | Structure | Tabs Available | Use Case | |------|-----------|---------------|----------| | DIRECT | User -> Actions | User-Actions | Small apps | | RBAC | User -> Role -> Actions | Role-Actions, User-Roles | Standard apps | | FULL | Both combined | All tabs | Maximum flexibility |
When company feature is enabled, a Company-Actions tab is added for whitelisting.
Routes
Export: IAM_ROUTES from @flusys/ng-iam
/iam → IamContainerComponent (tabbed navigation)
/actions → ActionListPageComponent (hierarchical tree)
/actions/new → ActionFormPageComponent (create)
/actions/:id → ActionFormPageComponent (edit)
/roles → RoleListPageComponent (paginated table)
/roles/new → RoleFormPageComponent (create)
/roles/:id → RoleFormPageComponent (edit)
/permissions → PermissionPageComponent (tabbed assignment UI)| Route | Guard | Permission |
|-------|-------|------------|
| /actions | permissionGuard | ACTION_PERMISSIONS.READ |
| /roles | permissionGuard | ROLE_PERMISSIONS.READ |
| /permissions | anyPermissionGuard | Multiple READ permissions |
Services
PermissionStateService
File: services/permission-state.service.ts | Provided at root level
Manages current user's permissions with signal-based state.
| API | Type | Description |
|-----|------|-------------|
| permissions | Signal<IMyPermissionsResponseDto \| null> | Current user permissions (readonly) |
| isLoading | Signal<boolean> | Loading state (readonly) |
| loadPermissions(dto?) | Observable<void> | Load from /iam/permissions/my-permissions |
| isLoaded() | boolean | Check if permissions loaded |
Permission checking delegated to PermissionValidatorService from ng-shared.
await firstValueFrom(permissionState.loadPermissions()); // Load in guard
validator.hasAction('user.create'); // Check permission
validator.hasAllActions(['user.create', 'user.update']); // Check multipleActionApiService
File: services/action-api.service.ts | Extends ApiResourceService<IUpdateActionDto, IAction>
Resource: iam/actions
| Method | Endpoint | Description |
|--------|----------|-------------|
| getTree(search?, isActive?, withDeleted?) | POST /iam/actions/tree | Hierarchical action tree |
| getActionsForPermission() | POST /iam/actions/tree-for-permission | Actions filtered by company whitelist |
Inherits standard CRUD: getAll, getById, insert, update, delete.
RoleApiService
File: services/role-api.service.ts | Extends ApiResourceService<IUpdateRoleDto, IRole>
Resource: iam/roles | Only available in RBAC/FULL mode.
PermissionApiService
File: services/permission-api.service.ts | Extends BaseApiService
All endpoints use POST (POST-only RPC convention).
| Method | Endpoint | Description |
|--------|----------|-------------|
| assignUserActions(data) | POST /iam/permissions/user-actions/assign | Assign/remove actions for user |
| getUserActions(userId, query?) | POST /iam/permissions/get-user-actions | Get user's direct actions |
| assignUserRoles(data) | POST /iam/permissions/user-roles/assign | Assign/remove roles for user |
| getUserRoles(userId, query?) | POST /iam/permissions/get-user-roles | Get user's roles |
| assignRoleActions(data) | POST /iam/permissions/role-actions/assign | Assign/remove actions for role |
| getRoleActions(roleId, query?) | POST /iam/permissions/get-role-actions | Get role's actions |
| assignCompanyActions(data) | POST /iam/permissions/company-actions/assign | Assign/remove company whitelist |
| getCompanyActions(companyId) | POST /iam/permissions/get-company-actions | Get company's whitelisted actions |
ActionPermissionLogicService
File: services/action-permission-logic.service.ts
Manages smart prerequisite validation using AND/OR logic trees.
| Method | Description |
|--------|-------------|
| handleCheck(action, selection, allActions, onUpdate, onCancel) | Handle selection with prerequisite validation |
| handleUncheck(action, selection, allActions, onUpdate) | Handle deselection with dependency detection |
| hasUnmetPrerequisites(action, selection, allActions) | Check if action has unmet prerequisites |
| showValidationErrorDialog(invalid, selection, allActions, onUpdate) | Pre-save validation with auto-fix |
| getPrerequisiteTooltip(action, selection, allActions) | Get tooltip text with prerequisite info |
Features: Recursive deep-scan, smart AND/OR optimization, alternative suggestions, pre-save validation.
Components
LogicBuilderComponent
Selector: lib-logic-builder | Visual AND/OR permission logic tree editor.
| Input/Output | Type | Description |
|--------------|------|-------------|
| logic (input) | ILogicNode \| null | Current permission logic tree |
| actions (input) | Array<{id, name}> | Available actions for dropdowns |
| logicChange (output) | ILogicNode \| null | Emitted when logic changes |
Selector Components (Common Pattern)
All selector components share a common pattern with these signals:
| Signal | Description |
|--------|-------------|
| selectedXxxId | Selected entity ID (user/role/company) |
| treeNodes / items | Available items (TreeNode[] or flat array) |
| flatActions | Flattened action list (for action selectors) |
| selectedIds | Currently selected IDs (Set) |
| originalSelectedIds | Original state for change tracking |
| isLoading / isSaving | Loading/saving states |
| pendingAdd / pendingRemove | Computed: items to add/remove |
| hasChanges | Computed: whether there are pending changes |
Common features: AbortController for request cancellation, responsive tables with overflow-x-auto, change tracking, select/deselect all.
UserActionSelectorComponent
Selector: flusys-user-action-selector | Assigns actions directly to users (DIRECT/FULL mode).
- User dropdown (
lib-user-selectfrom ng-shared) - Branch selector (conditional, from USER_PERMISSION_PROVIDER)
- Hierarchical TreeTable with checkboxes
- Prerequisite validation with tooltips
UserRoleSelectorComponent
Selector: flusys-user-role-selector | Assigns roles to users (RBAC/FULL mode).
- User dropdown with branch selector
- DataTable with pagination
- No prerequisite validation (roles are simple assignments)
RoleActionSelectorComponent
Selector: flusys-role-action-selector | Assigns actions to roles (RBAC/FULL mode).
- Role dropdown with filter
- Hierarchical TreeTable with checkboxes
- Prerequisite validation with auto-fix dialog
CompanyActionSelectorComponent
Selector: flusys-company-action-selector | Manages company action whitelisting.
- Company dropdown (from COMPANY_API_PROVIDER)
- Full action tree (not filtered)
- Backend prerequisite error handling with auto-fix
Page Components
Page components are internal (not exported). Accessed via IAM_ROUTES.
| Component | Selector | Features |
|-----------|----------|----------|
| IamContainerComponent | lib-iam-container | Tabbed navigation, responsive tabs |
| ActionListPageComponent | lib-action-list-page | TreeTable, CRUD, read-only indicators |
| ActionFormPageComponent | lib-action-form-page | Signal forms, LogicBuilder integration |
| RoleListPageComponent | lib-role-list-page | Paginated table (10/25/50), CRUD |
| RoleFormPageComponent | lib-role-form-page | Signal forms, company auto-assignment |
| PermissionPageComponent | lib-permission-page | Dynamic tabs based on permission mode |
Interfaces
Core Types
enum ActionType { BACKEND = 'backend', FRONTEND = 'frontend', BOTH = 'both' }
type PermissionAction = 'add' | 'remove';
type PermissionMode = 'rbac' | 'direct' | 'full';IAction (extends IBaseEntity)
interface IAction extends IBaseEntity {
readOnly: boolean;
name: string;
description: string | null;
code: string | null;
actionType: ActionType;
permissionLogic: ILogicNode | null; // AND/OR prerequisite rules
parentId: string | null;
serial: number | null;
isActive: boolean;
metadata: Record<string, unknown> | null;
}
interface IActionTreeDto extends IAction { children: IActionTreeDto[]; }IRole (extends IBaseEntity)
interface IRole extends IBaseEntity {
readOnly: boolean;
name: string;
description: string | null;
companyId: string | null;
isActive: boolean;
serial: number | null;
metadata: Record<string, unknown> | null;
}Assignment DTOs
interface IPermissionItemDto { id: string; action: PermissionAction; }
interface IAssignUserActionsDto {
userId: string; companyId?: string; branchId?: string; items: IPermissionItemDto[];
}
interface IAssignRoleActionsDto { roleId: string; items: IPermissionItemDto[]; }
interface IAssignCompanyActionsDto { companyId: string; items: IPermissionItemDto[]; }
interface IAssignUserRolesDto {
userId: string; companyId?: string; branchId?: string; items: IPermissionItemDto[];
}Response DTOs
All response DTOs include: id, entity reference IDs, actionCode/roleCode, actionName/roleName, createdAt.
interface IMyPermissionsResponseDto {
frontendActions: Array<{ id: string; code: string; name: string; description: string }>;
cachedEndpoints: number; // Endpoint count for backend PermissionGuard
}
interface IPermissionOperationResultDto {
success: boolean; added: number; removed: number; message: string;
prerequisiteErrors?: IPrerequisiteValidationError[];
}Simple Models
interface IUser { id: string; name: string; email: string; }
interface IBranch { id: string; name: string; companyId: string; }
interface ICompany { id: string; name: string; slug?: string; }Utils & Constants
Utils are internal (not exported from public API).
permission-logic.util.ts
| Function | Description |
|----------|-------------|
| validateActionPrerequisites(action, selectedIds, allActions) | Validate prerequisites respecting AND/OR |
| extractRequiredActionIds(logic, allActions) | Extract all action IDs from logic tree |
tree-utils.ts
| Function | Description |
|----------|-------------|
| flattenTree<T>(tree) | Flatten hierarchical tree into flat array |
| buildTreeFromFlat<T>(flatList) | Build tree from flat list using parentId |
| convertActionToTreeNode(actions) | Convert to PrimeNG TreeNode<IAction>[] |
Constants
export const MAX_DROPDOWN_ITEMS = 100; // Max items for dropdown listsResponsive Table Patterns
All IAM tables use consistent Tailwind CSS patterns:
<!-- Container: horizontal scroll on mobile, full-width on desktop -->
<div class="overflow-x-auto -mx-4 sm:mx-0">
<p-treeTable [tableStyle]="{ 'min-width': '50rem' }">...</p-treeTable>
</div>
<!-- Responsive columns -->
<th>Name</th> <!-- Always visible -->
<th class="hidden sm:table-cell">Code</th> <!-- 640px+ -->
<th class="hidden md:table-cell">Status</th> <!-- 768px+ -->
<th class="hidden lg:table-cell">Description</th> <!-- 1024px+ -->| Breakpoint | Class | Visibility |
|------------|-------|------------|
| Always | (none) | Always visible |
| sm | hidden sm:table-cell | 640px+ |
| md | hidden md:table-cell | 768px+ |
| lg | hidden lg:table-cell | 1024px+ |
Angular 21 Signal Forms Pattern
import { form, required } from '@angular/forms';
readonly formModel = {
name: '',
description: '',
code: '',
actionType: ActionType.FRONTEND,
parentId: null as string | null,
serial: null as number | null,
isActive: true,
permissionLogic: null as ILogicNode | null,
};
readonly actionForm = form(this.formModel, (f) => {
required(f.name, { message: 'Name is required' });
});
readonly canSubmit = computed(() => this.actionForm.valid() && !this.isSaving());Backend Integration
Required Endpoints
# Action CRUD
POST /iam/actions/tree
POST /iam/actions/tree-for-permission
POST /iam/actions/get-all
POST /iam/actions/get/:id
POST /iam/actions/insert
POST /iam/actions/update
POST /iam/actions/delete
# Role CRUD
POST /iam/roles/get-all
POST /iam/roles/get/:id
POST /iam/roles/insert
POST /iam/roles/update
POST /iam/roles/delete
# Permission Assignments
POST /iam/permissions/my-permissions
POST /iam/permissions/user-actions/assign
POST /iam/permissions/get-user-actions
POST /iam/permissions/user-roles/assign
POST /iam/permissions/get-user-roles
POST /iam/permissions/role-actions/assign
POST /iam/permissions/get-role-actions
POST /iam/permissions/company-actions/assign
POST /iam/permissions/get-company-actionsBest Practices
Permission Loading
// In guard - block navigation until ready
if (!permissionState.isLoaded()) {
await firstValueFrom(permissionState.loadPermissions());
}- Cache in memory (PermissionStateService handles this)
- Clear on logout
- Never store in localStorage (security risk)
- Never load in component
ngOnInit(race conditions)
Permission Checks
validator.hasAction('user.create'); // Single
validator.hasAllActions(['user.create', 'user.update']); // ALL required
validator.hasAnyAction(['user.view', 'user.list']); // ANY requiredCache check results in computed signals, not in templates.
Menu Filtering
this.layoutService.setMenu(menuItems);
this.layoutService.setPermissionChecker((code) => validator.hasAction(code));Action Naming
Format: module.operation or module.entity.operation
// Good: 'user.create', 'report.export', 'admin.system.config'
// Bad: 'create' (generic), 'userCreate' (not dot notation)LogicNode Pattern
// User needs 'user.create' AND ('user.update' OR 'user.delete')
const logic: ILogicNode = {
type: 'AND',
children: [
{ type: 'ACTION', value: 'user.create' },
{ type: 'OR', children: [
{ type: 'ACTION', value: 'user.update' },
{ type: 'ACTION', value: 'user.delete' },
]},
],
};Configuration
// Frontend: environment.ts
services: { iam: { baseUrl: 'http://localhost:2002/iam', enabled: true } }# Backend: .env
IAM_PERMISSION_MODE=FULL # DIRECT, RBAC, or FULL| Scenario | services.iam.enabled | Result |
|----------|---------------------|--------|
| Full IAM | true | Permission-based menu |
| IAM Disabled | false | Static menu (all items visible) |
| Service Down | true (API fails) | Graceful fallback to static menu |
Provider Functions
provideIamProviders()
Registers adapters for cross-package communication (e.g., ng-auth profile page).
import { provideIamProviders } from '@flusys/ng-iam';
providers: [...provideIamProviders()]Registers PROFILE_PERMISSION_PROVIDER with ProfilePermissionProviderAdapter.
Common Issues
| Problem | Solution |
|---------|----------|
| permissionState.isLoaded() returns false | Check services.iam.enabled: true, verify /iam/permissions/my-permissions works |
| Empty menu after login | Verify user has frontend actions, check requiredAction matches action codes |
| Menu items not filtered | Call layoutService.setPermissionChecker() after loading permissions |
| 401 on IAM endpoints | Ensure auth interceptor adds Authorization header |
| Provider not configured error | Add required providers to app.config.ts |
| Empty dropdowns | Ensure adapters return IListResponse<T> format |
| Table not scrollable on mobile | Add overflow-x-auto -mx-4 sm:mx-0 wrapper |
| Columns overflow on mobile | Add hidden sm:table-cell to non-essential columns |
See Also
- CORE-GUIDE.md - Configuration system (APP_CONFIG)
- AUTH-GUIDE.md - Authentication and provider adapters
- SHARED-GUIDE.md - Provider interface definitions
- LAYOUT-GUIDE.md - Menu system and filtering
Last Updated: 2026-02-25 Version: 3.0.1 Angular Version: 21
