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
ngOnInitfărăunsubscribelangOnDestroypoate lăsa un listener viu. PentruBehaviorSubjectla nivel de root service e benign în lab; în producție folosițitakeUntilDestroyed()sauasyncpipe.
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ătruela OK,falsela Cancel. Simplu și suficient pentru lab.- Refresh local cu
filter— în loc dethis.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
-
În
ArticleListComponent, adăugați:- Câmp privat
private currentUser: CurrentUser | null = null;(importațiCurrentUserdinshared/models/article.model). - În
ngOnInit,this.authService.currentUser$.subscribe(u => this.currentUser = u);înainte deloadArticles().
- Câmp privat
-
Implementați
canModify(article: Article): boolean:- Dacă
currentUsere null →false. - Altfel
currentUser.roles.includes('Admin') || article.authorId === currentUser.id.
- Dacă
-
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' });.
-
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@123dacă există). - Pe articole create de altul: fără butoane.
- Creați un articol cu Ex 2 → pe acel articol vedeți butoanele.
- Login ca
-
Backend verify: încercați un
DELETE /api/articles/:idAlAltuidin 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).