feat: create ApiService and TokenService to refactor and delegate functions from other services

This commit is contained in:
Chneemann 2025-04-22 07:34:28 +02:00
parent 8a2ed1a733
commit b5e0f52822
6 changed files with 209 additions and 204 deletions

View file

@ -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<T>(endpoint: string, auth: boolean = false): Observable<T> {
const headers = auth ? { headers: this.getAuthHeaders() } : {};
return this.http
.get<T>(`${environment.baseUrl}${endpoint}`, headers)
.pipe(catchError(this.handleError));
}
post<T>(endpoint: string, body: any, auth: boolean = false): Observable<T> {
const headers = auth ? { headers: this.getAuthHeaders() } : {};
return this.http
.post<T>(`${environment.baseUrl}${endpoint}`, body, headers)
.pipe(catchError(this.handleError));
}
put<T>(endpoint: string, body: any, auth: boolean = false): Observable<T> {
const headers = auth ? { headers: this.getAuthHeaders() } : {};
return this.http
.put<T>(`${environment.baseUrl}${endpoint}`, body, headers)
.pipe(catchError(this.handleError));
}
private handleError(error: any) {
console.error('API error:', error);
return throwError(() => error);
}
}

View file

@ -1,18 +1,69 @@
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { catchError, lastValueFrom, map, Observable, of } from 'rxjs'; import { catchError, firstValueFrom, map, Observable, of } from 'rxjs';
import { environment } from '../../environments/environment'; import { ApiService } from './api.service';
import { UserService } from './user.service'; import { UserService } from './user.service';
import { Router } from '@angular/router';
import { TokenService } from './token.service';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class AuthService { export class AuthService {
errorMsg: string | null = null; errorMsg: string | null = null;
passwordFieldType: string = 'password'; passwordFieldType = 'password';
passwordIcon: string = './assets/img/close-eye.svg'; 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<boolean> {
return this.apiService.get<any>('/auth/', true).pipe(
map((response) => {
this.userService.currentUserId = response;
return true;
}),
catchError(() => of(false))
);
}
togglePasswordVisibility() { togglePasswordVisibility() {
this.passwordFieldType = this.passwordFieldType =
@ -26,76 +77,4 @@ export class AuthService {
? './assets/img/open-eye.svg' ? './assets/img/open-eye.svg'
: './assets/img/close-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<boolean> {
const headers = this.getAuthHeaders();
return this.http.get<any>(`${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}`,
});
}
} }

View file

@ -1,8 +1,14 @@
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { BehaviorSubject, lastValueFrom, Observable } from 'rxjs'; import {
import { environment } from '../../environments/environment'; BehaviorSubject,
import { Router } from '@angular/router'; catchError,
firstValueFrom,
map,
Observable,
of,
} from 'rxjs';
import { ApiService } from './api.service';
import { AuthService } from './auth.service';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
@ -10,109 +16,47 @@ import { Router } from '@angular/router';
export class MovieService { export class MovieService {
private movieCache: { [key: number]: { [resolution: string]: boolean } } = {}; private movieCache: { [key: number]: { [resolution: string]: boolean } } = {};
constructor(private http: HttpClient, private router: Router) {} constructor(
private apiService: ApiService,
private authService: AuthService
) {}
getAllMovies(): Promise<any> { getAllMovies(): Promise<any> {
const url = environment.baseUrl + '/video/'; return firstValueFrom(this.apiService.get('/video/', true));
const headers = this.getAuthHeaders();
return lastValueFrom(this.http.get(url, { headers }));
} }
getMovieFiles(videoUrl: number): Promise<any> { getMovieFiles(videoUrl: number): Promise<any> {
const url = environment.baseUrl + `${videoUrl}`; return firstValueFrom(this.apiService.get(`/${videoUrl}`, true));
const headers = this.getAuthHeaders();
return lastValueFrom(this.http.get(url, { headers }));
} }
uploadMovie(formData: FormData) { uploadMovie(formData: FormData): Promise<any> {
const url = environment.baseUrl + '/video/upload/'; return firstValueFrom(
const headers = this.getAuthHeaders(); this.apiService.post('/video/upload/', formData, true)
return lastValueFrom(this.http.post(url, formData, { headers })); );
} }
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( isMovieResolutionUploaded(
videoID: number videoID: number
): Observable<{ [resolution: string]: boolean }> { ): Observable<{ [resolution: string]: boolean }> {
if (this.movieCache.hasOwnProperty(videoID)) { const cachedRes = this.movieCache[videoID];
const cachedResolutions = this.movieCache[videoID]; if (cachedRes && Object.values(cachedRes).every(Boolean)) {
return new BehaviorSubject(cachedRes).asObservable();
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);
} }
}
/** return this.apiService.get(`/video/movie/${videoID}/`, true).pipe(
* Fetches the availability of multiple resolutions for a specific movie. map((res: any) => {
* const resolutions = {
* @param {number} videoID - The ID of the movie to fetch resolutions for. '360': res['360'] || false,
* @returns {Observable<{ [resolution: string]: boolean }>} Observable emitting an object with the availability of each resolution. '720': res['720'] || false,
*/ '1080': res['1080'] || false,
private fetchAndCacheResolutions( };
videoID: number this.movieCache[videoID] = resolutions;
): Observable<{ [resolution: string]: boolean }> { return resolutions;
const url = `${environment.baseUrl}/video/movie/${videoID}/`; }),
const headers = this.getAuthHeaders(); catchError(() => {
this.authService.logout();
return new Observable<{ [resolution: string]: boolean }>((observer) => { return of({ '360': false, '720': false, '1080': false });
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();
}
);
});
} }
} }

View file

@ -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(['/']);
}
}

View file

@ -1,7 +1,6 @@
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { environment } from '../../environments/environment'; import { ApiService } from './api.service';
import { lastValueFrom } from 'rxjs'; import { firstValueFrom } from 'rxjs';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
@ -9,33 +8,31 @@ import { lastValueFrom } from 'rxjs';
export class UserService { export class UserService {
currentUserId: string | null = null; currentUserId: string | null = null;
constructor(private http: HttpClient) {} constructor(private apiService: ApiService) {}
async getLikedAndWatchedMovies(): Promise<any> { getLikedAndWatchedMovies(): Promise<any> {
const url = environment.baseUrl + `/users/${this.currentUserId}/`; return firstValueFrom(
const headers = this.getAuthHeaders(); this.apiService.get(`/users/${this.currentUserId}/`, true)
return lastValueFrom(this.http.get(url, { headers })); );
} }
updateLikedMovies(likedMovies: any) { updateLikedMovies(likedMovies: any): Promise<any> {
const url = `${environment.baseUrl}/users/liked/${this.currentUserId}/`; return firstValueFrom(
const headers = this.getAuthHeaders(); this.apiService.put(
return lastValueFrom(this.http.put(url, likedMovies, { headers })); `/users/liked/${this.currentUserId}/`,
likedMovies,
true
)
);
} }
updateWatchedMovies(watchedMovies: any) { updateWatchedMovies(watchedMovies: any): Promise<any> {
const url = `${environment.baseUrl}/users/watched/${this.currentUserId}/`; return firstValueFrom(
const headers = this.getAuthHeaders(); this.apiService.put(
return lastValueFrom(this.http.put(url, watchedMovies, { headers })); `/users/watched/${this.currentUserId}/`,
} watchedMovies,
true
private getAuthHeaders(): HttpHeaders { )
let authToken = localStorage.getItem('authToken'); );
if (!authToken) {
authToken = sessionStorage.getItem('authToken');
}
return new HttpHeaders({
Authorization: `Token ${authToken}`,
});
} }
} }

View file

@ -1,8 +1,8 @@
import { Component, EventEmitter, Input, Output } from '@angular/core'; import { Component, EventEmitter, Input, Output } from '@angular/core';
import { BtnLargeComponent } from '../buttons/btn-large/btn-large.component'; 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 { AuthService } from '../../../services/auth.service';
import { UserService } from '../../../services/user.service'; import { TokenService } from '../../../services/token.service';
@Component({ @Component({
selector: 'app-header', selector: 'app-header',
@ -15,7 +15,10 @@ export class HeaderComponent {
@Input() showFullLogo: boolean = true; @Input() showFullLogo: boolean = true;
@Output() moviesChange = new EventEmitter<any[]>(); @Output() moviesChange = new EventEmitter<any[]>();
constructor(private router: Router, private authService: AuthService) {} constructor(
private authService: AuthService,
private tokenService: TokenService
) {}
backToOverview(newMovies: any[]) { backToOverview(newMovies: any[]) {
this.moviesChange.emit(newMovies); this.moviesChange.emit(newMovies);
@ -24,9 +27,7 @@ export class HeaderComponent {
async logout() { async logout() {
try { try {
await this.authService.logout(); await this.authService.logout();
localStorage.clear(); this.tokenService.clearSession();
sessionStorage.clear();
this.router.navigate(['/']);
} catch (error) { } catch (error) {
console.log(error); console.log(error);
} }