- 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>
408 lines
9.8 KiB
TypeScript
408 lines
9.8 KiB
TypeScript
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();
|
|
}
|
|
} |