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:
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user