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": {
|
||||
"@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",
|
||||
|
||||
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,
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user