Files
ui-essentials/projects/auth-client/USAGE_GUIDE.md
Giuliano Silvestro 9b40aa3afb Add auth-client library for Elixir auth service integration
- Complete Angular client library for authentication and authorization
- JWT token management with automatic refresh and storage
- OAuth integration with social providers (Google, GitHub, etc.)
- Two-factor authentication support (TOTP and backup codes)
- Route guards for authentication and scope-based authorization
- HTTP interceptor for automatic token injection and refresh
- Comprehensive TypeScript interfaces for all API models
- User management features (profile updates, password changes)
- Cross-tab synchronization and token validation
- Complete usage guide with practical examples

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-11 14:56:59 +10:00

28 KiB

Auth Client Library Usage Guide

Quick Start

1. Installation & Setup

# Install the library (after building it locally)
npm install auth-client

# Or if using locally
ng build auth-client
# Then reference from dist/auth-client

2. App Module Configuration

// app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { ReactiveFormsModule } from '@angular/forms';

import { AuthInterceptor } from 'auth-client';
import { AppComponent } from './app.component';

@NgModule({
  declarations: [AppComponent],
  imports: [
    BrowserModule,
    HttpClientModule,  // Required for HTTP calls
    ReactiveFormsModule // For forms
  ],
  providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: AuthInterceptor,
      multi: true
    }
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

3. Environment Configuration

// src/environments/environment.ts
export const environment = {
  production: false,
  authServiceUrl: 'http://localhost:4000'  // Your Elixir auth service URL
};

// src/environments/environment.prod.ts
export const environment = {
  production: true,
  authServiceUrl: 'https://your-auth-service.com'
};

4. App Component Setup

// app.component.ts
import { Component, OnInit } from '@angular/core';
import { AuthService } from 'auth-client';
import { environment } from '../environments/environment';

@Component({
  selector: 'app-root',
  template: `
    <div class="app">
      <nav *ngIf="isAuthenticated$ | async">
        <span>Welcome, {{ (currentUser$ | async)?.email }}</span>
        <button (click)="logout()">Logout</button>
      </nav>
      <router-outlet></router-outlet>
    </div>
  `
})
export class AppComponent implements OnInit {
  isAuthenticated$ = this.authService.isAuthenticated$;
  currentUser$ = this.authService.currentUser$;

  constructor(private authService: AuthService) {}

  ngOnInit() {
    // Configure the auth service with your API URL
    this.authService.configure(environment.authServiceUrl);
  }

  logout() {
    this.authService.logout().subscribe();
  }
}

Authentication Components

Login Component

// login.component.ts
import { Component } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { AuthService, ApiError } from 'auth-client';

@Component({
  selector: 'app-login',
  template: `
    <form [formGroup]="loginForm" (ngSubmit)="onSubmit()">
      <h2>Login</h2>
      
      <div class="form-group">
        <label for="email">Email:</label>
        <input 
          id="email" 
          type="email" 
          formControlName="email"
          [class.error]="loginForm.get('email')?.invalid && loginForm.get('email')?.touched">
      </div>

      <div class="form-group">
        <label for="password">Password:</label>
        <input 
          id="password" 
          type="password" 
          formControlName="password"
          [class.error]="loginForm.get('password')?.invalid && loginForm.get('password')?.touched">
      </div>

      <!-- 2FA Code (only show if required) -->
      <div class="form-group" *ngIf="requires2FA">
        <label for="totpCode">2FA Code:</label>
        <input 
          id="totpCode" 
          type="text" 
          formControlName="totp_code"
          placeholder="Enter 6-digit code">
      </div>

      <div class="error-message" *ngIf="errorMessage">
        {{ errorMessage }}
      </div>

      <button 
        type="submit" 
        [disabled]="loginForm.invalid || isLoading"
        class="btn-primary">
        {{ isLoading ? 'Logging in...' : 'Login' }}
      </button>

      <!-- Social Login Buttons -->
      <div class="social-login">
        <button type="button" (click)="loginWithGoogle()" class="btn-google">
          Login with Google
        </button>
        <button type="button" (click)="loginWithGitHub()" class="btn-github">
          Login with GitHub
        </button>
      </div>

      <p>
        Don't have an account? 
        <a routerLink="/register">Register here</a>
      </p>
    </form>
  `
})
export class LoginComponent {
  loginForm: FormGroup;
  errorMessage = '';
  requires2FA = false;
  isLoading = false;

  constructor(
    private fb: FormBuilder,
    private authService: AuthService,
    private router: Router
  ) {
    this.loginForm = this.fb.group({
      email: ['', [Validators.required, Validators.email]],
      password: ['', [Validators.required, Validators.minLength(6)]],
      totp_code: ['']
    });
  }

  onSubmit() {
    if (this.loginForm.valid) {
      this.isLoading = true;
      this.errorMessage = '';

      this.authService.login(this.loginForm.value).subscribe({
        next: (response) => {
          console.log('Login successful:', response.user);
          this.router.navigate(['/dashboard']);
        },
        error: (error: ApiError) => {
          this.isLoading = false;
          
          if (error.requires_2fa) {
            this.requires2FA = true;
            this.errorMessage = 'Please enter your 2FA code';
          } else {
            this.errorMessage = error.error;
          }
        },
        complete: () => {
          this.isLoading = false;
        }
      });
    }
  }

  loginWithGoogle() {
    // Redirect method
    this.authService.initiateOAuthFlow('google', window.location.origin + '/oauth/callback');
  }

  loginWithGitHub() {
    // Popup method
    this.authService.initiateOAuthPopup('github').then(
      result => {
        console.log('OAuth successful:', result);
        this.router.navigate(['/dashboard']);
      },
      error => {
        this.errorMessage = 'OAuth login failed: ' + error.message;
      }
    );
  }
}

Register Component

// register.component.ts
import { Component } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { AuthService, ApiError } from 'auth-client';

@Component({
  selector: 'app-register',
  template: `
    <form [formGroup]="registerForm" (ngSubmit)="onSubmit()">
      <h2>Register</h2>
      
      <div class="form-group">
        <label for="firstName">First Name:</label>
        <input id="firstName" type="text" formControlName="first_name">
      </div>

      <div class="form-group">
        <label for="lastName">Last Name:</label>
        <input id="lastName" type="text" formControlName="last_name">
      </div>

      <div class="form-group">
        <label for="email">Email:</label>
        <input id="email" type="email" formControlName="email" required>
      </div>

      <div class="form-group">
        <label for="password">Password:</label>
        <input id="password" type="password" formControlName="password" required>
      </div>

      <div class="form-group">
        <label for="confirmPassword">Confirm Password:</label>
        <input id="confirmPassword" type="password" formControlName="password_confirmation" required>
      </div>

      <div class="error-message" *ngIf="errorMessage">
        {{ errorMessage }}
      </div>

      <div class="validation-errors" *ngIf="validationErrors">
        <ul>
          <li *ngFor="let error of getValidationErrorMessages()">{{ error }}</li>
        </ul>
      </div>

      <button 
        type="submit" 
        [disabled]="registerForm.invalid || isLoading"
        class="btn-primary">
        {{ isLoading ? 'Creating Account...' : 'Register' }}
      </button>

      <p>
        Already have an account? 
        <a routerLink="/login">Login here</a>
      </p>
    </form>
  `
})
export class RegisterComponent {
  registerForm: FormGroup;
  errorMessage = '';
  validationErrors: any = null;
  isLoading = false;

  constructor(
    private fb: FormBuilder,
    private authService: AuthService,
    private router: Router
  ) {
    this.registerForm = this.fb.group({
      first_name: [''],
      last_name: [''],
      email: ['', [Validators.required, Validators.email]],
      password: ['', [Validators.required, Validators.minLength(8)]],
      password_confirmation: ['', [Validators.required]]
    }, {
      validators: this.passwordMatchValidator
    });
  }

  passwordMatchValidator(form: FormGroup) {
    const password = form.get('password');
    const confirmPassword = form.get('password_confirmation');
    
    if (password && confirmPassword && password.value !== confirmPassword.value) {
      confirmPassword.setErrors({ passwordMismatch: true });
    }
    return null;
  }

  onSubmit() {
    if (this.registerForm.valid) {
      this.isLoading = true;
      this.errorMessage = '';
      this.validationErrors = null;

      this.authService.register(this.registerForm.value).subscribe({
        next: (response) => {
          console.log('Registration successful:', response.user);
          this.router.navigate(['/dashboard']);
        },
        error: (error: ApiError) => {
          this.isLoading = false;
          
          if (error.details) {
            this.validationErrors = error.details;
          } else {
            this.errorMessage = error.error;
          }
        },
        complete: () => {
          this.isLoading = false;
        }
      });
    }
  }

  getValidationErrorMessages(): string[] {
    if (!this.validationErrors) return [];
    
    const messages: string[] = [];
    Object.keys(this.validationErrors).forEach(field => {
      const errors = this.validationErrors[field];
      errors.forEach((error: string) => {
        messages.push(`${field}: ${error}`);
      });
    });
    return messages;
  }
}

Route Protection

Setting Up Protected Routes

// app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AuthGuard, GuestGuard } from 'auth-client';

import { LoginComponent } from './login/login.component';
import { RegisterComponent } from './register/register.component';
import { DashboardComponent } from './dashboard/dashboard.component';
import { AdminComponent } from './admin/admin.component';
import { ProfileComponent } from './profile/profile.component';

const routes: Routes = [
  // Guest routes (redirect to dashboard if authenticated)
  {
    path: 'login',
    component: LoginComponent,
    canActivate: [GuestGuard],
    data: { authenticatedRedirect: '/dashboard' }
  },
  {
    path: 'register',
    component: RegisterComponent,
    canActivate: [GuestGuard]
  },

  // Protected routes (require authentication)
  {
    path: 'dashboard',
    component: DashboardComponent,
    canActivate: [AuthGuard]
  },
  {
    path: 'profile',
    component: ProfileComponent,
    canActivate: [AuthGuard]
  },

  // Admin routes (require authentication + admin scope)
  {
    path: 'admin',
    component: AdminComponent,
    canActivate: [AuthGuard],
    data: {
      requiredScopes: ['admin'],
      requireAllScopes: true,
      unauthorizedRedirect: '/access-denied'
    }
  },

  // Multi-scope example
  {
    path: 'reports',
    component: ReportsComponent,
    canActivate: [AuthGuard],
    data: {
      requiredScopes: ['read:reports', 'analytics'],
      requireAllScopes: false, // User needs ANY of these scopes
      unauthorizedRedirect: '/dashboard'
    }
  },

  { path: '', redirectTo: '/dashboard', pathMatch: 'full' },
  { path: '**', redirectTo: '/dashboard' }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

User Profile Management

Profile Component

// profile.component.ts
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { AuthService, User } from 'auth-client';

@Component({
  selector: 'app-profile',
  template: `
    <div class="profile-container">
      <h2>User Profile</h2>

      <!-- Profile Information -->
      <div class="profile-info" *ngIf="currentUser">
        <h3>Account Information</h3>
        <p><strong>Email:</strong> {{ currentUser.email }}</p>
        <p><strong>Status:</strong> 
          <span [class]="currentUser.is_active ? 'status-active' : 'status-inactive'">
            {{ currentUser.is_active ? 'Active' : 'Inactive' }}
          </span>
        </p>
        <p><strong>Email Verified:</strong> 
          <span [class]="currentUser.email_verified ? 'status-verified' : 'status-unverified'">
            {{ currentUser.email_verified ? 'Yes' : 'No' }}
          </span>
        </p>
        <p><strong>Member Since:</strong> {{ currentUser.created_at | date }}</p>
      </div>

      <!-- Update Profile Form -->
      <form [formGroup]="profileForm" (ngSubmit)="updateProfile()">
        <h3>Update Profile</h3>
        
        <div class="form-group">
          <label for="firstName">First Name:</label>
          <input id="firstName" type="text" formControlName="first_name">
        </div>

        <div class="form-group">
          <label for="lastName">Last Name:</label>
          <input id="lastName" type="text" formControlName="last_name">
        </div>

        <button type="submit" [disabled]="profileForm.invalid || isUpdating">
          {{ isUpdating ? 'Updating...' : 'Update Profile' }}
        </button>
      </form>

      <!-- Change Password Form -->
      <form [formGroup]="passwordForm" (ngSubmit)="changePassword()">
        <h3>Change Password</h3>
        
        <div class="form-group">
          <label for="currentPassword">Current Password:</label>
          <input id="currentPassword" type="password" formControlName="current_password">
        </div>

        <div class="form-group">
          <label for="newPassword">New Password:</label>
          <input id="newPassword" type="password" formControlName="new_password">
        </div>

        <div class="form-group">
          <label for="confirmNewPassword">Confirm New Password:</label>
          <input id="confirmNewPassword" type="password" formControlName="new_password_confirmation">
        </div>

        <button type="submit" [disabled]="passwordForm.invalid || isChangingPassword">
          {{ isChangingPassword ? 'Changing...' : 'Change Password' }}
        </button>
      </form>

      <!-- 2FA Management -->
      <div class="two-factor-section">
        <h3>Two-Factor Authentication</h3>
        <div *ngIf="twoFactorStatus">
          <p>Status: {{ twoFactorStatus.enabled ? 'Enabled' : 'Disabled' }}</p>
          <div *ngIf="twoFactorStatus.enabled">
            <p>Backup codes remaining: {{ twoFactorStatus.backup_codes_remaining }}</p>
            <button (click)="disable2FA()" class="btn-danger">Disable 2FA</button>
          </div>
          <div *ngIf="!twoFactorStatus.enabled">
            <button (click)="setup2FA()" class="btn-primary">Enable 2FA</button>
          </div>
        </div>
      </div>

      <!-- Messages -->
      <div class="message success" *ngIf="successMessage">{{ successMessage }}</div>
      <div class="message error" *ngIf="errorMessage">{{ errorMessage }}</div>
    </div>
  `
})
export class ProfileComponent implements OnInit {
  currentUser: User | null = null;
  profileForm: FormGroup;
  passwordForm: FormGroup;
  twoFactorStatus: any = null;
  
  isUpdating = false;
  isChangingPassword = false;
  successMessage = '';
  errorMessage = '';

  constructor(
    private fb: FormBuilder,
    private authService: AuthService
  ) {
    this.profileForm = this.fb.group({
      first_name: [''],
      last_name: ['']
    });

    this.passwordForm = this.fb.group({
      current_password: ['', Validators.required],
      new_password: ['', [Validators.required, Validators.minLength(8)]],
      new_password_confirmation: ['', Validators.required]
    });
  }

  ngOnInit() {
    this.loadUserData();
    this.load2FAStatus();
  }

  loadUserData() {
    this.authService.currentUser$.subscribe(user => {
      if (user) {
        this.currentUser = user;
        this.profileForm.patchValue({
          first_name: user.first_name || '',
          last_name: user.last_name || ''
        });
      }
    });
  }

  load2FAStatus() {
    this.authService.get2FAStatus().subscribe({
      next: (status) => {
        this.twoFactorStatus = status;
      },
      error: (error) => {
        console.error('Failed to load 2FA status:', error);
      }
    });
  }

  updateProfile() {
    if (this.profileForm.valid) {
      this.isUpdating = true;
      this.clearMessages();

      this.authService.updateProfile(this.profileForm.value).subscribe({
        next: (user) => {
          this.successMessage = 'Profile updated successfully';
          this.currentUser = user;
        },
        error: (error) => {
          this.errorMessage = error.error;
        },
        complete: () => {
          this.isUpdating = false;
        }
      });
    }
  }

  changePassword() {
    if (this.passwordForm.valid) {
      this.isChangingPassword = true;
      this.clearMessages();

      this.authService.changePassword(this.passwordForm.value).subscribe({
        next: () => {
          this.successMessage = 'Password changed successfully';
          this.passwordForm.reset();
        },
        error: (error) => {
          this.errorMessage = error.error;
        },
        complete: () => {
          this.isChangingPassword = false;
        }
      });
    }
  }

  setup2FA() {
    this.authService.setup2FA().subscribe({
      next: (response) => {
        // Show QR code and backup codes in a modal or new component
        console.log('2FA Setup:', response);
        // You would typically show this in a modal
        alert(`Scan this QR code: ${response.qr_code}\nBackup codes: ${response.backup_codes.join(', ')}`);
      },
      error: (error) => {
        this.errorMessage = error.error;
      }
    });
  }

  disable2FA() {
    if (confirm('Are you sure you want to disable 2FA?')) {
      this.authService.disable2FA().subscribe({
        next: () => {
          this.successMessage = '2FA disabled successfully';
          this.load2FAStatus();
        },
        error: (error) => {
          this.errorMessage = error.error;
        }
      });
    }
  }

  private clearMessages() {
    this.successMessage = '';
    this.errorMessage = '';
  }
}

OAuth Integration

OAuth Callback Component

// oauth-callback.component.ts
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { OAuthService } from 'auth-client';

@Component({
  selector: 'app-oauth-callback',
  template: `
    <div class="oauth-callback">
      <h2>Processing OAuth Login...</h2>
      <div class="spinner" *ngIf="isProcessing">Loading...</div>
      <div class="error" *ngIf="errorMessage">
        <p>{{ errorMessage }}</p>
        <a routerLink="/login">Back to Login</a>
      </div>
    </div>
  `
})
export class OAuthCallbackComponent implements OnInit {
  isProcessing = true;
  errorMessage = '';

  constructor(
    private route: ActivatedRoute,
    private router: Router,
    private oauthService: OAuthService
  ) {}

  ngOnInit() {
    // Get provider from route params
    const provider = this.route.snapshot.params['provider'];
    
    if (!provider) {
      this.errorMessage = 'Invalid OAuth callback - no provider specified';
      this.isProcessing = false;
      return;
    }

    // Complete OAuth flow
    this.oauthService.completeOAuthFlow(provider).subscribe({
      next: (result) => {
        console.log('OAuth login successful:', result);
        this.router.navigate(['/dashboard']);
      },
      error: (error) => {
        console.error('OAuth login failed:', error);
        this.errorMessage = error.error || 'OAuth login failed';
        this.isProcessing = false;
      }
    });
  }
}

OAuth Buttons Component

// oauth-buttons.component.ts
import { Component, OnInit } from '@angular/core';
import { OAuthService, OAuthProvider } from 'auth-client';

@Component({
  selector: 'app-oauth-buttons',
  template: `
    <div class="oauth-buttons">
      <h3>Or login with:</h3>
      <div class="providers">
        <button 
          *ngFor="let provider of providers" 
          (click)="loginWithProvider(provider.name)"
          [class]="'btn-' + provider.name">
          {{ provider.display_name }}
        </button>
      </div>
    </div>
  `
})
export class OAuthButtonsComponent implements OnInit {
  providers: OAuthProvider[] = [];

  constructor(private oauthService: OAuthService) {}

  ngOnInit() {
    this.loadProviders();
  }

  loadProviders() {
    this.oauthService.getProviders().subscribe({
      next: (providers) => {
        this.providers = providers;
      },
      error: (error) => {
        console.error('Failed to load OAuth providers:', error);
      }
    });
  }

  loginWithProvider(provider: string) {
    const redirectUri = `${window.location.origin}/oauth/callback/${provider}`;
    this.oauthService.initiateOAuthFlow(provider, redirectUri);
  }
}

Advanced Usage

Checking User Permissions

// any.component.ts
import { Component } from '@angular/core';
import { AuthService } from 'auth-client';

@Component({
  selector: 'app-example',
  template: `
    <div>
      <button *ngIf="canEdit" (click)="editItem()">Edit</button>
      <button *ngIf="canDelete" (click)="deleteItem()">Delete</button>
      <button *ngIf="isAdmin" (click)="adminAction()">Admin Action</button>
    </div>
  `
})
export class ExampleComponent {
  get canEdit(): boolean {
    return this.authService.hasScope('edit');
  }

  get canDelete(): boolean {
    return this.authService.hasScope('delete');
  }

  get isAdmin(): boolean {
    return this.authService.hasScope('admin');
  }

  constructor(private authService: AuthService) {}

  editItem() {
    console.log('Editing item...');
  }

  deleteItem() {
    console.log('Deleting item...');
  }

  adminAction() {
    console.log('Admin action...');
  }
}

Custom Auth State Service

// auth-state.service.ts
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { AuthService, User } from 'auth-client';

@Injectable({
  providedIn: 'root'
})
export class AuthStateService {
  private userMenuOpenSubject = new BehaviorSubject<boolean>(false);
  public userMenuOpen$ = this.userMenuOpenSubject.asObservable();

  constructor(private authService: AuthService) {}

  get isAuthenticated$(): Observable<boolean> {
    return this.authService.isAuthenticated$;
  }

  get currentUser$(): Observable<User | null> {
    return this.authService.currentUser$;
  }

  get userScopes(): string[] {
    return this.authService.getUserScopes();
  }

  toggleUserMenu(): void {
    this.userMenuOpenSubject.next(!this.userMenuOpenSubject.value);
  }

  closeUserMenu(): void {
    this.userMenuOpenSubject.next(false);
  }

  hasPermission(permission: string): boolean {
    return this.authService.hasScope(permission);
  }

  hasAnyPermission(permissions: string[]): boolean {
    return this.authService.hasAnyScope(permissions);
  }

  hasAllPermissions(permissions: string[]): boolean {
    return this.authService.hasAllScopes(permissions);
  }
}

Error Handling Examples

Global Error Handler

// global-error.handler.ts
import { Injectable, ErrorHandler } from '@angular/core';
import { AuthService } from 'auth-client';

@Injectable()
export class GlobalErrorHandler implements ErrorHandler {
  constructor(private authService: AuthService) {}

  handleError(error: any): void {
    console.error('Global error caught:', error);

    // Handle auth-related errors
    if (error?.status === 401) {
      this.authService.logoutSilently();
      // Optionally redirect to login
      window.location.href = '/login';
    }

    // Handle other errors...
  }
}

HTTP Error Interceptor

// error.interceptor.ts
import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpErrorResponse } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { AuthService } from 'auth-client';

@Injectable()
export class ErrorInterceptor implements HttpInterceptor {
  constructor(private authService: AuthService) {}

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<any> {
    return next.handle(req).pipe(
      catchError((error: HttpErrorResponse) => {
        if (error.status === 401) {
          // Token expired or invalid
          this.authService.logoutSilently();
        } else if (error.status === 403) {
          // Insufficient permissions
          console.warn('Access denied:', error.error);
        } else if (error.status >= 500) {
          // Server error
          console.error('Server error:', error.error);
        }

        return throwError(() => error);
      })
    );
  }
}

Testing Examples

Service Testing

// auth.service.spec.ts
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { AuthService, AuthHttpService, TokenService } from 'auth-client';

describe('AuthService', () => {
  let service: AuthService;
  let httpMock: HttpTestingController;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      providers: [AuthService, AuthHttpService, TokenService]
    });

    service = TestBed.inject(AuthService);
    httpMock = TestBed.inject(HttpTestingController);
    
    // Configure the service
    service.configure('http://localhost:4000');
  });

  afterEach(() => {
    httpMock.verify();
  });

  it('should login successfully', () => {
    const mockResponse = {
      access_token: 'mock-token',
      refresh_token: 'mock-refresh',
      token_type: 'Bearer',
      expires_in: 3600,
      user: { id: '1', email: 'test@example.com' }
    };

    service.login({ email: 'test@example.com', password: 'password' }).subscribe(response => {
      expect(response.user.email).toBe('test@example.com');
    });

    const req = httpMock.expectOne('http://localhost:4000/api/v1/auth/login');
    expect(req.request.method).toBe('POST');
    req.flush(mockResponse);
  });
});

Component Testing

// login.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ReactiveFormsModule } from '@angular/forms';
import { Router } from '@angular/router';
import { of, throwError } from 'rxjs';
import { LoginComponent } from './login.component';
import { AuthService } from 'auth-client';

describe('LoginComponent', () => {
  let component: LoginComponent;
  let fixture: ComponentFixture<LoginComponent>;
  let mockAuthService: jasmine.SpyObj<AuthService>;
  let mockRouter: jasmine.SpyObj<Router>;

  beforeEach(() => {
    const authSpy = jasmine.createSpyObj('AuthService', ['login']);
    const routerSpy = jasmine.createSpyObj('Router', ['navigate']);

    TestBed.configureTestingModule({
      declarations: [LoginComponent],
      imports: [ReactiveFormsModule],
      providers: [
        { provide: AuthService, useValue: authSpy },
        { provide: Router, useValue: routerSpy }
      ]
    });

    fixture = TestBed.createComponent(LoginComponent);
    component = fixture.componentInstance;
    mockAuthService = TestBed.inject(AuthService) as jasmine.SpyObj<AuthService>;
    mockRouter = TestBed.inject(Router) as jasmine.SpyObj<Router>;
  });

  it('should login successfully', () => {
    const mockResponse = {
      access_token: 'token',
      refresh_token: 'refresh',
      token_type: 'Bearer',
      expires_in: 3600,
      user: { id: '1', email: 'test@example.com' }
    };

    mockAuthService.login.and.returnValue(of(mockResponse));

    component.loginForm.patchValue({
      email: 'test@example.com',
      password: 'password'
    });

    component.onSubmit();

    expect(mockAuthService.login).toHaveBeenCalled();
    expect(mockRouter.navigate).toHaveBeenCalledWith(['/dashboard']);
  });

  it('should handle login error', () => {
    const mockError = { error: 'Invalid credentials' };
    mockAuthService.login.and.returnValue(throwError(() => mockError));

    component.loginForm.patchValue({
      email: 'test@example.com',
      password: 'wrong-password'
    });

    component.onSubmit();

    expect(component.errorMessage).toBe('Invalid credentials');
  });
});

This comprehensive usage guide covers all the major features and use cases of the auth client library, providing practical examples for implementing authentication in Angular applications.