Add auth-client library for Elixir auth service integration
- 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>
This commit is contained in:
10
.claude/settings.local.json
Normal file
10
.claude/settings.local.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(ng generate:*)",
|
||||||
|
"Bash(git add:*)"
|
||||||
|
],
|
||||||
|
"deny": [],
|
||||||
|
"ask": []
|
||||||
|
}
|
||||||
|
}
|
||||||
30
angular.json
30
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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-devkit/build-angular": "^19.2.15",
|
"@angular-devkit/build-angular": "^19.2.15",
|
||||||
|
"@angular/build": "^20.2.0",
|
||||||
"@angular/cli": "^19.2.15",
|
"@angular/cli": "^19.2.15",
|
||||||
"@angular/compiler-cli": "^19.2.0",
|
"@angular/compiler-cli": "^19.2.0",
|
||||||
"@types/jasmine": "~5.1.0",
|
"@types/jasmine": "~5.1.0",
|
||||||
@@ -42,4 +43,4 @@
|
|||||||
"ng-packagr": "^19.2.0",
|
"ng-packagr": "^19.2.0",
|
||||||
"typescript": "~5.7.2"
|
"typescript": "~5.7.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
468
projects/auth-client/README.md
Normal file
468
projects/auth-client/README.md
Normal file
@@ -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
|
||||||
1076
projects/auth-client/USAGE_GUIDE.md
Normal file
1076
projects/auth-client/USAGE_GUIDE.md
Normal file
File diff suppressed because it is too large
Load Diff
7
projects/auth-client/ng-package.json
Normal file
7
projects/auth-client/ng-package.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
|
||||||
|
"dest": "../../dist/auth-client",
|
||||||
|
"lib": {
|
||||||
|
"entryFile": "src/public-api.ts"
|
||||||
|
}
|
||||||
|
}
|
||||||
12
projects/auth-client/package.json
Normal file
12
projects/auth-client/package.json
Normal file
@@ -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
|
||||||
|
}
|
||||||
23
projects/auth-client/src/lib/auth-client.spec.ts
Normal file
23
projects/auth-client/src/lib/auth-client.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { AuthClient } from './auth-client';
|
||||||
|
|
||||||
|
describe('AuthClient', () => {
|
||||||
|
let component: AuthClient;
|
||||||
|
let fixture: ComponentFixture<AuthClient>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [AuthClient]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(AuthClient);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
15
projects/auth-client/src/lib/auth-client.ts
Normal file
15
projects/auth-client/src/lib/auth-client.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'lib-auth-client',
|
||||||
|
imports: [],
|
||||||
|
template: `
|
||||||
|
<p>
|
||||||
|
auth-client works!
|
||||||
|
</p>
|
||||||
|
`,
|
||||||
|
styles: ``
|
||||||
|
})
|
||||||
|
export class AuthClient {
|
||||||
|
|
||||||
|
}
|
||||||
67
projects/auth-client/src/lib/guards/auth.guard.ts
Normal file
67
projects/auth-client/src/lib/guards/auth.guard.ts
Normal file
@@ -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<boolean> {
|
||||||
|
return this.checkAuth(route, state.url);
|
||||||
|
}
|
||||||
|
|
||||||
|
canActivateChild(
|
||||||
|
childRoute: ActivatedRouteSnapshot,
|
||||||
|
state: RouterStateSnapshot
|
||||||
|
): Observable<boolean> {
|
||||||
|
return this.canActivate(childRoute, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
private checkAuth(route: ActivatedRouteSnapshot, redirectUrl: string): Observable<boolean> {
|
||||||
|
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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
33
projects/auth-client/src/lib/guards/guest.guard.ts
Normal file
33
projects/auth-client/src/lib/guards/guest.guard.ts
Normal file
@@ -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<boolean> {
|
||||||
|
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;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
118
projects/auth-client/src/lib/interceptors/auth.interceptor.ts
Normal file
118
projects/auth-client/src/lib/interceptors/auth.interceptor.ts
Normal file
@@ -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<any> = new BehaviorSubject<any>(null);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private tokenService: TokenService,
|
||||||
|
private authHttpService: AuthHttpService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||||
|
// 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<any>): 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<any>): HttpRequest<any> {
|
||||||
|
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<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||||
|
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)))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
257
projects/auth-client/src/lib/models/auth.models.ts
Normal file
257
projects/auth-client/src/lib/models/auth.models.ts
Normal file
@@ -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<string, any>;
|
||||||
|
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<string, string[]>;
|
||||||
|
requires_2fa?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiResponse<T = any> {
|
||||||
|
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<string, any>;
|
||||||
|
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<string, any>;
|
||||||
|
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;
|
||||||
|
}
|
||||||
331
projects/auth-client/src/lib/services/auth-http.service.ts
Normal file
331
projects/auth-client/src/lib/services/auth-http.service.ts
Normal file
@@ -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<never> {
|
||||||
|
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<RegisterResponse> {
|
||||||
|
return this.http.post<RegisterResponse>(
|
||||||
|
this.getApiUrl('/auth/register'),
|
||||||
|
request,
|
||||||
|
{ headers: this.getHeaders(false) }
|
||||||
|
).pipe(
|
||||||
|
catchError(this.handleError)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Login with email and password
|
||||||
|
*/
|
||||||
|
login(request: LoginRequest): Observable<LoginResponse> {
|
||||||
|
return this.http.post<LoginResponse>(
|
||||||
|
this.getApiUrl('/auth/login'),
|
||||||
|
request,
|
||||||
|
{ headers: this.getHeaders(false) }
|
||||||
|
).pipe(
|
||||||
|
catchError(this.handleError)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh access token
|
||||||
|
*/
|
||||||
|
refreshToken(request: RefreshTokenRequest): Observable<TokenPair> {
|
||||||
|
return this.http.post<TokenPair>(
|
||||||
|
this.getApiUrl('/auth/refresh'),
|
||||||
|
request,
|
||||||
|
{ headers: this.getHeaders(false) }
|
||||||
|
).pipe(
|
||||||
|
catchError(this.handleError)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate token (for API Gateway)
|
||||||
|
*/
|
||||||
|
validateToken(request: TokenValidationRequest): Observable<TokenValidationResponse> {
|
||||||
|
return this.http.post<TokenValidationResponse>(
|
||||||
|
this.getApiUrl('/auth/validate'),
|
||||||
|
request,
|
||||||
|
{ headers: this.getHeaders(false) }
|
||||||
|
).pipe(
|
||||||
|
catchError(this.handleError)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logout user
|
||||||
|
*/
|
||||||
|
logout(request?: LogoutRequest): Observable<ApiResponse> {
|
||||||
|
return this.http.post<ApiResponse>(
|
||||||
|
this.getApiUrl('/auth/logout'),
|
||||||
|
request || {},
|
||||||
|
{ headers: this.getHeaders() }
|
||||||
|
).pipe(
|
||||||
|
catchError(this.handleError)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current user information
|
||||||
|
*/
|
||||||
|
getCurrentUser(): Observable<User> {
|
||||||
|
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<User> {
|
||||||
|
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<User>): Observable<User> {
|
||||||
|
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<ApiResponse> {
|
||||||
|
return this.http.post<ApiResponse>(
|
||||||
|
this.getApiUrl('/users/change-password'),
|
||||||
|
request,
|
||||||
|
{ headers: this.getHeaders() }
|
||||||
|
).pipe(
|
||||||
|
catchError(this.handleError)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request password reset
|
||||||
|
*/
|
||||||
|
forgotPassword(request: PasswordResetRequest): Observable<ApiResponse> {
|
||||||
|
return this.http.post<ApiResponse>(
|
||||||
|
this.getApiUrl('/users/forgot-password'),
|
||||||
|
request,
|
||||||
|
{ headers: this.getHeaders(false) }
|
||||||
|
).pipe(
|
||||||
|
catchError(this.handleError)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset password with token
|
||||||
|
*/
|
||||||
|
resetPassword(request: PasswordResetConfirmRequest): Observable<ApiResponse> {
|
||||||
|
return this.http.post<ApiResponse>(
|
||||||
|
this.getApiUrl('/users/reset-password'),
|
||||||
|
request,
|
||||||
|
{ headers: this.getHeaders(false) }
|
||||||
|
).pipe(
|
||||||
|
catchError(this.handleError)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify email with token
|
||||||
|
*/
|
||||||
|
verifyEmail(request: EmailVerificationRequest): Observable<ApiResponse> {
|
||||||
|
return this.http.post<ApiResponse>(
|
||||||
|
this.getApiUrl('/users/verify-email'),
|
||||||
|
request,
|
||||||
|
{ headers: this.getHeaders(false) }
|
||||||
|
).pipe(
|
||||||
|
catchError(this.handleError)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resend email verification
|
||||||
|
*/
|
||||||
|
resendEmailVerification(request?: ResendVerificationRequest): Observable<ApiResponse> {
|
||||||
|
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<ApiResponse>(url, body, { headers }).pipe(
|
||||||
|
catchError(this.handleError)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Two-Factor Authentication
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup 2FA for user
|
||||||
|
*/
|
||||||
|
setup2FA(): Observable<TwoFactorSetupResponse> {
|
||||||
|
return this.http.post<TwoFactorSetupResponse>(
|
||||||
|
this.getApiUrl('/users/2fa/setup'),
|
||||||
|
{},
|
||||||
|
{ headers: this.getHeaders() }
|
||||||
|
).pipe(
|
||||||
|
catchError(this.handleError)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify 2FA setup
|
||||||
|
*/
|
||||||
|
verify2FASetup(request: TwoFactorVerifyRequest): Observable<ApiResponse> {
|
||||||
|
return this.http.post<ApiResponse>(
|
||||||
|
this.getApiUrl('/users/2fa/verify'),
|
||||||
|
request,
|
||||||
|
{ headers: this.getHeaders() }
|
||||||
|
).pipe(
|
||||||
|
catchError(this.handleError)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable 2FA
|
||||||
|
*/
|
||||||
|
disable2FA(): Observable<ApiResponse> {
|
||||||
|
return this.http.delete<ApiResponse>(
|
||||||
|
this.getApiUrl('/users/2fa'),
|
||||||
|
{ headers: this.getHeaders() }
|
||||||
|
).pipe(
|
||||||
|
catchError(this.handleError)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check 2FA status
|
||||||
|
*/
|
||||||
|
get2FAStatus(): Observable<TwoFactorStatusResponse> {
|
||||||
|
return this.http.get<TwoFactorStatusResponse>(
|
||||||
|
this.getApiUrl('/security/2fa/status'),
|
||||||
|
{ headers: this.getHeaders() }
|
||||||
|
).pipe(
|
||||||
|
catchError(this.handleError)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Health Check
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check service health
|
||||||
|
*/
|
||||||
|
healthCheck(): Observable<any> {
|
||||||
|
return this.http.get(
|
||||||
|
`${this.baseUrl}/health`,
|
||||||
|
{ headers: this.getHeaders(false) }
|
||||||
|
).pipe(
|
||||||
|
catchError(this.handleError)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
408
projects/auth-client/src/lib/services/auth.service.ts
Normal file
408
projects/auth-client/src/lib/services/auth.service.ts
Normal file
@@ -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<User | null>(null);
|
||||||
|
private isAuthenticatedSubject = new BehaviorSubject<boolean>(false);
|
||||||
|
private isLoadingSubject = new BehaviorSubject<boolean>(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<RegisterResponse> {
|
||||||
|
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<LoginResponse> {
|
||||||
|
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<ApiResponse> {
|
||||||
|
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<TokenPair> {
|
||||||
|
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<User> {
|
||||||
|
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<User> {
|
||||||
|
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<User>): Observable<User> {
|
||||||
|
return this.authHttpService.updateUserProfile(updates).pipe(
|
||||||
|
tap(user => this.currentUserSubject.next(user))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change password
|
||||||
|
*/
|
||||||
|
changePassword(request: ChangePasswordRequest): Observable<ApiResponse> {
|
||||||
|
return this.authHttpService.changePassword(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request password reset
|
||||||
|
*/
|
||||||
|
forgotPassword(request: PasswordResetRequest): Observable<ApiResponse> {
|
||||||
|
return this.authHttpService.forgotPassword(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset password with token
|
||||||
|
*/
|
||||||
|
resetPassword(request: PasswordResetConfirmRequest): Observable<ApiResponse> {
|
||||||
|
return this.authHttpService.resetPassword(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify email
|
||||||
|
*/
|
||||||
|
verifyEmail(request: EmailVerificationRequest): Observable<ApiResponse> {
|
||||||
|
return this.authHttpService.verifyEmail(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resend email verification
|
||||||
|
*/
|
||||||
|
resendEmailVerification(request?: ResendVerificationRequest): Observable<ApiResponse> {
|
||||||
|
return this.authHttpService.resendEmailVerification(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Two-Factor Authentication
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup 2FA
|
||||||
|
*/
|
||||||
|
setup2FA(): Observable<TwoFactorSetupResponse> {
|
||||||
|
return this.authHttpService.setup2FA();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify 2FA setup
|
||||||
|
*/
|
||||||
|
verify2FASetup(request: TwoFactorVerifyRequest): Observable<ApiResponse> {
|
||||||
|
return this.authHttpService.verify2FASetup(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable 2FA
|
||||||
|
*/
|
||||||
|
disable2FA(): Observable<ApiResponse> {
|
||||||
|
return this.authHttpService.disable2FA();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get 2FA status
|
||||||
|
*/
|
||||||
|
get2FAStatus(): Observable<TwoFactorStatusResponse> {
|
||||||
|
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<any> {
|
||||||
|
return this.authHttpService.healthCheck();
|
||||||
|
}
|
||||||
|
}
|
||||||
290
projects/auth-client/src/lib/services/oauth.service.ts
Normal file
290
projects/auth-client/src/lib/services/oauth.service.ts
Normal file
@@ -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<never> {
|
||||||
|
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<OAuthProvider[]> {
|
||||||
|
return this.http.get<OAuthProvidersResponse>(
|
||||||
|
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<string> {
|
||||||
|
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<any> {
|
||||||
|
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<ApiResponse> {
|
||||||
|
const request: OAuthLinkRequest = {
|
||||||
|
provider,
|
||||||
|
code,
|
||||||
|
state
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.http.post<ApiResponse>(
|
||||||
|
this.getApiUrl(`/oauth/${provider}/link`),
|
||||||
|
request,
|
||||||
|
{ headers: this.getHeaders() }
|
||||||
|
).pipe(
|
||||||
|
catchError(this.handleError)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unlink OAuth provider from account (requires authentication)
|
||||||
|
*/
|
||||||
|
unlinkProvider(provider: string): Observable<ApiResponse> {
|
||||||
|
return this.http.delete<ApiResponse>(
|
||||||
|
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<any> {
|
||||||
|
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<any> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
226
projects/auth-client/src/lib/services/token.service.ts
Normal file
226
projects/auth-client/src/lib/services/token.service.ts
Normal file
@@ -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<string | null>(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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
22
projects/auth-client/src/public-api.ts
Normal file
22
projects/auth-client/src/public-api.ts
Normal file
@@ -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';
|
||||||
18
projects/auth-client/tsconfig.lib.json
Normal file
18
projects/auth-client/tsconfig.lib.json
Normal file
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
11
projects/auth-client/tsconfig.lib.prod.json
Normal file
11
projects/auth-client/tsconfig.lib.prod.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
14
projects/auth-client/tsconfig.spec.json
Normal file
14
projects/auth-client/tsconfig.spec.json
Normal file
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -15,6 +15,9 @@
|
|||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"paths": {
|
"paths": {
|
||||||
|
"auth-client": [
|
||||||
|
"./dist/auth-client"
|
||||||
|
],
|
||||||
"hcl-studio": [
|
"hcl-studio": [
|
||||||
"./dist/hcl-studio"
|
"./dist/hcl-studio"
|
||||||
],
|
],
|
||||||
@@ -58,5 +61,13 @@
|
|||||||
"strictInjectionParameters": true,
|
"strictInjectionParameters": true,
|
||||||
"strictInputAccessModifiers": true,
|
"strictInputAccessModifiers": true,
|
||||||
"strictTemplates": true
|
"strictTemplates": true
|
||||||
}
|
},
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./projects/auth-client/tsconfig.lib.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./projects/auth-client/tsconfig.spec.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user