12

Lab 12 - Angular Frontend (Part 2) - CRUD, Reactive Forms, Role-based UI

Partea 3 din 5

Actualizat 2026-05-17Sursă pe GitHub ↗Cod start

Parte 3 — Role-based UI: canModify și delete cu confirm

Form-ul merge. Acum facem lista să respecte rolul: butoanele Edit / Delete apar doar pentru cine chiar poate modifica articolul. Și implementăm deleteArticle cu confirm + refresh.

Recapitulare: ce avem deja în Lab12_start

ArticleListComponent din Lab 11 era simplu: listă carduri + buton Detalii. În Lab12_start am pre-adăugat:

  • Buton „+ Articol nou" la header (*ngIf="isAuthenticated()").
  • Per card: butoane Editează și Șterge, ambele învelite în *ngIf="canModify(article)".
  • Metode noi în component: isAuthenticated(), canModify(article), createArticle(), editArticle(id, event), deleteArticle(id, event).

Trei dintre ele sunt funcționale (isAuthenticated, createArticle, editArticle). Două au TODO Lab 12: canModify (returnează hardcodat false) și deleteArticle (corp gol).

AuthService — cum citim user-ul curent sincron

AuthService din Lab 11 expune două piese:

private currentUserSubject = new BehaviorSubject<CurrentUser | null>(null);
public currentUser$ = this.currentUserSubject.asObservable();
Acces Sincron? Folosit în
currentUser$ (Observable) nu — subscribe/async pipe template-uri, când vrei reactivitate (header se update-ează la login)
currentUserSubject.value da — întoarce direct ultima valoare logică de decizie sincronă (e.g. canModify)

Pentru canModify(article) apelat sincron din template (la fiecare *ngIf), avem nevoie de acces sincron. Două abordări:

A. Adăugăm un getter pe AuthService:

// In auth.ts (optional - nu cerut de exercitiu, dar curat)
get currentUser(): CurrentUser | null {
  return this.currentUserSubject.value;
}

B. Subscribe în component, salvăm într-o proprietate locală:

private currentUser: CurrentUser | null = null;

ngOnInit(): void {
  this.authService.currentUser$.subscribe(u => this.currentUser = u);
  this.loadArticles();
}

Pentru lab folosim B — nu modifică AuthService (rămâne ca în Lab 11), și demonstrează patternul reactiv.

Notă: Subscription-ul în ngOnInit fără unsubscribe la ngOnDestroy poate lăsa un listener viu. Pentru BehaviorSubject la nivel de root service e benign în lab; în producție folosiți takeUntilDestroyed() sau async pipe.

canModify(article) — autor sau Admin

Logica e identică cu backend-ul:

// Lab12_start/backend/Authorization/ClaimsPrincipalExtensions.cs
public static bool CanModifyArticle(this ClaimsPrincipal user, Article article)
{
    var userId = user.FindFirstValue(ClaimTypes.NameIdentifier);
    return article.AuthorId == userId || user.IsInRole("Admin");
}

Versiunea TypeScript:

canModify(article: Article): boolean {
  if (!this.currentUser) return false;
  return this.currentUser.roles.includes('Admin')
      || article.authorId === this.currentUser.id;
}

Trei cazuri:

User Articol scris de el Articol scris de altcineva
Anonim n/a false (nu vede butoane)
User true false
Admin true true

Backend-ul respectă aceeași regulă la PUT/DELETE /api/articles/:id — UI-ul doar ascunde butoane, securitatea e în endpoint.

deleteArticle — confirm + delete + refresh

deleteArticle(id: number, event: Event): void {
  event.stopPropagation();

  if (!confirm('Sigur stergeti articolul?')) return;

  this.articleService.delete(id).subscribe({
    next: () => {
      this.articles = this.articles.filter(a => a.id !== id);
    },
    error: () => this.error = 'Eroare la stergere'
  });
}

Trei lucruri:

  • event.stopPropagation() — în viitor poți face cardul clickabil ((click)="viewArticle(...)" pe <div class="card">). Fără stopPropagation, click pe Delete ar bubble-ui și ar trigger-ui și viewArticle. Defensive.
  • confirm(...) — dialog browser nativ. Returnează true la OK, false la Cancel. Simplu și suficient pentru lab.
  • Refresh local cu filter — în loc de this.loadArticles() care face alt GET, scoatem articolul din array. Faster + consistent imediat. Trade-off: dacă altcineva a creat un articol între timp, nu-l vezi până la următorul refresh manual.

Alternativă: this.loadArticles() după success — un round-trip în plus, dar lista e mereu fresh. Pentru lab, oricare merge.

Buton „+ Articol nou" — deja vizibil

În template-ul din Lab12_start:

<button *ngIf="isAuthenticated()" class="btn btn-success" (click)="createArticle()">
  + Articol nou
</button>

isAuthenticated() apelează authService.isAuthenticated() care verifică doar prezența token-ului în localStorage. Pentru un JWT expirat încă întoarce true — și cererea POST primește 401. Pentru lab e OK; în producție extrageți exp din JWT.

Butoane Edit / Delete — deja învelite în *ngIf

<ng-container *ngIf="canModify(article)">
  <button class="btn btn-sm btn-warning" (click)="editArticle(article.id, $event)">
    Editeaza
  </button>
  <button class="btn btn-sm btn-danger" (click)="deleteArticle(article.id, $event)">
    Sterge
  </button>
</ng-container>

<ng-container> — wrapper logic, nu randează un element DOM. Ideal pentru *ngIf peste mai multe elemente.

(click)="editArticle(article.id, $event)" — pasăm $event ca să facem stopPropagation în handler (la fel cum face deleteArticle).

Ordinea fluxului end-to-end

Pentru un user Admin autentificat, pe lista de articole:

ngOnInit
  -> authService.currentUser$.subscribe -> this.currentUser = { id, roles: ['Admin'] }
  -> loadArticles -> this.articles = [...]

Render template
  -> "+ Articol nou": isAuthenticated() = true (token in localStorage) -> randat
  -> per card: canModify(article) -> currentUser.roles includes 'Admin' = true -> Edit/Delete randate

Click pe Sterge
  -> deleteArticle(5, event)
  -> stopPropagation
  -> confirm("Sigur stergeti articolul?") -> OK
  -> articleService.delete(5).subscribe
       -> Interceptor adauga Bearer
       -> DELETE /api/articles/5 -> 204
  -> next: this.articles = filter(a => a.id !== 5)
  -> *ngFor re-randeaza fara articolul sters

Pentru un user obișnuit (rol User) pe un articol al altcuiva: canModify returnează false, <ng-container> nu randează nimic, butoanele nu apar. Și dacă cumva canModify ar fi greșit, backend-ul respinge DELETE cu 403.


Exercițiul 4 (1p) — canModify + delete cu confirm

Cerințe

  1. În ArticleListComponent, adăugați:

    • Câmp privat private currentUser: CurrentUser | null = null; (importați CurrentUser din shared/models/article.model).
    • În ngOnInit, this.authService.currentUser$.subscribe(u => this.currentUser = u); înainte de loadArticles().
  2. Implementați canModify(article: Article): boolean:

    • Dacă currentUser e null → false.
    • Altfel currentUser.roles.includes('Admin') || article.authorId === currentUser.id.
  3. Implementați deleteArticle(id, event):

    • event.stopPropagation().
    • if (!confirm('Sigur stergeti articolul?')) return;.
    • articleService.delete(id).subscribe({ next: () => this.articles = this.articles.filter(a => a.id !== id), error: () => this.error = 'Eroare la stergere' });.
  4. Test end-to-end:

    • Login ca Admin. Pe fiecare card vedeți Edit + Delete.
    • Click Delete → confirm → articolul dispare din listă.
    • Logout. Pe carduri nu mai vedeți Edit / Delete (anonim, canModify = false).
    • Login ca user obișnuit (creați unul nou cu Register, sau folosiți seed-ul user1@newsportal.com / User@123 dacă există).
    • Pe articole create de altul: fără butoane.
    • Creați un articol cu Ex 2 → pe acel articol vedeți butoanele.
  5. Backend verify: încercați un DELETE /api/articles/:idAlAltui din DevTools cu token de user obișnuit:

    const token = localStorage.getItem('token');
    fetch('https://localhost:7001/api/articles/1', {
      method: 'DELETE',
      headers: { 'Authorization': `Bearer ${token}` }
    }).then(r => console.log(r.status))
    

    Răspuns: 403. UI-ul deja v-a ascuns butonul, backend-ul îl respinge dacă-l forțați.

Output așteptat

  • Edit + Delete vizibile doar pentru autor sau Admin.
  • Click Delete cu OK la confirm → articolul dispare din listă fără reload.
  • Click Cancel la confirm → nimic nu se întâmplă.
  • La eroare HTTP, mesaj global afișat în UI (nu crash, nu mesaj raw).