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