diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..b4a33e98 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +bin/ +obj/ +x64/ +x86/ +[Bb]in/ +[Oo]bj/ +[Oo]ut/ +*.log +*.nupkg diff --git a/Back-endDeveloper.docx.pdf b/Back-endDeveloper.docx.pdf new file mode 100644 index 00000000..6e08163f Binary files /dev/null and b/Back-endDeveloper.docx.pdf differ diff --git a/Controllers/book/BookController.cs b/Controllers/book/BookController.cs new file mode 100644 index 00000000..19c3a54f --- /dev/null +++ b/Controllers/book/BookController.cs @@ -0,0 +1,111 @@ +using api.Model; +using api.Repositories.book; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace api.Controllers.book +{ + [Route("api/[controller]")] + [ApiController] + public class BookController : ControllerBase + { + private readonly IBookRepository _bookRepository; + + public BookController(IBookRepository bookRepository) + { + _bookRepository = bookRepository; + } + + /// + /// Get all books in asceding order. Any user can submit this request. + /// + [HttpGet] + [AllowAnonymous] + public async Task>> GetBooks(int pageNumber = 1, int pageSize = 10, string orderBy = "id", string search = "") + { + var books = await _bookRepository.Get(pageNumber, pageSize, orderBy, search); + + if (books == null || !books.Any()) + return NotFound(); + + return Ok(books); + } + + /// + /// Get book by Id. Any user can submit this request. + /// + /// + [HttpGet("{Id}")] + [AllowAnonymous] + public async Task> GetBookById([FromRoute] int Id) + { + var book = await _bookRepository.Get(Id); + if (book == null) return NotFound(); + + return Ok(book); + } + + /// + /// Create book. Only users with ADMIN and EMPLOYEE privileges can make this request. + /// + /// + /// + [HttpPost] + [Authorize(Roles = "ADMIN,EMPLOYEE")] + public async Task> CreateBook([FromBody] Book book) + { + if (!ModelState.IsValid) + return BadRequest(ModelState); + + var existingBook = await _bookRepository.Get(book.Author, book.Title); + + if (existingBook != null) + { + return BadRequest(new { message = "A book with the same author and title already exists" }); + } + + var createBook = await _bookRepository.Post(book); + return CreatedAtAction(nameof(GetBookById), new { id = createBook.Id }, createBook); + } + + /// + /// Update Book. Only users with ADMIN and EMPLOYEE privileges can make this request. + /// + /// + /// + [HttpPut("{Id}")] + [Authorize(Roles = "ADMIN,EMPLOYEE")] + public async Task UpdateBook([FromRoute] int Id, [FromBody] Book book) + { + if (Id != book.Id) + return BadRequest(StatusCode(400)); + + var existingBook = await _bookRepository.Get(Id); + if (existingBook == null) + return NotFound(); + + await _bookRepository.Put(book); + return NoContent(); + } + + /// + /// Delete Books. Only users with ADMIN privileges can make this request. + /// + /// + [HttpDelete("{Id}")] + [Authorize(Roles = "ADMIN")] + public async Task DeleteBook([FromRoute] int Id) + { + var book = await _bookRepository.Get(Id); + if (book == null) + return NotFound(); + + await _bookRepository.Delete(book.Id); + return NoContent(); + + } + } +} \ No newline at end of file diff --git a/Controllers/category/CategoryController.cs b/Controllers/category/CategoryController.cs new file mode 100644 index 00000000..d35462dd --- /dev/null +++ b/Controllers/category/CategoryController.cs @@ -0,0 +1,36 @@ +using api.Model; +using api.Repositories.category; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using System.Threading.Tasks; + +namespace api.Controllers.category +{ + + [Route("api/[controller]")] + [ApiController] + public class CategoryController : ControllerBase + { + private readonly ICategoryRepository _categoryRepository; + + public CategoryController(ICategoryRepository categoryRepository) + { + _categoryRepository = categoryRepository; + } + + /// + /// Create Category. Only users with ADMIN privileges can make this request. + /// + /// + [HttpPost] + [Authorize(Roles = "ADMIN, EMPLOYEE")] + public async Task> CreateCategory([FromBody] Category category) + { + if (!ModelState.IsValid) + return BadRequest(ModelState); + + var createCategory = await _categoryRepository.Post(category); + return CreatedAtAction(nameof(CreateCategory), new { id = createCategory.Id }, createCategory); + } + } +} diff --git a/Controllers/user/UserController.cs b/Controllers/user/UserController.cs new file mode 100644 index 00000000..d37903cc --- /dev/null +++ b/Controllers/user/UserController.cs @@ -0,0 +1,98 @@ +using api.Model; +using api.Repositories.user; +using api.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using System.Collections; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace api.Controllers.user +{ + [Route("api/")] + [ApiController] + public class UserController : ControllerBase + { + + private readonly IUserRepository _userRepository; + private readonly TokenService _tokenService; + + public UserController(IUserRepository userRepository, TokenService tokenService) + { + _userRepository = userRepository; + _tokenService = tokenService; + } + + /// + /// Register User. Any user can submit this request. + /// + /// + [HttpPost("register")] + [AllowAnonymous] + public async Task Register([FromBody] Users users) + { + var existingUser = await _userRepository.Get(users.Id); + + if (existingUser != null) + { + return BadRequest(new { message = "user already exists" }); + } + + var user = new Users + { + Username = users.Username, + Password = users.Password, + Role = users.Role, + }; + + if (!ModelState.IsValid) + return BadRequest(ModelState); + + if (string.IsNullOrEmpty(users.Username)) + return StatusCode(400, new { message = "Username is null. please, enter a user to create your credentials." }); + + user.Password = ""; + + var token = _tokenService.GenerateToken(user); + + return Ok(new { user, token }); + } + + /// + /// Login. Any user can submit this request. + /// + /// + [HttpPost("login")] + [AllowAnonymous] + public async Task> Login([FromBody] Login login) + { + var user = await _userRepository.Get(login.Username, login.Password); + + if (user == null) + return StatusCode(404, new { message = "User not found!" }); + + if (string.IsNullOrEmpty(user.Username)) + return StatusCode(500, new { message = "Username is null or empty!" }); + + var token = _tokenService.GenerateToken(user); + + user.Password = ""; + + return Ok(new { user, token }); + } + + /// + /// Get all users. Only users with ADMIN privileges can make this request. + /// + [HttpGet("users")] + [Authorize(Roles = "ADMIN")] + public async Task>> GetUsers() + { + var user = await _userRepository.Get(); + if (user == null) + return NotFound(); + + return Ok(user); + } + } +} diff --git a/DoroBooks.json b/DoroBooks.json new file mode 100644 index 00000000..f6dca92d --- /dev/null +++ b/DoroBooks.json @@ -0,0 +1,521 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "DoroBooks API", + "description": "Simple book API with JWT Authentication", + "contact": { + "name": "Yan Lima", + "email": "yanborges125@gmail.com" + }, + "version": "v1" + }, + "paths": { + "/api/Book": { + "get": { + "tags": [ + "Book" + ], + "summary": "Get all books in asceding order. Any user can submit this request.", + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Book" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Book" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Book" + } + } + } + } + } + } + }, + "post": { + "tags": [ + "Book" + ], + "summary": "Create book. Only users with ADMIN and EMPLOYEE privileges can make this request.", + "requestBody": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Book" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/Book" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/Book" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/Book" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/Book" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/Book" + } + } + } + } + } + } + }, + "/api/Book/{Id}": { + "get": { + "tags": [ + "Book" + ], + "summary": "Get book by Id. Any user can submit this request.", + "parameters": [ + { + "name": "Id", + "in": "path", + "description": "", + "required": true, + "schema": { + "type": "integer", + "description": "", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/Book" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/Book" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/Book" + } + } + } + } + } + }, + "put": { + "tags": [ + "Book" + ], + "summary": "Update Book. Only users with ADMIN and EMPLOYEE privileges can make this request.", + "parameters": [ + { + "name": "Id", + "in": "path", + "description": "", + "required": true, + "schema": { + "type": "integer", + "description": "", + "format": "int32" + } + } + ], + "requestBody": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Book" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/Book" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/Book" + } + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } + }, + "delete": { + "tags": [ + "Book" + ], + "summary": "Delete Books. Only users with ADMIN privileges can make this request.", + "parameters": [ + { + "name": "Id", + "in": "path", + "description": "", + "required": true, + "schema": { + "type": "integer", + "description": "", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/api/Category": { + "post": { + "tags": [ + "Category" + ], + "summary": "Create Category. Only users with ADMIN privileges can make this request.", + "requestBody": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Category" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/Category" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/Category" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/Category" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/Category" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/Category" + } + } + } + } + } + } + }, + "/api/User/register": { + "post": { + "tags": [ + "User" + ], + "summary": "Register User. Any user can submit this request.", + "requestBody": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Users" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/Users" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/Users" + } + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/api/User/login": { + "post": { + "tags": [ + "User" + ], + "summary": "Login. Any user can submit this request.", + "requestBody": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Login" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/Login" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/Login" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/Users" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/Users" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/Users" + } + } + } + } + } + } + }, + "/api/User/user": { + "get": { + "tags": [ + "User" + ], + "summary": "Get all users. Only users with ADMIN privileges can make this request.", + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Users" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Users" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Users" + } + } + } + } + } + } + } + }, + "/api/User/create": { + "post": { + "tags": [ + "User" + ], + "summary": "Create users. Only users with ADMIN privileges can make this request", + "requestBody": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Users" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/Users" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/Users" + } + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } + } + } + }, + "components": { + "schemas": { + "Category": { + "required": [ + "name" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + }, + "books": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Book" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "Book": { + "required": [ + "author", + "description", + "title" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "author": { + "type": "string" + }, + "categoryId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "category": { + "$ref": "#/components/schemas/Category" + } + }, + "additionalProperties": false + }, + "Role": { + "enum": [ + 0, + 1 + ], + "type": "integer", + "format": "int32" + }, + "Users": { + "required": [ + "password", + "username" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "username": { + "type": "string" + }, + "password": { + "type": "string" + }, + "role": { + "$ref": "#/components/schemas/Role" + } + }, + "additionalProperties": false + }, + "Login": { + "type": "object", + "properties": { + "username": { + "type": "string", + "nullable": true + }, + "password": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + } + } + } +} \ No newline at end of file diff --git a/Migrations/20230215175055_DoroBooks.Designer.cs b/Migrations/20230215175055_DoroBooks.Designer.cs new file mode 100644 index 00000000..cb774e7e --- /dev/null +++ b/Migrations/20230215175055_DoroBooks.Designer.cs @@ -0,0 +1,108 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using api.Model; + +namespace api.Migrations +{ + [DbContext(typeof(DataContext))] + [Migration("20230215175055_DoroBooks")] + partial class DoroBooks + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Relational:MaxIdentifierLength", 128) + .HasAnnotation("ProductVersion", "5.0.11") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("api.Model.Book", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("Author") + .IsRequired() + .HasColumnType("varchar(100)"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasColumnType("varchar(300)"); + + b.Property("Title") + .IsRequired() + .HasColumnType("varchar(250)"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.ToTable("Books"); + }); + + modelBuilder.Entity("api.Model.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(250)"); + + b.HasKey("Id"); + + b.ToTable("Category"); + }); + + modelBuilder.Entity("api.Model.Users", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("Password") + .IsRequired() + .HasColumnType("varchar(250)"); + + b.Property("Role") + .HasColumnType("nvarchar(max)"); + + b.Property("Username") + .IsRequired() + .HasColumnType("varchar(250)"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("api.Model.Book", b => + { + b.HasOne("api.Model.Category", "Category") + .WithMany("Books") + .HasForeignKey("CategoryId"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("api.Model.Category", b => + { + b.Navigation("Books"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Migrations/20230215175055_DoroBooks.cs b/Migrations/20230215175055_DoroBooks.cs new file mode 100644 index 00000000..347dfdfb --- /dev/null +++ b/Migrations/20230215175055_DoroBooks.cs @@ -0,0 +1,77 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace api.Migrations +{ + public partial class DoroBooks : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Category", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Name = table.Column(type: "varchar(250)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Category", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Users", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Username = table.Column(type: "varchar(250)", nullable: false), + Password = table.Column(type: "varchar(250)", nullable: false), + Role = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Users", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Books", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Title = table.Column(type: "varchar(250)", nullable: false), + Description = table.Column(type: "varchar(300)", nullable: false), + Author = table.Column(type: "varchar(100)", nullable: false), + CategoryId = table.Column(type: "int", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Books", x => x.Id); + table.ForeignKey( + name: "FK_Books_Category_CategoryId", + column: x => x.CategoryId, + principalTable: "Category", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateIndex( + name: "IX_Books_CategoryId", + table: "Books", + column: "CategoryId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Books"); + + migrationBuilder.DropTable( + name: "Users"); + + migrationBuilder.DropTable( + name: "Category"); + } + } +} diff --git a/Migrations/DataContextModelSnapshot.cs b/Migrations/DataContextModelSnapshot.cs new file mode 100644 index 00000000..7244ecab --- /dev/null +++ b/Migrations/DataContextModelSnapshot.cs @@ -0,0 +1,104 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using api.Model; + +namespace api.Migrations +{ + [DbContext(typeof(DataContext))] + partial class DataContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Relational:MaxIdentifierLength", 128) + .HasAnnotation("ProductVersion", "5.0.11") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("api.Model.Book", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("Author") + .IsRequired() + .HasColumnType("varchar(100)"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasColumnType("varchar(300)"); + + b.Property("Title") + .IsRequired() + .HasColumnType("varchar(250)"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.ToTable("Books"); + }); + + modelBuilder.Entity("api.Model.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(250)"); + + b.HasKey("Id"); + + b.ToTable("Category"); + }); + + modelBuilder.Entity("api.Model.Users", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("Password") + .IsRequired() + .HasColumnType("varchar(250)"); + + b.Property("Role") + .HasColumnType("int"); + + b.Property("Username") + .IsRequired() + .HasColumnType("varchar(250)"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("api.Model.Book", b => + { + b.HasOne("api.Model.Category", "Category") + .WithMany("Books") + .HasForeignKey("CategoryId"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("api.Model.Category", b => + { + b.Navigation("Books"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Model/Book.cs b/Model/Book.cs new file mode 100644 index 00000000..11cd2075 --- /dev/null +++ b/Model/Book.cs @@ -0,0 +1,30 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace api.Model +{ + public class Book + { + [Key] + public int Id { get; set; } + + + [Required(ErrorMessage = "{0} is required!")] + [Column(TypeName = "varchar(250)")] + public string Title { get; set; } + + + [Required(ErrorMessage = "{0} is required!")] + [Column(TypeName = "varchar(300)")] + public string Description { get; set; } + + + [Required(ErrorMessage = "{0} is required!")] + [Column(TypeName = "varchar(100)")] + public string Author { get; set; } + + public int? CategoryId { get; set; } + + public Category Category { get; set; } + } +} diff --git a/Model/Category.cs b/Model/Category.cs new file mode 100644 index 00000000..bb449a67 --- /dev/null +++ b/Model/Category.cs @@ -0,0 +1,18 @@ +using Microsoft.EntityFrameworkCore.Metadata.Internal; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace api.Model +{ + public class Category + { + [Key] + public int Id { get; set; } + + [Required(ErrorMessage = "{0} is required!")] + [Column(TypeName = "varchar(250)")] + public string Name { get; set; } + public virtual List Books { get; set; } + } +} diff --git a/Model/Context/DataContext.cs b/Model/Context/DataContext.cs new file mode 100644 index 00000000..2d01bdc4 --- /dev/null +++ b/Model/Context/DataContext.cs @@ -0,0 +1,22 @@ +using api.Model.Map; +using Microsoft.EntityFrameworkCore; + +namespace api.Model +{ + public class DataContext : DbContext + { + public DataContext(DbContextOptions options) : base(options) + { } + + public DbSet Users { get; set; } + public DbSet Books { get; set; } + public DbSet Category { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.ApplyConfiguration(new BookMap()); + + base.OnModelCreating(modelBuilder); + } + } +} diff --git a/Model/Login.cs b/Model/Login.cs new file mode 100644 index 00000000..26e02c69 --- /dev/null +++ b/Model/Login.cs @@ -0,0 +1,8 @@ +namespace api.Model +{ + public class Login + { + public string Username { get; set;} + public string Password { get; set;} + } +} diff --git a/Model/Map/BookMap.cs b/Model/Map/BookMap.cs new file mode 100644 index 00000000..7aa0e38e --- /dev/null +++ b/Model/Map/BookMap.cs @@ -0,0 +1,14 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace api.Model.Map +{ + public class BookMap : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(x => x.Id); + builder.HasOne(x => x.Category); + } + } +} diff --git a/Model/Users.cs b/Model/Users.cs new file mode 100644 index 00000000..9c4032f6 --- /dev/null +++ b/Model/Users.cs @@ -0,0 +1,20 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace api.Model +{ + public class Users + { + [Key] + public int Id { get; set; } + + [Required(ErrorMessage = "{0} is required!")] + [Column(TypeName = "varchar(250)")] + public string Username { get; set; } + + [Required(ErrorMessage = "{0} is required!")] + [Column(TypeName = "varchar(250)")] + public string Password { get; set; } + public string Role { get; set; } + } +} diff --git a/Program.cs b/Program.cs new file mode 100644 index 00000000..ad0f1c42 --- /dev/null +++ b/Program.cs @@ -0,0 +1,22 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace api +{ + public class Program + { +#pragma warning disable CS1591 + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +#pragma warning disable CS1591 +} diff --git a/Properties/launchSettings.json b/Properties/launchSettings.json new file mode 100644 index 00000000..f8b48f2d --- /dev/null +++ b/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:37589", + "sslPort": 44317 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "api": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/README.md b/README.md index b8775d12..472ac336 100644 --- a/README.md +++ b/README.md @@ -1,97 +1,14 @@ ## Desafio para Back-end Developer na DoroTech - C# .NET -#### Requisitos Gerais: +## .gitignore +https://github.com/github/gitignore -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: +Aqui estão alguns arquivos de compilação ignorados: +
[x] Arquivos binários: arquivos gerados durante a compilação, como DLLs e arquivos executáveis;
+
[x] Arquivos intermediários: arquivos temporários gerados durante a compilação, como o arquivo .obj;
+
[x] Arquivos NuGet: pacotes NuGet que são baixados automaticamente durante a compilação e restauração de pacotes.
+## Acesse o link e veja mais informações sobre o projeto (Setup, Modelagem de dados, Tasks e Steps) : -* 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. +https://early-mustard-4f8.notion.site/Challenge-DoroTech-C-302bde63b4cc407398a5121ec657e807 -#### Requisitos Técnicos: - -* 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. - - -#### 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. - -#### 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. - - -## Como deverá ser entregue: - - 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. - - - -## C# Back-end Challenge (English) - -#### 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: - - -* 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. - -#### Technical requirements: - -* 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. - -#### 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. - - -## 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/Repositories/book/BookRepository.cs b/Repositories/book/BookRepository.cs new file mode 100644 index 00000000..612e5c1d --- /dev/null +++ b/Repositories/book/BookRepository.cs @@ -0,0 +1,140 @@ +using api.Model; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using System.Collections.Generic; +using System.Data.Common; +using System.Linq; +using System.Threading.Tasks; + +namespace api.Repositories.book +{ + public class BookRepository : IBookRepository + { + private readonly DataContext _context; + private readonly ILogger _logger; + + public BookRepository(DataContext context, ILogger logger) + { + _context = context; + _logger = logger; + } + + public async Task> Get() + { + return await _context.Books.OrderBy(b => b.Title).ToListAsync(); + } + + public async Task Get(int Id) + { + return await _context.Books.FindAsync(Id); + } + + public async Task Post(Book book) + { + if (string.IsNullOrWhiteSpace(book.Title)) + { + _logger.LogWarning("a book with empty title was not created"); + return null; + } + + _logger.LogInformation($"creating book with name{book.Title}"); + _context.Books.Add(book); + + try + { + await _context.SaveChangesAsync(); + _logger.LogInformation($"a book {book.Title} is created successfully. :)"); + } + catch (DbException ex) + { + _logger.LogError(ex, "error creating book."); + return null; + } + + return book; + } + + public async Task Put(Book book) + { + if (string.IsNullOrWhiteSpace(book.Title)) + { + _logger.LogWarning("a book with empty title was not updated"); + return; + } + + _logger.LogInformation($"updating book with name{book.Title}"); + _context.Entry(book).State = EntityState.Modified; + + try + { + await _context.SaveChangesAsync(); + _logger.LogInformation($"book with name{book.Title} updated"); + } + catch (DbException ex) + { + _logger.LogInformation(ex, $"erro updated book {book.Title}"); + return; + } + } + public async Task Delete(int Id) + { + var bookId = await _context.Books.FindAsync(Id); + + if (bookId == null) + { + _logger.LogWarning($"book not found"); + return; + } + + _logger.LogInformation($"deleting book..."); + _context.Books.Remove(bookId); + + try + { + await _context.SaveChangesAsync(); + _logger.LogInformation($"book removed"); + } + catch (DbException ex) + { + _logger.LogError(ex, $"error with remove book"); + return; + } + } + + public async Task> Get(int pageNumber, int pageSize, string orderBy, string search) + { + var books = _context.Books.AsQueryable(); + + // Filtrar pelo campo search + if (!string.IsNullOrEmpty(search)) + { + books = books.Where(b => b.Title.ToLower().Contains(search.ToLower())); + } + + // Ordenar pelo campo orderBy + switch (orderBy?.ToLower()) + { + case "title": + books = books.OrderBy(b => b.Title); + break; + case "author": + books = books.OrderBy(b => b.Author); + break; + default: + books = books.OrderBy(b => b.Id); + break; + } + + // Paginação + var skip = (pageNumber - 1) * pageSize; + books = books.Skip(skip).Take(pageSize); + + return await books.ToListAsync(); + } + + public async Task Get(string author, string title) + { + return await _context.Books.SingleOrDefaultAsync(u => u.Author == author && u.Title == title); + } + } +} diff --git a/Repositories/book/IBookRepository.cs b/Repositories/book/IBookRepository.cs new file mode 100644 index 00000000..d8ab0c3e --- /dev/null +++ b/Repositories/book/IBookRepository.cs @@ -0,0 +1,17 @@ +using api.Model; +using System.Collections; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace api.Repositories.book +{ + public interface IBookRepository + { + Task> Get(int pageNumber, int pageSize, string orderBy, string search); + Task Get(int Id); + Task Get(string author, string title); + Task Post(Book book); + Task Put(Book book); + Task Delete(int Id); + } +} diff --git a/Repositories/category/CategoryRepository.cs b/Repositories/category/CategoryRepository.cs new file mode 100644 index 00000000..58a27196 --- /dev/null +++ b/Repositories/category/CategoryRepository.cs @@ -0,0 +1,44 @@ +using api.Model; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace api.Repositories.category +{ + public class CategoryRepository : ICategoryRepository + { + private readonly DataContext _context; + private readonly ILogger _logger; + + public CategoryRepository(DataContext context, ILogger logger) + { + _context = context; + _logger = logger; + } + + public async Task Post(Category category) + { + if (string.IsNullOrWhiteSpace(category.Name)) + { + _logger.LogWarning("a category with empty name was not created."); + return null; + } + + _logger.LogInformation($"creating category with name {category.Name}"); + _context.Category.Add(category); + + try + { + await _context.SaveChangesAsync(); + _logger.LogInformation($"category {category.Name} created successfuly. :)"); + } + catch (DbUpdateException ex) + { + _logger.LogError(ex, $"error creating category with name{category.Name}"); + throw; + } + return category; + } + } +} diff --git a/Repositories/category/ICategoryRepository.cs b/Repositories/category/ICategoryRepository.cs new file mode 100644 index 00000000..95db723d --- /dev/null +++ b/Repositories/category/ICategoryRepository.cs @@ -0,0 +1,12 @@ +using api.Model; +using System.Collections; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace api.Repositories.category +{ + public interface ICategoryRepository + { + Task Post(Category category); + } +} diff --git a/Repositories/user/IUserRepository.cs b/Repositories/user/IUserRepository.cs new file mode 100644 index 00000000..bb4e450f --- /dev/null +++ b/Repositories/user/IUserRepository.cs @@ -0,0 +1,14 @@ +using api.Model; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace api.Repositories.user +{ + public interface IUserRepository + { + Task Get(string username, string password); + Task Get(int Id); + Task> Get(); + Task Post(Users users); + } +} diff --git a/Repositories/user/UserRepository.cs b/Repositories/user/UserRepository.cs new file mode 100644 index 00000000..7053edd0 --- /dev/null +++ b/Repositories/user/UserRepository.cs @@ -0,0 +1,100 @@ +using api.Model; +using Microsoft.EntityFrameworkCore; +using System.Text; +using System.Threading.Tasks; +using System.Security.Cryptography; +using Microsoft.Extensions.Logging; +using System.Collections.Generic; +using System.Linq; + +namespace api.Repositories.user +{ + public class UserRepository : IUserRepository + { + private readonly DataContext _context; + private readonly ILogger _logger; + + + public UserRepository(DataContext context, ILogger logger) + { + _context = context; + _logger = logger; + } + public static string HashPassword(string password) + { + using (SHA256 sha256Hash = SHA256.Create()) + { + // Converte a senha em um array de bytes. + byte[] bytes = sha256Hash.ComputeHash(Encoding.UTF8.GetBytes(password)); + + // Converte o array de bytes em uma string hexadecimal. + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < bytes.Length; i++) + { + builder.Append(bytes[i].ToString("x2")); + } + return builder.ToString(); + } + } + public async Task Get(string username, string password) + { + _logger.LogInformation($"Getting user: {username}"); + + var hashedPassword = HashPassword(password); + + var user = await _context.Users.SingleOrDefaultAsync(u => u.Username == username && u.Password == hashedPassword); + + return new Users + { + Username = username, + Password = password, + Role = user.Role + }; + } + + public async Task> Get() + { + return await _context.Users.ToListAsync(); + } + + public async Task Get(int Id) + { + return await _context.Users.FindAsync(Id); + } + + public async Task Post(Users users) + { + _logger.LogInformation($"Create User: {users.Username}"); + + if (string.IsNullOrWhiteSpace(users.Username) || + string.IsNullOrWhiteSpace(users.Password)) + { + _logger.LogWarning("credentials is not inserted!"); + return null; + } + + var user = new Users + { + Username = users.Username, + Password = HashPassword(users.Password), + Role = users.Role + }; + + _logger.LogInformation("creating user"); + _context.Users.Add(user); + + try + { + _logger.LogInformation("user created successfully!"); + await _context.SaveChangesAsync(); + } + catch (DbUpdateException ex) + { + _logger.LogError(ex, $"Error register user: {users.Username}"); + return null; + } + + return user; + } + } +} diff --git a/Services/TokenService.cs b/Services/TokenService.cs new file mode 100644 index 00000000..a0b9dcd1 --- /dev/null +++ b/Services/TokenService.cs @@ -0,0 +1,41 @@ +using api.Model; +using Microsoft.IdentityModel.Tokens; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using System; +using Microsoft.Extensions.Configuration; +using api.Repositories.user; + +namespace api.Services +{ + public class TokenService + { + private readonly IConfiguration _configuration; + + public TokenService(IConfiguration configuration) + { + _configuration = configuration; + } + + public string GenerateToken(Users users) + { + var tokenHandler = new JwtSecurityTokenHandler(); + var key = Encoding.ASCII.GetBytes(_configuration.GetSection("JWT_SECRET").Value); + var tokenDescriptor = new SecurityTokenDescriptor + { + Subject = new ClaimsIdentity(new[] + { + new Claim(ClaimTypes.Name, users.Username), + new Claim(ClaimTypes.Role, users.Role), + }), + + Expires = DateTime.UtcNow.AddHours(2), + SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature) + }; + + var token = tokenHandler.CreateToken(tokenDescriptor); + return tokenHandler.WriteToken(token); + } + } +} diff --git a/Startup.cs b/Startup.cs new file mode 100644 index 00000000..02628aaf --- /dev/null +++ b/Startup.cs @@ -0,0 +1,142 @@ +using api.Model; +using api.Repositories.book; +using api.Repositories.category; +using api.Repositories.user; +using api.Services; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.IdentityModel.Tokens; +using Microsoft.OpenApi.Models; +using System; +using System.IO; +using System.Reflection; +using System.Text; + +namespace api +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddCors(options => + { + options.AddDefaultPolicy(builder => + { + builder.AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader(); + }); + }); + + services.AddSwaggerGen(c => + { + c.SwaggerDoc("v1", new OpenApiInfo + { + Version = "v1", + Title = "DoroBooks API", + Description = "Simple book API with JWT Authentication", + Contact = new OpenApiContact + { + Name = Configuration.GetSection("CONTACT_NAME").Value, + Email = Configuration.GetSection("CONTACT_EMAIL").Value + } + }); + + c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme() + { + Name = "Authorization", + Type = SecuritySchemeType.ApiKey, + Scheme = "Bearer", + BearerFormat = "JWT", + In = ParameterLocation.Header, + Description = "JWT Authorization header using the Bearer scheme" + }); + + c.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "Bearer" + } + }, + new string[] {} + } + }); + + // Set the comments path for the Swagger JSON and UI. + var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; + var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); + c.IncludeXmlComments(xmlPath); + }); + + services.AddLogging(builder => builder.AddConsole()); // Adicionando logger e um dos provedores de logger + services.AddLogging(); + services.AddDbContext(options => options.UseSqlServer(Configuration.GetConnectionString("devConn"))); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddControllers(); + + var key = Encoding.ASCII.GetBytes(Configuration["JWT_SECRET"]); + services.AddAuthentication(x => + { + x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }).AddJwtBearer(x => + { + x.RequireHttpsMetadata = false; + x.SaveToken = true; + x.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(key), + ValidateIssuer = false, + ValidateAudience = false, + }; + }); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app.UseCors(); + + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + app.UseSwagger(); + app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "api v1")); + } + + app.UseHttpsRedirection(); + app.UseRouting(); + + app.UseAuthentication(); + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + } + } +} diff --git a/api.csproj b/api.csproj new file mode 100644 index 00000000..ba2b7c75 --- /dev/null +++ b/api.csproj @@ -0,0 +1,23 @@ + + + + true + $(NoWarn);1591 + net5.0 + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + diff --git a/appsettings.Development.json b/appsettings.Development.json new file mode 100644 index 00000000..8983e0fc --- /dev/null +++ b/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/appsettings.json b/appsettings.json new file mode 100644 index 00000000..c6a23d0e --- /dev/null +++ b/appsettings.json @@ -0,0 +1,20 @@ +{ + "AllowedHosts": "*", + "ConnectionStrings": { + "devConn": "Data Source=localhost;Initial Catalog=DoroBooks;Integrated Security=True" + }, + "JWT_SECRET": "aGcB6CzZsWx8VuY2", + "CONTACT_NAME": "Yan Lima", + "CONTACT_EMAIL": "yanborges125@gmail.com", + "Logging": { + "LogLevel": { + "Default": "Information" + }, + "Console": { + "IncludeScopes": true, + "LogLevel": { + "Microsoft": "Warning" + } + } + } +}