Parte 6 — Recap, Footguns și Exerciții
Pasul 11 — Structura finală a fișierelor noi
Controllers/
Api/
ArticlesApiController.cs ← GET/POST/PUT/DELETE + ownership
AuthApiController.cs ← Login + GenerateJwt
DTOs/
ArticleDto.cs ← record — response
CreateArticleDto.cs ← record — POST body
UpdateArticleDto.cs ← record — PUT body
LoginDto.cs ← record — login body
Mappings/
ArticleMappings.cs ← extension methods ToDto / ToEntity / ApplyTo
Modificate:
Program.cs ← AddSwaggerGen, AddJwtBearer, UseSwagger, UseSwaggerUI
appsettings.json ← secțiunea Jwt
Fără breaking changes — MVC-ul din Lab 7 continuă să funcționeze identic.
Unde vă puteți arde
Lista scurtă de footgun-uri care fac request-uri să pice silent — verificați aici înainte să rulați /exit:
AddAuthentication(JwtBearerDefaults.AuthenticationScheme)în loc deAddAuthentication(). Reflexul din tutoriale online. Strică Identity (schema default e acum JWT, nu Cookie) → login-ul MVC/Auth/Loginnu mai setează cookie → MVC-ul rămâne „rupt" silent. Fix:AddAuthentication()fără argument — Identity a setat deja Cookies ca default.[Authorize]fărăAuthenticationSchemes = ...pe endpoint API. Cu schema default Cookie, API-ul răspunde cu302 redirectcătre/Auth/Loginîn loc de401 JSON. Clientul Swagger/Postman primește HTML în loc de JSON și nu înțelege ce se întâmplă. Fix:[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]explicit pe POST/PUT/DELETE.- Swashbuckle 10.x în loc de 6.9.0. Dacă rulați
dotnet add package Swashbuckle.AspNetCorefără--version 6.9.0sau dacă adăugați prin NuGet UI și dați pe „Latest", primiți 10.x care a rescris API-ul (Microsoft.OpenApi.Modelsnu mai există) și laboratorul nu compilează. Fix:--version 6.9.0explicit — vedeți Pasul 1. - Cheie JWT sub 32 de bytes.
SymmetricSecurityKeyaruncăArgumentOutOfRangeExceptionla startup — nici măcar nu pornește aplicația. Mesajul e obscur (IDX10720), ușor de ratat. Fix: numărați caracterele dinJwt:Key, trebuie minim 32. - Copy-paste de pe site în C# cu
„"tipografic. Dacă browser-ul normalizează quote-urile tipografice, string-ul se termină prematur și codul nu compilează. Fix: blocurile de cod din acest lab sunt ASCII-only intenționat; dacă vedeți erori de sintaxă pe string-uri, înlocuiți manual„/"cu"straight. [ApiController]uitat. Fără el,400 Bad Requestautomat pentru validare nu funcționează, iar binding-ul din body al DTO-ului poate cere[FromBody]explicit. Fix: mereu pe clasa API, deasupra[Route(...)].- Portul 7001 ocupat. Dacă Lab 7 rulează încă într-o altă fereastră, Lab 8 nu pornește (conflict pe HTTPS). Fix:
Ctrl+Cpe cel vechi, sau schimbați port înProperties/launchSettings.json. - Token expirat în timpul testării. 60 de minute e mult pentru un demo, dar puțin când povestiți. Dacă primiți
401după ce ați fost autentificat, re-login și copiați token nou.
Test end-to-end — checklist
dotnet run→ aplicația pornește fără erori/(MVC Home) → funcționează cu cookie auth/swagger→ Swagger UI afișează endpoint-urileGET /api/articlesapidin Swagger → listă JSON de articolePOST /api/authapi/logincuadmin@newsportal.com/Admin@123→ primim token- Decodăm token-ul pe jwt.io — vedem
name,email,role: Admin - Click „Authorize" în Swagger, lipim token-ul
POST /api/articlesapicuCreateArticleDto→201 Created, headerLocationPUT /api/articlesapi/{id}ca user diferit →403 ForbiddenDELETE /api/articlesapi/{id}fără token →401 Unauthorized
Exerciții (4p)
Lab-ul e structurat ca patru checkpoints — fiecare exercițiu acoperă o porțiune din parcurs. Parcurgerea lab-ului (Parte 1 → Parte 5) vă duce prin Ex 1–3. Ex 4 e peste lab.
Exercițiul 1 — API read-only + DTOs + Swagger (1p)
Acoperă: Parte 1 (DTOs + Mappings) + Pasul 4 (GET) + Parte 3 (Swagger).
Merg toate simultan:
DTOs/ArticleDto.cs,CreateArticleDto.cs,UpdateArticleDto.cs,LoginDto.csexistă și sunt records cu sintaxă pozițională.Mappings/ArticleMappings.csareToDto(),ToDtoList(),ToEntity(),ApplyTo()— toate extension methods.Controllers/Api/ArticlesApiController.csareGET /api/articlesapișiGET /api/articlesapi/{id}(fără[Authorize]pe ele — lectură publică).GET /api/articlesapi→200 OK+ listă deArticleDtoîn JSON.GET /api/articlesapi/999(id inexistent) →404 Not Found.https://localhost:7001/swaggerse deschide și arată endpoint-urile API.
Exercițiul 2 — CRUD complet + JWT Login (1p)
Acoperă: Pasul 5 (POST/PUT/DELETE) + Parte 4 (JWT configurare + AuthApiController).
Merg toate simultan:
POST /api/articlesapi,PUT /api/articlesapi/{id},DELETE /api/articlesapi/{id}există, fiecare cu[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)].appsettings.jsonare secțiuneaJwtcuKey≥ 32 caractere,Issuer,Audience,ExpiresInMinutes.Controllers/Api/AuthApiController.csarePOST /api/authapi/logincare întoarce{ "token": "...", "expiresIn": 3600 }.Program.csconfigureazăAddAuthentication().AddJwtBearer(...)cuTokenValidationParameterscomplete (Issuer/Audience/Lifetime/SigningKey).POST /api/articlesapifără token →401 Unauthorized.POST /api/articlesapicuAuthorization: Bearer <token>valid →201 Created+ headerLocation.- Token-ul decodat pe jwt.io conține
sub,name,email,role.
Exercițiul 3 — Content ownership + Swagger „Authorize" (1p)
Acoperă: IsOwnerOrAdmin din Pasul 5 (testabil abia după Ex 2) + Parte 5 (buton Authorize în Swagger).
Merg toate simultan:
IsOwnerOrAdmin(article)există ca helper privat și verificăarticle.AuthorId == currentUserId || User.IsInRole("Admin").PUT/DELETEca user non-admin și non-autor →403 Forbidden.- Aceleași request-uri ca Admin (sau ca autorul articolului) →
204 No Content. AddSwaggerGenareAddSecurityDefinition("Bearer", ...)+AddSecurityRequirement(...)— butonul „Authorize" apare în Swagger UI.- După click pe „Authorize" și paste token, POST/PUT/DELETE merg direct din Swagger (header-ul e trimis automat).
Exercițiul 4 — Paginare pe API (1p)
Acoperă: muncă peste lab — nu e în walkthrough.
Hint-uri:
- Nu rescrieți GET-ul existent — adăugați o acțiune nouă sau suportați
page/pageSizepe cea curentă, cu default-uri ca să nu spargeți comportamentul GET-all.- Refolosiți
_articleService.GetPagedAsync(page, pageSize, ct)și_articleService.CountAsync(ct)din Lab 6. Nu scriețiSkip/Takeîn controller.totalPages = (int)Math.Ceiling((double)totalCount / pageSize).- Validare:
page < 1saupageSize < 1→BadRequest.pageSize > 100→ clamp la 100 (anti-DoS prinpageSize=999999).- Atenție
Task.WhenAll: cele două apeluri folosesc acelașiDbContext(Scoped per request). EF Core aruncăInvalidOperationException: A second operation was started on this context...pe query-uri concurente. Rulați serial, sau injectațiIDbContextFactory<AppDbContext>pentru contexte separate. Paragraful din Pasul 10 e valabil pentru HttpClient / contexte separate — nu pentru același DbContext.
Merg toate simultan:
DTOs/PagedResultDto.csexistă ca record generic:public record PagedResultDto<T>( IReadOnlyList<T> Items, int TotalCount, int Page, int PageSize, int TotalPages);GET /api/articlesapi?page=1&pageSize=10→200 OKcu body de forma:{ "items": [ /* ArticleDto[] */ ], "totalCount": 42, "page": 1, "pageSize": 10, "totalPages": 5 }GET /api/articlesapi?page=0&pageSize=10→400 Bad Request(sau clamp lapage=1).GET /api/articlesapi?page=1&pageSize=999→pageSizeclamp la 100 (sau400).
Total: 4p — 1p pe fiecare exercițiu.
Exercițiile se rezolvă în timpul laboratorului. Fără breaking changes pe partea MVC — dacă ați spart login-ul cu cookie, ați greșit configurarea schemelor.
Referință rapidă — Postman
Înainte de demo: creați un user non-admin din UI la
/Auth/Register(ex.user@test.com/User@123). Tokenurile se obțin din pașii 1–2 sau din Swagger la/swagger.
Auth
Login admin → copiați token-ul
POST https://localhost:7001/api/authapi/login
Content-Type: application/json
{
"email": "admin@newsportal.com",
"password": "Admin@123"
}
200 OK — { "token": "eyJ...", "expiresIn": 3600 }
Login user non-admin → copiați token-ul
POST https://localhost:7001/api/authapi/login
Content-Type: application/json
{
"email": "user@test.com",
"password": "User@123"
}
Articole — read (fără token)
GET https://localhost:7001/api/articlesapi
200 OK
GET https://localhost:7001/api/articlesapi/1
200 OK
GET https://localhost:7001/api/articlesapi/99999
404 Not Found
Articole — create
Fără token
POST https://localhost:7001/api/articlesapi
Content-Type: application/json
{
"title": "Test fara token",
"content": "Continut suficient de lung pentru validare",
"categoryId": 1
}
401 Unauthorized
Cu token admin
POST https://localhost:7001/api/articlesapi
Content-Type: application/json
Authorization: Bearer TOKENUL_ADMINULUI
{
"title": "Articol din Postman",
"content": "Continut suficient de lung pentru validare",
"categoryId": 1
}
201 Created — rețineți id-ul din răspuns
Articole — update
Fără token
PUT https://localhost:7001/api/articlesapi/1
Content-Type: application/json
{
"title": "Incerc sa modific",
"content": "Continut suficient de lung pentru validare",
"categoryId": 1
}
401 Unauthorized
Ca non-owner ← bug Forbid()
PUT https://localhost:7001/api/articlesapi/1
Content-Type: application/json
Authorization: Bearer TOKENUL_USERULUI
{
"title": "Incerc sa suprascriu",
"content": "Continut suficient de lung pentru validare",
"categoryId": 1
}
Așteptat: 403 Forbidden → Primit: 200 OK + HTML (redirect la login)
Forbid()fără argument cade pe schema cookie → redirect 302 → Postman urmează redirect-ul → pagina de login →200 OK. Fix în Lab 9:Forbid(JwtBearerDefaults.AuthenticationScheme).
Ca owner
PUT https://localhost:7001/api/articlesapi/1
Content-Type: application/json
Authorization: Bearer TOKENUL_ADMINULUI
{
"title": "Titlu modificat de owner",
"content": "Continut suficient de lung pentru validare",
"categoryId": 1
}
204 No Content
Articole — delete
Fără token
DELETE https://localhost:7001/api/articlesapi/1
401 Unauthorized
Ca non-owner ← bug Forbid()
DELETE https://localhost:7001/api/articlesapi/1
Authorization: Bearer TOKENUL_USERULUI
Așteptat: 403 Forbidden → Primit: 200 OK + HTML (același bug)
Ca owner
DELETE https://localhost:7001/api/articlesapi/ID_DIN_PAS_CREATE
Authorization: Bearer TOKENUL_ADMINULUI
204 No Content