- 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>
1076 lines
28 KiB
Markdown
1076 lines
28 KiB
Markdown
# 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: `
|
|
<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
|
|
|
|
```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: `
|
|
<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
|
|
|
|
```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: `
|
|
<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
|
|
|
|
```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: `
|
|
<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
|
|
|
|
```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: `
|
|
<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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```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<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
|
|
|
|
```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<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
|
|
|
|
```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<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. |