diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..a2cd6ab0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +# Build e pastas temporárias +bin/ +obj/ +out/ + +# Configurações da IDE Rider +.idea/ +*.sln.iml + +# Configurações do Visual Studio Code +.vscode/ + +# Arquivos de log e temporários +/Logs +*.log +*.tmp +*.temp + +# Configurações do sistema +.DS_Store +Thumbs.db + +# Arquivos de backup do Visual Studio +*.user +*.suo + +# Arquivos de pacotes NuGet +*.nupkg +*.snupkg +packages/ + +# Publicação e publicação local +publish/ + +# Configurações locais (NÃO subir) +appsettings.Development.json \ No newline at end of file diff --git a/Business/Entities/Livro.cs b/Business/Entities/Livro.cs new file mode 100644 index 00000000..7dbc5b73 --- /dev/null +++ b/Business/Entities/Livro.cs @@ -0,0 +1,12 @@ +namespace dorotech_csharp_test.Business; + +public class Livro +{ + public Guid Id { get; set; } = Guid.NewGuid(); + public string Nome { get; set; } + public string? Descricao { get; set; } + public decimal Preco { get; set; } + public int Estoque { get; set; } + public DateTime DataCadastro { get; set; } = DateTime.UtcNow; + +} \ No newline at end of file diff --git a/Business/Interfaces/Repositories/ILivroRepository.cs b/Business/Interfaces/Repositories/ILivroRepository.cs new file mode 100644 index 00000000..ca42213d --- /dev/null +++ b/Business/Interfaces/Repositories/ILivroRepository.cs @@ -0,0 +1,11 @@ +namespace dorotech_csharp_test.Business.Interfaces.Repositories; + +public interface ILivroRepository +{ + Task> ObeterTodosLivros(); + Task ExisteLivroComNome(string livroNome); + Task Adicionar(Livro livro); + Task ObterLivroPeloId(Guid id); + Task Delete(Guid id); + Task Update(Guid id, Livro livroAtualizado); +} \ No newline at end of file diff --git a/Business/Services/LivroService.cs b/Business/Services/LivroService.cs new file mode 100644 index 00000000..c3efaf91 --- /dev/null +++ b/Business/Services/LivroService.cs @@ -0,0 +1,76 @@ +using dorotech_csharp_test.Business.Interfaces.Repositories; + +namespace dorotech_csharp_test.Business.Services; + +public class LivroService +{ + private readonly ILivroRepository _livroRepository; + private readonly ILogger _logger; + + public LivroService(ILivroRepository livroRepository, ILogger logger) + { + _livroRepository = livroRepository; + _logger = logger; + } + + public async Task CreateLivro(Livro livro) + { + _logger.LogInformation("Inicio do Cadastro : {Nome}", livro.Nome); + if (await _livroRepository.ExisteLivroComNome(livro.Nome)) + { + _logger.LogWarning("Livro já cadastrado: {Nome}", livro.Nome); + throw new InvalidOperationException("Livro já Cadastrado"); + } + + await _livroRepository.Adicionar(livro); + _logger.LogInformation( + "Livro cadastrado com sucesso. Id: {Id}", livro.Id); + } + + + public async Task> ObterTodoslivros( int page, int pageSize, string? nome) + { + _logger.LogInformation("Consulta de todos os livros"); + return (await _livroRepository.ObeterTodosLivros()) + .OrderBy(l => l.Nome) + .Skip((page - 1) * pageSize) + .Take(pageSize); + } + + + public async Task ObterLivroPeloId(Guid id) + { + _logger.LogInformation("Consulta de Livro {Id}", id); + return await _livroRepository.ObterLivroPeloId(id); + } + + + public async Task Delete(Guid id) + { + _logger.LogInformation("Tentando excluir livro {Id}", id); + var deleted = await _livroRepository.Delete(id); + if (!deleted) + { + _logger.LogWarning( + "Falha ao excluir. Livro não encontrado {Id}", id); + throw new KeyNotFoundException("livro nao encontrado"); + } + _logger.LogInformation( + "Livro excluído com sucesso {Id}", id); + } + + public async Task Upadate(Guid id, Livro livroAtualizado) + { + _logger.LogInformation("Tentando atualizar livro {Id}", id); + var livro = await _livroRepository.Update(id, livroAtualizado); + if (livro == null) + { + _logger.LogWarning( + "Livro não encontrado para atualização {Id}", id); + throw new KeyNotFoundException("Livro Não encontrado"); + } + _logger.LogInformation( + "Livro atualizado com sucesso {Id}", id); + return livro; + } +} \ No newline at end of file diff --git a/Business/Services/TokenService.cs b/Business/Services/TokenService.cs new file mode 100644 index 00000000..21a1113e --- /dev/null +++ b/Business/Services/TokenService.cs @@ -0,0 +1,31 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using Microsoft.IdentityModel.Tokens; + +namespace dorotech_csharp_test.Business.Services; + +public class TokenService +{ + public string GenerateToken(string role) + { + var claims = new[] + { + new Claim(ClaimTypes.Role, role) + }; + + var key = new SymmetricSecurityKey( + Encoding.UTF8.GetBytes("minha-chave-super-secreta-com-32-caracteres") + ); + + var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); + + var token = new JwtSecurityToken( + claims: claims, + expires: DateTime.Now.AddHours(2), + signingCredentials: creds + ); + + return new JwtSecurityTokenHandler().WriteToken(token); + } +} \ No newline at end of file diff --git a/Data/Context/DorotechContext.cs b/Data/Context/DorotechContext.cs new file mode 100644 index 00000000..3718156a --- /dev/null +++ b/Data/Context/DorotechContext.cs @@ -0,0 +1,12 @@ +using dorotech_csharp_test.Business; +using Microsoft.EntityFrameworkCore; + +namespace dorotech_csharp_test.Data.Context; + +public class DorotechContext : DbContext +{ + public DorotechContext(DbContextOptions options) : base(options) + { + } + public DbSet Livros { get; set; } +} \ No newline at end of file diff --git a/Data/Repositories/LivroRepository.cs b/Data/Repositories/LivroRepository.cs new file mode 100644 index 00000000..04436d23 --- /dev/null +++ b/Data/Repositories/LivroRepository.cs @@ -0,0 +1,67 @@ +using dorotech_csharp_test.Business; +using dorotech_csharp_test.Business.Interfaces.Repositories; +using dorotech_csharp_test.Data.Context; +using Microsoft.EntityFrameworkCore; + +namespace dorotech_csharp_test.Data.Repositories; + +public class LivroRepository : ILivroRepository +{ + private readonly DorotechContext _context; + + public LivroRepository(DorotechContext context) + { + _context = context; + } + + public async Task> ObeterTodosLivros() + { + return await _context.Livros + .OrderBy(l => l.Nome) + .ToListAsync(); + } + + public Task ExisteLivroComNome(string livroNome) + { + return _context.Livros.AnyAsync(l => l.Nome == livroNome); + } + + public async Task Adicionar(Livro livro) + { + await _context.Livros.AddAsync(livro); + await _context.SaveChangesAsync(); + } + + public async Task ObterLivroPeloId(Guid id) + { + return await _context.Livros.FindAsync(id); + } + + public async Task Delete(Guid id) + { + var livro = await _context.Livros.FindAsync(id); + if (livro == null) + return false; + + _context.Livros.Remove(livro); //remove o livro + await _context.SaveChangesAsync(); //Salva no Banco + return true; + } + + public async Task Update(Guid id, Livro livroAtualizado) + { + var livro = await _context.Livros.FindAsync(id); + if (livro == null) + return null; + + livro.Nome = livroAtualizado.Nome; + livro.Descricao = livroAtualizado.Descricao; + livro.Estoque = livroAtualizado.Estoque; + livro.Preco = livroAtualizado.Preco; + + _context.Livros.Update(livro); + await _context.SaveChangesAsync(); + + return livro; + } +} diff --git a/Data/Seed/SeedData.cs b/Data/Seed/SeedData.cs new file mode 100644 index 00000000..f7e20cd7 --- /dev/null +++ b/Data/Seed/SeedData.cs @@ -0,0 +1,40 @@ +using dorotech_csharp_test.Business; +using dorotech_csharp_test.Data.Context; + +namespace dorotech_csharp_test.Data.Seed; + +public static class SeedData +{ + public static void Initialize(DorotechContext context) + { + // Checa se já existem livros no banco + if (!context.Livros.Any()) + { + context.Livros.AddRange( + new Livro + { + Nome = "Garen", + Descricao = "Livro teste", + Preco = 50, + Estoque = 10 + }, + new Livro + { + Nome = "C#", + Descricao = "Livro de programação", + Preco = 80, + Estoque = 5 + }, + new Livro + { + Nome = "Matematica", + Descricao = "Livro de Matematica", + Preco = 70, + Estoque = 7 + } + ); + + context.SaveChanges(); // Salva no banco + } + } +} \ No newline at end of file diff --git a/DorotechApi/Auth/AuthController.cs b/DorotechApi/Auth/AuthController.cs new file mode 100644 index 00000000..77a87b0c --- /dev/null +++ b/DorotechApi/Auth/AuthController.cs @@ -0,0 +1,29 @@ +using dorotech_csharp_test.Business.Services; +using Microsoft.AspNetCore.Mvc; + +namespace dorotech_csharp_test.DorotechApi.Auth; + +[ApiController] +[Route("api/auth")] +public class AuthController : ControllerBase +{ + private readonly TokenService _tokenService; + + public AuthController(TokenService tokenService) + { + _tokenService = tokenService; + } + + [HttpPost("login")] + public IActionResult Login(LoginDto dto) + { + + if (dto.Usuario == "admin" && dto.Senha == "123") + { + var token = _tokenService.GenerateToken("Admin"); + return Ok(new { token }); + } + + return Unauthorized(); + } +} \ No newline at end of file diff --git a/DorotechApi/Auth/LoginDto.cs b/DorotechApi/Auth/LoginDto.cs new file mode 100644 index 00000000..78fdb8e6 --- /dev/null +++ b/DorotechApi/Auth/LoginDto.cs @@ -0,0 +1,7 @@ +namespace dorotech_csharp_test.DorotechApi.Auth; + +public class LoginDto +{ + public string Usuario { get; set; } + public string Senha { get; set; } +} \ No newline at end of file diff --git a/DorotechApi/Controllers/LivroController.cs b/DorotechApi/Controllers/LivroController.cs new file mode 100644 index 00000000..0df32cce --- /dev/null +++ b/DorotechApi/Controllers/LivroController.cs @@ -0,0 +1,177 @@ +using dorotech_csharp_test.Business; +using dorotech_csharp_test.Business.Services; +using dorotech_csharp_test.DorotechApi.Dtos; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace dorotech_csharp_test.DorotechApi.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class LivroController : ControllerBase +{ + private readonly LivroService _livroService; + + public LivroController(LivroService livroService) + { + _livroService = livroService; + } + + /// + /// Cria um livro. + /// + /// Dados do livro a ser criado. + /// O livro criado com o ID gerado. + /// Livro criado com sucesso. + /// Livro já existe. + + [HttpPost] + [Authorize(Roles = "Admin")] + public async Task> PostLivro([FromBody] LivroCreateDto dto) + { + var livro = new Livro + { + Nome = dto.Nome, + Descricao = dto.Descricao, + Preco = dto.Preco, + Estoque = dto.Estoque + }; + + try + { + await _livroService.CreateLivro(livro); + } + catch (InvalidOperationException ex) + { + return Conflict(ex.Message); // erro 409 + } + + var livroRead = new LivroReadDto + { + Id = livro.Id, + Nome = livro.Nome, + Descricao = livro.Descricao, + Preco = livro.Preco, + Estoque = livro.Estoque, + DataCadastro = livro.DataCadastro + }; + + return CreatedAtAction(nameof(GetLivroById), new { id = livroRead.Id }, livroRead); + } + + /// + /// Retorna todos os livros com paginação e filtro opcional por nome. + /// + /// Número da página (padrão: 1). + /// Tamanho da página (padrão: 10). + /// Filtro opcional pelo nome do livro. + /// Lista de livros. + [HttpGet] + [AllowAnonymous] // permite acesso publico + public async Task>> GetLivros( + [FromQuery] int page = 1, + [FromQuery] int pageSize = 10, + [FromQuery] string? nome = null) + { + var livros = await _livroService.ObterTodoslivros(page, pageSize, nome); + var result = livros.Select(livro => new LivroReadDto + { + Id = livro.Id, + Nome = livro.Nome, + Descricao = livro.Descricao, + Preco = livro.Preco, + Estoque = livro.Estoque, + DataCadastro = livro.DataCadastro + }).ToList(); + + return Ok (result); + } + + /// + /// Retorna um livro pelo ID. + /// + /// ID do livro. + /// Livro correspondente ao ID informado. + /// Livro encontrado. + /// ID inválido. + /// Livro não encontrado. + [HttpGet("{id}")] + [AllowAnonymous] + public async Task> GetLivroById(Guid id) + { + if (id == Guid.Empty) + return BadRequest("Id Invalido"); + + var livro = await _livroService.ObterLivroPeloId(id); + + if (livro == null) + { + return NotFound("Livro nao encontrado"); + } + + var livroRead = new LivroReadDto + { + Id = livro.Id, + Nome = livro.Nome, + Descricao = livro.Descricao, + Preco = livro.Preco, + Estoque = livro.Estoque, + DataCadastro = livro.DataCadastro + }; + + return Ok(livroRead); + } + + /// + /// Remove um livro pelo ID. + /// + /// ID do livro a ser removido. + /// Livro removido com sucesso. + /// ID inválido. + /// Livro não encontrado. + [HttpDelete("{id}")] + [Authorize(Roles = "Admin")] + public async Task DeleteLivro(Guid id) + { + if (id == Guid.Empty) + return BadRequest("ID invalido"); + + try + { + await _livroService.Delete(id); + return NoContent(); //204 sucesso + } + catch (KeyNotFoundException e) + { + return NotFound(e.Message); // 404 nao encontrado + } + } + + /// + /// Atualiza os dados de um livro. + /// + /// ID do livro a ser atualizado. + /// Dados atualizados do livro. + /// Atualização realizada com sucesso. + /// ID inválido. + /// Livro não encontrado. + [HttpPut("{id}")] + [Authorize(Roles = "Admin")] + public async Task UpdateLivro(Guid id, [FromBody] Livro dto) + { + if (id == Guid.Empty) + { + return BadRequest("ID invalido"); + } + try + { + var livro = await _livroService.Upadate(id, dto); + return NoContent(); //204 A operação foi realizada com sucesso, + // mas não há conteúdo para retornar.” + } + catch (Exception e) + { + return NotFound(e.Message); + } + } +} \ No newline at end of file diff --git a/DorotechApi/Dtos/LivroCreateDto.cs b/DorotechApi/Dtos/LivroCreateDto.cs new file mode 100644 index 00000000..219347d5 --- /dev/null +++ b/DorotechApi/Dtos/LivroCreateDto.cs @@ -0,0 +1,19 @@ +using System.ComponentModel.DataAnnotations; + +namespace dorotech_csharp_test.DorotechApi.Dtos; + +public class LivroCreateDto +{ + [Required(ErrorMessage = "O nome do livro é obrigatório.")] + [MaxLength(50)] + public string Nome { get; set; } + + [MaxLength(500)] + public string? Descricao { get; set; } + + [Range(0.01, double.MaxValue, ErrorMessage = "Preço inválido")] + public decimal Preco { get; set; } + + [Range(0, int.MaxValue, ErrorMessage = "Estoque inválido")] + public int Estoque { get; set; } +} \ No newline at end of file diff --git a/DorotechApi/Dtos/LivroReadDto.cs b/DorotechApi/Dtos/LivroReadDto.cs new file mode 100644 index 00000000..0f6a8597 --- /dev/null +++ b/DorotechApi/Dtos/LivroReadDto.cs @@ -0,0 +1,11 @@ +namespace dorotech_csharp_test.DorotechApi.Dtos; + +public class LivroReadDto +{ + public Guid Id { get; set; } + public string Nome { get; set; } + public string? Descricao { get; set; } + public decimal Preco { get; set; } + public float Estoque { get; set; } + public DateTime DataCadastro { get; set; } +} \ No newline at end of file diff --git a/DorotechApi/Dtos/LivroUpdateDto.cs b/DorotechApi/Dtos/LivroUpdateDto.cs new file mode 100644 index 00000000..d753ddf2 --- /dev/null +++ b/DorotechApi/Dtos/LivroUpdateDto.cs @@ -0,0 +1,9 @@ +namespace dorotech_csharp_test.DorotechApi.Dtos; + +public class LivroUpdateDto +{ + public string Nome { get; set; } + public string Descricao { get; set; } + public decimal Preco { get; set; } + public int Estoque { get; set; } +} \ No newline at end of file diff --git a/Migrations/20260129043356_InicialCreate.Designer.cs b/Migrations/20260129043356_InicialCreate.Designer.cs new file mode 100644 index 00000000..821c6b30 --- /dev/null +++ b/Migrations/20260129043356_InicialCreate.Designer.cs @@ -0,0 +1,54 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using dorotech_csharp_test.Data.Context; + +#nullable disable + +namespace dorotech_csharp_test.Migrations +{ + [DbContext(typeof(DorotechContext))] + [Migration("20260129043356_InicialCreate")] + partial class InicialCreate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("dorotech_csharp_test.Business.Livro", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("DataCadastro") + .HasColumnType("datetime(6)"); + + b.Property("Descricao") + .HasColumnType("longtext"); + + b.Property("Estoque") + .HasColumnType("int"); + + b.Property("Nome") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Preco") + .HasColumnType("decimal(65,30)"); + + b.HasKey("Id"); + + b.ToTable("Livros"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Migrations/20260129043356_InicialCreate.cs b/Migrations/20260129043356_InicialCreate.cs new file mode 100644 index 00000000..0709f987 --- /dev/null +++ b/Migrations/20260129043356_InicialCreate.cs @@ -0,0 +1,44 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace dorotech_csharp_test.Migrations +{ + /// + public partial class InicialCreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterDatabase() + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "Livros", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + Nome = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Descricao = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + Preco = table.Column(type: "decimal(65,30)", nullable: false), + Estoque = table.Column(type: "int", nullable: false), + DataCadastro = table.Column(type: "datetime(6)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Livros", x => x.Id); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Livros"); + } + } +} diff --git a/Migrations/DorotechContextModelSnapshot.cs b/Migrations/DorotechContextModelSnapshot.cs new file mode 100644 index 00000000..c80c0853 --- /dev/null +++ b/Migrations/DorotechContextModelSnapshot.cs @@ -0,0 +1,51 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using dorotech_csharp_test.Data.Context; + +#nullable disable + +namespace dorotech_csharp_test.Migrations +{ + [DbContext(typeof(DorotechContext))] + partial class DorotechContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("dorotech_csharp_test.Business.Livro", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("DataCadastro") + .HasColumnType("datetime(6)"); + + b.Property("Descricao") + .HasColumnType("longtext"); + + b.Property("Estoque") + .HasColumnType("int"); + + b.Property("Nome") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Preco") + .HasColumnType("decimal(65,30)"); + + b.HasKey("Id"); + + b.ToTable("Livros"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Program.cs b/Program.cs new file mode 100644 index 00000000..4a98ad91 --- /dev/null +++ b/Program.cs @@ -0,0 +1,120 @@ +using System.Reflection; +using System.Text; +using dorotech_csharp_test.Business.Interfaces.Repositories; +using dorotech_csharp_test.Business.Services; +using dorotech_csharp_test.Data.Context; +using dorotech_csharp_test.Data.Repositories; +using dorotech_csharp_test.Data.Seed; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.EntityFrameworkCore; +using Microsoft.IdentityModel.Tokens; +using Microsoft.OpenApi.Models; +using Serilog; + +Log.Logger = new LoggerConfiguration() + .WriteTo.Console() + .WriteTo.File( + "Logs/dorotech-log-.txt", + rollingInterval: RollingInterval.Day) + .CreateLogger(); + + +var builder = WebApplication.CreateBuilder(args); + +// Configurar MySQL +builder.Services.AddDbContext(options => + options.UseMySql( + builder.Configuration.GetConnectionString("DefaultConnection"), + new MySqlServerVersion(new Version(8, 0, 33)) + ) +); + +// Configurar Repositórios e Serviços +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +// Swagger +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(c => +{ + c.SwaggerDoc("v1", new() { Title = "DoroTech API", Version = "v1" }); + + // Lê os comentários XML + var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; + var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); + c.IncludeXmlComments(xmlPath); + + c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme + { + Name = "Authorization", + Type = SecuritySchemeType.Http, + Scheme = "bearer", + BearerFormat = "JWT", + In = ParameterLocation.Header, + Description = "Digite: Bearer {seu token}" + }); + + c.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "Bearer" + } + }, + Array.Empty() + } + }); +}); + +// Controllers +builder.Services.AddControllers(); + +//Authentication +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(options => + { + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = false, + ValidateAudience = false, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey( + Encoding.UTF8.GetBytes("minha-chave-super-secreta-com-32-caracteres") + ) + }; + }); +builder.Services.AddAuthorization(); +builder.Services.AddScoped(); +builder.Host.UseSerilog(); + +var app = builder.Build(); + +// Middleware +app.UseHttpsRedirection(); +app.UseAuthentication(); +app.UseAuthorization(); // se tiver autenticação + +// Swagger +app.UseSwagger(); +app.UseSwaggerUI(c => +{ + c.SwaggerEndpoint("/swagger/v1/swagger.json", "DoroTech API V1"); + c.RoutePrefix = string.Empty; // abre direto em http://localhost:5166/ +}); + +app.MapControllers(); + + +// Popula o banco de dados com uma massa inicial de livros para facilitar testes e validação da API +using (var scope = app.Services.CreateScope()) +{ + var context = scope.ServiceProvider.GetRequiredService(); + SeedData.Initialize(context); +} + +app.Run(); \ No newline at end of file diff --git a/Properties/launchSettings.json b/Properties/launchSettings.json new file mode 100644 index 00000000..4d8b6f0f --- /dev/null +++ b/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:3251", + "sslPort": 44366 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5166", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7228;http://localhost:5166", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/README.md b/README.md index b8775d12..9da0dce9 100644 --- a/README.md +++ b/README.md @@ -1,97 +1,217 @@ -## Desafio para Back-end Developer na DoroTech - C# .NET +# 📚 DoroTech – API de Gerenciamento de Livros -#### Requisitos Gerais: +API REST desenvolvida em **C# com ASP.NET Core (.NET 8)** como solução para o **Desafio Back-end Developer da DoroTech**. -Uma livraria da cidade teve um aumento no número de seus exemplares e está com um problema para identificar todos os livros que possui em estoque. -Para ajudar a livraria foi solicitado a você desenvolver uma aplicação web para gerenciar estes exemplares. Requisitos: +O projeto tem como objetivo auxiliar uma livraria no gerenciamento de seus livros em estoque, permitindo **consulta, cadastro, edição e exclusão**, com persistência em banco de dados, autenticação JWT e documentação via Swagger. +--- -* O sistema deverá mostrar todos os livros cadastrados ordenados de forma ascendente pelo nome. -* Ao persistir, validar se o livro já foi cadastrado. -* O sistema deverá permitir consultar, criar, editar e excluir um livro. -* Os livros devem ser persistidos em um banco de dados. -* Criar algum mecanismo de log de registro e de erro. +## 🛠️ Tecnologias Utilizadas -#### Requisitos Técnicos: +- **C#** +- **ASP.NET Core Web API (.NET 8)** +- **Entity Framework Core** +- **MySQL** +- **JWT (JSON Web Token)** +- **Swagger / OpenAPI** +- **Serilog (logs de registro e erro)** +- **Injeção de Dependência** +- **Padrões Repository e Service** +- **Princípios SOLID e Clean Code** -* Configurar o Swagger na aplicação (fundamental), pois será usado para testes. -* Incluir mecanismo de autenticação no Swagger, usando Token JWT (Bearer). -* Para a persistência dos dados deve ser utilizado o Entity Framework. -* Como banco de dados, pode ser usado MySQL, PostgreSQL ou SQL Server. -* Utilizar migrations ou Gerar Scripts e disponibilizá-los um uma pasta. -* Incluir git.ignore no repositório para não subir arquivos de compilação. +--- +## 🎯 Enunciado do Desafio -#### Observações: -* O sistema deverá ser desenvolvido na plataforma .NET com C#. - (preferêncialmente 5.0+, caso for usado outra versão, informar no pull-request) -* Deve conter autenticação com dois níveis de acesso, um administrador e um público, o usuário de nível - público não terá autenticação, ou seja, terá acesso livre a consulta de livros -* Atenção aos princípio do SOLID. -* Não é necessária a criação de front-end, o teste será feito pelo Swagger UI. +Uma livraria da cidade teve um aumento no número de seus exemplares e está com dificuldades para identificar todos os livros em estoque. +Foi solicitado o desenvolvimento de uma aplicação web para gerenciar esses livros. -#### Diferenciais do desafio: -* Aplicação das boas práticas do DDD, TDD, Design Patterns, SOLID e Clean Code. -* A modelagem dos dados não será fornecida, de propósito. Desejamos avaliar a sua capacidade de abstração. -* A API deverá realizar tratamento de entrada de dados e retornar códigos de erro quando aplicáveis. -* Criar massa de dados para que seja possível verificar o funcionamento das lógicas propostas. -* Incluir parâmetros de paginação e campos de filtro nos métodos de consulta (GET). -* Documentar, via código-fonte, os campos, parâmetros e dados de retorno da API para exibição no Swagger. +### Requisitos Funcionais +- Listar todos os livros cadastrados ordenados de forma ascendente pelo nome +- Validar se o livro já está cadastrado ao persistir +- Permitir consultar, criar, editar e excluir livros +- Persistir os dados em banco de dados +- Criar mecanismo de log de registros e erros +--- -## Como deverá ser entregue: +## ✅ Atendimento aos Requisitos - 1. Faça um fork deste repositório; - 2. Realize o teste; - 3. Adicione seu currículo na raiz do repositório; - 4. Envie-nos o PULL-REQUEST para que seja avaliado. +### Requisitos Gerais +| Requisito | Implementação | +|---------|---------------| +| Listagem ordenada por nome | ✅ Implementado | +| Validação de duplicidade | ✅ Implementado | +| CRUD completo | ✅ Implementado | +| Persistência em banco de dados | ✅ Implementado | +| Logs de registro e erro | ✅ Serilog | +### Requisitos Técnicos -## C# Back-end Challenge (English) +| Requisito | Implementação | +|---------|---------------| +| Swagger configurado | ✅ Swagger + XML Comments | +| Autenticação JWT (Bearer) | ✅ Implementado | +| Entity Framework Core | ✅ Utilizado | +| Banco de dados | ✅ MySQL | +| Migrations / Scripts | ✅ EF Core | +| `.gitignore` | ✅ Incluído | -#### General requirements: +--- -A bookstore in town has had an increase in the number of its copies and is having a problem identifying all the books it has in stock. -To help the bookstore, you were asked to develop a web application to manage these copies. Requirements: +## 🔐 Autenticação e Autorização +A API utiliza **JWT (Bearer Token)** para autenticação. -* The system should show all registered books sorted in ascending order by name. -* When persisting, validate if the book has already been registered. -* The system should allow consulting, creating, editing and deleting books. -* Books must be persisted in a database. -* Create some logging and error logging mechanism. +### Níveis de acesso +- **Administrador (Admin)** + - Criar livros + - Atualizar livros + - Excluir livros +- **Usuário Público** + - Listar livros + - Buscar livro por ID + - Não requer autenticação -#### Technical requirements: +No Swagger UI, utilize o botão **Authorize** e informe: +Bearer {seu_token_jwt} -* Configure Swagger in the application (fundamental), as it will be used for testing. -* Include authentication mechanism in Swagger, using JWT Token (Bearer). -* For data persistence, Entity Framework must be used. -* As a database, MySQL, PostgreSQL or SQL Server can be used. -* Use migrations or Generate Scripts and make them available in a folder. -* Include git.ignore in the repository to avoid uploading deployment files. +--- -#### Comments: -* The system must be developed on the .NET platform with C#. -(preferably 5.0+, if another version is used, inform the pull-request) -* Must contain authentication with two levels of access, an administrator and a public, user level -public will not have authentication, that is, it will have free access to consult books -* Attention to the principles of SOLID. -* No front-end creation required, testing will be done by Swagger UI. +## 🏗️ Arquitetura da Aplicação -#### Challenge differentials: -* Application of DDD, TDD, Design Patterns, SOLID and Clean Code best practices. -* Data modeling will not be provided on purpose. We wish to assess your capacity for abstraction. -* The API must perform data entry handling and return error codes when applicable. -* Create mass of data so that it is possible to verify the functioning of the proposed logics. -* Include pagination parameters and filter fields in query (GET) methods. -* Document, via source code, the API fields, parameters and return data for display in Swagger. +A aplicação foi desenvolvida seguindo princípios de **SOLID**, **Clean Code** e separação de responsabilidades. + +ontroller +↓ +Service (Regras de Negócio) +↓ +Repository (Acesso a Dados) +↓ +Entity Framework Core (DbContext) +↓ +Banco de Dados MySQL + + + +### Estrutura em Camadas +- **Controller**: recebe requisições HTTP e retorna respostas apropriadas +- **Service**: contém regras de negócio e validações +- **Repository**: responsável pelo acesso ao banco de dados +- **DTOs**: controlam entrada e saída de dados +- **Seed**: criação de massa inicial de dados + +--- + +## 📦 Entidade Principal + +### Livro + +| Campo | Tipo | +|------|------| +| Id | Guid | +| Nome | string | +| Descricao | string | +| Preco | decimal | +| Estoque | int | +| DataCadastro | DateTime | + +--- + +## 🚀 Endpoints da API + +### ➕ Criar Livro +`POST /api/livro` +🔒 **Admin** + +- Valida se já existe livro com o mesmo nome +- Retorna `409 Conflict` em caso de duplicidade + +--- + +### 📄 Listar Livros +`GET /api/livro` + +- Ordenação ascendente por nome +- Paginação (`page`, `pageSize`) +- Filtro opcional por nome + +--- + +### 🔍 Buscar Livro por ID +`GET /api/livro/{id}` + +- Retorna `400` para ID inválido +- Retorna `404` caso não encontrado + +--- + +### ✏️ Atualizar Livro +`PUT /api/livro/{id}` +🔒 **Admin** + +- Atualiza apenas se o livro existir +- Retorna `204 No Content` + +--- + +### 🗑️ Remover Livro +`DELETE /api/livro/{id}` +🔒 **Admin** + +- Remove o livro do banco de dados +- Retorna `404` caso não encontrado + +--- + +## 🧪 Massa de Dados (Seed) + +Ao iniciar a aplicação, o banco de dados é populado automaticamente com dados iniciais para facilitar testes e validação da API. + +```csharp +SeedData.Initialize(context); + + +⚙️ Configuração do Banco de Dados +No arquivo appsettings.json: + +{ + "ConnectionStrings": { + "DefaultConnection": "server=localhost;database=dorotech_db;user=root;password=senha" + } +} +Banco utilizado: MySQL 8 + + +▶️ Como Executar o Projeto +dotnet restore +dotnet run + +Acesse o Swagger em: +http://localhost:5166 + +🧠 Boas Práticas Aplicadas + +Separação clara de responsabilidades + +Uso de interfaces para desacoplamento + +Regras de negócio centralizadas na camada de serviço + +Tratamento adequado de erros HTTP + +Logs estruturados de informação e erro + +Código preparado para manutenção e evolução + + +👨‍💻 Autor + +Gabriel Carvalho +Back-end Developer +C# | .NET | APIs REST | SQL -## How it should be delivered: - 1. Fork this repository; - 2. Carry out the test; - 3. Add your CV to the repository root; - 4. Send us the PULL-REQUEST to be evaluated. diff --git a/appsettings.Development.json b/appsettings.Development.json new file mode 100644 index 00000000..66dda659 --- /dev/null +++ b/appsettings.Development.json @@ -0,0 +1,29 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + + + "ConnectionStrings": { + "DefaultConnection": "Server=localhost;Database=dorotech_books;User=root;Password=@senha123;" + }, + + + "Jwt": { + "Key": "12345", + "Issuer": "MinhaApi", + "Audience": "MinhaApi" + } + + + + +} + + + + diff --git a/appsettings.json b/appsettings.json new file mode 100644 index 00000000..bf3517d5 --- /dev/null +++ b/appsettings.json @@ -0,0 +1,29 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + + + "ConnectionStrings": { + "DefaultConnection": "Server=localhost;Database=dorotech_books;User=root;Password=SUA_SENHA;" + }, + + + "Jwt": { + "Key": "sua-chave-jtw", + "Issuer": "MinhaApi", + "Audience": "MinhaApi" + } + + + + +} + + + + diff --git a/dorotech-csharp-test.csproj b/dorotech-csharp-test.csproj new file mode 100644 index 00000000..52607a97 --- /dev/null +++ b/dorotech-csharp-test.csproj @@ -0,0 +1,26 @@ + + + + net8.0 + enable + enable + dorotech_csharp_test + true + $(NoWarn);1591 + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/dorotech-csharp-test.http b/dorotech-csharp-test.http new file mode 100644 index 00000000..2d0135d0 --- /dev/null +++ b/dorotech-csharp-test.http @@ -0,0 +1,6 @@ +@dorotech_csharp_test_HostAddress = http://localhost:5166 + +GET {{dorotech_csharp_test_HostAddress}}/weatherforecast/ +Accept: application/json + +###