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:
Giuliano Silvestro
2025-09-11 14:56:59 +10:00
parent 246c62fd49
commit 9b40aa3afb
22 changed files with 3450 additions and 2 deletions

View File

@@ -0,0 +1,10 @@
{
"permissions": {
"allow": [
"Bash(ng generate:*)",
"Bash(git add:*)"
],
"deny": [],
"ask": []
}
}

View File

@@ -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"
]
}
}
}
}
}
}

View File

@@ -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",

View 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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,7 @@
{
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
"dest": "../../dist/auth-client",
"lib": {
"entryFile": "src/public-api.ts"
}
}

View 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
}

View 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();
});
});

View 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 {
}

View 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]);
}
}

View 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;
})
);
}
}

View 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)))
);
}
}
}

View 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;
}

View 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)
);
}
}

View 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();
}
}

View 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);
}
}

View 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);
}
});
}
}

View 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';

View 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"
]
}

View 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"
}
}

View 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"
]
}

View File

@@ -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"
}
]
}