# Auth Client Library Usage Guide ## Quick Start ### 1. Installation & Setup ```bash # 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 ```typescript // 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 ```typescript // 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 ```typescript // app.component.ts import { Component, OnInit } from '@angular/core'; import { AuthService } from 'auth-client'; import { environment } from '../environments/environment'; @Component({ selector: 'app-root', template: `
` }) 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 ```typescript // 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: `

Login

{{ errorMessage }}

Don't have an account? Register here

` }) 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 ```typescript // 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: `

Register

{{ errorMessage }}

Already have an account? Login here

` }) 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 ```typescript // 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 ```typescript // 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: `

User Profile

Account Information

Email: {{ currentUser.email }}

Status: {{ currentUser.is_active ? 'Active' : 'Inactive' }}

Email Verified: {{ currentUser.email_verified ? 'Yes' : 'No' }}

Member Since: {{ currentUser.created_at | date }}

Update Profile

Change Password

Two-Factor Authentication

Status: {{ twoFactorStatus.enabled ? 'Enabled' : 'Disabled' }}

Backup codes remaining: {{ twoFactorStatus.backup_codes_remaining }}

{{ successMessage }}
{{ errorMessage }}
` }) 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 ```typescript // 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: `

Processing OAuth Login...

Loading...

{{ errorMessage }}

Back to Login
` }) 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 ```typescript // oauth-buttons.component.ts import { Component, OnInit } from '@angular/core'; import { OAuthService, OAuthProvider } from 'auth-client'; @Component({ selector: 'app-oauth-buttons', template: `

Or login with:

` }) 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 ```typescript // any.component.ts import { Component } from '@angular/core'; import { AuthService } from 'auth-client'; @Component({ selector: 'app-example', template: `
` }) 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 ```typescript // 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(false); public userMenuOpen$ = this.userMenuOpenSubject.asObservable(); constructor(private authService: AuthService) {} get isAuthenticated$(): Observable { return this.authService.isAuthenticated$; } get currentUser$(): Observable { 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 ```typescript // 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 ```typescript // 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, next: HttpHandler): Observable { 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 ```typescript // 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 ```typescript // 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; let mockAuthService: jasmine.SpyObj; let mockRouter: jasmine.SpyObj; 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; mockRouter = TestBed.inject(Router) as jasmine.SpyObj; }); 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.