08

Lab 08 - Web API, DTOs, Swagger si JWT Authentication

Partea 2 din 6

Parte 2 — CRUD pe API

Pasul 4 — ArticlesApiController — GET

Controllers/Api/ArticlesApiController.cs — fișier nou

using Lab08.DTOs;
using Lab08.Mappings;
using Lab08.Models;
using Lab08.Services;
using Microsoft.AspNetCore.Mvc;

namespace Lab08.Controllers.Api;

[ApiController]
[Route("api/[controller]")]
public class ArticlesApiController : ControllerBase
{
    private readonly IArticleService _articleService;

    public ArticlesApiController(IArticleService articleService)
    {
        _articleService = articleService;
    }

    // GET: /api/articlesapi
    [HttpGet]
    [ProducesResponseType(typeof(List<ArticleDto>), StatusCodes.Status200OK)]
    public async Task<ActionResult<List<ArticleDto>>> GetAll(CancellationToken cancellationToken)
    {
        var articles = await _articleService.GetAllAsync(cancellationToken);
        return Ok(articles.ToDtoList());
    }

    // GET: /api/articlesapi/5
    [HttpGet("{id:int}")]
    [ProducesResponseType(typeof(ArticleDto), StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<ActionResult<ArticleDto>> GetById(int id, CancellationToken cancellationToken)
    {
        var article = await _articleService.GetByIdAsync(id, cancellationToken);
        if (article == null)
            return NotFound();

        return Ok(article.ToDto());
    }
}

Ce e de observat:

  • [ApiController] — activează validare automată a modelului (400 Bad Request dacă ModelState e invalid), binding automat din body și erori de validare standardizate.
  • [Route("api/[controller]")] — token-ul [controller] se înlocuiește cu numele clasei fără sufixul Controller. Aici → api/articlesapi.
  • Constrângerea {id:int} — dacă cineva trimite /api/articlesapi/abc, ruta nu se potrivește și primește 404 direct, fără să ajungă în acțiune.
  • ActionResult<ArticleDto> — permite atât return Ok(dto) cât și return NotFound() în aceeași semnătură.
  • IArticleService e refolosit din Lab 6 — nu rescriem logica. Aceasta e valoarea Service Layer: aceeași logică servește și MVC, și API.
  • [ProducesResponseType] — metadata pentru Swagger: spune ce status codes returnează metoda și cu ce schema de body.

Întrebare: Care e diferența între REST și RPC? Endpoint-urile noastre sunt RESTful?

RESTful înseamnă resurse identificate prin URI, verbe HTTP care exprimă operația (GET/POST/PUT/DELETE), stateless. /api/articlesapi/5 cu GET = „citește articolul cu id 5" — RESTful. /api/articlesapi/deleteArticle?id=5 cu POST ar fi RPC — URI-ul conține verbul.


Pasul 5 — CRUD complet pe API

Adăugați în același ArticlesApiController:

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using System.Security.Claims;

// ... în corpul clasei ArticlesApiController ...

// POST: /api/articlesapi
[HttpPost]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[ProducesResponseType(typeof(ArticleDto), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<ActionResult<ArticleDto>> Create(
    CreateArticleDto dto,
    CancellationToken cancellationToken)
{
    var article = dto.ToEntity();
    article.AuthorId = User.FindFirstValue(ClaimTypes.NameIdentifier);

    await _articleService.AddAsync(article, cancellationToken);

    // Reîncarcă articolul cu Category și Author populate, pentru DTO-ul de răspuns
    var created = await _articleService.GetByIdAsync(article.Id, cancellationToken);
    return CreatedAtAction(nameof(GetById), new { id = article.Id }, created!.ToDto());
}

// PUT: /api/articlesapi/5
[HttpPut("{id:int}")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> Update(
    int id,
    UpdateArticleDto dto,
    CancellationToken cancellationToken)
{
    var article = await _articleService.GetByIdAsync(id, cancellationToken);
    if (article == null)
        return NotFound();

    if (!IsOwnerOrAdmin(article))
        return Forbid();

    dto.ApplyTo(article);
    await _articleService.UpdateAsync(article, cancellationToken);

    return NoContent();
}

// DELETE: /api/articlesapi/5
[HttpDelete("{id:int}")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> Delete(int id, CancellationToken cancellationToken)
{
    var article = await _articleService.GetByIdAsync(id, cancellationToken);
    if (article == null)
        return NotFound();

    if (!IsOwnerOrAdmin(article))
        return Forbid();

    await _articleService.DeleteAsync(id, cancellationToken);
    return NoContent();
}

private bool IsOwnerOrAdmin(Article article)
{
    var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
    return article.AuthorId == userId || User.IsInRole("Admin");
}

De ce AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme? Identity (din Lab 7) a setat Cookies ca schema default de autentificare. Un [Authorize] simplu ar cere cookie — dar API-ul trebuie să accepte token JWT în header. Atributul spune explicit „folosește schema JWT Bearer pentru această acțiune". Schema în sine o configurăm în Program.cs la Pasul 7 — până atunci codul compilează, dar dacă apelați POST/PUT/DELETE înainte să terminați Pasul 7, cererea eșuează fiindcă schema încă nu e înregistrată.

Status codes — recapitulare

Cod Metoda helper Când
200 OK Ok(value) GET reușit, cu body
201 Created CreatedAtAction(action, routeValues, value) POST reușit — Location header pointează la resursa nouă
204 No Content NoContent() PUT/DELETE reușit, fără body
400 Bad Request BadRequest(errors) Validare eșuată (automat cu [ApiController])
401 Unauthorized automat — JWT middleware Utilizatorul nu e autentificat
403 Forbidden Forbid() Autentificat, dar fără drept (nu e owner, nu e admin)
404 Not Found NotFound() Resursa nu există

401 vs 403: 401 înseamnă „cine ești?" — trimite token. 403 înseamnă „știu cine ești, dar nu ai voie". Nu le inversați — clienții au logică diferită pentru fiecare.

De ce CreatedAtAction și nu Ok?

CreatedAtAction returnează automat status 201 și adaugă un header Location: /api/articlesapi/42 către resursa nou creată. Clientul știe imediat URL-ul la care poate să o citească. Este pattern RESTful standard pentru POST.