From b5e0f5282228e1f71e28fa0e27f833ee92ba92ea Mon Sep 17 00:00:00 2001 From: Chneemann Date: Tue, 22 Apr 2025 07:34:28 +0200 Subject: [PATCH] feat: create ApiService and TokenService to refactor and delegate functions from other services --- frontend/src/app/services/api.service.ts | 47 ++++++ frontend/src/app/services/auth.service.ts | 135 ++++++++---------- frontend/src/app/services/movie.service.ts | 132 +++++------------ frontend/src/app/services/token.service.ts | 37 +++++ frontend/src/app/services/user.service.ts | 49 +++---- .../components/header/header.component.ts | 13 +- 6 files changed, 209 insertions(+), 204 deletions(-) create mode 100644 frontend/src/app/services/api.service.ts create mode 100644 frontend/src/app/services/token.service.ts diff --git a/frontend/src/app/services/api.service.ts b/frontend/src/app/services/api.service.ts new file mode 100644 index 0000000..e3a59ee --- /dev/null +++ b/frontend/src/app/services/api.service.ts @@ -0,0 +1,47 @@ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpHeaders } from '@angular/common/http'; +import { TokenService } from './token.service'; +import { environment } from '../../environments/environment'; +import { Observable, throwError } from 'rxjs'; +import { catchError } from 'rxjs/operators'; + +@Injectable({ + providedIn: 'root', +}) +export class ApiService { + constructor(private http: HttpClient, private tokenService: TokenService) {} + + private getAuthHeaders(): HttpHeaders { + const token = this.tokenService.getToken(); + if (!token) { + this.tokenService.clearSession(); + } + return new HttpHeaders({ Authorization: `Token ${token}` }); + } + + get(endpoint: string, auth: boolean = false): Observable { + const headers = auth ? { headers: this.getAuthHeaders() } : {}; + return this.http + .get(`${environment.baseUrl}${endpoint}`, headers) + .pipe(catchError(this.handleError)); + } + + post(endpoint: string, body: any, auth: boolean = false): Observable { + const headers = auth ? { headers: this.getAuthHeaders() } : {}; + return this.http + .post(`${environment.baseUrl}${endpoint}`, body, headers) + .pipe(catchError(this.handleError)); + } + + put(endpoint: string, body: any, auth: boolean = false): Observable { + const headers = auth ? { headers: this.getAuthHeaders() } : {}; + return this.http + .put(`${environment.baseUrl}${endpoint}`, body, headers) + .pipe(catchError(this.handleError)); + } + + private handleError(error: any) { + console.error('API error:', error); + return throwError(() => error); + } +} diff --git a/frontend/src/app/services/auth.service.ts b/frontend/src/app/services/auth.service.ts index 65b96bb..a81c527 100644 --- a/frontend/src/app/services/auth.service.ts +++ b/frontend/src/app/services/auth.service.ts @@ -1,18 +1,69 @@ -import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { catchError, lastValueFrom, map, Observable, of } from 'rxjs'; -import { environment } from '../../environments/environment'; +import { catchError, firstValueFrom, map, Observable, of } from 'rxjs'; +import { ApiService } from './api.service'; import { UserService } from './user.service'; +import { Router } from '@angular/router'; +import { TokenService } from './token.service'; @Injectable({ providedIn: 'root', }) export class AuthService { errorMsg: string | null = null; - passwordFieldType: string = 'password'; - passwordIcon: string = './assets/img/close-eye.svg'; + passwordFieldType = 'password'; + passwordIcon = './assets/img/close-eye.svg'; - constructor(private http: HttpClient, private userService: UserService) {} + constructor( + private router: Router, + private apiService: ApiService, + private userService: UserService, + private tokenService: TokenService + ) {} + + async login(body: any, storage: boolean) { + const data = await firstValueFrom( + this.apiService.post<{ token: string }>('/auth/login/', body) + ); + if (data?.token) { + this.tokenService.setToken(data.token, storage); + } else { + throw new Error('Login failed: No token received'); + } + } + + async logout() { + await firstValueFrom(this.apiService.post('/auth/logout/', null, true)); + } + + async register(body: any) { + await firstValueFrom(this.apiService.post('/auth/register/', body)); + } + + async verifyEmail(body: any) { + await firstValueFrom(this.apiService.post('/auth/verify-email/', body)); + } + + async forgotPassword(body: any) { + await firstValueFrom(this.apiService.post('/auth/forgot-password/', body)); + } + + async changePassword(body: any) { + await firstValueFrom(this.apiService.post('/auth/change-password/', body)); + } + + async checkAuthUserMail(body: any) { + await firstValueFrom(this.apiService.post('/auth/', body)); + } + + checkAuthUser(): Observable { + return this.apiService.get('/auth/', true).pipe( + map((response) => { + this.userService.currentUserId = response; + return true; + }), + catchError(() => of(false)) + ); + } togglePasswordVisibility() { this.passwordFieldType = @@ -26,76 +77,4 @@ export class AuthService { ? './assets/img/open-eye.svg' : './assets/img/close-eye.svg'; } - - async register(body: any) { - await lastValueFrom( - this.http.post(`${environment.baseUrl}/auth/register/`, body) - ); - } - - async login(body: any, storage: boolean) { - const data = (await lastValueFrom( - this.http.post(`${environment.baseUrl}/auth/login/`, body) - )) as { token: string }; - this.storeAuthToken(data.token, storage); - } - - async logout() { - const headers = this.getAuthHeaders(); - await lastValueFrom( - this.http.post(`${environment.baseUrl}/auth/logout/`, null, { headers }) - ); - } - - async verifyEmail(body: any) { - await lastValueFrom( - this.http.post(`${environment.baseUrl}/auth/verify-email/`, body) - ); - } - - async forgotPassword(body: any) { - await lastValueFrom( - this.http.post(`${environment.baseUrl}/auth/forgot-password/`, body) - ); - } - - async changePassword(body: any) { - await lastValueFrom( - this.http.post(`${environment.baseUrl}/auth/change-password/`, body) - ); - } - - storeAuthToken(data: any, storage: boolean) { - storage - ? localStorage.setItem('authToken', data.toString()) - : sessionStorage.setItem('authToken', data.toString()); - } - - checkAuthUser(): Observable { - const headers = this.getAuthHeaders(); - return this.http.get(`${environment.baseUrl}/auth/`, { headers }).pipe( - map((response) => { - this.saveUserId(response); - return true; - }), - catchError(() => of(false)) - ); - } - - saveUserId(userId: string): void { - this.userService.currentUserId = userId; - } - - async checkAuthUserMail(body: any) { - await lastValueFrom(this.http.post(`${environment.baseUrl}/auth/`, body)); - } - - private getAuthHeaders(): HttpHeaders { - let authToken = - localStorage.getItem('authToken') || sessionStorage.getItem('authToken'); - - return new HttpHeaders({ - Authorization: `Token ${authToken}`, - }); - } } diff --git a/frontend/src/app/services/movie.service.ts b/frontend/src/app/services/movie.service.ts index 5d41fff..3eb06b5 100644 --- a/frontend/src/app/services/movie.service.ts +++ b/frontend/src/app/services/movie.service.ts @@ -1,8 +1,14 @@ -import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { BehaviorSubject, lastValueFrom, Observable } from 'rxjs'; -import { environment } from '../../environments/environment'; -import { Router } from '@angular/router'; +import { + BehaviorSubject, + catchError, + firstValueFrom, + map, + Observable, + of, +} from 'rxjs'; +import { ApiService } from './api.service'; +import { AuthService } from './auth.service'; @Injectable({ providedIn: 'root', @@ -10,109 +16,47 @@ import { Router } from '@angular/router'; export class MovieService { private movieCache: { [key: number]: { [resolution: string]: boolean } } = {}; - constructor(private http: HttpClient, private router: Router) {} + constructor( + private apiService: ApiService, + private authService: AuthService + ) {} getAllMovies(): Promise { - const url = environment.baseUrl + '/video/'; - const headers = this.getAuthHeaders(); - return lastValueFrom(this.http.get(url, { headers })); + return firstValueFrom(this.apiService.get('/video/', true)); } getMovieFiles(videoUrl: number): Promise { - const url = environment.baseUrl + `${videoUrl}`; - const headers = this.getAuthHeaders(); - return lastValueFrom(this.http.get(url, { headers })); + return firstValueFrom(this.apiService.get(`/${videoUrl}`, true)); } - uploadMovie(formData: FormData) { - const url = environment.baseUrl + '/video/upload/'; - const headers = this.getAuthHeaders(); - return lastValueFrom(this.http.post(url, formData, { headers })); + uploadMovie(formData: FormData): Promise { + return firstValueFrom( + this.apiService.post('/video/upload/', formData, true) + ); } - private getAuthHeaders(): HttpHeaders { - let authToken = - localStorage.getItem('authToken') || sessionStorage.getItem('authToken'); - - if (!authToken) { - this.logout(); - } - - return new HttpHeaders({ - Authorization: `Token ${authToken}`, - }); - } - - private logout(): void { - localStorage.clear(); - sessionStorage.clear(); - this.router.navigate(['/']); - } - - /** - * Checks if a movie is uploaded in multiple resolutions. - * - * @param {number} videoID - The ID of the movie to check. - * @returns {Observable<{ [resolution: string]: boolean }>} Observable emitting an object with the availability of each resolution. - */ isMovieResolutionUploaded( videoID: number ): Observable<{ [resolution: string]: boolean }> { - if (this.movieCache.hasOwnProperty(videoID)) { - const cachedResolutions = this.movieCache[videoID]; - - const missingResolutions = Object.keys(cachedResolutions).filter( - (res) => !cachedResolutions[res] - ); - - if (missingResolutions.length === 0) { - return new BehaviorSubject(cachedResolutions).asObservable(); - } else { - return this.fetchAndCacheResolutions(videoID); - } - } else { - return this.fetchAndCacheResolutions(videoID); + const cachedRes = this.movieCache[videoID]; + if (cachedRes && Object.values(cachedRes).every(Boolean)) { + return new BehaviorSubject(cachedRes).asObservable(); } - } - /** - * Fetches the availability of multiple resolutions for a specific movie. - * - * @param {number} videoID - The ID of the movie to fetch resolutions for. - * @returns {Observable<{ [resolution: string]: boolean }>} Observable emitting an object with the availability of each resolution. - */ - private fetchAndCacheResolutions( - videoID: number - ): Observable<{ [resolution: string]: boolean }> { - const url = `${environment.baseUrl}/video/movie/${videoID}/`; - const headers = this.getAuthHeaders(); - - return new Observable<{ [resolution: string]: boolean }>((observer) => { - this.http.get(url, { headers }).subscribe( - (response: any) => { - const resolutions = { - '360': response['360'] || false, - '720': response['720'] || false, - '1080': response['1080'] || false, - }; - this.movieCache[videoID] = resolutions; - observer.next(resolutions); - observer.complete(); - }, - (error) => { - if (error.status === 401) { - this.logout(); - } - observer.next( - this.movieCache[videoID] || { - '360': false, - '720': false, - '1080': false, - } - ); - observer.complete(); - } - ); - }); + return this.apiService.get(`/video/movie/${videoID}/`, true).pipe( + map((res: any) => { + const resolutions = { + '360': res['360'] || false, + '720': res['720'] || false, + '1080': res['1080'] || false, + }; + this.movieCache[videoID] = resolutions; + return resolutions; + }), + catchError(() => { + this.authService.logout(); + return of({ '360': false, '720': false, '1080': false }); + }) + ); } } diff --git a/frontend/src/app/services/token.service.ts b/frontend/src/app/services/token.service.ts new file mode 100644 index 0000000..8181939 --- /dev/null +++ b/frontend/src/app/services/token.service.ts @@ -0,0 +1,37 @@ +import { Injectable } from '@angular/core'; +import { Router } from '@angular/router'; + +@Injectable({ + providedIn: 'root', +}) +export class TokenService { + private readonly TOKEN_KEY = 'authToken'; + + constructor(private router: Router) {} + + setToken(token: string, storage: boolean): void { + storage + ? localStorage.setItem(this.TOKEN_KEY, token) + : sessionStorage.setItem(this.TOKEN_KEY, token); + } + + getToken(): string | null { + const tokenFromSession = sessionStorage.getItem(this.TOKEN_KEY); + if (tokenFromSession) { + return tokenFromSession; + } + + const tokenFromLocal = localStorage.getItem(this.TOKEN_KEY); + return tokenFromLocal; + } + + removeToken(): void { + localStorage.removeItem(this.TOKEN_KEY); + sessionStorage.removeItem(this.TOKEN_KEY); + } + + clearSession(): void { + this.removeToken(); + this.router.navigate(['/']); + } +} diff --git a/frontend/src/app/services/user.service.ts b/frontend/src/app/services/user.service.ts index 54ff1a2..62bcbc3 100644 --- a/frontend/src/app/services/user.service.ts +++ b/frontend/src/app/services/user.service.ts @@ -1,7 +1,6 @@ -import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { environment } from '../../environments/environment'; -import { lastValueFrom } from 'rxjs'; +import { ApiService } from './api.service'; +import { firstValueFrom } from 'rxjs'; @Injectable({ providedIn: 'root', @@ -9,33 +8,31 @@ import { lastValueFrom } from 'rxjs'; export class UserService { currentUserId: string | null = null; - constructor(private http: HttpClient) {} + constructor(private apiService: ApiService) {} - async getLikedAndWatchedMovies(): Promise { - const url = environment.baseUrl + `/users/${this.currentUserId}/`; - const headers = this.getAuthHeaders(); - return lastValueFrom(this.http.get(url, { headers })); + getLikedAndWatchedMovies(): Promise { + return firstValueFrom( + this.apiService.get(`/users/${this.currentUserId}/`, true) + ); } - updateLikedMovies(likedMovies: any) { - const url = `${environment.baseUrl}/users/liked/${this.currentUserId}/`; - const headers = this.getAuthHeaders(); - return lastValueFrom(this.http.put(url, likedMovies, { headers })); + updateLikedMovies(likedMovies: any): Promise { + return firstValueFrom( + this.apiService.put( + `/users/liked/${this.currentUserId}/`, + likedMovies, + true + ) + ); } - updateWatchedMovies(watchedMovies: any) { - const url = `${environment.baseUrl}/users/watched/${this.currentUserId}/`; - const headers = this.getAuthHeaders(); - return lastValueFrom(this.http.put(url, watchedMovies, { headers })); - } - - private getAuthHeaders(): HttpHeaders { - let authToken = localStorage.getItem('authToken'); - if (!authToken) { - authToken = sessionStorage.getItem('authToken'); - } - return new HttpHeaders({ - Authorization: `Token ${authToken}`, - }); + updateWatchedMovies(watchedMovies: any): Promise { + return firstValueFrom( + this.apiService.put( + `/users/watched/${this.currentUserId}/`, + watchedMovies, + true + ) + ); } } diff --git a/frontend/src/app/shared/components/header/header.component.ts b/frontend/src/app/shared/components/header/header.component.ts index f15a570..b072838 100644 --- a/frontend/src/app/shared/components/header/header.component.ts +++ b/frontend/src/app/shared/components/header/header.component.ts @@ -1,8 +1,8 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; import { BtnLargeComponent } from '../buttons/btn-large/btn-large.component'; -import { Router, RouterLink } from '@angular/router'; +import { RouterLink } from '@angular/router'; import { AuthService } from '../../../services/auth.service'; -import { UserService } from '../../../services/user.service'; +import { TokenService } from '../../../services/token.service'; @Component({ selector: 'app-header', @@ -15,7 +15,10 @@ export class HeaderComponent { @Input() showFullLogo: boolean = true; @Output() moviesChange = new EventEmitter(); - constructor(private router: Router, private authService: AuthService) {} + constructor( + private authService: AuthService, + private tokenService: TokenService + ) {} backToOverview(newMovies: any[]) { this.moviesChange.emit(newMovies); @@ -24,9 +27,7 @@ export class HeaderComponent { async logout() { try { await this.authService.logout(); - localStorage.clear(); - sessionStorage.clear(); - this.router.navigate(['/']); + this.tokenService.clearSession(); } catch (error) { console.log(error); }