add: autenticacao e autorizacao
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -234,6 +234,7 @@ PublishScripts/
|
|||||||
# NuGet v3's project.json files produces more ignorable files
|
# NuGet v3's project.json files produces more ignorable files
|
||||||
*.nuget.props
|
*.nuget.props
|
||||||
*.nuget.targets
|
*.nuget.targets
|
||||||
|
nuget.config
|
||||||
|
|
||||||
# Microsoft Azure Build Output
|
# Microsoft Azure Build Output
|
||||||
csx/
|
csx/
|
||||||
|
|||||||
57
Controllers/AuthController.cs
Normal file
57
Controllers/AuthController.cs
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace CampusWorkshops.Api.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
public class AuthController : ControllerBase
|
||||||
|
{
|
||||||
|
// Usuários de exemplo (NÃO use plaintext em produção)
|
||||||
|
private static readonly Dictionary<string,(string Password, string[] Roles)> Users = new()
|
||||||
|
{
|
||||||
|
["admin@campus"] = ("admin123", new[] { "Admin", "Instructor" }),
|
||||||
|
["inst@campus"] = ("inst123", new[] { "Instructor" }),
|
||||||
|
["aluno@campus"] = ("aluno123", Array.Empty<string>())
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly IConfiguration _cfg;
|
||||||
|
public AuthController(IConfiguration cfg) => _cfg = cfg;
|
||||||
|
|
||||||
|
public record LoginRequest(string Username, string Password);
|
||||||
|
public record TokenResponse(string AccessToken, DateTime ExpiresAt);
|
||||||
|
|
||||||
|
[HttpPost("login")]
|
||||||
|
[ProducesResponseType(typeof(TokenResponse), 200)]
|
||||||
|
[ProducesResponseType(401)]
|
||||||
|
public IActionResult Login([FromBody] LoginRequest body)
|
||||||
|
{
|
||||||
|
if (!Users.TryGetValue(body.Username, out var u) || u.Password != body.Password)
|
||||||
|
return Unauthorized();
|
||||||
|
|
||||||
|
var jwt = _cfg.GetSection("Jwt");
|
||||||
|
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwt["Key"]!));
|
||||||
|
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
|
||||||
|
|
||||||
|
var claims = new List<Claim>
|
||||||
|
{
|
||||||
|
new(JwtRegisteredClaimNames.Sub, body.Username),
|
||||||
|
new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
|
||||||
|
};
|
||||||
|
claims.AddRange(u.Roles.Select(r => new Claim(ClaimTypes.Role, r)));
|
||||||
|
|
||||||
|
var expires = DateTime.UtcNow.AddHours(2);
|
||||||
|
var token = new JwtSecurityToken(
|
||||||
|
issuer: jwt["Issuer"],
|
||||||
|
audience: jwt["Audience"],
|
||||||
|
claims: claims,
|
||||||
|
expires: expires,
|
||||||
|
signingCredentials: creds
|
||||||
|
);
|
||||||
|
var encoded = new JwtSecurityTokenHandler().WriteToken(token);
|
||||||
|
return Ok(new TokenResponse(encoded, expires));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
using CampusWorkshops.Api.Dtos;
|
using CampusWorkshops.Api.Dtos;
|
||||||
using CampusWorkshops.Api.Models;
|
using CampusWorkshops.Api.Models;
|
||||||
using CampusWorkshops.Api.Repositories;
|
using CampusWorkshops.Api.Repositories;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace CampusWorkshops.Api.Controllers;
|
namespace CampusWorkshops.Api.Controllers;
|
||||||
@@ -16,6 +17,7 @@ public class WorkshopsController : ControllerBase
|
|||||||
|
|
||||||
/// <summary>Lista workshops com filtros opcionais.</summary>
|
/// <summary>Lista workshops com filtros opcionais.</summary>
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
|
[Authorize]
|
||||||
public async Task<IActionResult> GetAll([FromQuery] DateTimeOffset? from, [FromQuery] DateTimeOffset? to, [FromQuery] string? q, CancellationToken ct)
|
public async Task<IActionResult> GetAll([FromQuery] DateTimeOffset? from, [FromQuery] DateTimeOffset? to, [FromQuery] string? q, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var workshops = await _repo.GetAllAsync(from, to, q, ct);
|
var workshops = await _repo.GetAllAsync(from, to, q, ct);
|
||||||
@@ -36,6 +38,7 @@ public class WorkshopsController : ControllerBase
|
|||||||
|
|
||||||
/// <summary>Obtém um workshop por Id.</summary>
|
/// <summary>Obtém um workshop por Id.</summary>
|
||||||
[HttpGet("{id:guid}")]
|
[HttpGet("{id:guid}")]
|
||||||
|
[Authorize]
|
||||||
public async Task<IActionResult> GetById(Guid id, CancellationToken ct)
|
public async Task<IActionResult> GetById(Guid id, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var workshop = await _repo.GetByIdAsync(id, ct);
|
var workshop = await _repo.GetByIdAsync(id, ct);
|
||||||
@@ -61,6 +64,7 @@ public class WorkshopsController : ControllerBase
|
|||||||
|
|
||||||
/// <summary>Cria um novo workshop.</summary>
|
/// <summary>Cria um novo workshop.</summary>
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
|
[Authorize(Policy = "CanWriteWorkshops")]
|
||||||
public async Task<IActionResult> Create([FromBody] CreateWorkshopRequest body, CancellationToken ct)
|
public async Task<IActionResult> Create([FromBody] CreateWorkshopRequest body, CancellationToken ct)
|
||||||
{
|
{
|
||||||
if (!ModelState.IsValid)
|
if (!ModelState.IsValid)
|
||||||
@@ -119,6 +123,8 @@ public class WorkshopsController : ControllerBase
|
|||||||
|
|
||||||
/// <summary>Atualiza parcialmente um workshop existente.</summary>
|
/// <summary>Atualiza parcialmente um workshop existente.</summary>
|
||||||
[HttpPatch("{id:guid}")]
|
[HttpPatch("{id:guid}")]
|
||||||
|
[Authorize(Policy = "CanWriteWorkshops")]
|
||||||
|
|
||||||
public async Task<IActionResult> Patch(Guid id, [FromBody] PatchWorkshopRequest body, CancellationToken ct)
|
public async Task<IActionResult> Patch(Guid id, [FromBody] PatchWorkshopRequest body, CancellationToken ct)
|
||||||
{
|
{
|
||||||
if (!ModelState.IsValid)
|
if (!ModelState.IsValid)
|
||||||
@@ -199,6 +205,8 @@ public class WorkshopsController : ControllerBase
|
|||||||
|
|
||||||
/// <summary>Remove um workshop.</summary>
|
/// <summary>Remove um workshop.</summary>
|
||||||
[HttpDelete("{id:guid}")]
|
[HttpDelete("{id:guid}")]
|
||||||
|
[Authorize(Policy = "CanDeleteWorkshops")]
|
||||||
|
|
||||||
public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
|
public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var workshop = await _repo.GetByIdAsync(id, ct);
|
var workshop = await _repo.GetByIdAsync(id, ct);
|
||||||
|
|||||||
67
Program.cs
67
Program.cs
@@ -4,9 +4,42 @@ using Microsoft.AspNetCore.Mvc;
|
|||||||
using CampusWorkshops.Api.Repositories;
|
using CampusWorkshops.Api.Repositories;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using CampusWorkshops.Api.Infrastructure.Data;
|
using CampusWorkshops.Api.Infrastructure.Data;
|
||||||
|
using System.Text;
|
||||||
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
using Microsoft.OpenApi.Models;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
// JWT
|
||||||
|
var jwt = builder.Configuration.GetSection("Jwt");
|
||||||
|
var keyBytes = Encoding.UTF8.GetBytes(jwt["Key"]!);
|
||||||
|
|
||||||
|
builder.Services
|
||||||
|
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||||
|
.AddJwtBearer(options =>
|
||||||
|
{
|
||||||
|
options.TokenValidationParameters = new()
|
||||||
|
{
|
||||||
|
ValidateIssuer = true,
|
||||||
|
ValidateAudience = true,
|
||||||
|
ValidateIssuerSigningKey = true,
|
||||||
|
ValidIssuer = jwt["Issuer"],
|
||||||
|
ValidAudience = jwt["Audience"],
|
||||||
|
IssuerSigningKey = new SymmetricSecurityKey(keyBytes),
|
||||||
|
ClockSkew = TimeSpan.FromMinutes(1) // previsível para testes
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.AddAuthorization();
|
||||||
|
|
||||||
|
builder.Services.AddAuthorization(options =>
|
||||||
|
{
|
||||||
|
options.AddPolicy("CanWriteWorkshops", p => p.RequireRole("Instructor","Admin"));
|
||||||
|
options.AddPolicy("CanDeleteWorkshops", p => p.RequireRole("Admin"));
|
||||||
|
options.AddPolicy("CanViewAnalytics", p => p.RequireRole("Admin"));
|
||||||
|
});
|
||||||
|
|
||||||
// Add services
|
// Add services
|
||||||
builder.Services.AddDbContext<WorkshopsDbContext>(options =>
|
builder.Services.AddDbContext<WorkshopsDbContext>(options =>
|
||||||
options.UseSqlite(builder.Configuration.GetConnectionString("WorkshopsDb")));
|
options.UseSqlite(builder.Configuration.GetConnectionString("WorkshopsDb")));
|
||||||
@@ -22,6 +55,37 @@ builder.Services.AddSwaggerGen(o =>
|
|||||||
Version = "v1",
|
Version = "v1",
|
||||||
Description = "API para gestão de workshops do campus (MVP in-memory)."
|
Description = "API para gestão de workshops do campus (MVP in-memory)."
|
||||||
});
|
});
|
||||||
|
var bearerScheme = new OpenApiSecurityScheme
|
||||||
|
{
|
||||||
|
Name = "Authorization",
|
||||||
|
Description = "Cole apenas o JWT (sem 'Bearer ').",
|
||||||
|
In = ParameterLocation.Header,
|
||||||
|
Type = SecuritySchemeType.Http, // <- IMPORTANTE
|
||||||
|
Scheme = "bearer", // <- minúsculo
|
||||||
|
BearerFormat = "JWT",
|
||||||
|
Reference = new OpenApiReference // <- garante que o requirement aponte para esta definição
|
||||||
|
{
|
||||||
|
Type = ReferenceType.SecurityScheme,
|
||||||
|
Id = "Bearer"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
o.AddSecurityDefinition("Bearer", bearerScheme);
|
||||||
|
|
||||||
|
o.AddSecurityRequirement(new OpenApiSecurityRequirement
|
||||||
|
{
|
||||||
|
{
|
||||||
|
new OpenApiSecurityScheme
|
||||||
|
{
|
||||||
|
Reference = new OpenApiReference
|
||||||
|
{
|
||||||
|
Type = ReferenceType.SecurityScheme,
|
||||||
|
Id = "Bearer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Array.Empty<string>()
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// DI
|
// DI
|
||||||
@@ -51,6 +115,9 @@ app.UseExceptionHandler(errApp =>
|
|||||||
|
|
||||||
app.UseHttpsRedirection();
|
app.UseHttpsRedirection();
|
||||||
|
|
||||||
|
app.UseAuthentication(); // <-- antes
|
||||||
|
app.UseAuthorization(); // <-- depois
|
||||||
|
|
||||||
app.UseSwagger();
|
app.UseSwagger();
|
||||||
app.UseSwaggerUI(c =>
|
app.UseSwaggerUI(c =>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.9" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.2" />
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.2" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.8" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.8" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.8">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.8">
|
||||||
|
|||||||
@@ -5,5 +5,10 @@
|
|||||||
"Microsoft.AspNetCore": "Warning"
|
"Microsoft.AspNetCore": "Warning"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Jwt": {
|
||||||
|
"Issuer": "CampusWorkshops",
|
||||||
|
"Audience": "CampusWorkshops.Api",
|
||||||
|
"Key": "some-key"
|
||||||
|
},
|
||||||
"AllowedHosts": "*"
|
"AllowedHosts": "*"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user