npm package: @flusys/ng-auth
Description: Authentication module for FLUSYS Angular applications
Latest version: 4.1.1

# @flusys/ng-auth

> Complete authentication library for the FLUSYS Angular platform — JWT session management, HTTP-only cookie tokens, multi-company selection, and provider adapter registration.

[![npm version](https://img.shields.io/npm/v/@flusys/ng-auth.svg)](https://www.npmjs.com/package/@flusys/ng-auth)
[![Angular](https://img.shields.io/badge/Angular-21-red.svg)](https://angular.io)
[![TypeScript](https://img.shields.io/badge/TypeScript-5.x-blue.svg)](https://www.typescriptlang.org)
[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)

---

## Table of Contents

- [Overview](#overview)
- [Features](#features)
- [Compatibility](#compatibility)
- [Installation](#installation)
- [Quick Start](#quick-start)
- [Module Registration](#module-registration)
- [Session Restoration Flow](#session-restoration-flow)
- [Auth State Service](#auth-state-service)
- [Auth API Service](#auth-api-service)
- [HTTP Interceptors](#http-interceptors)
- [Guards](#guards)
- [Pages](#pages)
- [Provider Adapters](#provider-adapters)
- [API Endpoints](#api-endpoints)
- [Configuration Reference](#configuration-reference)
- [Troubleshooting](#troubleshooting)
- [License](#license)

---

## Overview

`@flusys/ng-auth` provides end-to-end authentication for FLUSYS Angular applications. It handles the complete lifecycle: login, registration, token refresh, session restoration on page reload, email verification, profile management, password changes, and multi-company selection.

Tokens are stored in **HTTP-only cookies** (managed by the backend) — never in localStorage. The access token is held in memory only and automatically refreshed via a silent refresh interceptor.

---

## Features

- ✅ JWT authentication with HTTP-only refresh token cookies
- ✅ Silent token refresh on 401 responses
- ✅ Session restoration on page reload (`AuthInitService`)
- ✅ Multi-company selection flow
- ✅ Email verification with OTP
- ✅ Profile management (name, password, avatar)
- ✅ `AuthStateService` — signal-based reactive auth state
- ✅ `provideAuthProviders()` — registers `USER_PROVIDER`, `COMPANY_PROVIDER`
- ✅ `provideAuthLayoutIntegration()` — bridges auth to layout
- ✅ Guard: `appInitGuard` for session restoration before navigation
- ✅ Guard: `AuthenticatedGuard`, `GuestGuard`, `CompanySelectedGuard`

---

## Compatibility

| Package | Version |
|---------|---------|
| Angular | 21+ |
| @flusys/ng-core | 4.x |
| @flusys/ng-shared | 4.x |

---

## Installation

```bash
npm install @flusys/ng-auth @flusys/ng-core @flusys/ng-shared
```

---

## Quick Start

### 1. Register Auth Providers

```typescript
// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { APP_CONFIG } from '@flusys/ng-core';
import {
  provideAuthProviders,
  provideAuthLayoutIntegration,
  authInterceptor,
  tokenRefreshInterceptor,
} from '@flusys/ng-auth';
import { environment } from './environments/environment';

export const appConfig: ApplicationConfig = {
  providers: [
    { provide: APP_CONFIG, useValue: environment },
    provideHttpClient(
      withInterceptors([
        authInterceptor,          // Attaches access token to requests
        tokenRefreshInterceptor,  // Handles 401 → refresh → retry
      ])
    ),
    ...provideAuthProviders(),           // USER_PROVIDER, COMPANY_PROVIDER
    ...provideAuthLayoutIntegration(),   // LAYOUT_AUTH_STATE, LAYOUT_AUTH_API
  ],
};
```

### 2. Protect Routes with Guards

```typescript
// app.routes.ts
import { Routes } from '@angular/router';
import { appInitGuard } from '@flusys/ng-auth';
import { AppLayoutComponent } from '@flusys/ng-layout';

export const routes: Routes = [
  {
    path: '',
    component: AppLayoutComponent,
    canActivate: [appInitGuard],  // Restores session on page refresh
    children: [
      { path: 'dashboard', loadComponent: () => import('./pages/dashboard.component') },
    ],
  },
  {
    path: 'auth',
    loadChildren: () => import('@flusys/ng-auth').then(m => m.AUTH_ROUTES),
  },
];
```

### 3. Enable Auth in App Config

```typescript
// environments/environment.ts
export const environment = {
  apiBaseUrl: 'http://localhost:2002',
  services: {
    auth: {
      enabled: true,
      features: {
        signUp: true,
        emailVerification: true,
        companySelection: true,
      },
    },
  },
};
```

---

## Module Registration

`ng-auth` uses function-based providers (not Angular modules):

### `provideAuthProviders()`

Registers auth adapters against shared provider tokens:

```typescript
...provideAuthProviders()

// Provides:
// USER_PROVIDER    → AuthStateService (IUserProvider)
// COMPANY_PROVIDER → AuthStateService (ICompanyProvider)
```

These tokens are consumed by `ng-iam`, `ng-storage`, `ng-notification` — no direct dependency on `ng-auth`.

### `provideAuthLayoutIntegration()`

Bridges auth state and API to the layout:

```typescript
...provideAuthLayoutIntegration()

// Provides:
// LAYOUT_AUTH_STATE → AuthLayoutStateAdapter (profile picture, company logo)
// LAYOUT_AUTH_API   → AuthLayoutApiAdapter (logout, select-company actions)
```

---

## Session Restoration Flow

On every page refresh, auth state is cleared from memory. The `appInitGuard` restores the session transparently:

```
Page Refresh
     │
     ▼
appInitGuard runs
     │
     ├─ User in memory? ──YES──► Check company → Allow navigation
     │
     └─ NO ──► AuthInitService.initialize()
                    │
                    ├─ POST /auth/refresh (HTTP-only cookie) → access token
                    ├─ GET /auth/me → user data
                    └─ Session restored in memory
                         │
                         ├─ Auth enabled? → Check company selection
                         ├─ IAM enabled? → Load permissions
                         └─ Allow navigation
```

### Company Selection Flow

```
User logs in
     │
     ├─ requiresSelection: false ──► Navigate to app
     │
     └─ requiresSelection: true
           │
           ├─ sessionId + companies[] returned
           └─ Redirect to /auth/select-company
                    │
                    └─ User picks company/branch
                              │
                              └─ POST /auth/select { sessionId, companyId, branchId }
                                        │
                                        └─ Tokens + company data → Navigate to app
```

---

## Auth State Service

`AuthStateService` is the single source of truth for authentication state, powered by Angular signals.

```typescript
import { AuthStateService } from '@flusys/ng-auth';

@Component({ ... })
export class MyComponent {
  private authState = inject(AuthStateService);

  // Signals (read-only)
  user = this.authState.user;                   // Signal<ICurrentUser | null>
  company = this.authState.company;             // Signal<ICompany | null>
  branch = this.authState.branch;               // Signal<IBranch | null>
  isAuthenticated = this.authState.isAuthenticated; // Signal<boolean>
  accessToken = this.authState.accessToken;     // Signal<string | null>

  // Computed
  userName = computed(() => this.authState.user()?.name ?? 'Guest');
}
```

**AuthStateService API:**

| Member | Type | Description |
|--------|------|-------------|
| `user` | `Signal<ICurrentUser \| null>` | Current authenticated user |
| `company` | `Signal<ICompany \| null>` | Selected company |
| `branch` | `Signal<IBranch \| null>` | Selected branch |
| `isAuthenticated` | `Signal<boolean>` | True if user is logged in |
| `accessToken` | `Signal<string \| null>` | In-memory JWT access token |
| `setUser(user)` | `void` | Update user state |
| `setTokens(tokens)` | `void` | Update access token |
| `setCompany(company, branch)` | `void` | Update company/branch |
| `clearAll()` | `void` | Clear all auth state (logout) |

---

## Auth API Service

`AuthApiService` handles all HTTP calls to the auth backend.

```typescript
import { AuthApiService } from '@flusys/ng-auth';

@Component({ ... })
export class LoginPageComponent {
  private authApi = inject(AuthApiService);

  login(email: string, password: string): void {
    this.authApi.login({ email, password }).subscribe(response => {
      if (response.requiresSelection) {
        // Redirect to company selection
      }
      // Navigate to app
    });
  }
}
```

**AuthApiService Methods:**

| Method | Endpoint | Description |
|--------|----------|-------------|
| `login(dto)` | `POST /auth/login` | Authenticate user |
| `register(dto)` | `POST /auth/register` | Self-register |
| `logout()` | `POST /auth/logout` | Invalidate tokens |
| `refresh()` | `POST /auth/refresh` | Refresh access token |
| `me()` | `GET /auth/me` | Get current user info |
| `selectCompany(dto)` | `POST /auth/select` | Select company after login |
| `verifyEmail(dto)` | `POST /auth/verify-email` | Verify OTP |
| `resendVerification()` | `POST /auth/resend-verification` | Resend OTP email |
| `changePassword(dto)` | `POST /auth/change-password` | Update password |
| `updateProfile(dto)` | `POST /auth/update-profile` | Update name/avatar |

---

## HTTP Interceptors

### authInterceptor

Attaches the in-memory access token as `Authorization: Bearer <token>` header to every outgoing request (except public endpoints).

```typescript
provideHttpClient(
  withInterceptors([authInterceptor])
)
```

### tokenRefreshInterceptor

On 401 responses, automatically:
1. Calls `POST /auth/refresh`
2. Updates `AuthStateService` with new token
3. Retries the original failed request

All concurrent 401s are queued — only one refresh call is made.

```typescript
provideHttpClient(
  withInterceptors([tokenRefreshInterceptor])
)
```

**Interceptor Order Matters:**

```typescript
withInterceptors([
  authInterceptor,          // Must be FIRST (attaches token)
  tokenRefreshInterceptor,  // Must be SECOND (handles 401)
])
```

---

## Guards

| Guard | Usage | Behavior |
|-------|-------|---------|
| `appInitGuard` | `canActivate` | Restores session from cookie on page refresh |
| `AuthenticatedGuard` | `canActivate` | Redirects to `/auth/login` if not authenticated |
| `GuestGuard` | `canActivate` | Redirects to `/` if already authenticated |
| `CompanySelectedGuard` | `canActivate` | Redirects to `/auth/select-company` if company not chosen |

```typescript
// Route with all guards
{
  path: '',
  canActivate: [appInitGuard],
  children: [
    {
      path: 'dashboard',
      canActivate: [AuthenticatedGuard, CompanySelectedGuard],
      loadComponent: () => import('./dashboard.component'),
    },
  ],
}
```

---

## Pages

`ng-auth` exports pre-built page components routable via `AUTH_ROUTES`:

```typescript
import { AUTH_ROUTES } from '@flusys/ng-auth';

// app.routes.ts
{ path: 'auth', loadChildren: () => AUTH_ROUTES }
```

| Route | Component | Description |
|-------|-----------|-------------|
| `/auth/login` | `LoginPageComponent` | Email + password login form |
| `/auth/register` | `RegisterPageComponent` | Self-registration form |
| `/auth/verify-email` | `VerifyEmailPageComponent` | OTP verification |
| `/auth/select-company` | `SelectCompanyPageComponent` | Company/branch picker |
| `/auth/profile` | `ProfilePageComponent` | User profile editor |

All pages support full i18n via `TranslatePipe`.

---

## Provider Adapters

### AUTH_EMAIL_PROVIDER

`ng-auth` requires an email adapter for verification emails. Register this in your NestJS backend (not Angular). On the Angular side, this is handled automatically by the backend.

### User Enricher

You can inject additional user data at login time:

```typescript
// Custom enricher (rare — usually not needed)
import { USER_ENRICHER } from '@flusys/ng-auth';

providers: [
  {
    provide: USER_ENRICHER,
    useClass: MyUserEnricher,
  },
]
```

---

## API Endpoints

| Method | Endpoint | Auth | Description |
|--------|----------|------|-------------|
| POST | `/auth/login` | Public | Login with email/password |
| POST | `/auth/register` | Public | Self-registration |
| POST | `/auth/logout` | Bearer | Logout and clear cookie |
| POST | `/auth/refresh` | Cookie | Refresh access token |
| GET | `/auth/me` | Bearer | Get current user |
| POST | `/auth/select` | Session | Select company after login |
| POST | `/auth/verify-email` | Bearer | Verify email OTP |
| POST | `/auth/resend-verification` | Bearer | Resend OTP |
| POST | `/auth/change-password` | Bearer | Change password |
| POST | `/auth/update-profile` | Bearer | Update profile |

---

## Configuration Reference

| Config Key | Type | Default | Description |
|------------|------|---------|-------------|
| `services.auth.enabled` | `boolean` | `false` | Enable auth module |
| `services.auth.features.signUp` | `boolean` | `false` | Allow self-registration |
| `services.auth.features.emailVerification` | `boolean` | `false` | Require email OTP |
| `services.auth.features.companySelection` | `boolean` | `false` | Enable multi-company |

---

## Troubleshooting

**Token refresh loop (endless 401 cycle)**

This happens when the refresh endpoint itself returns 401. Ensure the refresh token cookie hasn't expired. Also verify that `tokenRefreshInterceptor` skips the `/auth/refresh` endpoint:

```typescript
// The interceptor automatically skips /auth/refresh to prevent loops
```

**`appInitGuard` redirects to login on every refresh**

Ensure your backend sets the refresh token cookie with `httpOnly: true`, `sameSite: 'lax'`, and the correct `domain`/`path`. The browser must send the cookie automatically with `POST /auth/refresh`.

**Company selection page shown even after selecting**

Verify that `POST /auth/select` returns both tokens AND the company/branch data. `AuthStateService.setCompany()` must be called with the response.

**User profile picture doesn't load**

Profile pictures are fetched via `FileUrlService`, not by constructing URLs. Ensure `ng-storage` providers are registered and `services.storage.enabled: true`.

**Login form validation errors not translated**

Provide `TRANSLATE_ADAPTER` via `provideLocalization()`. The fallback text (non-translated) is always shown if the adapter is absent.

---

## License

MIT © FLUSYS