diff --git a/.claude/settings.local.json b/.claude/settings.local.json
new file mode 100644
index 0000000..1bcdb07
--- /dev/null
+++ b/.claude/settings.local.json
@@ -0,0 +1,10 @@
+{
+ "permissions": {
+ "allow": [
+ "Bash(ng generate:*)",
+ "Bash(git add:*)"
+ ],
+ "deny": [],
+ "ask": []
+ }
+}
\ No newline at end of file
diff --git a/angular.json b/angular.json
index 51c7d61..7f13c05 100644
--- a/angular.json
+++ b/angular.json
@@ -460,6 +460,36 @@
}
}
}
+ },
+ "auth-client": {
+ "projectType": "library",
+ "root": "projects/auth-client",
+ "sourceRoot": "projects/auth-client/src",
+ "prefix": "lib",
+ "architect": {
+ "build": {
+ "builder": "@angular/build:ng-packagr",
+ "configurations": {
+ "production": {
+ "tsConfig": "projects/auth-client/tsconfig.lib.prod.json"
+ },
+ "development": {
+ "tsConfig": "projects/auth-client/tsconfig.lib.json"
+ }
+ },
+ "defaultConfiguration": "production"
+ },
+ "test": {
+ "builder": "@angular/build:karma",
+ "options": {
+ "tsConfig": "projects/auth-client/tsconfig.spec.json",
+ "polyfills": [
+ "zone.js",
+ "zone.js/testing"
+ ]
+ }
+ }
+ }
}
}
}
diff --git a/package.json b/package.json
index a42f0c5..ba2b0f4 100644
--- a/package.json
+++ b/package.json
@@ -29,6 +29,7 @@
},
"devDependencies": {
"@angular-devkit/build-angular": "^19.2.15",
+ "@angular/build": "^20.2.0",
"@angular/cli": "^19.2.15",
"@angular/compiler-cli": "^19.2.0",
"@types/jasmine": "~5.1.0",
@@ -42,4 +43,4 @@
"ng-packagr": "^19.2.0",
"typescript": "~5.7.2"
}
-}
+}
\ No newline at end of file
diff --git a/projects/auth-client/README.md b/projects/auth-client/README.md
new file mode 100644
index 0000000..2463c58
--- /dev/null
+++ b/projects/auth-client/README.md
@@ -0,0 +1,468 @@
+# Auth Client Library
+
+Angular client library for integrating with the Elixir-based auth service. Provides authentication, authorization, and user management capabilities.
+
+## Features
+
+- **Authentication**: Login, register, logout with JWT tokens
+- **Token Management**: Automatic token refresh and storage
+- **OAuth Integration**: Social authentication with multiple providers
+- **Two-Factor Authentication**: TOTP and backup codes support
+- **Route Guards**: Protect routes based on authentication and scopes
+- **HTTP Interceptor**: Automatic token injection and refresh
+- **TypeScript Support**: Fully typed interfaces and models
+
+## Installation
+
+```bash
+npm install auth-client
+```
+
+## Setup
+
+### 1. Import the HTTP Client Module
+
+```typescript
+import { HttpClientModule } from '@angular/common/http';
+import { NgModule } from '@angular/core';
+
+@NgModule({
+ imports: [
+ HttpClientModule,
+ // ... other imports
+ ],
+})
+export class AppModule {}
+```
+
+### 2. Configure the Auth Service
+
+```typescript
+import { AuthService } from 'auth-client';
+
+export class AppComponent {
+ constructor(private authService: AuthService) {
+ // Configure the base URL for your auth service
+ this.authService.configure('http://localhost:4000');
+ }
+}
+```
+
+### 3. Add HTTP Interceptor (Optional)
+
+```typescript
+import { HTTP_INTERCEPTORS } from '@angular/common/http';
+import { AuthInterceptor } from 'auth-client';
+
+@NgModule({
+ providers: [
+ {
+ provide: HTTP_INTERCEPTORS,
+ useClass: AuthInterceptor,
+ multi: true
+ }
+ ]
+})
+export class AppModule {}
+```
+
+## Usage
+
+### Authentication
+
+#### Login
+
+```typescript
+import { AuthService } from 'auth-client';
+
+constructor(private authService: AuthService) {}
+
+login() {
+ this.authService.login({
+ email: 'user@example.com',
+ password: 'password123'
+ }).subscribe({
+ next: (response) => {
+ console.log('Login successful:', response.user);
+ },
+ error: (error) => {
+ console.error('Login failed:', error);
+ }
+ });
+}
+```
+
+#### Register
+
+```typescript
+register() {
+ this.authService.register({
+ email: 'user@example.com',
+ password: 'password123',
+ first_name: 'John',
+ last_name: 'Doe'
+ }).subscribe({
+ next: (response) => {
+ console.log('Registration successful:', response.user);
+ },
+ error: (error) => {
+ console.error('Registration failed:', error);
+ }
+ });
+}
+```
+
+#### Logout
+
+```typescript
+logout() {
+ this.authService.logout().subscribe({
+ next: () => {
+ console.log('Logout successful');
+ },
+ error: (error) => {
+ console.error('Logout failed:', error);
+ }
+ });
+}
+```
+
+### Authentication State
+
+```typescript
+import { AuthService } from 'auth-client';
+
+constructor(private authService: AuthService) {
+ // Subscribe to authentication state
+ this.authService.isAuthenticated$.subscribe(isAuth => {
+ console.log('Is authenticated:', isAuth);
+ });
+
+ // Subscribe to current user
+ this.authService.currentUser$.subscribe(user => {
+ console.log('Current user:', user);
+ });
+
+ // Check if authenticated synchronously
+ const isAuth = this.authService.isAuthenticated();
+}
+```
+
+### Route Guards
+
+#### Protect Authenticated Routes
+
+```typescript
+import { AuthGuard } from 'auth-client';
+
+const routes: Routes = [
+ {
+ path: 'dashboard',
+ component: DashboardComponent,
+ canActivate: [AuthGuard]
+ },
+ {
+ path: 'admin',
+ component: AdminComponent,
+ canActivate: [AuthGuard],
+ data: {
+ requiredScopes: ['admin'], // Require admin scope
+ requireAllScopes: true, // Must have all scopes (default: false)
+ unauthorizedRedirect: '/access-denied'
+ }
+ }
+];
+```
+
+#### Protect Guest Routes
+
+```typescript
+import { GuestGuard } from 'auth-client';
+
+const routes: Routes = [
+ {
+ path: 'login',
+ component: LoginComponent,
+ canActivate: [GuestGuard], // Redirect to home if already authenticated
+ data: {
+ authenticatedRedirect: '/dashboard'
+ }
+ }
+];
+```
+
+### OAuth Integration
+
+#### Get Available Providers
+
+```typescript
+import { OAuthService } from 'auth-client';
+
+constructor(private oauthService: OAuthService) {}
+
+getProviders() {
+ this.oauthService.getProviders().subscribe(providers => {
+ console.log('Available providers:', providers);
+ });
+}
+```
+
+#### Initiate OAuth Flow
+
+```typescript
+// Redirect flow
+loginWithGoogle() {
+ this.oauthService.initiateOAuthFlow('google', 'http://localhost:4200/oauth/callback');
+}
+
+// Popup flow
+async loginWithGooglePopup() {
+ try {
+ const result = await this.oauthService.initiateOAuthPopup('google');
+ console.log('OAuth successful:', result);
+ } catch (error) {
+ console.error('OAuth failed:', error);
+ }
+}
+```
+
+#### Handle OAuth Callback
+
+```typescript
+// In your callback component
+ngOnInit() {
+ this.oauthService.completeOAuthFlow('google').subscribe({
+ next: (result) => {
+ console.log('OAuth login successful:', result);
+ this.router.navigate(['/dashboard']);
+ },
+ error: (error) => {
+ console.error('OAuth login failed:', error);
+ this.router.navigate(['/login']);
+ }
+ });
+}
+```
+
+### Two-Factor Authentication
+
+#### Setup 2FA
+
+```typescript
+setup2FA() {
+ this.authService.setup2FA().subscribe({
+ next: (response) => {
+ // Display QR code and backup codes
+ console.log('QR Code:', response.qr_code);
+ console.log('Backup codes:', response.backup_codes);
+ },
+ error: (error) => {
+ console.error('2FA setup failed:', error);
+ }
+ });
+}
+```
+
+#### Verify 2FA Setup
+
+```typescript
+verify2FA(token: string) {
+ this.authService.verify2FASetup({ token }).subscribe({
+ next: () => {
+ console.log('2FA enabled successfully');
+ },
+ error: (error) => {
+ console.error('2FA verification failed:', error);
+ }
+ });
+}
+```
+
+### User Management
+
+#### Update Profile
+
+```typescript
+updateProfile() {
+ this.authService.updateProfile({
+ first_name: 'Jane',
+ last_name: 'Smith'
+ }).subscribe({
+ next: (user) => {
+ console.log('Profile updated:', user);
+ },
+ error: (error) => {
+ console.error('Profile update failed:', error);
+ }
+ });
+}
+```
+
+#### Change Password
+
+```typescript
+changePassword() {
+ this.authService.changePassword({
+ current_password: 'oldpassword',
+ new_password: 'newpassword',
+ new_password_confirmation: 'newpassword'
+ }).subscribe({
+ next: () => {
+ console.log('Password changed successfully');
+ },
+ error: (error) => {
+ console.error('Password change failed:', error);
+ }
+ });
+}
+```
+
+### Token Management
+
+#### Manual Token Operations
+
+```typescript
+import { TokenService } from 'auth-client';
+
+constructor(private tokenService: TokenService) {}
+
+checkToken() {
+ // Check if token exists and is valid
+ const isValid = this.tokenService.isTokenValid();
+
+ // Get user information from token
+ const userId = this.tokenService.getUserId();
+ const email = this.tokenService.getUserEmail();
+ const scopes = this.tokenService.getUserScopes();
+
+ // Check scopes
+ const hasAdminScope = this.tokenService.hasScope('admin');
+ const hasAnyScope = this.tokenService.hasAnyScope(['read', 'write']);
+ const hasAllScopes = this.tokenService.hasAllScopes(['read', 'write']);
+}
+```
+
+## API Reference
+
+### Models
+
+All TypeScript interfaces are available for import:
+
+```typescript
+import {
+ User,
+ LoginRequest,
+ LoginResponse,
+ RegisterRequest,
+ TokenPair,
+ ApiError,
+ // ... and more
+} from 'auth-client';
+```
+
+### Services
+
+- **AuthService**: Main authentication service
+- **TokenService**: Token management and validation
+- **OAuthService**: OAuth provider integration
+- **AuthHttpService**: Low-level HTTP client
+
+### Guards
+
+- **AuthGuard**: Protect authenticated routes
+- **GuestGuard**: Protect guest-only routes
+
+### Interceptors
+
+- **AuthInterceptor**: Automatic token injection and refresh
+
+## Configuration
+
+### Environment Variables
+
+You can configure the auth service URL through your environment:
+
+```typescript
+// environment.ts
+export const environment = {
+ authServiceUrl: 'http://localhost:4000'
+};
+
+// app.component.ts
+constructor(private authService: AuthService) {
+ this.authService.configure(environment.authServiceUrl);
+}
+```
+
+### Token Storage
+
+By default, tokens are stored in localStorage. The library handles:
+
+- Automatic token refresh before expiration
+- Cross-tab synchronization
+- Token validation and cleanup
+
+## Error Handling
+
+All services return Observable streams with proper error handling:
+
+```typescript
+this.authService.login(credentials).subscribe({
+ next: (response) => {
+ // Handle success
+ },
+ error: (apiError: ApiError) => {
+ console.error('Error:', apiError.error);
+
+ // Handle specific errors
+ if (apiError.requires_2fa) {
+ // Redirect to 2FA input
+ }
+
+ if (apiError.details) {
+ // Handle validation errors
+ console.error('Validation errors:', apiError.details);
+ }
+ }
+});
+```
+
+## Building
+
+To build the library, run:
+
+```bash
+ng build auth-client
+```
+
+This command will compile your project, and the build artifacts will be placed in the `dist/` directory.
+
+## Publishing the Library
+
+Once the project is built, you can publish your library by following these steps:
+
+1. Navigate to the `dist` directory:
+ ```bash
+ cd dist/auth-client
+ ```
+
+2. Run the `npm publish` command to publish your library to the npm registry:
+ ```bash
+ npm publish
+ ```
+
+## Running unit tests
+
+To execute unit tests with the [Karma](https://karma-runner.github.io) test runner, use the following command:
+
+```bash
+ng test auth-client
+```
+
+## Contributing
+
+This library is designed to work with the Elixir auth service. Please ensure API compatibility when making changes.
+
+## License
+
+MIT License
diff --git a/projects/auth-client/USAGE_GUIDE.md b/projects/auth-client/USAGE_GUIDE.md
new file mode 100644
index 0000000..5ae19ff
--- /dev/null
+++ b/projects/auth-client/USAGE_GUIDE.md
@@ -0,0 +1,1076 @@
+# 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.
\ No newline at end of file
diff --git a/projects/auth-client/ng-package.json b/projects/auth-client/ng-package.json
new file mode 100644
index 0000000..26c7901
--- /dev/null
+++ b/projects/auth-client/ng-package.json
@@ -0,0 +1,7 @@
+{
+ "$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
+ "dest": "../../dist/auth-client",
+ "lib": {
+ "entryFile": "src/public-api.ts"
+ }
+}
\ No newline at end of file
diff --git a/projects/auth-client/package.json b/projects/auth-client/package.json
new file mode 100644
index 0000000..5d33921
--- /dev/null
+++ b/projects/auth-client/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "auth-client",
+ "version": "0.0.1",
+ "peerDependencies": {
+ "@angular/common": "^20.2.0",
+ "@angular/core": "^20.2.0"
+ },
+ "dependencies": {
+ "tslib": "^2.3.0"
+ },
+ "sideEffects": false
+}
diff --git a/projects/auth-client/src/lib/auth-client.spec.ts b/projects/auth-client/src/lib/auth-client.spec.ts
new file mode 100644
index 0000000..328c7e4
--- /dev/null
+++ b/projects/auth-client/src/lib/auth-client.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { AuthClient } from './auth-client';
+
+describe('AuthClient', () => {
+ let component: AuthClient;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [AuthClient]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(AuthClient);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/projects/auth-client/src/lib/auth-client.ts b/projects/auth-client/src/lib/auth-client.ts
new file mode 100644
index 0000000..887b574
--- /dev/null
+++ b/projects/auth-client/src/lib/auth-client.ts
@@ -0,0 +1,15 @@
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'lib-auth-client',
+ imports: [],
+ template: `
+
+ auth-client works!
+
+ `,
+ styles: ``
+})
+export class AuthClient {
+
+}
diff --git a/projects/auth-client/src/lib/guards/auth.guard.ts b/projects/auth-client/src/lib/guards/auth.guard.ts
new file mode 100644
index 0000000..0a943d4
--- /dev/null
+++ b/projects/auth-client/src/lib/guards/auth.guard.ts
@@ -0,0 +1,67 @@
+import { Injectable } from '@angular/core';
+import { CanActivate, CanActivateChild, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router';
+import { Observable, of } from 'rxjs';
+import { map, take, tap } from 'rxjs/operators';
+import { AuthService } from '../services/auth.service';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class AuthGuard implements CanActivate, CanActivateChild {
+ constructor(
+ private authService: AuthService,
+ private router: Router
+ ) {}
+
+ canActivate(
+ route: ActivatedRouteSnapshot,
+ state: RouterStateSnapshot
+ ): Observable {
+ return this.checkAuth(route, state.url);
+ }
+
+ canActivateChild(
+ childRoute: ActivatedRouteSnapshot,
+ state: RouterStateSnapshot
+ ): Observable {
+ return this.canActivate(childRoute, state);
+ }
+
+ private checkAuth(route: ActivatedRouteSnapshot, redirectUrl: string): Observable {
+ return this.authService.isAuthenticated$.pipe(
+ take(1),
+ map(isAuthenticated => {
+ if (isAuthenticated) {
+ // Check for required scopes if specified
+ const requiredScopes = route.data?.['requiredScopes'] as string[];
+ if (requiredScopes && requiredScopes.length > 0) {
+ const hasScopes = route.data?.['requireAllScopes']
+ ? this.authService.hasAllScopes(requiredScopes)
+ : this.authService.hasAnyScope(requiredScopes);
+
+ if (!hasScopes) {
+ this.handleUnauthorized(route.data?.['unauthorizedRedirect']);
+ return false;
+ }
+ }
+ return true;
+ } else {
+ this.handleUnauthenticated(redirectUrl, route.data?.['loginRedirect']);
+ return false;
+ }
+ })
+ );
+ }
+
+ private handleUnauthenticated(redirectUrl: string, customLoginPath?: string): void {
+ const loginPath = customLoginPath || '/login';
+ this.router.navigate([loginPath], {
+ queryParams: { returnUrl: redirectUrl }
+ });
+ }
+
+ private handleUnauthorized(customUnauthorizedPath?: string): void {
+ const unauthorizedPath = customUnauthorizedPath || '/unauthorized';
+ this.router.navigate([unauthorizedPath]);
+ }
+}
\ No newline at end of file
diff --git a/projects/auth-client/src/lib/guards/guest.guard.ts b/projects/auth-client/src/lib/guards/guest.guard.ts
new file mode 100644
index 0000000..5340ab3
--- /dev/null
+++ b/projects/auth-client/src/lib/guards/guest.guard.ts
@@ -0,0 +1,33 @@
+import { Injectable } from '@angular/core';
+import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router';
+import { Observable } from 'rxjs';
+import { map, take } from 'rxjs/operators';
+import { AuthService } from '../services/auth.service';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class GuestGuard implements CanActivate {
+ constructor(
+ private authService: AuthService,
+ private router: Router
+ ) {}
+
+ canActivate(
+ route: ActivatedRouteSnapshot,
+ state: RouterStateSnapshot
+ ): Observable {
+ return this.authService.isAuthenticated$.pipe(
+ take(1),
+ map(isAuthenticated => {
+ if (isAuthenticated) {
+ // User is authenticated, redirect to home or specified route
+ const redirectTo = route.data?.['authenticatedRedirect'] || '/';
+ this.router.navigate([redirectTo]);
+ return false;
+ }
+ return true;
+ })
+ );
+ }
+}
\ No newline at end of file
diff --git a/projects/auth-client/src/lib/interceptors/auth.interceptor.ts b/projects/auth-client/src/lib/interceptors/auth.interceptor.ts
new file mode 100644
index 0000000..ba9e5b4
--- /dev/null
+++ b/projects/auth-client/src/lib/interceptors/auth.interceptor.ts
@@ -0,0 +1,118 @@
+import { Injectable } from '@angular/core';
+import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpErrorResponse } from '@angular/common/http';
+import { Observable, throwError, BehaviorSubject } from 'rxjs';
+import { catchError, switchMap, filter, take } from 'rxjs/operators';
+import { TokenService } from '../services/token.service';
+import { AuthHttpService } from '../services/auth-http.service';
+
+@Injectable()
+export class AuthInterceptor implements HttpInterceptor {
+ private isRefreshing = false;
+ private refreshTokenSubject: BehaviorSubject = new BehaviorSubject(null);
+
+ constructor(
+ private tokenService: TokenService,
+ private authHttpService: AuthHttpService
+ ) {}
+
+ intercept(request: HttpRequest, next: HttpHandler): Observable> {
+ // Add auth token to request if available and not already present
+ if (this.shouldAddToken(request)) {
+ request = this.addToken(request);
+ }
+
+ return next.handle(request).pipe(
+ catchError(error => {
+ if (error instanceof HttpErrorResponse && error.status === 401) {
+ return this.handle401Error(request, next);
+ }
+ return throwError(() => error);
+ })
+ );
+ }
+
+ /**
+ * Check if token should be added to this request
+ */
+ private shouldAddToken(request: HttpRequest): boolean {
+ // Don't add token if already present
+ if (request.headers.has('Authorization')) {
+ return false;
+ }
+
+ // Don't add token to auth endpoints that don't require it
+ const url = request.url.toLowerCase();
+ const publicEndpoints = [
+ '/auth/login',
+ '/auth/register',
+ '/auth/refresh',
+ '/auth/validate',
+ '/users/forgot-password',
+ '/users/reset-password',
+ '/users/verify-email',
+ '/oauth/providers',
+ '/oauth/',
+ '/health'
+ ];
+
+ return !publicEndpoints.some(endpoint => url.includes(endpoint));
+ }
+
+ /**
+ * Add authentication token to request
+ */
+ private addToken(request: HttpRequest): HttpRequest {
+ const authHeader = this.tokenService.getAuthorizationHeader();
+
+ if (authHeader) {
+ return request.clone({
+ setHeaders: {
+ Authorization: authHeader
+ }
+ });
+ }
+
+ return request;
+ }
+
+ /**
+ * Handle 401 errors by attempting token refresh
+ */
+ private handle401Error(request: HttpRequest, next: HttpHandler): Observable> {
+ if (!this.isRefreshing) {
+ this.isRefreshing = true;
+ this.refreshTokenSubject.next(null);
+
+ const refreshToken = this.tokenService.getRefreshToken();
+
+ if (refreshToken) {
+ return this.authHttpService.refreshToken({ refresh_token: refreshToken }).pipe(
+ switchMap((tokenPair) => {
+ this.isRefreshing = false;
+ this.tokenService.setTokens(tokenPair);
+ this.refreshTokenSubject.next(tokenPair.access_token);
+
+ // Retry original request with new token
+ return next.handle(this.addToken(request));
+ }),
+ catchError((error) => {
+ this.isRefreshing = false;
+ this.tokenService.clearTokens();
+ return throwError(() => error);
+ })
+ );
+ } else {
+ this.isRefreshing = false;
+ this.tokenService.clearTokens();
+ return throwError(() => new Error('No refresh token available'));
+ }
+ } else {
+ // If refresh is in progress, wait for it to complete
+ return this.refreshTokenSubject.pipe(
+ filter(token => token != null),
+ take(1),
+ switchMap(() => next.handle(this.addToken(request)))
+ );
+ }
+ }
+}
\ No newline at end of file
diff --git a/projects/auth-client/src/lib/models/auth.models.ts b/projects/auth-client/src/lib/models/auth.models.ts
new file mode 100644
index 0000000..f58cb48
--- /dev/null
+++ b/projects/auth-client/src/lib/models/auth.models.ts
@@ -0,0 +1,257 @@
+export interface LoginRequest {
+ email: string;
+ password: string;
+ totp_code?: string;
+ backup_code?: string;
+}
+
+export interface RegisterRequest {
+ email: string;
+ password: string;
+ password_confirmation?: string;
+ first_name?: string;
+ last_name?: string;
+}
+
+export interface TokenPair {
+ access_token: string;
+ refresh_token: string;
+ token_type: string;
+ expires_in: number;
+ scopes?: string[];
+}
+
+export interface LoginResponse extends TokenPair {
+ user: User;
+}
+
+export interface RegisterResponse extends TokenPair {
+ user: User;
+}
+
+export interface User {
+ id: string;
+ email: string;
+ first_name?: string;
+ last_name?: string;
+ is_active: boolean;
+ email_verified: boolean;
+ profile_data?: Record;
+ created_at: string;
+ updated_at: string;
+}
+
+export interface TokenValidationRequest {
+ token: string;
+}
+
+export interface TokenValidationResponse {
+ valid: boolean;
+ user_id?: string;
+ email?: string;
+ scopes?: string[];
+ organization?: string;
+ expires_at?: number;
+ reason?: string;
+}
+
+export interface RefreshTokenRequest {
+ refresh_token: string;
+}
+
+export interface LogoutRequest {
+ refresh_token?: string;
+}
+
+export interface PasswordResetRequest {
+ email: string;
+}
+
+export interface PasswordResetConfirmRequest {
+ token: string;
+ password: string;
+ password_confirmation: string;
+}
+
+export interface ChangePasswordRequest {
+ current_password: string;
+ new_password: string;
+ new_password_confirmation: string;
+}
+
+export interface EmailVerificationRequest {
+ token: string;
+}
+
+export interface ResendVerificationRequest {
+ email: string;
+}
+
+export interface TwoFactorSetupResponse {
+ secret: string;
+ qr_code: string;
+ backup_codes: string[];
+}
+
+export interface TwoFactorVerifyRequest {
+ token: string;
+}
+
+export interface TwoFactorStatusResponse {
+ enabled: boolean;
+ backup_codes_remaining?: number;
+}
+
+export interface ApiError {
+ error: string;
+ details?: Record;
+ requires_2fa?: boolean;
+}
+
+export interface ApiResponse {
+ data?: T;
+ error?: ApiError;
+ message?: string;
+}
+
+export interface OAuthProvider {
+ name: string;
+ display_name: string;
+ authorization_url?: string;
+}
+
+export interface OAuthProvidersResponse {
+ providers: OAuthProvider[];
+}
+
+export interface OAuthLinkRequest {
+ provider: string;
+ code: string;
+ state?: string;
+}
+
+export interface OrganizationMember {
+ id: string;
+ user_id: string;
+ email: string;
+ first_name?: string;
+ last_name?: string;
+ role: string;
+ joined_at: string;
+}
+
+export interface Organization {
+ id: string;
+ name: string;
+ description?: string;
+ settings?: Record;
+ created_at: string;
+ updated_at: string;
+}
+
+export interface CreateOrganizationRequest {
+ name: string;
+ description?: string;
+}
+
+export interface UpdateOrganizationRequest {
+ name?: string;
+ description?: string;
+}
+
+export interface InviteMemberRequest {
+ email: string;
+ role: string;
+}
+
+export interface UpdateMemberRoleRequest {
+ role: string;
+}
+
+export interface Service {
+ id: string;
+ name: string;
+ description?: string;
+ permissions: string[];
+ validation_mode: 'trust_gateway' | 'validate_sensitive' | 'always_validate';
+ created_at: string;
+ updated_at: string;
+}
+
+export interface UserPermissions {
+ service_id: string;
+ service_name: string;
+ permissions: string[];
+}
+
+export interface ApiKey {
+ id: string;
+ name: string;
+ key_prefix: string;
+ scopes: string[];
+ is_active: boolean;
+ last_used_at?: string;
+ expires_at?: string;
+ created_at: string;
+ updated_at: string;
+}
+
+export interface CreateApiKeyRequest {
+ name: string;
+ scopes: string[];
+ expires_at?: string;
+}
+
+export interface UpdateApiKeyRequest {
+ name?: string;
+ scopes?: string[];
+ expires_at?: string;
+}
+
+export interface ApiKeyUsageStats {
+ total_requests: number;
+ requests_today: number;
+ requests_this_week: number;
+ requests_this_month: number;
+ last_request_at?: string;
+}
+
+export interface AuditLog {
+ id: string;
+ action: string;
+ resource_type?: string;
+ resource_id?: string;
+ details?: Record;
+ ip_address?: string;
+ user_agent?: string;
+ status: 'success' | 'failure';
+ created_at: string;
+}
+
+export interface LoginAttempt {
+ id: string;
+ email: string;
+ ip_address: string;
+ user_agent?: string;
+ status: 'success' | 'failure';
+ failure_reason?: string;
+ created_at: string;
+}
+
+export interface SecurityStats {
+ total_users: number;
+ active_sessions: number;
+ failed_logins_today: number;
+ blocked_ips: number;
+ two_fa_enabled_users: number;
+}
+
+export interface RateLimit {
+ identifier: string;
+ identifier_type: 'ip' | 'user' | 'api_key';
+ endpoint: string;
+ requests_count: number;
+ window_start: string;
+ is_blocked: boolean;
+ blocked_until?: string;
+}
\ No newline at end of file
diff --git a/projects/auth-client/src/lib/services/auth-http.service.ts b/projects/auth-client/src/lib/services/auth-http.service.ts
new file mode 100644
index 0000000..1cb673b
--- /dev/null
+++ b/projects/auth-client/src/lib/services/auth-http.service.ts
@@ -0,0 +1,331 @@
+import { Injectable } from '@angular/core';
+import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
+import { Observable, throwError } from 'rxjs';
+import { catchError, map } from 'rxjs/operators';
+import { TokenService } from './token.service';
+import {
+ LoginRequest,
+ LoginResponse,
+ RegisterRequest,
+ RegisterResponse,
+ TokenValidationRequest,
+ TokenValidationResponse,
+ RefreshTokenRequest,
+ TokenPair,
+ LogoutRequest,
+ ApiResponse,
+ ApiError,
+ PasswordResetRequest,
+ PasswordResetConfirmRequest,
+ ChangePasswordRequest,
+ EmailVerificationRequest,
+ ResendVerificationRequest,
+ User,
+ TwoFactorSetupResponse,
+ TwoFactorVerifyRequest,
+ TwoFactorStatusResponse
+} from '../models/auth.models';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class AuthHttpService {
+ private baseUrl = '';
+
+ constructor(
+ private http: HttpClient,
+ private tokenService: TokenService
+ ) {}
+
+ /**
+ * Set the base URL for the auth service
+ */
+ setBaseUrl(url: string): void {
+ this.baseUrl = url.replace(/\/$/, ''); // Remove trailing slash
+ }
+
+ /**
+ * Get API URL with version prefix
+ */
+ private getApiUrl(path: string): string {
+ return `${this.baseUrl}/api/v1${path}`;
+ }
+
+ /**
+ * Get HTTP headers with authorization if available
+ */
+ private getHeaders(includeAuth = true): HttpHeaders {
+ let headers = new HttpHeaders({
+ 'Content-Type': 'application/json'
+ });
+
+ if (includeAuth) {
+ const authHeader = this.tokenService.getAuthorizationHeader();
+ if (authHeader) {
+ headers = headers.set('Authorization', authHeader);
+ }
+ }
+
+ return headers;
+ }
+
+ /**
+ * Handle HTTP errors and extract API error information
+ */
+ private handleError(error: any): Observable {
+ let apiError: ApiError;
+
+ if (error.error && typeof error.error === 'object') {
+ apiError = error.error;
+ } else {
+ apiError = {
+ error: error.message || 'An unknown error occurred'
+ };
+ }
+
+ return throwError(() => apiError);
+ }
+
+ // Authentication Endpoints
+
+ /**
+ * Register a new user
+ */
+ register(request: RegisterRequest): Observable {
+ return this.http.post(
+ this.getApiUrl('/auth/register'),
+ request,
+ { headers: this.getHeaders(false) }
+ ).pipe(
+ catchError(this.handleError)
+ );
+ }
+
+ /**
+ * Login with email and password
+ */
+ login(request: LoginRequest): Observable {
+ return this.http.post(
+ this.getApiUrl('/auth/login'),
+ request,
+ { headers: this.getHeaders(false) }
+ ).pipe(
+ catchError(this.handleError)
+ );
+ }
+
+ /**
+ * Refresh access token
+ */
+ refreshToken(request: RefreshTokenRequest): Observable {
+ return this.http.post(
+ this.getApiUrl('/auth/refresh'),
+ request,
+ { headers: this.getHeaders(false) }
+ ).pipe(
+ catchError(this.handleError)
+ );
+ }
+
+ /**
+ * Validate token (for API Gateway)
+ */
+ validateToken(request: TokenValidationRequest): Observable {
+ return this.http.post(
+ this.getApiUrl('/auth/validate'),
+ request,
+ { headers: this.getHeaders(false) }
+ ).pipe(
+ catchError(this.handleError)
+ );
+ }
+
+ /**
+ * Logout user
+ */
+ logout(request?: LogoutRequest): Observable {
+ return this.http.post(
+ this.getApiUrl('/auth/logout'),
+ request || {},
+ { headers: this.getHeaders() }
+ ).pipe(
+ catchError(this.handleError)
+ );
+ }
+
+ /**
+ * Get current user information
+ */
+ getCurrentUser(): Observable {
+ return this.http.get<{ user: User }>(
+ this.getApiUrl('/auth/me'),
+ { headers: this.getHeaders() }
+ ).pipe(
+ map(response => response.user),
+ catchError(this.handleError)
+ );
+ }
+
+ // User Management Endpoints
+
+ /**
+ * Get user profile
+ */
+ getUserProfile(): Observable {
+ return this.http.get<{ user: User }>(
+ this.getApiUrl('/users/profile'),
+ { headers: this.getHeaders() }
+ ).pipe(
+ map(response => response.user),
+ catchError(this.handleError)
+ );
+ }
+
+ /**
+ * Update user profile
+ */
+ updateUserProfile(updates: Partial): Observable {
+ return this.http.put<{ user: User }>(
+ this.getApiUrl('/users/profile'),
+ updates,
+ { headers: this.getHeaders() }
+ ).pipe(
+ map(response => response.user),
+ catchError(this.handleError)
+ );
+ }
+
+ /**
+ * Change password
+ */
+ changePassword(request: ChangePasswordRequest): Observable {
+ return this.http.post(
+ this.getApiUrl('/users/change-password'),
+ request,
+ { headers: this.getHeaders() }
+ ).pipe(
+ catchError(this.handleError)
+ );
+ }
+
+ /**
+ * Request password reset
+ */
+ forgotPassword(request: PasswordResetRequest): Observable {
+ return this.http.post(
+ this.getApiUrl('/users/forgot-password'),
+ request,
+ { headers: this.getHeaders(false) }
+ ).pipe(
+ catchError(this.handleError)
+ );
+ }
+
+ /**
+ * Reset password with token
+ */
+ resetPassword(request: PasswordResetConfirmRequest): Observable {
+ return this.http.post(
+ this.getApiUrl('/users/reset-password'),
+ request,
+ { headers: this.getHeaders(false) }
+ ).pipe(
+ catchError(this.handleError)
+ );
+ }
+
+ /**
+ * Verify email with token
+ */
+ verifyEmail(request: EmailVerificationRequest): Observable {
+ return this.http.post(
+ this.getApiUrl('/users/verify-email'),
+ request,
+ { headers: this.getHeaders(false) }
+ ).pipe(
+ catchError(this.handleError)
+ );
+ }
+
+ /**
+ * Resend email verification
+ */
+ resendEmailVerification(request?: ResendVerificationRequest): Observable {
+ const url = request
+ ? this.getApiUrl('/users/resend-verification')
+ : this.getApiUrl('/users/resend-verification');
+
+ const body = request || {};
+ const headers = request ? this.getHeaders(false) : this.getHeaders();
+
+ return this.http.post(url, body, { headers }).pipe(
+ catchError(this.handleError)
+ );
+ }
+
+ // Two-Factor Authentication
+
+ /**
+ * Setup 2FA for user
+ */
+ setup2FA(): Observable {
+ return this.http.post(
+ this.getApiUrl('/users/2fa/setup'),
+ {},
+ { headers: this.getHeaders() }
+ ).pipe(
+ catchError(this.handleError)
+ );
+ }
+
+ /**
+ * Verify 2FA setup
+ */
+ verify2FASetup(request: TwoFactorVerifyRequest): Observable {
+ return this.http.post(
+ this.getApiUrl('/users/2fa/verify'),
+ request,
+ { headers: this.getHeaders() }
+ ).pipe(
+ catchError(this.handleError)
+ );
+ }
+
+ /**
+ * Disable 2FA
+ */
+ disable2FA(): Observable {
+ return this.http.delete(
+ this.getApiUrl('/users/2fa'),
+ { headers: this.getHeaders() }
+ ).pipe(
+ catchError(this.handleError)
+ );
+ }
+
+ /**
+ * Check 2FA status
+ */
+ get2FAStatus(): Observable {
+ return this.http.get(
+ this.getApiUrl('/security/2fa/status'),
+ { headers: this.getHeaders() }
+ ).pipe(
+ catchError(this.handleError)
+ );
+ }
+
+ // Health Check
+
+ /**
+ * Check service health
+ */
+ healthCheck(): Observable {
+ return this.http.get(
+ `${this.baseUrl}/health`,
+ { headers: this.getHeaders(false) }
+ ).pipe(
+ catchError(this.handleError)
+ );
+ }
+}
\ No newline at end of file
diff --git a/projects/auth-client/src/lib/services/auth.service.ts b/projects/auth-client/src/lib/services/auth.service.ts
new file mode 100644
index 0000000..fff3e32
--- /dev/null
+++ b/projects/auth-client/src/lib/services/auth.service.ts
@@ -0,0 +1,408 @@
+import { Injectable } from '@angular/core';
+import { Observable, BehaviorSubject, throwError, timer, EMPTY } from 'rxjs';
+import { switchMap, tap, catchError, filter, take, shareReplay } from 'rxjs/operators';
+import { AuthHttpService } from './auth-http.service';
+import { TokenService } from './token.service';
+import {
+ LoginRequest,
+ LoginResponse,
+ RegisterRequest,
+ RegisterResponse,
+ User,
+ TokenPair,
+ LogoutRequest,
+ PasswordResetRequest,
+ PasswordResetConfirmRequest,
+ ChangePasswordRequest,
+ EmailVerificationRequest,
+ ResendVerificationRequest,
+ TwoFactorSetupResponse,
+ TwoFactorVerifyRequest,
+ TwoFactorStatusResponse,
+ ApiResponse,
+ ApiError
+} from '../models/auth.models';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class AuthService {
+ private currentUserSubject = new BehaviorSubject(null);
+ private isAuthenticatedSubject = new BehaviorSubject(false);
+ private isLoadingSubject = new BehaviorSubject(false);
+
+ public currentUser$ = this.currentUserSubject.asObservable();
+ public isAuthenticated$ = this.isAuthenticatedSubject.asObservable();
+ public isLoading$ = this.isLoadingSubject.asObservable();
+
+ private refreshTimer?: any;
+
+ constructor(
+ private authHttpService: AuthHttpService,
+ private tokenService: TokenService
+ ) {
+ this.initialize();
+ }
+
+ /**
+ * Initialize auth service
+ */
+ private initialize(): void {
+ // Initialize storage listener for cross-tab sync
+ this.tokenService.initStorageListener();
+
+ // Subscribe to token changes
+ this.tokenService.token$.subscribe(token => {
+ const isAuthenticated = !!token && this.tokenService.isTokenValid();
+ this.isAuthenticatedSubject.next(isAuthenticated);
+
+ if (isAuthenticated) {
+ this.loadCurrentUser();
+ this.scheduleTokenRefresh();
+ } else {
+ this.currentUserSubject.next(null);
+ this.clearRefreshTimer();
+ }
+ });
+
+ // Check initial authentication state
+ if (this.tokenService.isTokenValid()) {
+ this.isAuthenticatedSubject.next(true);
+ this.loadCurrentUser();
+ this.scheduleTokenRefresh();
+ }
+ }
+
+ /**
+ * Configure auth service with base URL
+ */
+ configure(baseUrl: string): void {
+ this.authHttpService.setBaseUrl(baseUrl);
+ }
+
+ // Authentication Methods
+
+ /**
+ * Register new user
+ */
+ register(request: RegisterRequest): Observable {
+ this.isLoadingSubject.next(true);
+
+ return this.authHttpService.register(request).pipe(
+ tap(response => {
+ this.tokenService.setTokens(response);
+ this.currentUserSubject.next(response.user);
+ this.isAuthenticatedSubject.next(true);
+ this.scheduleTokenRefresh();
+ }),
+ catchError(error => {
+ this.isLoadingSubject.next(false);
+ return throwError(() => error);
+ }),
+ tap(() => this.isLoadingSubject.next(false))
+ );
+ }
+
+ /**
+ * Login user
+ */
+ login(request: LoginRequest): Observable {
+ this.isLoadingSubject.next(true);
+
+ return this.authHttpService.login(request).pipe(
+ tap(response => {
+ this.tokenService.setTokens(response);
+ this.currentUserSubject.next(response.user);
+ this.isAuthenticatedSubject.next(true);
+ this.scheduleTokenRefresh();
+ }),
+ catchError(error => {
+ this.isLoadingSubject.next(false);
+ return throwError(() => error);
+ }),
+ tap(() => this.isLoadingSubject.next(false))
+ );
+ }
+
+ /**
+ * Logout user
+ */
+ logout(revokeRefreshToken = true): Observable {
+ this.isLoadingSubject.next(true);
+
+ const logoutRequest: LogoutRequest = revokeRefreshToken
+ ? { refresh_token: this.tokenService.getRefreshToken() || undefined }
+ : {};
+
+ return this.authHttpService.logout(logoutRequest).pipe(
+ tap(() => {
+ this.clearAuthState();
+ }),
+ catchError(error => {
+ // Even if logout fails on server, clear local state
+ this.clearAuthState();
+ this.isLoadingSubject.next(false);
+ return throwError(() => error);
+ }),
+ tap(() => this.isLoadingSubject.next(false))
+ );
+ }
+
+ /**
+ * Silently logout (clear local state only)
+ */
+ logoutSilently(): void {
+ this.clearAuthState();
+ }
+
+ /**
+ * Refresh access token
+ */
+ refreshToken(): Observable {
+ const refreshToken = this.tokenService.getRefreshToken();
+
+ if (!refreshToken) {
+ this.logoutSilently();
+ return throwError(() => ({ error: 'No refresh token available' } as ApiError));
+ }
+
+ return this.authHttpService.refreshToken({ refresh_token: refreshToken }).pipe(
+ tap(tokenPair => {
+ this.tokenService.setTokens(tokenPair);
+ this.scheduleTokenRefresh();
+ }),
+ catchError(error => {
+ // If refresh fails, logout user
+ this.logoutSilently();
+ return throwError(() => error);
+ })
+ );
+ }
+
+ /**
+ * Get current user information
+ */
+ getCurrentUser(): Observable {
+ if (this.currentUserSubject.value) {
+ return this.currentUserSubject.asObservable().pipe(
+ filter(user => !!user),
+ take(1)
+ );
+ }
+
+ return this.loadCurrentUser();
+ }
+
+ /**
+ * Load current user from server
+ */
+ private loadCurrentUser(): Observable {
+ return this.authHttpService.getCurrentUser().pipe(
+ tap(user => this.currentUserSubject.next(user)),
+ catchError(error => {
+ // If getting current user fails, might be invalid token
+ if (error.status === 401) {
+ this.logoutSilently();
+ }
+ return throwError(() => error);
+ }),
+ shareReplay(1)
+ );
+ }
+
+ // User Management
+
+ /**
+ * Update user profile
+ */
+ updateProfile(updates: Partial): Observable {
+ return this.authHttpService.updateUserProfile(updates).pipe(
+ tap(user => this.currentUserSubject.next(user))
+ );
+ }
+
+ /**
+ * Change password
+ */
+ changePassword(request: ChangePasswordRequest): Observable {
+ return this.authHttpService.changePassword(request);
+ }
+
+ /**
+ * Request password reset
+ */
+ forgotPassword(request: PasswordResetRequest): Observable {
+ return this.authHttpService.forgotPassword(request);
+ }
+
+ /**
+ * Reset password with token
+ */
+ resetPassword(request: PasswordResetConfirmRequest): Observable {
+ return this.authHttpService.resetPassword(request);
+ }
+
+ /**
+ * Verify email
+ */
+ verifyEmail(request: EmailVerificationRequest): Observable {
+ return this.authHttpService.verifyEmail(request);
+ }
+
+ /**
+ * Resend email verification
+ */
+ resendEmailVerification(request?: ResendVerificationRequest): Observable {
+ return this.authHttpService.resendEmailVerification(request);
+ }
+
+ // Two-Factor Authentication
+
+ /**
+ * Setup 2FA
+ */
+ setup2FA(): Observable {
+ return this.authHttpService.setup2FA();
+ }
+
+ /**
+ * Verify 2FA setup
+ */
+ verify2FASetup(request: TwoFactorVerifyRequest): Observable {
+ return this.authHttpService.verify2FASetup(request);
+ }
+
+ /**
+ * Disable 2FA
+ */
+ disable2FA(): Observable {
+ return this.authHttpService.disable2FA();
+ }
+
+ /**
+ * Get 2FA status
+ */
+ get2FAStatus(): Observable {
+ return this.authHttpService.get2FAStatus();
+ }
+
+ // Token Management
+
+ /**
+ * Check if user is authenticated
+ */
+ isAuthenticated(): boolean {
+ return this.isAuthenticatedSubject.value;
+ }
+
+ /**
+ * Get access token
+ */
+ getAccessToken(): string | null {
+ return this.tokenService.getAccessToken();
+ }
+
+ /**
+ * Check if user has specific scope
+ */
+ hasScope(scope: string): boolean {
+ return this.tokenService.hasScope(scope);
+ }
+
+ /**
+ * Check if user has any of the specified scopes
+ */
+ hasAnyScope(scopes: string[]): boolean {
+ return this.tokenService.hasAnyScope(scopes);
+ }
+
+ /**
+ * Check if user has all of the specified scopes
+ */
+ hasAllScopes(scopes: string[]): boolean {
+ return this.tokenService.hasAllScopes(scopes);
+ }
+
+ /**
+ * Get user scopes
+ */
+ getUserScopes(): string[] {
+ return this.tokenService.getUserScopes();
+ }
+
+ /**
+ * Get user ID from token
+ */
+ getUserId(): string | null {
+ return this.tokenService.getUserId();
+ }
+
+ /**
+ * Get user email from token
+ */
+ getUserEmail(): string | null {
+ return this.tokenService.getUserEmail();
+ }
+
+ /**
+ * Get user organization from token
+ */
+ getUserOrganization(): string | null {
+ return this.tokenService.getUserOrganization();
+ }
+
+ // Private Methods
+
+ /**
+ * Clear authentication state
+ */
+ private clearAuthState(): void {
+ this.tokenService.clearTokens();
+ this.currentUserSubject.next(null);
+ this.isAuthenticatedSubject.next(false);
+ this.clearRefreshTimer();
+ }
+
+ /**
+ * Schedule automatic token refresh
+ */
+ private scheduleTokenRefresh(): void {
+ this.clearRefreshTimer();
+
+ const timeUntilExpiry = this.tokenService.getTimeUntilExpiry();
+ if (timeUntilExpiry <= 0) return;
+
+ // Refresh token 2 minutes before expiry
+ const refreshTime = Math.max(1000, timeUntilExpiry - (2 * 60 * 1000));
+
+ this.refreshTimer = timer(refreshTime).pipe(
+ switchMap(() => {
+ if (this.tokenService.isTokenExpiringSoon()) {
+ return this.refreshToken();
+ }
+ return EMPTY;
+ }),
+ catchError(error => {
+ console.warn('Auto token refresh failed:', error);
+ return EMPTY;
+ })
+ ).subscribe();
+ }
+
+ /**
+ * Clear refresh timer
+ */
+ private clearRefreshTimer(): void {
+ if (this.refreshTimer) {
+ this.refreshTimer.unsubscribe();
+ this.refreshTimer = undefined;
+ }
+ }
+
+ /**
+ * Check service health
+ */
+ healthCheck(): Observable {
+ return this.authHttpService.healthCheck();
+ }
+}
\ No newline at end of file
diff --git a/projects/auth-client/src/lib/services/oauth.service.ts b/projects/auth-client/src/lib/services/oauth.service.ts
new file mode 100644
index 0000000..eebf6ad
--- /dev/null
+++ b/projects/auth-client/src/lib/services/oauth.service.ts
@@ -0,0 +1,290 @@
+import { Injectable } from '@angular/core';
+import { HttpClient } from '@angular/common/http';
+import { Observable, throwError } from 'rxjs';
+import { catchError, map } from 'rxjs/operators';
+import { TokenService } from './token.service';
+import {
+ OAuthProvider,
+ OAuthProvidersResponse,
+ OAuthLinkRequest,
+ ApiResponse,
+ ApiError
+} from '../models/auth.models';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class OAuthService {
+ private baseUrl = '';
+
+ constructor(
+ private http: HttpClient,
+ private tokenService: TokenService
+ ) {}
+
+ /**
+ * Set the base URL for the auth service
+ */
+ setBaseUrl(url: string): void {
+ this.baseUrl = url.replace(/\/$/, ''); // Remove trailing slash
+ }
+
+ /**
+ * Get API URL with version prefix
+ */
+ private getApiUrl(path: string): string {
+ return `${this.baseUrl}/api/v1${path}`;
+ }
+
+ /**
+ * Get HTTP headers with authorization if available
+ */
+ private getHeaders(includeAuth = true): { [header: string]: string } {
+ const headers: { [header: string]: string } = {
+ 'Content-Type': 'application/json'
+ };
+
+ if (includeAuth) {
+ const authHeader = this.tokenService.getAuthorizationHeader();
+ if (authHeader) {
+ headers['Authorization'] = authHeader;
+ }
+ }
+
+ return headers;
+ }
+
+ /**
+ * Handle HTTP errors and extract API error information
+ */
+ private handleError(error: any): Observable {
+ let apiError: ApiError;
+
+ if (error.error && typeof error.error === 'object') {
+ apiError = error.error;
+ } else {
+ apiError = {
+ error: error.message || 'An unknown error occurred'
+ };
+ }
+
+ return throwError(() => apiError);
+ }
+
+ /**
+ * Get available OAuth providers
+ */
+ getProviders(): Observable {
+ return this.http.get(
+ this.getApiUrl('/oauth/providers'),
+ { headers: this.getHeaders(false) }
+ ).pipe(
+ map(response => response.providers),
+ catchError(this.handleError)
+ );
+ }
+
+ /**
+ * Get OAuth authorization URL for a provider
+ */
+ getAuthorizationUrl(provider: string, redirectUri?: string, state?: string): Observable {
+ let url = this.getApiUrl(`/oauth/${provider}`);
+
+ const params = new URLSearchParams();
+ if (redirectUri) {
+ params.append('redirect_uri', redirectUri);
+ }
+ if (state) {
+ params.append('state', state);
+ }
+
+ if (params.toString()) {
+ url += `?${params.toString()}`;
+ }
+
+ return this.http.get(url, {
+ headers: this.getHeaders(false),
+ responseType: 'text'
+ }).pipe(
+ catchError(this.handleError)
+ );
+ }
+
+ /**
+ * Handle OAuth callback (exchange code for tokens)
+ */
+ handleCallback(provider: string, code: string, state?: string): Observable {
+ const params = new URLSearchParams();
+ params.append('code', code);
+ if (state) {
+ params.append('state', state);
+ }
+
+ const url = `${this.getApiUrl(`/oauth/${provider}/callback`)}?${params.toString()}`;
+
+ return this.http.get(url, {
+ headers: this.getHeaders(false)
+ }).pipe(
+ catchError(this.handleError)
+ );
+ }
+
+ /**
+ * Link OAuth provider to existing account (requires authentication)
+ */
+ linkProvider(provider: string, code: string, state?: string): Observable {
+ const request: OAuthLinkRequest = {
+ provider,
+ code,
+ state
+ };
+
+ return this.http.post(
+ this.getApiUrl(`/oauth/${provider}/link`),
+ request,
+ { headers: this.getHeaders() }
+ ).pipe(
+ catchError(this.handleError)
+ );
+ }
+
+ /**
+ * Unlink OAuth provider from account (requires authentication)
+ */
+ unlinkProvider(provider: string): Observable {
+ return this.http.delete(
+ this.getApiUrl(`/oauth/${provider}`),
+ { headers: this.getHeaders() }
+ ).pipe(
+ catchError(this.handleError)
+ );
+ }
+
+ /**
+ * Initiate OAuth flow by redirecting to provider
+ */
+ initiateOAuthFlow(provider: string, redirectUri?: string, state?: string): void {
+ this.getAuthorizationUrl(provider, redirectUri, state).subscribe({
+ next: (authUrl) => {
+ window.location.href = authUrl;
+ },
+ error: (error) => {
+ console.error('Failed to initiate OAuth flow:', error);
+ throw error;
+ }
+ });
+ }
+
+ /**
+ * Open OAuth flow in popup window
+ */
+ initiateOAuthPopup(
+ provider: string,
+ redirectUri?: string,
+ state?: string,
+ popupFeatures = 'width=500,height=600,scrollbars=yes,resizable=yes'
+ ): Promise {
+ return new Promise((resolve, reject) => {
+ this.getAuthorizationUrl(provider, redirectUri, state).subscribe({
+ next: (authUrl) => {
+ const popup = window.open(authUrl, 'oauth', popupFeatures);
+
+ if (!popup) {
+ reject(new Error('Failed to open popup window'));
+ return;
+ }
+
+ // Poll for popup closure or message
+ const checkClosed = setInterval(() => {
+ if (popup.closed) {
+ clearInterval(checkClosed);
+ reject(new Error('OAuth popup was closed by user'));
+ }
+ }, 1000);
+
+ // Listen for messages from popup
+ const messageListener = (event: MessageEvent) => {
+ if (event.origin !== window.location.origin) {
+ return;
+ }
+
+ if (event.data.type === 'OAUTH_SUCCESS') {
+ clearInterval(checkClosed);
+ window.removeEventListener('message', messageListener);
+ popup.close();
+ resolve(event.data.payload);
+ } else if (event.data.type === 'OAUTH_ERROR') {
+ clearInterval(checkClosed);
+ window.removeEventListener('message', messageListener);
+ popup.close();
+ reject(new Error(event.data.error));
+ }
+ };
+
+ window.addEventListener('message', messageListener);
+ },
+ error: (error) => {
+ reject(error);
+ }
+ });
+ });
+ }
+
+ /**
+ * Generate state parameter for OAuth security
+ */
+ generateState(): string {
+ const array = new Uint8Array(32);
+ crypto.getRandomValues(array);
+ return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
+ }
+
+ /**
+ * Store state in session storage for verification
+ */
+ storeState(state: string): void {
+ sessionStorage.setItem('oauth_state', state);
+ }
+
+ /**
+ * Verify state parameter
+ */
+ verifyState(state: string): boolean {
+ const storedState = sessionStorage.getItem('oauth_state');
+ sessionStorage.removeItem('oauth_state');
+ return storedState === state;
+ }
+
+ /**
+ * Extract code and state from URL params (for redirect handling)
+ */
+ extractCallbackParams(): { code?: string; state?: string; error?: string } {
+ const urlParams = new URLSearchParams(window.location.search);
+ return {
+ code: urlParams.get('code') || undefined,
+ state: urlParams.get('state') || undefined,
+ error: urlParams.get('error') || undefined
+ };
+ }
+
+ /**
+ * Complete OAuth flow with extracted parameters
+ */
+ completeOAuthFlow(provider: string): Observable {
+ const params = this.extractCallbackParams();
+
+ if (params.error) {
+ return throwError(() => ({ error: params.error } as ApiError));
+ }
+
+ if (!params.code) {
+ return throwError(() => ({ error: 'No authorization code received' } as ApiError));
+ }
+
+ if (params.state && !this.verifyState(params.state)) {
+ return throwError(() => ({ error: 'Invalid state parameter' } as ApiError));
+ }
+
+ return this.handleCallback(provider, params.code, params.state);
+ }
+}
\ No newline at end of file
diff --git a/projects/auth-client/src/lib/services/token.service.ts b/projects/auth-client/src/lib/services/token.service.ts
new file mode 100644
index 0000000..08286d7
--- /dev/null
+++ b/projects/auth-client/src/lib/services/token.service.ts
@@ -0,0 +1,226 @@
+import { Injectable } from '@angular/core';
+import { BehaviorSubject, Observable } from 'rxjs';
+import { TokenPair } from '../models/auth.models';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class TokenService {
+ private readonly ACCESS_TOKEN_KEY = 'auth_access_token';
+ private readonly REFRESH_TOKEN_KEY = 'auth_refresh_token';
+ private readonly TOKEN_TYPE_KEY = 'auth_token_type';
+ private readonly EXPIRES_AT_KEY = 'auth_expires_at';
+
+ private tokenSubject = new BehaviorSubject(this.getAccessToken());
+ public token$ = this.tokenSubject.asObservable();
+
+ constructor() {
+ // Check token validity on service initialization
+ this.checkTokenValidity();
+ }
+
+ /**
+ * Store token pair in localStorage
+ */
+ setTokens(tokenPair: TokenPair): void {
+ const expiresAt = Date.now() + (tokenPair.expires_in * 1000);
+
+ localStorage.setItem(this.ACCESS_TOKEN_KEY, tokenPair.access_token);
+ localStorage.setItem(this.REFRESH_TOKEN_KEY, tokenPair.refresh_token);
+ localStorage.setItem(this.TOKEN_TYPE_KEY, tokenPair.token_type);
+ localStorage.setItem(this.EXPIRES_AT_KEY, expiresAt.toString());
+
+ this.tokenSubject.next(tokenPair.access_token);
+ }
+
+ /**
+ * Get access token from localStorage
+ */
+ getAccessToken(): string | null {
+ return localStorage.getItem(this.ACCESS_TOKEN_KEY);
+ }
+
+ /**
+ * Get refresh token from localStorage
+ */
+ getRefreshToken(): string | null {
+ return localStorage.getItem(this.REFRESH_TOKEN_KEY);
+ }
+
+ /**
+ * Get token type from localStorage
+ */
+ getTokenType(): string {
+ return localStorage.getItem(this.TOKEN_TYPE_KEY) || 'Bearer';
+ }
+
+ /**
+ * Get token expiration timestamp
+ */
+ getExpiresAt(): number | null {
+ const expiresAt = localStorage.getItem(this.EXPIRES_AT_KEY);
+ return expiresAt ? parseInt(expiresAt, 10) : null;
+ }
+
+ /**
+ * Check if token exists
+ */
+ hasToken(): boolean {
+ return this.getAccessToken() !== null;
+ }
+
+ /**
+ * Check if token is expired
+ */
+ isTokenExpired(): boolean {
+ const expiresAt = this.getExpiresAt();
+ if (!expiresAt) return true;
+
+ // Add 30 second buffer to account for clock skew
+ return Date.now() >= (expiresAt - 30000);
+ }
+
+ /**
+ * Check if token is valid (exists and not expired)
+ */
+ isTokenValid(): boolean {
+ return this.hasToken() && !this.isTokenExpired();
+ }
+
+ /**
+ * Get authorization header value
+ */
+ getAuthorizationHeader(): string | null {
+ const token = this.getAccessToken();
+ const tokenType = this.getTokenType();
+
+ return token ? `${tokenType} ${token}` : null;
+ }
+
+ /**
+ * Clear all tokens from storage
+ */
+ clearTokens(): void {
+ localStorage.removeItem(this.ACCESS_TOKEN_KEY);
+ localStorage.removeItem(this.REFRESH_TOKEN_KEY);
+ localStorage.removeItem(this.TOKEN_TYPE_KEY);
+ localStorage.removeItem(this.EXPIRES_AT_KEY);
+
+ this.tokenSubject.next(null);
+ }
+
+ /**
+ * Get time until token expires (in milliseconds)
+ */
+ getTimeUntilExpiry(): number {
+ const expiresAt = this.getExpiresAt();
+ if (!expiresAt) return 0;
+
+ const timeLeft = expiresAt - Date.now();
+ return Math.max(0, timeLeft);
+ }
+
+ /**
+ * Check if token will expire soon (within 5 minutes)
+ */
+ isTokenExpiringSoon(): boolean {
+ const timeLeft = this.getTimeUntilExpiry();
+ return timeLeft > 0 && timeLeft < 5 * 60 * 1000; // 5 minutes
+ }
+
+ /**
+ * Decode JWT token payload (without verification)
+ */
+ decodeToken(token?: string): any {
+ const tokenToDecoded = token || this.getAccessToken();
+ if (!tokenToDecoded) return null;
+
+ try {
+ const payload = tokenToDecoded.split('.')[1];
+ const decoded = atob(payload);
+ return JSON.parse(decoded);
+ } catch (error) {
+ console.warn('Failed to decode token:', error);
+ return null;
+ }
+ }
+
+ /**
+ * Get user ID from token
+ */
+ getUserId(): string | null {
+ const payload = this.decodeToken();
+ return payload?.sub || null;
+ }
+
+ /**
+ * Get user email from token
+ */
+ getUserEmail(): string | null {
+ const payload = this.decodeToken();
+ return payload?.email || null;
+ }
+
+ /**
+ * Get user scopes from token
+ */
+ getUserScopes(): string[] {
+ const payload = this.decodeToken();
+ return payload?.scopes || [];
+ }
+
+ /**
+ * Get user organization from token
+ */
+ getUserOrganization(): string | null {
+ const payload = this.decodeToken();
+ return payload?.organization || null;
+ }
+
+ /**
+ * Check if user has specific scope
+ */
+ hasScope(scope: string): boolean {
+ const scopes = this.getUserScopes();
+ return scopes.includes(scope);
+ }
+
+ /**
+ * Check if user has any of the specified scopes
+ */
+ hasAnyScope(scopes: string[]): boolean {
+ const userScopes = this.getUserScopes();
+ return scopes.some(scope => userScopes.includes(scope));
+ }
+
+ /**
+ * Check if user has all of the specified scopes
+ */
+ hasAllScopes(scopes: string[]): boolean {
+ const userScopes = this.getUserScopes();
+ return scopes.every(scope => userScopes.includes(scope));
+ }
+
+ /**
+ * Check token validity and clear if invalid
+ */
+ private checkTokenValidity(): void {
+ if (this.hasToken() && this.isTokenExpired()) {
+ this.clearTokens();
+ }
+ }
+
+ /**
+ * Subscribe to storage events for cross-tab synchronization
+ */
+ initStorageListener(): void {
+ window.addEventListener('storage', (event) => {
+ if (event.key === this.ACCESS_TOKEN_KEY) {
+ this.tokenSubject.next(event.newValue);
+ } else if (event.key === null) {
+ // Storage was cleared
+ this.tokenSubject.next(null);
+ }
+ });
+ }
+}
\ No newline at end of file
diff --git a/projects/auth-client/src/public-api.ts b/projects/auth-client/src/public-api.ts
new file mode 100644
index 0000000..3818536
--- /dev/null
+++ b/projects/auth-client/src/public-api.ts
@@ -0,0 +1,22 @@
+/*
+ * Public API Surface of auth-client
+ */
+
+// Models
+export * from './lib/models/auth.models';
+
+// Services
+export * from './lib/services/auth.service';
+export * from './lib/services/auth-http.service';
+export * from './lib/services/token.service';
+export * from './lib/services/oauth.service';
+
+// Guards
+export * from './lib/guards/auth.guard';
+export * from './lib/guards/guest.guard';
+
+// Interceptors
+export * from './lib/interceptors/auth.interceptor';
+
+// Legacy export for compatibility
+export * from './lib/auth-client';
diff --git a/projects/auth-client/tsconfig.lib.json b/projects/auth-client/tsconfig.lib.json
new file mode 100644
index 0000000..edb0551
--- /dev/null
+++ b/projects/auth-client/tsconfig.lib.json
@@ -0,0 +1,18 @@
+/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
+/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../out-tsc/lib",
+ "declaration": true,
+ "declarationMap": true,
+ "inlineSources": true,
+ "types": []
+ },
+ "include": [
+ "src/**/*.ts"
+ ],
+ "exclude": [
+ "**/*.spec.ts"
+ ]
+}
diff --git a/projects/auth-client/tsconfig.lib.prod.json b/projects/auth-client/tsconfig.lib.prod.json
new file mode 100644
index 0000000..9215caa
--- /dev/null
+++ b/projects/auth-client/tsconfig.lib.prod.json
@@ -0,0 +1,11 @@
+/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
+/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
+{
+ "extends": "./tsconfig.lib.json",
+ "compilerOptions": {
+ "declarationMap": false
+ },
+ "angularCompilerOptions": {
+ "compilationMode": "partial"
+ }
+}
diff --git a/projects/auth-client/tsconfig.spec.json b/projects/auth-client/tsconfig.spec.json
new file mode 100644
index 0000000..0feea88
--- /dev/null
+++ b/projects/auth-client/tsconfig.spec.json
@@ -0,0 +1,14 @@
+/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
+/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../out-tsc/spec",
+ "types": [
+ "jasmine"
+ ]
+ },
+ "include": [
+ "src/**/*.ts"
+ ]
+}
diff --git a/tsconfig.json b/tsconfig.json
index 5ff9191..310aa8e 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -15,6 +15,9 @@
"experimentalDecorators": true,
"moduleResolution": "bundler",
"paths": {
+ "auth-client": [
+ "./dist/auth-client"
+ ],
"hcl-studio": [
"./dist/hcl-studio"
],
@@ -58,5 +61,13 @@
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
- }
+ },
+ "references": [
+ {
+ "path": "./projects/auth-client/tsconfig.lib.json"
+ },
+ {
+ "path": "./projects/auth-client/tsconfig.spec.json"
+ }
+ ]
}