mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
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:
committed by
Nino Righi
parent
c5ea5ed3ec
commit
2e0853c91a
@@ -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),
|
||||
)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
816
libs/core/auth/README.md
Normal 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
|
||||
34
libs/core/auth/eslint.config.cjs
Normal file
34
libs/core/auth/eslint.config.cjs
Normal 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: {},
|
||||
},
|
||||
];
|
||||
20
libs/core/auth/project.json
Normal file
20
libs/core/auth/project.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
10
libs/core/auth/src/index.ts
Normal file
10
libs/core/auth/src/index.ts
Normal 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';
|
||||
157
libs/core/auth/src/lib/if-role.directive.spec.ts
Normal file
157
libs/core/auth/src/lib/if-role.directive.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
105
libs/core/auth/src/lib/if-role.directive.ts
Normal file
105
libs/core/auth/src/lib/if-role.directive.ts
Normal 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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
95
libs/core/auth/src/lib/role.service.spec.ts
Normal file
95
libs/core/auth/src/lib/role.service.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
71
libs/core/auth/src/lib/role.service.ts
Normal file
71
libs/core/auth/src/lib/role.service.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
13
libs/core/auth/src/lib/role.ts
Normal file
13
libs/core/auth/src/lib/role.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export const Role = {
|
||||
/**
|
||||
* HSC
|
||||
*/
|
||||
CallCenter: 'CallCenter',
|
||||
|
||||
/**
|
||||
* Filiale
|
||||
*/
|
||||
Store: 'Store',
|
||||
} as const;
|
||||
|
||||
export type Role = (typeof Role)[keyof typeof Role];
|
||||
67
libs/core/auth/src/lib/token-provider.ts
Normal file
67
libs/core/auth/src/lib/token-provider.ts
Normal 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;
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
);
|
||||
13
libs/core/auth/src/test-setup.ts
Normal file
13
libs/core/auth/src/test-setup.ts
Normal 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(),
|
||||
);
|
||||
30
libs/core/auth/tsconfig.json
Normal file
30
libs/core/auth/tsconfig.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
27
libs/core/auth/tsconfig.lib.json
Normal file
27
libs/core/auth/tsconfig.lib.json
Normal 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"]
|
||||
}
|
||||
29
libs/core/auth/tsconfig.spec.json
Normal file
29
libs/core/auth/tsconfig.spec.json
Normal 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"]
|
||||
}
|
||||
33
libs/core/auth/vite.config.mts
Normal file
33
libs/core/auth/vite.config.mts
Normal 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
407
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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"],
|
||||
|
||||
Reference in New Issue
Block a user