Initial
This commit is contained in:
102
listify-client/src/app/auth/auth.service.ts
Normal file
102
listify-client/src/app/auth/auth.service.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { Injectable, computed, inject, signal } from '@angular/core';
|
||||
import { Observable, finalize, shareReplay, tap, throwError } from 'rxjs';
|
||||
import {
|
||||
AuthTokenResponse,
|
||||
LoginRequest,
|
||||
PublicUser,
|
||||
RegisterRequest,
|
||||
RegisterResponse,
|
||||
VerifyEmailResponse,
|
||||
} from './auth.models';
|
||||
|
||||
const ACCESS_TOKEN_KEY = 'listify.accessToken';
|
||||
const REFRESH_TOKEN_KEY = 'listify.refreshToken';
|
||||
const USER_KEY = 'listify.user';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AuthService {
|
||||
private readonly http = inject(HttpClient);
|
||||
private readonly apiUrl = '/api/auth';
|
||||
private readonly userSignal = signal<PublicUser | null>(this.readStoredUser());
|
||||
private refreshRequest$: Observable<AuthTokenResponse> | null = null;
|
||||
|
||||
readonly user = this.userSignal.asReadonly();
|
||||
readonly isAuthenticated = computed(() => Boolean(this.userSignal()));
|
||||
|
||||
login(credentials: LoginRequest): Observable<AuthTokenResponse> {
|
||||
return this.http
|
||||
.post<AuthTokenResponse>(`${this.apiUrl}/login`, credentials)
|
||||
.pipe(tap((response) => this.storeSession(response)));
|
||||
}
|
||||
|
||||
register(data: RegisterRequest): Observable<RegisterResponse> {
|
||||
return this.http.post<RegisterResponse>(`${this.apiUrl}/register`, data);
|
||||
}
|
||||
|
||||
verifyEmail(token: string): Observable<VerifyEmailResponse> {
|
||||
const params = new HttpParams().set('token', token);
|
||||
return this.http.get<VerifyEmailResponse>(`${this.apiUrl}/verify-email`, { params });
|
||||
}
|
||||
|
||||
accessToken(): string | null {
|
||||
return this.storage?.getItem(ACCESS_TOKEN_KEY) ?? null;
|
||||
}
|
||||
|
||||
refreshToken(): string | null {
|
||||
return this.storage?.getItem(REFRESH_TOKEN_KEY) ?? null;
|
||||
}
|
||||
|
||||
refreshSession(): Observable<AuthTokenResponse> {
|
||||
const refreshToken = this.refreshToken();
|
||||
|
||||
if (!refreshToken) {
|
||||
return throwError(() => new Error('Refresh token is missing.'));
|
||||
}
|
||||
|
||||
this.refreshRequest$ ??= this.http
|
||||
.post<AuthTokenResponse>(`${this.apiUrl}/refresh`, { refreshToken })
|
||||
.pipe(
|
||||
tap((response) => this.storeSession(response)),
|
||||
finalize(() => {
|
||||
this.refreshRequest$ = null;
|
||||
}),
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
);
|
||||
|
||||
return this.refreshRequest$;
|
||||
}
|
||||
|
||||
logout(): void {
|
||||
this.storage?.removeItem(ACCESS_TOKEN_KEY);
|
||||
this.storage?.removeItem(REFRESH_TOKEN_KEY);
|
||||
this.storage?.removeItem(USER_KEY);
|
||||
this.userSignal.set(null);
|
||||
}
|
||||
|
||||
private storeSession(response: AuthTokenResponse): void {
|
||||
this.storage?.setItem(ACCESS_TOKEN_KEY, response.accessToken);
|
||||
this.storage?.setItem(REFRESH_TOKEN_KEY, response.refreshToken);
|
||||
this.storage?.setItem(USER_KEY, JSON.stringify(response.user));
|
||||
this.userSignal.set(response.user);
|
||||
}
|
||||
|
||||
private readStoredUser(): PublicUser | null {
|
||||
const rawUser = this.storage?.getItem(USER_KEY);
|
||||
|
||||
if (!rawUser) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(rawUser) as PublicUser;
|
||||
} catch {
|
||||
this.storage?.removeItem(USER_KEY);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private get storage(): Storage | null {
|
||||
return typeof window === 'undefined' ? null : window.localStorage;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user