# 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: `
`
})
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: `
`
})
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 }}
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...
`
})
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: `
`
})
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.