# @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
