- 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>
28 KiB
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.