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"
+ }
+ }
+ }
+}