# Login API Integration — Design Spec
*Date: 2026-05-26 | Project: ROLAC Angular App*

## Overview

Connect the Angular login page to the ROLAC C# API authentication endpoints. The login page component (`login-page.ts`) is already complete; the work is updating the service layer underneath it to talk to the correct API with a secure, memory-safe token strategy.

---

## Scope

**In scope:**
- Update `AuthService` to call new ROLAC API auth endpoints
- Update Angular interfaces to match API DTO shapes exactly
- Replace `localStorage` token storage with in-memory `BehaviorSubject`
- Update `AuthInterceptor` to fix path matching and add silent auto-refresh on 401
- Add `APP_INITIALIZER` session restore via the refresh token cookie

**Out of scope:**
- MFA API integration (no MFA endpoint exists in current ROLAC API — code path kept dormant)
- Secret-link token login flow (existing `verifySecretLinkToken()` kept as-is)
- UI/template changes to `login-page.component.html`

---

## API Contracts

### `POST /api/auth/login`
**Request body:**
```json
{ "email": "user@example.com", "password": "secret" }
```
**Success response (200):**
```json
{
  "accessToken": "<JWT>",
  "expiresIn": 900,
  "user": { "id": "...", "email": "...", "roles": ["Admin"], "languagePreference": "en" }
}
```
**Failure response (401):** `{ "message": "..." }`

Sets `rolac_rt` HttpOnly cookie (30-day refresh token).

### `POST /api/auth/refresh`
No body. Uses `rolac_rt` HttpOnly cookie.  
**Success (200):** Same shape as login response. Rotates the refresh token cookie.  
**Failure (401):** Cookie expired or revoked.

### `POST /api/auth/logout`
No body. Uses `rolac_rt` HttpOnly cookie.  
**Response:** 204 No Content. Clears the cookie.

---

## Architecture

### File changes

| File | Change type |
|------|-------------|
| `src/app/shared/services/auth.service.ts` | Full rewrite |
| `src/app/core/interceptors/auth.interceptor.ts` | Update |
| `src/app/app.config.ts` | Add APP_INITIALIZER |

### Updated interfaces in `auth.service.ts`

**Removed:** `User`, `UserDto`, `AccessDto`, `TokenCreateResponse`, `LoginResponse` (old).

**Added/updated:**
```typescript
// Matches API UserInfo DTO exactly
export interface UserInfo {
  id: string;
  email: string;
  roles: string[];
  languagePreference: string;
}

// Matches API LoginResponse DTO exactly
export interface ApiLoginResponse {
  accessToken: string;
  expiresIn: number;
  user: UserInfo;
}

// LoginCredentials — kept; mfaCode kept for future MFA support
export interface LoginCredentials {
  email: string;
  password: string;
  mfaCode?: string;
}

// LoginResultType — kept; MfaRequired kept dormant
export enum LoginResultType {
  Success = 'Success',
  MfaRequired = 'MfaRequired',
  InvalidCredentials = 'InvalidCredentials',
  Error = 'Error'
}

// LoginResult — kept; responseData type changes from User to UserInfo
export interface LoginResult {
  result: LoginResultType;
  responseData?: UserInfo;
  message?: string;
}

// TokenVerificationResult — updated: user field changes from User → UserInfo
export interface TokenVerificationResult {
  isValid: boolean;
  user?: UserInfo;   // was User (old); now UserInfo — verifySecretLinkToken extracts
                     // id, email, roles[], languagePreference from the JWT payload
  message?: string;
  expiresAt?: Date;
  requiresMfa?: boolean;
}
```

### `AuthService` — methods

```
login(credentials: LoginCredentials): Observable<LoginResult>
  POST /api/auth/login with JSON body
  On 200 → store accessToken + user in BehaviorSubjects → return LoginResultType.Success
  On 401 → return LoginResultType.InvalidCredentials
  On other error → return LoginResultType.Error

refresh(): Observable<boolean>
  POST /api/auth/refresh (no body; browser sends rolac_rt cookie automatically)
  On 200 → update accessToken$ BehaviorSubject → return true
  On 401/error → return false

logout(): void
  POST /api/auth/logout (fire-and-forget)
  Clear accessToken$ and currentUser$ BehaviorSubjects

initializeFromRefreshToken(): Promise<void>
  Calls refresh(); resolves regardless of outcome (used as APP_INITIALIZER)

getToken(): string | null
  Snapshot of accessToken$.value

isAuthenticated(): boolean
  currentUser$.value !== null

getCurrentUser(): UserInfo | null
  currentUser$.value

setCurrentUser(user: UserInfo): void
  Update currentUser$ (used by MFA dialog success callback)

// Kept (logic unchanged, type updated):
getRedirectUrl(): string
setRedirectUrl(url: string): void
verifySecretLinkToken(token: string): Observable<TokenVerificationResult>
  // Constructs UserInfo from JWT payload: id, email, roles, languagePreference
  // (username/firstName/lastName/branchIds are no longer extracted)
isTokenExpired(token: string): boolean
```

**Token storage:** In-memory `BehaviorSubject` only. No `localStorage`. The refresh token lives exclusively in the HttpOnly `rolac_rt` cookie managed by the browser.

### `AuthInterceptor` — updates

**Fix 1 — Skip list for `shouldAddToken()`:**  
Replace old paths (`/Auth/login`, `/Token/Create`) with:
```typescript
const publicPaths = ['/auth/login', '/auth/refresh', '/auth/logout'];
```

**Fix 2 — Silent auto-refresh on 401:**
```
On 401 response (and not a retry):
  1. Call authService.refresh()
  2. If refresh returns true → re-attach new token header → retry original request
  3. If refresh returns false → authService.logout() → navigate to /login → propagate error

On 401 response (already a retry):
  → authService.logout() → navigate to /login → propagate error
```

Use an `X-Retry: true` header sentinel to prevent infinite retry loops.

### `app.config.ts` — APP_INITIALIZER

```typescript
{
  provide: APP_INITIALIZER,
  useFactory: (authService: AuthService) => () => authService.initializeFromRefreshToken(),
  deps: [AuthService],
  multi: true
}
```

Ensures that on every page load, the app attempts to restore the session from the HttpOnly refresh token cookie before rendering. If the cookie is absent or expired, `initializeFromRefreshToken()` resolves silently and the user sees the login page.

---

## Data Flow

### Standard login
```
User submits form
  → LoginPage.onSubmit()
  → authService.login({ email, password })
  → POST /api/auth/login
  → 200: store accessToken + user in memory → LoginResultType.Success
  → LoginPage redirects to /dashboard

  → 401: LoginResultType.InvalidCredentials → show error message
  → network error: LoginResultType.Error → show error message
```

### Page reload (session restore)
```
App bootstrap
  → APP_INITIALIZER calls authService.initializeFromRefreshToken()
  → POST /api/auth/refresh (browser sends rolac_rt cookie)
  → 200: store new accessToken in memory → user is authenticated
  → 401: user$ remains null → router guard redirects to /login
```

### Authenticated API call (any protected endpoint)
```
Component makes HTTP call
  → AuthInterceptor: getToken() → attach Authorization: Bearer <token>
  → 200: response returned normally
  → 401: authService.refresh() → retry with new token
  → 401 again: authService.logout() → /login
```

---

## Error Handling

| Scenario | Handling |
|----------|----------|
| Wrong password | API returns 401 → `InvalidCredentials` → "Invalid email or password" shown in form |
| Network offline | `catchError` → `LoginResultType.Error` → "An error occurred" message |
| Token expired mid-session | Interceptor silently refreshes + retries; user never sees a 401 |
| Refresh cookie expired (30 days) | `initializeFromRefreshToken()` resolves false → router guard → `/login` |
| Second 401 after refresh attempt | `authService.logout()` + redirect to `/login` |

---

## Security Notes

- Access token: **memory only** (not accessible to XSS via localStorage)
- Refresh token: **HttpOnly cookie** managed by browser (not accessible to JavaScript)
- Cookie flags: `HttpOnly`, `Secure` (in non-dev), `SameSite=Strict`, `Path=/api/auth`
- `shouldAddToken()` skips auth endpoints to avoid attaching tokens to login/refresh/logout calls

---

## Testing Considerations

- `AuthService.login()` — mock HTTP, verify BehaviorSubjects update, verify `LoginResult` shape
- `AuthService.refresh()` — mock HTTP, verify token updates; mock 401, verify returns false
- `AuthService.logout()` — verify memory cleared; verify HTTP called
- `authInterceptor` — test 401-then-refresh path; test infinite-loop prevention with X-Retry sentinel
- `initializeFromRefreshToken()` — verify it resolves even on 401 (does not block bootstrap)
