Parte 2 — Modele și servicii
Frontend-ul Angular are aceeași stratificare ca backend-ul: modele (descriu datele), servicii (operații peste API), componente (UI). Aici scriem stratul de date și logică, separat de UI.
Modele TypeScript
Modelele sunt interfețe — descriu forma JSON-ului primit de la API. TypeScript nu le rulează la runtime (sunt șterse la build), dar la compile time verifică că nu accesăm câmpuri inexistente.
src/app/shared/models/article.ts:
export interface Article {
id: number;
title: string;
content: string;
publishedAt: string; // ISO date string din JSON
categoryId: number;
categoryName: string;
authorId: string;
authorName: string;
tags?: string[]; // optional - poate lipsi
}
export interface Category {
id: number;
name: string;
}
export interface CurrentUser {
id: string;
name: string;
email: string;
roles: string[];
}
| Detaliu | De ce |
|---|---|
publishedAt: string (nu Date) |
JSON nu are tipul Date — vine ca string ISO 8601. Conversia o faceți în template cu | date pipe sau în service. |
tags?: string[] |
? = opțional. Dacă API-ul nu populează tags, article.tags e undefined — iar TS îl semnalează. |
roles: string[] |
JWT-ul nostru pune rolurile ca un array de claims ClaimTypes.Role. Decodarea le adună într-un string[]. |
Notă: Modelele frontend nu sunt o copie 1-la-1 a entităților EF — sunt o copie a DTO-urilor API (
ArticleDto, etc). Backend-ul deja faceEntity → DTOînMappings/. Frontend-ul trebuie să se uite doar la DTO, nu la entitate.
AuthService — login, register, state
AuthService e responsabil de tot ce ține de autentificare: apel API pentru login/register, salvarea token-ului JWT, expunerea utilizatorului curent ca stream observabil.
src/app/core/services/auth.ts:
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, Observable, tap } from 'rxjs';
import { environment } from '../../../environments/environment';
import { CurrentUser } from '../../shared/models/article';
interface AuthResponse {
token: string;
expiresIn: number;
}
@Injectable({ providedIn: 'root' })
export class AuthService {
private apiUrl = `${environment.apiUrl}/api/auth`;
private currentUserSubject = new BehaviorSubject<CurrentUser | null>(null);
public currentUser$ = this.currentUserSubject.asObservable();
constructor(private http: HttpClient) {
this.loadUserFromToken();
}
login(email: string, password: string): Observable<AuthResponse> {
return this.http.post<AuthResponse>(`${this.apiUrl}/login`, { email, password })
.pipe(tap(res => this.handleAuthResponse(res.token)));
}
register(email: string, fullName: string, password: string): Observable<AuthResponse> {
return this.http.post<AuthResponse>(`${this.apiUrl}/register`, { email, fullName, password })
.pipe(tap(res => this.handleAuthResponse(res.token)));
}
logout(): void {
localStorage.removeItem('token');
this.currentUserSubject.next(null);
}
isAuthenticated(): boolean {
return localStorage.getItem('token') !== null;
}
getToken(): string | null {
return localStorage.getItem('token');
}
private handleAuthResponse(token: string): void {
localStorage.setItem('token', token);
this.currentUserSubject.next(this.decodeUser(token));
}
private loadUserFromToken(): void {
const token = localStorage.getItem('token');
if (token) {
this.currentUserSubject.next(this.decodeUser(token));
}
}
private decodeUser(token: string): CurrentUser {
const payload = this.decodeJwtPayload(token);
const roleClaim = payload['http://schemas.microsoft.com/ws/2008/06/identity/claims/role'];
const roles = Array.isArray(roleClaim) ? roleClaim : roleClaim ? [roleClaim] : [];
return {
id: payload['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier'],
name: payload['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name'],
email: payload['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'],
roles
};
}
private decodeJwtPayload(token: string): any {
const base64Url = token.split('.')[1];
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
const padded = base64 + '='.repeat((4 - base64.length % 4) % 4);
return JSON.parse(atob(padded));
}
}
Concepte cheie
@Injectable({ providedIn: 'root' })— Angular îl creează o singură dată pe toată aplicația (singleton). Nu trebuie listat înprovidersla modul.BehaviorSubject— un Observable care reține ultima valoare emisă. Orice componentă care se abonează primește instant valoarea curentă (utilizatorul logat saunull). E echivalentul reactiv al unei proprietăți cu getter, fără polling.localStorage— stocăm JWT-ul în browser, supraviețuiește refresh-urilor de pagină. La pornire,loadUserFromToken()reconstruiește state-ul.tap()— un operator RxJS care ne lasă să facem side effects (salvare, log) fără să modificăm valoarea ce trece prin stream.
De ce URL-urile alea ciudate la decode?
JWT-urile generate de ASP.NET Core Identity au claims cu schema XML (ClaimTypes.NameIdentifier se serializează ca http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier). Sunt verbose dar predictibile — exact strings constante.
Backend-ul nostru pune trei claim-uri standard + rolurile:
// In AuthApiController.GenerateJwtAsync
var claims = new List<Claim>
{
new(ClaimTypes.NameIdentifier, user.Id), // -> .../nameidentifier
new(ClaimTypes.Name, user.UserName!), // -> .../name
new(ClaimTypes.Email, user.Email!) // -> .../emailaddress
};
foreach (var role in roles)
claims.Add(new Claim(ClaimTypes.Role, role)); // -> .../role (poate fi array daca >1)
Alternativă: la backend, mapați-le explicit la nume scurte (sub, name, email, role) cu JwtRegisteredClaimNames — mai standard JWT, dar atunci trebuie ajustat și middleware-ul JWT din Program.cs ca să le citească corect. Pentru lab rămânem la default-ul ASP.NET Core.
decodeJwtPayload — de ce nu doar atob(token.split('.')[1])
Token-ul JWT codifică payload-ul în base64url (variantă URL-safe a base64). Diferențele:
| Standard | Caractere | Padding |
|---|---|---|
base64 (folosit de atob) |
A-Z a-z 0-9 + / = |
= la sfârșit dacă lungimea nu e multiplu de 4 |
base64url (folosit JWT) |
A-Z a-z 0-9 - _ |
fără padding |
atob direct pe base64url fail-uiește când payload-ul conține - sau _ sau când lungimea nu e divizibilă cu 4. Patch-urile replace + padding îl normalizează.
ArticleService (read-only)
src/app/core/services/article.ts:
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { Article } from '../../shared/models/article';
import { environment } from '../../../environments/environment';
@Injectable({ providedIn: 'root' })
export class ArticleService {
private apiUrl = `${environment.apiUrl}/api/articles`;
constructor(private http: HttpClient) {}
getAll(): Observable<Article[]> {
return this.http.get<Article[]>(this.apiUrl);
}
getById(id: number): Observable<Article> {
return this.http.get<Article>(`${this.apiUrl}/${id}`);
}
// create(), update(), delete() ajung in Lab 12
}
Despre Observable vs Promise
HttpClient returnează Observable, nu Promise. Observabilele:
- pot emite mai multe valori (relevant la WebSocket / SSE)
- sunt lazy — request-ul HTTP se trimite doar la
.subscribe(), nu la apel - sunt anulabile —
subscription.unsubscribe()întrerupe cererea - se compun cu operatori (
map,filter,switchMap, …)
Echivalentul mental .NET: Task<T> e Promise (rezultat singular, nu se compune ușor). IAsyncEnumerable<T> e cel mai aproape de Observable din C#.
Pattern de consum în componentă (state în signals, vezi Partea 3 pentru detalii):
articles = signal<Article[]>([]);
error = signal<string | null>(null);
this.articleService.getAll().subscribe({
next: a => this.articles.set(a),
error: () => this.error.set('Eroare la incarcare')
});
Notă:
subscribefără să mai chemămunsubscribepoate produce memory leaks — Angular nu le curăță automat. În lab, pentru request-uri HTTP scurte la inițializare, e acceptabil. Pentru abonări de lungă durată folosițitakeUntilDestroyed()(Angular 16+) sauasyncpipe.
authInterceptor — Authorization header automat
Un HTTP Interceptor prinde fiecare request înainte să plece și fiecare response înainte să ajungă la apelant. Îl folosim ca să atașăm Authorization: Bearer <token> global, fără să-l scriem în fiecare service.
În Angular 16+ interceptorii sunt funcții pure (HttpInterceptorFn), nu clase. Folosesc inject() în loc de constructor și se înregistrează cu provideHttpClient(withInterceptors([...])).
src/app/core/services/auth-interceptor.ts:
import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { AuthService } from './auth';
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const token = inject(AuthService).getToken();
if (token) {
req = req.clone({
setHeaders: { Authorization: `Bearer ${token}` }
});
}
return next(req);
};
Detalii
HttpRequeste immutable —req.clone({ setHeaders: ... })produce o copie modificată. Așa Angular evită bug-uri de retry care ar muta același request.- Interceptor funcțional: e o funcție
(req, next) => Observable<HttpEvent>.inject(AuthService)funcționează în interiorul ei pentru că rulează într-un context Angular activ (provided prinprovideHttpClientînapp.config.ts— vezi Partea 3). - Pentru request-urile fără token (login, register, GET public) interceptor-ul nu adaugă nimic — backend-ul ignoră Bearer absent pe rute publice.
- Vechiul API (clase cu
implements HttpInterceptor+HTTP_INTERCEPTORSmulti-token) încă funcționează, dar nu mai e default-ul. CLI 17+ generează interceptors funcționali.
authGuard — protecție de rute
Un Guard verifică o condiție înainte de a permite navigarea către o rută. CanActivateFn rulează la matching — rezultă fie acces permis (true), fie blocare cu redirect.
Ca și interceptorii, guards-urile moderne sunt funcții, nu clase, și folosesc inject() direct.
src/app/core/guards/auth-guard.ts:
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { AuthService } from '../services/auth';
export const authGuard: CanActivateFn = (route, state) => {
const auth = inject(AuthService);
const router = inject(Router);
if (auth.isAuthenticated()) {
return true;
}
router.navigate(['/login'], { queryParams: { returnUrl: state.url } });
return false;
};
Folosire (în Lab 12)
// In app.routes.ts (Lab 12, pentru rute protejate)
{ path: 'articles/new', component: ArticleCreate, canActivate: [authGuard] }
În Lab 11 toate rutele sunt fie publice (lista, detaliu), fie de auth (login, register) — nu folosim guard-ul efectiv. Îl scriem acum ca infrastructură pregătită pentru Lab 12.
Notă: Verificarea autentificării se face doar pe prezența token-ului, nu pe valabilitatea lui. Un JWT expirat trece de guard, dar primul request către API întoarce
401. Pentru protecție mai strictă: parsațiexpdin JWT și verificați-l. În producție, folosiți și un interceptor de erori care la401face logout automat.