Merged PR 2016: feat(core/auth): add type-safe role-based authorization library

feat(core/auth): add type-safe role-based authorization library

Created @isa/core/auth library with comprehensive role checking:
- RoleService for programmatic hasRole() checks
- IfRoleDirective for declarative *ifRole/*ifNotRole templates
- Type-safe Role enum (CallCenter, Store)
- TokenProvider abstraction with OAuth2 integration
- Signal-based reactive rendering with Angular effects
- Zero-configuration setup via InjectionToken factory

Fixed Bug #5451:
- Hide action buttons for HSC (CallCenter) users on reward order confirmation
- Applied *ifNotRole="Role.CallCenter" to actions container
- Actions now hidden while maintaining card visibility

Testing:
- 18/18 unit tests passing with Vitest
- JUnit and Cobertura reporting configured
- Complete test coverage for role checking logic

Documentation:
- Comprehensive README (817 lines) with API reference
- Usage examples and architecture diagrams
- Updated library-reference.md (62→63 libraries)

Technical:
- Handles both string and array JWT role formats
- Integrated with @isa/core/logging
- Standalone directive (no module imports)
- Full TypeScript type safety

Closes #5451

Related work items: #5451
This commit is contained in:
Lorenz Hilpert
2025-11-10 17:00:39 +00:00
committed by Nino Righi
parent c5ea5ed3ec
commit 2e0853c91a
21 changed files with 1539 additions and 412 deletions

View File

@@ -6,7 +6,7 @@ import { NavigationRoute } from './defs/navigation-route';
import {
encodeFormData,
mapCustomerInfoDtoToCustomerCreateFormData,
} from 'apps/isa-app/src/page/customer';
} from '@page/customer';
@Injectable({ providedIn: 'root' })
export class CustomerCreateNavigation {
@@ -58,7 +58,7 @@ export class CustomerCreateNavigation {
},
];
let formData = params?.customerInfo
const formData = params?.customerInfo
? encodeFormData(
mapCustomerInfoDtoToCustomerCreateFormData(params.customerInfo),
)

View File

@@ -1,11 +1,11 @@
# Library Reference Guide
> **Last Updated:** 2025-10-27
> **Last Updated:** 2025-01-10
> **Angular Version:** 20.1.2
> **Nx Version:** 21.3.2
> **Total Libraries:** 62
> **Total Libraries:** 63
All 62 libraries in the monorepo have comprehensive README.md documentation located at `libs/[domain]/[layer]/[feature]/README.md`.
All 63 libraries in the monorepo have comprehensive README.md documentation located at `libs/[domain]/[layer]/[feature]/README.md`.
**IMPORTANT: Always use the `docs-researcher` subagent** to retrieve and analyze library documentation. This keeps the main context clean and prevents pollution.
@@ -82,7 +82,14 @@ A comprehensive print management library for Angular applications providing prin
---
## Core Libraries (5 libraries)
## Core Libraries (6 libraries)
### `@isa/core/auth`
Type-safe role-based authorization utilities with Angular signals integration for the ISA Frontend application. Provides Role enum, RoleService for programmatic checks, and IfRoleDirective for declarative template rendering with automatic JWT token parsing via OAuthService.
**Location:** `libs/core/auth/`
**Testing:** Vitest (18 passing tests)
**Features:** Signal-based reactivity, type-safe Role enum, zero-configuration OAuth2 integration
### `@isa/core/config`
A lightweight, type-safe configuration management system for Angular applications with runtime validation and nested object access.

View File

@@ -16,6 +16,7 @@
</div>
<div
*ifNotRole="Role.CallCenter"
class="flex flex-col gap-[0.62rem] desktop-large:gap-0 desktop-large:flex-row desktop-large:justify-between desktop-large:items-center"
>
<ui-dropdown

View File

@@ -32,6 +32,7 @@ import {
hasOrderTypeFeature,
buildItemQuantityMap,
} from '@isa/checkout/data-access';
import { IfRoleDirective, Role } from '@isa/core/auth';
@Component({
selector: 'checkout-confirmation-list-item-action-card',
@@ -43,6 +44,7 @@ import {
ButtonComponent,
DropdownButtonComponent,
DropdownOptionComponent,
IfRoleDirective,
],
providers: [
provideIcons({ isaActionCheck }),
@@ -52,6 +54,7 @@ import {
],
})
export class ConfirmationListItemActionCardComponent {
protected readonly Role = Role;
LoyaltyCollectType = LoyaltyCollectType;
ProcessingStatusState = ProcessingStatusState;
#orderRewardCollectFacade = inject(OrderRewardCollectFacade);

816
libs/core/auth/README.md Normal file
View File

@@ -0,0 +1,816 @@
# @isa/core/auth
Type-safe role-based authorization utilities with Angular signals integration for the ISA Frontend application.
## Table of Contents
- [Overview](#overview)
- [Features](#features)
- [Quick Start](#quick-start)
- [Core Concepts](#core-concepts)
- [API Reference](#api-reference)
- [Role (Enum)](#role-enum)
- [RoleService](#roleservice)
- [IfRoleDirective](#ifroledirective)
- [TokenProvider](#tokenprovider)
- [Usage Examples](#usage-examples)
- [Configuration](#configuration)
- [Testing](#testing)
- [Architecture](#architecture)
- [Dependencies](#dependencies)
## Overview
`@isa/core/auth` provides a lightweight, type-safe system for managing role-based authorization in Angular applications. Built with modern Angular patterns (signals, standalone components), it integrates seamlessly with OAuth2 authentication flows.
### The Problem It Solves
Traditional role-checking often involves:
- ❌ String literals scattered throughout templates and components
- ❌ No compile-time safety for role names
- ❌ Manual token parsing and claim extraction
- ❌ Repetitive conditional rendering logic
This library provides:
- ✅ Type-safe `Role` enum with autocomplete
- ✅ Automatic JWT token parsing via `OAuthService`
- ✅ Declarative role-based rendering with `*ifRole` directive
- ✅ Reactive updates using Angular signals
- ✅ Centralized role management
## Features
- 🔐 **Type-Safe Roles** - Enum-based role definitions prevent typos
- 🎯 **Declarative Templates** - `*ifRole` and `*ifNotRole` structural directives
-**Signal-Based** - Reactive role checking with Angular signals
- 🔄 **Flexible Token Provider** - Injectable abstraction with OAuth2 default
- 📝 **Comprehensive Logging** - Integrated with `@isa/core/logging`
- 🧪 **Fully Tested** - 18 unit tests with Vitest
- 🎨 **Standalone** - No module imports required
## Quick Start
**1. Import the directive and Role enum:**
```typescript
import { Component } from '@angular/core';
import { IfRoleDirective, Role } from '@isa/core/auth';
@Component({
selector: 'app-dashboard',
standalone: true,
imports: [IfRoleDirective],
template: `
<!-- Show content only for Store users -->
<div *ifRole="Role.Store">
<h2>Store Dashboard</h2>
<!-- Store-specific features -->
</div>
<!-- Show content only for CallCenter users -->
<div *ifRole="Role.CallCenter">
<h2>CallCenter Dashboard</h2>
<!-- CallCenter-specific features -->
</div>
<!-- Hide content from CallCenter users -->
<div *ifNotRole="Role.CallCenter">
<button>Complete Order</button>
</div>
`
})
export class DashboardComponent {
protected readonly Role = Role; // Expose to template
}
```
**2. Use RoleService programmatically:**
```typescript
import { Component, inject } from '@angular/core';
import { RoleService, Role } from '@isa/core/auth';
@Component({
selector: 'app-nav',
template: `...`
})
export class NavComponent {
private readonly roleService = inject(RoleService);
ngOnInit() {
if (this.roleService.hasRole(Role.Store)) {
// Enable store-specific navigation
}
}
}
```
**3. No configuration needed!** The library automatically uses `OAuthService` to parse JWT tokens.
## Core Concepts
### Role Enum
Roles are defined as a const object with TypeScript type safety:
```typescript
export const Role = {
CallCenter: 'CallCenter', // HSC (Hugendubel Service Center)
Store: 'Store', // Store/Branch users
} as const;
export type Role = (typeof Role)[keyof typeof Role];
```
**Benefits:**
- Autocomplete in IDEs
- Compile-time checking prevents invalid roles
- Easy to extend with new roles
### Token Provider Pattern
The library uses an injectable `TokenProvider` abstraction to decouple from specific authentication implementations:
```typescript
export interface TokenProvider {
getClaimByKey(key: string): unknown;
}
```
**Default Implementation:**
- Automatically provided via `InjectionToken` factory
- Uses `OAuthService.getAccessToken()` to fetch JWT
- Parses token using `parseJwt()` utility
- No manual configuration required
### Signal-Based Reactivity
The `IfRoleDirective` uses Angular effects for automatic re-rendering when roles change:
```typescript
constructor() {
effect(() => {
this.render(); // Re-render when ifRole/ifNotRole inputs change
});
}
```
## API Reference
### Role (Enum)
Type-safe role definitions for the application.
```typescript
export const Role = {
CallCenter: 'CallCenter', // HSC users
Store: 'Store', // Store users
} as const;
```
**Usage:**
```typescript
import { Role } from '@isa/core/auth';
if (roleService.hasRole(Role.Store)) {
// Type-safe!
}
```
---
### RoleService
Service for programmatic role checks.
#### Methods
##### `hasRole(role: Role | Role[]): boolean`
Check if the authenticated user has specific role(s).
**Parameters:**
- `role` - Single role or array of roles to check (AND logic for arrays)
**Returns:** `true` if user has all specified roles, `false` otherwise
**Examples:**
```typescript
import { inject } from '@angular/core';
import { RoleService, Role } from '@isa/core/auth';
export class ExampleComponent {
private readonly roleService = inject(RoleService);
checkAccess() {
// Single role check
if (this.roleService.hasRole(Role.Store)) {
console.log('User is a store employee');
}
// Multiple roles (AND logic)
if (this.roleService.hasRole([Role.Store, Role.CallCenter])) {
console.log('User has BOTH store AND call center access');
}
// Multiple checks
const isStore = this.roleService.hasRole(Role.Store);
const isCallCenter = this.roleService.hasRole(Role.CallCenter);
if (isStore || isCallCenter) {
console.log('User has at least one role (OR logic)');
}
}
}
```
**Logging:**
The service logs all role checks at `debug` level:
```
[RoleService] Role check: Store => true
[RoleService] Role check: Store, CallCenter => false
```
---
### IfRoleDirective
Structural directive for declarative role-based rendering.
**Selector:** `[ifRole]`, `[ifRoleElse]`, `[ifNotRole]`, `[ifNotRoleElse]`
#### Inputs
| Input | Type | Description |
|-------|------|-------------|
| `ifRole` | `Role \| Role[]` | Role(s) required to show template |
| `ifRoleElse` | `TemplateRef` | Alternative template if user lacks role |
| `ifNotRole` | `Role \| Role[]` | Role(s) that should NOT be present |
| `ifNotRoleElse` | `TemplateRef` | Alternative template if user has role |
#### Examples
**Basic Usage:**
```html
<!-- Show for Store users -->
<div *ifRole="Role.Store">
Store-specific content
</div>
<!-- Show for CallCenter users -->
<div *ifRole="Role.CallCenter">
CallCenter-specific content
</div>
```
**With Else Template:**
```html
<div *ifRole="Role.Store; else noAccess">
<button>Complete Order</button>
</div>
<ng-template #noAccess>
<p>You don't have permission to complete orders</p>
</ng-template>
```
**Negation (`ifNotRole`):**
```html
<!-- Hide from CallCenter users -->
<div *ifNotRole="Role.CallCenter">
<button>Release Reward</button>
<button>Mark Not Found</button>
<button>Cancel</button>
</div>
```
**Multiple Roles (AND logic):**
```html
<!-- Only show if user has BOTH roles -->
<div *ifRole="[Role.Store, Role.CallCenter]">
Advanced features requiring both roles
</div>
```
**Component Integration:**
```typescript
import { Component } from '@angular/core';
import { IfRoleDirective, Role } from '@isa/core/auth';
@Component({
selector: 'app-actions',
standalone: true,
imports: [IfRoleDirective],
template: `
<div *ifNotRole="Role.CallCenter">
<button (click)="completeOrder()">Complete</button>
</div>
`
})
export class ActionsComponent {
// Expose Role to template
protected readonly Role = Role;
completeOrder() {
// ...
}
}
```
---
### TokenProvider
Injectable abstraction for JWT token parsing.
```typescript
export interface TokenProvider {
getClaimByKey(key: string): unknown;
}
```
**Default Implementation:**
Automatically provided via `InjectionToken` factory:
```typescript
export const TOKEN_PROVIDER = new InjectionToken<TokenProvider>(
'TOKEN_PROVIDER',
{
providedIn: 'root',
factory: () => {
const oAuthService = inject(OAuthService);
return {
getClaimByKey: (key: string) => {
const claims = parseJwt(oAuthService.getAccessToken());
return claims?.[key] ?? null;
},
};
},
},
);
```
**Custom Provider (Advanced):**
Override the default implementation:
```typescript
import { TOKEN_PROVIDER, TokenProvider } from '@isa/core/auth';
providers: [
{
provide: TOKEN_PROVIDER,
useValue: {
getClaimByKey: (key: string) => {
// Custom token parsing logic
return myCustomAuthService.getClaim(key);
}
} as TokenProvider
}
]
```
---
### parseJwt()
Utility function to parse JWT tokens.
```typescript
export function parseJwt(
token: string | null
): Record<string, unknown> | null
```
**Parameters:**
- `token` - JWT token string or null
**Returns:** Parsed claims object or null
**Example:**
```typescript
import { parseJwt } from '@isa/core/auth';
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...';
const claims = parseJwt(token);
console.log(claims?.['role']); // ['Store']
console.log(claims?.['sub']); // User ID
```
## Usage Examples
### Example 1: Conditional Navigation
```typescript
import { Component, inject } from '@angular/core';
import { RouterLink } from '@angular/router';
import { IfRoleDirective, Role } from '@isa/core/auth';
@Component({
selector: 'app-side-menu',
standalone: true,
imports: [RouterLink, IfRoleDirective],
template: `
<nav>
<!-- Store-only navigation -->
<a *ifRole="Role.Store" routerLink="/inventory">
Inventory Management
</a>
<a *ifRole="Role.Store" routerLink="/store-orders">
Store Orders
</a>
<!-- CallCenter-only navigation -->
<a *ifRole="Role.CallCenter" routerLink="/customer-service">
Customer Service
</a>
<!-- Show for both roles -->
<a routerLink="/dashboard">
Dashboard
</a>
</nav>
`
})
export class SideMenuComponent {
protected readonly Role = Role;
}
```
### Example 2: Guard with RoleService
```typescript
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { RoleService, Role } from '@isa/core/auth';
export const storeGuard: CanActivateFn = () => {
const roleService = inject(RoleService);
const router = inject(Router);
if (roleService.hasRole(Role.Store)) {
return true;
}
// Redirect to unauthorized page
return router.createUrlTree(['/unauthorized']);
};
// Route configuration
export const routes = [
{
path: 'inventory',
component: InventoryComponent,
canActivate: [storeGuard]
}
];
```
### Example 3: Computed Signals with Roles
```typescript
import { Component, inject, computed } from '@angular/core';
import { RoleService, Role } from '@isa/core/auth';
import { toSignal } from '@angular/core/rxjs-interop';
@Component({
selector: 'app-dashboard',
template: `
@if (canManageInventory()) {
<button (click)="openInventory()">Manage Inventory</button>
}
@if (canProcessReturns()) {
<button (click)="openReturns()">Process Returns</button>
}
`
})
export class DashboardComponent {
private readonly roleService = inject(RoleService);
// Computed permissions
canManageInventory = computed(() =>
this.roleService.hasRole(Role.Store)
);
canProcessReturns = computed(() =>
this.roleService.hasRole([Role.Store, Role.CallCenter])
);
openInventory() { /* ... */ }
openReturns() { /* ... */ }
}
```
### Example 4: Real-World Component (Reward Order Confirmation)
```typescript
import { Component } from '@angular/core';
import { IfRoleDirective, Role } from '@isa/core/auth';
import { ButtonComponent } from '@isa/ui/buttons';
@Component({
selector: 'checkout-confirmation-actions',
standalone: true,
imports: [IfRoleDirective, ButtonComponent],
template: `
<div class="action-card">
<div class="message">
Please complete the order or select an action.
</div>
<!-- Hide actions from CallCenter (HSC) users -->
<div *ifNotRole="Role.CallCenter" class="actions">
<select [(ngModel)]="selectedAction">
<option value="collect">Release Reward</option>
<option value="not-found">Not Found</option>
<option value="cancel">Cancel</option>
</select>
<button uiButton color="primary" (click)="complete()">
Complete
</button>
</div>
</div>
`
})
export class ConfirmationActionsComponent {
protected readonly Role = Role;
selectedAction = 'collect';
complete() {
// Complete order logic
}
}
```
## Configuration
### Default Configuration (Recommended)
No configuration needed! The library automatically uses `OAuthService`:
```typescript
import { Component } from '@angular/core';
import { IfRoleDirective, Role } from '@isa/core/auth';
@Component({
standalone: true,
imports: [IfRoleDirective],
// Works out of the box!
})
export class MyComponent {}
```
### Custom TokenProvider (Advanced)
Override the default token provider:
```typescript
import { ApplicationConfig } from '@angular/core';
import { TOKEN_PROVIDER, TokenProvider } from '@isa/core/auth';
export const appConfig: ApplicationConfig = {
providers: [
{
provide: TOKEN_PROVIDER,
useFactory: () => {
const customAuth = inject(CustomAuthService);
return {
getClaimByKey: (key: string) => customAuth.getClaim(key)
} as TokenProvider;
}
}
]
};
```
### JWT Token Structure
The library expects JWT tokens with a `role` claim:
```json
{
"sub": "user123",
"role": ["Store"],
"exp": 1234567890
}
```
**Supported formats:**
- Single role: `"role": "Store"`
- Multiple roles: `"role": ["Store", "CallCenter"]`
## Testing
### Run Tests
```bash
# Run all tests
npx nx test core-auth
# Run with coverage
npx nx test core-auth --coverage.enabled=true
# Skip cache (fresh run)
npx nx test core-auth --skip-nx-cache
```
### Test Results
```
✓ src/lib/role.service.spec.ts (11 tests)
✓ src/lib/if-role.directive.spec.ts (7 tests)
Test Files 2 passed (2)
Tests 18 passed (18)
```
### Testing in Your App
**Mock RoleService:**
```typescript
import { describe, it, expect, vi } from 'vitest';
import { TestBed } from '@angular/core/testing';
import { RoleService, Role } from '@isa/core/auth';
describe('MyComponent', () => {
let roleService: RoleService;
beforeEach(() => {
roleService = {
hasRole: vi.fn().mockReturnValue(true)
} as any;
TestBed.configureTestingModule({
providers: [
{ provide: RoleService, useValue: roleService }
]
});
});
it('should show store content for store users', () => {
vi.spyOn(roleService, 'hasRole').mockReturnValue(true);
const fixture = TestBed.createComponent(MyComponent);
fixture.detectChanges();
expect(roleService.hasRole).toHaveBeenCalledWith(Role.Store);
// Assert UI changes
});
});
```
**Mock TokenProvider:**
```typescript
import { TOKEN_PROVIDER, TokenProvider, Role } from '@isa/core/auth';
const mockTokenProvider: TokenProvider = {
getClaimByKey: vi.fn().mockReturnValue([Role.Store])
};
TestBed.configureTestingModule({
providers: [
{ provide: TOKEN_PROVIDER, useValue: mockTokenProvider }
]
});
```
## Architecture
### Design Patterns
**1. Token Provider Pattern**
- Abstracts JWT parsing behind injectable interface
- Allows custom implementations without changing consumers
- Default factory provides OAuthService integration
**2. Signal-Based Reactivity**
- Uses Angular signals for reactive role checks
- Effect-driven template updates
- Minimal re-renders with fine-grained reactivity
**3. Type-Safe Enum Pattern**
- Const object with `as const` assertion
- Provides autocomplete and compile-time safety
- Prevents typos and invalid role strings
### Architecture Diagram
```
┌─────────────────────────────────────────────────────┐
│ Application Layer │
│ ┌─────────────────┐ ┌──────────────────┐ │
│ │ Components │ │ Route Guards │ │
│ │ (Templates) │ │ │ │
│ └────────┬────────┘ └────────┬─────────┘ │
│ │ │ │
│ │ *ifRole │ hasRole() │
│ ▼ ▼ │
├───────────────────────────────────────────────────┤
│ @isa/core/auth Library │
│ ┌──────────────────┐ ┌─────────────────┐ │
│ │ IfRoleDirective │ │ RoleService │ │
│ │ (Signals) │──────▶│ (Injectable) │ │
│ └──────────────────┘ └────────┬────────┘ │
│ │ │
│ hasRole(Role[]) │
│ │ │
│ ▼ │
│ ┌────────────────────┐ │
│ │ TokenProvider │ │
│ │ (InjectionToken) │ │
│ └────────┬───────────┘ │
│ │ │
│ getClaimByKey('role') │
│ │ │
├───────────────────────────────────┼──────────────┤
│ ▼ │
│ ┌──────────────────────────┐ │
│ │ OAuthService │ │
│ │ (angular-oauth2-oidc) │ │
│ └──────────┬───────────────┘ │
│ │ │
│ getAccessToken() │
│ │ │
└──────────────────────────┼────────────────────────┘
┌───────────────┐
│ JWT Token │
│ { role: ... }│
└───────────────┘
```
### Role Claim Handling
The library handles both single and multiple role formats:
```typescript
// Single role (string)
{ "role": "Store" }
// Multiple roles (array)
{ "role": ["Store", "CallCenter"] }
// Internal normalization using coerceArray()
const userRolesArray = coerceArray(userRoles);
```
## Dependencies
### External Dependencies
- **`@angular/core`** - Angular framework
- **`@angular/cdk/coercion`** - Array coercion utility
- **`angular-oauth2-oidc`** - OAuth2/OIDC authentication
- **`@isa/core/logging`** - Logging integration
### Internal Dependencies
No other ISA libraries required beyond `@isa/core/logging`.
### Import Path
```typescript
import {
RoleService,
IfRoleDirective,
Role,
TokenProvider,
TOKEN_PROVIDER,
parseJwt
} from '@isa/core/auth';
```
**Path Alias:** `@isa/core/auth``libs/core/auth/src/index.ts`
## Related Documentation
- [CLAUDE.md](../../../CLAUDE.md) - Project guidelines
- [Testing Guidelines](../../../docs/guidelines/testing.md) - Vitest setup
- [Library Reference](../../../docs/library-reference.md) - All libraries
## Related Libraries
- [`@isa/core/logging`](../logging/README.md) - Structured logging
- [`@isa/core/config`](../config/README.md) - Configuration management
- [`@isa/core/storage`](../storage/README.md) - State persistence
---
**License:** ISC
**Version:** 1.0.0
**Last Updated:** 2025-01-10

View File

@@ -0,0 +1,34 @@
const nx = require('@nx/eslint-plugin');
const baseConfig = require('../../../eslint.config.js');
module.exports = [
...baseConfig,
...nx.configs['flat/angular'],
...nx.configs['flat/angular-template'],
{
files: ['**/*.ts'],
rules: {
'@angular-eslint/directive-selector': [
'error',
{
type: 'attribute',
prefix: 'lib',
style: 'camelCase',
},
],
'@angular-eslint/component-selector': [
'error',
{
type: 'element',
prefix: 'lib',
style: 'kebab-case',
},
],
},
},
{
files: ['**/*.html'],
// Override or add rules here
rules: {},
},
];

View File

@@ -0,0 +1,20 @@
{
"name": "core-auth",
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/core/auth/src",
"prefix": "lib",
"projectType": "library",
"tags": [],
"targets": {
"test": {
"executor": "@nx/vite:test",
"outputs": ["{options.reportsDirectory}"],
"options": {
"reportsDirectory": "../../../coverage/libs/core/auth"
}
},
"lint": {
"executor": "@nx/eslint:lint"
}
}
}

View File

@@ -0,0 +1,10 @@
/**
* Core Auth Library
*
* Provides role-based authorization utilities for the ISA Frontend application.
*/
export { RoleService } from './lib/role.service';
export { IfRoleDirective } from './lib/if-role.directive';
export { TokenProvider, TOKEN_PROVIDER, parseJwt } from './lib/token-provider';
export { Role } from './lib/role';

View File

@@ -0,0 +1,157 @@
import { Component } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { IfRoleDirective } from './if-role.directive';
import { RoleService } from './role.service';
import { Role } from './role';
@Component({
standalone: true,
imports: [IfRoleDirective],
template: `
<div *ifRole="role" data-test="content">Store Content</div>
`,
})
class TestIfRoleComponent {
role = Role.Store;
}
@Component({
standalone: true,
imports: [IfRoleDirective],
template: `
<div *ifRole="role; else noAccess" data-test="content">Store Content</div>
<ng-template #noAccess>
<div data-test="else">No Access</div>
</ng-template>
`,
})
class TestIfRoleElseComponent {
role = Role.Store;
}
@Component({
standalone: true,
imports: [IfRoleDirective],
template: `
<div *ifNotRole="role" data-test="content">Non-Store Content</div>
`,
})
class TestIfNotRoleComponent {
role = Role.Store;
}
describe('IfRoleDirective', () => {
let roleService: { hasRole: ReturnType<typeof vi.fn> };
beforeEach(() => {
roleService = {
hasRole: vi.fn(),
};
TestBed.configureTestingModule({
providers: [{ provide: RoleService, useValue: roleService }],
});
});
describe('ifRole', () => {
it('should render content when user has role', () => {
roleService.hasRole.mockReturnValue(true);
const fixture = TestBed.createComponent(TestIfRoleComponent);
fixture.detectChanges();
const content = fixture.nativeElement.querySelector('[data-test="content"]');
expect(content).toBeTruthy();
expect(content?.textContent).toContain('Store Content');
});
it('should not render content when user does not have role', () => {
roleService.hasRole.mockReturnValue(false);
const fixture = TestBed.createComponent(TestIfRoleComponent);
fixture.detectChanges();
const content = fixture.nativeElement.querySelector('[data-test="content"]');
expect(content).toBeFalsy();
});
it('should render else template when user does not have role', () => {
roleService.hasRole.mockReturnValue(false);
const fixture = TestBed.createComponent(TestIfRoleElseComponent);
fixture.detectChanges();
const content = fixture.nativeElement.querySelector('[data-test="content"]');
const elseContent = fixture.nativeElement.querySelector('[data-test="else"]');
expect(content).toBeFalsy();
expect(elseContent).toBeTruthy();
expect(elseContent?.textContent).toContain('No Access');
});
it('should update when role input changes', () => {
roleService.hasRole.mockReturnValue(true);
const fixture = TestBed.createComponent(TestIfRoleComponent);
fixture.detectChanges();
let content = fixture.nativeElement.querySelector('[data-test="content"]');
expect(content).toBeTruthy();
// Change role and mock to return false
roleService.hasRole.mockReturnValue(false);
fixture.componentInstance.role = Role.CallCenter;
fixture.detectChanges();
content = fixture.nativeElement.querySelector('[data-test="content"]');
expect(content).toBeFalsy();
});
});
describe('ifNotRole', () => {
it('should render content when user does NOT have role', () => {
roleService.hasRole.mockReturnValue(false);
const fixture = TestBed.createComponent(TestIfNotRoleComponent);
fixture.detectChanges();
const content = fixture.nativeElement.querySelector('[data-test="content"]');
expect(content).toBeTruthy();
expect(content?.textContent).toContain('Non-Store Content');
});
it('should not render content when user has role', () => {
roleService.hasRole.mockReturnValue(true);
const fixture = TestBed.createComponent(TestIfNotRoleComponent);
fixture.detectChanges();
const content = fixture.nativeElement.querySelector('[data-test="content"]');
expect(content).toBeFalsy();
});
});
describe('multiple roles', () => {
it('should handle array of roles', () => {
roleService.hasRole.mockReturnValue(true);
@Component({
standalone: true,
imports: [IfRoleDirective],
template: `<div *ifRole="roles" data-test="content">Content</div>`,
})
class TestMultipleRolesComponent {
roles = [Role.Store, Role.CallCenter];
}
const fixture = TestBed.createComponent(TestMultipleRolesComponent);
fixture.detectChanges();
expect(roleService.hasRole).toHaveBeenCalledWith([Role.Store, Role.CallCenter]);
const content = fixture.nativeElement.querySelector('[data-test="content"]');
expect(content).toBeTruthy();
});
});
});

View File

@@ -0,0 +1,105 @@
import {
Directive,
effect,
inject,
input,
TemplateRef,
ViewContainerRef,
} from '@angular/core';
import { RoleService } from './role.service';
import { Role } from './role';
/**
* Structural directive for role-based conditional rendering using Angular signals
*
* @example
* ```html
* <!-- Show content if user has role -->
* <div *ifRole="Role.Store">Store content</div>
*
* <!-- Show content if user has multiple roles -->
* <div *ifRole="[Role.Store, Role.CallCenter]">Multiple roles</div>
*
* <!-- Show alternate content if user doesn't have role -->
* <div *ifRole="Role.Store; else noAccess">Store content</div>
* <ng-template #noAccess>No access</ng-template>
*
* <!-- Show content if user does NOT have role -->
* <div *ifNotRole="Role.CallCenter">Non-CallCenter content</div>
* ```
*/
@Directive({
selector: '[ifRole],[ifRoleElse],[ifNotRole],[ifNotRoleElse]',
standalone: true,
})
export class IfRoleDirective {
private readonly _templateRef = inject(TemplateRef<{ $implicit: Role | Role[] }>);
private readonly _viewContainer = inject(ViewContainerRef);
private readonly _roleService = inject(RoleService);
/**
* Role(s) required to show the template
*/
readonly ifRole = input<Role | Role[]>();
/**
* Alternative template to show if user doesn't have ifRole
*/
readonly ifRoleElse = input<TemplateRef<unknown>>();
/**
* Role(s) that should NOT be present to show the template
*/
readonly ifNotRole = input<Role | Role[]>();
/**
* Alternative template to show if user has ifNotRole
*/
readonly ifNotRoleElse = input<TemplateRef<unknown>>();
constructor() {
// Use effect to reactively update the view when inputs change
effect(() => {
this.render();
});
}
private get renderTemplateRef(): boolean {
const role = this.ifRole();
const notRole = this.ifNotRole();
if (role) {
return this._roleService.hasRole(role);
}
if (notRole) {
return !this._roleService.hasRole(notRole);
}
return false;
}
private get elseTemplateRef(): TemplateRef<unknown> | undefined {
return this.ifRoleElse() || this.ifNotRoleElse();
}
private render() {
if (this.renderTemplateRef) {
this._viewContainer.clear();
this._viewContainer.createEmbeddedView(this._templateRef, this.getContext());
return;
}
if (this.elseTemplateRef) {
this._viewContainer.clear();
this._viewContainer.createEmbeddedView(this.elseTemplateRef, this.getContext());
return;
}
this._viewContainer.clear();
}
private getContext(): { $implicit: Role | Role[] | undefined } {
return {
$implicit: this.ifRole() || this.ifNotRole(),
};
}
}

View File

@@ -0,0 +1,95 @@
import { TestBed } from '@angular/core/testing';
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { RoleService } from './role.service';
import { TOKEN_PROVIDER, TokenProvider } from './token-provider';
import { Role } from './role';
describe('RoleService', () => {
let service: RoleService;
let tokenProvider: TokenProvider;
beforeEach(() => {
tokenProvider = {
getClaimByKey: vi.fn(),
};
TestBed.configureTestingModule({
providers: [RoleService, { provide: TOKEN_PROVIDER, useValue: tokenProvider }],
});
service = TestBed.inject(RoleService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
describe('hasRole', () => {
it('should return true when user has single required role', () => {
vi.spyOn(tokenProvider, 'getClaimByKey').mockReturnValue([Role.Store, Role.CallCenter]);
expect(service.hasRole(Role.Store)).toBe(true);
});
it('should return false when user does not have required role', () => {
vi.spyOn(tokenProvider, 'getClaimByKey').mockReturnValue([Role.CallCenter]);
expect(service.hasRole(Role.Store)).toBe(false);
});
it('should return true when user has all required roles (array)', () => {
vi.spyOn(tokenProvider, 'getClaimByKey').mockReturnValue([
Role.Store,
Role.CallCenter,
]);
expect(service.hasRole([Role.Store, Role.CallCenter])).toBe(true);
});
it('should return false when user is missing one of required roles', () => {
vi.spyOn(tokenProvider, 'getClaimByKey').mockReturnValue([Role.CallCenter]);
expect(service.hasRole([Role.Store, Role.CallCenter])).toBe(false);
});
it('should return false when user has no roles in token', () => {
vi.spyOn(tokenProvider, 'getClaimByKey').mockReturnValue(null);
expect(service.hasRole(Role.Store)).toBe(false);
});
it('should return false when user has undefined roles', () => {
vi.spyOn(tokenProvider, 'getClaimByKey').mockReturnValue(undefined);
expect(service.hasRole(Role.Store)).toBe(false);
});
it('should handle errors gracefully', () => {
vi.spyOn(tokenProvider, 'getClaimByKey').mockImplementation(() => {
throw new Error('Token parsing error');
});
expect(service.hasRole(Role.Store)).toBe(false);
});
it('should handle empty role array', () => {
vi.spyOn(tokenProvider, 'getClaimByKey').mockReturnValue([Role.Store]);
expect(service.hasRole([])).toBe(true); // empty array means no requirements
});
it('should handle single role as string (not array)', () => {
// JWT might return a single string instead of array for single role
vi.spyOn(tokenProvider, 'getClaimByKey').mockReturnValue(Role.Store);
expect(service.hasRole(Role.Store)).toBe(true);
expect(service.hasRole(Role.CallCenter)).toBe(false);
});
it('should handle single role string when checking multiple roles', () => {
vi.spyOn(tokenProvider, 'getClaimByKey').mockReturnValue(Role.Store);
expect(service.hasRole([Role.Store, Role.CallCenter])).toBe(false);
});
});
});

View File

@@ -0,0 +1,71 @@
import { coerceArray } from '@angular/cdk/coercion';
import { inject, Injectable } from '@angular/core';
import { logger } from '@isa/core/logging';
import { TOKEN_PROVIDER } from './token-provider';
import { Role } from './role';
/**
* Service for role-based authorization checks
*
* @example
* ```typescript
* import { Role } from '@isa/core/auth';
*
* const roleService = inject(RoleService);
* if (roleService.hasRole(Role.Store)) {
* // Show store features
* }
* ```
*/
@Injectable({
providedIn: 'root',
})
export class RoleService {
private readonly _log = logger({ service: 'RoleService' });
private readonly _tokenProvider = inject(TOKEN_PROVIDER);
/**
* Check if the authenticated user has specific role(s)
*
* @param role Single role or array of roles to check
* @returns true if user has all specified roles, false otherwise
*
* @example
* ```typescript
* import { Role } from '@isa/core/auth';
*
* // Check single role
* hasRole(Role.Store) // true if user has Store role
*
* // Check multiple roles (AND logic)
* hasRole([Role.Store, Role.CallCenter]) // true only if user has BOTH roles
* ```
*/
hasRole(role: Role | Role[]): boolean {
const roles = coerceArray(role);
try {
const userRoles = this._tokenProvider.getClaimByKey('role');
if (!userRoles) {
this._log.debug('No roles found in token claims');
return false;
}
// Coerce userRoles to array in case it's a single string
const userRolesArray = coerceArray(userRoles);
const hasAllRoles = roles.every((r) => userRolesArray.includes(r));
this._log.debug(`Role check: ${roles.join(', ')} => ${hasAllRoles}`, () => ({
requiredRoles: roles,
userRoles: userRolesArray,
}));
return hasAllRoles;
} catch (error) {
this._log.error('Error checking roles', error as Error, () => ({ requiredRoles: roles }));
return false;
}
}
}

View File

@@ -0,0 +1,13 @@
export const Role = {
/**
* HSC
*/
CallCenter: 'CallCenter',
/**
* Filiale
*/
Store: 'Store',
} as const;
export type Role = (typeof Role)[keyof typeof Role];

View File

@@ -0,0 +1,67 @@
import { inject, InjectionToken } from '@angular/core';
import { OAuthService } from 'angular-oauth2-oidc';
/**
* Token provider interface for role checking
* The app can provide a custom implementation that returns user roles from the auth token
*/
export interface TokenProvider {
/**
* Get a claim value from the authentication token
* @param key The claim key (e.g., 'role')
* @returns The claim value or null if not found
*/
getClaimByKey(key: string): unknown;
}
/**
* Parse JWT token to extract claims
*/
export function parseJwt(token: string | null): Record<string, unknown> | null {
if (!token) {
return null;
}
try {
const base64Url = token.split('.')[1];
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
const encoded = window.atob(base64);
return JSON.parse(encoded);
} catch {
return null;
}
}
/**
* Injection token for TokenProvider with default OAuthService implementation
*
* By default, this uses OAuthService to extract claims from the access token.
* You can override this by providing your own implementation:
*
* @example
* ```typescript
* providers: [
* {
* provide: TOKEN_PROVIDER,
* useValue: {
* getClaimByKey: (key) => customTokenService.getClaim(key)
* }
* }
* ]
* ```
*/
export const TOKEN_PROVIDER = new InjectionToken<TokenProvider>(
'TOKEN_PROVIDER',
{
providedIn: 'root',
factory: () => {
const oAuthService = inject(OAuthService);
return {
getClaimByKey: (key: string) => {
const claims = parseJwt(oAuthService.getAccessToken());
return claims?.[key] ?? null;
},
};
},
},
);

View File

@@ -0,0 +1,13 @@
import '@angular/compiler';
import '@analogjs/vitest-angular/setup-zone';
import {
BrowserTestingModule,
platformBrowserTesting,
} from '@angular/platform-browser/testing';
import { getTestBed } from '@angular/core/testing';
getTestBed().initTestEnvironment(
BrowserTestingModule,
platformBrowserTesting(),
);

View File

@@ -0,0 +1,30 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"importHelpers": true,
"moduleResolution": "bundler",
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"module": "preserve"
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"typeCheckHostBindings": true,
"strictTemplates": true
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
]
}

View File

@@ -0,0 +1,27 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../../dist/out-tsc",
"declaration": true,
"declarationMap": true,
"inlineSources": true,
"types": []
},
"exclude": [
"src/**/*.spec.ts",
"src/test-setup.ts",
"jest.config.ts",
"src/**/*.test.ts",
"vite.config.ts",
"vite.config.mts",
"vitest.config.ts",
"vitest.config.mts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx"
],
"include": ["src/**/*.ts"]
}

View File

@@ -0,0 +1,29 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../../dist/out-tsc",
"types": [
"vitest/globals",
"vitest/importMeta",
"vite/client",
"node",
"vitest"
]
},
"include": [
"vite.config.ts",
"vite.config.mts",
"vitest.config.ts",
"vitest.config.mts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx",
"src/**/*.d.ts"
],
"files": ["src/test-setup.ts"]
}

View File

@@ -0,0 +1,33 @@
/// <reference types='vitest' />
import { defineConfig } from 'vite';
import angular from '@analogjs/vite-plugin-angular';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin';
export default
// @ts-expect-error - Vitest reporter tuple types have complex inference issues
defineConfig(() => ({
root: __dirname,
cacheDir: '../../../node_modules/.vite/libs/core/auth',
plugins: [angular(), nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])],
// Uncomment this if you are using workers.
// worker: {
// plugins: [ nxViteTsPaths() ],
// },
test: {
watch: false,
globals: true,
environment: 'jsdom',
include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
setupFiles: ['src/test-setup.ts'],
reporters: [
'default',
['junit', { outputFile: '../../../testresults/junit-core-auth.xml' }],
],
coverage: {
reportsDirectory: '../../../coverage/libs/core/auth',
provider: 'v8' as const,
reporter: ['text', 'cobertura'],
},
},
}));

407
package-lock.json generated
View File

@@ -6563,110 +6563,6 @@
"win32"
]
},
"node_modules/@mapbox/node-pre-gyp": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz",
"integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==",
"dev": true,
"license": "BSD-3-Clause",
"optional": true,
"dependencies": {
"detect-libc": "^2.0.0",
"https-proxy-agent": "^5.0.0",
"make-dir": "^3.1.0",
"node-fetch": "^2.6.7",
"nopt": "^5.0.0",
"npmlog": "^5.0.1",
"rimraf": "^3.0.2",
"semver": "^7.3.5",
"tar": "^6.1.11"
},
"bin": {
"node-pre-gyp": "bin/node-pre-gyp"
}
},
"node_modules/@mapbox/node-pre-gyp/node_modules/abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
"dev": true,
"license": "ISC",
"optional": true
},
"node_modules/@mapbox/node-pre-gyp/node_modules/agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"debug": "4"
},
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/@mapbox/node-pre-gyp/node_modules/https-proxy-agent": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"agent-base": "6",
"debug": "4"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/@mapbox/node-pre-gyp/node_modules/make-dir": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
"integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"semver": "^6.0.0"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@mapbox/node-pre-gyp/node_modules/make-dir/node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
"license": "ISC",
"optional": true,
"bin": {
"semver": "bin/semver.js"
}
},
"node_modules/@mapbox/node-pre-gyp/node_modules/nopt": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
"integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==",
"dev": true,
"license": "ISC",
"optional": true,
"dependencies": {
"abbrev": "1"
},
"bin": {
"nopt": "bin/nopt.js"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@mdx-js/react": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.1.tgz",
@@ -15100,30 +14996,6 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/aproba": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz",
"integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==",
"dev": true,
"license": "ISC",
"optional": true
},
"node_modules/are-we-there-yet": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz",
"integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==",
"deprecated": "This package is no longer supported.",
"dev": true,
"license": "ISC",
"optional": true,
"dependencies": {
"delegates": "^1.0.0",
"readable-stream": "^3.6.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/arg": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
@@ -16623,17 +16495,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/color-support": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
"integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==",
"dev": true,
"license": "ISC",
"optional": true,
"bin": {
"color-support": "bin.js"
}
},
"node_modules/colord": {
"version": "2.9.3",
"resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz",
@@ -16800,14 +16661,6 @@
"node": ">=0.8"
}
},
"node_modules/console-control-strings": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
"integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==",
"dev": true,
"license": "ISC",
"optional": true
},
"node_modules/content-disposition": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz",
@@ -18288,20 +18141,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/decompress-response": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz",
"integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"mimic-response": "^2.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/dedent": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.0.tgz",
@@ -18843,6 +18682,7 @@
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"iconv-lite": "^0.6.2"
}
@@ -20561,72 +20401,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/gauge": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz",
"integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==",
"deprecated": "This package is no longer supported.",
"dev": true,
"license": "ISC",
"optional": true,
"dependencies": {
"aproba": "^1.0.3 || ^2.0.0",
"color-support": "^1.1.2",
"console-control-strings": "^1.0.0",
"has-unicode": "^2.0.1",
"object-assign": "^4.1.1",
"signal-exit": "^3.0.0",
"string-width": "^4.2.3",
"strip-ansi": "^6.0.1",
"wide-align": "^1.1.2"
},
"engines": {
"node": ">=10"
}
},
"node_modules/gauge/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true,
"license": "MIT",
"optional": true
},
"node_modules/gauge/node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
"node": ">=8"
}
},
"node_modules/gauge/node_modules/signal-exit": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
"dev": true,
"license": "ISC",
"optional": true
},
"node_modules/gauge/node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/generator-function": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz",
@@ -21096,14 +20870,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-unicode": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
"integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==",
"dev": true,
"license": "ISC",
"optional": true
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
@@ -28676,20 +28442,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/mimic-response": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz",
"integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/min-indent": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
@@ -29045,14 +28797,6 @@
"thenify-all": "^1.0.0"
}
},
"node_modules/nan": {
"version": "2.23.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.23.0.tgz",
"integrity": "sha512-1UxuyYGdoQHcGg87Lkqm3FzefucTa0NAiOcuRsDmysep3c1LVCRK2krrUDafMWtjSG04htvAmvg96+SDknOmgQ==",
"dev": true,
"license": "MIT",
"optional": true
},
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
@@ -29767,21 +29511,6 @@
"node": ">=8"
}
},
"node_modules/npmlog": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz",
"integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==",
"deprecated": "This package is no longer supported.",
"dev": true,
"license": "ISC",
"optional": true,
"dependencies": {
"are-we-there-yet": "^2.0.0",
"console-control-strings": "^1.1.0",
"gauge": "^3.0.0",
"set-blocking": "^2.0.0"
}
},
"node_modules/nth-check": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
@@ -32830,73 +32559,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
"deprecated": "Rimraf versions prior to v4 are no longer supported",
"dev": true,
"license": "ISC",
"optional": true,
"dependencies": {
"glob": "^7.1.3"
},
"bin": {
"rimraf": "bin.js"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/rimraf/node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/rimraf/node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"deprecated": "Glob versions prior to v9 are no longer supported",
"dev": true,
"license": "ISC",
"optional": true,
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/rimraf/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"license": "ISC",
"optional": true,
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/roarr": {
"version": "2.15.4",
"resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz",
@@ -33947,14 +33609,6 @@
"node": ">= 18"
}
},
"node_modules/set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
"dev": true,
"license": "ISC",
"optional": true
},
"node_modules/set-cookie-parser": {
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
@@ -34152,19 +33806,6 @@
"license": "MIT",
"optional": true
},
"node_modules/simple-get": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz",
"integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"decompress-response": "^4.2.0",
"once": "^1.3.1",
"simple-concat": "^1.0.0"
}
},
"node_modules/sirv": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz",
@@ -37853,52 +37494,6 @@
"node": ">=8"
}
},
"node_modules/wide-align": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",
"integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==",
"dev": true,
"license": "ISC",
"optional": true,
"dependencies": {
"string-width": "^1.0.2 || 2 || 3 || 4"
}
},
"node_modules/wide-align/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true,
"license": "MIT",
"optional": true
},
"node_modules/wide-align/node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
"node": ">=8"
}
},
"node_modules/wide-align/node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/wildcard": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz",

View File

@@ -62,6 +62,7 @@
"@isa/common/data-access": ["libs/common/data-access/src/index.ts"],
"@isa/common/decorators": ["libs/common/decorators/src/index.ts"],
"@isa/common/print": ["libs/common/print/src/index.ts"],
"@isa/core/auth": ["libs/core/auth/src/index.ts"],
"@isa/core/config": ["libs/core/config/src/index.ts"],
"@isa/core/logging": ["libs/core/logging/src/index.ts"],
"@isa/core/navigation": ["libs/core/navigation/src/index.ts"],